domma-cms 0.92.1 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
* Feedback - one sidebar entry, two screens, decided by the server:
|
|
3
3
|
*
|
|
4
4
|
* on a site Send feedback, and what this site has sent (send.js)
|
|
5
|
-
* on the manager the inbox of every site's feedback (inbox.js)
|
|
5
|
+
* on the manager the inbox of every site's feedback (inbox.js); the level-0 role
|
|
6
|
+
* also gets a "My feedback" tab - send.js again, filing straight
|
|
7
|
+
* into that inbox as "Manager (this server)"
|
|
6
8
|
*
|
|
7
9
|
* #/plugins/feedback the screen
|
|
8
10
|
* #/plugins/feedback/<id> the same, with that report open (a notification's link)
|
|
11
|
+
* #/plugins/feedback/mine the manager, on the My feedback tab
|
|
9
12
|
*
|
|
10
13
|
* Path segments, not ?query: the admin router 404s a query on a fresh load.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import {api, loadKit} from '../lib/kit.js';
|
|
16
|
+
import {api, loadKit, store} from '../lib/kit.js';
|
|
14
17
|
|
|
15
18
|
const ROUTE = /^#\/plugins\/feedback(\/|$)/;
|
|
16
|
-
const V = '1.
|
|
19
|
+
const V = '1.1.0';
|
|
20
|
+
const TABS = [
|
|
21
|
+
{key: 'inbox', label: 'Inbox', icon: 'inbox'},
|
|
22
|
+
{key: 'mine', label: 'My feedback', icon: 'send'}
|
|
23
|
+
];
|
|
17
24
|
let lifetime = null;
|
|
18
25
|
|
|
19
26
|
export const feedbackView = {
|
|
@@ -50,14 +57,76 @@ export const feedbackView = {
|
|
|
50
57
|
}
|
|
51
58
|
if (!here()) return;
|
|
52
59
|
|
|
53
|
-
const
|
|
54
|
-
try {
|
|
60
|
+
const load = async (screen, into, ctx) => {
|
|
55
61
|
const mod = await import(`/plugins/feedback/admin/views/${screen}.js?v=${V}`);
|
|
56
|
-
if (!here()) return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
if (!here()) return null;
|
|
63
|
+
return mod.mount(into, {kit, container, $container, meta, here, ...ctx});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// A site, or a manager user below level 0: one screen, as before.
|
|
67
|
+
if (mode.mode !== 'receiver' || !mode.canSend) {
|
|
68
|
+
try {
|
|
69
|
+
const cleanup = await load(mode.mode === 'receiver' ? 'inbox' : 'send', root, {scope, mode, id});
|
|
70
|
+
if (typeof cleanup === 'function') scope(cleanup);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
fail(`This screen could not load: ${err.message || err}`);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
61
75
|
}
|
|
76
|
+
|
|
77
|
+
// The manager's level-0 role: the inbox, and its own feedback. One tab mounted at a time,
|
|
78
|
+
// each with its own cleanups, so switching back always shows fresh data.
|
|
79
|
+
root.innerHTML = `
|
|
80
|
+
<div class="fb-bar fb-tabs">
|
|
81
|
+
<div class="fb-seg" role="tablist" aria-label="Feedback">
|
|
82
|
+
${TABS.map(t => `<button type="button" role="tab" data-tab="${t.key}"><span data-icon="${t.icon}" data-icon-size="14"></span> ${t.label}</button>`).join('')}
|
|
83
|
+
</div>
|
|
84
|
+
<span data-help-title="Your own feedback" data-help="Inbox is every report, from every site and from this server. My feedback is where you file your own - a bug, a request or a note to self about the CMS or the Manager. It goes straight into the Inbox, marked Manager (this server), and then works like any site's report: status, reply and a Waypoint issue. Only the most senior role sees this tab."></span>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-feedback-pane></div>`;
|
|
87
|
+
scope(kit.helpPopovers(root, {position: 'bottom'}));
|
|
88
|
+
I.scan(root);
|
|
89
|
+
const pane = root.querySelector('[data-feedback-pane]');
|
|
90
|
+
const tabs = root.querySelector('.fb-seg');
|
|
91
|
+
let paneCleanups = [];
|
|
92
|
+
const disposePane = () => { for (const fn of paneCleanups.splice(0)) { try { fn(); } catch { /* already gone */ } } };
|
|
93
|
+
scope(disposePane);
|
|
94
|
+
|
|
95
|
+
let current = '';
|
|
96
|
+
const show = async (key, openId = '') => {
|
|
97
|
+
if (key === current) return;
|
|
98
|
+
current = key;
|
|
99
|
+
store.set('feedback.tab', key);
|
|
100
|
+
for (const b of tabs.querySelectorAll('[data-tab]')) {
|
|
101
|
+
const on = b.dataset.tab === key;
|
|
102
|
+
b.classList.toggle('is-on', on);
|
|
103
|
+
b.setAttribute('aria-selected', String(on));
|
|
104
|
+
}
|
|
105
|
+
disposePane();
|
|
106
|
+
pane.innerHTML = '';
|
|
107
|
+
const mine = () => current === key && here();
|
|
108
|
+
const paneScope = (fn) => { paneCleanups.push(fn); return fn; };
|
|
109
|
+
try {
|
|
110
|
+
const cleanup = await load(key === 'mine' ? 'send' : 'inbox', pane, {
|
|
111
|
+
scope: paneScope, here: mine, id: openId,
|
|
112
|
+
mode: key === 'mine' ? {mode: 'sender', connected: true, local: true} : mode
|
|
113
|
+
});
|
|
114
|
+
if (typeof cleanup === 'function') paneScope(cleanup);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
pane.innerHTML = '';
|
|
117
|
+
const p = document.createElement('p');
|
|
118
|
+
p.className = 'fb-note is-bad';
|
|
119
|
+
p.textContent = `This screen could not load: ${err.message || err}`;
|
|
120
|
+
pane.appendChild(p);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const onTab = (e) => { const b = e.target.closest('[data-tab]'); if (b) show(b.dataset.tab); };
|
|
124
|
+
tabs.addEventListener('click', onTab);
|
|
125
|
+
scope(() => tabs.removeEventListener('click', onTab));
|
|
126
|
+
|
|
127
|
+
// A report id in the path opens the inbox on it; /mine, or the last tab used, otherwise.
|
|
128
|
+
if (id === 'mine') await show('mine');
|
|
129
|
+
else if (id) await show('inbox', id);
|
|
130
|
+
else await show(store.get('feedback.tab') === 'mine' ? 'mine' : 'inbox');
|
|
62
131
|
}
|
|
63
132
|
};
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* A report is kept on this site before it is sent, so a Domma server that is
|
|
6
6
|
* restarting only delays it ("Waiting to send"); opening this screen tries
|
|
7
7
|
* again. A site the manager did not start has nowhere to send to, and says so.
|
|
8
|
+
*
|
|
9
|
+
* On the manager itself (ctx.mode.local - the level-0 role's "My feedback" tab)
|
|
10
|
+
* the same screen files straight into the inbox: nothing waits, nothing travels.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import {api, esc, openPanel, refreshBadge, stamp, when} from '../lib/kit.js';
|
|
@@ -22,6 +25,24 @@ const TONE = {new: 'info', acknowledged: 'muted', planned: 'info', 'in-progress'
|
|
|
22
25
|
|
|
23
26
|
export async function mount(root, ctx) {
|
|
24
27
|
const {kit, scope, here, meta, id} = ctx;
|
|
28
|
+
const local = Boolean(ctx.mode.local);
|
|
29
|
+
const COPY = local ? {
|
|
30
|
+
lead: 'File your own feedback on the CMS or the Manager.',
|
|
31
|
+
sub: 'It goes straight into the Inbox as Manager (this server) - status, reply and Waypoint work as for any site.',
|
|
32
|
+
help: 'For you and the other holders of the most senior role on this server. A report lands in the Inbox tab at once, marked Manager (this server), so it sits beside the sites\' reports and can be answered or made a Waypoint issue there. This list is everything this server has filed, whoever filed it, with its status and any reply.',
|
|
33
|
+
title: 'Send feedback',
|
|
34
|
+
hint: 'Filed with your name and this server\'s CMS version.',
|
|
35
|
+
replied: 'Reply',
|
|
36
|
+
noReply: 'No reply yet. Answer it, or file it in Waypoint, from the Inbox tab.'
|
|
37
|
+
} : {
|
|
38
|
+
lead: 'Tell Domma what is broken, missing or good.',
|
|
39
|
+
sub: 'Every report is read. You will see its progress here, and any reply.',
|
|
40
|
+
help: 'Reports go to the Domma team, with your name and this site attached so we can come back to you. You see everything this site has sent, whoever sent it. A report shows as Seen, Planned, In progress, Done or Not planned as it moves, and a reply from Domma appears in it - the sidebar badge counts replies you have not opened.',
|
|
41
|
+
title: 'Send feedback to Domma',
|
|
42
|
+
hint: 'Sent with your name, this site and its CMS version, so Domma can reply.',
|
|
43
|
+
replied: 'Domma replied',
|
|
44
|
+
noReply: 'No reply yet. The status above moves as Domma works on it.'
|
|
45
|
+
};
|
|
25
46
|
const types = meta.types || [];
|
|
26
47
|
const iconOf = (t) => types.find(x => x.key === t)?.icon || 'message-square';
|
|
27
48
|
const labelOf = (t) => types.find(x => x.key === t)?.short || t;
|
|
@@ -58,10 +79,10 @@ export async function mount(root, ctx) {
|
|
|
58
79
|
<div data-bind-hidden="!connected.value">
|
|
59
80
|
<div class="fb-bar">
|
|
60
81
|
<div class="fb-intro">
|
|
61
|
-
<strong
|
|
62
|
-
<span
|
|
82
|
+
<strong>${esc(COPY.lead)}</strong>
|
|
83
|
+
<span>${esc(COPY.sub)}</span>
|
|
63
84
|
</div>
|
|
64
|
-
<span data-help-title="Feedback" data-help="
|
|
85
|
+
<span data-help-title="Feedback" data-help="${esc(COPY.help)}"></span>
|
|
65
86
|
<span class="fb-spacer"></span>
|
|
66
87
|
<button type="button" class="btn btn-primary btn-sm" data-on-click="compose"><span data-icon="send" data-icon-size="14"></span> Send feedback</button>
|
|
67
88
|
</div>
|
|
@@ -116,7 +137,7 @@ export async function mount(root, ctx) {
|
|
|
116
137
|
const isBug = M.computed(() => f.type.value === 'bug');
|
|
117
138
|
let panel = null;
|
|
118
139
|
openPanel({
|
|
119
|
-
title:
|
|
140
|
+
title: COPY.title,
|
|
120
141
|
size: 'md',
|
|
121
142
|
model: {...f, isBug},
|
|
122
143
|
html: `<div class="fb-form">
|
|
@@ -136,7 +157,7 @@ export async function mount(root, ctx) {
|
|
|
136
157
|
</div>
|
|
137
158
|
<label class="form-label" for="fb-where">Where <span data-help="The screen or page it is about, if there is one - e.g. Pages, the Blog editor, /contact. Optional."></span></label>
|
|
138
159
|
<input id="fb-where" class="form-input" maxlength="300" data-model="where.value" placeholder="Optional">
|
|
139
|
-
<p class="fb-hint"
|
|
160
|
+
<p class="fb-hint">${esc(COPY.hint)}</p>
|
|
140
161
|
<div class="fb-actions"><span class="fb-spacer"></span>
|
|
141
162
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
142
163
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="send" data-bind-disabled="busy.value"><span data-icon="send" data-icon-size="13"></span> Send</button>
|
|
@@ -152,6 +173,7 @@ export async function mount(root, ctx) {
|
|
|
152
173
|
where: f.where.peek(), severity: f.type.peek() === 'bug' ? f.severity.peek() : ''});
|
|
153
174
|
E.toast(r.message || 'Sent.', {type: r.state === 'sent' ? 'success' : 'warning'});
|
|
154
175
|
panel?.close();
|
|
176
|
+
refreshBadge();
|
|
155
177
|
await load();
|
|
156
178
|
} catch (err) {
|
|
157
179
|
E.toast(err.message || String(err), {type: 'error'});
|
|
@@ -189,7 +211,7 @@ export async function mount(root, ctx) {
|
|
|
189
211
|
<dl class="fb-facts">${lines.map(([a, b]) => `<div><dt>${esc(a)}</dt><dd>${esc(b)}</dd></div>`).join('')}</dl>
|
|
190
212
|
<h4>What was sent</h4>
|
|
191
213
|
<div class="fb-text">${esc(r.description)}</div>
|
|
192
|
-
${r.reply ? `<h4><span data-icon="corner-up-left" data-icon-size="14"></span>
|
|
214
|
+
${r.reply ? `<h4><span data-icon="corner-up-left" data-icon-size="14"></span> ${esc(COPY.replied)} <small>${esc(stamp(r.replyAt))}</small></h4><div class="fb-text fb-reply">${esc(r.reply)}</div>` : `<p class="fb-note">${esc(COPY.noReply)}</p>`}
|
|
193
215
|
</div>`
|
|
194
216
|
});
|
|
195
217
|
if (r.unread && r.id) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Guide
|
|
3
|
+
order: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Feedback lets you tell Domma what is broken, missing or good, straight from your admin, and follow what happens to it. It is free.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- Sends a report to the Domma team with your name, your site and its CMS version attached, so Domma can reply.
|
|
11
|
+
- Lists every report this site has sent, whoever sent it, with its status.
|
|
12
|
+
- Shows Domma's reply inside the report, and tells you when one arrives.
|
|
13
|
+
- Keeps a report safe if Domma cannot be reached, and sends it as soon as it can.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
1. Open [Feedback](#/plugins/feedback) from the admin sidebar.
|
|
18
|
+
2. Click **Send feedback**.
|
|
19
|
+
3. Under **What is it?** pick one: Something is broken, An idea or request, A question, Something we got right, or Something else.
|
|
20
|
+
4. Type a **Title** (one line) and a **Description**.
|
|
21
|
+
5. For something broken, choose **How bad is it?**: Minor - a nuisance, Major - gets in the way, or Blocker - cannot carry on.
|
|
22
|
+
6. Optional: fill in **Where** - the screen or page it is about, for example "Pages" or `/contact`.
|
|
23
|
+
7. Click **Send**.
|
|
24
|
+
|
|
25
|
+
A good bug report says what you did, what you expected and what happened instead. For a request, say what you are trying to get done.
|
|
26
|
+
|
|
27
|
+
## Screens
|
|
28
|
+
|
|
29
|
+
### Your reports
|
|
30
|
+
|
|
31
|
+
Each row shows the type, title, when it was sent and by whom, and a status:
|
|
32
|
+
|
|
33
|
+
| Status | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| New | Domma has it and has not looked yet |
|
|
36
|
+
| Seen | Domma has read it |
|
|
37
|
+
| Planned | It is on the list |
|
|
38
|
+
| In progress | Being worked on |
|
|
39
|
+
| Done | Finished |
|
|
40
|
+
| Not planned | Domma does not intend to do it |
|
|
41
|
+
| Waiting to send | Domma could not be reached yet; it goes automatically |
|
|
42
|
+
| Not accepted | Domma's server turned the report down |
|
|
43
|
+
|
|
44
|
+
A dot on a row means a new reply. Click a row to open it: you see the details, **What was sent**, and **Domma replied** with the reply, or "No reply yet".
|
|
45
|
+
|
|
46
|
+
If "Tracked as" appears, Domma has filed the report in its own issue tracker, and the status follows that issue.
|
|
47
|
+
|
|
48
|
+
When reports are waiting to send, a note says so, with **Try now**.
|
|
49
|
+
|
|
50
|
+
### Not connected
|
|
51
|
+
|
|
52
|
+
If the screen says "This site is not connected to Domma", your site was not started by a Domma server, so there is nowhere to send to. Ask whoever set the site up.
|
|
53
|
+
|
|
54
|
+
## Settings
|
|
55
|
+
|
|
56
|
+
There is no settings screen. Whoever runs the site can change one option in the plugin's configuration file:
|
|
57
|
+
|
|
58
|
+
| Option | Default | Meaning |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `syncMinutes` | 60 | How often the site sends waiting reports and checks for replies |
|
|
61
|
+
|
|
62
|
+
Opening the Feedback screen also sends anything waiting and checks for replies straight away.
|
|
63
|
+
|
|
64
|
+
## Notifications
|
|
65
|
+
|
|
66
|
+
When Domma replies, you get an admin notification "Domma replied:" followed by the report's title. The sidebar badge counts replies you have not opened; opening the report clears it.
|
|
67
|
+
|
|
68
|
+
## Permissions and roles
|
|
69
|
+
|
|
70
|
+
In System > Roles, Feedback appears in the **Plugins** group:
|
|
71
|
+
|
|
72
|
+
| Action | Label | Allows |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `feedback.send` | Send | Send feedback to Domma and see what this site has sent |
|
|
75
|
+
| `feedback.manage` | Manage | Only used on the Domma server (see below) |
|
|
76
|
+
|
|
77
|
+
Only the **admin** role has them by default. Everyone with **Send** sees every report the site has sent, not just their own.
|
|
78
|
+
|
|
79
|
+
## On the Domma server
|
|
80
|
+
|
|
81
|
+
On the Domma server itself (the Manager), the same screen is the **Inbox** of every site's reports, for holders of **Feedback > Manage**:
|
|
82
|
+
|
|
83
|
+
- Filter by **Open**, each status, or **All**; by site and type; and search title, text, person and site.
|
|
84
|
+
- Open a report to set its **Status**, write a **Reply to the site** (the site sees it and is notified) and keep **Internal notes** (never sent to the site). Click **Save**.
|
|
85
|
+
- With Waypoint Pro installed, **Make a Waypoint issue** files the report in a project with a type and priority. The report then follows the issue: planned, in progress, done. "Not planned" set by hand stays until you change it.
|
|
86
|
+
- Right-click a row to open it, change its status quickly, or **Delete** it. Deleting removes it for the site too.
|
|
87
|
+
- Holders of the most senior role also get **My feedback**, to file their own reports. These appear in the Inbox as "Manager (this server)".
|
|
88
|
+
|
|
89
|
+
## Limitations
|
|
90
|
+
|
|
91
|
+
- Only sites started by a Domma server can send. A standalone install cannot.
|
|
92
|
+
- No attachments or screenshots. Describe the problem in words, and use **Where** for the page.
|
|
93
|
+
- Titles are limited to 140 characters and descriptions to 8,000.
|
|
94
|
+
- The Domma server accepts up to 10 reports from one site in 10 minutes. Any more show as **Waiting to send** and go later.
|
|
95
|
+
- You cannot edit or withdraw a report once sent. Send a follow-up instead.
|
|
@@ -21,6 +21,10 @@ import {receivedReport, statusFromIssue, statusLabel} from './shape.js';
|
|
|
21
21
|
|
|
22
22
|
export const COLLECTION = 'feedback-reports';
|
|
23
23
|
|
|
24
|
+
/** The site slug the manager's OWN reports carry. Site slugs start [a-z0-9], so no site can be this. */
|
|
25
|
+
export const MANAGER_SITE = '_manager';
|
|
26
|
+
export const MANAGER_NAME = 'Manager (this server)';
|
|
27
|
+
|
|
24
28
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
25
29
|
const SITE_MANAGER = path.resolve(HERE, '..', '..', 'site-manager');
|
|
26
30
|
const TOKENS = path.join(SITE_MANAGER, 'services', 'fleetTokens.js');
|
|
@@ -68,9 +72,12 @@ export const removeReport = (id) => deleteEntry(COLLECTION, id);
|
|
|
68
72
|
* A site's report arrived. The same site sending the same `ref` again (a retry
|
|
69
73
|
* after a reply that never reached it) gets the report it already made.
|
|
70
74
|
*
|
|
75
|
+
* @param {string} slug
|
|
76
|
+
* @param {object} payload
|
|
77
|
+
* @param {string} [source] - entry meta source: 'fleet' from a site, 'manager' for its own
|
|
71
78
|
* @returns {Promise<{entry?: object, created?: boolean, error?: string}>}
|
|
72
79
|
*/
|
|
73
|
-
export async function receive(slug, payload) {
|
|
80
|
+
export async function receive(slug, payload, source = 'fleet') {
|
|
74
81
|
const {data, error} = receivedReport(payload, slug);
|
|
75
82
|
if (error) return {error};
|
|
76
83
|
if (data.ref) {
|
|
@@ -78,7 +85,7 @@ export async function receive(slug, payload) {
|
|
|
78
85
|
if (same) return {entry: same, created: false};
|
|
79
86
|
}
|
|
80
87
|
data.history = [{at: new Date().toISOString(), by: data.userName, what: 'Sent'}];
|
|
81
|
-
const entry = await createEntry(COLLECTION, data, {source
|
|
88
|
+
const entry = await createEntry(COLLECTION, data, {source});
|
|
82
89
|
return {entry, created: true};
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -31,10 +31,11 @@ export function managerOf(env = process.env) {
|
|
|
31
31
|
* The outbox: this site's copy of everything it sent, in data/outbox.json.
|
|
32
32
|
*
|
|
33
33
|
* @param {string} dataDir
|
|
34
|
+
* @param {string} [name] - the manager keeps only `seen` for its own reports, in own.json
|
|
34
35
|
* @returns {object}
|
|
35
36
|
*/
|
|
36
|
-
export function createOutbox(dataDir) {
|
|
37
|
-
const file = path.join(dataDir,
|
|
37
|
+
export function createOutbox(dataDir, name = 'outbox.json') {
|
|
38
|
+
const file = path.join(dataDir, name);
|
|
38
39
|
const read = () => {
|
|
39
40
|
try {
|
|
40
41
|
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* POST /fleet/submit a site's report (Bearer fleet token)
|
|
16
16
|
* GET /fleet/mine that site's reports, sender-safe fields only
|
|
17
17
|
* GET /mode · GET /count 'new' reports
|
|
18
|
+
* POST /send · GET /sent · PUT /sent/:id/seen
|
|
19
|
+
* the manager's OWN feedback (level-0 role only):
|
|
20
|
+
* written straight into the inbox as site `_manager`
|
|
18
21
|
* GET /reports · GET /reports/:id · PUT /reports/:id · DELETE /reports/:id
|
|
19
22
|
* GET /waypoint is Waypoint Pro here, and its projects
|
|
20
23
|
* POST /reports/:id/promote make a Waypoint issue from a report
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
SEVERITIES, STATUSES, TYPES, buildPayload, cleanSubmission, cleanUpdate, createThrottle, issueDescription, senderView
|
|
31
34
|
} from './lib/shape.js';
|
|
32
35
|
import {createOutbox, flush, managerApi, managerOf, mergeReports, newRef} from './lib/sender.js';
|
|
36
|
+
import {getEffectiveLevel} from '../../server/services/userRoles.js';
|
|
33
37
|
|
|
34
38
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
35
39
|
const NAME = 'feedback';
|
|
@@ -67,7 +71,7 @@ export default async function feedbackPlugin(fastify, options) {
|
|
|
67
71
|
|
|
68
72
|
fastify.get('/meta', can('send'), async () => ({types: TYPES, statuses: STATUSES, severities: SEVERITIES}));
|
|
69
73
|
|
|
70
|
-
if (mode === 'receiver') await receiverRoutes(fastify, {can, hooks, notifyOk, receiverMod, settings, options});
|
|
74
|
+
if (mode === 'receiver') await receiverRoutes(fastify, {can, authenticate, requirePermission, hooks, notifyOk, receiverMod, settings, options});
|
|
71
75
|
else await senderRoutes(fastify, {can, hooks, notifyOk, settings, options});
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -152,10 +156,21 @@ async function senderRoutes(fastify, {can, hooks, notifyOk, settings, options})
|
|
|
152
156
|
// Receiver (the manager)
|
|
153
157
|
// ---------------------------------------------------------------------------------
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
/** Level 0 - the most senior role, whatever it is called. Roles are data, never names in code. */
|
|
160
|
+
const isTopLevel = (user) => { try { return getEffectiveLevel(user) === 0; } catch { return false; } };
|
|
161
|
+
|
|
162
|
+
async function receiverRoutes(fastify, {can, authenticate, requirePermission, hooks, notifyOk, receiverMod, settings, options}) {
|
|
156
163
|
const rx = receiverMod;
|
|
157
164
|
const slugOf = options.slugFromToken || rx.slugFromToken;
|
|
158
|
-
const
|
|
165
|
+
const siteNames = options.siteNames || rx.siteNames;
|
|
166
|
+
// The manager's own reports are filed as `_manager` - a slug no site can have (theirs start [a-z0-9]).
|
|
167
|
+
const names = () => ({...siteNames(), [rx.MANAGER_SITE]: rx.MANAGER_NAME});
|
|
168
|
+
// Its own feedback: the level-0 role only, on top of the `feedback` send permission.
|
|
169
|
+
const topOnly = async (request, reply) => {
|
|
170
|
+
if (!isTopLevel(request.user)) return reply.code(403).send({error: 'Only the most senior role on this server can send feedback from here.'});
|
|
171
|
+
};
|
|
172
|
+
const own = {preHandler: [authenticate, requirePermission('feedback', 'send'), topOnly]};
|
|
173
|
+
const ownSeen = createOutbox(options.dataDir || path.join(HERE, 'data'), 'own.json');
|
|
159
174
|
const allowed = createThrottle({max: Number(settings.perSiteMax) || 10, windowMs: (Number(settings.perSiteMinutes) || 10) * 60_000});
|
|
160
175
|
try { await rx.ensureCollection(); } catch (err) { fastify.log.error(`[feedback] collection: ${err.message}`); }
|
|
161
176
|
|
|
@@ -195,20 +210,49 @@ async function receiverRoutes(fastify, {can, hooks, notifyOk, receiverMod, setti
|
|
|
195
210
|
|
|
196
211
|
// ---- The inbox ----
|
|
197
212
|
|
|
198
|
-
fastify.get('/mode', can('manage'), async () => ({mode: 'receiver', waypoint: Boolean(waypoint())}));
|
|
213
|
+
fastify.get('/mode', can('manage'), async (request) => ({mode: 'receiver', waypoint: Boolean(waypoint()), canSend: isTopLevel(request.user)}));
|
|
199
214
|
|
|
200
|
-
fastify.get('/count', can('manage'), async () => {
|
|
201
|
-
const
|
|
215
|
+
fastify.get('/count', can('manage'), async (request) => {
|
|
216
|
+
const all = await rx.allReports();
|
|
217
|
+
const fresh = all.filter(e => e.data?.status === 'new');
|
|
202
218
|
const n = names();
|
|
219
|
+
const seen = ownSeen.seen();
|
|
220
|
+
const replies = isTopLevel(request.user)
|
|
221
|
+
? all.filter(e => e.data?.site === rx.MANAGER_SITE && e.data.reply && e.data.replyAt && seen[e.id] !== e.data.replyAt).length : 0;
|
|
203
222
|
return {
|
|
204
223
|
count: fresh.length,
|
|
205
224
|
label: fresh.length ? `${fresh.length} new` : '',
|
|
206
225
|
tone: fresh.some(e => e.data?.severity === 'blocker') ? 'danger' : 'info',
|
|
207
226
|
details: {title: 'New feedback', items: fresh.slice(0, 6).map(e => ({text: e.data.title, meta: n[e.data.site] || e.data.site})),
|
|
227
|
+
...(replies && {rows: [{label: 'New', value: String(fresh.length)}, {label: 'Replies to your own feedback', value: String(replies)}]}),
|
|
208
228
|
empty: 'Nothing new.'}
|
|
209
229
|
};
|
|
210
230
|
});
|
|
211
231
|
|
|
232
|
+
// ---- The manager's own feedback: straight into the inbox, no HTTP round trip ----
|
|
233
|
+
|
|
234
|
+
fastify.post('/send', own, async (request, reply) => {
|
|
235
|
+
const {report, error} = cleanSubmission(request.body || {});
|
|
236
|
+
if (error) return reply.code(400).send({error});
|
|
237
|
+
if (!allowed(rx.MANAGER_SITE)) return reply.code(429).send({error: 'That is a lot of feedback at once - try again in a few minutes.'});
|
|
238
|
+
const payload = buildPayload({report, user: request.user, cmsVersion: cmsVersion(), site: rx.MANAGER_SITE, ref: newRef()});
|
|
239
|
+
const r = await rx.receive(rx.MANAGER_SITE, payload, 'manager');
|
|
240
|
+
if (r.error) return reply.code(400).send({error: r.error});
|
|
241
|
+
return reply.code(201).send({id: r.entry.id, ref: payload.ref, state: 'sent', message: 'Filed in the inbox.'});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
fastify.get('/sent', own, async () => {
|
|
245
|
+
const seen = ownSeen.seen();
|
|
246
|
+
const reports = (await rx.reportsOf(rx.MANAGER_SITE)).map(senderView)
|
|
247
|
+
.map(r => ({...r, local: false, unread: Boolean(r.reply && r.replyAt && seen[r.id] !== r.replyAt)}));
|
|
248
|
+
return {connected: true, reachable: true, error: '', local: true, reports};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
fastify.put('/sent/:id/seen', own, async (request) => {
|
|
252
|
+
ownSeen.markSeen(String(request.params.id), String(request.body?.replyAt || ''));
|
|
253
|
+
return {ok: true};
|
|
254
|
+
});
|
|
255
|
+
|
|
212
256
|
fastify.get('/reports', can('manage'), async () => {
|
|
213
257
|
const n = names();
|
|
214
258
|
return {sites: n, reports: (await rx.allReports()).map(e => ({id: e.id, ...e.data, siteName: n[e.data.site] || e.data.site,
|
|
@@ -235,6 +279,10 @@ async function receiverRoutes(fastify, {can, hooks, notifyOk, receiverMod, setti
|
|
|
235
279
|
const {patch, error} = cleanUpdate(request.body || {}, e.data);
|
|
236
280
|
if (error) return reply.code(400).send({error});
|
|
237
281
|
const next = await rx.applyPatch(e.id, patch, request.user?.name || 'Domma');
|
|
282
|
+
// Answering your own report is not news to you.
|
|
283
|
+
if (next.data.site === rx.MANAGER_SITE && next.data.replyAt && next.data.userId && next.data.userId === String(request.user?.id || '')) {
|
|
284
|
+
ownSeen.markSeen(next.id, next.data.replyAt);
|
|
285
|
+
}
|
|
238
286
|
return {id: next.id, ...next.data, createdAt: next.meta?.createdAt, updatedAt: next.meta?.updatedAt};
|
|
239
287
|
});
|
|
240
288
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedback",
|
|
3
3
|
"displayName": "Feedback",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.2",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Tell Domma what is broken, missing or good - straight from your admin - and see what became of it: seen, planned, in progress, done, with Domma's reply.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"label": "Feedback",
|
|
14
14
|
"group": "Plugins",
|
|
15
15
|
"icon": "message-square",
|
|
16
|
-
"description": "Send feedback to Domma and read the replies (send). On the Domma server itself: read, answer and file everything the sites sent (manage).",
|
|
16
|
+
"description": "Send feedback to Domma and read the replies (send). On the Domma server itself: read, answer and file everything the sites sent (manage); the level-0 role can also file its own there.",
|
|
17
17
|
"actions": [
|
|
18
|
-
{"key": "send", "label": "Send", "description": "Send feedback to Domma and see what this site has sent"},
|
|
18
|
+
{"key": "send", "label": "Send", "description": "Send feedback to Domma and see what this site has sent (on the Domma server: the level-0 role only)"},
|
|
19
19
|
{"key": "manage", "label": "Manage", "description": "On the Domma server: the inbox of every site's feedback"}
|
|
20
20
|
],
|
|
21
21
|
"grant": ["admin"]
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"views": {
|
|
42
42
|
"plugin-feedback": {
|
|
43
|
-
"entry": "feedback/admin/views/feedback.js?v=1.
|
|
43
|
+
"entry": "feedback/admin/views/feedback.js?v=1.1.1",
|
|
44
44
|
"exportName": "feedbackView"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -11,6 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import Fastify from 'fastify';
|
|
13
13
|
import feedback from '../plugin.js';
|
|
14
|
+
import {getRoleHierarchy, getRoleLevel, load as loadRoles} from '../../../server/services/roles.js';
|
|
14
15
|
|
|
15
16
|
const PLUGINS = path.resolve('config/plugins.json');
|
|
16
17
|
const COLL = path.resolve('content/collections/feedback-reports');
|
|
@@ -21,6 +22,9 @@ let dataDir;
|
|
|
21
22
|
let managerUp = true;
|
|
22
23
|
const notices = [];
|
|
23
24
|
const siteNotices = [];
|
|
25
|
+
// Roles are data: the level-0 role and one below it, whatever this checkout calls them.
|
|
26
|
+
let TOP;
|
|
27
|
+
let BELOW;
|
|
24
28
|
|
|
25
29
|
/** A site's fetch: into the manager app, or a connection failure while it is "restarting". */
|
|
26
30
|
const fetchInto = (app) => async (url, init = {}) => {
|
|
@@ -53,10 +57,15 @@ before(async () => {
|
|
|
53
57
|
savedPlugins = await fs.readFile(PLUGINS, 'utf8').catch(() => null);
|
|
54
58
|
await fs.rm(COLL, {recursive: true, force: true});
|
|
55
59
|
dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'feedback-test-'));
|
|
60
|
+
await loadRoles();
|
|
61
|
+
TOP = getRoleHierarchy().find(r => getRoleLevel(r) === 0);
|
|
62
|
+
BELOW = getRoleHierarchy().find(r => getRoleLevel(r) > 0);
|
|
56
63
|
manager = Fastify();
|
|
57
64
|
await manager.register(feedback, {
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
// x-test-user swaps the signed-in user for one request; otherwise the vendor at level 0.
|
|
66
|
+
auth: {authenticate: async (req) => { req.user = req.headers['x-test-user'] ? JSON.parse(req.headers['x-test-user']) : {id: 'v1', name: 'Vendor', role: TOP}; },
|
|
67
|
+
requirePermission: () => async () => {}},
|
|
68
|
+
prefix: '/api/plugins/feedback', dataDir: await fs.mkdtemp(path.join(dataDir, 'manager-')),
|
|
60
69
|
settings: {perSiteMax: 6, perSiteMinutes: 10}, mode: 'receiver',
|
|
61
70
|
slugFromToken: async (h) => TOKENS[String(h || '').replace(/^Bearer\s+/i, '')] || null,
|
|
62
71
|
siteNames: () => ({acme: 'Acme Ltd', beta: 'Beta Co'}),
|
|
@@ -217,3 +226,66 @@ test('a site the manager did not start cannot send, and says so', async () => {
|
|
|
217
226
|
assert.equal((await call(app, 'POST', '/send', {title: 't', description: 'd'})).status, 409);
|
|
218
227
|
await app.close();
|
|
219
228
|
});
|
|
229
|
+
|
|
230
|
+
test('the manager\'s level-0 role files its own feedback straight into the inbox', async () => {
|
|
231
|
+
assert.ok(TOP && BELOW, 'this checkout has a level-0 role and one below it');
|
|
232
|
+
const P = '/api/plugins/feedback';
|
|
233
|
+
const me = {id: 'v1', name: 'Vendor', email: 'v@domma.test', role: TOP};
|
|
234
|
+
const as = (u) => ({'x-test-user': JSON.stringify(u)});
|
|
235
|
+
assert.equal((await call(manager, 'GET', `${P}/mode`, null, as(me))).body.canSend, true);
|
|
236
|
+
|
|
237
|
+
let r = await call(manager, 'POST', `${P}/send`, {type: 'bug', title: 'Manager hiccup', description: 'The fleet list flickers.', severity: 'minor',
|
|
238
|
+
user: {name: 'Mallory'}, site: 'acme'}, as(me));
|
|
239
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
240
|
+
assert.equal(r.body.state, 'sent');
|
|
241
|
+
|
|
242
|
+
const inbox = (await call(manager, 'GET', `${P}/reports`)).body;
|
|
243
|
+
const got = inbox.reports.find(x => x.title === 'Manager hiccup');
|
|
244
|
+
assert.equal(got.site, '_manager', 'marked as the manager, whatever the body says');
|
|
245
|
+
assert.equal(got.siteName, 'Manager (this server)');
|
|
246
|
+
assert.equal(got.userName, 'Vendor', 'the session names the sender');
|
|
247
|
+
assert.equal(got.status, 'new');
|
|
248
|
+
assert.equal(inbox.sites._manager, 'Manager (this server)', 'the site filter offers it');
|
|
249
|
+
assert.ok(!notices.some(n => n.title.includes('Manager hiccup')), 'no notice to yourself');
|
|
250
|
+
|
|
251
|
+
// Below level 0: refused, and not offered.
|
|
252
|
+
const admin = {id: 'a1', name: 'Ada', role: BELOW};
|
|
253
|
+
assert.equal((await call(manager, 'GET', `${P}/mode`, null, as(admin))).body.canSend, false);
|
|
254
|
+
assert.equal((await call(manager, 'POST', `${P}/send`, {title: 't', description: 'd'}, as(admin))).status, 403);
|
|
255
|
+
assert.equal((await call(manager, 'GET', `${P}/sent`, null, as(admin))).status, 403);
|
|
256
|
+
assert.equal((await call(manager, 'PUT', `${P}/sent/${got.id}/seen`, {replyAt: 'x'}, as(admin))).status, 403);
|
|
257
|
+
assert.equal((await call(manager, 'POST', `${P}/send`, {title: 't'}, as(me))).status, 400);
|
|
258
|
+
|
|
259
|
+
// Its own list: only the manager's reports, sender-safe fields, same as a site sees.
|
|
260
|
+
let mine = (await call(manager, 'GET', `${P}/sent`, null, as(me))).body;
|
|
261
|
+
assert.equal(mine.connected, true);
|
|
262
|
+
assert.deepEqual(mine.reports.map(x => x.title), ['Manager hiccup']);
|
|
263
|
+
for (const k of ['notes', 'userEmail', 'site', 'history']) assert.equal(mine.reports[0][k], undefined, `${k} stays in the inbox`);
|
|
264
|
+
|
|
265
|
+
// The same reply flow: another level-0 holder answers; it shows unread until opened.
|
|
266
|
+
r = await call(manager, 'PUT', `${P}/reports/${got.id}`, {status: 'planned', reply: 'Next release.', notes: 'css'},
|
|
267
|
+
as({id: 'v2', name: 'Other', role: TOP}));
|
|
268
|
+
assert.equal(r.status, 200);
|
|
269
|
+
mine = (await call(manager, 'GET', `${P}/sent`, null, as(me))).body.reports[0];
|
|
270
|
+
assert.equal(mine.statusLabel, 'Planned');
|
|
271
|
+
assert.equal(mine.reply, 'Next release.');
|
|
272
|
+
assert.equal(mine.unread, true);
|
|
273
|
+
assert.equal((await call(manager, 'GET', `${P}/count`, null, as(me))).body.details.rows[1].value, '1');
|
|
274
|
+
await call(manager, 'PUT', `${P}/sent/${mine.id}/seen`, {replyAt: mine.replyAt}, as(me));
|
|
275
|
+
assert.equal((await call(manager, 'GET', `${P}/sent`, null, as(me))).body.reports[0].unread, false);
|
|
276
|
+
|
|
277
|
+
// Replying to your own report is not news to you.
|
|
278
|
+
await call(manager, 'PUT', `${P}/reports/${got.id}`, {reply: 'Done it myself.'}, as(me));
|
|
279
|
+
assert.equal((await call(manager, 'GET', `${P}/sent`, null, as(me))).body.reports[0].unread, false);
|
|
280
|
+
|
|
281
|
+
// And Waypoint: the same promote path.
|
|
282
|
+
globalThis.dommaWaypoint = {version: 1, listProjects: async () => [], getIssue: async () => null,
|
|
283
|
+
createIssue: async (input) => ({id: 'i9', key: 'CMS-99', made: input})};
|
|
284
|
+
r = await call(manager, 'POST', `${P}/reports/${got.id}/promote`, {projectId: 'p1', type: 'bug'});
|
|
285
|
+
delete globalThis.dommaWaypoint;
|
|
286
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
287
|
+
assert.equal(r.body.issueKey, 'CMS-99');
|
|
288
|
+
|
|
289
|
+
// No site token can speak as the manager.
|
|
290
|
+
assert.equal((await call(manager, 'GET', `${P}/fleet/mine`, null, {authorization: 'Bearer _manager'})).status, 401);
|
|
291
|
+
});
|