webspresso 0.0.77 → 0.0.78

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.
Files changed (33) hide show
  1. package/README.md +5 -0
  2. package/bin/commands/db-scaffold.js +81 -0
  3. package/bin/utils/model-migrations.js +211 -0
  4. package/bin/webspresso.js +2 -0
  5. package/core/content/cache.js +64 -0
  6. package/core/content/field-types.js +180 -0
  7. package/core/content/index.js +30 -0
  8. package/core/content/renderer.js +84 -0
  9. package/core/content/schema.js +75 -0
  10. package/core/content/service.js +400 -0
  11. package/core/content/types.js +59 -0
  12. package/index.d.ts +17 -0
  13. package/index.js +7 -0
  14. package/package.json +1 -1
  15. package/plugins/admin-panel/app.js +7 -7
  16. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
  17. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
  18. package/plugins/admin-panel/index.js +17 -18
  19. package/plugins/admin-panel/modules/menu.js +1 -0
  20. package/plugins/content/admin/content-entries-component.js +291 -0
  21. package/plugins/content/admin/content-types-component.js +250 -0
  22. package/plugins/content/api-handlers.js +157 -0
  23. package/plugins/content/client/inline-edit.css +296 -0
  24. package/plugins/content/client/inline-edit.js +366 -0
  25. package/plugins/content/helpers.js +77 -0
  26. package/plugins/content/index.js +231 -0
  27. package/plugins/content/migration-template.js +54 -0
  28. package/plugins/content/models/content-entry.js +45 -0
  29. package/plugins/content/models/content-type.js +36 -0
  30. package/plugins/index.js +2 -0
  31. package/src/file-router.js +21 -1
  32. package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
  33. package/templates/skills/webspresso-usage/SKILL.md +5 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Inline content editing for admin users on public pages.
