@a83/orbiter-admin 0.3.18 → 0.3.20
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/package.json +1 -1
- package/public/editor.html +51 -11
- package/public/entries.html +59 -20
- package/public/router.js +10 -1
- package/public/settings.html +32 -10
- package/public/style.css +12 -6
- package/public/xfce.js +1 -1
- package/src/routes/entries.js +57 -41
package/package.json
CHANGED
package/public/editor.html
CHANGED
|
@@ -381,6 +381,14 @@
|
|
|
381
381
|
.serp-url { font-size:9px; color:var(--jade); margin-bottom:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
382
382
|
.serp-title { font-size:13px; color:#8ab4f8; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
383
383
|
.serp-desc { font-size:10px; color:var(--mid); line-height:1.5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
|
384
|
+
/* Locale switcher bar */
|
|
385
|
+
.locale-bar { display:flex; align-items:center; gap:6px; padding:6px 20px; border-bottom:1px solid var(--line); background:var(--bg1); flex-shrink:0; }
|
|
386
|
+
.locale-bar-label { font-size:9px; color:var(--muted); text-transform:uppercase; letter-spacing:0.12em; margin-right:2px; }
|
|
387
|
+
.locale-tab { font-size:10px; font-family:var(--mono); letter-spacing:0.08em; text-transform:uppercase; padding:3px 10px; border:1px solid var(--line); border-radius:12px; cursor:pointer; background:none; color:var(--muted); text-decoration:none; transition:all .12s; }
|
|
388
|
+
.locale-tab:hover { border-color:var(--accent); color:var(--accent); }
|
|
389
|
+
.locale-tab.active { background:var(--accent-bg); border-color:var(--accent); color:var(--accent); font-weight:500; }
|
|
390
|
+
.locale-tab.missing { border-style:dashed; opacity:0.55; }
|
|
391
|
+
.locale-tab.missing:hover { opacity:1; border-style:solid; }
|
|
384
392
|
</style>
|
|
385
393
|
</head>
|
|
386
394
|
<body>
|
|
@@ -440,6 +448,10 @@
|
|
|
440
448
|
</button>
|
|
441
449
|
</div>
|
|
442
450
|
</div>
|
|
451
|
+
<div id="locale-bar" class="locale-bar" style="display:none">
|
|
452
|
+
<span class="locale-bar-label">Locale</span>
|
|
453
|
+
<div id="locale-tabs" style="display:flex;gap:5px;"></div>
|
|
454
|
+
</div>
|
|
443
455
|
<div class="editor-scroll">
|
|
444
456
|
<div class="editor-page">
|
|
445
457
|
<textarea id="title-input" class="editor-title-input" placeholder="Untitled" rows="1"
|
|
@@ -488,10 +500,11 @@
|
|
|
488
500
|
location.replace('/login.html');
|
|
489
501
|
});
|
|
490
502
|
|
|
491
|
-
// Parse ?collection=X&slug=Y&singleton=1 from URL
|
|
503
|
+
// Parse ?collection=X&slug=Y&singleton=1&locale=de from URL
|
|
492
504
|
const params = new URLSearchParams(location.search);
|
|
493
505
|
const COLLECTION = params.get('collection');
|
|
494
506
|
const IS_SINGLETON = params.get('singleton') === '1';
|
|
507
|
+
const LOCALE = params.get('locale') ?? '';
|
|
495
508
|
|
|
496
509
|
if (!COLLECTION) { location.replace('/collections.html'); }
|
|
497
510
|
|
|
@@ -505,21 +518,46 @@
|
|
|
505
518
|
}
|
|
506
519
|
}
|
|
507
520
|
const IS_NEW = SLUG === 'new';
|
|
521
|
+
const localeQ = LOCALE ? `?locale=${encodeURIComponent(LOCALE)}` : '';
|
|
508
522
|
|
|
509
523
|
document.getElementById('collection-id-display').textContent = COLLECTION;
|
|
510
524
|
|
|
511
|
-
// Load collection schema + entry
|
|
512
|
-
const [colData, entryData, versionsData, activityData, mediaData, commentsData] = await Promise.all([
|
|
525
|
+
// Load collection schema + entry + locale meta
|
|
526
|
+
const [colData, entryData, versionsData, activityData, mediaData, commentsData, siteLocalesMeta, entryLocalesData] = await Promise.all([
|
|
513
527
|
fetch(`/api/collections/${COLLECTION}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
|
|
514
|
-
IS_NEW ? null : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
|
|
528
|
+
IS_NEW ? null : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}${localeQ}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
|
|
515
529
|
IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/versions`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
516
530
|
IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/activity`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
517
531
|
fetch('/api/media',{credentials:'include'}).then(r=>r.json()).catch(()=>[]),
|
|
518
532
|
IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/comments`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
533
|
+
fetch('/api/meta/site~locales',{credentials:'include'}).then(r=>r.ok?r.json():null).catch(()=>null),
|
|
534
|
+
IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/locales`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
519
535
|
]);
|
|
520
536
|
|
|
537
|
+
// IS_TRANSLATION_NEW: slug exists but no entry for this locale yet
|
|
538
|
+
const IS_TRANSLATION_NEW = !IS_NEW && !entryData && LOCALE !== '';
|
|
539
|
+
|
|
521
540
|
if (!colData) { location.replace('/collections.html'); }
|
|
522
541
|
|
|
542
|
+
// ── Locale switcher ───────────────────────────────────────────────
|
|
543
|
+
const siteLocales = (siteLocalesMeta?.value ?? '').split(',').map(s => s.trim()).filter(Boolean);
|
|
544
|
+
if (siteLocales.length > 1 && !IS_NEW) {
|
|
545
|
+
const existingLocales = new Set(entryLocalesData.map(l => l.locale));
|
|
546
|
+
const bar = document.getElementById('locale-bar');
|
|
547
|
+
const tabsEl = document.getElementById('locale-tabs');
|
|
548
|
+
tabsEl.innerHTML = siteLocales.map((loc, i) => {
|
|
549
|
+
// First locale in the list maps to the default (locale='')
|
|
550
|
+
const dbLocale = i === 0 ? '' : loc;
|
|
551
|
+
const isActive = dbLocale === LOCALE;
|
|
552
|
+
const exists = existingLocales.has(dbLocale);
|
|
553
|
+
const localeParam = dbLocale ? `&locale=${encodeURIComponent(dbLocale)}` : '';
|
|
554
|
+
const href = `/editor.html?collection=${encodeURIComponent(COLLECTION)}&slug=${encodeURIComponent(SLUG)}${localeParam}`;
|
|
555
|
+
const title = exists ? loc.toUpperCase() : `${loc.toUpperCase()} — click to create`;
|
|
556
|
+
return `<a href="${href}" class="locale-tab${isActive?' active':''}${!exists?' missing':''}" title="${title}">${loc.toUpperCase()}</a>`;
|
|
557
|
+
}).join('');
|
|
558
|
+
bar.style.display = 'flex';
|
|
559
|
+
}
|
|
560
|
+
|
|
523
561
|
// Entry locking — claim lock, warn if someone else is editing
|
|
524
562
|
let lockHeld = false;
|
|
525
563
|
let lockHeartbeat = null;
|
|
@@ -552,7 +590,8 @@
|
|
|
552
590
|
// Breadcrumb
|
|
553
591
|
document.getElementById('back-to-col').textContent = colData.label;
|
|
554
592
|
document.getElementById('back-to-col').href = `/entries.html?col=${COLLECTION}&label=${encodeURIComponent(colData.label)}`;
|
|
555
|
-
|
|
593
|
+
const breadcrumbSlug = IS_NEW ? 'New entry' : IS_TRANSLATION_NEW ? `${SLUG} [${LOCALE}] — new translation` : LOCALE ? `${SLUG} [${LOCALE}]` : SLUG;
|
|
594
|
+
document.getElementById('breadcrumb-slug').textContent = breadcrumbSlug;
|
|
556
595
|
document.title = `${IS_NEW ? 'New entry' : SLUG} — ${colData.label} — Orbiter`;
|
|
557
596
|
|
|
558
597
|
const schema = colData.schema ? (typeof colData.schema==='string'?JSON.parse(colData.schema):colData.schema) : {};
|
|
@@ -1103,19 +1142,20 @@
|
|
|
1103
1142
|
}
|
|
1104
1143
|
}
|
|
1105
1144
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1145
|
+
const localeParam = LOCALE ? `&locale=${encodeURIComponent(LOCALE)}` : '';
|
|
1146
|
+
if ((IS_NEW || IS_TRANSLATION_NEW) && !currentSlug) {
|
|
1147
|
+
// Create new entry (or new translation of existing slug)
|
|
1108
1148
|
const res = await fetch(`/api/collections/${COLLECTION}/entries`,{
|
|
1109
1149
|
method:'POST', credentials:'include',
|
|
1110
1150
|
headers:{'Content-Type':'application/json'},
|
|
1111
|
-
body: JSON.stringify({ slug, data, status, publish_at }),
|
|
1151
|
+
body: JSON.stringify({ slug, data, status, publish_at, locale: LOCALE }),
|
|
1112
1152
|
});
|
|
1113
1153
|
const json = await res.json();
|
|
1114
1154
|
if (json.slug || json.id) {
|
|
1115
1155
|
currentSlug = json.slug ?? slug;
|
|
1116
1156
|
document.getElementById('slug-preview').textContent = currentSlug;
|
|
1117
1157
|
slugInput.value = currentSlug;
|
|
1118
|
-
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
|
|
1158
|
+
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}${localeParam}`;
|
|
1119
1159
|
history.replaceState(null,'',newUrl);
|
|
1120
1160
|
currentPath = newUrl;
|
|
1121
1161
|
setIndicator('saved');
|
|
@@ -1127,13 +1167,13 @@
|
|
|
1127
1167
|
const res = await fetch(`/api/collections/${COLLECTION}/entries/${targetSlug}`,{
|
|
1128
1168
|
method:'PUT', credentials:'include',
|
|
1129
1169
|
headers:{'Content-Type':'application/json'},
|
|
1130
|
-
body: JSON.stringify({ slug, data, status, publish_at, unpublish_at }),
|
|
1170
|
+
body: JSON.stringify({ slug, data, status, publish_at, unpublish_at, locale: LOCALE }),
|
|
1131
1171
|
});
|
|
1132
1172
|
if (res.ok) {
|
|
1133
1173
|
const json = await res.json();
|
|
1134
1174
|
if (json.slug && json.slug!==currentSlug) {
|
|
1135
1175
|
currentSlug = json.slug;
|
|
1136
|
-
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
|
|
1176
|
+
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}${localeParam}`;
|
|
1137
1177
|
history.replaceState(null,'',newUrl);
|
|
1138
1178
|
document.getElementById('slug-preview').textContent = currentSlug;
|
|
1139
1179
|
slugInput.value = currentSlug;
|
package/public/entries.html
CHANGED
|
@@ -143,9 +143,38 @@
|
|
|
143
143
|
document.getElementById('col-label').textContent = colLabel;
|
|
144
144
|
document.getElementById('page-title').textContent = colLabel;
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// Load site locales to build locale filter tabs
|
|
147
|
+
const siteLocalesMeta = await fetch('/api/meta/site~locales',{credentials:'include'}).then(r=>r.ok?r.json():null).catch(()=>null);
|
|
148
|
+
const siteLocales = (siteLocalesMeta?.value ?? '').split(',').map(s => s.trim()).filter(Boolean);
|
|
149
|
+
|
|
150
|
+
let allEntries = [];
|
|
147
151
|
let activeFilter = '';
|
|
148
|
-
let
|
|
152
|
+
let activeLocale = ''; // '' = default/primary locale
|
|
153
|
+
let selected = new Set();
|
|
154
|
+
|
|
155
|
+
// Render locale tabs if multiple locales configured
|
|
156
|
+
if (siteLocales.length > 1) {
|
|
157
|
+
const filterBar = document.querySelector('.entries-filter-bar');
|
|
158
|
+
const sep = document.createElement('div');
|
|
159
|
+
sep.style.cssText = 'width:1px;background:var(--line);margin:0 4px;';
|
|
160
|
+
filterBar.insertBefore(sep, filterBar.querySelector('[data-status="trash"]'));
|
|
161
|
+
siteLocales.forEach((loc, i) => {
|
|
162
|
+
const dbLocale = i === 0 ? '' : loc;
|
|
163
|
+
const btn = document.createElement('button');
|
|
164
|
+
btn.className = 'filter-tab' + (dbLocale === '' ? ' active' : '');
|
|
165
|
+
btn.dataset.locale = dbLocale;
|
|
166
|
+
btn.textContent = loc.toUpperCase();
|
|
167
|
+
btn.title = i === 0 ? `${loc.toUpperCase()} (default)` : loc.toUpperCase();
|
|
168
|
+
btn.style.cssText = 'margin-left:' + (i === 0 ? '4px' : '0');
|
|
169
|
+
btn.addEventListener('click', () => {
|
|
170
|
+
document.querySelectorAll('.filter-tab[data-locale]').forEach(b => b.classList.remove('active'));
|
|
171
|
+
btn.classList.add('active');
|
|
172
|
+
activeLocale = dbLocale;
|
|
173
|
+
loadEntries();
|
|
174
|
+
});
|
|
175
|
+
filterBar.insertBefore(btn, filterBar.querySelector('[data-status="trash"]'));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
149
178
|
|
|
150
179
|
function updateBulkBar() {
|
|
151
180
|
const bar = document.getElementById('bulk-bar');
|
|
@@ -163,7 +192,10 @@
|
|
|
163
192
|
async function loadEntries() {
|
|
164
193
|
selected.clear();
|
|
165
194
|
updateBulkBar();
|
|
166
|
-
const
|
|
195
|
+
const qp = new URLSearchParams();
|
|
196
|
+
if (activeFilter) qp.set('status', activeFilter);
|
|
197
|
+
if (activeLocale !== undefined) qp.set('locale', activeLocale);
|
|
198
|
+
const url = `/api/collections/${colId}/entries` + (qp.toString() ? '?' + qp.toString() : '');
|
|
167
199
|
allEntries = await fetch(url, { credentials: 'include' }).then(r => r.json());
|
|
168
200
|
renderEntries(allEntries);
|
|
169
201
|
}
|
|
@@ -195,11 +227,13 @@
|
|
|
195
227
|
const nextStatus = e.status === 'published' ? 'draft' : 'published';
|
|
196
228
|
const toggleLabel = e.status === 'published' ? 'Unpublish' : e.status === 'scheduled' ? 'Publish now' : 'Publish';
|
|
197
229
|
const schedInfo = e.status === 'scheduled' && e.publish_at ? ` · ${e.publish_at.split(' ')[0]}` : '';
|
|
198
|
-
|
|
230
|
+
const localeParam = e.locale ? `&locale=${encodeURIComponent(e.locale)}` : '';
|
|
231
|
+
const localeBadge = e.locale ? `<span style="font-family:var(--mono);font-size:9px;color:var(--muted);background:var(--bg3);border:1px solid var(--line);border-radius:4px;padding:1px 5px;margin-left:5px;vertical-align:middle">${e.locale}</span>` : '';
|
|
232
|
+
return `<tr data-slug="${e.slug}" data-locale="${e.locale ?? ''}"${canSort ? ' draggable="true"' : ''}>
|
|
199
233
|
${canSort ? `<td class="drag-col"><span class="drag-handle" title="Drag to reorder">⠿</span></td>` : ''}
|
|
200
|
-
<td class="cb-col"><input type="checkbox" class="row-cb" data-slug="${e.slug}" ${selected.has(e.slug) ? 'checked' : ''} /></td>
|
|
234
|
+
<td class="cb-col"><input type="checkbox" class="row-cb" data-slug="${e.slug}" data-locale="${e.locale ?? ''}" ${selected.has(e.slug) ? 'checked' : ''} /></td>
|
|
201
235
|
<td>
|
|
202
|
-
<div style="color:var(--heading);font-weight:500">${title}</div>
|
|
236
|
+
<div style="color:var(--heading);font-weight:500">${title}${localeBadge}</div>
|
|
203
237
|
<div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:2px">${e.slug}</div>
|
|
204
238
|
</td>
|
|
205
239
|
<td>${isTrash ? `<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${deleted}</span>` : `<span class="badge badge-${e.status}">${e.status}${schedInfo}</span>`}</td>
|
|
@@ -207,12 +241,12 @@
|
|
|
207
241
|
<td style="width:1%;white-space:nowrap">
|
|
208
242
|
<div class="row-actions">
|
|
209
243
|
${isTrash
|
|
210
|
-
? `<button class="btn-row restore-btn" data-slug="${e.slug}">Restore</button>
|
|
211
|
-
<button class="btn-row btn-row-danger perm-del-btn" data-slug="${e.slug}">Delete forever</button>`
|
|
212
|
-
: `<a class="btn-row" href="/editor.html?collection=${colId}&slug=${e.slug}">Edit</a>
|
|
213
|
-
<button class="btn-row btn-row-toggle status-toggle" data-slug="${e.slug}" data-next="${nextStatus}">${toggleLabel}</button>
|
|
214
|
-
<button class="btn-row btn-row-icon dup-btn" data-slug="${e.slug}" title="Duplicate">⧉</button>
|
|
215
|
-
<button class="btn-row btn-row-danger delete-btn" data-slug="${e.slug}">Trash</button>`
|
|
244
|
+
? `<button class="btn-row restore-btn" data-slug="${e.slug}" data-locale="${e.locale ?? ''}">Restore</button>
|
|
245
|
+
<button class="btn-row btn-row-danger perm-del-btn" data-slug="${e.slug}" data-locale="${e.locale ?? ''}">Delete forever</button>`
|
|
246
|
+
: `<a class="btn-row" href="/editor.html?collection=${colId}&slug=${e.slug}${localeParam}">Edit</a>
|
|
247
|
+
<button class="btn-row btn-row-toggle status-toggle" data-slug="${e.slug}" data-locale="${e.locale ?? ''}" data-next="${nextStatus}">${toggleLabel}</button>
|
|
248
|
+
<button class="btn-row btn-row-icon dup-btn" data-slug="${e.slug}" data-locale="${e.locale ?? ''}" title="Duplicate">⧉</button>
|
|
249
|
+
<button class="btn-row btn-row-danger delete-btn" data-slug="${e.slug}" data-locale="${e.locale ?? ''}">Trash</button>`
|
|
216
250
|
}
|
|
217
251
|
</div>
|
|
218
252
|
</td>
|
|
@@ -288,7 +322,7 @@
|
|
|
288
322
|
await fetch(`/api/collections/${colId}/entries/${btn.dataset.slug}/status`, {
|
|
289
323
|
method: 'PATCH', credentials: 'include',
|
|
290
324
|
headers: { 'Content-Type': 'application/json' },
|
|
291
|
-
body: JSON.stringify({ status: btn.dataset.next }),
|
|
325
|
+
body: JSON.stringify({ status: btn.dataset.next, locale: btn.dataset.locale ?? '' }),
|
|
292
326
|
});
|
|
293
327
|
loadEntries();
|
|
294
328
|
});
|
|
@@ -298,7 +332,8 @@
|
|
|
298
332
|
wrap.querySelectorAll('.dup-btn').forEach(btn => {
|
|
299
333
|
btn.addEventListener('click', async () => {
|
|
300
334
|
btn.disabled = true;
|
|
301
|
-
const
|
|
335
|
+
const loc = btn.dataset.locale ?? '';
|
|
336
|
+
const res = await fetch(`/api/collections/${colId}/entries/${btn.dataset.slug}/duplicate${loc ? '?locale=' + encodeURIComponent(loc) : ''}`, {
|
|
302
337
|
method: 'POST', credentials: 'include',
|
|
303
338
|
});
|
|
304
339
|
if (res.ok) loadEntries();
|
|
@@ -309,7 +344,8 @@
|
|
|
309
344
|
// Delete → Trash
|
|
310
345
|
wrap.querySelectorAll('.delete-btn').forEach(btn => {
|
|
311
346
|
btn.addEventListener('click', async () => {
|
|
312
|
-
|
|
347
|
+
const loc = btn.dataset.locale ?? '';
|
|
348
|
+
await fetch(`/api/collections/${colId}/entries/${btn.dataset.slug}${loc ? '?locale=' + encodeURIComponent(loc) : ''}`, {
|
|
313
349
|
method: 'DELETE', credentials: 'include',
|
|
314
350
|
});
|
|
315
351
|
loadEntries();
|
|
@@ -319,7 +355,8 @@
|
|
|
319
355
|
// Restore from Trash
|
|
320
356
|
wrap.querySelectorAll('.restore-btn').forEach(btn => {
|
|
321
357
|
btn.addEventListener('click', async () => {
|
|
322
|
-
|
|
358
|
+
const loc = btn.dataset.locale ?? '';
|
|
359
|
+
await fetch(`/api/collections/${colId}/entries/${btn.dataset.slug}/restore${loc ? '?locale=' + encodeURIComponent(loc) : ''}`, {
|
|
323
360
|
method: 'POST', credentials: 'include',
|
|
324
361
|
});
|
|
325
362
|
loadEntries();
|
|
@@ -330,7 +367,8 @@
|
|
|
330
367
|
wrap.querySelectorAll('.perm-del-btn').forEach(btn => {
|
|
331
368
|
btn.addEventListener('click', async () => {
|
|
332
369
|
if (!confirm(`Permanently delete "${btn.dataset.slug}"? This cannot be undone.`)) return;
|
|
333
|
-
|
|
370
|
+
const loc = btn.dataset.locale ?? '';
|
|
371
|
+
await fetch(`/api/collections/${colId}/entries/${btn.dataset.slug}/permanent${loc ? '?locale=' + encodeURIComponent(loc) : ''}`, {
|
|
334
372
|
method: 'DELETE', credentials: 'include',
|
|
335
373
|
});
|
|
336
374
|
loadEntries();
|
|
@@ -356,7 +394,7 @@
|
|
|
356
394
|
await fetch(`/api/collections/${colId}/entries/bulk`, {
|
|
357
395
|
method: 'POST', credentials: 'include',
|
|
358
396
|
headers: { 'Content-Type': 'application/json' },
|
|
359
|
-
body: JSON.stringify({ action, slugs }),
|
|
397
|
+
body: JSON.stringify({ action, slugs, locale: activeLocale }),
|
|
360
398
|
});
|
|
361
399
|
loadEntries();
|
|
362
400
|
}
|
|
@@ -424,11 +462,12 @@
|
|
|
424
462
|
const res = await fetch(`/api/collections/${colId}/entries`, {
|
|
425
463
|
method: 'POST', credentials: 'include',
|
|
426
464
|
headers: { 'Content-Type': 'application/json' },
|
|
427
|
-
body: JSON.stringify({ slug, data: { title }, status: 'draft' }),
|
|
465
|
+
body: JSON.stringify({ slug, data: { title }, status: 'draft', locale: activeLocale }),
|
|
428
466
|
});
|
|
429
467
|
if (res.ok) {
|
|
430
468
|
overlay.style.display = 'none';
|
|
431
|
-
|
|
469
|
+
const localeParam = activeLocale ? `&locale=${encodeURIComponent(activeLocale)}` : '';
|
|
470
|
+
location.href = `/editor.html?collection=${colId}&slug=${slug}${localeParam}`;
|
|
432
471
|
} else {
|
|
433
472
|
const d = await res.json();
|
|
434
473
|
errEl.textContent = d.error ?? 'Could not create entry';
|
package/public/router.js
CHANGED
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
function updateActiveNav(href) {
|
|
25
25
|
var target;
|
|
26
26
|
try { target = new URL(href, location.origin); } catch (e) { return; }
|
|
27
|
-
|
|
27
|
+
var pg = target.pathname.split('/').pop().replace('.html', '');
|
|
28
|
+
|
|
29
|
+
// Update classic sidebar + xfce dock/HUD link items
|
|
30
|
+
document.querySelectorAll('.nav-item, a.xfce-dock-item, a.xfce-hud-nav-item, a.xfce-tools-item').forEach(function (a) {
|
|
28
31
|
var raw = a.getAttribute('href');
|
|
29
32
|
if (!raw) return;
|
|
30
33
|
var itemUrl;
|
|
@@ -37,6 +40,12 @@
|
|
|
37
40
|
}
|
|
38
41
|
a.classList.toggle('active', match);
|
|
39
42
|
});
|
|
43
|
+
|
|
44
|
+
// xfce tools button is a <button> (not an <a>), update separately
|
|
45
|
+
var toolsBtn = document.getElementById('xfce-tools-btn');
|
|
46
|
+
if (toolsBtn) {
|
|
47
|
+
toolsBtn.classList.toggle('active', ['schema', 'build', 'import'].indexOf(pg) !== -1);
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function swapPageStyle(doc) {
|
package/public/settings.html
CHANGED
|
@@ -10,14 +10,29 @@
|
|
|
10
10
|
<link rel="stylesheet" href="/style.css" />
|
|
11
11
|
<script src="/theme.js"></script>
|
|
12
12
|
<style>
|
|
13
|
-
.settings-wrap { max-width:
|
|
13
|
+
.settings-wrap { max-width:1100px; padding-bottom:40px; }
|
|
14
|
+
|
|
15
|
+
/* Two-column grid for settings groups */
|
|
16
|
+
.settings-grid {
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: 1fr 1fr;
|
|
19
|
+
gap: 12px;
|
|
20
|
+
align-items: start;
|
|
21
|
+
margin-bottom: 12px;
|
|
22
|
+
}
|
|
23
|
+
.settings-grid .settings-group { margin-bottom: 0; }
|
|
24
|
+
.settings-wrap > .settings-group { margin-bottom: 12px; }
|
|
25
|
+
|
|
26
|
+
@media (max-width: 720px) {
|
|
27
|
+
.settings-wrap { max-width: 720px; }
|
|
28
|
+
.settings-grid { grid-template-columns: 1fr; }
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
/* Card — einheitliches bg2, eine Farbe, kein Alternieren */
|
|
16
32
|
.settings-group {
|
|
17
33
|
background: var(--bg2);
|
|
18
34
|
border: 1px solid var(--line);
|
|
19
35
|
border-radius: 10px;
|
|
20
|
-
margin-bottom: 12px;
|
|
21
36
|
overflow: hidden;
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -250,8 +265,9 @@
|
|
|
250
265
|
</div>
|
|
251
266
|
</nav>
|
|
252
267
|
<main class="main">
|
|
253
|
-
<div class="page-header glass-card">
|
|
268
|
+
<div class="page-header glass-card" style="display:flex;align-items:center;justify-content:space-between;">
|
|
254
269
|
<h1 class="page-title">Settings</h1>
|
|
270
|
+
<button type="submit" form="site-form" class="btn-save" id="header-save-btn">Save settings</button>
|
|
255
271
|
</div>
|
|
256
272
|
<div class="settings-wrap" id="settings-content">
|
|
257
273
|
<div class="empty"><div class="spinner"></div></div>
|
|
@@ -301,6 +317,7 @@
|
|
|
301
317
|
|
|
302
318
|
<!-- Site + Build + GitHub + API — one form -->
|
|
303
319
|
<form id="site-form">
|
|
320
|
+
<div class="settings-grid">
|
|
304
321
|
<div class="settings-group">
|
|
305
322
|
<div class="group-header">Site</div>
|
|
306
323
|
<div class="setting-row">
|
|
@@ -485,9 +502,7 @@
|
|
|
485
502
|
</div>
|
|
486
503
|
</div>
|
|
487
504
|
|
|
488
|
-
|
|
489
|
-
<button type="submit" class="btn-save">Save settings</button>
|
|
490
|
-
</div>
|
|
505
|
+
</div><!-- /settings-grid -->
|
|
491
506
|
</form>
|
|
492
507
|
|
|
493
508
|
<!-- GitHub Sync (conditional) -->
|
|
@@ -508,8 +523,9 @@
|
|
|
508
523
|
</div>
|
|
509
524
|
` : ''}
|
|
510
525
|
|
|
511
|
-
<!-- Account -->
|
|
526
|
+
<!-- Account + Interface language (two-column) -->
|
|
512
527
|
<div id="account-banner" class="banner" style="display:none"></div>
|
|
528
|
+
<div class="settings-grid">
|
|
513
529
|
<div class="settings-group">
|
|
514
530
|
<div class="group-header">Account — ${me.user.username}</div>
|
|
515
531
|
<form id="pw-form">
|
|
@@ -555,8 +571,9 @@
|
|
|
555
571
|
</div>
|
|
556
572
|
</div>
|
|
557
573
|
</div>
|
|
574
|
+
</div><!-- /settings-grid account+lang -->
|
|
558
575
|
|
|
559
|
-
<!-- Appearance -->
|
|
576
|
+
<!-- Appearance (full width) -->
|
|
560
577
|
<div class="settings-group">
|
|
561
578
|
<div class="group-header">Appearance</div>
|
|
562
579
|
<div style="padding:16px 28px;border-bottom:1px solid var(--line2)">
|
|
@@ -654,7 +671,8 @@
|
|
|
654
671
|
</div>
|
|
655
672
|
</div>
|
|
656
673
|
|
|
657
|
-
<!-- Data +
|
|
674
|
+
<!-- Data + Pod (two-column) -->
|
|
675
|
+
<div class="settings-grid">
|
|
658
676
|
<div class="settings-group">
|
|
659
677
|
<div class="group-header">Data</div>
|
|
660
678
|
<div class="setting-row">
|
|
@@ -666,7 +684,6 @@
|
|
|
666
684
|
</div>
|
|
667
685
|
</div>
|
|
668
686
|
|
|
669
|
-
<!-- Pod info -->
|
|
670
687
|
<div class="settings-group" id="pod-info-group">
|
|
671
688
|
<div class="group-header">Pod</div>
|
|
672
689
|
<div class="pod-meta">
|
|
@@ -677,6 +694,11 @@
|
|
|
677
694
|
<div class="pod-col-row"><span class="pod-col-name" style="color:var(--muted)">Loading…</span></div>
|
|
678
695
|
</div>
|
|
679
696
|
</div>
|
|
697
|
+
</div><!-- /settings-grid data+pod -->
|
|
698
|
+
|
|
699
|
+
<div class="save-row">
|
|
700
|
+
<button type="submit" form="site-form" class="btn-save">Save settings</button>
|
|
701
|
+
</div>
|
|
680
702
|
|
|
681
703
|
<!-- Support widget -->
|
|
682
704
|
<div class="support-widget">
|
package/public/style.css
CHANGED
|
@@ -1040,7 +1040,7 @@ html[data-style="xfce"] {
|
|
|
1040
1040
|
--glass-shadow: 0 8px 40px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.07);
|
|
1041
1041
|
--sb-h: 26px;
|
|
1042
1042
|
--dock-h: 76px;
|
|
1043
|
-
--dock-item-base:
|
|
1043
|
+
--dock-item-base: 50px;
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
1046
|
html[data-style="xfce"][data-scheme="light"],
|
|
@@ -1158,8 +1158,8 @@ html[data-style="xfce"] .editor-shell {
|
|
|
1158
1158
|
flex-direction: column;
|
|
1159
1159
|
align-items: center;
|
|
1160
1160
|
justify-content: flex-end;
|
|
1161
|
-
width: var(--dock-item-base,
|
|
1162
|
-
padding:
|
|
1161
|
+
width: var(--dock-item-base, 50px);
|
|
1162
|
+
padding: 8px 7px 7px;
|
|
1163
1163
|
background: transparent;
|
|
1164
1164
|
border: none;
|
|
1165
1165
|
border-radius: 12px;
|
|
@@ -1217,7 +1217,7 @@ html[data-style="xfce"] .editor-shell {
|
|
|
1217
1217
|
font-size: 18px;
|
|
1218
1218
|
line-height: 1;
|
|
1219
1219
|
display: block;
|
|
1220
|
-
opacity: .
|
|
1220
|
+
opacity: .58;
|
|
1221
1221
|
transition: filter .12s, transform .12s cubic-bezier(.34,1.5,.64,1), opacity .12s, color .12s;
|
|
1222
1222
|
}
|
|
1223
1223
|
.xfce-dock-item:hover .xfce-dock-icon {
|
|
@@ -1239,7 +1239,7 @@ html[data-style="xfce"] .editor-shell {
|
|
|
1239
1239
|
color: var(--mid);
|
|
1240
1240
|
white-space: nowrap;
|
|
1241
1241
|
transition: color .12s, opacity .12s;
|
|
1242
|
-
opacity: .
|
|
1242
|
+
opacity: .48;
|
|
1243
1243
|
}
|
|
1244
1244
|
.xfce-dock-item:hover .xfce-dock-lbl {
|
|
1245
1245
|
opacity: 1;
|
|
@@ -1637,7 +1637,13 @@ html[data-style="xfce"] .table-wrap {
|
|
|
1637
1637
|
}
|
|
1638
1638
|
|
|
1639
1639
|
/* Settings / Import content: center within padded main */
|
|
1640
|
-
html[data-style="xfce"] .settings-wrap
|
|
1640
|
+
html[data-style="xfce"] .settings-wrap {
|
|
1641
|
+
width: 100%;
|
|
1642
|
+
max-width: 1100px;
|
|
1643
|
+
margin: 0 auto;
|
|
1644
|
+
padding-left: 0;
|
|
1645
|
+
padding-right: 0;
|
|
1646
|
+
}
|
|
1641
1647
|
html[data-style="xfce"] .import-wrap {
|
|
1642
1648
|
width: 100%;
|
|
1643
1649
|
max-width: 720px;
|
package/public/xfce.js
CHANGED
|
@@ -564,7 +564,7 @@
|
|
|
564
564
|
var r = item.getBoundingClientRect();
|
|
565
565
|
var mid = r.left + r.width / 2;
|
|
566
566
|
var d = Math.abs(cx - mid);
|
|
567
|
-
var s = d <
|
|
567
|
+
var s = d < 80 ? 1 + (1 - d / 80) * 0.50 : 1;
|
|
568
568
|
item.style.setProperty('--ds', s.toFixed(3));
|
|
569
569
|
});
|
|
570
570
|
}
|
package/src/routes/entries.js
CHANGED
|
@@ -15,18 +15,18 @@ function fireWebhook(podPath) {
|
|
|
15
15
|
// POST /api/collections/:id/entries/bulk — bulk publish or delete
|
|
16
16
|
entryRoutes.post('/:collectionId/entries/bulk', async (c) => {
|
|
17
17
|
const { collectionId } = c.req.param();
|
|
18
|
-
const { action, slugs } = await c.req.json();
|
|
18
|
+
const { action, slugs, locale = '' } = await c.req.json();
|
|
19
19
|
if (!Array.isArray(slugs) || !slugs.length) return c.json({ error: 'slugs required' }, 400);
|
|
20
20
|
if (!['publish', 'draft', 'delete', 'restore', 'permanent'].includes(action)) return c.json({ error: 'Invalid action' }, 400);
|
|
21
21
|
const db = openPod(c.get('podPath'));
|
|
22
|
-
if (action === 'delete') { slugs.forEach(slug => db.deleteEntry(collectionId, slug)); }
|
|
23
|
-
else if (action === 'restore') { slugs.forEach(slug => db.restoreEntry(collectionId, slug)); }
|
|
24
|
-
else if (action === 'permanent') { slugs.forEach(slug => db.permanentDeleteEntry(collectionId, slug)); }
|
|
22
|
+
if (action === 'delete') { slugs.forEach(slug => db.deleteEntry(collectionId, slug, locale)); }
|
|
23
|
+
else if (action === 'restore') { slugs.forEach(slug => db.restoreEntry(collectionId, slug, locale)); }
|
|
24
|
+
else if (action === 'permanent') { slugs.forEach(slug => db.permanentDeleteEntry(collectionId, slug, locale)); }
|
|
25
25
|
else {
|
|
26
26
|
const status = action === 'publish' ? 'published' : 'draft';
|
|
27
27
|
slugs.forEach(slug => {
|
|
28
|
-
const entry = db.getEntry(collectionId, slug);
|
|
29
|
-
if (entry) db.updateEntry(collectionId, slug, { slug, data: entry.data, status });
|
|
28
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
29
|
+
if (entry) db.updateEntry(collectionId, slug, { slug, data: entry.data, status, locale });
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
db.close();
|
|
@@ -47,39 +47,50 @@ entryRoutes.patch('/:collectionId/entries/reorder', async (c) => {
|
|
|
47
47
|
return c.json({ ok: true });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// GET /api/collections/:id/entries?status=draft|published
|
|
50
|
+
// GET /api/collections/:id/entries?status=draft|published&locale=
|
|
51
51
|
entryRoutes.get('/:collectionId/entries', (c) => {
|
|
52
52
|
const { collectionId } = c.req.param();
|
|
53
|
-
const status
|
|
54
|
-
const
|
|
53
|
+
const status = c.req.query('status') || undefined;
|
|
54
|
+
const locale = c.req.query('locale'); // undefined = all locales
|
|
55
|
+
const db = openPod(c.get('podPath'));
|
|
55
56
|
if (!db.getCollection(collectionId)) { db.close(); return c.json({ error: 'Collection not found' }, 404); }
|
|
56
|
-
const entries = db.getEntries(collectionId, { status });
|
|
57
|
+
const entries = db.getEntries(collectionId, { status, locale });
|
|
57
58
|
db.close();
|
|
58
59
|
return c.json(entries);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
// GET /api/collections/:id/entries/:slug
|
|
62
|
+
// GET /api/collections/:id/entries/:slug?locale=
|
|
62
63
|
entryRoutes.get('/:collectionId/entries/:slug', (c) => {
|
|
63
64
|
const { collectionId, slug } = c.req.param();
|
|
64
|
-
const
|
|
65
|
-
const
|
|
65
|
+
const locale = c.req.query('locale') ?? '';
|
|
66
|
+
const db = openPod(c.get('podPath'));
|
|
67
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
66
68
|
db.close();
|
|
67
69
|
if (!entry) return c.json({ error: 'Not found' }, 404);
|
|
68
70
|
return c.json(entry);
|
|
69
71
|
});
|
|
70
72
|
|
|
73
|
+
// GET /api/collections/:id/entries/:slug/locales — list all locale versions
|
|
74
|
+
entryRoutes.get('/:collectionId/entries/:slug/locales', (c) => {
|
|
75
|
+
const { collectionId, slug } = c.req.param();
|
|
76
|
+
const db = openPod(c.get('podPath'));
|
|
77
|
+
const locales = db.getEntryLocales(collectionId, slug);
|
|
78
|
+
db.close();
|
|
79
|
+
return c.json(locales);
|
|
80
|
+
});
|
|
81
|
+
|
|
71
82
|
// POST /api/collections/:id/entries
|
|
72
83
|
entryRoutes.post('/:collectionId/entries', async (c) => {
|
|
73
84
|
const { collectionId } = c.req.param();
|
|
74
|
-
const { slug, data = {}, status = 'draft' } = await c.req.json();
|
|
85
|
+
const { slug, data = {}, status = 'draft', locale = '' } = await c.req.json();
|
|
75
86
|
if (!slug) return c.json({ error: 'slug is required' }, 400);
|
|
76
87
|
|
|
77
88
|
const db = openPod(c.get('podPath'));
|
|
78
89
|
if (!db.getCollection(collectionId)) { db.close(); return c.json({ error: 'Collection not found' }, 404); }
|
|
79
|
-
if (db.getEntry(collectionId, slug)) { db.close(); return c.json({ error: `Entry "${slug}" already exists` }, 409); }
|
|
90
|
+
if (db.getEntry(collectionId, slug, locale)) { db.close(); return c.json({ error: `Entry "${slug}" (${locale || 'default'}) already exists` }, 409); }
|
|
80
91
|
|
|
81
|
-
const id = db.createEntry(collectionId, slug, data, status);
|
|
82
|
-
const entry = db.getEntry(collectionId, slug);
|
|
92
|
+
const id = db.createEntry(collectionId, slug, data, status, locale);
|
|
93
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
83
94
|
db.logAudit(id, c.get('user')?.username ?? 'unknown', 'create');
|
|
84
95
|
db.close();
|
|
85
96
|
return c.json({ ...entry, id }, 201);
|
|
@@ -88,13 +99,14 @@ entryRoutes.post('/:collectionId/entries', async (c) => {
|
|
|
88
99
|
// PUT /api/collections/:id/entries/:slug
|
|
89
100
|
entryRoutes.put('/:collectionId/entries/:slug', async (c) => {
|
|
90
101
|
const { collectionId, slug } = c.req.param();
|
|
91
|
-
const body
|
|
102
|
+
const body = await c.req.json();
|
|
103
|
+
const locale = body.locale ?? c.req.query('locale') ?? '';
|
|
92
104
|
|
|
93
105
|
const db = openPod(c.get('podPath'));
|
|
94
|
-
const before = db.getEntry(collectionId, slug);
|
|
95
|
-
const ok = db.updateEntry(collectionId, slug, body);
|
|
106
|
+
const before = db.getEntry(collectionId, slug, locale);
|
|
107
|
+
const ok = db.updateEntry(collectionId, slug, { ...body, locale });
|
|
96
108
|
if (!ok) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
97
|
-
const updated = db.getEntry(collectionId, body.slug ?? slug);
|
|
109
|
+
const updated = db.getEntry(collectionId, body.slug ?? slug, locale);
|
|
98
110
|
const username = c.get('user')?.username ?? 'unknown';
|
|
99
111
|
if (body.status === 'published' && before?.status !== 'published') {
|
|
100
112
|
db.logAudit(updated.id, username, 'publish');
|
|
@@ -114,35 +126,38 @@ entryRoutes.put('/:collectionId/entries/:slug', async (c) => {
|
|
|
114
126
|
return c.json(updated);
|
|
115
127
|
});
|
|
116
128
|
|
|
117
|
-
// DELETE /api/collections/:id/entries/:slug → soft delete (trash)
|
|
129
|
+
// DELETE /api/collections/:id/entries/:slug?locale= → soft delete (trash)
|
|
118
130
|
entryRoutes.delete('/:collectionId/entries/:slug', (c) => {
|
|
119
131
|
const { collectionId, slug } = c.req.param();
|
|
120
|
-
const
|
|
121
|
-
const
|
|
132
|
+
const locale = c.req.query('locale') ?? '';
|
|
133
|
+
const db = openPod(c.get('podPath'));
|
|
134
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
122
135
|
if (!entry) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
123
|
-
db.deleteEntry(collectionId, slug);
|
|
136
|
+
db.deleteEntry(collectionId, slug, locale);
|
|
124
137
|
db.logAudit(entry.id, c.get('user')?.username ?? 'unknown', 'delete');
|
|
125
138
|
db.close();
|
|
126
139
|
return c.json({ ok: true });
|
|
127
140
|
});
|
|
128
141
|
|
|
129
|
-
// POST /api/collections/:id/entries/:slug/restore
|
|
142
|
+
// POST /api/collections/:id/entries/:slug/restore?locale=
|
|
130
143
|
entryRoutes.post('/:collectionId/entries/:slug/restore', (c) => {
|
|
131
144
|
const { collectionId, slug } = c.req.param();
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
145
|
+
const locale = c.req.query('locale') ?? '';
|
|
146
|
+
const db = openPod(c.get('podPath'));
|
|
147
|
+
const row = db.db.prepare('SELECT * FROM _entries WHERE collection_id = ? AND slug = ? AND locale = ? AND deleted_at IS NOT NULL').get(collectionId, slug, locale);
|
|
148
|
+
const ok = db.restoreEntry(collectionId, slug, locale);
|
|
135
149
|
if (row) db.logAudit(row.id, c.get('user')?.username ?? 'unknown', 'restore');
|
|
136
150
|
db.close();
|
|
137
151
|
if (!ok) return c.json({ error: 'Not found in trash' }, 404);
|
|
138
152
|
return c.json({ ok: true });
|
|
139
153
|
});
|
|
140
154
|
|
|
141
|
-
// DELETE /api/collections/:id/entries/:slug/permanent
|
|
155
|
+
// DELETE /api/collections/:id/entries/:slug/permanent?locale=
|
|
142
156
|
entryRoutes.delete('/:collectionId/entries/:slug/permanent', (c) => {
|
|
143
157
|
const { collectionId, slug } = c.req.param();
|
|
144
|
-
const
|
|
145
|
-
const
|
|
158
|
+
const locale = c.req.query('locale') ?? '';
|
|
159
|
+
const db = openPod(c.get('podPath'));
|
|
160
|
+
const ok = db.permanentDeleteEntry(collectionId, slug, locale);
|
|
146
161
|
db.close();
|
|
147
162
|
if (!ok) return c.json({ error: 'Not found' }, 404);
|
|
148
163
|
return c.json({ ok: true });
|
|
@@ -258,17 +273,18 @@ entryRoutes.post('/:collectionId/entries/import.csv', async (c) => {
|
|
|
258
273
|
return c.json({ ok: true, created, updated, skipped });
|
|
259
274
|
});
|
|
260
275
|
|
|
261
|
-
// POST /api/collections/:id/entries/:slug/duplicate
|
|
276
|
+
// POST /api/collections/:id/entries/:slug/duplicate?locale=
|
|
262
277
|
entryRoutes.post('/:collectionId/entries/:slug/duplicate', (c) => {
|
|
263
278
|
const { collectionId, slug } = c.req.param();
|
|
264
|
-
const
|
|
265
|
-
const
|
|
279
|
+
const locale = c.req.query('locale') ?? '';
|
|
280
|
+
const db = openPod(c.get('podPath'));
|
|
281
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
266
282
|
if (!entry) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
267
283
|
let newSlug = slug + '-copy';
|
|
268
284
|
let i = 2;
|
|
269
|
-
while (db.getEntry(collectionId, newSlug)) newSlug = `${slug}-copy-${i++}`;
|
|
270
|
-
db.createEntry(collectionId, newSlug, entry.data, 'draft');
|
|
271
|
-
const created = db.getEntry(collectionId, newSlug);
|
|
285
|
+
while (db.getEntry(collectionId, newSlug, locale)) newSlug = `${slug}-copy-${i++}`;
|
|
286
|
+
db.createEntry(collectionId, newSlug, entry.data, 'draft', locale);
|
|
287
|
+
const created = db.getEntry(collectionId, newSlug, locale);
|
|
272
288
|
db.close();
|
|
273
289
|
return c.json(created, 201);
|
|
274
290
|
});
|
|
@@ -276,15 +292,15 @@ entryRoutes.post('/:collectionId/entries/:slug/duplicate', (c) => {
|
|
|
276
292
|
// PATCH /api/collections/:id/entries/:slug/status
|
|
277
293
|
entryRoutes.patch('/:collectionId/entries/:slug/status', async (c) => {
|
|
278
294
|
const { collectionId, slug } = c.req.param();
|
|
279
|
-
const { status, publish_at, unpublish_at } = await c.req.json();
|
|
295
|
+
const { status, publish_at, unpublish_at, locale = '' } = await c.req.json();
|
|
280
296
|
if (!['draft', 'published', 'scheduled'].includes(status)) return c.json({ error: 'Invalid status' }, 400);
|
|
281
297
|
if (status === 'scheduled' && !publish_at) return c.json({ error: 'publish_at required for scheduled status' }, 400);
|
|
282
298
|
const db = openPod(c.get('podPath'));
|
|
283
|
-
const entry = db.getEntry(collectionId, slug);
|
|
299
|
+
const entry = db.getEntry(collectionId, slug, locale);
|
|
284
300
|
if (!entry) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
285
301
|
const pa = status === 'scheduled' ? publish_at : null;
|
|
286
302
|
const ua = status === 'published' ? (unpublish_at ?? null) : null;
|
|
287
|
-
db.updateEntry(collectionId, slug, { slug, data: entry.data, status, publish_at: pa, unpublish_at: ua });
|
|
303
|
+
db.updateEntry(collectionId, slug, { slug, data: entry.data, status, publish_at: pa, unpublish_at: ua, locale });
|
|
288
304
|
const username = c.get('user')?.username ?? 'unknown';
|
|
289
305
|
if (status === 'scheduled') db.logAudit(entry.id, username, 'schedule');
|
|
290
306
|
db.close();
|