domma-cms 0.66.0 → 0.69.3
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 +92 -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 +307 -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,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surveys plugin - results / statistics dashboard.
|
|
3
|
-
*
|
|
4
|
-
* Renders a response-rate summary, then per-question cards: distribution bars
|
|
5
|
-
* for choice/rating fields and a tidy answer list for free-text fields. Authed
|
|
6
|
-
* CSV/JSON export.
|
|
7
|
-
*
|
|
8
|
-
* @module surveys/admin/views/results
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {renderDistribution} from '/plugins/surveys/public/dist-shared.mjs';
|
|
12
|
-
|
|
13
|
-
const BASE = '/api/plugins/surveys';
|
|
14
|
-
|
|
15
|
-
/** Authenticated JSON fetch helper. Mirrors plugins/contacts/admin/views/contacts.js. */
|
|
16
|
-
async function api(url, method = 'GET', body) {
|
|
17
|
-
const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
|
|
18
|
-
if (body !== undefined) {
|
|
19
|
-
opts.headers['Content-Type'] = 'application/json';
|
|
20
|
-
opts.body = JSON.stringify(body);
|
|
21
|
-
}
|
|
22
|
-
const res = await fetch(url, opts);
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
const err = await res.json().catch(() => ({error: res.statusText}));
|
|
25
|
-
throw new Error(err.error || res.statusText);
|
|
26
|
-
}
|
|
27
|
-
const text = await res.text();
|
|
28
|
-
return text ? JSON.parse(text) : {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Escape HTML special characters. */
|
|
32
|
-
function esc(str) {
|
|
33
|
-
return String(str ?? '')
|
|
34
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const TYPE_LABEL = {choice: 'Multiple choice', rating: 'Rating', text: 'Free text'};
|
|
38
|
-
|
|
39
|
-
/** Total answers represented by a field's aggregation. */
|
|
40
|
-
function answerCount(f) {
|
|
41
|
-
if (f.type === 'text') return Array.isArray(f.responses) ? f.responses.length : 0;
|
|
42
|
-
return Array.isArray(f.items) ? f.items.reduce((n, i) => n + (i.value || 0), 0) : 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const resultsView = {
|
|
46
|
-
templateUrl: '/plugins/surveys/admin/templates/results.html',
|
|
47
|
-
|
|
48
|
-
async onMount($container) {
|
|
49
|
-
const root = $container.find('.survey-results-plugin').get(0) || $container.get(0);
|
|
50
|
-
const hashMatch = location.hash.match(/^#\/plugins\/surveys\/results\/(.+)$/);
|
|
51
|
-
const slug = hashMatch ? decodeURIComponent(hashMatch[1]) : null;
|
|
52
|
-
if (!slug) {
|
|
53
|
-
E.toast('No survey specified.', {type: 'error'});
|
|
54
|
-
location.hash = '#/plugins/surveys';
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---- Load survey (for title/status) + stats -----------------------
|
|
59
|
-
let survey = null;
|
|
60
|
-
let stats = null;
|
|
61
|
-
try {
|
|
62
|
-
[survey, stats] = await Promise.all([
|
|
63
|
-
api(`${BASE}/surveys/${encodeURIComponent(slug)}`).catch(() => null),
|
|
64
|
-
api(`${BASE}/surveys/${encodeURIComponent(slug)}/stats`)
|
|
65
|
-
]);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
E.toast(err?.message ?? 'Failed to load results.', {type: 'error'});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const health = stats?.health ?? {invited: 0, completed: 0, responseRate: 0};
|
|
72
|
-
const invited = health.invited ?? 0;
|
|
73
|
-
const completed = health.completed ?? 0;
|
|
74
|
-
const rate = health.responseRate ?? 0;
|
|
75
|
-
const outstanding = Math.max(0, invited - completed);
|
|
76
|
-
|
|
77
|
-
// ---- Header -------------------------------------------------------
|
|
78
|
-
const titleEl = root.querySelector('#results-title');
|
|
79
|
-
if (titleEl) titleEl.textContent = `Results - ${survey?.title || slug}`;
|
|
80
|
-
const subEl = root.querySelector('#results-sub');
|
|
81
|
-
if (subEl) {
|
|
82
|
-
const statusBadge = survey?.status ? ` · ${survey.status}` : '';
|
|
83
|
-
const modeBadge = survey?.mode === 'anonymous' ? ' · anonymous' : '';
|
|
84
|
-
subEl.textContent = `${completed} of ${invited} invited have responded${statusBadge}${modeBadge}.`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---- Summary cards ------------------------------------------------
|
|
88
|
-
const kpi = (num, label) =>
|
|
89
|
-
`<div class="card"><div class="card-body"><div class="sr-kpi-num">${esc(num)}</div><div class="sr-kpi-lbl">${esc(label)}</div></div></div>`;
|
|
90
|
-
$container.find('#summary').html(
|
|
91
|
-
`<div class="card"><div class="card-body">
|
|
92
|
-
<div class="sr-rate-num">${esc(rate)}%</div>
|
|
93
|
-
<div class="sr-kpi-lbl">Response rate</div>
|
|
94
|
-
<div class="sr-rate-bar"><div class="sr-rate-fill" data-pct="${Math.min(100, rate)}" style="width:${Math.min(100, rate)}%"></div></div>
|
|
95
|
-
</div></div>` +
|
|
96
|
-
kpi(invited, 'Invited') +
|
|
97
|
-
kpi(completed, 'Completed') +
|
|
98
|
-
kpi(outstanding, 'Awaiting response')
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// ---- Per-question cards -------------------------------------------
|
|
102
|
-
const fields = Array.isArray(stats?.fields) ? stats.fields : [];
|
|
103
|
-
let questionsHtml;
|
|
104
|
-
|
|
105
|
-
if (!completed && !fields.some((f) => answerCount(f) > 0)) {
|
|
106
|
-
questionsHtml = `<div class="card"><div class="card-body"><p class="sr-empty">No responses yet - once people complete the survey, their answers appear here.</p></div></div>`;
|
|
107
|
-
} else {
|
|
108
|
-
questionsHtml = fields.map((f, idx) => {
|
|
109
|
-
const n = answerCount(f);
|
|
110
|
-
const meta = `${TYPE_LABEL[f.type] || f.type} · ${n} answer${n === 1 ? '' : 's'}`;
|
|
111
|
-
let body;
|
|
112
|
-
if (f.type === 'choice' || f.type === 'rating') {
|
|
113
|
-
const items = Array.isArray(f.items) ? f.items : [];
|
|
114
|
-
const bars = items.length ? renderDistribution(items) : `<p class="sr-empty">No answers.</p>`;
|
|
115
|
-
const avg = (f.type === 'rating' && typeof f.average !== 'undefined')
|
|
116
|
-
? `<p class="sr-avg">Average <strong>${esc(f.average)}</strong></p>` : '';
|
|
117
|
-
body = avg + bars;
|
|
118
|
-
} else {
|
|
119
|
-
const responses = (Array.isArray(f.responses) ? f.responses : [])
|
|
120
|
-
.map((r) => (Array.isArray(r) ? r.join(', ') : r))
|
|
121
|
-
.filter((r) => String(r ?? '').trim() !== '');
|
|
122
|
-
body = responses.length
|
|
123
|
-
? `<div class="sr-quotes">` + responses.map((r) => `<div class="sr-quote">${esc(r)}</div>`).join('') + `</div>`
|
|
124
|
-
: `<p class="sr-empty">No answers.</p>`;
|
|
125
|
-
}
|
|
126
|
-
return `<div class="card mb-3"><div class="card-body">
|
|
127
|
-
<div class="sr-q-head">
|
|
128
|
-
<span class="sr-q-num">${idx + 1}</span>
|
|
129
|
-
<span class="sr-q-label">${esc(f.label || f.field)}</span>
|
|
130
|
-
<span class="sr-q-meta">${esc(meta)}</span>
|
|
131
|
-
</div>
|
|
132
|
-
${body}
|
|
133
|
-
</div></div>`;
|
|
134
|
-
}).join('');
|
|
135
|
-
}
|
|
136
|
-
$container.find('#questions').html(questionsHtml);
|
|
137
|
-
|
|
138
|
-
// Bar widths: set from data-pct via JS so they survive even if the admin's
|
|
139
|
-
// HTML sanitiser strips the inline width style.
|
|
140
|
-
root.querySelectorAll('[data-pct]').forEach((el) => {
|
|
141
|
-
const p = Number(el.getAttribute('data-pct'));
|
|
142
|
-
el.style.width = `${Math.max(0, Math.min(100, Number.isFinite(p) ? p : 0))}%`;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ---- Authed blob export -------------------------------------------
|
|
146
|
-
async function exportAs(format) {
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetch(`${BASE}/surveys/${encodeURIComponent(slug)}/export?format=${format}`, {
|
|
149
|
-
headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
|
|
150
|
-
});
|
|
151
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
152
|
-
const blob = await res.blob();
|
|
153
|
-
const url = URL.createObjectURL(blob);
|
|
154
|
-
const a = document.createElement('a');
|
|
155
|
-
a.href = url;
|
|
156
|
-
a.download = `${slug}.${format}`;
|
|
157
|
-
document.body.appendChild(a);
|
|
158
|
-
a.click();
|
|
159
|
-
document.body.removeChild(a);
|
|
160
|
-
URL.revokeObjectURL(url);
|
|
161
|
-
} catch (err) {
|
|
162
|
-
E.toast(err?.message ?? 'Export failed.', {type: 'error'});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
root.querySelector('#export-csv')?.addEventListener('click', () => exportAs('csv'));
|
|
167
|
-
root.querySelector('#export-json')?.addEventListener('click', () => exportAs('json'));
|
|
168
|
-
root.querySelector('#back-surveys')?.addEventListener('click', () => { location.hash = '#/plugins/surveys'; });
|
|
169
|
-
|
|
170
|
-
if (typeof Domma !== 'undefined' && Domma.icons) Domma.icons.scan();
|
|
171
|
-
}
|
|
172
|
-
};
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surveys plugin - campaign create/edit view.
|
|
3
|
-
*
|
|
4
|
-
* The form is hand-built in the template as native inputs laid out in an
|
|
5
|
-
* explicit CSS grid (guaranteed multi-column), so this view only populates the
|
|
6
|
-
* dynamic options (forms, groups), seeds values on edit, collects the payload,
|
|
7
|
-
* and POST/PUTs to the admin API. On an open survey it also exposes the
|
|
8
|
-
* send / reminder actions.
|
|
9
|
-
*
|
|
10
|
-
* @module surveys/admin/views/survey-editor
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const BASE = '/api/plugins/surveys';
|
|
14
|
-
|
|
15
|
-
/** Authenticated JSON fetch helper. Mirrors plugins/contacts/admin/views/contacts.js. */
|
|
16
|
-
async function api(url, method = 'GET', body) {
|
|
17
|
-
const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
|
|
18
|
-
if (body !== undefined) {
|
|
19
|
-
opts.headers['Content-Type'] = 'application/json';
|
|
20
|
-
opts.body = JSON.stringify(body);
|
|
21
|
-
}
|
|
22
|
-
const res = await fetch(url, opts);
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
const err = await res.json().catch(() => ({error: res.statusText}));
|
|
25
|
-
throw new Error(err.error || res.statusText);
|
|
26
|
-
}
|
|
27
|
-
const text = await res.text();
|
|
28
|
-
return text ? JSON.parse(text) : {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const surveyEditorView = {
|
|
32
|
-
templateUrl: '/plugins/surveys/admin/templates/survey-editor.html',
|
|
33
|
-
|
|
34
|
-
async onMount($container) {
|
|
35
|
-
const root = $container.find('.survey-editor-plugin').get(0) || $container.get(0);
|
|
36
|
-
if (!root) return;
|
|
37
|
-
|
|
38
|
-
const hashMatch = location.hash.match(/^#\/plugins\/surveys\/edit\/(.+)$/);
|
|
39
|
-
const editSlug = hashMatch ? decodeURIComponent(hashMatch[1]) : null;
|
|
40
|
-
const isEdit = !!editSlug;
|
|
41
|
-
|
|
42
|
-
const titleEl = root.querySelector('#editor-title');
|
|
43
|
-
if (titleEl) titleEl.textContent = isEdit ? 'Edit Survey' : 'New Survey';
|
|
44
|
-
|
|
45
|
-
// ---- Helpers to read/write the native inputs by field name ----------
|
|
46
|
-
const field = (name) => root.querySelector(`[name="${name}"]`);
|
|
47
|
-
|
|
48
|
-
function setValue(name, value) {
|
|
49
|
-
const el = field(name);
|
|
50
|
-
if (!el) return;
|
|
51
|
-
if (el.multiple) {
|
|
52
|
-
const set = new Set(Array.isArray(value) ? value : []);
|
|
53
|
-
Array.from(el.options).forEach((o) => { o.selected = set.has(o.value); });
|
|
54
|
-
} else {
|
|
55
|
-
el.value = value ?? '';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function readField(name) {
|
|
60
|
-
const el = field(name);
|
|
61
|
-
if (!el) return undefined;
|
|
62
|
-
if (el.multiple) return Array.from(el.selectedOptions).map((o) => o.value);
|
|
63
|
-
if (el.type === 'number') return el.value === '' ? '' : Number(el.value);
|
|
64
|
-
return el.value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---- Load reference data + (on edit) the survey ---------------------
|
|
68
|
-
let survey = null;
|
|
69
|
-
let forms = [];
|
|
70
|
-
let groups = [];
|
|
71
|
-
try { forms = await api('/api/forms'); } catch { forms = []; }
|
|
72
|
-
try { groups = await api(`${BASE}/groups`); } catch { groups = []; }
|
|
73
|
-
if (isEdit) {
|
|
74
|
-
try {
|
|
75
|
-
survey = await api(`${BASE}/surveys/${encodeURIComponent(editSlug)}`);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
E.toast(err?.message ?? 'Failed to load survey.', {type: 'error'});
|
|
78
|
-
location.hash = '#/plugins/surveys';
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---- Populate dynamic <select> options ------------------------------
|
|
84
|
-
const formSelect = field('formSlug');
|
|
85
|
-
if (formSelect) {
|
|
86
|
-
forms.forEach((f) => {
|
|
87
|
-
const opt = document.createElement('option');
|
|
88
|
-
opt.value = f.slug;
|
|
89
|
-
opt.textContent = f.title || f.slug;
|
|
90
|
-
formSelect.appendChild(opt);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
// Audience uses group NAMES (sending.js reads groupIds as names).
|
|
94
|
-
const groupSelect = field('groupIds');
|
|
95
|
-
if (groupSelect) {
|
|
96
|
-
groups.forEach((g) => {
|
|
97
|
-
const opt = document.createElement('option');
|
|
98
|
-
opt.value = g.name;
|
|
99
|
-
opt.textContent = g.name;
|
|
100
|
-
groupSelect.appendChild(opt);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---- Seed values ----------------------------------------------------
|
|
105
|
-
const invite = survey?.inviteEmail ?? {};
|
|
106
|
-
setValue('title', survey?.title ?? '');
|
|
107
|
-
setValue('slug', survey?.slug ?? '');
|
|
108
|
-
setValue('description', survey?.description ?? '');
|
|
109
|
-
setValue('formSlug', survey?.formSlug ?? '');
|
|
110
|
-
setValue('mode', survey?.mode ?? 'identified');
|
|
111
|
-
setValue('status', survey?.status ?? 'draft');
|
|
112
|
-
setValue('groupIds', Array.isArray(survey?.groupIds) ? survey.groupIds : []);
|
|
113
|
-
setValue('opensAt', survey?.opensAt ?? '');
|
|
114
|
-
setValue('closesAt', survey?.closesAt ?? '');
|
|
115
|
-
setValue('reminderAfterDays', survey?.reminderAfterDays ?? 7);
|
|
116
|
-
setValue('intro', survey?.intro ?? '');
|
|
117
|
-
setValue('footer', survey?.footer ?? '');
|
|
118
|
-
setValue('thankYou', survey?.thankYou ?? '');
|
|
119
|
-
setValue('inviteSubject', invite.subject ?? '');
|
|
120
|
-
setValue('inviteBody', invite.body ?? '');
|
|
121
|
-
|
|
122
|
-
// Slug is immutable after creation.
|
|
123
|
-
if (isEdit) {
|
|
124
|
-
const slugEl = field('slug');
|
|
125
|
-
if (slugEl) slugEl.disabled = true;
|
|
126
|
-
const hint = root.querySelector('#slug-hint');
|
|
127
|
-
if (hint) hint.textContent = 'Slug cannot be changed after creation.';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---- Collect + save -------------------------------------------------
|
|
131
|
-
function collect() {
|
|
132
|
-
return {
|
|
133
|
-
title: (readField('title') ?? '').trim(),
|
|
134
|
-
slug: (readField('slug') ?? '').trim(),
|
|
135
|
-
description: (readField('description') ?? '').trim(),
|
|
136
|
-
formSlug: readField('formSlug') ?? '',
|
|
137
|
-
mode: readField('mode') ?? 'identified',
|
|
138
|
-
status: readField('status') ?? 'draft',
|
|
139
|
-
groupIds: readField('groupIds') ?? [],
|
|
140
|
-
opensAt: readField('opensAt') ?? '',
|
|
141
|
-
closesAt: readField('closesAt') ?? '',
|
|
142
|
-
reminderAfterDays: Number(readField('reminderAfterDays')) || 0,
|
|
143
|
-
intro: (readField('intro') ?? '').trim(),
|
|
144
|
-
footer: (readField('footer') ?? '').trim(),
|
|
145
|
-
thankYou: (readField('thankYou') ?? '').trim(),
|
|
146
|
-
inviteEmail: {
|
|
147
|
-
subject: (readField('inviteSubject') ?? '').trim(),
|
|
148
|
-
body: (readField('inviteBody') ?? '').trim()
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function save() {
|
|
154
|
-
const body = collect();
|
|
155
|
-
if (!body.title) { E.toast('Title is required.', {type: 'error'}); return; }
|
|
156
|
-
if (!isEdit && !body.slug) { E.toast('Slug is required.', {type: 'error'}); return; }
|
|
157
|
-
try {
|
|
158
|
-
if (isEdit) {
|
|
159
|
-
const {slug, ...patch} = body; // slug immutable
|
|
160
|
-
await api(`${BASE}/surveys/${encodeURIComponent(editSlug)}`, 'PUT', patch);
|
|
161
|
-
E.toast('Survey updated.', {type: 'success'});
|
|
162
|
-
} else {
|
|
163
|
-
await api(`${BASE}/surveys`, 'POST', body);
|
|
164
|
-
E.toast('Survey created.', {type: 'success'});
|
|
165
|
-
}
|
|
166
|
-
location.hash = '#/plugins/surveys';
|
|
167
|
-
} catch (err) {
|
|
168
|
-
E.toast(err?.message ?? 'Failed to save survey.', {type: 'error'});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const saveBtn = root.querySelector('#save-btn');
|
|
173
|
-
if (saveBtn) {
|
|
174
|
-
saveBtn.textContent = isEdit ? 'Update Survey' : 'Create Survey';
|
|
175
|
-
saveBtn.addEventListener('click', save);
|
|
176
|
-
}
|
|
177
|
-
const goBack = () => { location.hash = '#/plugins/surveys'; };
|
|
178
|
-
root.querySelector('#cancel-btn')?.addEventListener('click', goBack);
|
|
179
|
-
root.querySelector('#back-btn')?.addEventListener('click', goBack);
|
|
180
|
-
|
|
181
|
-
// ---- Send / reminder (edit + open only) -----------------------------
|
|
182
|
-
const sendPanel = root.querySelector('#send-panel');
|
|
183
|
-
const sendHint = root.querySelector('#send-hint');
|
|
184
|
-
if (sendPanel && isEdit) {
|
|
185
|
-
sendPanel.style.display = '';
|
|
186
|
-
if (sendHint) {
|
|
187
|
-
sendHint.textContent = survey?.status === 'open'
|
|
188
|
-
? ''
|
|
189
|
-
: 'Survey must be Open before invitations are meaningful.';
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function send(audience, label) {
|
|
194
|
-
const ok = await E.confirm(`${label}?`);
|
|
195
|
-
if (!ok) return;
|
|
196
|
-
try {
|
|
197
|
-
const res = await api(`${BASE}/surveys/${encodeURIComponent(editSlug)}/send`, 'POST', {audience});
|
|
198
|
-
const results = Array.isArray(res?.results) ? res.results : [];
|
|
199
|
-
const sent = results.filter((r) => r.ok).length;
|
|
200
|
-
E.toast(`${sent} of ${results.length} invitation(s) sent.`, {type: 'success'});
|
|
201
|
-
} catch (err) {
|
|
202
|
-
E.toast(err?.message ?? 'Send failed.', {type: 'error'});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
root.querySelector('#send-all-btn')?.addEventListener('click', () => send('all', 'Send invitations to the whole audience'));
|
|
207
|
-
root.querySelector('#send-reminder-btn')?.addEventListener('click', () => send('non-responders', 'Send a reminder to non-responders'));
|
|
208
|
-
|
|
209
|
-
if (typeof Domma !== 'undefined' && Domma.icons) Domma.icons.scan();
|
|
210
|
-
}
|
|
211
|
-
};
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surveys plugin - campaign list view.
|
|
3
|
-
*
|
|
4
|
-
* Lists survey campaigns in a Domma table with status/mode badges and
|
|
5
|
-
* row actions that navigate to the editor and results views.
|
|
6
|
-
*
|
|
7
|
-
* @module surveys/admin/views/surveys
|
|
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
|
-
const STATUS_VARIANT = {draft: 'badge-secondary', open: 'badge-success', closed: 'badge-warning'};
|
|
50
|
-
|
|
51
|
-
export const surveysView = {
|
|
52
|
-
templateUrl: '/plugins/surveys/admin/templates/surveys.html',
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Mount the surveys list view.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} $container - Domma-wrapped container element
|
|
58
|
-
* @returns {Promise<void>}
|
|
59
|
-
*/
|
|
60
|
-
async onMount($container) {
|
|
61
|
-
let surveys = [];
|
|
62
|
-
let table = null;
|
|
63
|
-
|
|
64
|
-
async function load() {
|
|
65
|
-
try {
|
|
66
|
-
surveys = await api(`${BASE}/surveys`);
|
|
67
|
-
} catch (err) {
|
|
68
|
-
surveys = [];
|
|
69
|
-
E.toast(err?.message ?? 'Failed to load surveys.', {type: 'error'});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function build() {
|
|
74
|
-
const tableEl = $container.find('#surveys-table').get(0);
|
|
75
|
-
const emptyEl = $container.find('#surveys-empty').get(0);
|
|
76
|
-
|
|
77
|
-
if (!surveys.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(surveys);
|
|
87
|
-
Domma.icons.scan();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Render functions MUST return HTML strings (T.create contract).
|
|
92
|
-
// Action buttons carry data-slug + a class; DOMPurify (run by the
|
|
93
|
-
// table renderer) strips inline onclick, so clicks are wired via
|
|
94
|
-
// unnamespaced delegation on $container after the table mounts.
|
|
95
|
-
table = T.create($container.find('#surveys-table').get(0), {
|
|
96
|
-
data: surveys,
|
|
97
|
-
columns: [
|
|
98
|
-
{
|
|
99
|
-
key: 'title',
|
|
100
|
-
title: 'Title',
|
|
101
|
-
sortable: true,
|
|
102
|
-
render: (v) => `<strong>${esc(v)}</strong>`
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
key: 'status',
|
|
106
|
-
title: 'Status',
|
|
107
|
-
sortable: true,
|
|
108
|
-
render: (v) => {
|
|
109
|
-
const variant = STATUS_VARIANT[v] || 'badge-secondary';
|
|
110
|
-
return `<span class="badge ${variant}">${esc(v || 'draft')}</span>`;
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
key: 'mode',
|
|
115
|
-
title: 'Mode',
|
|
116
|
-
sortable: true,
|
|
117
|
-
render: (v) => esc(v || 'identified')
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
key: 'slug',
|
|
121
|
-
title: 'Actions',
|
|
122
|
-
sortable: false,
|
|
123
|
-
render: (slug) => {
|
|
124
|
-
const s = esc(slug);
|
|
125
|
-
return `<button class="btn btn-sm btn-outline btn-edit-survey" ` +
|
|
126
|
-
`data-slug="${s}" style="margin-right:4px;">` +
|
|
127
|
-
`<span data-icon="edit" data-icon-size="14"></span> Edit</button>` +
|
|
128
|
-
`<button class="btn btn-sm btn-outline btn-results-survey" ` +
|
|
129
|
-
`data-slug="${s}">` +
|
|
130
|
-
`<span data-icon="bar-chart" data-icon-size="14"></span> Results</button>`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
],
|
|
134
|
-
emptyMessage: 'No surveys yet.'
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
Domma.icons.scan();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Toolbar wiring
|
|
141
|
-
const newBtn = $container.find('#new-survey').get(0);
|
|
142
|
-
if (newBtn) newBtn.addEventListener('click', () => { location.hash = '#/plugins/surveys/new'; });
|
|
143
|
-
|
|
144
|
-
const audienceBtn = $container.find('#manage-audience').get(0);
|
|
145
|
-
if (audienceBtn) audienceBtn.addEventListener('click', () => { location.hash = '#/plugins/surveys/audience'; });
|
|
146
|
-
|
|
147
|
-
// Row-action delegation (proven pattern - see admin/js/views/api-tokens.js).
|
|
148
|
-
// Unnamespaced + idempotent via .off()/.on(); namespaced delegation is
|
|
149
|
-
// broken in this codebase.
|
|
150
|
-
$container.off('click', '.btn-edit-survey').on('click', '.btn-edit-survey', function () {
|
|
151
|
-
location.hash = '#/plugins/surveys/edit/' + this.getAttribute('data-slug');
|
|
152
|
-
});
|
|
153
|
-
$container.off('click', '.btn-results-survey').on('click', '.btn-results-survey', function () {
|
|
154
|
-
location.hash = '#/plugins/surveys/results/' + this.getAttribute('data-slug');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
await load();
|
|
158
|
-
build();
|
|
159
|
-
Domma.icons.scan();
|
|
160
|
-
}
|
|
161
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "surveys-contacts",
|
|
3
|
-
"title": "Survey Contacts",
|
|
4
|
-
"description": "Shared survey audience contacts.",
|
|
5
|
-
"plugin": "surveys",
|
|
6
|
-
"storage": {"adapter": "mongodb", "connection": "default"},
|
|
7
|
-
"fields": [
|
|
8
|
-
{"name": "name", "label": "Name", "type": "text", "required": false},
|
|
9
|
-
{"name": "email", "label": "Email", "type": "text", "required": true},
|
|
10
|
-
{"name": "groups","label": "Groups","type": "text", "required": false},
|
|
11
|
-
{"name": "meta", "label": "Meta", "type": "textarea", "required": false}
|
|
12
|
-
]
|
|
13
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "surveys-groups",
|
|
3
|
-
"title": "Survey Groups",
|
|
4
|
-
"description": "Named survey audience groups.",
|
|
5
|
-
"plugin": "surveys",
|
|
6
|
-
"storage": {"adapter": "mongodb", "connection": "default"},
|
|
7
|
-
"fields": [
|
|
8
|
-
{"name": "name", "label": "Name", "type": "text", "required": true},
|
|
9
|
-
{"name": "description", "label": "Description", "type": "textarea", "required": false}
|
|
10
|
-
]
|
|
11
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "surveys-invites",
|
|
3
|
-
"title": "Survey Invites",
|
|
4
|
-
"description": "Per-recipient invite ledger (token hashes + status).",
|
|
5
|
-
"plugin": "surveys",
|
|
6
|
-
"storage": {"adapter": "mongodb", "connection": "default"},
|
|
7
|
-
"fields": [
|
|
8
|
-
{"name": "surveyId", "label": "Survey", "type": "text", "required": true},
|
|
9
|
-
{"name": "contactId", "label": "Contact", "type": "text", "required": true},
|
|
10
|
-
{"name": "tokenHash", "label": "Token Hash", "type": "text", "required": true},
|
|
11
|
-
{"name": "status", "label": "Status", "type": "text", "required": true},
|
|
12
|
-
{"name": "sentAt", "label": "Sent At", "type": "text", "required": false},
|
|
13
|
-
{"name": "completedAt", "label": "Completed At","type": "text", "required": false},
|
|
14
|
-
{"name": "responseId", "label": "Response", "type": "text", "required": false}
|
|
15
|
-
]
|
|
16
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "surveys-surveys",
|
|
3
|
-
"title": "Surveys",
|
|
4
|
-
"description": "Survey campaigns.",
|
|
5
|
-
"plugin": "surveys",
|
|
6
|
-
"storage": {"adapter": "mongodb", "connection": "default"},
|
|
7
|
-
"fields": [
|
|
8
|
-
{"name": "slug", "label": "Slug", "type": "text", "required": true},
|
|
9
|
-
{"name": "title", "label": "Title", "type": "text", "required": true},
|
|
10
|
-
{"name": "description", "label": "Description", "type": "textarea", "required": false},
|
|
11
|
-
{"name": "formSlug", "label": "Form", "type": "text", "required": true},
|
|
12
|
-
{"name": "groupIds", "label": "Groups", "type": "text", "required": false},
|
|
13
|
-
{"name": "mode", "label": "Mode", "type": "text", "required": true},
|
|
14
|
-
{"name": "status", "label": "Status", "type": "text", "required": true},
|
|
15
|
-
{"name": "opensAt", "label": "Opens At", "type": "text", "required": false},
|
|
16
|
-
{"name": "closesAt", "label": "Closes At", "type": "text", "required": false},
|
|
17
|
-
{"name": "reminderAfterDays", "label": "Reminder (days)", "type": "text", "required": false},
|
|
18
|
-
{"name": "intro", "label": "Intro (Markdown)", "type": "textarea", "required": false},
|
|
19
|
-
{"name": "footer", "label": "Footer (Markdown)", "type": "textarea", "required": false},
|
|
20
|
-
{"name": "thankYou", "label": "Thank-you message", "type": "textarea", "required": false},
|
|
21
|
-
{"name": "inviteEmail", "label": "Invite Email", "type": "textarea", "required": false}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
// Pro: Mongo when config/connections.json exists; degrades to file otherwise.
|
|
3
|
-
storage: {adapter: 'mongodb', connection: 'default'},
|
|
4
|
-
// Service token (dcms_...) the external scheduler presents to /due-work + /run.
|
|
5
|
-
// Empty disables the scheduler endpoints (403). Set in config/plugins.json.
|
|
6
|
-
schedulerToken: '',
|
|
7
|
-
fromName: 'Surveys'
|
|
8
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const C = 'surveys-contacts';
|
|
2
|
-
|
|
3
|
-
function parseGroups(raw) {
|
|
4
|
-
if (Array.isArray(raw)) return raw;
|
|
5
|
-
if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return raw ? [raw] : []; } }
|
|
6
|
-
return [];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function createAudience(col) {
|
|
10
|
-
async function listContacts() {
|
|
11
|
-
const {entries} = await col.listEntries(C, {limit: 100000});
|
|
12
|
-
return entries.map(e => ({id: e.id, name: e.data.name ?? '', email: e.data.email ?? '', groups: parseGroups(e.data.groups)}));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function contactIdsForGroups(groupNames) {
|
|
16
|
-
const want = new Set(groupNames);
|
|
17
|
-
const contacts = await listContacts();
|
|
18
|
-
return contacts.filter(c => c.groups.some(g => want.has(g))).map(c => c.id);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function importFromContacts(sourceContacts) {
|
|
22
|
-
let imported = 0;
|
|
23
|
-
for (const c of sourceContacts) {
|
|
24
|
-
const email = (c.email ?? '').trim();
|
|
25
|
-
if (!email) continue;
|
|
26
|
-
await col.createEntry(C, {
|
|
27
|
-
name: (c.name ?? '').trim(),
|
|
28
|
-
email,
|
|
29
|
-
groups: parseGroups(c.groups)
|
|
30
|
-
}, {source: 'import-contacts'});
|
|
31
|
-
imported++;
|
|
32
|
-
}
|
|
33
|
-
return {imported};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {listContacts, contactIdsForGroups, importFromContacts};
|
|
37
|
-
}
|