domma-cms 0.7.7 → 0.7.9

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 (36) hide show
  1. package/config/plugins.json +0 -12
  2. package/package.json +1 -1
  3. package/plugins/data-transfer/admin/templates/data-transfer.html +0 -172
  4. package/plugins/data-transfer/admin/views/data-transfer.js +0 -558
  5. package/plugins/data-transfer/config.js +0 -9
  6. package/plugins/data-transfer/plugin.js +0 -424
  7. package/plugins/data-transfer/plugin.json +0 -33
  8. package/plugins/job-board/admin/templates/application-detail.html +0 -40
  9. package/plugins/job-board/admin/templates/applications.html +0 -10
  10. package/plugins/job-board/admin/templates/companies.html +0 -24
  11. package/plugins/job-board/admin/templates/dashboard.html +0 -36
  12. package/plugins/job-board/admin/templates/job-editor.html +0 -17
  13. package/plugins/job-board/admin/templates/jobs.html +0 -15
  14. package/plugins/job-board/admin/templates/profile.html +0 -17
  15. package/plugins/job-board/admin/views/application-detail.js +0 -62
  16. package/plugins/job-board/admin/views/applications.js +0 -47
  17. package/plugins/job-board/admin/views/companies.js +0 -104
  18. package/plugins/job-board/admin/views/dashboard.js +0 -88
  19. package/plugins/job-board/admin/views/job-editor.js +0 -86
  20. package/plugins/job-board/admin/views/jobs.js +0 -53
  21. package/plugins/job-board/admin/views/profile.js +0 -47
  22. package/plugins/job-board/config.js +0 -6
  23. package/plugins/job-board/plugin.js +0 -466
  24. package/plugins/job-board/plugin.json +0 -40
  25. package/plugins/job-board/schemas/jb-agent-companies.json +0 -17
  26. package/plugins/job-board/schemas/jb-applications.json +0 -20
  27. package/plugins/job-board/schemas/jb-candidate-profiles.json +0 -20
  28. package/plugins/job-board/schemas/jb-companies.json +0 -21
  29. package/plugins/job-board/schemas/jb-jobs.json +0 -23
  30. package/plugins/theme-roller/admin/templates/theme-roller.html +0 -71
  31. package/plugins/theme-roller/admin/views/theme-roller-view.js +0 -403
  32. package/plugins/theme-roller/config.js +0 -1
  33. package/plugins/theme-roller/plugin.js +0 -233
  34. package/plugins/theme-roller/plugin.json +0 -31
  35. package/plugins/theme-roller/public/active-theme.css +0 -0
  36. package/plugins/theme-roller/public/inject-head-late.html +0 -1
