Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion extensions/publisher-command-center/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from http import client
import asyncio
from fastapi import FastAPI, Header
from fastapi import FastAPI, Header, Body
from fastapi.staticfiles import StaticFiles
from posit import connect
from posit.connect.errors import ClientError
Expand Down Expand Up @@ -61,6 +61,29 @@ async def content(
visitor = get_visitor_client(posit_connect_user_session_token)
return visitor.content.get(content_id)

@app.patch("/api/content/{content_id}/lock")
async def lock_content(
content_id: str,
posit_connect_user_session_token: str = Header(None),
):
visitor = get_visitor_client(posit_connect_user_session_token)
content = visitor.content.get(content_id)
is_locked = content.locked

content.update(locked=not is_locked)
return content

@app.patch("/api/content/{content_id}/rename")
async def rename_content(
content_id: str,
title: str = Body(..., embed = True),
posit_connect_user_session_token: str = Header(None),
):
visitor = get_visitor_client(posit_connect_user_session_token)
content = visitor.content.get(content_id)
Comment on lines +82 to +83
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought with both of these was "do we already have the content data?". If we do we could avoid an API call here.

It looks like there isn't a good way, given only a GUID, to call content.update(title = title) from the SDK though, perhaps that is something we could request to make things like this a bit easier so this could be faster.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is!

We can do:

    visitor = get_visitor_client(posit_connect_user_session_token)
    context = connect.Context(visitor)
    content = connect.ContentItem(context, content_id)

    content.update(title = title)

with the additional imports:

from posit.connect.context import Context
from posit.connect.content import ContentItem

We can make a Context for the ContentItem and pass the guid. Since ContentItem only needs a Context and guid and all other attributes are optional this is enough to do the rename.

The lock has is_locked = content.locked which relies on the Content's locked attribute. If we can pass that in the FastAPI /api/content/{content_id}/lock API (or create two one for unlock and one for lock) then we can do the same there speeding it up by 2 times.


I don't think any of this is necessary in this PR to be fair. If it is very easy we can get it in otherwise we can follow-up since it would be good deal faster.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that looks good @dotNomad! For what it's worth, I wouldn't worry about the additional API call to retrieve the content in order to update it. It's all happening across the local network, so the overhead is fairly negligible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, I wouldn't worry about the additional API call to retrieve the content in order to update it. It's all happening across the local network, so the overhead is fairly negligible.

Oh that is a good point - it will be much faster than local dev 👍


content.update(title = title)
return content

@app.get("/api/contents/{content_id}/processes")
async def get_content_processes(
Expand Down
4 changes: 4 additions & 0 deletions extensions/publisher-command-center/scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ $colors: (
color: $blue;
}

.lock-loading {
color: $gray;
}

// Removes the white background of the modal
.modal.show {
background: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import m from "mithril";
import { format } from "date-fns";
import Contents from "../models/Contents";
import Languages from "./Languages";
import LockContentButton from "./LockContentButton";
import DeleteModal from "./DeleteModal";
import RenameModal from "./RenameModal";

const ContentsComponent = {
error: null,
Expand Down Expand Up @@ -43,6 +45,8 @@ const ContentsComponent = {
m("th", { scope: "col" }, "Date Added"),
m("th", { scope: "col" }, ""),
m("th", { scope: "col" }, ""),
m("th", { scope: "col" }, ""),
m("th", { scope: "col" }, ""),
]),
),
m(
Expand Down Expand Up @@ -72,6 +76,28 @@ const ContentsComponent = {
"td",
m("button", {
class: "action-btn",
ariaLabel: `Rename ${title}`,
title: `Rename ${title}`,
"data-bs-toggle": "modal",
"data-bs-target": `#renameModal-${guid}`,
}, [
m("i", { class: "fa-solid fa-pencil" })
]),
),
m(
"td",
m(LockContentButton, {
contentId: guid,
contentTitle: title,
isLocked: content["locked"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sweet we have isLocked for the component, we can use that in Contents.lock and send it up to the API (or have two endpoints)

}),
),
m(
"td",
m("button", {
class: "action-btn",
title: `Delete ${title}`,
ariaLabel: `Delete ${title}`,
"data-bs-toggle": "modal",
"data-bs-target": `#deleteModal-${guid}`,
}, [
Expand All @@ -83,6 +109,8 @@ const ContentsComponent = {
m("a", {
class: "fa-solid fa-arrow-up-right-from-square",
href: content["content_url"],
ariaLabel: `Open ${title} (opens in new tab)`,
title: `Open ${title}`,
target: "_blank",
onclick: (e) => e.stopPropagation(),
}),
Expand All @@ -91,6 +119,10 @@ const ContentsComponent = {
contentId: guid,
contentTitle: title,
}),
m(RenameModal, {
contentId: guid,
contentTitle: title,
}),
],
);
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import m from "mithril";
import Contents from "../models/Contents";

const LockedContentButton = {
oninit: function () {
this.isLoading = false;
},

view: function(vnode) {
const labelMessage = vnode.attrs.isLocked ?
`Unlock ${vnode.attrs.contentTitle}` :
`Lock Content ${vnode.attrs.contentTitle}`;

const iconClassName = () => {
if (this.isLoading) return "fa-spinner fa-spin lock-loading";

if (vnode.attrs.isLocked) {
return "fa-lock";
} else {
return "fa-lock-open";
}
};

return m("button", {
class: "action-btn",
ariaLabel: labelMessage,
title: labelMessage,
disabled: this.isLoading,
onclick: async () => {
if (this.isLoading) { return; }

this.isLoading = true;
m.redraw();

try {
await Contents.lock(vnode.attrs.contentId)
} finally {
this.isLoading = false;
m.redraw();
}
}
}, [
m("i", {
class: `fa-solid ${iconClassName()}`,

}
)
])
}
};

export default LockedContentButton;
93 changes: 93 additions & 0 deletions extensions/publisher-command-center/src/components/RenameModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import m from "mithril";
import Contents from "../models/Contents";
import { Modal } from "bootstrap";

const RenameModalForm = {
newName: "",
guid: "",
isValid: false,
onsubmit: function(e) {
if (RenameModalForm.isValid) {
e.preventDefault();
Contents.rename(RenameModalForm.guid, RenameModalForm.newName);

const modalEl = document.getElementById(`renameModal-${RenameModalForm.guid}`);
const modal = Modal.getInstance(modalEl);
modal.hide();

RenameModalForm.newName = "";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

}
},
view: function(vnode) {
return m("form", {
onsubmit: (e) => { this.onsubmit(e); }
},
[
m("section", { class: "modal-body" }, [
m("div", { class: "form-group" }, [
m("label", {
for: "rename-content-input",
class: "mb-3",
}, "Enter new name for ", [
m("span", { class: "fw-bold" }, `${vnode.attrs.contentTitle}`)
]),
m("input", {
oninput: function(e) {
RenameModalForm.isValid = e.target.validity.valid;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that the value from the input has a minimum of 3 characters and a max of 1024. This is based off of a warning a user will receive on the Info Pane of the Settings Panel.

image

RenameModalForm.guid = vnode.attrs.contentId;
RenameModalForm.newName = e.target.value;
},
id: "rename-content-input",
type: "text",
class: "form-control",
required: true,
minlength: 3,
maxlength: 1024,
value: RenameModalForm.newName,
}),
])
]),
m("div", { class: "modal-footer" }, [
m(
"button",
{
type: "submit",
class: "btn btn-primary",
ariaLabel: "Rename Content",
},
"Rename Content",
),
]),
])
},
}

const RenameModal = {
view: function(vnode) {
return m("div", {
class: "modal",
id: `renameModal-${vnode.attrs.contentId}`,
tabindex: "-1",
ariaHidden: true
}, [
m("div", { class: "modal-dialog modal-dialog-centered" }, [
m("div", { class: "modal-content" }, [
m("div", { class: "modal-header"}, [
m("h1", { class: "modal-title fs-6" }, "Rename Content"),
m("button", {
class: "btn-close",
ariaLabel: "Close modal",
"data-bs-dismiss": "modal"
}),
]),
m(RenameModalForm, {
contentTitle: vnode.attrs.contentTitle,
contentId: vnode.attrs.contentId,
})
]),
]),
])
},
};

export default RenameModal;
2 changes: 1 addition & 1 deletion extensions/publisher-command-center/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import m from "mithril";

import "bootstrap/dist/js/bootstrap.bundle.min.js";
import * as bootstrap from "bootstrap";
import "@fortawesome/fontawesome-free/css/all.min.css";

import "../scss/index.scss";
Expand Down
23 changes: 23 additions & 0 deletions extensions/publisher-command-center/src/models/Contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ export default {
this.data = this.data.filter((c) => c.guid !== guid);
},

lock: async function (guid) {
await m.request({
method: "PATCH",
url: `api/content/${guid}/lock`,
}).then((response) => {
const targetContent = this.data.find((c) => c.guid === guid);
Object.assign(targetContent, response);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Mihtril.JS doesn't have reactivity like Vue or React, we need to change the content of the data in order to reflect the new changes to the UI via the automatic m.redraw(). Without this, we would need a page reload to refresh the UI.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call adjusting the data so we don't need the slower API call.

});
},

rename: async function (guid, newName) {
await m.request({
method: "PATCH",
url: `api/content/${guid}/rename`,
body: {
title: newName,
},
}).then((response) => {
const targetContent = this.data.find((c) => c.guid === guid);
Object.assign(targetContent, response);
});
},

reset: function () {
this.data = null;
this._fetch = null;
Expand Down