domma-cms 0.3.0 → 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 -1
- 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 -1
- package/admin/js/app.js +8 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +18 -10
- 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/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
- 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/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +39 -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 +123 -21
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -26
- 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 -3
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- 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 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/media.js +1 -1
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/page-editor.js +34 -15
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +10 -10
- 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 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +4 -7
- 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 +1 -0
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +12 -2
- package/package.json +24 -10
- package/plugins/example-analytics/stats.json +17 -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 -1
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -1
- 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 +1 -0
- package/server/middleware/auth.js +136 -120
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -146
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +79 -27
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
- package/server/routes/api/layouts.js +49 -39
- package/server/routes/api/media.js +118 -92
- package/server/routes/api/navigation.js +40 -36
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -88
- package/server/routes/api/users.js +27 -19
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -181
- 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/server/services/email.js +126 -0
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -747
- 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-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1444
- 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 -99
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/feedback.json +0 -130
- 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/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/plugin.json +0 -52
- 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
- package/server/services/userTypes.js +0 -167
- /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views Service (Pro — requires MongoDB)
|
|
3
|
+
*
|
|
4
|
+
* View configs are stored in MongoDB `cms__views` on the 'default' connection.
|
|
5
|
+
* The `connection` field in each view config specifies which connection to use
|
|
6
|
+
* when executing the aggregation pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Allowed aggregation stage types (Phase 1):
|
|
9
|
+
* $match, $lookup, $sort, $project, $unwind, $limit, $skip, $count,
|
|
10
|
+
* $addFields, $group
|
|
11
|
+
*
|
|
12
|
+
* Forbidden stage types:
|
|
13
|
+
* $out, $merge, $function, $accumulator, $graphLookup
|
|
14
|
+
*/
|
|
15
|
+
import {v4 as uuidv4} from 'uuid';
|
|
16
|
+
import {buildRowLevelMatch} from './rowAccess.js';
|
|
17
|
+
|
|
18
|
+
/** MongoDB collection where view configs are stored. */
|
|
19
|
+
const VIEWS_COLLECTION = 'cms__views';
|
|
20
|
+
|
|
21
|
+
/** Prefix applied to CMS data collections — mirrors MongoAdapter.PREFIX. */
|
|
22
|
+
const COLLECTION_PREFIX = 'cms_';
|
|
23
|
+
|
|
24
|
+
const ALLOWED_STAGES = new Set([
|
|
25
|
+
'$match', '$lookup', '$sort', '$project',
|
|
26
|
+
'$unwind', '$limit', '$skip', '$count',
|
|
27
|
+
'$addFields', '$group'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const FORBIDDEN_STAGES = new Set([
|
|
31
|
+
'$out', '$merge', '$function', '$accumulator', '$graphLookup'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the Db instance for storing/reading view configs ('default' connection).
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<import('mongodb').Db>}
|
|
42
|
+
* @throws {Error} If MongoDB is not configured
|
|
43
|
+
*/
|
|
44
|
+
async function getMetaDb() {
|
|
45
|
+
try {
|
|
46
|
+
const { getDb } = await import('./connectionManager.js');
|
|
47
|
+
return getDb('default');
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error('Views require a MongoDB connection. Configure a "default" connection in pro mode.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Slugify a string to a URL-safe identifier.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} str
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function slugify(str) {
|
|
60
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate that all pipeline stages use allowed types.
|
|
65
|
+
*
|
|
66
|
+
* @param {object[]} stages
|
|
67
|
+
* @throws {Error} If a stage type is forbidden or not in the allowlist
|
|
68
|
+
*/
|
|
69
|
+
function validateStages(stages) {
|
|
70
|
+
for (const stage of stages) {
|
|
71
|
+
if (!stage.type) throw new Error('Pipeline stage missing "type"');
|
|
72
|
+
if (FORBIDDEN_STAGES.has(stage.type)) {
|
|
73
|
+
throw new Error(`Stage type "${stage.type}" is not permitted for security reasons`);
|
|
74
|
+
}
|
|
75
|
+
if (!ALLOWED_STAGES.has(stage.type)) {
|
|
76
|
+
throw new Error(`Stage type "${stage.type}" is not in the allowed list`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert stage descriptors into MongoDB aggregation stage objects.
|
|
83
|
+
*
|
|
84
|
+
* @param {object[]} stages
|
|
85
|
+
* @returns {object[]}
|
|
86
|
+
*/
|
|
87
|
+
function buildPipelineStages(stages) {
|
|
88
|
+
return stages.map(stage => ({ [stage.type]: stage.config }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// CRUD
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* List all view configs.
|
|
97
|
+
*
|
|
98
|
+
* @returns {Promise<object[]>}
|
|
99
|
+
* @throws {Error} If MongoDB is not available
|
|
100
|
+
*/
|
|
101
|
+
export async function listViews() {
|
|
102
|
+
const db = await getMetaDb();
|
|
103
|
+
return db.collection(VIEWS_COLLECTION)
|
|
104
|
+
.find({}, { projection: { _id: 0 } })
|
|
105
|
+
.sort({ 'meta.createdAt': -1 })
|
|
106
|
+
.toArray();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a single view config by slug.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} slug
|
|
113
|
+
* @returns {Promise<object|null>}
|
|
114
|
+
* @throws {Error} If MongoDB is not available
|
|
115
|
+
*/
|
|
116
|
+
export async function getView(slug) {
|
|
117
|
+
const db = await getMetaDb();
|
|
118
|
+
const doc = await db.collection(VIEWS_COLLECTION).findOne({ slug }, { projection: { _id: 0 } });
|
|
119
|
+
return doc || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a new view config.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} data
|
|
126
|
+
* @param {string} data.title
|
|
127
|
+
* @param {string} [data.slug]
|
|
128
|
+
* @param {string} [data.description]
|
|
129
|
+
* @param {string} [data.connection='default']
|
|
130
|
+
* @param {object} data.pipeline - { source: string, stages: object[] }
|
|
131
|
+
* @param {object} [data.display] - { mode, columns, pageSize }
|
|
132
|
+
* @param {object} [data.access] - { roles, public }
|
|
133
|
+
* @param {string|null} [userId]
|
|
134
|
+
* @returns {Promise<object>} Created view config
|
|
135
|
+
* @throws {Error} If slug already exists or validation fails
|
|
136
|
+
*/
|
|
137
|
+
export async function createView(data, userId = null) {
|
|
138
|
+
const db = await getMetaDb();
|
|
139
|
+
const { title, description = '', connection = 'default', pipeline, display, access } = data;
|
|
140
|
+
|
|
141
|
+
if (!title) throw new Error('title is required');
|
|
142
|
+
if (!pipeline?.source) throw new Error('pipeline.source is required');
|
|
143
|
+
|
|
144
|
+
const slug = data.slug ? slugify(data.slug) : slugify(title);
|
|
145
|
+
if (!slug) throw new Error('Could not derive a slug from the title');
|
|
146
|
+
|
|
147
|
+
const existing = await db.collection(VIEWS_COLLECTION).findOne({ slug });
|
|
148
|
+
if (existing) throw new Error(`A view with slug "${slug}" already exists`);
|
|
149
|
+
|
|
150
|
+
const stages = pipeline.stages || [];
|
|
151
|
+
validateStages(stages);
|
|
152
|
+
|
|
153
|
+
const now = new Date().toISOString();
|
|
154
|
+
const view = {
|
|
155
|
+
id: uuidv4(),
|
|
156
|
+
slug,
|
|
157
|
+
title: title.trim(),
|
|
158
|
+
description: description.trim(),
|
|
159
|
+
connection,
|
|
160
|
+
pipeline: { source: pipeline.source, stages },
|
|
161
|
+
display: {
|
|
162
|
+
mode: display?.mode || 'table',
|
|
163
|
+
columns: display?.columns || [],
|
|
164
|
+
pageSize: display?.pageSize || 25,
|
|
165
|
+
block: display?.block || ''
|
|
166
|
+
},
|
|
167
|
+
access: {
|
|
168
|
+
roles: access?.roles || ['admin'],
|
|
169
|
+
public: access?.public || false,
|
|
170
|
+
rowLevel: access?.rowLevel || null
|
|
171
|
+
},
|
|
172
|
+
meta: { createdAt: now, updatedAt: now, createdBy: userId }
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await db.collection(VIEWS_COLLECTION).insertOne({ ...view });
|
|
176
|
+
return view;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Update an existing view config.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} slug
|
|
183
|
+
* @param {object} data - Partial view fields to merge
|
|
184
|
+
* @returns {Promise<object>} Updated view config
|
|
185
|
+
* @throws {Error} If view not found or validation fails
|
|
186
|
+
*/
|
|
187
|
+
export async function updateView(slug, data) {
|
|
188
|
+
const db = await getMetaDb();
|
|
189
|
+
const existing = await db.collection(VIEWS_COLLECTION).findOne({ slug });
|
|
190
|
+
if (!existing) throw new Error(`View "${slug}" not found`);
|
|
191
|
+
|
|
192
|
+
const stages = data.pipeline?.stages ?? existing.pipeline?.stages ?? [];
|
|
193
|
+
validateStages(stages);
|
|
194
|
+
|
|
195
|
+
const { _id, id, meta, ...rest } = data;
|
|
196
|
+
const updated = {
|
|
197
|
+
...existing,
|
|
198
|
+
...rest,
|
|
199
|
+
slug,
|
|
200
|
+
pipeline: {
|
|
201
|
+
...existing.pipeline,
|
|
202
|
+
...data.pipeline,
|
|
203
|
+
stages
|
|
204
|
+
},
|
|
205
|
+
meta: { ...existing.meta, updatedAt: new Date().toISOString() }
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await db.collection(VIEWS_COLLECTION).replaceOne({ slug }, { ...updated });
|
|
209
|
+
const { _id: _stripped, ...result } = updated;
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Delete a view config.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} slug
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
* @throws {Error} If view not found
|
|
219
|
+
*/
|
|
220
|
+
export async function deleteView(slug) {
|
|
221
|
+
const db = await getMetaDb();
|
|
222
|
+
const result = await db.collection(VIEWS_COLLECTION).deleteOne({ slug });
|
|
223
|
+
if (result.deletedCount === 0) throw new Error(`View "${slug}" not found`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Execution
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Execute a view's aggregation pipeline and return paginated results.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} slug
|
|
234
|
+
* @param {object} [opts]
|
|
235
|
+
* @param {number} [opts.page=1]
|
|
236
|
+
* @param {number} [opts.limit=25]
|
|
237
|
+
* @param {object|null} [opts.user=null] - Authenticated user (for row-level filtering)
|
|
238
|
+
* @returns {Promise<{ results: object[], total: number, page: number, limit: number }>}
|
|
239
|
+
* @throws {Error} If view not found, connection unavailable, or stage validation fails
|
|
240
|
+
*/
|
|
241
|
+
export async function executeView(slug, {page = 1, limit = 25, user = null} = {}) {
|
|
242
|
+
const view = await getView(slug);
|
|
243
|
+
if (!view) throw new Error(`View "${slug}" not found`);
|
|
244
|
+
|
|
245
|
+
const { getDb } = await import('./connectionManager.js');
|
|
246
|
+
const db = getDb(view.connection || 'default');
|
|
247
|
+
|
|
248
|
+
const sourceCollection = `${COLLECTION_PREFIX}${view.pipeline.source}`;
|
|
249
|
+
const stages = view.pipeline?.stages || [];
|
|
250
|
+
validateStages(stages);
|
|
251
|
+
|
|
252
|
+
const pipeline = buildPipelineStages(stages);
|
|
253
|
+
|
|
254
|
+
// Prepend row-level $match if configured
|
|
255
|
+
const rowFilter = buildRowLevelMatch(user, view.access?.rowLevel);
|
|
256
|
+
if (rowFilter) {
|
|
257
|
+
pipeline.unshift({$match: rowFilter});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Count total without pagination
|
|
261
|
+
const countPipeline = [...pipeline, { $count: 'total' }];
|
|
262
|
+
const countResult = await db.collection(sourceCollection)
|
|
263
|
+
.aggregate(countPipeline)
|
|
264
|
+
.toArray();
|
|
265
|
+
const total = countResult[0]?.total ?? 0;
|
|
266
|
+
|
|
267
|
+
// Add pagination stages
|
|
268
|
+
const offset = (page - 1) * limit;
|
|
269
|
+
pipeline.push({ $skip: offset });
|
|
270
|
+
pipeline.push({ $limit: limit });
|
|
271
|
+
|
|
272
|
+
const docs = await db.collection(sourceCollection)
|
|
273
|
+
.aggregate(pipeline, { allowDiskUse: true })
|
|
274
|
+
.toArray();
|
|
275
|
+
|
|
276
|
+
// Strip MongoDB internal _id from all results
|
|
277
|
+
const results = docs.map(({ _id, ...rest }) => rest);
|
|
278
|
+
|
|
279
|
+
return { results, total, page, limit };
|
|
280
|
+
}
|
|
@@ -1,113 +1,119 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en-GB">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>{{seoTitle}}</title>
|
|
7
|
-
<meta name="description" content="{{seoDescription}}">
|
|
8
|
-
{{#if ogImage}}<meta property="og:image" content="{{ogImage}}">{{/if}}
|
|
9
|
-
|
|
10
|
-
<!-- Fonts -->
|
|
11
|
-
{{#if fontLink}}{{fontLink}}{{/if}}
|
|
12
|
-
|
|
13
|
-
<!-- DommaJS CSS -->
|
|
14
|
-
<link rel="stylesheet" href="/dist/domma/domma.css">
|
|
15
|
-
<link rel="stylesheet" href="/dist/domma/grid.css">
|
|
16
|
-
<link rel="stylesheet" href="/dist/domma/elements.css">
|
|
17
|
-
<link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
|
|
18
|
-
|
|
19
|
-
<!-- Site CSS -->
|
|
20
|
-
<link rel="stylesheet" href="/public/css/site.css">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{{
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-GB">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{seoTitle}}</title>
|
|
7
|
+
<meta name="description" content="{{seoDescription}}">
|
|
8
|
+
{{#if ogImage}}<meta property="og:image" content="{{ogImage}}">{{/if}}
|
|
9
|
+
|
|
10
|
+
<!-- Fonts -->
|
|
11
|
+
{{#if fontLink}}{{fontLink}}{{/if}}
|
|
12
|
+
|
|
13
|
+
<!-- DommaJS CSS -->
|
|
14
|
+
<link rel="stylesheet" href="/dist/domma/domma.css">
|
|
15
|
+
<link rel="stylesheet" href="/dist/domma/grid.css">
|
|
16
|
+
<link rel="stylesheet" href="/dist/domma/elements.css">
|
|
17
|
+
<link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
|
|
18
|
+
|
|
19
|
+
<!-- Site CSS -->
|
|
20
|
+
<link rel="stylesheet" href="/public/css/site.css">
|
|
21
|
+
<link rel="stylesheet" href="/public/css/forms.css">
|
|
22
|
+
|
|
23
|
+
<!-- Font overrides -->
|
|
24
|
+
{{fontStyleTag}}
|
|
25
|
+
|
|
26
|
+
<!-- Plugin head injection -->
|
|
27
|
+
{{headInject}}
|
|
28
|
+
|
|
29
|
+
<!-- Late head injection — custom CSS always loads last so it can override everything -->
|
|
30
|
+
{{headInjectLate}}
|
|
31
|
+
</head>
|
|
32
|
+
<body class="dm-cloaked dm-theme-{{theme}}" data-layout="{{layout}}">
|
|
33
|
+
|
|
34
|
+
{{#if showNavbar}}
|
|
35
|
+
<nav id="site-navbar"></nav>
|
|
36
|
+
{{/if}}
|
|
37
|
+
|
|
38
|
+
<main class="site-main {{#if showSidebar}}with-sidebar{{/if}}">
|
|
39
|
+
{{#if showSidebar}}
|
|
40
|
+
<aside id="site-sidebar" class="site-sidebar"></aside>
|
|
41
|
+
{{/if}}
|
|
42
|
+
|
|
43
|
+
<article class="site-content">
|
|
44
|
+
<div class="container">
|
|
45
|
+
{{breadcrumbsHtml}}
|
|
46
|
+
<div class="page-body">
|
|
47
|
+
{{html}}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</article>
|
|
51
|
+
</main>
|
|
52
|
+
|
|
53
|
+
{{#if showFooter}}
|
|
54
|
+
<footer id="site-footer" class="page-footer"></footer>
|
|
55
|
+
{{/if}}
|
|
56
|
+
|
|
57
|
+
<!-- DOMPurify - must load before DommaJS -->
|
|
58
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
|
59
|
+
|
|
60
|
+
<!-- DommaJS -->
|
|
61
|
+
<script src="/dist/domma/domma.min.js"></script>
|
|
62
|
+
|
|
63
|
+
<!-- Initialise DommaJS before module loads -->
|
|
64
|
+
<script>
|
|
65
|
+
(function () {
|
|
66
|
+
var _stored;
|
|
67
|
+
try {
|
|
68
|
+
_stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
}
|
|
71
|
+
if (_stored === true) {
|
|
72
|
+
document.documentElement.classList.add('dm-reduced-motion');
|
|
73
|
+
}
|
|
74
|
+
// Override window.matchMedia so Domma JS effects (scribe, breathe, etc.)
|
|
75
|
+
// respect the stored preference. When explicitly set to false the user is
|
|
76
|
+
// overriding the OS "reduce" preference to allow motion on this site.
|
|
77
|
+
if (_stored !== null && _stored !== undefined && window.matchMedia) {
|
|
78
|
+
var _orig = window.matchMedia.bind(window);
|
|
79
|
+
window.matchMedia = function (q) {
|
|
80
|
+
if (q === '(prefers-reduced-motion: reduce)') {
|
|
81
|
+
return {
|
|
82
|
+
matches: !!_stored, media: q, onchange: null,
|
|
83
|
+
addListener: function () {
|
|
84
|
+
}, removeListener: function () {
|
|
85
|
+
},
|
|
86
|
+
addEventListener: function () {
|
|
87
|
+
}, removeEventListener: function () {
|
|
88
|
+
},
|
|
89
|
+
dispatchEvent: function () {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return _orig(q);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}());
|
|
98
|
+
if (window.Domma && typeof window.Domma.init === 'function') {
|
|
99
|
+
window.Domma.init();
|
|
100
|
+
}
|
|
101
|
+
if (window.Domma && window.Domma.theme) {
|
|
102
|
+
window.Domma.theme.init({ theme: '{{theme}}', persist: false });
|
|
103
|
+
}
|
|
104
|
+
window.__CMS_NAV__ = {{navJson}};
|
|
105
|
+
window.__CMS_SITE__ = {{siteJson}};
|
|
106
|
+
{{dconfigScript}}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<!-- Site initialisation -->
|
|
110
|
+
<script src="/public/js/site.js" type="module"></script>
|
|
111
|
+
|
|
112
|
+
<!-- Core Forms (logic engine must load before renderer) -->
|
|
113
|
+
<script src="/public/js/form-logic-engine.js"></script>
|
|
114
|
+
<script src="/public/js/forms.js" type="module"></script>
|
|
115
|
+
|
|
116
|
+
<!-- Plugin body-end injection -->
|
|
117
|
+
{{bodyEndInject}}
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="settings"></span> Form Builder Settings</h1>
|
|
3
|
-
<div style="display:flex;gap:.5rem;">
|
|
4
|
-
<a href="#/plugins/form-builder" class="btn btn-ghost btn-sm">
|
|
5
|
-
<span data-icon="layout"></span> All Forms
|
|
6
|
-
</a>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
<div class="card mb-4">
|
|
11
|
-
<div class="card-body" style="display:flex;align-items:center;gap:1rem;">
|
|
12
|
-
<span data-icon="info" style="flex-shrink:0;"></span>
|
|
13
|
-
<div>
|
|
14
|
-
<p style="margin:0 0 .25rem;">SMTP and email identity settings have moved to <a href="#/settings" class="link">Site Settings</a>.</p>
|
|
15
|
-
<p class="text-muted" style="margin:0;font-size:.85rem;">Configure your mail server there, then use the button below to verify the connection.</p>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<div class="card">
|
|
21
|
-
<div class="card-header"><h2>Test Email</h2></div>
|
|
22
|
-
<div class="card-body">
|
|
23
|
-
<p class="text-muted" style="margin:0 0 1rem;font-size:.9rem;">Send a test email using the SMTP settings configured in Site Settings to verify your mail server connection.</p>
|
|
24
|
-
<button id="test-email-btn" class="btn btn-primary">
|
|
25
|
-
<span data-icon="send"></span> Send Test Email
|
|
26
|
-
</button>
|
|
27
|
-
<p id="test-email-result" class="form-hint" style="margin-top:.75rem;display:none;"></p>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|