domma-cms 0.10.0 → 0.12.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/CLAUDE.md +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
package/server/services/roles.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Roles Service
|
|
3
3
|
* Preset Collection — seeds roles on first startup, caches them in memory.
|
|
4
4
|
* Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
|
|
5
|
+
*
|
|
6
|
+
* Base roles: super-admin (0), admin (1), user (2).
|
|
7
|
+
* Plugin-contributed roles are tagged with a `plugin` field and removed on plugin teardown.
|
|
5
8
|
*/
|
|
6
9
|
import fs from 'fs/promises';
|
|
7
10
|
import path from 'path';
|
|
@@ -15,6 +18,15 @@ const DIR = path.join(COLLECTIONS_DIR, SLUG);
|
|
|
15
18
|
const SCHEMA_PATH = path.join(DIR, 'schema.json');
|
|
16
19
|
const DATA_PATH = path.join(DIR, 'data.json');
|
|
17
20
|
|
|
21
|
+
/** Users dir — derived from collections dir parent (content/users/) */
|
|
22
|
+
const USERS_DIR = path.resolve(path.dirname(COLLECTIONS_DIR), 'users');
|
|
23
|
+
|
|
24
|
+
/** Base role slugs that should always exist */
|
|
25
|
+
const BASE_ROLE_NAMES = ['super-admin', 'admin', 'user'];
|
|
26
|
+
|
|
27
|
+
/** Role slugs that were seeded by the old 4-role system — used to detect legacy data */
|
|
28
|
+
const DEFUNCT_ROLE_NAMES = ['manager', 'editor', 'subscriber'];
|
|
29
|
+
|
|
18
30
|
export {RESOURCES, ACTIONS};
|
|
19
31
|
|
|
20
32
|
const PRESET_SCHEMA = {
|
|
@@ -35,7 +47,8 @@ const PRESET_SCHEMA = {
|
|
|
35
47
|
{
|
|
36
48
|
name: 'badgeClass', label: 'Badge Class', type: 'select',
|
|
37
49
|
options: ['badge-danger', 'badge-warning', 'badge-info', 'badge-secondary', 'badge-success', 'badge-primary']
|
|
38
|
-
}
|
|
50
|
+
},
|
|
51
|
+
{name: 'plugin', label: 'Plugin', type: 'text', required: false}
|
|
39
52
|
],
|
|
40
53
|
api: {
|
|
41
54
|
create: {enabled: false, access: 'admin'},
|
|
@@ -46,22 +59,31 @@ const PRESET_SCHEMA = {
|
|
|
46
59
|
};
|
|
47
60
|
|
|
48
61
|
const SEED_ENTRIES = [
|
|
49
|
-
{name: 'admin', label: 'Admin', level: 0, permissions: RESOURCES, badgeClass: 'badge-danger'},
|
|
50
62
|
{
|
|
51
|
-
name: '
|
|
52
|
-
label: '
|
|
63
|
+
name: 'super-admin',
|
|
64
|
+
label: 'Super Admin',
|
|
65
|
+
level: 0,
|
|
66
|
+
permissions: RESOURCES,
|
|
67
|
+
badgeClass: 'badge-danger'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'admin',
|
|
71
|
+
label: 'Admin',
|
|
53
72
|
level: 1,
|
|
54
|
-
permissions: [
|
|
73
|
+
permissions: [
|
|
74
|
+
'pages', 'media', 'blocks', 'navigation', 'layouts',
|
|
75
|
+
'collections', 'views', 'actions',
|
|
76
|
+
'users', 'settings', 'notifications'
|
|
77
|
+
],
|
|
55
78
|
badgeClass: 'badge-warning'
|
|
56
79
|
},
|
|
57
80
|
{
|
|
58
|
-
name: '
|
|
59
|
-
label: '
|
|
81
|
+
name: 'user',
|
|
82
|
+
label: 'User',
|
|
60
83
|
level: 2,
|
|
61
|
-
permissions: ['
|
|
62
|
-
badgeClass: 'badge-
|
|
63
|
-
}
|
|
64
|
-
{name: 'subscriber', label: 'Subscriber', level: 3, permissions: [], badgeClass: 'badge-secondary'}
|
|
84
|
+
permissions: ['notifications'],
|
|
85
|
+
badgeClass: 'badge-secondary'
|
|
86
|
+
}
|
|
65
87
|
];
|
|
66
88
|
|
|
67
89
|
// ---------------------------------------------------------------------------
|
|
@@ -80,7 +102,7 @@ let rawPermissionsMap = new Map();
|
|
|
80
102
|
/**
|
|
81
103
|
* Build in-memory maps from an array of data entries.
|
|
82
104
|
* Supports both bare resource names ('pages') and dotted action strings ('pages.read').
|
|
83
|
-
* Bare names expand to all
|
|
105
|
+
* Bare names expand to all actions for backward compatibility.
|
|
84
106
|
*
|
|
85
107
|
* @param {object[]} entries
|
|
86
108
|
*/
|
|
@@ -101,44 +123,111 @@ function buildCache(entries) {
|
|
|
101
123
|
for (const perm of (d.permissions || [])) {
|
|
102
124
|
if (perm.includes('.')) {
|
|
103
125
|
const [res] = perm.split('.');
|
|
104
|
-
addTo(perm, d.name);
|
|
105
|
-
addTo(res, d.name);
|
|
126
|
+
addTo(perm, d.name);
|
|
127
|
+
addTo(res, d.name);
|
|
106
128
|
} else {
|
|
107
|
-
addTo(perm, d.name);
|
|
129
|
+
addTo(perm, d.name);
|
|
108
130
|
for (const action of getActionsForResource(perm)) {
|
|
109
|
-
addTo(`${perm}.${action}`, d.name);
|
|
131
|
+
addTo(`${perm}.${action}`, d.name);
|
|
110
132
|
}
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
}
|
|
114
136
|
}
|
|
115
137
|
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Internal helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
async function readData() {
|
|
143
|
+
const raw = await fs.readFile(DATA_PATH, 'utf8');
|
|
144
|
+
return JSON.parse(raw);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function writeData(entries) {
|
|
148
|
+
await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function makeEntry(data) {
|
|
152
|
+
return {
|
|
153
|
+
id: uuidv4(),
|
|
154
|
+
data,
|
|
155
|
+
createdAt: new Date().toISOString(),
|
|
156
|
+
updatedAt: new Date().toISOString()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
116
160
|
// ---------------------------------------------------------------------------
|
|
117
161
|
// Public API
|
|
118
162
|
// ---------------------------------------------------------------------------
|
|
119
163
|
|
|
120
164
|
/**
|
|
121
|
-
* Seed the preset collection on first startup
|
|
165
|
+
* Seed the preset collection on first startup.
|
|
166
|
+
* Always writes schema. Writes base roles if missing; migrates legacy on-disk data.
|
|
167
|
+
* Scans content/users/ and rewrites any defunct role assignments to 'user'.
|
|
122
168
|
*
|
|
123
169
|
* @returns {Promise<void>}
|
|
124
170
|
*/
|
|
125
171
|
export async function seed() {
|
|
126
172
|
await fs.mkdir(DIR, {recursive: true});
|
|
127
|
-
|
|
128
|
-
// Always write schema (overwrite to keep in sync with code)
|
|
129
173
|
await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
|
|
130
174
|
|
|
131
|
-
|
|
175
|
+
let entries;
|
|
132
176
|
try {
|
|
133
177
|
await fs.access(DATA_PATH);
|
|
178
|
+
entries = await readData();
|
|
179
|
+
|
|
180
|
+
// Migrate: if the data still carries defunct base roles, replace the base layer
|
|
181
|
+
const hasDefunct = entries.some(e => DEFUNCT_ROLE_NAMES.includes(e.data?.name));
|
|
182
|
+
const missingBase = !entries.some(e => e.data?.name === 'super-admin');
|
|
183
|
+
if (hasDefunct || missingBase) {
|
|
184
|
+
// Keep only plugin-contributed entries, rebuild base entries
|
|
185
|
+
const pluginEntries = entries.filter(e => e.data?.plugin);
|
|
186
|
+
const baseEntries = SEED_ENTRIES.map(data => makeEntry(data));
|
|
187
|
+
entries = [...baseEntries, ...pluginEntries];
|
|
188
|
+
await writeData(entries);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// data.json doesn't exist — fresh install
|
|
192
|
+
entries = SEED_ENTRIES.map(data => makeEntry(data));
|
|
193
|
+
await writeData(entries);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Migrate existing user files whose role is no longer recognised
|
|
197
|
+
await migrateUserRoles(entries);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rewrite any user file whose role is not in the current role set to 'user'.
|
|
202
|
+
*
|
|
203
|
+
* @param {object[]} roleEntries - Current on-disk role entries
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async function migrateUserRoles(roleEntries) {
|
|
207
|
+
const knownRoles = new Set(roleEntries.map(e => e.data?.name).filter(Boolean));
|
|
208
|
+
|
|
209
|
+
let files;
|
|
210
|
+
try {
|
|
211
|
+
files = await fs.readdir(USERS_DIR);
|
|
134
212
|
} catch {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
213
|
+
return; // users dir doesn't exist yet — nothing to migrate
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
if (!file.endsWith('.json')) continue;
|
|
218
|
+
const filePath = path.join(USERS_DIR, file);
|
|
219
|
+
try {
|
|
220
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
221
|
+
const user = JSON.parse(raw);
|
|
222
|
+
if (user.role && !knownRoles.has(user.role)) {
|
|
223
|
+
const oldRole = user.role;
|
|
224
|
+
user.role = 'user';
|
|
225
|
+
await fs.writeFile(filePath, JSON.stringify(user, null, 2) + '\n', 'utf8');
|
|
226
|
+
console.log(`[roles] Migrated user ${user.email ?? file} from '${oldRole}' to 'user'`);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// skip unreadable files
|
|
230
|
+
}
|
|
142
231
|
}
|
|
143
232
|
}
|
|
144
233
|
|
|
@@ -149,8 +238,7 @@ export async function seed() {
|
|
|
149
238
|
*/
|
|
150
239
|
export async function load() {
|
|
151
240
|
try {
|
|
152
|
-
const
|
|
153
|
-
const entries = JSON.parse(raw);
|
|
241
|
+
const entries = await readData();
|
|
154
242
|
buildCache(entries);
|
|
155
243
|
} catch (err) {
|
|
156
244
|
console.warn('[roles] Failed to load roles collection:', err.message);
|
|
@@ -166,6 +254,77 @@ export async function invalidate() {
|
|
|
166
254
|
await load();
|
|
167
255
|
}
|
|
168
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Persist a new role entry and rebuild the cache.
|
|
259
|
+
* Idempotent — if a role with the same name exists, it is updated in place.
|
|
260
|
+
*
|
|
261
|
+
* @param {{name:string,label:string,level:number,permissions:string[],badgeClass?:string,plugin?:string}} data
|
|
262
|
+
* @returns {Promise<void>}
|
|
263
|
+
*/
|
|
264
|
+
export async function createRole(data) {
|
|
265
|
+
let entries;
|
|
266
|
+
try {
|
|
267
|
+
entries = await readData();
|
|
268
|
+
} catch {
|
|
269
|
+
entries = [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const idx = entries.findIndex(e => e.data?.name === data.name);
|
|
273
|
+
if (idx >= 0) {
|
|
274
|
+
entries[idx].data = {...entries[idx].data, ...data, updatedAt: new Date().toISOString()};
|
|
275
|
+
entries[idx].updatedAt = new Date().toISOString();
|
|
276
|
+
} else {
|
|
277
|
+
entries.push(makeEntry(data));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await writeData(entries);
|
|
281
|
+
buildCache(entries);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Remove a role entry by name and rebuild the cache.
|
|
286
|
+
* Refuses to remove base roles (level 0 protection is enforced elsewhere;
|
|
287
|
+
* here we additionally refuse by name).
|
|
288
|
+
*
|
|
289
|
+
* @param {string} name - Role slug
|
|
290
|
+
* @returns {Promise<void>}
|
|
291
|
+
*/
|
|
292
|
+
export async function removeRole(name) {
|
|
293
|
+
if (BASE_ROLE_NAMES.includes(name)) {
|
|
294
|
+
throw new Error(`Cannot remove built-in role '${name}'`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let entries;
|
|
298
|
+
try {
|
|
299
|
+
entries = await readData();
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
entries = entries.filter(e => e.data?.name !== name);
|
|
305
|
+
await writeData(entries);
|
|
306
|
+
buildCache(entries);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Remove all roles contributed by a plugin and rebuild the cache.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} plugin - Plugin name
|
|
313
|
+
* @returns {Promise<void>}
|
|
314
|
+
*/
|
|
315
|
+
export async function removeRolesByPlugin(plugin) {
|
|
316
|
+
let entries;
|
|
317
|
+
try {
|
|
318
|
+
entries = await readData();
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
entries = entries.filter(e => e.data?.plugin !== plugin);
|
|
324
|
+
await writeData(entries);
|
|
325
|
+
buildCache(entries);
|
|
326
|
+
}
|
|
327
|
+
|
|
169
328
|
/**
|
|
170
329
|
* Return the full role map.
|
|
171
330
|
*
|
|
@@ -187,26 +346,19 @@ export function getRoleLevel(roleName) {
|
|
|
187
346
|
|
|
188
347
|
/**
|
|
189
348
|
* Return the role names allowed to access a resource (and optional action).
|
|
190
|
-
* - getPermissionsFor('pages') → roles with ANY action on pages (backward compat)
|
|
191
|
-
* - getPermissionsFor('pages', 'delete')→ roles with delete on pages
|
|
192
|
-
* - getPermissionsFor('pages.delete') → same as above (dot-notation shorthand)
|
|
193
349
|
*
|
|
194
350
|
* @param {string} resource - Resource key, or 'resource.action' dot notation
|
|
195
351
|
* @param {string} [action] - Optional action (read | create | update | delete)
|
|
196
352
|
* @returns {string[]}
|
|
197
353
|
*/
|
|
198
354
|
export function getPermissionsFor(resource, action) {
|
|
199
|
-
if (action) {
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
if (resource.includes('.')) {
|
|
203
|
-
return permissionsMap.get(resource) ?? [];
|
|
204
|
-
}
|
|
355
|
+
if (action) return permissionsMap.get(`${resource}.${action}`) ?? [];
|
|
356
|
+
if (resource.includes('.')) return permissionsMap.get(resource) ?? [];
|
|
205
357
|
return permissionsMap.get(resource) ?? [];
|
|
206
358
|
}
|
|
207
359
|
|
|
208
360
|
/**
|
|
209
|
-
* Return the raw permissions array for a role
|
|
361
|
+
* Return the raw permissions array for a role.
|
|
210
362
|
*
|
|
211
363
|
* @param {string} roleName
|
|
212
364
|
* @returns {string[]}
|
package/server/services/users.js
CHANGED
|
@@ -122,7 +122,7 @@ export async function getUserByEmail(email) {
|
|
|
122
122
|
* @param {object} data - { name, email, password, role }
|
|
123
123
|
* @returns {Promise<object>} Created user (password stripped)
|
|
124
124
|
*/
|
|
125
|
-
export async function createUser({ name, email, password, role = '
|
|
125
|
+
export async function createUser({ name, email, password, role = 'user' }) {
|
|
126
126
|
const existing = await getUserByEmail(email);
|
|
127
127
|
if (existing) throw new Error('A user with that email already exists');
|
|
128
128
|
|
package/server/services/views.js
CHANGED
|
@@ -165,7 +165,7 @@ export async function createView(data, userId = null) {
|
|
|
165
165
|
block: display?.block || ''
|
|
166
166
|
},
|
|
167
167
|
access: {
|
|
168
|
-
roles: access?.roles || ['admin'],
|
|
168
|
+
roles: access?.roles || ['admin', 'super-admin'],
|
|
169
169
|
public: access?.public || false,
|
|
170
170
|
rowLevel: access?.rowLevel || null
|
|
171
171
|
},
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<!-- Late head injection — custom CSS always loads last so it can override everything -->
|
|
30
30
|
{{headInjectLate}}
|
|
31
31
|
</head>
|
|
32
|
-
<body class="dm-cloaked dm-theme-{{theme}}" data-layout="{{layout}}">
|
|
32
|
+
<body class="dm-cloaked dm-theme-{{theme}} {{layoutBodyClass}}" data-layout="{{layout}}">
|
|
33
33
|
|
|
34
34
|
{{#if showNavbar}}
|
|
35
35
|
<nav id="site-navbar"></nav>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<article class="site-content">
|
|
44
44
|
<div class="container">
|
|
45
45
|
{{breadcrumbsHtml}}
|
|
46
|
-
<div class="page-body">
|
|
46
|
+
<div class="page-body"{{#if pageBodyStyle}} style="{{pageBodyStyle}}"{{/if}}>
|
|
47
47
|
{{html}}
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
<div class="docs-layout" style="display:grid;grid-template-columns:200px 260px 1fr;height:calc(100vh - 120px);gap:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius);overflow:hidden;">
|
|
2
|
-
|
|
3
|
-
<!-- Folder sidebar -->
|
|
4
|
-
<div class="docs-folders" style="border-right:1px solid var(--dm-border);display:flex;flex-direction:column;">
|
|
5
|
-
<div style="padding:0.75rem;border-bottom:1px solid var(--dm-border);display:flex;align-items:center;justify-content:space-between;">
|
|
6
|
-
<span style="font-weight:600;font-size:0.85rem;">Folders</span>
|
|
7
|
-
<button id="new-folder-btn" class="btn btn-sm btn-ghost" title="New Folder">
|
|
8
|
-
<span data-icon="plus" data-icon-size="14"></span>
|
|
9
|
-
</button>
|
|
10
|
-
</div>
|
|
11
|
-
<div id="folder-sidebar" style="flex:1;overflow-y:auto;padding:0.5rem;"></div>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<!-- Document list -->
|
|
15
|
-
<div class="docs-list-pane" style="border-right:1px solid var(--dm-border);display:flex;flex-direction:column;">
|
|
16
|
-
<div style="padding:0.75rem;border-bottom:1px solid var(--dm-border);">
|
|
17
|
-
<input id="doc-search" class="form-input form-input-sm" placeholder="Search documents..." style="width:100%;">
|
|
18
|
-
</div>
|
|
19
|
-
<div style="padding:0.5rem;border-bottom:1px solid var(--dm-border);display:flex;gap:0.5rem;">
|
|
20
|
-
<button id="new-doc-btn" class="btn btn-sm btn-primary" style="flex:1;">
|
|
21
|
-
<span data-icon="plus" data-icon-size="14"></span> New
|
|
22
|
-
</button>
|
|
23
|
-
<button id="new-from-template-btn" class="btn btn-sm btn-outline" title="New from Template">
|
|
24
|
-
<span data-icon="layout" data-icon-size="14"></span>
|
|
25
|
-
</button>
|
|
26
|
-
</div>
|
|
27
|
-
<div id="doc-list" style="flex:1;overflow-y:auto;"></div>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<!-- Editor pane -->
|
|
31
|
-
<div style="display:flex;flex-direction:column;overflow:hidden;">
|
|
32
|
-
|
|
33
|
-
<!-- Placeholder when no doc is selected -->
|
|
34
|
-
<div id="editor-placeholder" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dm-text-muted);">
|
|
35
|
-
<div style="text-align:center;">
|
|
36
|
-
<span data-icon="book-open" data-icon-size="48" style="display:block;margin-bottom:1rem;opacity:0.4;"></span>
|
|
37
|
-
<p>Select a document to start editing</p>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<!-- Editor when a doc is open -->
|
|
42
|
-
<div id="editor-pane" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
43
|
-
<!-- Toolbar -->
|
|
44
|
-
<div style="padding:0.5rem 0.75rem;border-bottom:1px solid var(--dm-border);display:flex;align-items:center;gap:0.5rem;flex-shrink:0;">
|
|
45
|
-
<input id="doc-title-input" class="form-input" placeholder="Document title"
|
|
46
|
-
style="flex:1;border:none;font-weight:600;font-size:1rem;background:transparent;outline:none;padding:0.25rem;">
|
|
47
|
-
<button id="save-doc-btn" class="btn btn-sm btn-primary">
|
|
48
|
-
<span data-icon="save" data-icon-size="14"></span> Save
|
|
49
|
-
</button>
|
|
50
|
-
<button id="find-replace-btn" class="btn btn-sm btn-ghost" title="Find & Replace">
|
|
51
|
-
<span data-icon="search" data-icon-size="14"></span>
|
|
52
|
-
</button>
|
|
53
|
-
<button id="version-history-btn" class="btn btn-sm btn-ghost" title="Version History">
|
|
54
|
-
<span data-icon="clock" data-icon-size="14"></span>
|
|
55
|
-
</button>
|
|
56
|
-
<button id="duplicate-doc-btn" class="btn btn-sm btn-ghost" title="Duplicate Document">
|
|
57
|
-
<span data-icon="copy" data-icon-size="14"></span>
|
|
58
|
-
</button>
|
|
59
|
-
<button id="delete-doc-btn" class="btn btn-sm btn-ghost btn-danger" title="Delete Document">
|
|
60
|
-
<span data-icon="trash" data-icon-size="14"></span>
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
<!-- Content area -->
|
|
64
|
-
<div id="doc-editor-content" style="flex:1;overflow-y:auto;padding:1rem;"></div>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
</div>
|