domma-cms 0.2.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/admin/css/admin.css +1 -1200
- package/admin/dist/domma/domma-tools.css +2313 -0
- package/admin/dist/domma/domma-tools.min.js +10 -0
- package/admin/index.html +4 -0
- package/admin/js/api.js +1 -242
- package/admin/js/app.js +9 -279
- package/admin/js/config/sidebar-config.js +1 -115
- package/admin/js/lib/card.js +1 -63
- package/admin/js/lib/image-editor.js +1 -869
- package/admin/js/lib/markdown-toolbar.js +54 -421
- package/admin/js/templates/action-editor.html +171 -0
- package/admin/js/templates/actions-list.html +19 -0
- package/admin/js/templates/api-reference.html +1411 -0
- package/admin/js/templates/block-editor.html +158 -0
- package/admin/js/templates/blocks.html +8 -0
- package/admin/js/templates/collection-editor.html +47 -0
- package/admin/js/templates/collection-entries.html +3 -0
- package/admin/js/templates/collections.html +51 -4
- package/admin/js/templates/documentation.html +258 -0
- package/admin/js/templates/form-editor.html +238 -0
- package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
- package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
- package/admin/js/templates/layouts.html +44 -7
- package/admin/js/templates/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +48 -0
- package/admin/js/templates/pages.html +6 -1
- package/admin/js/templates/pro-docs.html +259 -0
- package/admin/js/templates/role-editor.html +59 -0
- package/admin/js/templates/roles.html +10 -0
- package/admin/js/templates/settings.html +137 -18
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -1
- package/admin/js/templates/view-editor.html +201 -0
- package/admin/js/templates/view-preview.html +51 -0
- package/admin/js/templates/views-list.html +19 -0
- package/admin/js/views/action-editor.js +1 -0
- package/admin/js/views/actions-list.js +1 -0
- package/admin/js/views/api-reference.js +1 -0
- package/admin/js/views/block-editor.js +8 -0
- package/admin/js/views/blocks.js +4 -0
- package/admin/js/views/collection-editor.js +3 -487
- package/admin/js/views/collection-entries.js +1 -484
- package/admin/js/views/collections.js +1 -153
- package/admin/js/views/dashboard.js +1 -56
- package/admin/js/views/documentation.js +1 -12
- package/admin/js/views/form-editor.js +8 -0
- package/admin/js/views/form-submissions.js +1 -0
- package/admin/js/views/forms.js +1 -0
- package/admin/js/views/index.js +1 -39
- package/admin/js/views/layouts.js +9 -42
- package/admin/js/views/login.js +7 -251
- package/admin/js/views/media.js +1 -240
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/navigation.js +14 -212
- package/admin/js/views/page-editor.js +72 -661
- package/admin/js/views/pages.js +5 -72
- package/admin/js/views/plugins.js +13 -90
- package/admin/js/views/pro-docs.js +1 -0
- package/admin/js/views/role-editor.js +1 -0
- package/admin/js/views/roles.js +4 -0
- package/admin/js/views/settings.js +3 -199
- package/admin/js/views/tutorials.js +1 -12
- package/admin/js/views/user-editor.js +1 -88
- package/admin/js/views/users.js +4 -76
- package/admin/js/views/view-editor.js +1 -0
- package/admin/js/views/view-preview.js +1 -0
- package/admin/js/views/views-list.js +1 -0
- package/bin/cli.js +1 -1
- package/config/auth.json +2 -17
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/navigation.json +15 -0
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +17 -6
- package/package.json +24 -10
- package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
- package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
- package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
- package/plugins/domma-effects/public/celebrations/index.js +1 -535
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
- package/plugins/example-analytics/stats.json +21 -12
- package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/plugin.js +233 -0
- package/plugins/theme-roller/plugin.json +31 -0
- package/plugins/theme-roller/public/active-theme.css +0 -0
- package/plugins/theme-roller/public/inject-head-late.html +1 -0
- package/public/css/forms.css +1 -0
- package/public/css/site.css +1 -302
- package/public/js/btt.js +1 -90
- package/public/js/cookie-consent.js +1 -61
- package/public/js/form-logic-engine.js +1 -0
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -204
- package/scripts/build.js +194 -129
- package/scripts/pro.js +254 -0
- package/scripts/reset.js +33 -8
- package/scripts/seed.js +343 -78
- package/scripts/setup.js +5 -4
- package/server/middleware/auth.js +136 -97
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -116
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +88 -23
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
- package/server/routes/api/layouts.js +49 -25
- package/server/routes/api/media.js +118 -93
- package/server/routes/api/navigation.js +40 -37
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -89
- package/server/routes/api/users.js +27 -21
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -173
- package/server/services/actions.js +387 -0
- package/server/services/adapterRegistry.js +98 -0
- package/server/services/adapters/FileAdapter.js +192 -0
- package/server/services/adapters/MongoAdapter.js +220 -0
- package/server/services/blocks.js +162 -0
- package/server/services/collections.js +74 -86
- package/server/services/connectionManager.js +102 -0
- package/server/services/content.js +312 -307
- package/{plugins/form-builder → server/services}/email.js +126 -103
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -648
- package/server/services/permissionRegistry.js +173 -0
- package/server/services/presetCollections.js +251 -0
- package/server/services/renderer.js +75 -1
- package/server/services/roles.js +227 -0
- package/server/services/rowAccess.js +104 -0
- package/server/services/userProfiles.js +199 -0
- package/server/services/users.js +281 -212
- package/server/services/views.js +280 -0
- package/server/templates/page.html +119 -113
- package/plugins/form-builder/admin/templates/form-editor.html +0 -171
- package/plugins/form-builder/admin/templates/form-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1442
- package/plugins/form-builder/admin/views/form-settings.js +0 -38
- package/plugins/form-builder/admin/views/form-submissions.js +0 -295
- package/plugins/form-builder/admin/views/forms-list.js +0 -164
- package/plugins/form-builder/config.js +0 -9
- package/plugins/form-builder/data/forms/consent.json +0 -104
- package/plugins/form-builder/data/forms/contact-details.json +0 -63
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/submissions/consent.json +0 -13
- package/plugins/form-builder/data/submissions/contact-details.json +0 -1
- package/plugins/form-builder/data/submissions/contacts.json +0 -26
- package/plugins/form-builder/plugin.json +0 -52
- package/plugins/form-builder/public/form-logic-engine.js +0 -568
- package/plugins/form-builder/public/inject-body.html +0 -352
- package/plugins/form-builder/public/inject-head.html +0 -58
- package/plugins/form-builder/public/package.json +0 -1
- package/scripts/copy-domma.js +0 -48
|
@@ -1,103 +1,126 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Nodemailer transport factory and generic form email sending utility.
|
|
4
|
-
*/
|
|
5
|
-
import nodemailer from 'nodemailer';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Escape HTML special characters for safe use in email bodies.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} str
|
|
11
|
-
* @returns {string}
|
|
12
|
-
*/
|
|
13
|
-
export function escapeHtml(str) {
|
|
14
|
-
return String(str)
|
|
15
|
-
.replace(/&/g, '&')
|
|
16
|
-
.replace(/</g, '<')
|
|
17
|
-
.replace(/>/g, '>')
|
|
18
|
-
.replace(/"/g, '"')
|
|
19
|
-
.replace(/'/g, ''');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Create a nodemailer transport.
|
|
24
|
-
* Falls back to an Ethereal test account when no SMTP host is configured.
|
|
25
|
-
*
|
|
26
|
-
* @param {{ host: string, port: number, secure: boolean, user: string, pass: string }} smtp
|
|
27
|
-
* @returns {Promise<import('nodemailer').Transporter>}
|
|
28
|
-
* @throws {Error} If Ethereal test account creation fails.
|
|
29
|
-
*/
|
|
30
|
-
export async function createTransport(smtp) {
|
|
31
|
-
if (smtp?.host) {
|
|
32
|
-
return nodemailer.createTransport({
|
|
33
|
-
host: smtp.host,
|
|
34
|
-
port: smtp.port || 587,
|
|
35
|
-
secure: smtp.secure || false,
|
|
36
|
-
auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// No SMTP configured — use Ethereal for dev/demo
|
|
41
|
-
const testAccount = await nodemailer.createTestAccount();
|
|
42
|
-
console.log('[
|
|
43
|
-
return nodemailer.createTransport({
|
|
44
|
-
host: 'smtp.ethereal.email',
|
|
45
|
-
port: 587,
|
|
46
|
-
secure: false,
|
|
47
|
-
auth: { user: testAccount.user, pass: testAccount.pass }
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Send
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @param {
|
|
56
|
-
* @
|
|
57
|
-
* @
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Core Email Utility
|
|
3
|
+
* Nodemailer transport factory and generic form email sending utility.
|
|
4
|
+
*/
|
|
5
|
+
import nodemailer from 'nodemailer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Escape HTML special characters for safe use in email bodies.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} str
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function escapeHtml(str) {
|
|
14
|
+
return String(str)
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a nodemailer transport.
|
|
24
|
+
* Falls back to an Ethereal test account when no SMTP host is configured.
|
|
25
|
+
*
|
|
26
|
+
* @param {{ host: string, port: number, secure: boolean, user: string, pass: string }} smtp
|
|
27
|
+
* @returns {Promise<import('nodemailer').Transporter>}
|
|
28
|
+
* @throws {Error} If Ethereal test account creation fails.
|
|
29
|
+
*/
|
|
30
|
+
export async function createTransport(smtp) {
|
|
31
|
+
if (smtp?.host) {
|
|
32
|
+
return nodemailer.createTransport({
|
|
33
|
+
host: smtp.host,
|
|
34
|
+
port: smtp.port || 587,
|
|
35
|
+
secure: smtp.secure || false,
|
|
36
|
+
auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// No SMTP configured — use Ethereal for dev/demo
|
|
41
|
+
const testAccount = await nodemailer.createTestAccount();
|
|
42
|
+
console.log('[email] No SMTP configured. Using Ethereal test account:', testAccount.user);
|
|
43
|
+
return nodemailer.createTransport({
|
|
44
|
+
host: 'smtp.ethereal.email',
|
|
45
|
+
port: 587,
|
|
46
|
+
secure: false,
|
|
47
|
+
auth: { user: testAccount.user, pass: testAccount.pass }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Send a generic transactional email.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('nodemailer').Transporter} transport
|
|
55
|
+
* @param {{ from: string, fromName: string, to: string, subject: string, html: string, text: string }} opts
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
* @throws {Error} If sending the email fails.
|
|
58
|
+
*/
|
|
59
|
+
export async function sendEmail(transport, {from, fromName, to, subject, html, text}) {
|
|
60
|
+
const info = await transport.sendMail({
|
|
61
|
+
from: `"${fromName}" <${from}>`,
|
|
62
|
+
to,
|
|
63
|
+
subject,
|
|
64
|
+
text,
|
|
65
|
+
html
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const previewUrl = nodemailer.getTestMessageUrl(info);
|
|
69
|
+
if (previewUrl) {
|
|
70
|
+
console.log('[email] Preview URL:', previewUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Send an HTML + plain-text form submission notification email.
|
|
76
|
+
* Builds a generic table of field→value pairs from the submitted data.
|
|
77
|
+
*
|
|
78
|
+
* @param {import('nodemailer').Transporter} transport
|
|
79
|
+
* @param {{ from: string, fromName: string, to: string, subject: string, formTitle: string, fields: Array<{name: string, label: string}>, data: Record<string, unknown> }} opts
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
* @throws {Error} If sending the email fails.
|
|
82
|
+
*/
|
|
83
|
+
export async function sendFormEmail(transport, { from, fromName, to, subject, formTitle, fields, data }) {
|
|
84
|
+
const rows = fields.map(field => {
|
|
85
|
+
const val = data[field.name] ?? '';
|
|
86
|
+
const safe = escapeHtml(String(val)).replace(/\n/g, '<br>');
|
|
87
|
+
const safeLabel = escapeHtml(field.label || field.name);
|
|
88
|
+
return `
|
|
89
|
+
<tr>
|
|
90
|
+
<td style="padding:8px 12px;font-weight:600;background:#f9f9f9;border:1px solid #eee;white-space:nowrap;vertical-align:top;">${safeLabel}</td>
|
|
91
|
+
<td style="padding:8px 12px;border:1px solid #eee;vertical-align:top;">${safe}</td>
|
|
92
|
+
</tr>`.trim();
|
|
93
|
+
}).join('\n');
|
|
94
|
+
|
|
95
|
+
const plainRows = fields.map(field => {
|
|
96
|
+
const val = data[field.name] ?? '';
|
|
97
|
+
return `${field.label || field.name}: ${val}`;
|
|
98
|
+
}).join('\n');
|
|
99
|
+
|
|
100
|
+
const html = `
|
|
101
|
+
<!DOCTYPE html>
|
|
102
|
+
<html>
|
|
103
|
+
<body style="font-family:sans-serif;max-width:640px;margin:0 auto;padding:20px;">
|
|
104
|
+
<h2 style="color:#333;margin-bottom:4px;">New Form Submission</h2>
|
|
105
|
+
<p style="color:#888;margin-top:0;font-size:.9rem;">${escapeHtml(formTitle)}</p>
|
|
106
|
+
<table style="width:100%;border-collapse:collapse;margin-top:16px;">
|
|
107
|
+
${rows}
|
|
108
|
+
</table>
|
|
109
|
+
</body>
|
|
110
|
+
</html>`.trim();
|
|
111
|
+
|
|
112
|
+
const text = `New form submission: ${formTitle}\n\n${plainRows}`;
|
|
113
|
+
|
|
114
|
+
const info = await transport.sendMail({
|
|
115
|
+
from: `"${fromName}" <${from}>`,
|
|
116
|
+
to,
|
|
117
|
+
subject,
|
|
118
|
+
text,
|
|
119
|
+
html
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const previewUrl = nodemailer.getTestMessageUrl(info);
|
|
123
|
+
if (previewUrl) {
|
|
124
|
+
console.log('[email] Preview URL:', previewUrl);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Forms Service
|
|
3
|
+
* CRUD operations for form definitions stored in content/forms/.
|
|
4
|
+
* Submissions are stored exclusively in Collections (slug matching form slug).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {fileURLToPath} from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
12
|
+
export const FORMS_DIR = path.join(ROOT, 'content', 'forms');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure the forms directory exists.
|
|
16
|
+
*
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureFormsDir() {
|
|
20
|
+
await fs.mkdir(FORMS_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read a single form definition by slug.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} slug
|
|
27
|
+
* @returns {Promise<object>}
|
|
28
|
+
* @throws {Error} If the form file does not exist or cannot be parsed.
|
|
29
|
+
*/
|
|
30
|
+
export async function readForm(slug) {
|
|
31
|
+
const file = path.join(FORMS_DIR, `${slug}.json`);
|
|
32
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write a form definition to disk.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} slug
|
|
39
|
+
* @param {object} data
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
export async function writeForm(slug, data) {
|
|
43
|
+
await ensureFormsDir();
|
|
44
|
+
const file = path.join(FORMS_DIR, `${slug}.json`);
|
|
45
|
+
await fs.writeFile(file, JSON.stringify(data, null, 4) + '\n', 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List all form definitions.
|
|
50
|
+
*
|
|
51
|
+
* @returns {Promise<object[]>}
|
|
52
|
+
*/
|
|
53
|
+
export async function listForms() {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = await fs.readdir(FORMS_DIR);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const forms = [];
|
|
61
|
+
for (const entry of entries.filter(e => e.endsWith('.json'))) {
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(await fs.readFile(path.join(FORMS_DIR, entry), 'utf8'));
|
|
64
|
+
forms.push(data);
|
|
65
|
+
} catch {
|
|
66
|
+
// skip malformed
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return forms;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete a form definition by slug.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} slug
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
* @throws {Error} If the form file does not exist.
|
|
78
|
+
*/
|
|
79
|
+
export async function deleteForm(slug) {
|
|
80
|
+
await fs.unlink(path.join(FORMS_DIR, `${slug}.json`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert a string to a URL-friendly slug.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} str
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function slugify(str) {
|
|
90
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** System collections that should never have an auto-generated public form. */
|
|
94
|
+
const NO_FORM_SLUGS = new Set(['roles', 'user-profiles']);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Collection field type → form field type mapping.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} type
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function toFormFieldType(type) {
|
|
103
|
+
const map = {
|
|
104
|
+
string: 'string', text: 'string', email: 'email', tel: 'tel',
|
|
105
|
+
number: 'number', textarea: 'textarea', select: 'select', radio: 'radio',
|
|
106
|
+
checkbox: 'checkbox', 'checkbox-group': 'checkbox-group',
|
|
107
|
+
date: 'date', time: 'time', url: 'url', hidden: 'hidden'
|
|
108
|
+
};
|
|
109
|
+
return map[type] || 'string';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build a form definition object from a collection schema.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} schema - Collection schema
|
|
116
|
+
* @returns {object} Form definition
|
|
117
|
+
*/
|
|
118
|
+
function buildFormFromCollection(schema) {
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
const fields = (schema.fields || []).map(f => {
|
|
121
|
+
const field = {
|
|
122
|
+
name: f.name,
|
|
123
|
+
type: toFormFieldType(f.type),
|
|
124
|
+
label: f.label || f.name,
|
|
125
|
+
required: !!f.required,
|
|
126
|
+
placeholder: f.placeholder || '',
|
|
127
|
+
helper: f.helper || ''
|
|
128
|
+
};
|
|
129
|
+
if (f.options) field.options = f.options;
|
|
130
|
+
if (f.validation) field.validation = f.validation;
|
|
131
|
+
return field;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
slug: schema.slug,
|
|
136
|
+
title: schema.title,
|
|
137
|
+
description: schema.description || '',
|
|
138
|
+
fields,
|
|
139
|
+
settings: {
|
|
140
|
+
submitText: 'Submit',
|
|
141
|
+
successMessage: 'Thank you for your submission.',
|
|
142
|
+
layout: 'stacked',
|
|
143
|
+
honeypot: true,
|
|
144
|
+
rateLimitPerMinute: 5
|
|
145
|
+
},
|
|
146
|
+
actions: {
|
|
147
|
+
email: {enabled: false, recipients: '', subjectPrefix: `[${schema.slug}]`},
|
|
148
|
+
webhook: {enabled: false, url: '', method: 'POST'},
|
|
149
|
+
collection: {enabled: true, slug: schema.slug}
|
|
150
|
+
},
|
|
151
|
+
createdAt: now,
|
|
152
|
+
updatedAt: now
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Ensure a form exists for the given collection schema.
|
|
158
|
+
* Creates the form only if absent — never overwrites an existing one.
|
|
159
|
+
* Skips system collections (roles, user-profiles).
|
|
160
|
+
*
|
|
161
|
+
* @param {object} schema - Collection schema
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
export async function ensureFormForCollection(schema) {
|
|
165
|
+
if (NO_FORM_SLUGS.has(schema.slug)) return;
|
|
166
|
+
await ensureFormsDir();
|
|
167
|
+
const filePath = path.join(FORMS_DIR, `${schema.slug}.json`);
|
|
168
|
+
try {
|
|
169
|
+
await fs.access(filePath);
|
|
170
|
+
} catch {
|
|
171
|
+
await writeForm(schema.slug, buildFormFromCollection(schema));
|
|
172
|
+
}
|
|
173
|
+
}
|