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.
- package/admin/js/app.js +4 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +8 -6
- package/config/plugins.json +4 -0
- package/package.json +1 -1
- package/plugins/analytics/stats.json +1 -1
- package/plugins/job-board/admin/templates/application-detail.html +40 -0
- package/plugins/job-board/admin/templates/applications.html +10 -0
- package/plugins/job-board/admin/templates/companies.html +24 -0
- package/plugins/job-board/admin/templates/dashboard.html +36 -0
- package/plugins/job-board/admin/templates/job-editor.html +17 -0
- package/plugins/job-board/admin/templates/jobs.html +15 -0
- package/plugins/job-board/admin/templates/profile.html +17 -0
- package/plugins/job-board/admin/views/application-detail.js +62 -0
- package/plugins/job-board/admin/views/applications.js +47 -0
- package/plugins/job-board/admin/views/companies.js +104 -0
- package/plugins/job-board/admin/views/dashboard.js +88 -0
- package/plugins/job-board/admin/views/job-editor.js +86 -0
- package/plugins/job-board/admin/views/jobs.js +53 -0
- package/plugins/job-board/admin/views/profile.js +47 -0
- package/plugins/job-board/config.js +6 -0
- package/plugins/job-board/plugin.js +466 -0
- package/plugins/job-board/plugin.json +40 -0
- package/plugins/job-board/schemas/jb-agent-companies.json +17 -0
- package/plugins/job-board/schemas/jb-applications.json +20 -0
- package/plugins/job-board/schemas/jb-candidate-profiles.json +20 -0
- package/plugins/job-board/schemas/jb-companies.json +21 -0
- package/plugins/job-board/schemas/jb-jobs.json +23 -0
- package/server/routes/api/collections.js +4 -0
- package/server/routes/api/plugins.js +9 -1
- package/server/services/plugins.js +30 -0
- package/plugins/example-analytics/admin/templates/analytics.html +0 -10
- package/plugins/example-analytics/admin/views/analytics.js +0 -51
- package/plugins/example-analytics/config.js +0 -6
- package/plugins/example-analytics/plugin.js +0 -58
- package/plugins/example-analytics/plugin.json +0 -45
- package/plugins/example-analytics/public/inject-body.html +0 -14
- package/plugins/example-analytics/public/inject-head.html +0 -1
- 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,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
|
-
}
|