Skip to content

Commit 86db2de

Browse files
authored
Add lock and rename content functionality to Publisher Command Center (#138)
* Add lock and rename endpoint, lock button, and functionality * Add rename API endpoint, rename modal, and update tooltip wording * Fix hiding modal and import for bootstrap * Update manifest and bump version * Add loading state for lock, add aria-label * Remove changes to manifest
1 parent 276560e commit 86db2de

File tree

7 files changed

+228
-1
lines changed

7 files changed

+228
-1
lines changed

extensions/publisher-command-center/app.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ async def content(
9595
visitor = get_visitor_client(posit_connect_user_session_token)
9696
return visitor.content.get(content_id)
9797

98+
@app.patch("/api/content/{content_id}/lock")
99+
async def lock_content(
100+
content_id: str,
101+
posit_connect_user_session_token: str = Header(None),
102+
):
103+
visitor = get_visitor_client(posit_connect_user_session_token)
104+
content = visitor.content.get(content_id)
105+
is_locked = content.locked
106+
107+
content.update(locked=not is_locked)
108+
return content
109+
110+
@app.patch("/api/content/{content_id}/rename")
111+
async def rename_content(
112+
content_id: str,
113+
title: str = Body(..., embed = True),
114+
posit_connect_user_session_token: str = Header(None),
115+
):
116+
visitor = get_visitor_client(posit_connect_user_session_token)
117+
content = visitor.content.get(content_id)
118+
119+
content.update(title = title)
120+
return content
98121

99122
@app.get("/api/contents/{content_id}/processes")
100123
async def get_content_processes(

extensions/publisher-command-center/scss/index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ $colors: (
3939
color: $blue;
4040
}
4141

42+
.lock-loading {
43+
color: $gray;
44+
}
45+
4246
// Removes the white background of the modal
4347
.modal.show {
4448
background: none;

extensions/publisher-command-center/src/components/ContentsComponent.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import m from "mithril";
22
import { format } from "date-fns";
33
import Contents from "../models/Contents";
44
import Languages from "./Languages";
5+
import LockContentButton from "./LockContentButton";
56
import DeleteModal from "./DeleteModal";
7+
import RenameModal from "./RenameModal";
68

79
const ContentsComponent = {
810
error: null,
@@ -43,6 +45,8 @@ const ContentsComponent = {
4345
m("th", { scope: "col" }, "Date Added"),
4446
m("th", { scope: "col" }, ""),
4547
m("th", { scope: "col" }, ""),
48+
m("th", { scope: "col" }, ""),
49+
m("th", { scope: "col" }, ""),
4650
]),
4751
),
4852
m(
@@ -72,6 +76,28 @@ const ContentsComponent = {
7276
"td",
7377
m("button", {
7478
class: "action-btn",
79+
ariaLabel: `Rename ${title}`,
80+
title: `Rename ${title}`,
81+
"data-bs-toggle": "modal",
82+
"data-bs-target": `#renameModal-${guid}`,
83+
}, [
84+
m("i", { class: "fa-solid fa-pencil" })
85+
]),
86+
),
87+
m(
88+
"td",
89+
m(LockContentButton, {
90+
contentId: guid,
91+
contentTitle: title,
92+
isLocked: content["locked"],
93+
}),
94+
),
95+
m(
96+
"td",
97+
m("button", {
98+
class: "action-btn",
99+
title: `Delete ${title}`,
100+
ariaLabel: `Delete ${title}`,
75101
"data-bs-toggle": "modal",
76102
"data-bs-target": `#deleteModal-${guid}`,
77103
}, [
@@ -83,6 +109,8 @@ const ContentsComponent = {
83109
m("a", {
84110
class: "fa-solid fa-arrow-up-right-from-square",
85111
href: content["content_url"],
112+
ariaLabel: `Open ${title} (opens in new tab)`,
113+
title: `Open ${title}`,
86114
target: "_blank",
87115
onclick: (e) => e.stopPropagation(),
88116
}),
@@ -91,6 +119,10 @@ const ContentsComponent = {
91119
contentId: guid,
92120
contentTitle: title,
93121
}),
122+
m(RenameModal, {
123+
contentId: guid,
124+
contentTitle: title,
125+
}),
94126
],
95127
);
96128
}),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import m from "mithril";
2+
import Contents from "../models/Contents";
3+
4+
const LockedContentButton = {
5+
oninit: function () {
6+
this.isLoading = false;
7+
},
8+
9+
view: function(vnode) {
10+
const labelMessage = vnode.attrs.isLocked ?
11+
`Unlock ${vnode.attrs.contentTitle}` :
12+
`Lock Content ${vnode.attrs.contentTitle}`;
13+
14+
const iconClassName = () => {
15+
if (this.isLoading) return "fa-spinner fa-spin lock-loading";
16+
17+
if (vnode.attrs.isLocked) {
18+
return "fa-lock";
19+
} else {
20+
return "fa-lock-open";
21+
}
22+
};
23+
24+
return m("button", {
25+
class: "action-btn",
26+
ariaLabel: labelMessage,
27+
title: labelMessage,
28+
disabled: this.isLoading,
29+
onclick: async () => {
30+
if (this.isLoading) { return; }
31+
32+
this.isLoading = true;
33+
m.redraw();
34+
35+
try {
36+
await Contents.lock(vnode.attrs.contentId)
37+
} finally {
38+
this.isLoading = false;
39+
m.redraw();
40+
}
41+
}
42+
}, [
43+
m("i", {
44+
class: `fa-solid ${iconClassName()}`,
45+
46+
}
47+
)
48+
])
49+
}
50+
};
51+
52+
export default LockedContentButton;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import m from "mithril";
2+
import Contents from "../models/Contents";
3+
import { Modal } from "bootstrap";
4+
5+
const RenameModalForm = {
6+
newName: "",
7+
guid: "",
8+
isValid: false,
9+
onsubmit: function(e) {
10+
if (RenameModalForm.isValid) {
11+
e.preventDefault();
12+
Contents.rename(RenameModalForm.guid, RenameModalForm.newName);
13+
14+
const modalEl = document.getElementById(`renameModal-${RenameModalForm.guid}`);
15+
const modal = Modal.getInstance(modalEl);
16+
modal.hide();
17+
18+
RenameModalForm.newName = "";
19+
}
20+
},
21+
view: function(vnode) {
22+
return m("form", {
23+
onsubmit: (e) => { this.onsubmit(e); }
24+
},
25+
[
26+
m("section", { class: "modal-body" }, [
27+
m("div", { class: "form-group" }, [
28+
m("label", {
29+
for: "rename-content-input",
30+
class: "mb-3",
31+
}, "Enter new name for ", [
32+
m("span", { class: "fw-bold" }, `${vnode.attrs.contentTitle}`)
33+
]),
34+
m("input", {
35+
oninput: function(e) {
36+
RenameModalForm.isValid = e.target.validity.valid;
37+
RenameModalForm.guid = vnode.attrs.contentId;
38+
RenameModalForm.newName = e.target.value;
39+
},
40+
id: "rename-content-input",
41+
type: "text",
42+
class: "form-control",
43+
required: true,
44+
minlength: 3,
45+
maxlength: 1024,
46+
value: RenameModalForm.newName,
47+
}),
48+
])
49+
]),
50+
m("div", { class: "modal-footer" }, [
51+
m(
52+
"button",
53+
{
54+
type: "submit",
55+
class: "btn btn-primary",
56+
ariaLabel: "Rename Content",
57+
},
58+
"Rename Content",
59+
),
60+
]),
61+
])
62+
},
63+
}
64+
65+
const RenameModal = {
66+
view: function(vnode) {
67+
return m("div", {
68+
class: "modal",
69+
id: `renameModal-${vnode.attrs.contentId}`,
70+
tabindex: "-1",
71+
ariaHidden: true
72+
}, [
73+
m("div", { class: "modal-dialog modal-dialog-centered" }, [
74+
m("div", { class: "modal-content" }, [
75+
m("div", { class: "modal-header"}, [
76+
m("h1", { class: "modal-title fs-6" }, "Rename Content"),
77+
m("button", {
78+
class: "btn-close",
79+
ariaLabel: "Close modal",
80+
"data-bs-dismiss": "modal"
81+
}),
82+
]),
83+
m(RenameModalForm, {
84+
contentTitle: vnode.attrs.contentTitle,
85+
contentId: vnode.attrs.contentId,
86+
})
87+
]),
88+
]),
89+
])
90+
},
91+
};
92+
93+
export default RenameModal;

extensions/publisher-command-center/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import m from "mithril";
22

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

66
import "../scss/index.scss";

extensions/publisher-command-center/src/models/Contents.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ export default {
3434
this.data = this.data.filter((c) => c.guid !== guid);
3535
},
3636

37+
lock: async function (guid) {
38+
await m.request({
39+
method: "PATCH",
40+
url: `api/content/${guid}/lock`,
41+
}).then((response) => {
42+
const targetContent = this.data.find((c) => c.guid === guid);
43+
Object.assign(targetContent, response);
44+
});
45+
},
46+
47+
rename: async function (guid, newName) {
48+
await m.request({
49+
method: "PATCH",
50+
url: `api/content/${guid}/rename`,
51+
body: {
52+
title: newName,
53+
},
54+
}).then((response) => {
55+
const targetContent = this.data.find((c) => c.guid === guid);
56+
Object.assign(targetContent, response);
57+
});
58+
},
59+
3760
reset: function () {
3861
this.data = null;
3962
this._fetch = null;

0 commit comments

Comments
 (0)