@@ -1,558 +0,0 @@
1
- /**
2
- * Data Transfer Plugin — Admin View
3
- *
4
- * Three tabs: Transfer, Backups, Clone.
5
- * Uses raw adapter-level API for faithful bulk operations.
6
- *
7
- * Pattern: follows garage plugin conventions.
8
- * - api() — fetch wrapper with CMS Bearer token (same as garage)
9
- * - registerIcons() — registers arrow-left-right (not in Domma built-ins)
10
- * - loadCollections() — populates Transfer tab table
11
- * - loadBackups() — populates Backups tab table
12
- * - wireClone() — wires Clone tab form
13
- */
14
-
15
- const BASE = '/api/plugins/data-transfer';
16
-
17
- // ---------------------------------------------------------------------------
18
- // HTTP helper
19
- // ---------------------------------------------------------------------------
20
-
21
- /**
22
- * Authenticated fetch helper. Mirrors the garage plugin pattern exactly.
23
- *
24
- * @param {string} url
25
- * @param {string} [method]
26
- * @param {object} [body]
27
- * @returns {Promise<any>}
28
- */
29
- async function api(url, method = 'GET', body) {
30
- const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
31
- if (body !== undefined) {
32
- opts.headers['Content-Type'] = 'application/json';
33
- opts.body = JSON.stringify(body);
34
- }
35
- const res = await fetch(url, opts);
36
- if (!res.ok) {
37
- const err = await res.json().catch(() => ({error: res.statusText}));
38
- throw new Error(err.error || res.statusText);
39
- }
40
- return res.json();
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // Icons
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Register the arrow-left-right icon — two outward-pointing arrowheads joined
49
- * by a horizontal line. Not in the Domma built-in set so registered here.
50
- */
51
- function registerIcons() {
52
- try {
53
- I.register('arrow-left-right', {
54
- viewBox: '0 0 24 24',
55
- path: 'M7 16 L3 12 L7 8 M3 12 L21 12 M17 8 L21 12 L17 16',
56
- stroke: 'currentColor',
57
- fill: 'none',
58
- strokeWidth: 2,
59
- strokeLinecap: 'round',
60
- strokeLinejoin: 'round'
61
- });
62
- } catch {
63
- // Already registered — safe to ignore on re-mount
64
- }
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Helpers
69
- // ---------------------------------------------------------------------------
70
-
71
- /**
72
- * Format an ISO backup ID (filesystem-safe) into a readable local datetime.
73
- *
74
- * @param {string} id - e.g. "2026-03-28T14-30-00.000Z"
75
- * @returns {string}
76
- */
77
- function formatBackupId(id) {
78
- const iso = id.replace(/T(\d{2})-(\d{2})-(\d{2})/, 'T$1:$2:$3');
79
- return new Date(iso).toLocaleString();
80
- }
81
-
82
- /**
83
- * Slugify a title string for the clone slug field.
84
- *
85
- * @param {string} title
86
- * @returns {string}
87
- */
88
- function slugify(title) {
89
- return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
90
- }
91
-
92
- /**
93
- * Create a status badge element.
94
- *
95
- * @param {'ok'|'error'|'skipped'} status
96
- * @returns {HTMLElement}
97
- */
98
- function createStatusBadge(status) {
99
- const map = {ok: 'badge-success', error: 'badge-danger', skipped: 'badge-secondary'};
100
- const span = document.createElement('span');
101
- span.className = `badge ${map[status] || 'badge-secondary'}`;
102
- span.textContent = status;
103
- return span;
104
- }
105
-
106
- /**
107
- * Create a small icon button element.
108
- *
109
- * @param {string} label
110
- * @param {string} iconName
111
- * @param {string} className
112
- * @returns {HTMLButtonElement}
113
- */
114
- function createIconBtn(label, iconName, className) {
115
- const btn = document.createElement('button');
116
- btn.className = className;
117
- const icon = document.createElement('span');
118
- icon.dataset.icon = iconName;
119
- icon.style.cssText = 'width:12px;height:12px;';
120
- btn.appendChild(icon);
121
- if (label) {
122
- btn.appendChild(document.createTextNode(` ${label}`));
123
- }
124
- return btn;
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // Transfer tab
129
- // ---------------------------------------------------------------------------
130
-
131
- /**
132
- * Load collections list and populate the transfer table.
133
- * Disables File→Mongo direction if MongoDB is not connected.
134
- *
135
- * @param {object} $container - Domma wrapper
136
- */
137
- async function loadCollections($container) {
138
- let res;
139
- try {
140
- res = await api(`${BASE}/collections`);
141
- } catch (err) {
142
- const loading = $container.find('#dt-collections-loading').get(0);
143
- loading.textContent = `Could not load collections: ${err.message}`;
144
- loading.style.color = 'var(--dm-danger)';
145
- return;
146
- }
147
-
148
- $container.find('#dt-collections-loading').hide();
149
- $container.find('#dt-collections-wrap').show();
150
-
151
- if (!res.mongoConnected) {
152
- $container.find('#dt-mongo-warning').show();
153
- $container.find('#dt-dir-ftm').get(0).disabled = true;
154
- $container.find('#dt-dir-mtf').get(0).checked = true;
155
- }
156
-
157
- const tbody = $container.find('#dt-collections-body').get(0);
158
- tbody.innerHTML = '';
159
-
160
- for (const col of res.collections) {
161
- const tr = document.createElement('tr');
162
-
163
- const checkTd = document.createElement('td');
164
- const check = document.createElement('input');
165
- check.type = 'checkbox';
166
- check.className = 'dt-col-check form-check-input';
167
- check.dataset.slug = col.slug;
168
- check.dataset.adapter = col.adapter;
169
- checkTd.appendChild(check);
170
-
171
- const titleTd = document.createElement('td');
172
- titleTd.textContent = col.title;
173
-
174
- const slugTd = document.createElement('td');
175
- const code = document.createElement('code');
176
- code.textContent = col.slug;
177
- slugTd.appendChild(code);
178
-
179
- const adapterTd = document.createElement('td');
180
- const badge = document.createElement('span');
181
- badge.className = 'badge badge-secondary';
182
- badge.textContent = col.adapter;
183
- adapterTd.appendChild(badge);
184
-
185
- const countTd = document.createElement('td');
186
- countTd.textContent = col.entryCount;
187
-
188
- tr.appendChild(checkTd);
189
- tr.appendChild(titleTd);
190
- tr.appendChild(slugTd);
191
- tr.appendChild(adapterTd);
192
- tr.appendChild(countTd);
193
- tbody.appendChild(tr);
194
- }
195
-
196
- // Populate clone source dropdown with the same data
197
- const cloneSelect = $container.find('#dt-clone-source').get(0);
198
- while (cloneSelect.options.length > 1) cloneSelect.remove(1);
199
- for (const col of res.collections) {
200
- const opt = document.createElement('option');
201
- opt.value = col.slug;
202
- opt.textContent = `${col.title} (${col.slug})`;
203
- cloneSelect.appendChild(opt);
204
- }
205
-
206
- I.scan();
207
- }
208
-
209
- /**
210
- * Wire the select-all checkbox for the transfer table.
211
- *
212
- * @param {object} $container
213
- */
214
- function wireSelectAll($container) {
215
- $container.find('#dt-select-all').get(0).addEventListener('change', function () {
216
- const checked = this.checked;
217
- $container.find('.dt-col-check').each(function () {
218
- this.checked = checked;
219
- });
220
- });
221
- }
222
-
223
- /**
224
- * Execute bulk transfer for checked collections.
225
- *
226
- * @param {object} $container
227
- */
228
- async function executeTransfer($container) {
229
- const checked = [];
230
- $container.find('.dt-col-check').each(function () {
231
- if (this.checked) checked.push(this.dataset.slug);
232
- });
233
-
234
- if (checked.length === 0) {
235
- E.toast('Select at least one collection.', {type: 'warning'});
236
- return;
237
- }
238
-
239
- const direction = $container.find('input[name="dt-direction"]:checked').val();
240
- const btn = $container.find('#dt-transfer-btn').get(0);
241
- btn.disabled = true;
242
- btn.textContent = 'Transferring…';
243
-
244
- try {
245
- const res = await api(`${BASE}/transfer`, 'POST', {slugs: checked, direction});
246
- renderTransferResults($container, res.results);
247
- E.toast('Transfer complete.', {type: 'success'});
248
- await loadCollections($container);
249
- } catch (err) {
250
- E.toast(`Transfer failed: ${err.message || 'Unknown error'}`, {type: 'error'});
251
- } finally {
252
- btn.disabled = false;
253
- const icon = document.createElement('span');
254
- icon.dataset.icon = 'arrow-left-right';
255
- icon.style.cssText = 'width:14px;height:14px;';
256
- btn.textContent = ' Transfer Selected';
257
- btn.prepend(icon);
258
- I.scan();
259
- }
260
- }
261
-
262
- /**
263
- * Render per-collection transfer results below the table.
264
- *
265
- * @param {object} $container
266
- * @param {Array<object>} results
267
- */
268
- function renderTransferResults($container, results) {
269
- const wrap = $container.find('#dt-transfer-results').get(0);
270
- const body = $container.find('#dt-transfer-results-body').get(0);
271
-
272
- body.textContent = '';
273
-
274
- for (const r of results) {
275
- const row = document.createElement('div');
276
- row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.35rem 0;border-bottom:1px solid var(--dm-border);font-size:0.875rem;';
277
-
278
- row.appendChild(createStatusBadge(r.status));
279
-
280
- const slugCode = document.createElement('code');
281
- slugCode.textContent = r.slug;
282
- row.appendChild(slugCode);
283
-
284
- const detail = document.createElement('span');
285
- detail.style.color = 'var(--dm-text-muted)';
286
- detail.textContent = r.count !== undefined
287
- ? `${r.count} entries`
288
- : (r.reason || r.error || '');
289
- row.appendChild(detail);
290
-
291
- body.appendChild(row);
292
- }
293
-
294
- wrap.style.display = 'block';
295
- }
296
-
297
- // ---------------------------------------------------------------------------
298
- // Backups tab
299
- // ---------------------------------------------------------------------------
300
-
301
- /**
302
- * Load and render the backups list.
303
- *
304
- * @param {object} $container
305
- */
306
- async function loadBackups($container) {
307
- $container.find('#dt-backups-loading').show();
308
- $container.find('#dt-backups-wrap').hide();
309
-
310
- let res;
311
- try {
312
- res = await api(`${BASE}/backups`);
313
- } catch (err) {
314
- const loading = $container.find('#dt-backups-loading').get(0);
315
- loading.textContent = `Could not load backups: ${err.message}`;
316
- loading.style.color = 'var(--dm-danger)';
317
- return;
318
- }
319
-
320
- $container.find('#dt-backups-loading').hide();
321
- $container.find('#dt-backups-wrap').show();
322
-
323
- const tbody = $container.find('#dt-backups-body').get(0);
324
- const empty = $container.find('#dt-backups-empty').get(0);
325
- tbody.textContent = '';
326
-
327
- if (res.backups.length === 0) {
328
- empty.style.display = '';
329
- return;
330
- }
331
- empty.style.display = 'none';
332
-
333
- for (const backup of res.backups) {
334
- const tr = document.createElement('tr');
335
- tr.dataset.backupId = backup.id;
336
-
337
- const dateCell = document.createElement('td');
338
- dateCell.textContent = formatBackupId(backup.id);
339
-
340
- const colCell = document.createElement('td');
341
- colCell.textContent = backup.collections.length;
342
-
343
- const entryCell = document.createElement('td');
344
- entryCell.textContent = backup.totalEntries;
345
-
346
- const actionsCell = document.createElement('td');
347
-
348
- const restoreBtn = createIconBtn('Restore', 'upload', 'btn btn-secondary btn-xs mr-1');
349
- restoreBtn.addEventListener('click', () => promptRestore($container, backup));
350
-
351
- const deleteBtn = createIconBtn('', 'trash', 'btn btn-danger btn-xs');
352
- deleteBtn.addEventListener('click', () => deleteBackup($container, backup.id));
353
-
354
- actionsCell.appendChild(restoreBtn);
355
- actionsCell.appendChild(deleteBtn);
356
-
357
- tr.appendChild(dateCell);
358
- tr.appendChild(colCell);
359
- tr.appendChild(entryCell);
360
- tr.appendChild(actionsCell);
361
- tbody.appendChild(tr);
362
- }
363
-
364
- I.scan();
365
- }
366
-
367
- /**
368
- * Prompt the user for restore mode then execute.
369
- *
370
- * TODO: Implement a richer restore mode selection UI here.
371
- *
372
- * The restore API accepts { mode: 'replace' | 'merge', slugs?: string[] }.
373
- * - 'replace' clears existing entries before inserting — a clean, full restore.
374
- * - 'merge' appends backup entries alongside existing data — for partial restores.
375
- *
376
- * Your task: replace the E.confirm() below with a proper selection UI so the
377
- * admin can consciously choose between the two modes. Ideas:
378
- * a) A modal with two distinct buttons ("Replace" / "Merge") instead of a
379
- * single confirm — clearer intent, harder to misclick.
380
- * b) A radio group inside an E.modal() followed by a single confirm button,
381
- * which also lets you add a collection filter checklist.
382
- * c) Two separate "Restore (Replace)" / "Restore (Merge)" action buttons
383
- * per row in the backups table.
384
- *
385
- * Constraints:
386
- * - Use E.confirm() or E.modal() — NOT window.confirm()
387
- * - Call executeRestore($container, backup.id, mode) once the user confirms
388
- * - mode must be the string 'replace' or 'merge'
389
- *
390
- * @param {object} $container
391
- * @param {object} backup - the backup manifest object
392
- */
393
- async function promptRestore($container, backup) {
394
- // TODO: Replace with a mode-selection UI (see jsdoc above).
395
- const confirmed = await E.confirm(
396
- `Restore backup from ${formatBackupId(backup.id)}?\n\nThis uses REPLACE mode — existing entries in each restored collection will be cleared first.`
397
- );
398
- if (!confirmed) return;
399
- await executeRestore($container, backup.id, 'replace');
400
- }
401
-
402
- /**
403
- * Execute a restore operation.
404
- *
405
- * @param {object} $container
406
- * @param {string} backupId
407
- * @param {'replace'|'merge'} mode
408
- * @param {string[]} [slugs] - optional subset of collections to restore
409
- */
410
- async function executeRestore($container, backupId, mode, slugs) {
411
- try {
412
- const body = {mode};
413
- if (slugs) body.slugs = slugs;
414
- const res = await api(`${BASE}/backups/${backupId}/restore`, 'POST', body);
415
- const ok = res.results.filter(r => r.status === 'ok').length;
416
- const errs = res.results.filter(r => r.status === 'error').length;
417
- E.toast(
418
- errs > 0
419
- ? `Restored ${ok} collection(s), ${errs} error(s).`
420
- : `Restored ${ok} collection(s) successfully.`,
421
- {type: errs > 0 ? 'warning' : 'success'}
422
- );
423
- } catch (err) {
424
- E.toast(`Restore failed: ${err.message || 'Unknown error'}`, {type: 'error'});
425
- }
426
- }
427
-
428
- /**
429
- * Delete a backup after confirmation.
430
- *
431
- * @param {object} $container
432
- * @param {string} backupId
433
- */
434
- async function deleteBackup($container, backupId) {
435
- const confirmed = await E.confirm(
436
- `Delete backup from ${formatBackupId(backupId)}? This cannot be undone.`
437
- );
438
- if (!confirmed) return;
439
-
440
- try {
441
- await api(`${BASE}/backups/${backupId}`, 'DELETE');
442
- E.toast('Backup deleted.', {type: 'success'});
443
- await loadBackups($container);
444
- } catch (err) {
445
- E.toast(`Delete failed: ${err.message || 'Unknown error'}`, {type: 'error'});
446
- }
447
- }
448
-
449
- // ---------------------------------------------------------------------------
450
- // Clone tab
451
- // ---------------------------------------------------------------------------
452
-
453
- /**
454
- * Wire the title → slug auto-generation and clone button.
455
- *
456
- * @param {object} $container
457
- */
458
- function wireClone($container) {
459
- const titleInput = $container.find('#dt-clone-title').get(0);
460
- const slugInput = $container.find('#dt-clone-slug').get(0);
461
- let slugEdited = false;
462
-
463
- titleInput.addEventListener('input', () => {
464
- if (!slugEdited) slugInput.value = slugify(titleInput.value);
465
- });
466
- slugInput.addEventListener('input', () => {
467
- slugEdited = !!slugInput.value;
468
- });
469
-
470
- $container.find('#dt-clone-btn').on('click', async () => {
471
- const sourceSlug = $container.find('#dt-clone-source').val();
472
- const newTitle = titleInput.value.trim();
473
- const newSlug = slugInput.value.trim() || undefined;
474
- const copyEntries = $container.find('#dt-clone-entries').get(0).checked;
475
-
476
- if (!sourceSlug || !newTitle) {
477
- E.toast('Choose a source collection and enter a title.', {type: 'warning'});
478
- return;
479
- }
480
-
481
- const btn = $container.find('#dt-clone-btn').get(0);
482
- btn.disabled = true;
483
- btn.textContent = 'Cloning…';
484
-
485
- try {
486
- const res = await api(`${BASE}/clone`, 'POST', {sourceSlug, newTitle, newSlug, copyEntries});
487
- E.toast(
488
- copyEntries
489
- ? `Cloned to "${res.slug}" with ${res.copiedEntries} entries.`
490
- : `Cloned to "${res.slug}".`,
491
- {type: 'success'}
492
- );
493
- $container.find('#dt-clone-source').val('');
494
- titleInput.value = '';
495
- slugInput.value = '';
496
- slugEdited = false;
497
- $container.find('#dt-clone-entries').get(0).checked = false;
498
- } catch (err) {
499
- E.toast(`Clone failed: ${err.message || 'Unknown error'}`, {type: 'error'});
500
- } finally {
501
- btn.disabled = false;
502
- const icon = document.createElement('span');
503
- icon.dataset.icon = 'copy';
504
- icon.style.cssText = 'width:14px;height:14px;';
505
- btn.textContent = ' Clone Collection';
506
- btn.prepend(icon);
507
- I.scan();
508
- }
509
- });
510
- }
511
-
512
- // ---------------------------------------------------------------------------
513
- // Mount
514
- // ---------------------------------------------------------------------------
515
-
516
- export const dataTransferView = {
517
- templateUrl: '/plugins/data-transfer/admin/templates/data-transfer.html',
518
-
519
- async onMount($container) {
520
- registerIcons();
521
- E.tabs($container.find('#dt-tabs').get(0));
522
-
523
- // Transfer tab
524
- wireSelectAll($container);
525
- await loadCollections($container);
526
- $container.find('#dt-transfer-btn').on('click', () => executeTransfer($container));
527
-
528
- // Backups tab — pre-fetch and reload on tab switch
529
- $container.find('[data-tab="backups"]').on('click', () => loadBackups($container));
530
- loadBackups($container);
531
-
532
- $container.find('#dt-create-backup-btn').on('click', async () => {
533
- const btn = $container.find('#dt-create-backup-btn').get(0);
534
- btn.disabled = true;
535
- btn.textContent = 'Creating…';
536
- try {
537
- await api(`${BASE}/backups`, 'POST', {});
538
- E.toast('Backup created.', {type: 'success'});
539
- await loadBackups($container);
540
- } catch (err) {
541
- E.toast(`Backup failed: ${err.message || 'Unknown error'}`, {type: 'error'});
542
- } finally {
543
- btn.disabled = false;
544
- const icon = document.createElement('span');
545
- icon.dataset.icon = 'download';
546
- icon.style.cssText = 'width:14px;height:14px;';
547
- btn.textContent = ' Create Backup';
548
- btn.prepend(icon);
549
- I.scan();
550
- }
551
- });
552
-
553
- // Clone tab
554
- wireClone($container);
555
-
556
- I.scan();
557
- }
558
- };
@@ -1,9 +0,0 @@
1
- /**
2
- * Data Transfer Plugin — Default Configuration
3
- *
4
- * Override any of these via config/plugins.json under "data-transfer.settings".
5
- */
6
- export default {
7
- /** Maximum number of backups to retain. Oldest are pruned automatically. */
8
- maxBackups: 50
9
- };