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.
- package/config/plugins.json +0 -12
- package/package.json +1 -1
- package/plugins/data-transfer/admin/templates/data-transfer.html +0 -172
- package/plugins/data-transfer/admin/views/data-transfer.js +0 -558
- package/plugins/data-transfer/config.js +0 -9
- package/plugins/data-transfer/plugin.js +0 -424
- package/plugins/data-transfer/plugin.json +0 -33
- package/plugins/job-board/admin/templates/application-detail.html +0 -40
- package/plugins/job-board/admin/templates/applications.html +0 -10
- package/plugins/job-board/admin/templates/companies.html +0 -24
- package/plugins/job-board/admin/templates/dashboard.html +0 -36
- package/plugins/job-board/admin/templates/job-editor.html +0 -17
- package/plugins/job-board/admin/templates/jobs.html +0 -15
- package/plugins/job-board/admin/templates/profile.html +0 -17
- package/plugins/job-board/admin/views/application-detail.js +0 -62
- package/plugins/job-board/admin/views/applications.js +0 -47
- package/plugins/job-board/admin/views/companies.js +0 -104
- package/plugins/job-board/admin/views/dashboard.js +0 -88
- package/plugins/job-board/admin/views/job-editor.js +0 -86
- package/plugins/job-board/admin/views/jobs.js +0 -53
- package/plugins/job-board/admin/views/profile.js +0 -47
- package/plugins/job-board/config.js +0 -6
- package/plugins/job-board/plugin.js +0 -466
- package/plugins/job-board/plugin.json +0 -40
- package/plugins/job-board/schemas/jb-agent-companies.json +0 -17
- package/plugins/job-board/schemas/jb-applications.json +0 -20
- package/plugins/job-board/schemas/jb-candidate-profiles.json +0 -20
- package/plugins/job-board/schemas/jb-companies.json +0 -21
- package/plugins/job-board/schemas/jb-jobs.json +0 -23
- package/plugins/theme-roller/admin/templates/theme-roller.html +0 -71
- package/plugins/theme-roller/admin/views/theme-roller-view.js +0 -403
- package/plugins/theme-roller/config.js +0 -1
- package/plugins/theme-roller/plugin.js +0 -233
- package/plugins/theme-roller/plugin.json +0 -31
- package/plugins/theme-roller/public/active-theme.css +0 -0
- 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
|
-
};
|