domma-cms 0.66.0 → 0.69.4
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 +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/posts/schema.json +3 -1
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +94 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +310 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/plugin.json +0 -33
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice plugin - opening the printable invoice.
|
|
3
|
-
*
|
|
4
|
-
* `authenticate` is Bearer-only (see server/middleware/auth.js), so a plain
|
|
5
|
-
* <a href target="_blank"> to /invoices/:id/print arrives with no Authorization
|
|
6
|
-
* header and comes back 401 every time. The markup is fetched through H, which
|
|
7
|
-
* the admin's http-interceptor tokenises, and written into a window opened
|
|
8
|
-
* SYNCHRONOUSLY on the click - a window opened after an await counts as
|
|
9
|
-
* unsolicited and the pop-up blocker eats it.
|
|
10
|
-
*/
|
|
11
|
-
const BASE = '/api/plugins/invoice';
|
|
12
|
-
|
|
13
|
-
const PLACEHOLDER = '<!doctype html><html><head><title>Invoice</title></head>' +
|
|
14
|
-
'<body style="font:14px system-ui,sans-serif;padding:2rem;color:#555;">Preparing invoice…</body></html>';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {string} id invoice id
|
|
18
|
-
* @param {boolean} [auto] have the printout raise the print dialog itself
|
|
19
|
-
* @returns {Promise<void>}
|
|
20
|
-
*/
|
|
21
|
-
export async function openPrintWindow(id, auto = false) {
|
|
22
|
-
const win = window.open('', '_blank');
|
|
23
|
-
if (!win) {
|
|
24
|
-
E.toast('Allow pop-ups for this site to print an invoice.', { type: 'error' });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
win.document.write(PLACEHOLDER);
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const res = await H.get(`${BASE}/invoices/${id}/print-html${auto ? '?print=1' : ''}`);
|
|
31
|
-
const html = (res?.data ?? res)?.html;
|
|
32
|
-
if (!html) throw new Error('The server returned an empty invoice.');
|
|
33
|
-
win.document.open();
|
|
34
|
-
win.document.write(html);
|
|
35
|
-
win.document.close();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
win.close();
|
|
38
|
-
E.toast(err?.message ?? 'Could not open the printout.', { type: 'error' });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Does this host have a renderer? Asked once and remembered - the answer cannot
|
|
44
|
-
* change without a server restart, and both views ask on every mount.
|
|
45
|
-
*
|
|
46
|
-
* @returns {Promise<boolean>}
|
|
47
|
-
*/
|
|
48
|
-
let pdfAvailable = null;
|
|
49
|
-
|
|
50
|
-
export async function canDownloadPdf() {
|
|
51
|
-
if (pdfAvailable !== null) return pdfAvailable;
|
|
52
|
-
try {
|
|
53
|
-
const res = await H.get(`${BASE}/pdf/status`);
|
|
54
|
-
pdfAvailable = Boolean((res?.data ?? res)?.available);
|
|
55
|
-
} catch {
|
|
56
|
-
pdfAvailable = false;
|
|
57
|
-
}
|
|
58
|
-
return pdfAvailable;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Download the server-rendered PDF.
|
|
63
|
-
*
|
|
64
|
-
* A download is a NAVIGATION, so it cannot carry the admin's bearer token - the
|
|
65
|
-
* same reason the Print button could not link straight at /print. The server
|
|
66
|
-
* mints a two-minute token bound to this one invoice and puts it in the query
|
|
67
|
-
* string, and an <a download> click is what turns that URL into a file without
|
|
68
|
-
* taking the SPA off its route.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} id
|
|
71
|
-
* @returns {Promise<void>}
|
|
72
|
-
*/
|
|
73
|
-
export async function downloadPdf(id) {
|
|
74
|
-
try {
|
|
75
|
-
const res = await H.post(`${BASE}/invoices/${id}/pdf-link`, {});
|
|
76
|
-
const link = res?.data ?? res;
|
|
77
|
-
if (!link?.url) throw new Error(link?.error || 'No download link was returned.');
|
|
78
|
-
|
|
79
|
-
const a = document.createElement('a');
|
|
80
|
-
a.href = link.url;
|
|
81
|
-
a.download = link.filename || 'invoice.pdf';
|
|
82
|
-
a.rel = 'noopener';
|
|
83
|
-
document.body.appendChild(a);
|
|
84
|
-
a.click();
|
|
85
|
-
a.remove();
|
|
86
|
-
E.toast('Preparing your PDF…', { type: 'info' });
|
|
87
|
-
} catch (err) {
|
|
88
|
-
E.toast(err?.message ?? 'Could not build the PDF.', { type: 'error' });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Download one PDF holding every invoice given, a page each.
|
|
94
|
-
*
|
|
95
|
-
* Same two-minute token dance as the single download, for the same reason - a
|
|
96
|
-
* download is a navigation and carries no bearer token. The ids are POSTed
|
|
97
|
-
* rather than put in the query string: the server keeps the list and the URL
|
|
98
|
-
* carries only a claim ticket, so the link stays short whether it is three
|
|
99
|
-
* invoices or a hundred.
|
|
100
|
-
*
|
|
101
|
-
* The toast is not decoration. Chrome lays out every sheet before the first
|
|
102
|
-
* byte arrives, so a large batch is several seconds during which an <a> click
|
|
103
|
-
* looks exactly like a button that did nothing.
|
|
104
|
-
*
|
|
105
|
-
* @param {string[]} ids
|
|
106
|
-
* @returns {Promise<void>}
|
|
107
|
-
*/
|
|
108
|
-
export async function downloadPdfBatch(ids) {
|
|
109
|
-
if (!ids.length) {
|
|
110
|
-
E.toast('There are no invoices to export.', { type: 'info' });
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
E.toast(`Building a PDF of ${ids.length} invoice${ids.length === 1 ? '' : 's'}…`, { type: 'info' });
|
|
116
|
-
const res = await H.post(`${BASE}/invoices/batch-pdf-link`, { ids });
|
|
117
|
-
const link = res?.data ?? res;
|
|
118
|
-
if (!link?.url) throw new Error(link?.error || 'No download link was returned.');
|
|
119
|
-
|
|
120
|
-
const a = document.createElement('a');
|
|
121
|
-
a.href = link.url;
|
|
122
|
-
a.download = link.filename || 'invoices.pdf';
|
|
123
|
-
a.rel = 'noopener';
|
|
124
|
-
document.body.appendChild(a);
|
|
125
|
-
a.click();
|
|
126
|
-
a.remove();
|
|
127
|
-
} catch (err) {
|
|
128
|
-
E.toast(err?.message ?? 'Could not build the PDF.', { type: 'error' });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice plugin - receivers screen.
|
|
3
|
-
*
|
|
4
|
-
* The same panel the settings slideover puts in its Receivers tab, plus the
|
|
5
|
-
* Contacts import - a receiver is a contact's billing side, and Contacts
|
|
6
|
-
* already holds the company name, the billing address and the VAT number.
|
|
7
|
-
*/
|
|
8
|
-
import {RECEIVER_FIELDS, mountPartyPanel} from './party-view.js?v=8';
|
|
9
|
-
import {billingProfile, contactsAvailable, openContactPicker} from './contacts-source.js?v=8';
|
|
10
|
-
import {disposeOnLeave} from './view-lifecycle.js?v=8';
|
|
11
|
-
|
|
12
|
-
export const receiversView = {
|
|
13
|
-
templateUrl: '/plugins/invoice/admin/templates/receivers.html',
|
|
14
|
-
|
|
15
|
-
async onMount($container) {
|
|
16
|
-
const host = $container.find('#party-host').get(0);
|
|
17
|
-
if (!host) return;
|
|
18
|
-
|
|
19
|
-
const panel = mountPartyPanel(host, {
|
|
20
|
-
kind: 'receiver',
|
|
21
|
-
endpoint: '/api/plugins/invoice/receivers',
|
|
22
|
-
label: 'Receiver',
|
|
23
|
-
fields: RECEIVER_FIELDS
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
disposeOnLeave(host, panel.dispose);
|
|
27
|
-
|
|
28
|
-
if (await contactsAvailable()) {
|
|
29
|
-
const importBtn = document.createElement('button');
|
|
30
|
-
importBtn.type = 'button';
|
|
31
|
-
importBtn.className = 'btn btn-sm btn-outline';
|
|
32
|
-
importBtn.textContent = 'Import from Contacts';
|
|
33
|
-
importBtn.addEventListener('click', () => openContactPicker({
|
|
34
|
-
title: 'Import a receiver',
|
|
35
|
-
onPick: async (contact) => {
|
|
36
|
-
try {
|
|
37
|
-
// The billing profile uses the receiver's own field
|
|
38
|
-
// names, which is the point of that endpoint - it goes
|
|
39
|
-
// straight to POST /receivers with nothing to translate.
|
|
40
|
-
const profile = await billingProfile(contact.id);
|
|
41
|
-
const res = await H.post('/api/plugins/invoice/receivers', profile);
|
|
42
|
-
if (res?.error) throw new Error(res.error);
|
|
43
|
-
E.toast(`${profile.company || profile.name} imported.`, { type: 'success' });
|
|
44
|
-
await panel.reload();
|
|
45
|
-
} catch (err) {
|
|
46
|
-
E.toast(err?.message ?? 'Could not import that contact.', { type: 'error' });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}));
|
|
50
|
-
host.querySelector('.invoice-panel-head')?.prepend(importBtn);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
Domma.icons.scan($container.get(0));
|
|
54
|
-
}
|
|
55
|
-
};
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice plugin - the Send dialog.
|
|
3
|
-
*
|
|
4
|
-
* The defaults (recipient, subject, covering note, whether a PDF can be
|
|
5
|
-
* attached) come from the server rather than being templated here, so the
|
|
6
|
-
* settings that shape the email have one reader.
|
|
7
|
-
*
|
|
8
|
-
* Recipients can also come from Contacts - one contact, or everybody in a
|
|
9
|
-
* group. They are appended to the To field as text rather than held in a
|
|
10
|
-
* parallel list, because the field has to stay typeable: half the time the
|
|
11
|
-
* address someone wants is an accounts inbox that is in nobody's contacts.
|
|
12
|
-
*
|
|
13
|
-
* The form is F.create - schema first, then .renderTo() - and the panel is
|
|
14
|
-
* projected into the slideover's light DOM, because setContent() sanitises and
|
|
15
|
-
* would strip the inputs.
|
|
16
|
-
*/
|
|
17
|
-
import {addressesFor, contactsAvailable, openContactPicker} from './contacts-source.js?v=8';
|
|
18
|
-
|
|
19
|
-
const BASE = '/api/plugins/invoice';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Merge new addresses into a comma-separated To field, without duplicates.
|
|
23
|
-
*
|
|
24
|
-
* @param {string} current
|
|
25
|
-
* @param {string[]} extra
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function mergeRecipients(current, extra) {
|
|
29
|
-
const seen = new Set();
|
|
30
|
-
const out = [];
|
|
31
|
-
for (const item of [...String(current ?? '').split(/[,;]/), ...extra]) {
|
|
32
|
-
const address = String(item ?? '').trim();
|
|
33
|
-
if (!address || seen.has(address.toLowerCase())) continue;
|
|
34
|
-
seen.add(address.toLowerCase());
|
|
35
|
-
out.push(address);
|
|
36
|
-
}
|
|
37
|
-
return out.join(', ');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} id invoice id
|
|
42
|
-
* @param {Function} [onSent] called with the updated invoice
|
|
43
|
-
* @returns {Promise<void>}
|
|
44
|
-
*/
|
|
45
|
-
export async function openSendDialog(id, onSent) {
|
|
46
|
-
let defaults;
|
|
47
|
-
try {
|
|
48
|
-
const res = await H.get(`${BASE}/invoices/${id}/send-defaults`);
|
|
49
|
-
defaults = res?.data ?? res;
|
|
50
|
-
if (defaults?.error) throw new Error(defaults.error);
|
|
51
|
-
} catch (err) {
|
|
52
|
-
E.toast(err?.message ?? 'Could not prepare the email.', { type: 'error' });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const haveContacts = await contactsAvailable();
|
|
57
|
-
|
|
58
|
-
const schema = {
|
|
59
|
-
to: { type: 'string', label: 'To', required: true,
|
|
60
|
-
formConfig: { placeholder: 'name@example.com',
|
|
61
|
-
hint: defaults.to
|
|
62
|
-
? 'Separate several addresses with commas.'
|
|
63
|
-
: 'This receiver has no email address on file.' } },
|
|
64
|
-
subject: { type: 'string', label: 'Subject', required: true },
|
|
65
|
-
message: { type: 'textarea', label: 'Covering note',
|
|
66
|
-
formConfig: { rows: 3, hint: 'Sent above the invoice summary.' } }
|
|
67
|
-
};
|
|
68
|
-
if (defaults.canAttachPdf) {
|
|
69
|
-
schema.attachPdf = { type: 'boolean', label: 'Attach a PDF',
|
|
70
|
-
formConfig: { hint: 'Without it the invoice is sent inline instead.' } };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const form = F.create(schema, {
|
|
74
|
-
to: defaults.to || '',
|
|
75
|
-
subject: defaults.subject || '',
|
|
76
|
-
message: defaults.message || '',
|
|
77
|
-
attachPdf: Boolean(defaults.attachPdf)
|
|
78
|
-
}, { showSubmitButton: false });
|
|
79
|
-
|
|
80
|
-
const wrapper = document.createElement('div');
|
|
81
|
-
wrapper.className = 'invoice-party-form';
|
|
82
|
-
|
|
83
|
-
if (defaults.sendCount) {
|
|
84
|
-
const trail = document.createElement('p');
|
|
85
|
-
trail.className = 'text-muted text-sm mb-0';
|
|
86
|
-
const when = defaults.sentAt ? D(defaults.sentAt).format('DD MMM YYYY HH:mm') : 'previously';
|
|
87
|
-
trail.textContent = `Last sent ${when} (${defaults.sendCount} ${defaults.sendCount === 1 ? 'time' : 'times'}).`;
|
|
88
|
-
wrapper.appendChild(trail);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const fields = document.createElement('div');
|
|
92
|
-
form.renderTo(fields);
|
|
93
|
-
wrapper.appendChild(fields);
|
|
94
|
-
|
|
95
|
-
// Contacts, appended to whatever is already in To. The value goes back
|
|
96
|
-
// through the MODEL as well as the input: Forma reads getData() off the
|
|
97
|
-
// model, so an input written behind its back would send the old address.
|
|
98
|
-
if (haveContacts) {
|
|
99
|
-
const toInput = fields.querySelector('[name="to"]');
|
|
100
|
-
if (toInput) {
|
|
101
|
-
const row = document.createElement('div');
|
|
102
|
-
row.className = 'invoice-recipient-actions';
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* @param {object[]} contacts
|
|
106
|
-
* @returns {void}
|
|
107
|
-
*/
|
|
108
|
-
const add = (contacts) => {
|
|
109
|
-
const found = contacts.flatMap((c) => addressesFor(c).slice(0, 1));
|
|
110
|
-
const missing = contacts.filter((c) => addressesFor(c).length === 0);
|
|
111
|
-
if (!found.length) {
|
|
112
|
-
E.toast('None of those contacts has an email address.', { type: 'error' });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const merged = mergeRecipients(form.getData().to, found);
|
|
116
|
-
form.setData({ to: merged });
|
|
117
|
-
toInput.value = merged;
|
|
118
|
-
if (missing.length) {
|
|
119
|
-
E.toast(`${missing.length} contact(s) had no email address and were skipped.`,
|
|
120
|
-
{ type: 'warning' });
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const one = document.createElement('button');
|
|
125
|
-
one.type = 'button';
|
|
126
|
-
one.className = 'btn btn-sm btn-outline';
|
|
127
|
-
one.textContent = 'Add a contact';
|
|
128
|
-
one.addEventListener('click', () => openContactPicker({
|
|
129
|
-
title: 'Add a recipient',
|
|
130
|
-
onPick: (contact) => add([contact])
|
|
131
|
-
}));
|
|
132
|
-
row.appendChild(one);
|
|
133
|
-
|
|
134
|
-
const many = document.createElement('button');
|
|
135
|
-
many.type = 'button';
|
|
136
|
-
many.className = 'btn btn-sm btn-outline';
|
|
137
|
-
many.textContent = 'Add a group';
|
|
138
|
-
many.title = 'Filter by group, then Select all shown';
|
|
139
|
-
many.addEventListener('click', () => openContactPicker({
|
|
140
|
-
title: 'Add recipients',
|
|
141
|
-
multiple: true,
|
|
142
|
-
onPick: (contacts) => add(contacts)
|
|
143
|
-
}));
|
|
144
|
-
row.appendChild(many);
|
|
145
|
-
|
|
146
|
-
// Under the field it belongs to, whatever Forma wrapped it in.
|
|
147
|
-
(toInput.closest('.domma-form-field') || toInput.parentElement || wrapper).appendChild(row);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const actions = document.createElement('div');
|
|
152
|
-
actions.className = 'invoice-party-actions';
|
|
153
|
-
const cancelBtn = document.createElement('button');
|
|
154
|
-
cancelBtn.type = 'button';
|
|
155
|
-
cancelBtn.className = 'btn btn-ghost';
|
|
156
|
-
cancelBtn.textContent = 'Cancel';
|
|
157
|
-
const sendBtn = document.createElement('button');
|
|
158
|
-
sendBtn.type = 'button';
|
|
159
|
-
sendBtn.className = 'btn btn-primary';
|
|
160
|
-
sendBtn.textContent = 'Send';
|
|
161
|
-
actions.appendChild(cancelBtn);
|
|
162
|
-
actions.appendChild(sendBtn);
|
|
163
|
-
wrapper.appendChild(actions);
|
|
164
|
-
|
|
165
|
-
const slideover = E.slideover({
|
|
166
|
-
title: 'Send Invoice',
|
|
167
|
-
size: 'md',
|
|
168
|
-
position: 'right',
|
|
169
|
-
onClose: () => setTimeout(() => {
|
|
170
|
-
slideover.destroy();
|
|
171
|
-
slideover.element?.remove();
|
|
172
|
-
}, 350)
|
|
173
|
-
});
|
|
174
|
-
slideover.element.appendChild(wrapper);
|
|
175
|
-
|
|
176
|
-
cancelBtn.addEventListener('click', () => slideover.close());
|
|
177
|
-
|
|
178
|
-
sendBtn.addEventListener('click', async () => {
|
|
179
|
-
if (!form.validate()) return;
|
|
180
|
-
const values = form.getData();
|
|
181
|
-
|
|
182
|
-
// Sending is slow (rendering the PDF is about a second) and it is not
|
|
183
|
-
// repeatable - say so, and make a second click impossible.
|
|
184
|
-
sendBtn.disabled = true;
|
|
185
|
-
cancelBtn.disabled = true;
|
|
186
|
-
const original = sendBtn.textContent;
|
|
187
|
-
sendBtn.textContent = 'Sending…';
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const res = await H.post(`${BASE}/invoices/${id}/send`, {
|
|
191
|
-
to: values.to,
|
|
192
|
-
subject: values.subject,
|
|
193
|
-
message: values.message,
|
|
194
|
-
attachPdf: Boolean(values.attachPdf)
|
|
195
|
-
});
|
|
196
|
-
const out = res?.data ?? res;
|
|
197
|
-
if (out?.error) throw new Error(out.error);
|
|
198
|
-
slideover.close();
|
|
199
|
-
E.toast(out.attached ? `Sent to ${out.to} with the PDF attached.` : `Sent to ${out.to}.`,
|
|
200
|
-
{ type: 'success' });
|
|
201
|
-
if (typeof onSent === 'function') onSent(out.invoice);
|
|
202
|
-
} catch (err) {
|
|
203
|
-
E.toast(err?.message ?? 'The invoice could not be sent.', { type: 'error' });
|
|
204
|
-
sendBtn.disabled = false;
|
|
205
|
-
cancelBtn.disabled = false;
|
|
206
|
-
sendBtn.textContent = original;
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
slideover.open();
|
|
211
|
-
Domma.icons.scan(wrapper);
|
|
212
|
-
}
|