domma-cms 0.6.15 → 0.6.16

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 (39) hide show
  1. package/admin/js/app.js +4 -4
  2. package/admin/js/config/sidebar-config.js +1 -1
  3. package/admin/js/lib/markdown-toolbar.js +8 -6
  4. package/config/plugins.json +4 -0
  5. package/package.json +1 -1
  6. package/plugins/analytics/stats.json +1 -1
  7. package/plugins/job-board/admin/templates/application-detail.html +40 -0
  8. package/plugins/job-board/admin/templates/applications.html +10 -0
  9. package/plugins/job-board/admin/templates/companies.html +24 -0
  10. package/plugins/job-board/admin/templates/dashboard.html +36 -0
  11. package/plugins/job-board/admin/templates/job-editor.html +17 -0
  12. package/plugins/job-board/admin/templates/jobs.html +15 -0
  13. package/plugins/job-board/admin/templates/profile.html +17 -0
  14. package/plugins/job-board/admin/views/application-detail.js +62 -0
  15. package/plugins/job-board/admin/views/applications.js +47 -0
  16. package/plugins/job-board/admin/views/companies.js +104 -0
  17. package/plugins/job-board/admin/views/dashboard.js +88 -0
  18. package/plugins/job-board/admin/views/job-editor.js +86 -0
  19. package/plugins/job-board/admin/views/jobs.js +53 -0
  20. package/plugins/job-board/admin/views/profile.js +47 -0
  21. package/plugins/job-board/config.js +6 -0
  22. package/plugins/job-board/plugin.js +466 -0
  23. package/plugins/job-board/plugin.json +40 -0
  24. package/plugins/job-board/schemas/jb-agent-companies.json +17 -0
  25. package/plugins/job-board/schemas/jb-applications.json +20 -0
  26. package/plugins/job-board/schemas/jb-candidate-profiles.json +20 -0
  27. package/plugins/job-board/schemas/jb-companies.json +21 -0
  28. package/plugins/job-board/schemas/jb-jobs.json +23 -0
  29. package/server/routes/api/collections.js +4 -0
  30. package/server/routes/api/plugins.js +9 -1
  31. package/server/services/plugins.js +30 -0
  32. package/plugins/example-analytics/admin/templates/analytics.html +0 -10
  33. package/plugins/example-analytics/admin/views/analytics.js +0 -51
  34. package/plugins/example-analytics/config.js +0 -6
  35. package/plugins/example-analytics/plugin.js +0 -58
  36. package/plugins/example-analytics/plugin.json +0 -45
  37. package/plugins/example-analytics/public/inject-body.html +0 -14
  38. package/plugins/example-analytics/public/inject-head.html +0 -1
  39. package/plugins/example-analytics/stats.json +0 -24
@@ -5,7 +5,7 @@
5
5
  * GET /api/plugins/admin-config - sidebar/routes/views for enabled plugins (authenticated)
6
6
  */
7
7
  import { authenticate, requireAdmin, requirePermission } from '../../middleware/auth.js';
8
- import { discoverPlugins, getPluginStates, savePluginState, getAdminPluginConfig } from '../../services/plugins.js';
8
+ import { discoverPlugins, getPluginStates, savePluginState, getAdminPluginConfig, runLifecycleHook } from '../../services/plugins.js';
9
9
 
10
10
  export async function pluginsRoutes(fastify) {
11
11
  const canRead = { preHandler: [authenticate, requirePermission('plugins', 'read')] };
@@ -38,7 +38,15 @@ export async function pluginsRoutes(fastify) {
38
38
  const manifest = manifests.find(m => m.name === name);
39
39
  if (!manifest) return reply.status(404).send({ error: 'Plugin not found' });
40
40
 
41
+ const prevState = getPluginStates()[name] || {};
41
42
  savePluginState(name, { enabled: !!enabled, settings: settings || {} });
43
+
44
+ if (!prevState.enabled && !!enabled) {
45
+ await runLifecycleHook(name, 'onEnable', fastify);
46
+ } else if (prevState.enabled && !enabled) {
47
+ await runLifecycleHook(name, 'onDisable', fastify);
48
+ }
49
+
42
50
  return { success: true };
43
51
  });
44
52
 
@@ -224,6 +224,36 @@ export async function getInjectionSnippets() {
224
224
  return {head, headLate, bodyEnd};
225
225
  }
