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,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "notes",
|
|
3
|
-
"displayName": "Notes",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Rich note-taking with categories, search, and markdown content.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-03-24",
|
|
8
|
-
"icon": "edit-3",
|
|
9
|
-
"admin": {
|
|
10
|
-
"sidebar": [
|
|
11
|
-
{
|
|
12
|
-
"id": "notes",
|
|
13
|
-
"text": "Notes",
|
|
14
|
-
"icon": "file-text",
|
|
15
|
-
"url": "#/plugins/notes",
|
|
16
|
-
"section": "#/plugins/notes"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"routes": [
|
|
20
|
-
{
|
|
21
|
-
"path": "/plugins/notes",
|
|
22
|
-
"view": "plugin-notes",
|
|
23
|
-
"title": "Notes - Domma CMS"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"views": {
|
|
27
|
-
"plugin-notes": {
|
|
28
|
-
"entry": "notes/admin/views/notes.js?v=ecd1b254",
|
|
29
|
-
"exportName": "notesView"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
<div class="survey-audience-plugin">
|
|
2
|
-
|
|
3
|
-
<div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
|
4
|
-
<h2 style="margin:0;flex:1;">Survey Audience</h2>
|
|
5
|
-
<button id="back-surveys" class="btn btn-outline">
|
|
6
|
-
<span data-icon="arrow-left"></span>
|
|
7
|
-
Surveys
|
|
8
|
-
</button>
|
|
9
|
-
<button id="import-contacts" class="btn btn-outline">
|
|
10
|
-
<span data-icon="download"></span>
|
|
11
|
-
Import from Contacts
|
|
12
|
-
</button>
|
|
13
|
-
<button id="add-contact" class="btn btn-primary">
|
|
14
|
-
<span data-icon="plus"></span>
|
|
15
|
-
Add contact
|
|
16
|
-
</button>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div class="grid grid-cols-3" style="gap:1rem;align-items:start;">
|
|
20
|
-
|
|
21
|
-
<!-- Contacts table -->
|
|
22
|
-
<div style="grid-column:span 2;">
|
|
23
|
-
<div id="audience-table"></div>
|
|
24
|
-
<div id="audience-empty" style="display:none;text-align:center;padding:2rem 1rem;color:var(--dm-text-muted);">
|
|
25
|
-
<p>No audience contacts yet. Add one or import from the Contacts plugin.</p>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<!-- Groups panel -->
|
|
30
|
-
<div class="card">
|
|
31
|
-
<div class="card-header"><strong>Groups</strong></div>
|
|
32
|
-
<div class="card-body">
|
|
33
|
-
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;">
|
|
34
|
-
<input id="new-group-name" type="text" class="form-input" placeholder="New group name…" style="flex:1;">
|
|
35
|
-
<button id="add-group" class="btn btn-primary">
|
|
36
|
-
<span data-icon="plus"></span>
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
<div id="groups-list">
|
|
40
|
-
<p class="text-muted">No groups yet.</p>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
</div>
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<div class="survey-results-plugin">
|
|
2
|
-
|
|
3
|
-
<style>
|
|
4
|
-
/* Layout only + real Domma theme tokens - surfaces use .card so the
|
|
5
|
-
active theme is always respected; no hardcoded colours. */
|
|
6
|
-
.survey-results-plugin .sr-head { display:flex; align-items:flex-start; gap:1rem; flex-wrap:wrap; margin-bottom:1.25rem; }
|
|
7
|
-
.survey-results-plugin .sr-head h2 { margin:0 0 0.25rem; }
|
|
8
|
-
.survey-results-plugin .sr-sub { color:var(--dm-text-muted); margin:0; }
|
|
9
|
-
.survey-results-plugin .sr-head-actions { margin-left:auto; display:flex; gap:0.5rem; flex-wrap:wrap; }
|
|
10
|
-
|
|
11
|
-
.survey-results-plugin .sr-summary { display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:1rem; margin-bottom:1.5rem; }
|
|
12
|
-
@media (max-width:760px){ .survey-results-plugin .sr-summary { grid-template-columns:1fr 1fr; } }
|
|
13
|
-
.survey-results-plugin .sr-rate-num { font-size:2.4rem; font-weight:800; line-height:1; color:var(--dm-primary); }
|
|
14
|
-
.survey-results-plugin .sr-kpi-num { font-size:2rem; font-weight:800; line-height:1; }
|
|
15
|
-
.survey-results-plugin .sr-kpi-lbl { color:var(--dm-text-muted); font-size:0.85rem; margin-top:0.35rem; }
|
|
16
|
-
.survey-results-plugin .sr-rate-bar { height:8px; border-radius:999px; background:var(--dm-border); margin-top:0.75rem; overflow:hidden; }
|
|
17
|
-
.survey-results-plugin .sr-rate-fill { height:100%; background:var(--dm-primary); border-radius:999px; }
|
|
18
|
-
|
|
19
|
-
.survey-results-plugin .sr-section-title { font-size:1.05rem; font-weight:700; margin:0 0 0.75rem; }
|
|
20
|
-
.survey-results-plugin .sr-q-head { display:flex; align-items:baseline; gap:0.6rem; margin-bottom:0.75rem; }
|
|
21
|
-
.survey-results-plugin .sr-q-num { display:inline-flex; align-items:center; justify-content:center; min-width:1.6rem; height:1.6rem; border-radius:50%; background:var(--dm-primary); color:var(--dm-text-inverse); font-size:0.8rem; font-weight:700; }
|
|
22
|
-
.survey-results-plugin .sr-q-label { font-weight:700; }
|
|
23
|
-
.survey-results-plugin .sr-q-meta { margin-left:auto; color:var(--dm-text-muted); font-size:0.8rem; text-transform:uppercase; letter-spacing:0.03em; }
|
|
24
|
-
.survey-results-plugin .sr-avg { font-size:1.1rem; margin:0 0 0.6rem; }
|
|
25
|
-
.survey-results-plugin .sr-avg strong { font-size:1.5rem; color:var(--dm-primary); }
|
|
26
|
-
|
|
27
|
-
/* Distribution bars (survey.css isn't loaded in admin, so style here). */
|
|
28
|
-
.survey-results-plugin .survey-dist { display:flex; flex-direction:column; gap:0.5rem; }
|
|
29
|
-
.survey-results-plugin .survey-dist-row { display:grid; grid-template-columns:minmax(80px,160px) 1fr auto; align-items:center; gap:0.75rem; }
|
|
30
|
-
.survey-results-plugin .survey-dist-label { font-size:0.9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
31
|
-
.survey-results-plugin .survey-dist-bar { height:14px; background:var(--dm-border); border-radius:999px; overflow:hidden; }
|
|
32
|
-
.survey-results-plugin .survey-dist-fill { height:100%; background:var(--dm-primary); border-radius:999px; min-width:2px; }
|
|
33
|
-
.survey-results-plugin .survey-dist-val { font-size:0.85rem; color:var(--dm-text-muted); white-space:nowrap; }
|
|
34
|
-
|
|
35
|
-
/* Free-text answers. */
|
|
36
|
-
.survey-results-plugin .sr-quotes { max-height:260px; overflow:auto; display:flex; flex-direction:column; gap:0.5rem; }
|
|
37
|
-
.survey-results-plugin .sr-quote { padding:0.5rem 0.75rem; background:var(--dm-surface-raised); border-left:3px solid var(--dm-border); border-radius:0 6px 6px 0; font-size:0.92rem; }
|
|
38
|
-
.survey-results-plugin .sr-empty { color:var(--dm-text-muted); font-style:italic; }
|
|
39
|
-
</style>
|
|
40
|
-
|
|
41
|
-
<div class="sr-head">
|
|
42
|
-
<div>
|
|
43
|
-
<h2 id="results-title">Survey Results</h2>
|
|
44
|
-
<p class="sr-sub" id="results-sub">How your audience responded.</p>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="sr-head-actions">
|
|
47
|
-
<button id="export-csv" class="btn btn-outline"><span data-icon="download"></span> CSV</button>
|
|
48
|
-
<button id="export-json" class="btn btn-outline"><span data-icon="download"></span> JSON</button>
|
|
49
|
-
<button id="back-surveys" class="btn btn-outline"><span data-icon="arrow-left"></span> Surveys</button>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div id="summary" class="sr-summary"></div>
|
|
54
|
-
|
|
55
|
-
<h3 class="sr-section-title">Question breakdown</h3>
|
|
56
|
-
<div id="questions"></div>
|
|
57
|
-
|
|
58
|
-
</div>
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
<div class="survey-editor-plugin">
|
|
2
|
-
|
|
3
|
-
<style>
|
|
4
|
-
/* Layout only - colours/borders come from Domma's themed .card and
|
|
5
|
-
.form-input, so the active theme is always respected. */
|
|
6
|
-
.survey-editor-plugin .se-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.25rem; }
|
|
7
|
-
.survey-editor-plugin .se-span-2 { grid-column: 1 / -1; }
|
|
8
|
-
.survey-editor-plugin .se-field { display: flex; flex-direction: column; gap: 0.35rem; }
|
|
9
|
-
.survey-editor-plugin .se-field label { font-weight: 600; font-size: 0.85rem; }
|
|
10
|
-
.survey-editor-plugin .se-field .se-hint { font-size: 0.78rem; color: var(--dm-text-muted); }
|
|
11
|
-
.survey-editor-plugin .se-field .form-input { width: 100%; }
|
|
12
|
-
.survey-editor-plugin .se-field textarea.form-input { min-height: 90px; resize: vertical; }
|
|
13
|
-
.survey-editor-plugin .se-field select[multiple].form-input { min-height: 120px; }
|
|
14
|
-
@media (max-width: 640px) { .survey-editor-plugin .se-grid { grid-template-columns: 1fr; } }
|
|
15
|
-
</style>
|
|
16
|
-
|
|
17
|
-
<div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;">
|
|
18
|
-
<h2 id="editor-title" style="margin:0;flex:1;">New Survey</h2>
|
|
19
|
-
<button id="back-btn" class="btn btn-outline"><span data-icon="arrow-left"></span> Back</button>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="card">
|
|
23
|
-
<div class="card-header"><strong>Basics</strong></div>
|
|
24
|
-
<div class="card-body">
|
|
25
|
-
<div class="se-grid">
|
|
26
|
-
<div class="se-field se-span-2">
|
|
27
|
-
<label for="f-title">Title</label>
|
|
28
|
-
<input id="f-title" name="title" type="text" class="form-input" placeholder="Customer satisfaction Q3">
|
|
29
|
-
</div>
|
|
30
|
-
<div class="se-field">
|
|
31
|
-
<label for="f-slug">Slug</label>
|
|
32
|
-
<input id="f-slug" name="slug" type="text" class="form-input" placeholder="customer-sat-q3">
|
|
33
|
-
<span class="se-hint" id="slug-hint">URL-safe identifier.</span>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="se-field">
|
|
36
|
-
<label for="f-formSlug">Form</label>
|
|
37
|
-
<select id="f-formSlug" name="formSlug" class="form-input"><option value="">Select a form…</option></select>
|
|
38
|
-
<span class="se-hint">The Forms definition respondents complete.</span>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="se-field se-span-2">
|
|
41
|
-
<label for="f-description">Description</label>
|
|
42
|
-
<textarea id="f-description" name="description" class="form-input" placeholder="Optional internal description…"></textarea>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div class="card mt-3">
|
|
49
|
-
<div class="card-header"><strong>Setup</strong></div>
|
|
50
|
-
<div class="card-body">
|
|
51
|
-
<div class="se-grid">
|
|
52
|
-
<div class="se-field">
|
|
53
|
-
<label for="f-status">Status</label>
|
|
54
|
-
<select id="f-status" name="status" class="form-input">
|
|
55
|
-
<option value="draft">Draft</option>
|
|
56
|
-
<option value="open">Open</option>
|
|
57
|
-
<option value="closed">Closed</option>
|
|
58
|
-
</select>
|
|
59
|
-
</div>
|
|
60
|
-
<div class="se-field">
|
|
61
|
-
<label for="f-mode">Mode</label>
|
|
62
|
-
<select id="f-mode" name="mode" class="form-input">
|
|
63
|
-
<option value="identified">Identified</option>
|
|
64
|
-
<option value="anonymous">Anonymous</option>
|
|
65
|
-
</select>
|
|
66
|
-
<span class="se-hint">Anonymous de-links responses from contacts.</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="se-field se-span-2">
|
|
69
|
-
<label for="f-groupIds">Audience groups</label>
|
|
70
|
-
<select id="f-groupIds" name="groupIds" class="form-input" multiple></select>
|
|
71
|
-
<span class="se-hint">Hold Ctrl/Cmd to select multiple groups.</span>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div class="card mt-3">
|
|
78
|
-
<div class="card-header"><strong>Schedule</strong></div>
|
|
79
|
-
<div class="card-body">
|
|
80
|
-
<div class="se-grid">
|
|
81
|
-
<div class="se-field"><label for="f-opensAt">Opens at</label><input id="f-opensAt" name="opensAt" type="date" class="form-input"></div>
|
|
82
|
-
<div class="se-field"><label for="f-closesAt">Closes at</label><input id="f-closesAt" name="closesAt" type="date" class="form-input"></div>
|
|
83
|
-
<div class="se-field"><label for="f-reminderAfterDays">Reminder after (days)</label><input id="f-reminderAfterDays" name="reminderAfterDays" type="number" min="0" value="7" class="form-input"></div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div class="card mt-3">
|
|
89
|
-
<div class="card-header"><strong>Survey page content</strong></div>
|
|
90
|
-
<div class="card-body">
|
|
91
|
-
<div class="se-grid">
|
|
92
|
-
<div class="se-field se-span-2">
|
|
93
|
-
<label for="f-intro">Intro (Markdown - shown above the form)</label>
|
|
94
|
-
<textarea id="f-intro" name="intro" class="form-input" placeholder="## Welcome Thanks for taking a moment to help us improve."></textarea>
|
|
95
|
-
</div>
|
|
96
|
-
<div class="se-field se-span-2">
|
|
97
|
-
<label for="f-footer">Footer (Markdown - shown below the form)</label>
|
|
98
|
-
<textarea id="f-footer" name="footer" class="form-input" placeholder="Questions? Contact us at hello@example.com"></textarea>
|
|
99
|
-
</div>
|
|
100
|
-
<div class="se-field se-span-2">
|
|
101
|
-
<label for="f-thankYou">Thank-you message (shown after submitting)</label>
|
|
102
|
-
<textarea id="f-thankYou" name="thankYou" class="form-input" placeholder="Thank you - your response has been recorded."></textarea>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div class="card mt-3">
|
|
109
|
-
<div class="card-header"><strong>Invitation email</strong></div>
|
|
110
|
-
<div class="card-body">
|
|
111
|
-
<div class="se-grid">
|
|
112
|
-
<div class="se-field se-span-2">
|
|
113
|
-
<label for="f-inviteSubject">Subject</label>
|
|
114
|
-
<input id="f-inviteSubject" name="inviteSubject" type="text" class="form-input" placeholder="We would love your feedback">
|
|
115
|
-
</div>
|
|
116
|
-
<div class="se-field se-span-2">
|
|
117
|
-
<label for="f-inviteBody">Body</label>
|
|
118
|
-
<textarea id="f-inviteBody" name="inviteBody" class="form-input" placeholder="Hi {{name}}, please complete our survey: {{link}}"></textarea>
|
|
119
|
-
<span class="se-hint">{{name}} and {{link}} are substituted per recipient; {{link}} becomes the button.</span>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<div class="card mt-3">
|
|
126
|
-
<div class="card-body" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
|
|
127
|
-
<button id="save-btn" class="btn btn-primary">Save Survey</button>
|
|
128
|
-
<button id="cancel-btn" class="btn btn-outline">Cancel</button>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div id="send-panel" class="card mt-3" style="display:none;">
|
|
133
|
-
<div class="card-header"><strong>Invitations</strong></div>
|
|
134
|
-
<div class="card-body" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
|
|
135
|
-
<button id="send-all-btn" class="btn btn-primary"><span data-icon="send"></span> Send invitations</button>
|
|
136
|
-
<button id="send-reminder-btn" class="btn btn-outline"><span data-icon="bell"></span> Send reminder to non-responders</button>
|
|
137
|
-
<span id="send-hint" class="text-muted" style="font-size:0.85rem;"></span>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
</div>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<div class="surveys-plugin">
|
|
2
|
-
|
|
3
|
-
<!-- Toolbar -->
|
|
4
|
-
<div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
|
5
|
-
<h2 style="margin:0;flex:1;">Surveys</h2>
|
|
6
|
-
<button id="manage-audience" class="btn btn-outline">
|
|
7
|
-
<span data-icon="users"></span>
|
|
8
|
-
Audience
|
|
9
|
-
</button>
|
|
10
|
-
<button id="new-survey" class="btn btn-primary">
|
|
11
|
-
<span data-icon="plus"></span>
|
|
12
|
-
New Survey
|
|
13
|
-
</button>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<!-- Surveys table -->
|
|
17
|
-
<div id="surveys-table"></div>
|
|
18
|
-
|
|
19
|
-
<!-- Empty state -->
|
|
20
|
-
<div id="surveys-empty" style="display:none;text-align:center;padding:3rem 1rem;color:var(--dm-text-muted);">
|
|
21
|
-
<span data-icon="clipboard-list" style="font-size:3rem;display:block;margin-bottom:1rem;"></span>
|
|
22
|
-
<p>No surveys yet. Click <strong>New Survey</strong> to create your first campaign.</p>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
</div>
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surveys plugin - shared audience view.
|
|
3
|
-
*
|
|
4
|
-
* Manages survey contacts and audience groups, plus an import action that pulls
|
|
5
|
-
* the audience from the Contacts plugin.
|
|
6
|
-
*
|
|
7
|
-
* @module surveys/admin/views/audience
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const BASE = '/api/plugins/surveys';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Authenticated JSON fetch helper. Mirrors plugins/contacts/admin/views/contacts.js.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} url
|
|
16
|
-
* @param {string} [method]
|
|
17
|
-
* @param {object} [body]
|
|
18
|
-
* @returns {Promise<*>}
|
|
19
|
-
*/
|
|
20
|
-
async function api(url, method = 'GET', body) {
|
|
21
|
-
const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
|
|
22
|
-
if (body !== undefined) {
|
|
23
|
-
opts.headers['Content-Type'] = 'application/json';
|
|
24
|
-
opts.body = JSON.stringify(body);
|
|
25
|
-
}
|
|
26
|
-
const res = await fetch(url, opts);
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const err = await res.json().catch(() => ({error: res.statusText}));
|
|
29
|
-
throw new Error(err.error || res.statusText);
|
|
30
|
-
}
|
|
31
|
-
const text = await res.text();
|
|
32
|
-
return text ? JSON.parse(text) : {};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Escape HTML special characters to prevent XSS in render strings.
|
|
37
|
-
*
|
|
38
|
-
* @param {*} str
|
|
39
|
-
* @returns {string}
|
|
40
|
-
*/
|
|
41
|
-
function esc(str) {
|
|
42
|
-
return String(str ?? '')
|
|
43
|
-
.replace(/&/g, '&')
|
|
44
|
-
.replace(/</g, '<')
|
|
45
|
-
.replace(/>/g, '>')
|
|
46
|
-
.replace(/"/g, '"');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const audienceView = {
|
|
50
|
-
templateUrl: '/plugins/surveys/admin/templates/audience.html',
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Mount the audience view.
|
|
54
|
-
*
|
|
55
|
-
* @param {object} $container - Domma-wrapped container element
|
|
56
|
-
* @returns {Promise<void>}
|
|
57
|
-
*/
|
|
58
|
-
async onMount($container) {
|
|
59
|
-
let contacts = [];
|
|
60
|
-
let groups = [];
|
|
61
|
-
let table = null;
|
|
62
|
-
|
|
63
|
-
async function load() {
|
|
64
|
-
const [c, g] = await Promise.all([
|
|
65
|
-
api(`${BASE}/contacts`).catch(() => []),
|
|
66
|
-
api(`${BASE}/groups`).catch(() => [])
|
|
67
|
-
]);
|
|
68
|
-
contacts = Array.isArray(c) ? c : [];
|
|
69
|
-
groups = Array.isArray(g) ? g : [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---- Contacts table ----------------------------------------------
|
|
73
|
-
function buildTable() {
|
|
74
|
-
const tableEl = $container.find('#audience-table').get(0);
|
|
75
|
-
const emptyEl = $container.find('#audience-empty').get(0);
|
|
76
|
-
|
|
77
|
-
if (!contacts.length) {
|
|
78
|
-
if (tableEl) tableEl.style.display = 'none';
|
|
79
|
-
if (emptyEl) emptyEl.style.display = 'block';
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (emptyEl) emptyEl.style.display = 'none';
|
|
83
|
-
if (tableEl) tableEl.style.display = '';
|
|
84
|
-
|
|
85
|
-
if (table) {
|
|
86
|
-
table.setData(contacts);
|
|
87
|
-
} else {
|
|
88
|
-
// Render functions return HTML strings; delete uses delegated
|
|
89
|
-
// listener below (not inline onclick) so we can await E.confirm.
|
|
90
|
-
table = T.create($container.find('#audience-table').get(0), {
|
|
91
|
-
data: contacts,
|
|
92
|
-
columns: [
|
|
93
|
-
{key: 'name', title: 'Name', sortable: true, render: (v) => esc(v) || '-'},
|
|
94
|
-
{
|
|
95
|
-
key: 'email', title: 'Email', sortable: true,
|
|
96
|
-
render: (v) => v ? `<a href="mailto:${esc(v)}">${esc(v)}</a>` : '-'
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
key: 'groups', title: 'Groups', sortable: false,
|
|
100
|
-
render: (v) => {
|
|
101
|
-
if (!Array.isArray(v) || !v.length) return '-';
|
|
102
|
-
return v.map((g) => `<span class="badge badge-primary" style="margin-right:2px;">${esc(g)}</span>`).join('');
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
key: 'id', title: 'Actions', sortable: false,
|
|
107
|
-
render: (id) =>
|
|
108
|
-
`<button class="btn btn-sm btn-danger contact-del" data-id="${esc(id)}">` +
|
|
109
|
-
`<span data-icon="trash" data-icon-size="14"></span> Delete</button>`
|
|
110
|
-
}
|
|
111
|
-
],
|
|
112
|
-
emptyMessage: 'No contacts.'
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const container = $container.find('#audience-table').get(0);
|
|
116
|
-
if (container) {
|
|
117
|
-
container.addEventListener('click', async (e) => {
|
|
118
|
-
const btn = e.target.closest('.contact-del');
|
|
119
|
-
if (!btn) return;
|
|
120
|
-
const id = btn.dataset.id;
|
|
121
|
-
const contact = contacts.find((c) => c.id === id);
|
|
122
|
-
const ok = await E.confirm(`Delete "${contact?.name || contact?.email || 'this contact'}"?`);
|
|
123
|
-
if (!ok) return;
|
|
124
|
-
try {
|
|
125
|
-
await api(`${BASE}/contacts/${encodeURIComponent(id)}`, 'DELETE');
|
|
126
|
-
E.toast('Contact deleted.', {type: 'success'});
|
|
127
|
-
await refresh();
|
|
128
|
-
} catch (err) {
|
|
129
|
-
E.toast(err?.message ?? 'Failed to delete contact.', {type: 'error'});
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
Domma.icons.scan();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---- Groups panel -------------------------------------------------
|
|
138
|
-
function buildGroups() {
|
|
139
|
-
const listEl = $container.find('#groups-list').get(0);
|
|
140
|
-
if (!listEl) return;
|
|
141
|
-
while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
|
|
142
|
-
|
|
143
|
-
if (!groups.length) {
|
|
144
|
-
const p = document.createElement('p');
|
|
145
|
-
p.className = 'text-muted';
|
|
146
|
-
p.textContent = 'No groups yet.';
|
|
147
|
-
listEl.appendChild(p);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
groups.forEach((g) => {
|
|
151
|
-
const row = document.createElement('div');
|
|
152
|
-
row.style.cssText = 'padding:0.4rem 0;border-bottom:1px solid var(--dm-border);';
|
|
153
|
-
const badge = document.createElement('span');
|
|
154
|
-
badge.className = 'badge badge-primary';
|
|
155
|
-
badge.textContent = g.name; // textContent - safe
|
|
156
|
-
row.appendChild(badge);
|
|
157
|
-
if (g.description) {
|
|
158
|
-
const desc = document.createElement('div');
|
|
159
|
-
desc.className = 'text-muted';
|
|
160
|
-
desc.style.fontSize = '0.8rem';
|
|
161
|
-
desc.textContent = g.description;
|
|
162
|
-
row.appendChild(desc);
|
|
163
|
-
}
|
|
164
|
-
listEl.appendChild(row);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---- Add-contact modal (light-DOM projection) ---------------------
|
|
169
|
-
function openContactModal() {
|
|
170
|
-
const modal = E.modal({title: 'Add Contact', size: 'md'});
|
|
171
|
-
|
|
172
|
-
const wrapper = document.createElement('div');
|
|
173
|
-
wrapper.style.cssText = 'padding:0.25rem 0 0.5rem;';
|
|
174
|
-
|
|
175
|
-
const formContainer = document.createElement('div');
|
|
176
|
-
wrapper.appendChild(formContainer);
|
|
177
|
-
|
|
178
|
-
// Canonical F.create: (blueprintObject, defaultsObject, opts).renderTo(el).
|
|
179
|
-
// Blueprint is keyed by field name; placeholder/hint live under
|
|
180
|
-
// formConfig (proven shape - collection-entries.js). We supply our own
|
|
181
|
-
// buttons (below) and read values from the DOM on save, so the modal's
|
|
182
|
-
// HTML sanitiser never strips an interactive submit button.
|
|
183
|
-
F.create(
|
|
184
|
-
{
|
|
185
|
-
name: {type: 'string', label: 'Name', formConfig: {placeholder: 'Full name'}},
|
|
186
|
-
email: {
|
|
187
|
-
type: 'string', label: 'Email', required: true,
|
|
188
|
-
formConfig: {placeholder: 'email@example.com'}
|
|
189
|
-
},
|
|
190
|
-
groups: {
|
|
191
|
-
type: 'multiselect', label: 'Groups',
|
|
192
|
-
options: groups.map((g) => ({value: g.name, label: g.name})),
|
|
193
|
-
formConfig: {hint: 'Hold Ctrl/Cmd to select multiple.'}
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
{},
|
|
197
|
-
{layout: 'stacked', columns: 1, showSubmitButton: false}
|
|
198
|
-
).renderTo(formContainer);
|
|
199
|
-
|
|
200
|
-
const btnWrap = document.createElement('div');
|
|
201
|
-
btnWrap.style.cssText = 'display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.75rem;';
|
|
202
|
-
|
|
203
|
-
const cancelBtn = document.createElement('button');
|
|
204
|
-
cancelBtn.className = 'btn btn-outline';
|
|
205
|
-
cancelBtn.textContent = 'Cancel';
|
|
206
|
-
cancelBtn.addEventListener('click', () => modal.close());
|
|
207
|
-
|
|
208
|
-
const saveBtn = document.createElement('button');
|
|
209
|
-
saveBtn.className = 'btn btn-primary';
|
|
210
|
-
saveBtn.textContent = 'Save contact';
|
|
211
|
-
saveBtn.addEventListener('click', async () => {
|
|
212
|
-
const nameEl = formContainer.querySelector('[name="name"]');
|
|
213
|
-
const emailEl = formContainer.querySelector('[name="email"]');
|
|
214
|
-
const groupsEl = formContainer.querySelector('[name="groups"]');
|
|
215
|
-
const email = (emailEl?.value ?? '').trim();
|
|
216
|
-
if (!email) {
|
|
217
|
-
E.toast('Email is required.', {type: 'error'});
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const selectedGroups = groupsEl
|
|
221
|
-
? Array.from(groupsEl.selectedOptions).map((o) => o.value)
|
|
222
|
-
: [];
|
|
223
|
-
try {
|
|
224
|
-
await api(`${BASE}/contacts`, 'POST', {
|
|
225
|
-
name: (nameEl?.value ?? '').trim(),
|
|
226
|
-
email,
|
|
227
|
-
groups: selectedGroups
|
|
228
|
-
});
|
|
229
|
-
E.toast('Contact added.', {type: 'success'});
|
|
230
|
-
modal.close();
|
|
231
|
-
await refresh();
|
|
232
|
-
} catch (err) {
|
|
233
|
-
E.toast(err?.message ?? 'Failed to add contact.', {type: 'error'});
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
btnWrap.appendChild(cancelBtn);
|
|
238
|
-
btnWrap.appendChild(saveBtn);
|
|
239
|
-
wrapper.appendChild(btnWrap);
|
|
240
|
-
|
|
241
|
-
// Project the assembled DOM into the modal's light DOM so the
|
|
242
|
-
// interactive buttons survive (E.modal().setContent strips them).
|
|
243
|
-
modal.element.appendChild(wrapper);
|
|
244
|
-
modal.open();
|
|
245
|
-
Domma.icons.scan();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ---- Refresh ------------------------------------------------------
|
|
249
|
-
async function refresh() {
|
|
250
|
-
await load();
|
|
251
|
-
buildTable();
|
|
252
|
-
buildGroups();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---- Wiring -------------------------------------------------------
|
|
256
|
-
const addContactBtn = $container.find('#add-contact').get(0);
|
|
257
|
-
if (addContactBtn) addContactBtn.addEventListener('click', openContactModal);
|
|
258
|
-
|
|
259
|
-
const backBtn = $container.find('#back-surveys').get(0);
|
|
260
|
-
if (backBtn) backBtn.addEventListener('click', () => { location.hash = '#/plugins/surveys'; });
|
|
261
|
-
|
|
262
|
-
const importBtn = $container.find('#import-contacts').get(0);
|
|
263
|
-
if (importBtn) {
|
|
264
|
-
importBtn.addEventListener('click', async () => {
|
|
265
|
-
const ok = await E.confirm('Import all contacts from the Contacts plugin?');
|
|
266
|
-
if (!ok) return;
|
|
267
|
-
try {
|
|
268
|
-
const res = await api(`${BASE}/import-from-contacts`, 'POST', {});
|
|
269
|
-
E.toast(`Imported ${res?.imported ?? 0} contact(s).`, {type: 'success'});
|
|
270
|
-
await refresh();
|
|
271
|
-
} catch (err) {
|
|
272
|
-
E.toast(err?.message ?? 'Import failed.', {type: 'error'});
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const addGroupBtn = $container.find('#add-group').get(0);
|
|
278
|
-
const newGroupInput = $container.find('#new-group-name').get(0);
|
|
279
|
-
async function addGroup() {
|
|
280
|
-
const name = (newGroupInput?.value ?? '').trim();
|
|
281
|
-
if (!name) return;
|
|
282
|
-
try {
|
|
283
|
-
await api(`${BASE}/groups`, 'POST', {name});
|
|
284
|
-
if (newGroupInput) newGroupInput.value = '';
|
|
285
|
-
E.toast('Group added.', {type: 'success'});
|
|
286
|
-
await refresh();
|
|
287
|
-
} catch (err) {
|
|
288
|
-
E.toast(err?.message ?? 'Failed to add group.', {type: 'error'});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (addGroupBtn) addGroupBtn.addEventListener('click', addGroup);
|
|
292
|
-
if (newGroupInput) {
|
|
293
|
-
newGroupInput.addEventListener('keydown', (e) => {
|
|
294
|
-
if (e.key === 'Enter') addGroup();
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
await refresh();
|
|
299
|
-
Domma.icons.scan();
|
|
300
|
-
}
|
|
301
|
-
};
|