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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Registry
|
|
3
|
+
* Single source of truth for all resource/action definitions in the CMS.
|
|
4
|
+
* Adding a new resource or custom action is a one-file change here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {ReadonlyArray<{key:string,label:string,description:string,icon:string,group:string,actions:{key:string,label:string,description:string}[]}>} */
|
|
8
|
+
export const REGISTRY = Object.freeze([
|
|
9
|
+
{
|
|
10
|
+
key: 'pages',
|
|
11
|
+
label: 'Pages',
|
|
12
|
+
description: 'Manage site pages and their content.',
|
|
13
|
+
icon: 'file-text',
|
|
14
|
+
group: 'Content',
|
|
15
|
+
actions: [
|
|
16
|
+
{key: 'read', label: 'View', description: 'View pages and their content'},
|
|
17
|
+
{key: 'create', label: 'Create', description: 'Create new pages'},
|
|
18
|
+
{key: 'update', label: 'Edit', description: 'Edit existing pages'},
|
|
19
|
+
{key: 'delete', label: 'Delete', description: 'Delete pages'}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'media',
|
|
24
|
+
label: 'Media',
|
|
25
|
+
description: 'Upload and manage media files.',
|
|
26
|
+
icon: 'image',
|
|
27
|
+
group: 'Content',
|
|
28
|
+
actions: [
|
|
29
|
+
{key: 'read', label: 'View', description: 'View media library'},
|
|
30
|
+
{key: 'create', label: 'Upload', description: 'Upload new files'},
|
|
31
|
+
{key: 'update', label: 'Edit', description: 'Rename or update file metadata'},
|
|
32
|
+
{key: 'delete', label: 'Delete', description: 'Delete media files'}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'blocks',
|
|
37
|
+
label: 'Blocks',
|
|
38
|
+
description: 'Manage reusable content blocks.',
|
|
39
|
+
icon: 'box',
|
|
40
|
+
group: 'Content',
|
|
41
|
+
actions: [
|
|
42
|
+
{key: 'read', label: 'View', description: 'View content blocks'},
|
|
43
|
+
{key: 'create', label: 'Create', description: 'Create new blocks'},
|
|
44
|
+
{key: 'update', label: 'Edit', description: 'Edit existing blocks'},
|
|
45
|
+
{key: 'delete', label: 'Delete', description: 'Delete blocks'}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'navigation',
|
|
50
|
+
label: 'Navigation',
|
|
51
|
+
description: 'Configure site navigation menus.',
|
|
52
|
+
icon: 'menu',
|
|
53
|
+
group: 'Structure',
|
|
54
|
+
actions: [
|
|
55
|
+
{key: 'read', label: 'View', description: 'View navigation menus'},
|
|
56
|
+
{key: 'create', label: 'Create', description: 'Create new menu items'},
|
|
57
|
+
{key: 'update', label: 'Edit', description: 'Edit navigation structure'},
|
|
58
|
+
{key: 'delete', label: 'Delete', description: 'Remove menu items'}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'layouts',
|
|
63
|
+
label: 'Layouts',
|
|
64
|
+
description: 'Manage page layouts and templates.',
|
|
65
|
+
icon: 'layout',
|
|
66
|
+
group: 'Structure',
|
|
67
|
+
actions: [
|
|
68
|
+
{key: 'read', label: 'View', description: 'View layouts'},
|
|
69
|
+
{key: 'create', label: 'Create', description: 'Create new layouts'},
|
|
70
|
+
{key: 'update', label: 'Edit', description: 'Edit layouts'},
|
|
71
|
+
{key: 'delete', label: 'Delete', description: 'Delete layouts'}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'collections',
|
|
76
|
+
label: 'Collections',
|
|
77
|
+
description: 'Manage data collections and entries.',
|
|
78
|
+
icon: 'database',
|
|
79
|
+
group: 'Data',
|
|
80
|
+
actions: [
|
|
81
|
+
{key: 'read', label: 'View', description: 'View collection entries'},
|
|
82
|
+
{key: 'create', label: 'Create', description: 'Create new entries'},
|
|
83
|
+
{key: 'update', label: 'Edit', description: 'Edit existing entries'},
|
|
84
|
+
{key: 'delete', label: 'Delete', description: 'Delete entries'}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: 'views',
|
|
89
|
+
label: 'Views',
|
|
90
|
+
description: 'Create and manage data views.',
|
|
91
|
+
icon: 'eye',
|
|
92
|
+
group: 'Data',
|
|
93
|
+
actions: [
|
|
94
|
+
{key: 'read', label: 'View', description: 'View data views'},
|
|
95
|
+
{key: 'create', label: 'Create', description: 'Create new views'},
|
|
96
|
+
{key: 'update', label: 'Edit', description: 'Edit views'},
|
|
97
|
+
{key: 'delete', label: 'Delete', description: 'Delete views'}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'actions',
|
|
102
|
+
label: 'Actions',
|
|
103
|
+
description: 'Configure automated actions.',
|
|
104
|
+
icon: 'zap',
|
|
105
|
+
group: 'Data',
|
|
106
|
+
actions: [
|
|
107
|
+
{key: 'read', label: 'View', description: 'View automated actions'},
|
|
108
|
+
{key: 'create', label: 'Create', description: 'Create new actions'},
|
|
109
|
+
{key: 'update', label: 'Edit', description: 'Edit actions'},
|
|
110
|
+
{key: 'delete', label: 'Delete', description: 'Delete actions'}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: 'users',
|
|
115
|
+
label: 'Users',
|
|
116
|
+
description: 'Manage user accounts.',
|
|
117
|
+
icon: 'users',
|
|
118
|
+
group: 'Configuration',
|
|
119
|
+
actions: [
|
|
120
|
+
{key: 'read', label: 'View', description: 'View user accounts'},
|
|
121
|
+
{key: 'create', label: 'Create', description: 'Create new users'},
|
|
122
|
+
{key: 'update', label: 'Edit', description: 'Edit user accounts'},
|
|
123
|
+
{key: 'delete', label: 'Delete', description: 'Delete users'}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'settings',
|
|
128
|
+
label: 'Settings',
|
|
129
|
+
description: 'Configure site settings.',
|
|
130
|
+
icon: 'settings',
|
|
131
|
+
group: 'Configuration',
|
|
132
|
+
actions: [
|
|
133
|
+
{key: 'read', label: 'View', description: 'View site settings'},
|
|
134
|
+
{key: 'create', label: 'Create', description: 'Add new settings'},
|
|
135
|
+
{key: 'update', label: 'Edit', description: 'Modify settings'},
|
|
136
|
+
{key: 'delete', label: 'Delete', description: 'Remove settings'}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: 'plugins',
|
|
141
|
+
label: 'Plugins',
|
|
142
|
+
description: 'Manage CMS plugins.',
|
|
143
|
+
icon: 'package',
|
|
144
|
+
group: 'Configuration',
|
|
145
|
+
actions: [
|
|
146
|
+
{key: 'read', label: 'View', description: 'View installed plugins'},
|
|
147
|
+
{key: 'create', label: 'Install', description: 'Install new plugins'},
|
|
148
|
+
{key: 'update', label: 'Configure', description: 'Configure plugin settings'},
|
|
149
|
+
{key: 'delete', label: 'Remove', description: 'Remove plugins'}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
/** Derived array of resource key strings — mirrors the old RESOURCES constant. */
|
|
155
|
+
export const RESOURCES = REGISTRY.map(r => r.key);
|
|
156
|
+
|
|
157
|
+
/** Default CRUD action keys — mirrors the old ACTIONS constant. */
|
|
158
|
+
export const ACTIONS = ['read', 'create', 'update', 'delete'];
|
|
159
|
+
|
|
160
|
+
/** Display order for permission groups. */
|
|
161
|
+
export const GROUP_ORDER = ['Content', 'Structure', 'Data', 'Configuration'];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Return the action keys defined for a given resource.
|
|
165
|
+
* Falls back to the default CRUD set if the resource is not in the registry.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} resourceKey
|
|
168
|
+
* @returns {string[]}
|
|
169
|
+
*/
|
|
170
|
+
export function getActionsForResource(resourceKey) {
|
|
171
|
+
const resource = REGISTRY.find(r => r.key === resourceKey);
|
|
172
|
+
return resource ? resource.actions.map(a => a.key) : [...ACTIONS];
|
|
173
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset Collections
|
|
3
|
+
* Built-in collections whose schemas are defined in code and overwritten on boot.
|
|
4
|
+
* Data files are never touched — only schema.json is reset each startup.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {config} from '../config.js';
|
|
9
|
+
import {ensureFormForCollection} from './forms.js';
|
|
10
|
+
|
|
11
|
+
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
12
|
+
|
|
13
|
+
const PRESETS = [
|
|
14
|
+
{
|
|
15
|
+
slug: 'contacts',
|
|
16
|
+
title: 'Contacts',
|
|
17
|
+
description: 'People and their contact information.',
|
|
18
|
+
preset: true,
|
|
19
|
+
fields: [
|
|
20
|
+
{name: 'full_name', label: 'Full Name', type: 'string', required: true},
|
|
21
|
+
{name: 'email_address', label: 'Email', type: 'string', required: true},
|
|
22
|
+
{name: 'phone_number', label: 'Phone', type: 'string', required: false}
|
|
23
|
+
],
|
|
24
|
+
api: {
|
|
25
|
+
create: {enabled: true, access: 'public'},
|
|
26
|
+
read: {enabled: true, access: 'admin'},
|
|
27
|
+
update: {enabled: false, access: 'admin'},
|
|
28
|
+
delete: {enabled: false, access: 'admin'}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: 'enquiries',
|
|
33
|
+
title: 'Enquiries',
|
|
34
|
+
description: 'Messages and enquiries from website visitors.',
|
|
35
|
+
preset: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{name: 'full_name', label: 'Full Name', type: 'text', required: true},
|
|
38
|
+
{name: 'email', label: 'Email', type: 'text', required: true},
|
|
39
|
+
{name: 'phone', label: 'Phone', type: 'text'},
|
|
40
|
+
{
|
|
41
|
+
name: 'subject', label: 'Subject', type: 'select', required: true,
|
|
42
|
+
options: [
|
|
43
|
+
{value: 'general-enquiry', label: 'General Enquiry'},
|
|
44
|
+
{value: 'support', label: 'Support'},
|
|
45
|
+
{value: 'sales', label: 'Sales'},
|
|
46
|
+
{value: 'partnership', label: 'Partnership'},
|
|
47
|
+
{value: 'other', label: 'Other'}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{name: 'message', label: 'Message', type: 'text', required: true}
|
|
51
|
+
],
|
|
52
|
+
api: {
|
|
53
|
+
create: {enabled: true, access: 'public'},
|
|
54
|
+
read: {enabled: true, access: 'admin'},
|
|
55
|
+
update: {enabled: false, access: 'admin'},
|
|
56
|
+
delete: {enabled: false, access: 'admin'}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
slug: 'feedback',
|
|
61
|
+
title: 'Feedback',
|
|
62
|
+
description: 'User feedback and ratings.',
|
|
63
|
+
preset: true,
|
|
64
|
+
fields: [
|
|
65
|
+
{name: 'name', label: 'Name', type: 'string', required: true},
|
|
66
|
+
{name: 'email', label: 'Email', type: 'string', required: true},
|
|
67
|
+
{
|
|
68
|
+
name: 'rating', label: 'Rating', type: 'select', required: true,
|
|
69
|
+
options: [
|
|
70
|
+
{value: 'excellent', label: 'Excellent'},
|
|
71
|
+
{value: 'good', label: 'Good'},
|
|
72
|
+
{value: 'average', label: 'Average'},
|
|
73
|
+
{value: 'poor', label: 'Poor'}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'category', label: 'Category', type: 'select',
|
|
78
|
+
options: [
|
|
79
|
+
{value: 'feature-request', label: 'Feature Request'},
|
|
80
|
+
{value: 'praise', label: 'Praise'},
|
|
81
|
+
{value: 'bug-report', label: 'Bug Report'},
|
|
82
|
+
{value: 'support', label: 'Support'}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{name: 'subject', label: 'Subject', type: 'string'},
|
|
86
|
+
{name: 'message', label: 'Message', type: 'string', required: true}
|
|
87
|
+
],
|
|
88
|
+
api: {
|
|
89
|
+
create: {enabled: true, access: 'public'},
|
|
90
|
+
read: {enabled: true, access: 'admin'},
|
|
91
|
+
update: {enabled: false, access: 'admin'},
|
|
92
|
+
delete: {enabled: false, access: 'admin'}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
slug: 'notes',
|
|
97
|
+
title: 'Notes',
|
|
98
|
+
description: 'Free-form notes with categories and tags.',
|
|
99
|
+
preset: true,
|
|
100
|
+
fields: [
|
|
101
|
+
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
102
|
+
{name: 'content', label: 'Content', type: 'text', required: true},
|
|
103
|
+
{
|
|
104
|
+
name: 'category', label: 'Category', type: 'select',
|
|
105
|
+
options: [
|
|
106
|
+
{value: 'general', label: 'General'},
|
|
107
|
+
{value: 'idea', label: 'Idea'},
|
|
108
|
+
{value: 'reminder', label: 'Reminder'},
|
|
109
|
+
{value: 'reference', label: 'Reference'}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{name: 'tags', label: 'Tags', type: 'text'}
|
|
113
|
+
],
|
|
114
|
+
api: {
|
|
115
|
+
create: {enabled: false, access: 'admin'},
|
|
116
|
+
read: {enabled: false, access: 'admin'},
|
|
117
|
+
update: {enabled: false, access: 'admin'},
|
|
118
|
+
delete: {enabled: false, access: 'admin'}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
slug: 'categories',
|
|
123
|
+
title: 'Categories',
|
|
124
|
+
description: 'Content categories for organising posts.',
|
|
125
|
+
preset: true,
|
|
126
|
+
fields: [
|
|
127
|
+
{name: 'name', label: 'Name', type: 'text', required: true},
|
|
128
|
+
{name: 'slug', label: 'Slug', type: 'text', required: true},
|
|
129
|
+
{name: 'description', label: 'Description', type: 'textarea'},
|
|
130
|
+
{name: 'parent_category', label: 'Parent Category', type: 'text'},
|
|
131
|
+
{name: 'sort_order', label: 'Sort Order', type: 'number'}
|
|
132
|
+
],
|
|
133
|
+
api: {
|
|
134
|
+
create: {enabled: false, access: 'admin'},
|
|
135
|
+
read: {enabled: true, access: 'public'},
|
|
136
|
+
update: {enabled: false, access: 'admin'},
|
|
137
|
+
delete: {enabled: false, access: 'admin'}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
slug: 'posts',
|
|
142
|
+
title: 'Posts',
|
|
143
|
+
description: 'Blog posts and articles.',
|
|
144
|
+
preset: true,
|
|
145
|
+
fields: [
|
|
146
|
+
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
147
|
+
{name: 'slug', label: 'Slug', type: 'text', required: true},
|
|
148
|
+
{name: 'content', label: 'Content', type: 'textarea', required: true},
|
|
149
|
+
{name: 'excerpt', label: 'Excerpt', type: 'textarea'},
|
|
150
|
+
{name: 'featured_image', label: 'Featured Image', type: 'url'},
|
|
151
|
+
{name: 'category', label: 'Category', type: 'text'},
|
|
152
|
+
{name: 'tags', label: 'Tags', type: 'text'},
|
|
153
|
+
{
|
|
154
|
+
name: 'status', label: 'Status', type: 'select', required: true,
|
|
155
|
+
options: [
|
|
156
|
+
{value: 'draft', label: 'Draft'},
|
|
157
|
+
{value: 'published', label: 'Published'},
|
|
158
|
+
{value: 'archived', label: 'Archived'}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{name: 'publish_date', label: 'Publish Date', type: 'date'},
|
|
162
|
+
{name: 'author', label: 'Author', type: 'text'}
|
|
163
|
+
],
|
|
164
|
+
api: {
|
|
165
|
+
create: {enabled: false, access: 'admin'},
|
|
166
|
+
read: {enabled: true, access: 'public'},
|
|
167
|
+
update: {enabled: false, access: 'admin'},
|
|
168
|
+
delete: {enabled: false, access: 'admin'}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
slug: 'comments',
|
|
173
|
+
title: 'Comments',
|
|
174
|
+
description: 'User comments on posts and content.',
|
|
175
|
+
preset: true,
|
|
176
|
+
fields: [
|
|
177
|
+
{name: 'post_slug', label: 'Post', type: 'text', required: true},
|
|
178
|
+
{name: 'author_name', label: 'Name', type: 'text', required: true},
|
|
179
|
+
{name: 'author_email', label: 'Email', type: 'email', required: true},
|
|
180
|
+
{name: 'body', label: 'Comment', type: 'textarea', required: true},
|
|
181
|
+
{
|
|
182
|
+
name: 'status', label: 'Status', type: 'select', required: true,
|
|
183
|
+
options: [
|
|
184
|
+
{value: 'pending', label: 'Pending'},
|
|
185
|
+
{value: 'approved', label: 'Approved'},
|
|
186
|
+
{value: 'rejected', label: 'Rejected'}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
api: {
|
|
191
|
+
create: {enabled: true, access: 'public'},
|
|
192
|
+
read: {enabled: true, access: 'public'},
|
|
193
|
+
update: {enabled: false, access: 'admin'},
|
|
194
|
+
delete: {enabled: false, access: 'admin'}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
slug: 'to-do',
|
|
199
|
+
title: 'To-Do',
|
|
200
|
+
description: 'Task tracking with priorities and due dates.',
|
|
201
|
+
preset: true,
|
|
202
|
+
fields: [
|
|
203
|
+
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
204
|
+
{name: 'description', label: 'Description', type: 'text'},
|
|
205
|
+
{
|
|
206
|
+
name: 'status', label: 'Status', type: 'select', required: true,
|
|
207
|
+
options: ['Pending', 'In Progress', 'Done']
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'priority', label: 'Priority', type: 'select',
|
|
211
|
+
options: ['Low', 'Medium', 'High']
|
|
212
|
+
},
|
|
213
|
+
{name: 'due_date', label: 'Due Date', type: 'text'},
|
|
214
|
+
{name: 'assigned_to', label: 'Assigned To', type: 'text'}
|
|
215
|
+
],
|
|
216
|
+
api: {
|
|
217
|
+
create: {enabled: false, access: 'admin'},
|
|
218
|
+
read: {enabled: false, access: 'admin'},
|
|
219
|
+
update: {enabled: false, access: 'admin'},
|
|
220
|
+
delete: {enabled: false, access: 'admin'}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
/** Slugs exported for use in adapterRegistry and the delete guard. */
|
|
226
|
+
export const PRESET_COLLECTION_SLUGS = PRESETS.map(p => p.slug);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Seed all preset collections at boot.
|
|
230
|
+
* Always overwrites schema.json; only creates data.json if absent.
|
|
231
|
+
*
|
|
232
|
+
* @returns {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
export async function seedAll() {
|
|
235
|
+
for (const schema of PRESETS) {
|
|
236
|
+
const dir = path.join(COLLECTIONS_DIR, schema.slug);
|
|
237
|
+
const schemaPath = path.join(dir, 'schema.json');
|
|
238
|
+
const dataPath = path.join(dir, 'data.json');
|
|
239
|
+
|
|
240
|
+
await fs.mkdir(dir, {recursive: true});
|
|
241
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n', 'utf8');
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await fs.access(dataPath);
|
|
245
|
+
} catch {
|
|
246
|
+
await fs.writeFile(dataPath, '[]\n', 'utf8');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await ensureFormForCollection(schema);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -61,6 +61,8 @@ export async function renderPage(page) {
|
|
|
61
61
|
? `window.__CMS_DCONFIG__ = ${dconfigJson};`
|
|
62
62
|
: '';
|
|
63
63
|
|
|
64
|
+
const breadcrumbsHtml = buildBreadcrumbsHtml(page, site);
|
|
65
|
+
|
|
64
66
|
const {fontLink, fontOverride} = buildFontVars(site.fontFamily, site.fontSize);
|
|
65
67
|
const fontStyleTag = fontOverride
|
|
66
68
|
? `<style>${fontOverride}</style>`
|
|
@@ -70,13 +72,23 @@ export async function renderPage(page) {
|
|
|
70
72
|
? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
|
|
71
73
|
: '';
|
|
72
74
|
|
|
75
|
+
const activeTheme = page.theme || site.theme || 'charcoal-dark';
|
|
76
|
+
// When a custom theme is active, Domma's theme engine must receive the base
|
|
77
|
+
// built-in theme (which it recognises). The custom override class is added
|
|
78
|
+
// separately after Domma's init so _applyTheme() cannot strip it.
|
|
79
|
+
const dommaTheme = site.baseTheme || activeTheme;
|
|
80
|
+
const customThemeClass = site.baseTheme ? `dm-theme-${activeTheme}` : '';
|
|
81
|
+
|
|
73
82
|
const vars = {
|
|
74
83
|
seoTitle,
|
|
75
84
|
seoDescription,
|
|
76
85
|
ogImage,
|
|
77
86
|
title: page.title,
|
|
78
87
|
html: page.html,
|
|
79
|
-
|
|
88
|
+
breadcrumbsHtml,
|
|
89
|
+
theme: activeTheme,
|
|
90
|
+
dommaTheme,
|
|
91
|
+
customThemeClass,
|
|
80
92
|
fontLink,
|
|
81
93
|
fontStyleTag,
|
|
82
94
|
layout: page.layout || 'default',
|
|
@@ -104,6 +116,68 @@ export async function renderPage(page) {
|
|
|
104
116
|
return applyTransforms('render:afterRender', html, {page});
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Build breadcrumbs HTML for a page.
|
|
121
|
+
* Returns an empty string if breadcrumbs are disabled globally or per-page.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} page - Parsed page object with urlPath, title, breadcrumbs fields
|
|
124
|
+
* @param {object} site - Site config
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function buildBreadcrumbsHtml(page, site) {
|
|
128
|
+
const cfg = site?.breadcrumbs || {};
|
|
129
|
+
if (!cfg.enabled) return '';
|
|
130
|
+
if (page.breadcrumbs === false) return '';
|
|
131
|
+
|
|
132
|
+
const urlPath = page.urlPath || '/';
|
|
133
|
+
// Don't render breadcrumbs on the home page
|
|
134
|
+
if (urlPath === '/') return '';
|
|
135
|
+
|
|
136
|
+
const homeLabel = cfg.homeLabel || 'Home';
|
|
137
|
+
const segments = urlPath.split('/').filter(Boolean);
|
|
138
|
+
if (!segments.length) return '';
|
|
139
|
+
|
|
140
|
+
// Build fixed-position inline style from corner + offsets
|
|
141
|
+
const pos = (cfg.position || 'TL').toUpperCase();
|
|
142
|
+
const offsetX = parseInt(cfg.offsetX, 10) || 8;
|
|
143
|
+
const offsetY = parseInt(cfg.offsetY, 10) || 60;
|
|
144
|
+
const posStyle = [
|
|
145
|
+
pos.includes('T') ? `top:${offsetY}px` : `bottom:${offsetY}px`,
|
|
146
|
+
pos.includes('L') ? `left:${offsetX}px` : `right:${offsetX}px`
|
|
147
|
+
].join(';');
|
|
148
|
+
|
|
149
|
+
const crumbs = [];
|
|
150
|
+
|
|
151
|
+
const homeIcon = '<svg class="dm-breadcrumbs-home-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
|
|
152
|
+
// Home crumb
|
|
153
|
+
crumbs.push(`<a href="/" class="dm-breadcrumbs-item dm-breadcrumbs-link">${homeIcon}${escapeHtml(homeLabel)}</a>`);
|
|
154
|
+
|
|
155
|
+
// Middle crumbs (each intermediate path segment)
|
|
156
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
157
|
+
const href = '/' + segments.slice(0, i + 1).join('/');
|
|
158
|
+
const label = toTitleCase(segments[i]);
|
|
159
|
+
crumbs.push(`<a href="${href}" class="dm-breadcrumbs-item dm-breadcrumbs-link">${escapeHtml(label)}</a>`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Last crumb — current page title, no link
|
|
163
|
+
crumbs.push(`<span class="dm-breadcrumbs-item dm-breadcrumbs-current" aria-current="page">${escapeHtml(page.title || toTitleCase(segments[segments.length - 1]))}</span>`);
|
|
164
|
+
|
|
165
|
+
const sep = '<span class="dm-breadcrumbs-separator" aria-hidden="true">›</span>';
|
|
166
|
+
return `<nav class="dm-breadcrumbs" aria-label="Breadcrumb" style="${posStyle}">${crumbs.join(sep)}</nav>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function toTitleCase(str) {
|
|
170
|
+
return str.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function escapeHtml(str) {
|
|
174
|
+
return String(str)
|
|
175
|
+
.replace(/&/g, '&')
|
|
176
|
+
.replace(/</g, '<')
|
|
177
|
+
.replace(/>/g, '>')
|
|
178
|
+
.replace(/"/g, '"');
|
|
179
|
+
}
|
|
180
|
+
|
|
107
181
|
function buildFontVars(fontFamily, fontSize) {
|
|
108
182
|
const PRECONNECT = '<link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
|
109
183
|
const family = fontFamily || 'Roboto';
|