domma-cms 1.6.55 → 1.6.57
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/admin/css/calculator.css +1 -1
- package/admin/js/lib/analytics-shape.js +1 -1
- package/admin/js/lib/calculator-engine.js +1 -1
- package/admin/js/lib/calculator.js +22 -10
- package/admin/js/lib/form-editor-arrange.js +1 -1
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/notes-arrange.js +1 -1
- package/admin/js/lib/search-settings-arrange.js +1 -1
- package/admin/js/lib/seo-arrange.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -1
- package/admin/js/lib/tool-kit.js +1 -1
- package/admin/js/lib/tool-recovery.js +4 -4
- package/admin/js/templates/analytics.html +26 -14
- package/admin/js/templates/contacts.html +26 -5
- package/admin/js/templates/form-editor.html +2 -0
- package/admin/js/templates/notes.html +11 -8
- package/admin/js/templates/search.html +9 -3
- package/admin/js/templates/seo.html +23 -17
- package/admin/js/templates/todo.html +18 -14
- package/admin/js/views/analytics.js +10 -10
- package/admin/js/views/contacts.js +47 -47
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/forms.js +5 -5
- package/admin/js/views/notes.js +6 -6
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +43 -43
- package/admin/js/views/todo.js +8 -8
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +4 -1
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-kit.js +364 -0
- package/plugins/_lib/admin/mail/reader-view.js +1009 -161
- package/plugins/_lib/admin/mail/when-closed.js +33 -3
- package/plugins/_lib/mail/readRoutes.js +48 -10
- package/plugins/_lib/mail/unreadBadge.js +4 -1
- package/plugins/blog/CLAUDE.md +14 -0
- package/plugins/blog/admin/css/index.css +5 -0
- package/plugins/blog/admin/lib/drafts.js +95 -0
- package/plugins/blog/admin/templates/blog.html +1 -1
- package/plugins/blog/admin/templates/categories.html +1 -1
- package/plugins/blog/admin/templates/comments.html +1 -1
- package/plugins/blog/admin/templates/post-editor.html +5 -1
- package/plugins/blog/admin/views/blog.js +44 -19
- package/plugins/blog/admin/views/categories.js +138 -20
- package/plugins/blog/admin/views/comments.js +32 -23
- package/plugins/blog/admin/views/kit.js +232 -22
- package/plugins/blog/admin/views/post-editor.js +348 -178
- package/plugins/blog/admin/views/settings.js +17 -3
- package/plugins/blog/plugin.js +1 -1
- package/plugins/blog/plugin.json +7 -7
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/drafts.test.js +50 -0
- package/plugins/booking/CLAUDE.md +13 -0
- package/plugins/booking/admin/lib/drafts.js +139 -0
- package/plugins/booking/admin/lib/kit.js +246 -25
- package/plugins/booking/admin/views/booking.js +11 -2
- package/plugins/booking/admin/views/bookings.js +7 -2
- package/plugins/booking/admin/views/diary.js +13 -6
- package/plugins/booking/admin/views/panels.js +85 -12
- package/plugins/booking/admin/views/people.js +41 -2
- package/plugins/booking/admin/views/services.js +45 -3
- package/plugins/booking/admin/views/settings.js +22 -2
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/tests/admin-drafts.test.js +67 -0
- package/plugins/feedback/CLAUDE.md +12 -0
- package/plugins/feedback/admin/css/index.css +5 -0
- package/plugins/feedback/admin/lib/drafts.js +145 -0
- package/plugins/feedback/admin/lib/kit.js +161 -5
- package/plugins/feedback/admin/lib/settings.js +46 -28
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/admin/views/inbox.js +110 -46
- package/plugins/feedback/admin/views/send.js +174 -83
- package/plugins/feedback/plugin.json +3 -3
- package/plugins/feedback/tests/drafts.test.js +93 -0
- package/plugins/free-tier.lock.json +75 -63
- package/plugins/security/CLAUDE.md +9 -0
- package/plugins/security/admin/lib/screen-state.js +120 -0
- package/plugins/security/admin/templates/security.html +15 -10
- package/plugins/security/admin/views/security.js +270 -132
- package/plugins/security/plugin.json +2 -2
- package/plugins/security/tests/screen-state.test.js +69 -0
- package/plugins/shopping-cart/CLAUDE.md +13 -0
- package/plugins/shopping-cart/admin/css/index.css +21 -5
- package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
- package/plugins/shopping-cart/admin/lib/kit.js +136 -28
- package/plugins/shopping-cart/admin/views/orders.js +117 -36
- package/plugins/shopping-cart/admin/views/overview.js +38 -8
- package/plugins/shopping-cart/admin/views/products.js +317 -68
- package/plugins/shopping-cart/admin/views/sales.js +21 -3
- package/plugins/shopping-cart/admin/views/settings.js +62 -3
- package/plugins/shopping-cart/admin/views/shop.js +18 -3
- package/plugins/shopping-cart/plugin.json +3 -3
- package/plugins/shopping-cart/tests/accounts.test.js +11 -0
- package/plugins/shopping-cart/tests/drafts.test.js +118 -0
- package/plugins/support/CLAUDE.md +14 -0
- package/plugins/support/admin/css/index.css +10 -0
- package/plugins/support/admin/lib/drafts.js +162 -0
- package/plugins/support/admin/lib/kit.js +156 -16
- package/plugins/support/admin/lib/settings.js +52 -29
- package/plugins/support/admin/views/desk.js +114 -33
- package/plugins/support/admin/views/inbox.js +85 -26
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/plugin.json +3 -3
- package/plugins/support/tests/drafts.test.js +92 -0
- package/public/js/site.js +1 -1
- package/server/routes/api/analytics.js +5 -1
- package/server/routes/api/contacts.js +42 -0
- package/server/routes/api/forms.js +8 -0
- package/server/routes/api/seo.js +4 -1
- package/server/routes/api/todo.js +50 -10
- package/server/server.js +1 -1
- package/server/templates/page.html +1 -1
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* Right-click (or the row's kebab, or Shift+F10) for the quick moves.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {FILES, api, esc, openPanel, refreshBadge, showFiles, stamp, store, when} from '../lib/kit.js';
|
|
11
|
+
import {FILES, api, copyText, esc, guardPanel, offerDraft, openPanel, panelSet, refreshBadge, rowKeys, showFiles, stamp, store, when} from '../lib/kit.js';
|
|
12
|
+
import {DRAFT_KEYS, draftQuestion, draftText, draftValues, reportForm, reportPrint} from '../lib/drafts.js';
|
|
12
13
|
|
|
13
14
|
const ROW = `
|
|
14
|
-
<div class="fb-row" data-id="{{id}}" tabindex="0" role="button">
|
|
15
|
+
<div class="fb-row" data-id="{{id}}" tabindex="0" role="button" aria-haspopup="menu">
|
|
15
16
|
<span class="fb-type" data-bind-title="typeLabel"><span data-icon="{{icon}}" data-icon-size="16"></span></span>
|
|
16
17
|
<span class="fb-main"><strong data-bind-text="title"></strong><small data-bind-text="meta"></small></span>
|
|
17
18
|
<span class="fb-key" data-bind-hidden="!issueKey" data-bind-text="issueKey"></span>
|
|
@@ -32,6 +33,8 @@ export async function mount(root, ctx) {
|
|
|
32
33
|
const statusOf = (s) => statuses.find(x => x.key === s)?.label || s;
|
|
33
34
|
|
|
34
35
|
const reports = M.observable([]);
|
|
36
|
+
const panels = panelSet();
|
|
37
|
+
scope(() => panels.leave());
|
|
35
38
|
const sites = M.observable({});
|
|
36
39
|
const loading = M.observable(true);
|
|
37
40
|
const error = M.observable('');
|
|
@@ -82,7 +85,7 @@ export async function mount(root, ctx) {
|
|
|
82
85
|
view.innerHTML = `
|
|
83
86
|
<div class="fb-bar">
|
|
84
87
|
<div class="fb-seg" role="group" aria-label="Show">
|
|
85
|
-
${FILTERS.map(f => `<button type="button" data-filter="${esc(f.key)}" data-bind-class="filter.value === '${esc(f.key)}' && 'is-on'">${esc(f.label)} <b data-bind-text="counts.value['${esc(f.key)}']"></b></button>`).join('')}
|
|
88
|
+
${FILTERS.map(f => `<button type="button" data-filter="${esc(f.key)}" data-bind-class="filter.value === '${esc(f.key)}' && 'is-on'" data-bind-aria-pressed="filter.value === '${esc(f.key)}' ? 'true' : 'false'">${esc(f.label)} <b data-bind-text="counts.value['${esc(f.key)}']"></b></button>`).join('')}
|
|
86
89
|
</div>
|
|
87
90
|
<span data-help-title="Feedback inbox" data-help="Everything the sites' admins sent. Open a report to set its status, reply (the site sees the reply and the status, never the internal notes) or make a Waypoint issue from it - once linked, the report follows the issue: planned, in progress, done. Right-click a row for quick moves."></span>
|
|
88
91
|
</div>
|
|
@@ -95,8 +98,9 @@ export async function mount(root, ctx) {
|
|
|
95
98
|
${types.map(t => `<option value="${esc(t.key)}">${esc(t.short)}</option>`).join('')}
|
|
96
99
|
</select>
|
|
97
100
|
<input type="search" class="form-input fb-search" placeholder="Search title, text, person, site..." aria-label="Search feedback" data-model="query.value">
|
|
101
|
+
<span data-help-title="Narrowing the list" data-help="The chips above pick a status (Open is everything not Done or Not planned); these narrow it further to one site or one kind of report. The search looks in the title, the text, who sent it, the site and the Waypoint key. The chip counts are for every site and type."></span>
|
|
98
102
|
</div>
|
|
99
|
-
<
|
|
103
|
+
<div class="fb-loading dm-loading-slot" data-bind-hidden="!loading.value">Loading...</div>
|
|
100
104
|
<p class="fb-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
|
|
101
105
|
<p class="fb-note" data-bind-hidden="loading.value || error.value || rows.value.length" data-bind-text="emptyText.value"></p>
|
|
102
106
|
<div class="fb-list" data-each="rows.value key=id"></div>`;
|
|
@@ -129,23 +133,29 @@ export async function mount(root, ctx) {
|
|
|
129
133
|
|
|
130
134
|
async function load() {
|
|
131
135
|
try {
|
|
132
|
-
const res = await api.reports();
|
|
136
|
+
const res = await kit.whileLoading(view.querySelector('.fb-loading'), api.reports(), kit.SHAPES.iconRows);
|
|
133
137
|
if (!here()) return;
|
|
134
138
|
sites.value = res.sites || {};
|
|
135
139
|
reports.value = Array.isArray(res.reports) ? res.reports : [];
|
|
136
140
|
error.value = '';
|
|
137
141
|
} catch (err) {
|
|
138
|
-
error.value = `Could not load the feedback: ${err.message || err}`;
|
|
142
|
+
if (here()) error.value = `Could not load the feedback: ${err.message || err}`;
|
|
139
143
|
} finally {
|
|
140
|
-
loading.value = false;
|
|
141
|
-
I.scan(view);
|
|
144
|
+
if (here()) { loading.value = false; I.scan(view); }
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
|
-
|
|
147
|
+
/** A report changed: the list, and the open panel when it shows that report. */
|
|
148
|
+
const replace = (r) => {
|
|
149
|
+
reports.value = reports.peek().map(x => (x.id === r.id ? {...x, ...r} : x));
|
|
150
|
+
const showing = panels.get('detail');
|
|
151
|
+
if (showing?.key === r.id) showing.changed?.(r);
|
|
152
|
+
};
|
|
145
153
|
|
|
146
154
|
async function setStatus(r, status) {
|
|
147
155
|
try {
|
|
148
|
-
|
|
156
|
+
const next = await api.update(r.id, {status});
|
|
157
|
+
if (!here()) return;
|
|
158
|
+
replace(next);
|
|
149
159
|
refreshBadge();
|
|
150
160
|
E.toast(`${statusOf(status)}.`, {type: 'success'});
|
|
151
161
|
} catch (err) {
|
|
@@ -157,6 +167,11 @@ export async function mount(root, ctx) {
|
|
|
157
167
|
if (!await E.confirm(`Delete "${esc(r.title)}"? The site will no longer see it either. This cannot be undone.`)) return;
|
|
158
168
|
try {
|
|
159
169
|
await api.remove(r.id);
|
|
170
|
+
// Deleted on purpose: its panel goes without asking, and so does any draft on it.
|
|
171
|
+
const showing = panels.get('detail');
|
|
172
|
+
if (showing?.key === r.id) showing.close?.(true);
|
|
173
|
+
store.remove(DRAFT_KEYS.report(r.id));
|
|
174
|
+
if (!here()) return;
|
|
160
175
|
reports.value = reports.peek().filter(x => x.id !== r.id);
|
|
161
176
|
refreshBadge();
|
|
162
177
|
E.toast('Deleted.', {type: 'success'});
|
|
@@ -165,15 +180,50 @@ export async function mount(root, ctx) {
|
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
|
|
183
|
+
/** One report open at a time: opening another closes (and may ask about) the one showing. */
|
|
168
184
|
async function open(reportId) {
|
|
185
|
+
const showing = panels.get('detail');
|
|
186
|
+
if (showing?.key === reportId) return;
|
|
187
|
+
if (showing) { await showing.close?.(); if (panels.get('detail')) return; }
|
|
188
|
+
if (!here() || !panels.claim('detail')) return;
|
|
189
|
+
let opened = false;
|
|
190
|
+
try { opened = await openReport(reportId); } catch (err) { E.toast(err.message || String(err), {type: 'error'}); }
|
|
191
|
+
if (!opened) panels.release('detail');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function openReport(reportId) {
|
|
195
|
+
const key = DRAFT_KEYS.report(reportId);
|
|
169
196
|
let r;
|
|
170
|
-
try { r = await api.report(reportId); } catch (err) {
|
|
197
|
+
try { r = await api.report(reportId); } catch (err) {
|
|
198
|
+
if (!here()) return false;
|
|
199
|
+
// Gone - and with unsaved work on it: say so, and offer the words back.
|
|
200
|
+
if (err?.status === 404 || /not found|no such/i.test(err?.message || '')) {
|
|
201
|
+
const offer = await offerDraft(key, {here, question: (d) => draftQuestion(d, null, {what: 'a report'})});
|
|
202
|
+
if (offer.state === 'keep' && await copyText(draftText(offer.draft), 'Your unsaved reply and notes are copied')) store.remove(key);
|
|
203
|
+
if (offer.state === 'none') E.toast('That report is no longer here.', {type: 'warning'});
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
E.toast(err.message || String(err), {type: 'error'});
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (!here()) return false;
|
|
171
210
|
replace(r);
|
|
211
|
+
// Opening a new report is seeing it - before the draft question, so the question and
|
|
212
|
+
// the form both see the status the site will.
|
|
213
|
+
if (r.status === 'new') {
|
|
214
|
+
try { r = await api.update(r.id, {status: 'acknowledged'}); if (here()) { replace(r); refreshBadge(); } } catch { /* stays new */ }
|
|
215
|
+
if (!here()) return false;
|
|
216
|
+
}
|
|
217
|
+
const offer = await offerDraft(key, {here, question: (d) => draftQuestion(d, reportPrint(r), {what: `the report “${r.title}”`, changedAt: r.updatedAt})});
|
|
218
|
+
if (offer.state === 'gone') return false;
|
|
172
219
|
let wp = {available: false, projects: []};
|
|
173
220
|
if (!r.issueId) { try { wp = await api.waypoint(); } catch { /* no Waypoint */ } }
|
|
221
|
+
if (!here()) return false;
|
|
174
222
|
|
|
223
|
+
const saved = reportForm(r);
|
|
224
|
+
const start = offer.state === 'keep' ? draftValues(offer.draft, saved) : saved;
|
|
175
225
|
const f = {
|
|
176
|
-
status: M.observable(
|
|
226
|
+
status: M.observable(start.status), reply: M.observable(start.reply), notes: M.observable(start.notes),
|
|
177
227
|
busy: M.observable(false),
|
|
178
228
|
project: M.observable(wp.projects?.[0]?.id || ''),
|
|
179
229
|
itype: M.observable(r.type === 'bug' ? 'bug' : r.type === 'feature' ? 'story' : 'task'),
|
|
@@ -189,13 +239,14 @@ export async function mount(root, ctx) {
|
|
|
189
239
|
...(r.where ? [['Where', r.where]] : []),
|
|
190
240
|
...(r.cmsVersion ? [['CMS', r.cmsVersion]] : [])
|
|
191
241
|
];
|
|
192
|
-
let
|
|
242
|
+
let guard = null;
|
|
193
243
|
let filesView = null;
|
|
194
|
-
|
|
244
|
+
let base = saved;
|
|
245
|
+
const panel = await openPanel({
|
|
195
246
|
title: r.title,
|
|
196
247
|
size: 'lg',
|
|
197
248
|
model: f,
|
|
198
|
-
cleanup: () => filesView?.dispose(),
|
|
249
|
+
cleanup: () => { filesView?.dispose(); panels.release('detail'); },
|
|
199
250
|
html: `<div class="fb-detail">
|
|
200
251
|
<dl class="fb-facts">${facts.map(([a, b]) => `<div><dt>${esc(a)}</dt><dd>${esc(b)}</dd></div>`).join('')}</dl>
|
|
201
252
|
<h4>What they sent</h4>
|
|
@@ -207,11 +258,12 @@ export async function mount(root, ctx) {
|
|
|
207
258
|
<select id="fb-status" class="form-select" data-model="status.value">
|
|
208
259
|
${statuses.map(s => `<option value="${esc(s.key)}">${esc(s.label)}</option>`).join('')}
|
|
209
260
|
</select>
|
|
210
|
-
<label class="form-label" for="fb-reply">Reply to the site <span data-help="The site's admins see this in their Feedback screen, and are told there is a reply."></span></label>
|
|
261
|
+
<label class="form-label" for="fb-reply">Reply to the site <span data-help="The site's admins see this in their Feedback screen, and are told there is a reply. Unsaved typing is kept if you close the browser or leave this screen, and offered back next time you open the report."></span></label>
|
|
211
262
|
<textarea id="fb-reply" class="form-textarea" rows="5" maxlength="8000" data-model="reply.value"></textarea>
|
|
212
263
|
<label class="form-label" for="fb-notes">Internal notes <span data-help="Only here. Never sent to the site."></span></label>
|
|
213
264
|
<textarea id="fb-notes" class="form-textarea" rows="3" maxlength="8000" data-model="notes.value"></textarea>
|
|
214
265
|
<div class="fb-actions"><span class="fb-spacer"></span>
|
|
266
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
215
267
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="busy.value"><span data-icon="check" data-icon-size="13"></span> Save</button>
|
|
216
268
|
</div>
|
|
217
269
|
</div>
|
|
@@ -236,26 +288,34 @@ export async function mount(root, ctx) {
|
|
|
236
288
|
<ul class="fb-history">${(r.history || []).slice().reverse().map(h => `<li><span>${esc(h.what)}</span><small>${esc(h.by || '')} · ${esc(stamp(h.at))}</small></li>`).join('')}</ul>
|
|
237
289
|
</div>`,
|
|
238
290
|
methods: {
|
|
291
|
+
cancel: () => guard?.close(),
|
|
239
292
|
async save() {
|
|
240
293
|
if (f.busy.peek()) return;
|
|
241
294
|
f.busy.value = true;
|
|
295
|
+
// The panel closes once saved: locked meanwhile, so nothing typed during the request is lost.
|
|
296
|
+
guard?.lock(true);
|
|
242
297
|
try {
|
|
243
298
|
const next = await api.update(r.id, {status: f.status.peek(), reply: f.reply.peek(), notes: f.notes.peek()});
|
|
299
|
+
guard?.settle();
|
|
300
|
+
guard?.close(true);
|
|
301
|
+
if (!here()) return;
|
|
244
302
|
replace(next);
|
|
245
303
|
refreshBadge();
|
|
246
304
|
E.toast('Saved.', {type: 'success'});
|
|
247
|
-
panel?.close();
|
|
248
305
|
} catch (err) {
|
|
306
|
+
guard?.lock(false);
|
|
249
307
|
E.toast(err.message || String(err), {type: 'error'});
|
|
250
308
|
} finally {
|
|
251
309
|
f.busy.value = false;
|
|
252
310
|
}
|
|
253
311
|
},
|
|
254
|
-
openIssue(e) {
|
|
312
|
+
async openIssue(e) {
|
|
255
313
|
e?.preventDefault?.();
|
|
256
314
|
const key = f.issueKey.peek();
|
|
257
315
|
if (!key) return;
|
|
258
|
-
|
|
316
|
+
// Unsaved typing asks first; Keep editing stays here.
|
|
317
|
+
await guard?.close();
|
|
318
|
+
if (panels.get('detail')?.key === r.id) return;
|
|
259
319
|
location.hash = `#/plugins/waypoint-pro/i/${encodeURIComponent(key)}`;
|
|
260
320
|
},
|
|
261
321
|
async promote() {
|
|
@@ -263,9 +323,8 @@ export async function mount(root, ctx) {
|
|
|
263
323
|
f.busy.value = true;
|
|
264
324
|
try {
|
|
265
325
|
const next = await api.promote(r.id, {projectId: f.project.peek(), type: f.itype.peek(), priority: f.priority.peek()});
|
|
326
|
+
if (!here()) return;
|
|
266
327
|
replace(next);
|
|
267
|
-
f.issueKey.value = next.issueKey || '';
|
|
268
|
-
f.status.value = next.status;
|
|
269
328
|
refreshBadge();
|
|
270
329
|
E.toast(`Filed as ${next.issueKey || 'a Waypoint issue'}.`, {type: 'success'});
|
|
271
330
|
} catch (err) {
|
|
@@ -276,24 +335,40 @@ export async function mount(root, ctx) {
|
|
|
276
335
|
}
|
|
277
336
|
}
|
|
278
337
|
});
|
|
338
|
+
guard = guardPanel(panel, {
|
|
339
|
+
key,
|
|
340
|
+
values: () => ({status: f.status.value, reply: f.reply.value, notes: f.notes.value}),
|
|
341
|
+
baseline: saved,
|
|
342
|
+
// A picked-up draft keeps its own `was`: the question next time still measures from when the edits began.
|
|
343
|
+
was: offer.state === 'keep' ? offer.draft.was : reportPrint(r)
|
|
344
|
+
});
|
|
345
|
+
panels.set('detail', {
|
|
346
|
+
key: r.id,
|
|
347
|
+
close: (how) => guard.close(how),
|
|
348
|
+
// The report moved elsewhere (a row-menu status, a Waypoint issue): what was not edited here follows it.
|
|
349
|
+
changed(next) {
|
|
350
|
+
const now = reportForm(next);
|
|
351
|
+
for (const k of Object.keys(now)) if (f[k].peek() === base[k]) f[k].value = now[k];
|
|
352
|
+
base = now;
|
|
353
|
+
f.issueKey.value = next.issueKey || '';
|
|
354
|
+
f.issueStatus.value = next.issueStatus || '';
|
|
355
|
+
guard.rebase(now, reportPrint(next));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
279
358
|
const host = panel.panel.querySelector('[data-files]');
|
|
280
359
|
if (host) {
|
|
281
360
|
host.innerHTML = FILES;
|
|
282
361
|
filesView = showFiles(host, r.attachments, (a) => api.reportFileUrl(r.id, a.id), 'on its way from the site');
|
|
283
362
|
}
|
|
284
|
-
|
|
285
|
-
if (r.status === 'new') {
|
|
286
|
-
try { const next = await api.update(r.id, {status: 'acknowledged'}); replace(next); f.status.value = next.status; refreshBadge(); }
|
|
287
|
-
catch { /* stays new */ }
|
|
288
|
-
}
|
|
363
|
+
return true;
|
|
289
364
|
}
|
|
290
365
|
|
|
291
366
|
const bindings = M.applyBindings({loading, error, filter, site, type, query, rows, counts, emptyText}, view);
|
|
292
367
|
scope(() => bindings.dispose());
|
|
293
|
-
view.querySelector('.fb-seg')
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
368
|
+
const seg = view.querySelector('.fb-seg');
|
|
369
|
+
const onFilter = (e) => { const b = e.target.closest('[data-filter]'); if (b) filter.value = b.dataset.filter; };
|
|
370
|
+
seg.addEventListener('click', onFilter);
|
|
371
|
+
scope(() => seg.removeEventListener('click', onFilter));
|
|
297
372
|
|
|
298
373
|
// Rows: click or Enter opens; right-click, the kebab or Shift+F10 = the menu.
|
|
299
374
|
const list = view.querySelector('.fb-list');
|
|
@@ -308,26 +383,15 @@ export async function mount(root, ctx) {
|
|
|
308
383
|
{type: 'divider'},
|
|
309
384
|
...statuses.filter(s => s.key !== r.status).map(s => ({label: `Mark ${s.label.toLowerCase()}`, icon: 'flag', action: () => setStatus(r, s.key)})),
|
|
310
385
|
{type: 'divider'},
|
|
386
|
+
...(r.userEmail ? [{label: 'Copy sender\'s email', icon: 'copy', action: () => copyText(r.userEmail, 'Email copied')}] : []),
|
|
387
|
+
...(r.issueKey ? [{label: `Open ${r.issueKey} in Waypoint`, icon: 'external-link', action: () => { location.hash = `#/plugins/waypoint-pro/i/${encodeURIComponent(r.issueKey)}`; }}] : []),
|
|
388
|
+
...(r.userEmail || r.issueKey ? [{type: 'divider'}] : []),
|
|
311
389
|
{label: 'Delete', icon: 'trash', danger: true, action: () => remove(r)}
|
|
312
390
|
];
|
|
313
391
|
}
|
|
314
392
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!row) return;
|
|
318
|
-
const kebab = e.target.closest('.fb-kebab');
|
|
319
|
-
if (kebab) { kit.openMenuAt(kebab); return; }
|
|
320
|
-
open(row.dataset.id);
|
|
321
|
-
};
|
|
322
|
-
const onKey = (e) => {
|
|
323
|
-
const row = e.target.closest('.fb-row');
|
|
324
|
-
if (!row) return;
|
|
325
|
-
if (e.key === 'F10' && e.shiftKey) { e.preventDefault(); kit.openMenuAt(row); }
|
|
326
|
-
else if (e.key === 'Enter' && e.target === row) open(row.dataset.id);
|
|
327
|
-
};
|
|
328
|
-
list.addEventListener('click', onClick);
|
|
329
|
-
list.addEventListener('keydown', onKey);
|
|
330
|
-
scope(() => { list.removeEventListener('click', onClick); list.removeEventListener('keydown', onKey); menu?.destroy?.(); });
|
|
393
|
+
scope(() => menu?.destroy?.());
|
|
394
|
+
scope(rowKeys(list, {rowSel: '.fb-row', kebabSel: '.fb-kebab', openMenuAt: kit.openMenuAt, primary: open}));
|
|
331
395
|
|
|
332
396
|
await load();
|
|
333
397
|
if (id && here()) open(id);
|