@a83/orbiter-admin 0.3.19 → 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/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/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();
|