From 22e9353cc636954e06dfa1a3806f618acdacefa0 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Tue, 20 May 2025 16:36:39 -0500 Subject: [PATCH 1/6] Add lock and rename endpoint, lock button, and functionality --- extensions/publisher-command-center/app.py | 23 +++++++++++++++++++ .../src/components/ContentsComponent.js | 9 ++++++++ .../src/components/LockContentButton.js | 21 +++++++++++++++++ .../src/models/Contents.js | 20 ++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 extensions/publisher-command-center/src/components/LockContentButton.js diff --git a/extensions/publisher-command-center/app.py b/extensions/publisher-command-center/app.py index d78c05a7..60b510b0 100644 --- a/extensions/publisher-command-center/app.py +++ b/extensions/publisher-command-center/app.py @@ -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, + new_name: str = "", + posit_connect_user_session_token: str = Header(None), +): + visitor = get_visitor_client(posit_connect_user_session_token) + content = visitor.content.get(content_id) + + content.update(title = new_name) + return content @app.get("/api/contents/{content_id}/processes") async def get_content_processes( diff --git a/extensions/publisher-command-center/src/components/ContentsComponent.js b/extensions/publisher-command-center/src/components/ContentsComponent.js index d803b8d1..d14e51f6 100644 --- a/extensions/publisher-command-center/src/components/ContentsComponent.js +++ b/extensions/publisher-command-center/src/components/ContentsComponent.js @@ -3,6 +3,7 @@ import { format } from "date-fns"; import Contents from "../models/Contents"; import Languages from "./Languages"; import DeleteModal from "./DeleteModal"; +import LockContentButton from "./LockContentButton"; const ContentsComponent = { error: null, @@ -68,10 +69,18 @@ const ContentsComponent = { m("td", content?.active_jobs?.length), m("td", format(content["last_deployed_time"], "MMM do, yyyy")), m("td", format(content["created_time"], "MMM do, yyyy")), + m( + "td", + m(LockContentButton, { + contentId: guid, + isLocked: content["locked"], + }), + ), m( "td", m("button", { class: "action-btn", + ariaLabel: "Delete Content", "data-bs-toggle": "modal", "data-bs-target": `#deleteModal-${guid}`, }, [ diff --git a/extensions/publisher-command-center/src/components/LockContentButton.js b/extensions/publisher-command-center/src/components/LockContentButton.js new file mode 100644 index 00000000..24015c5e --- /dev/null +++ b/extensions/publisher-command-center/src/components/LockContentButton.js @@ -0,0 +1,21 @@ +import m from "mithril"; +import Contents from "../models/Contents"; + +const LockedContentButton = { + view: function(vnode) { + const labelMessage = vnode.attrs.isLocked ? "Unlock Content" : "Lock Content"; + const iconClassName = vnode.attrs.isLocked ? "fa-lock" : "fa-lock-open"; + return m("button", { + class: "action-btn", + ariaLabel: labelMessage, + title: labelMessage, + onclick: () => { + Contents.lock(vnode.attrs.contentId) + } + }, [ + m("i", { class: `fa-solid ${iconClassName}` }) + ]) + } +}; + +export default LockedContentButton; diff --git a/extensions/publisher-command-center/src/models/Contents.js b/extensions/publisher-command-center/src/models/Contents.js index 64c3fb6d..a0d8fabf 100644 --- a/extensions/publisher-command-center/src/models/Contents.js +++ b/extensions/publisher-command-center/src/models/Contents.js @@ -34,6 +34,26 @@ 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); + }); + }, + + rename: async function (guid, newName) { + await m.request({ + method: "PATCH", + url: `api/content/${guid}/rename`, + body: { + title: newName, + }, + }); + }, + reset: function () { this.data = null; this._fetch = null; From 0cc11ccbe6f21d5a0950160d1a9a4ebfb5e41e87 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Wed, 21 May 2025 14:28:47 -0500 Subject: [PATCH 2/6] Add rename API endpoint, rename modal, and update tooltip wording --- extensions/publisher-command-center/app.py | 6 +- .../src/components/ContentsComponent.js | 26 +++++- .../src/components/LockContentButton.js | 4 +- .../src/components/RenameModal.js | 89 +++++++++++++++++++ .../src/models/Contents.js | 3 + 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 extensions/publisher-command-center/src/components/RenameModal.js diff --git a/extensions/publisher-command-center/app.py b/extensions/publisher-command-center/app.py index 60b510b0..cfd2cd2f 100644 --- a/extensions/publisher-command-center/app.py +++ b/extensions/publisher-command-center/app.py @@ -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 @@ -76,13 +76,13 @@ async def lock_content( @app.patch("/api/content/{content_id}/rename") async def rename_content( content_id: str, - new_name: 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) - content.update(title = new_name) + content.update(title = title) return content @app.get("/api/contents/{content_id}/processes") diff --git a/extensions/publisher-command-center/src/components/ContentsComponent.js b/extensions/publisher-command-center/src/components/ContentsComponent.js index d14e51f6..3a244561 100644 --- a/extensions/publisher-command-center/src/components/ContentsComponent.js +++ b/extensions/publisher-command-center/src/components/ContentsComponent.js @@ -2,8 +2,9 @@ import m from "mithril"; import { format } from "date-fns"; import Contents from "../models/Contents"; import Languages from "./Languages"; -import DeleteModal from "./DeleteModal"; import LockContentButton from "./LockContentButton"; +import DeleteModal from "./DeleteModal"; +import RenameModal from "./RenameModal"; const ContentsComponent = { error: null, @@ -44,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( @@ -69,10 +72,23 @@ const ContentsComponent = { m("td", content?.active_jobs?.length), m("td", format(content["last_deployed_time"], "MMM do, yyyy")), m("td", format(content["created_time"], "MMM do, yyyy")), + m( + "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"], }), ), @@ -80,7 +96,8 @@ const ContentsComponent = { "td", m("button", { class: "action-btn", - ariaLabel: "Delete Content", + title: `Delete ${title}`, + ariaLabel: `Delete ${title}`, "data-bs-toggle": "modal", "data-bs-target": `#deleteModal-${guid}`, }, [ @@ -92,6 +109,7 @@ const ContentsComponent = { m("a", { class: "fa-solid fa-arrow-up-right-from-square", href: content["content_url"], + title: `Open ${title} (opens in new tab)`, target: "_blank", onclick: (e) => e.stopPropagation(), }), @@ -100,6 +118,10 @@ const ContentsComponent = { contentId: guid, contentTitle: title, }), + m(RenameModal, { + contentId: guid, + contentTitle: title, + }), ], ); }), diff --git a/extensions/publisher-command-center/src/components/LockContentButton.js b/extensions/publisher-command-center/src/components/LockContentButton.js index 24015c5e..4c85f364 100644 --- a/extensions/publisher-command-center/src/components/LockContentButton.js +++ b/extensions/publisher-command-center/src/components/LockContentButton.js @@ -3,7 +3,9 @@ import Contents from "../models/Contents"; const LockedContentButton = { view: function(vnode) { - const labelMessage = vnode.attrs.isLocked ? "Unlock Content" : "Lock Content"; + const labelMessage = vnode.attrs.isLocked ? + `Unlock ${vnode.attrs.contentTitle}` : + `Lock Content ${vnode.attrs.contentTitle}`; const iconClassName = vnode.attrs.isLocked ? "fa-lock" : "fa-lock-open"; return m("button", { class: "action-btn", diff --git a/extensions/publisher-command-center/src/components/RenameModal.js b/extensions/publisher-command-center/src/components/RenameModal.js new file mode 100644 index 00000000..7304ea87 --- /dev/null +++ b/extensions/publisher-command-center/src/components/RenameModal.js @@ -0,0 +1,89 @@ +import m from "mithril"; +import Contents from "../models/Contents"; + +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 = bootstrap.Modal.getInstance(modalEl); + modal.hide(); + } + }, + view: function(vnode) { + return m("form", { + onsubmit: (e) => { this.onsubmit(e); } + }, + [ + m("section", { class: "modal-body" }, [ + m("div", { class: "form-group has-validation" }, [ + 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; + 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, + }), + ]) + ]), + 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; diff --git a/extensions/publisher-command-center/src/models/Contents.js b/extensions/publisher-command-center/src/models/Contents.js index a0d8fabf..2a5cb075 100644 --- a/extensions/publisher-command-center/src/models/Contents.js +++ b/extensions/publisher-command-center/src/models/Contents.js @@ -51,6 +51,9 @@ export default { body: { title: newName, }, + }).then((response) => { + const targetContent = this.data.find((c) => c.guid === guid); + Object.assign(targetContent, response); }); }, From 45ce6c9e0ae75b812d4fa0bb454f9051a9b999ff Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Wed, 21 May 2025 16:41:03 -0500 Subject: [PATCH 3/6] Fix hiding modal and import for bootstrap --- .../src/components/RenameModal.js | 8 ++++++-- extensions/publisher-command-center/src/index.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/publisher-command-center/src/components/RenameModal.js b/extensions/publisher-command-center/src/components/RenameModal.js index 7304ea87..fd0b159f 100644 --- a/extensions/publisher-command-center/src/components/RenameModal.js +++ b/extensions/publisher-command-center/src/components/RenameModal.js @@ -1,5 +1,6 @@ import m from "mithril"; import Contents from "../models/Contents"; +import { Modal } from "bootstrap"; const RenameModalForm = { newName: "", @@ -11,8 +12,10 @@ const RenameModalForm = { Contents.rename(RenameModalForm.guid, RenameModalForm.newName); const modalEl = document.getElementById(`renameModal-${RenameModalForm.guid}`); - const modal = bootstrap.Modal.getInstance(modalEl); + const modal = Modal.getInstance(modalEl); modal.hide(); + + RenameModalForm.newName = ""; } }, view: function(vnode) { @@ -21,7 +24,7 @@ const RenameModalForm = { }, [ m("section", { class: "modal-body" }, [ - m("div", { class: "form-group has-validation" }, [ + m("div", { class: "form-group" }, [ m("label", { for: "rename-content-input", class: "mb-3", @@ -40,6 +43,7 @@ const RenameModalForm = { required: true, minlength: 3, maxlength: 1024, + value: RenameModalForm.newName, }), ]) ]), diff --git a/extensions/publisher-command-center/src/index.js b/extensions/publisher-command-center/src/index.js index f8fc06af..a0f5823c 100644 --- a/extensions/publisher-command-center/src/index.js +++ b/extensions/publisher-command-center/src/index.js @@ -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"; From 9d0cac8618e4cc22aee8758df4518f2be77ff244 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Wed, 21 May 2025 16:51:14 -0500 Subject: [PATCH 4/6] Update manifest and bump version --- extensions/publisher-command-center/manifest.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/publisher-command-center/manifest.json b/extensions/publisher-command-center/manifest.json index d4a88b1b..6e79c167 100644 --- a/extensions/publisher-command-center/manifest.json +++ b/extensions/publisher-command-center/manifest.json @@ -20,21 +20,21 @@ "extension": { "name": "publisher-command-center", "title": "Publisher Command Center", - "description": "An app that helps publishers view and manage details about apps and dashboards deployed to Connect: View app metadata and history for all of the apps you are collaborating on as well as see and managing running process for individual apps.", + "description": "An app that helps publishers view and manage details about apps and dashboards deployed to Connect. View app metadata and history for all of the apps you are collaborating on as well as see and managing running process for individual apps.", "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/publisher-command-center", "minimumConnectVersion": "2025.04.0", "requiredFeatures": [ "API Publishing", "OAuth Integrations" ], - "version": "0.0.4" + "version": "0.0.5" }, "files": { "requirements.txt": { "checksum": "a162a98758867a701ce693948ffdfa67" }, "app.py": { - "checksum": "55729c283443e02bcfc4233ccf0c2b3d" + "checksum": "b56cfcda18d0d11dee415ccdf81a757e" }, "dist/assets/fa-brands-400.808443ae.ttf": { "checksum": "15d54d142da2f2d6f2e90ed1d55121af" @@ -60,14 +60,14 @@ "dist/assets/fa-v4compatibility.30f6abf6.ttf": { "checksum": "4ed293ceaca9b5b2d9cd74a477963fae" }, + "dist/assets/index.3a9c08bc.js": { + "checksum": "190b4f6c18d188527b1bfbf2bef1ede6" + }, "dist/assets/index.eba02280.css": { "checksum": "cb5a34d5ea88d4969288161bd3123ee4" }, - "dist/assets/index.f987f996.js": { - "checksum": "f8a777db2d3d8e211a31f70bbca57234" - }, "dist/index.html": { - "checksum": "c61715903d067cc4ece8190fbdddb516" + "checksum": "1cbc14166f5e5bcf06fbe9c6c35d55aa" } } } From 3e93e62329ed387b387cfad8473152180f5fe1c9 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Thu, 22 May 2025 11:24:54 -0500 Subject: [PATCH 5/6] Add loading state for lock, add aria-label --- .../publisher-command-center/manifest.json | 12 +++--- .../publisher-command-center/scss/index.scss | 4 ++ .../src/components/ContentsComponent.js | 3 +- .../src/components/LockContentButton.js | 39 ++++++++++++++++--- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/extensions/publisher-command-center/manifest.json b/extensions/publisher-command-center/manifest.json index 6e79c167..a67463e8 100644 --- a/extensions/publisher-command-center/manifest.json +++ b/extensions/publisher-command-center/manifest.json @@ -27,7 +27,7 @@ "API Publishing", "OAuth Integrations" ], - "version": "0.0.5" + "version": "0.0.4" }, "files": { "requirements.txt": { @@ -60,14 +60,14 @@ "dist/assets/fa-v4compatibility.30f6abf6.ttf": { "checksum": "4ed293ceaca9b5b2d9cd74a477963fae" }, - "dist/assets/index.3a9c08bc.js": { - "checksum": "190b4f6c18d188527b1bfbf2bef1ede6" + "dist/assets/index.043e0ce3.css": { + "checksum": "0e41eeba30bce2f8e92873acb12462f7" }, - "dist/assets/index.eba02280.css": { - "checksum": "cb5a34d5ea88d4969288161bd3123ee4" + "dist/assets/index.7d457dc3.js": { + "checksum": "5cbfee09ee6b22913005c1adbaff39f7" }, "dist/index.html": { - "checksum": "1cbc14166f5e5bcf06fbe9c6c35d55aa" + "checksum": "ae6f4b40d27ac5c0586b85fd13806c72" } } } diff --git a/extensions/publisher-command-center/scss/index.scss b/extensions/publisher-command-center/scss/index.scss index cca3d006..2cc39c78 100644 --- a/extensions/publisher-command-center/scss/index.scss +++ b/extensions/publisher-command-center/scss/index.scss @@ -39,6 +39,10 @@ $colors: ( color: $blue; } +.lock-loading { + color: $gray; +} + // Removes the white background of the modal .modal.show { background: none; diff --git a/extensions/publisher-command-center/src/components/ContentsComponent.js b/extensions/publisher-command-center/src/components/ContentsComponent.js index 3a244561..d6541c75 100644 --- a/extensions/publisher-command-center/src/components/ContentsComponent.js +++ b/extensions/publisher-command-center/src/components/ContentsComponent.js @@ -109,7 +109,8 @@ const ContentsComponent = { m("a", { class: "fa-solid fa-arrow-up-right-from-square", href: content["content_url"], - title: `Open ${title} (opens in new tab)`, + ariaLabel: `Open ${title} (opens in new tab)`, + title: `Open ${title}`, target: "_blank", onclick: (e) => e.stopPropagation(), }), diff --git a/extensions/publisher-command-center/src/components/LockContentButton.js b/extensions/publisher-command-center/src/components/LockContentButton.js index 4c85f364..6422110b 100644 --- a/extensions/publisher-command-center/src/components/LockContentButton.js +++ b/extensions/publisher-command-center/src/components/LockContentButton.js @@ -1,21 +1,50 @@ import m from "mithril"; import Contents from "../models/Contents"; -const LockedContentButton = { +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 = vnode.attrs.isLocked ? "fa-lock" : "fa-lock-open"; + + 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, - onclick: () => { - Contents.lock(vnode.attrs.contentId) + 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}` }) + m("i", { + class: `fa-solid ${iconClassName()}`, + + } + ) ]) } }; From 8efa8c01641988b49813160bbfb429d643522746 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Thu, 22 May 2025 12:00:42 -0500 Subject: [PATCH 6/6] Remove changes to manifest --- extensions/publisher-command-center/manifest.json | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/extensions/publisher-command-center/manifest.json b/extensions/publisher-command-center/manifest.json index 68a0d953..35d28b79 100644 --- a/extensions/publisher-command-center/manifest.json +++ b/extensions/publisher-command-center/manifest.json @@ -20,7 +20,7 @@ "extension": { "name": "publisher-command-center", "title": "Publisher Command Center", - "description": "An app that helps publishers view and manage details about apps and dashboards deployed to Connect. View app metadata and history for all of the apps you are collaborating on as well as see and managing running process for individual apps.", + "description": "An app that helps publishers view and manage details about apps and dashboards deployed to Connect: View app metadata and history for all of the apps you are collaborating on as well as see and managing running process for individual apps.", "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/publisher-command-center", "minimumConnectVersion": "2025.04.0", "requiredFeatures": [ @@ -34,7 +34,7 @@ "checksum": "a162a98758867a701ce693948ffdfa67" }, "app.py": { - "checksum": "b56cfcda18d0d11dee415ccdf81a757e" + "checksum": "55729c283443e02bcfc4233ccf0c2b3d" }, "dist/assets/fa-brands-400.808443ae.ttf": { "checksum": "15d54d142da2f2d6f2e90ed1d55121af" @@ -60,16 +60,6 @@ "dist/assets/fa-v4compatibility.30f6abf6.ttf": { "checksum": "4ed293ceaca9b5b2d9cd74a477963fae" }, -<<<<<<< HEAD - "dist/assets/index.043e0ce3.css": { - "checksum": "0e41eeba30bce2f8e92873acb12462f7" - }, - "dist/assets/index.7d457dc3.js": { - "checksum": "5cbfee09ee6b22913005c1adbaff39f7" - }, - "dist/index.html": { - "checksum": "ae6f4b40d27ac5c0586b85fd13806c72" -======= "dist/assets/index.90f1ae46.js": { "checksum": "8b85382a2fd0bb7f106cf03488e3bd72" }, @@ -78,7 +68,6 @@ }, "dist/index.html": { "checksum": "af4c52e366ba6e5be8f9776014856862" ->>>>>>> main } } }