226
226
 
227
+ /**
228
+ * Run a lifecycle hook (onEnable or onDisable) for a plugin if it exports one.
229
+ * Dynamically imports plugin.js and calls the named export with a context object.
230
+ * Errors are logged but do not crash the process.
231
+ *
232
+ * @param {string} name - Plugin directory name
233
+ * @param {string} hook - Export name to call ('onEnable' or 'onDisable')
234
+ * @param {import('fastify').FastifyInstance} fastify
235
+ * @returns {Promise<void>}
236
+ */
237
+ export async function runLifecycleHook(name, hook, fastify) {
238
+ // Validate hook name
239
+ if (!['onEnable', 'onDisable'].includes(hook)) return;
240
+
241
+ const pluginJsPath = path.join(PLUGINS_DIR, name, 'plugin.js');
242
+ try {
243
+ const mod = await import(pluginJsPath);
244
+ if (typeof mod[hook] !== 'function') return;
245
+
246
+ const [collections, roles] = await Promise.all([
247
+ import(path.resolve('server/services/collections.js')),
248
+ import(path.resolve('server/services/roles.js')),
249
+ ]);
250
+
251
+ await mod[hook]({ fastify, services: { collections, roles } });
252
+ } catch (err) {
253
+ fastify.log.error(`Plugin "${name}" lifecycle hook "${hook}" failed: ${err.message}`);
254
+ }
255
+ }
256
+
227
257
  /**
228
258
  * Return merged sidebar items, routes, and views from all enabled plugins.
229
259
  * Used by the frontend to dynamically extend the admin panel.
@@ -1,10 +0,0 @@
1
- <div class="view-header">
2
- <h1><span data-icon="chart-bar"></span> Analytics</h1>
3
- <button id="reset-btn" class="btn btn-ghost btn-sm">Reset stats</button>
4
- </div>
5
-
6
- <div class="card">
7
- <div class="card-body">
8
- <div id="analytics-table"></div>
9
- </div>
10
- </div>
@@ -1,51 +0,0 @@
1
- /**
2
- * Analytics Plugin — Admin View
3
- * Shows a sortable table of page hit counts.
4
- * Loaded dynamically from /plugins/ static path.
5
- */
6
- export const analyticsView = {
7
- templateUrl: '/plugins/example-analytics/admin/templates/analytics.html',
8
-
9
- async onMount($container) {
10
- await loadStats($container);
11
-
12
- $container.find('#reset-btn').on('click', async () => {
13
- const confirmed = await E.confirm('Reset all analytics data? This cannot be undone.');
14
- if (!confirmed) return;
15
- try {
16
- await fetch('/api/plugins/example-analytics/stats', {
17
- method: 'DELETE',
18
- headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
19
- });
20
- E.toast('Analytics reset.', {type: 'success'});
21
- await loadStats($container);
22
- } catch {
23
- E.toast('Reset failed.', {type: 'error'});
24
- }
25
- });
26
-
27
- Domma.icons.scan();
28
- }
29
- };
30
-
31
- async function loadStats($container) {
32
- let stats = [];
33
- try {
34
- const res = await fetch('/api/plugins/example-analytics/stats', {
35
- headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
36
- });
37
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
38
- stats = await res.json();
39
- } catch {
40
- E.toast('Could not load analytics data.', {type: 'error'});
41
- }
42
-
43
- T.create('#analytics-table', {
44
- data: stats,
45
- columns: [
46
- {key: 'url', title: 'Page URL', render: (val) => `<code>${val}</code>`},
47
- {key: 'hits', title: 'Page views'}
48
- ],
49
- emptyMessage: 'No page views recorded yet.'
50
- });
51
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Example Analytics Plugin — Default Configuration
3
- */
4
- export default {
5
- excludeAdmin: true
6
- };
@@ -1,58 +0,0 @@
1
- /**
2
- * Example Analytics Plugin — Server
3
- * Tracks page hits in a JSON file alongside the plugin.
4
- * Endpoints:
5
- * POST /api/plugins/example-analytics/hit - public: record a hit { url }
6
- * GET /api/plugins/example-analytics/stats - admin: return all hit counts
7
- * DELETE /api/plugins/example-analytics/stats - admin: reset all stats
8
- */
9
- import fs from 'fs/promises';
10
- import path from 'path';
11
- import {fileURLToPath} from 'url';
12
-
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const STATS_FILE = path.join(__dirname, 'stats.json');
15
-
16
- async function readStats() {
17
- try {
18
- return JSON.parse(await fs.readFile(STATS_FILE, 'utf8'));
19
- } catch {
20
- return {};
21
- }
22
- }
23
-
24
- async function writeStats(stats) {
25
- await fs.writeFile(STATS_FILE, JSON.stringify(stats, null, 2) + '\n', 'utf8');
26
- }
27
-
28
- export default async function analyticsPlugin(fastify, options) {
29
- const {authenticate, requireAdmin} = options.auth;
30
-
31
- // Record a page hit — called by the client-side injection script (public)
32
- fastify.post('/hit', async (request, reply) => {
33
- const {url} = request.body || {};
34
- if (!url || typeof url !== 'string') {
35
- return reply.status(400).send({error: 'url is required'});
36
- }
37
-
38
- const normalised = url.split('?')[0].replace(/\/$/, '') || '/';
39
- const stats = await readStats();
40
- stats[normalised] = (stats[normalised] || 0) + 1;
41
- await writeStats(stats);
42
- return {ok: true};
43
- });
44
-
45
- // Return all stats — admin only
46
- fastify.get('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
47
- const stats = await readStats();
48
- return Object.entries(stats)
49
- .map(([url, hits]) => ({url, hits}))
50
- .sort((a, b) => b.hits - a.hits);
51
- });
52
-
53
- // Reset stats — admin only
54
- fastify.delete('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
55
- await writeStats({});
56
- return {ok: true};
57
- });
58
- }
@@ -1,45 +0,0 @@
1
- {
2
- "name": "example-analytics",
3
- "displayName": "Analytics",
4
- "version": "1.0.0",
5
- "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-01",
8
- "icon": "chart-bar",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "analytics",
13
- "text": "Analytics",
14
- "icon": "chart-bar",
15
- "url": "#/plugins/analytics",
16
- "section": "#/plugins/analytics"
17
- }
18
- ],
19
- "routes": [
20
- {
21
- "path": "/plugins/analytics",
22
- "view": "plugin-analytics",
23
- "title": "Analytics - Domma CMS"
24
- }
25
- ],
26
- "views": {
27
- "plugin-analytics": {
28
- "entry": "example-analytics/admin/views/analytics.js",
29
- "exportName": "analyticsView"
30
- }
31
- }
32
- },
33
- "inject": {
34
- "head": "public/inject-head.html",
35
- "bodyEnd": "public/inject-body.html"
36
- },
37
- "scaffold": {
38
- "reset": [
39
- {
40
- "path": "stats.json",
41
- "content": "{}"
42
- }
43
- ]
44
- }
45
- }
@@ -1,14 +0,0 @@
1
- <!-- example-analytics: page view tracker -->
2
- <script>
3
- (function () {
4
- var url = window.location.pathname;
5
- if (typeof fetch === 'function') {
6
- fetch('/api/plugins/example-analytics/hit', {
7
- method: 'POST',
8
- headers: {'Content-Type': 'application/json'},
9
- body: JSON.stringify({url: url})
10
- }).catch(function () { /* silent fail */
11
- });
12
- }
13
- })();
14
- </script>
@@ -1 +0,0 @@
1
- <!-- example-analytics: head injection (empty — tracking is done via body script) -->
@@ -1,24 +0,0 @@
1
- {
2
- "/": 138,
3
- "/about": 71,
4
- "/blog": 30,
5
- "/contact": 30,
6
- "/resources/typography": 4,
7
- "/resources": 13,
8
- "/resources/shortcodes": 14,
9
- "/resources/cards": 15,
10
- "/resources/interactive": 13,
11
- "/resources/grid": 6,
12
- "/forms": 14,
13
- "/resources/effects": 6,
14
- "/blog/hello-world": 20,
15
- "/feedback": 38,
16
- "/resources/dependencies": 2,
17
- "/resources/components": 6,
18
- "/gdpr": 3,
19
- "/scratch": 51,
20
- "/getting-started": 3,
21
- "/resources/pro": 1,
22
- "/todo": 23,
23
- "/thank-you": 1
24
- }