3
+ * Popover anchored to the edit trigger — responsive bottom sheet on small screens.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ var config = window.__WS_CONTENT__;
9
+ if (!config || !config.enabled) return;
10
+
11
+ var API = config.adminPath + '/api/content';
12
+ var modalEl = null;
13
+ var activeEntry = null;
14
+ var activeSchema = null;
15
+ var activeAnchor = null;
16
+ var activeHost = null;
17
+ var formState = {};
18
+ var repositionBound = false;
19
+
20
+ function qs(sel, root) {
21
+ return (root || document).querySelector(sel);
22
+ }
23
+
24
+ function qsa(sel, root) {
25
+ return Array.prototype.slice.call((root || document).querySelectorAll(sel));
26
+ }
27
+
28
+ function ensureEditor() {
29
+ if (modalEl) return modalEl;
30
+ modalEl = document.createElement('div');
31
+ modalEl.id = 'ws-content-modal';
32
+ modalEl.innerHTML =
33
+ '<div class="ws-content-modal-backdrop" data-close="1"></div>' +
34
+ '<div class="ws-content-modal-panel" role="dialog" aria-modal="true" aria-labelledby="ws-content-modal-title">' +
35
+ '<div class="ws-content-popover-arrow" aria-hidden="true"></div>' +
36
+ '<div class="ws-content-modal-header">' +
37
+ '<h3 class="ws-content-modal-title" id="ws-content-modal-title">Edit content</h3>' +
38
+ '<button type="button" class="ws-content-modal-close" data-close="1" aria-label="Close">&times;</button>' +
39
+ '</div>' +
40
+ '<div class="ws-content-modal-body"></div>' +
41
+ '<div class="ws-content-modal-error" hidden></div>' +
42
+ '<div class="ws-content-modal-footer">' +
43
+ '<button type="button" class="ws-content-btn ws-content-btn-muted" data-close="1">Cancel</button>' +
44
+ '<button type="button" class="ws-content-btn ws-content-btn-primary" data-save="1">Save</button>' +
45
+ '</div></div>';
46
+ document.body.appendChild(modalEl);
47
+
48
+ modalEl.addEventListener('click', function (e) {
49
+ if (e.target && e.target.getAttribute('data-close')) closeEditor();
50
+ });
51
+ qs('[data-save="1"]', modalEl).addEventListener('click', saveEditor);
52
+
53
+ document.addEventListener('keydown', function (e) {
54
+ if (e.key === 'Escape' && modalEl.classList.contains('is-open')) {
55
+ e.preventDefault();
56
+ closeEditor();
57
+ }
58
+ });
59
+
60
+ if (!repositionBound) {
61
+ repositionBound = true;
62
+ window.addEventListener('resize', function () {
63
+ if (modalEl.classList.contains('is-open')) positionPopover();
64
+ });
65
+ window.addEventListener(
66
+ 'scroll',
67
+ function () {
68
+ if (modalEl.classList.contains('is-open')) positionPopover();
69
+ },
70
+ true
71
+ );
72
+ }
73
+
74
+ return modalEl;
75
+ }
76
+
77
+ function closeEditor() {
78
+ if (modalEl) modalEl.classList.remove('is-open');
79
+ if (activeHost) activeHost.classList.remove('is-editing');
80
+ activeEntry = null;
81
+ activeSchema = null;
82
+ activeAnchor = null;
83
+ activeHost = null;
84
+ formState = {};
85
+ }
86
+
87
+ function showError(msg) {
88
+ var el = qs('.ws-content-modal-error', modalEl);
89
+ if (!el) return;
90
+ if (msg) {
91
+ el.textContent = msg;
92
+ el.hidden = false;
93
+ } else {
94
+ el.textContent = '';
95
+ el.hidden = true;
96
+ }
97
+ }
98
+
99
+ function positionPopover() {
100
+ if (!modalEl || !activeAnchor) return;
101
+ var panel = qs('.ws-content-modal-panel', modalEl);
102
+ if (!panel) return;
103
+
104
+ if (window.innerWidth < 640) {
105
+ panel.classList.add('is-sheet');
106
+ panel.style.top = '';
107
+ panel.style.left = '';
108
+ panel.style.visibility = 'visible';
109
+ return;
110
+ }
111
+
112
+ panel.classList.remove('is-sheet');
113
+ panel.style.visibility = 'hidden';
114
+ panel.style.display = 'flex';
115
+
116
+ var panelRect = panel.getBoundingClientRect();
117
+ var anchorRect = activeAnchor.getBoundingClientRect();
118
+ var gap = 10;
119
+ var pad = 12;
120
+ var vw = window.innerWidth;
121
+ var vh = window.innerHeight;
122
+
123
+ var top = anchorRect.bottom + gap;
124
+ var left = anchorRect.left + anchorRect.width / 2 - panelRect.width / 2;
125
+ var above = false;
126
+
127
+ if (top + panelRect.height > vh - pad && anchorRect.top - panelRect.height - gap > pad) {
128
+ top = anchorRect.top - panelRect.height - gap;
129
+ above = true;
130
+ }
131
+
132
+ top = Math.max(pad, Math.min(top, vh - panelRect.height - pad));
133
+ left = Math.max(pad, Math.min(left, vw - panelRect.width - pad));
134
+
135
+ panel.style.top = top + 'px';
136
+ panel.style.left = left + 'px';
137
+ panel.style.visibility = 'visible';
138
+ panel.classList.toggle('is-above', above);
139
+
140
+ var arrow = qs('.ws-content-popover-arrow', panel);
141
+ if (arrow) {
142
+ var arrowLeft = anchorRect.left + anchorRect.width / 2 - left;
143
+ arrow.style.left = Math.max(16, Math.min(arrowLeft, panelRect.width - 16)) + 'px';
144
+ }
145
+ }
146
+
147
+ function focusFirstField() {
148
+ var body = qs('.ws-content-modal-body', modalEl);
149
+ if (!body) return;
150
+ var input = body.querySelector('input, textarea, select');
151
+ if (input) input.focus();
152
+ }
153
+
154
+ function renderField(field, container) {
155
+ var wrap = document.createElement('div');
156
+ wrap.className = 'ws-content-field';
157
+ var label = document.createElement('label');
158
+ label.textContent = field.label || field.name;
159
+ wrap.appendChild(label);
160
+
161
+ var value = formState[field.name];
162
+ var input;
163
+
164
+ if (field.type === 'boolean') {
165
+ input = document.createElement('input');
166
+ input.type = 'checkbox';
167
+ input.checked = !!value;
168
+ input.addEventListener('change', function () {
169
+ formState[field.name] = input.checked;
170
+ });
171
+ } else if (field.type === 'textarea' || field.type === 'rich-text') {
172
+ input = document.createElement('textarea');
173
+ input.rows = field.type === 'rich-text' ? 6 : 3;
174
+ input.value = value || '';
175
+ input.addEventListener('input', function () {
176
+ formState[field.name] = input.value;
177
+ });
178
+ } else if (field.type === 'select') {
179
+ input = document.createElement('select');
180
+ (field.options || []).forEach(function (opt) {
181
+ var o = document.createElement('option');
182
+ o.value = opt;
183
+ o.textContent = opt;
184
+ if (value === opt) o.selected = true;
185
+ input.appendChild(o);
186
+ });
187
+ input.addEventListener('change', function () {
188
+ formState[field.name] = input.value;
189
+ });
190
+ } else {
191
+ input = document.createElement('input');
192
+ input.type = field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text';
193
+ input.value = value != null ? value : '';
194
+ input.addEventListener('input', function () {
195
+ formState[field.name] = field.type === 'number' ? Number(input.value) : input.value;
196
+ });
197
+ }
198
+
199
+ wrap.appendChild(input);
200
+ container.appendChild(wrap);
201
+ }
202
+
203
+ function openEditor(anchorEl, hostEl, entryId, typeSlug) {
204
+ ensureEditor();
205
+ showError('');
206
+ activeAnchor = anchorEl;
207
+ activeHost = hostEl;
208
+ if (activeHost) activeHost.classList.add('is-editing');
209
+
210
+ var body = qs('.ws-content-modal-body', modalEl);
211
+ body.innerHTML = '<p>Loading…</p>';
212
+ modalEl.classList.add('is-open');
213
+ positionPopover();
214
+
215
+ Promise.all([
216
+ fetch(API + '/types/' + encodeURIComponent(typeSlug) + '/schema', { credentials: 'include' }).then(function (r) {
217
+ return r.json();
218
+ }),
219
+ fetch(API + '/entries/' + entryId, { credentials: 'include' }).then(function (r) {
220
+ return r.json();
221
+ }),
222
+ ])
223
+ .then(function (results) {
224
+ if (!results[0].data || !results[1].data) throw new Error('Failed to load content');
225
+ activeSchema = results[0].data.schema;
226
+ activeEntry = results[1].data;
227
+ formState = Object.assign({}, activeEntry.data || {});
228
+ body.innerHTML = '';
229
+ var title = qs('.ws-content-modal-title', modalEl);
230
+ if (title) title.textContent = 'Edit: ' + (activeEntry.title || activeEntry.slug);
231
+ (activeSchema.fields || []).forEach(function (field) {
232
+ renderField(field, body);
233
+ });
234
+ requestAnimationFrame(function () {
235
+ positionPopover();
236
+ focusFirstField();
237
+ });
238
+ })
239
+ .catch(function (err) {
240
+ body.innerHTML = '';
241
+ showError(err.message || 'Load failed');
242
+ positionPopover();
243
+ });
244
+ }
245
+
246
+ function updateDomFields(entryId, data) {
247
+ qsa('[data-ws-content-entry="' + entryId + '"][data-ws-content-field]').forEach(function (el) {
248
+ var field = el.getAttribute('data-ws-content-field');
249
+ if (!field || !(field in data)) return;
250
+ var val = data[field];
251
+ var isHtml = el.getAttribute('data-ws-content-html') === 'true';
252
+ if (isHtml) {
253
+ el.innerHTML = val || '';
254
+ } else {
255
+ el.textContent = val != null ? val : '';
256
+ }
257
+ });
258
+ }
259
+
260
+ function saveEditor() {
261
+ if (!activeEntry) return;
262
+ showError('');
263
+ var saveBtn = qs('[data-save="1"]', modalEl);
264
+ saveBtn.disabled = true;
265
+ saveBtn.textContent = 'Saving…';
266
+
267
+ fetch(API + '/entries/' + activeEntry.id, {
268
+ method: 'PUT',
269
+ credentials: 'include',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ data: formState }),
272
+ })
273
+ .then(function (r) {
274
+ return r.json().then(function (json) {
275
+ if (!r.ok) throw new Error(json.error || 'Save failed');
276
+ return json;
277
+ });
278
+ })
279
+ .then(function (res) {
280
+ var data = (res.data && res.data.data) || formState;
281
+ updateDomFields(activeEntry.id, data);
282
+ closeEditor();
283
+ })
284
+ .catch(function (err) {
285
+ showError(err.message || 'Save failed');
286
+ })
287
+ .finally(function () {
288
+ saveBtn.disabled = false;
289
+ saveBtn.textContent = 'Save';
290
+ });
291
+ }
292
+
293
+ function lowestCommonAncestor(nodes) {
294
+ if (!nodes.length) return null;
295
+ var ancestor = nodes[0].parentElement;
296
+ while (ancestor && ancestor !== document.body) {
297
+ var ok = nodes.every(function (n) {
298
+ return ancestor.contains(n);
299
+ });
300
+ if (ok) return ancestor;
301
+ ancestor = ancestor.parentElement;
302
+ }
303
+ return nodes[0].parentElement;
304
+ }
305
+
306
+ function getEntryHosts() {
307
+ var byEntry = {};
308
+ qsa('[data-ws-content-entry]').forEach(function (el) {
309
+ var entryId = el.getAttribute('data-ws-content-entry');
310
+ var typeSlug = el.getAttribute('data-ws-content-type');
311
+ if (!entryId || !typeSlug) return;
312
+ if (!byEntry[entryId]) byEntry[entryId] = { typeSlug: typeSlug, nodes: [] };
313
+ byEntry[entryId].nodes.push(el);
314
+ });
315
+
316
+ var hosts = [];
317
+ Object.keys(byEntry).forEach(function (entryId) {
318
+ var info = byEntry[entryId];
319
+ var block = qs('.ws-content-block[data-ws-content-entry="' + entryId + '"]');
320
+ var host = block || lowestCommonAncestor(info.nodes);
321
+ if (!host) return;
322
+ hosts.push({ host: host, entryId: entryId, typeSlug: info.typeSlug });
323
+ });
324
+ return hosts;
325
+ }
326
+
327
+ function attachToolbar(host, entryId, typeSlug) {
328
+ if (host.querySelector('.ws-content-toolbar')) return;
329
+ host.classList.add('ws-content-host');
330
+
331
+ var toolbar = document.createElement('div');
332
+ toolbar.className = 'ws-content-toolbar';
333
+
334
+ var btn = document.createElement('button');
335
+ btn.type = 'button';
336
+ btn.className = 'ws-content-edit-btn';
337
+ btn.title = 'Edit content';
338
+ btn.setAttribute('aria-haspopup', 'dialog');
339
+ btn.innerHTML = '<span class="ws-content-edit-btn-icon" aria-hidden="true">✎</span> Edit';
340
+ btn.addEventListener('click', function (e) {
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ openEditor(btn, host, entryId, typeSlug);
344
+ });
345
+
346
+ toolbar.appendChild(btn);
347
+ host.insertBefore(toolbar, host.firstChild);
348
+ }
349
+
350
+ function attachEditButtons() {
351
+ getEntryHosts().forEach(function (item) {
352
+ attachToolbar(item.host, item.entryId, item.typeSlug);
353
+ });
354
+ }
355
+
356
+ function init() {
357
+ attachEditButtons();
358
+ document.addEventListener('DOMContentLoaded', attachEditButtons);
359
+ }
360
+
361
+ if (document.readyState === 'loading') {
362
+ document.addEventListener('DOMContentLoaded', init);
363
+ } else {
364
+ init();
365
+ }
366
+ })();
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Request-scoped template helpers for content inline editing.
3
+ * @module plugins/content/helpers
4
+ */
5
+
6
+ const { wrapEditable, wrapEntryBlock } = require('../../core/content');
7
+
8
+ /**
9
+ * @param {import('express').Request} req
10
+ * @param {string} [adminPath='/_admin']
11
+ * @returns {boolean}
12
+ */
13
+ function isAdminSession(req, adminPath = '/_admin') {
14
+ return Boolean(req.session && req.session.adminUser);
15
+ }
16
+
17
+ /**
18
+ * @param {Object} options
19
+ * @param {string} [options.adminPath='/_admin']
20
+ * @returns {(req: import('express').Request) => Object}
21
+ */
22
+ function createRequestContentHelpers(options = {}) {
23
+ const adminPath = options.adminPath || '/_admin';
24
+
25
+ return function buildContentHelpers(req) {
26
+ const isAdmin = isAdminSession(req);
27
+
28
+ return {
29
+ isAdmin() {
30
+ return isAdmin;
31
+ },
32
+
33
+ adminPath() {
34
+ return adminPath;
35
+ },
36
+
37
+ /**
38
+ * @param {import('../../core/content/types').ContentEntryResult|null|undefined} entry
39
+ */
40
+ raw(entry) {
41
+ return entry?.data ?? {};
42
+ },
43
+
44
+ /**
45
+ * @param {unknown} value
46
+ * @param {Object} meta
47
+ * @param {number|string} meta.entryId
48
+ * @param {string} meta.typeSlug
49
+ * @param {string} [meta.field]
50
+ * @param {string} [meta.label]
51
+ * @param {boolean} [meta.safeHtml]
52
+ */
53
+ editable(value, meta = {}) {
54
+ return wrapEditable(value, {
55
+ ...meta,
56
+ isAdmin,
57
+ });
58
+ },
59
+
60
+ /**
61
+ * @param {string} innerHtml
62
+ * @param {Object} meta
63
+ */
64
+ block(innerHtml, meta = {}) {
65
+ return wrapEntryBlock(innerHtml, {
66
+ ...meta,
67
+ isAdmin,
68
+ });
69
+ },
70
+ };
71
+ };
72
+ }
73
+
74
+ module.exports = {
75
+ isAdminSession,
76
+ createRequestContentHelpers,
77
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Schema-driven content CMS plugin for Webspresso.
3
+ * @module plugins/content
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { createContentService } = require('../../core/content');
9
+ const { createContentTypeModel } = require('./models/content-type');
10
+ const { createContentEntryModel } = require('./models/content-entry');
11
+ const { createContentApiHandlers } = require('./api-handlers');
12
+ const { createRequestContentHelpers, isAdminSession } = require('./helpers');
13
+ const { generateContentTypesComponent } = require('./admin/content-types-component');
14
+ const { generateContentEntriesComponent } = require('./admin/content-entries-component');
15
+ const { generateContentMigrations } = require('./migration-template');
16
+
17
+ const INLINE_EDIT_JS = fs.readFileSync(path.join(__dirname, 'client/inline-edit.js'), 'utf8');
18
+ const INLINE_EDIT_CSS = fs.readFileSync(path.join(__dirname, 'client/inline-edit.css'), 'utf8');
19
+
20
+ /**
21
+ * @param {Object} options
22
+ * @param {import('../../core/orm').Database} options.db
23
+ * @param {string} [options.adminPath='/_admin']
24
+ * @param {string} [options.publicApiPath='/api/content']
25
+ * @param {boolean} [options.inlineEdit=true]
26
+ * @param {number|null} [options.cacheTtlMs] - In-memory cache TTL; null (default) = until write invalidation
27
+ */
28
+ function contentPlugin(options = {}) {
29
+ const {
30
+ db,
31
+ adminPath: adminPathOpt,
32
+ publicApiPath = '/api/content',
33
+ inlineEdit = true,
34
+ cacheTtlMs,
35
+ } = options;
36
+
37
+ if (!db) {
38
+ throw new Error('content plugin requires a database instance. Pass `db` in options.');
39
+ }
40
+
41
+ /** @type {ReturnType<typeof createContentService>|null} */
42
+ let contentService = null;
43
+ let adminPath = adminPathOpt || '/_admin';
44
+
45
+ function getService(database) {
46
+ if (!contentService) {
47
+ let sanitizeRichHtml;
48
+ try {
49
+ ({ sanitizeRichHtml } = require('../admin-panel/lib/sanitize-rich-html'));
50
+ } catch {
51
+ sanitizeRichHtml = (v) => v;
52
+ }
53
+ contentService = createContentService(database || db, { cacheTtlMs, sanitizeRichHtml });
54
+ }
55
+ return contentService;
56
+ }
57
+
58
+ function maybeInjectInlineEdit(req, html) {
59
+ if (!inlineEdit || !isAdminSession(req, adminPath)) {
60
+ return html;
61
+ }
62
+ const configScript =
63
+ '<script>window.__WS_CONTENT__=' +
64
+ JSON.stringify({ enabled: true, adminPath }) +
65
+ ';</script>';
66
+ const styleTag = '<style id="ws-content-inline-css">' + INLINE_EDIT_CSS + '</style>';
67
+ const scriptTag = '<script id="ws-content-inline-edit">' + INLINE_EDIT_JS + '</script>';
68
+ const bundle = configScript + styleTag + scriptTag;
69
+ if (html.includes('</body>')) {
70
+ return html.replace('</body>', bundle + '</body>');
71
+ }
72
+ return html + bundle;
73
+ }
74
+
75
+ return {
76
+ name: 'content',
77
+ version: '1.0.0',
78
+ description: 'Schema-driven SQLite CMS with inline admin editing',
79
+ dependencies: { 'admin-panel': '*' },
80
+
81
+ csp: {
82
+ styleSrc: ["'unsafe-inline'"],
83
+ scriptSrc: ["'unsafe-inline'"],
84
+ },
85
+
86
+ api: {
87
+ getContentService: getService,
88
+ createRequestHelpers: () => createRequestContentHelpers({ adminPath }),
89
+ maybeInjectInlineEdit,
90
+ getMigrationTemplate: generateContentMigrations,
91
+ _options: { adminPath },
92
+ },
93
+
94
+ register(ctx) {
95
+ const { hasModel, getModel } = require('../../core/orm/model');
96
+
97
+ if (!hasModel('ContentType')) {
98
+ db.registerModel(createContentTypeModel());
99
+ } else if (!db.hasModel('ContentType')) {
100
+ db.registerModel(getModel('ContentType'));
101
+ }
102
+
103
+ if (!hasModel('ContentEntry')) {
104
+ db.registerModel(createContentEntryModel());
105
+ } else if (!db.hasModel('ContentEntry')) {
106
+ db.registerModel(getModel('ContentEntry'));
107
+ }
108
+
109
+ getService(db);
110
+ },
111
+
112
+ onRoutesReady(ctx) {
113
+ adminPath = adminPathOpt || '/_admin';
114
+ this.api._options = { adminPath };
115
+
116
+ const service = getService(ctx.db || db);
117
+ const handlers = createContentApiHandlers({ contentService: service });
118
+
119
+ const publicBase = publicApiPath.replace(/\/$/, '');
120
+ ctx.addRoute(
121
+ 'get',
122
+ `${publicBase}/:typeSlug/:entrySlug`,
123
+ handlers.publicGetEntryHandler
124
+ );
125
+
126
+ const adminApi = ctx.usePlugin('admin-panel');
127
+ if (!adminApi) {
128
+ console.warn('[content] admin-panel plugin not found, skipping admin UI');
129
+ const { requireAuth } = require('../admin-panel/auth');
130
+ const base = `${adminPath}/api/content`;
131
+ ctx.addRoute('get', `${base}/types`, requireAuth, handlers.listTypesHandler);
132
+ ctx.addRoute('post', `${base}/types`, requireAuth, handlers.createTypeHandler);
133
+ ctx.addRoute('get', `${base}/types/:id`, requireAuth, handlers.getTypeHandler);
134
+ ctx.addRoute('put', `${base}/types/:id`, requireAuth, handlers.updateTypeHandler);
135
+ ctx.addRoute('delete', `${base}/types/:id`, requireAuth, handlers.deleteTypeHandler);
136
+ ctx.addRoute('get', `${base}/types/:typeSlug/schema`, requireAuth, handlers.getTypeSchemaHandler);
137
+ ctx.addRoute('get', `${base}/types/:typeSlug/entries`, requireAuth, handlers.listEntriesHandler);
138
+ ctx.addRoute('post', `${base}/types/:typeSlug/entries`, requireAuth, handlers.createEntryHandler);
139
+ ctx.addRoute('get', `${base}/entries/:id`, requireAuth, handlers.getEntryHandler);
140
+ ctx.addRoute('put', `${base}/entries/:id`, requireAuth, handlers.updateEntryHandler);
141
+ ctx.addRoute('delete', `${base}/entries/:id`, requireAuth, handlers.deleteEntryHandler);
142
+ return;
143
+ }
144
+
145
+ const apiPrefix = '/content';
146
+ adminApi.registerModule({
147
+ id: 'content',
148
+
149
+ menuGroups: {
150
+ cms: {
151
+ label: 'CMS',
152
+ icon: 'database',
153
+ order: -1,
154
+ },
155
+ },
156
+
157
+ menu: [
158
+ {
159
+ id: 'content-types',
160
+ label: 'Content Types',
161
+ path: '/content/types',
162
+ icon: 'database',
163
+ group: 'cms',
164
+ order: 0,
165
+ },
166
+ ],
167
+
168
+ pages: [
169
+ {
170
+ id: 'content-types',
171
+ title: 'Content Types',
172
+ path: '/content/types',
173
+ icon: 'database',
174
+ component: generateContentTypesComponent({ apiPrefix }),
175
+ },
176
+ {
177
+ id: 'content-types-new',
178
+ title: 'New Content Type',
179
+ path: '/content/types/new',
180
+ component: generateContentTypesComponent({ apiPrefix }),
181
+ },
182
+ {
183
+ id: 'content-types-edit',
184
+ title: 'Edit Content Type',
185
+ path: '/content/types/edit/:id',
186
+ component: generateContentTypesComponent({ apiPrefix }),
187
+ },
188
+ {
189
+ id: 'content-entries',
190
+ title: 'Content Entries',
191
+ path: '/content/types/:typeSlug/entries',
192
+ component: generateContentEntriesComponent({ apiPrefix }),
193
+ },
194
+ {
195
+ id: 'content-entries-new',
196
+ title: 'New Content Entry',
197
+ path: '/content/types/:typeSlug/entries/new',
198
+ component: generateContentEntriesComponent({ apiPrefix }),
199
+ },
200
+ {
201
+ id: 'content-entries-edit',
202
+ title: 'Edit Content Entry',
203
+ path: '/content/types/:typeSlug/entries/edit/:id',
204
+ component: generateContentEntriesComponent({ apiPrefix }),
205
+ },
206
+ ],
207
+
208
+ api: {
209
+ prefix: apiPrefix,
210
+ routes: [
211
+ { method: 'get', path: '/types', handler: handlers.listTypesHandler },
212
+ { method: 'post', path: '/types', handler: handlers.createTypeHandler },
213
+ { method: 'get', path: '/types/:typeSlug/schema', handler: handlers.getTypeSchemaHandler },
214
+ { method: 'get', path: '/types/:typeSlug/entries', handler: handlers.listEntriesHandler },
215
+ { method: 'post', path: '/types/:typeSlug/entries', handler: handlers.createEntryHandler },
216
+ { method: 'get', path: '/types/:id', handler: handlers.getTypeHandler },
217
+ { method: 'put', path: '/types/:id', handler: handlers.updateTypeHandler },
218
+ { method: 'delete', path: '/types/:id', handler: handlers.deleteTypeHandler },
219
+ { method: 'get', path: '/entries/:id', handler: handlers.getEntryHandler },
220
+ { method: 'put', path: '/entries/:id', handler: handlers.updateEntryHandler },
221
+ { method: 'delete', path: '/entries/:id', handler: handlers.deleteEntryHandler },
222
+ ],
223
+ },
224
+ });
225
+ },
226
+ };
227
+ }
228
+
229
+ module.exports = contentPlugin;
230
+ module.exports.contentPlugin = contentPlugin;
231
+ module.exports.generateContentMigrations = generateContentMigrations;