includio-cms 0.7.2 → 0.13.0
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/CHANGELOG.md +110 -0
- package/ROADMAP.md +40 -2
- package/dist/admin/api/generate-styles.d.ts +2 -0
- package/dist/admin/api/generate-styles.js +32 -0
- package/dist/admin/api/handler.js +33 -0
- package/dist/admin/api/media-gc.js +10 -4
- package/dist/admin/api/rest/handler.js +17 -0
- package/dist/admin/api/rest/routes/collections.js +25 -13
- package/dist/admin/api/rest/routes/entries.d.ts +1 -1
- package/dist/admin/api/rest/routes/entries.js +10 -10
- package/dist/admin/api/rest/routes/media.d.ts +2 -0
- package/dist/admin/api/rest/routes/media.js +9 -0
- package/dist/admin/api/rest/routes/schema.d.ts +5 -0
- package/dist/admin/api/rest/routes/schema.js +152 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
- package/dist/admin/api/rest/routes/singletons.js +8 -7
- package/dist/admin/api/rest/routes/upload.d.ts +2 -0
- package/dist/admin/api/rest/routes/upload.js +28 -0
- package/dist/admin/api/upload.js +13 -0
- package/dist/admin/client/collection/collection-entries.svelte +19 -6
- package/dist/admin/client/entry/entry.svelte +21 -23
- package/dist/admin/client/entry/header/a11y-validator.js +2 -2
- package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
- package/dist/admin/client/entry/header/status-badge.svelte +2 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
- package/dist/admin/client/entry/header/visibility.svelte +16 -10
- package/dist/admin/client/entry/utils.d.ts +3 -0
- package/dist/admin/client/entry/utils.js +22 -4
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
- package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
- package/dist/admin/components/fields/blocks-field.svelte +9 -10
- package/dist/admin/components/fields/field-renderer.svelte +4 -8
- package/dist/admin/components/fields/object-field.svelte +7 -12
- package/dist/admin/components/fields/select-field.svelte +8 -2
- package/dist/admin/components/fields/seo-field.svelte +40 -93
- package/dist/admin/components/fields/simple-array-field.svelte +5 -5
- package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
- package/dist/admin/components/fields/url-field.svelte +61 -72
- package/dist/admin/components/media/file-upload.svelte +5 -1
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +79 -11
- package/dist/admin/components/media/tag-sidebar.svelte +10 -6
- package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +21 -93
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/link-dialog.svelte +10 -11
- package/dist/admin/components/tiptap/slash-command.js +1 -1
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +22 -27
- package/dist/admin/remote/media.remote.d.ts +15 -0
- package/dist/admin/remote/media.remote.js +18 -2
- package/dist/admin/remote/preview.remote.js +3 -1
- package/dist/admin/utils/entryLabel.js +9 -6
- package/dist/admin/utils/translationStatus.js +1 -2
- package/dist/cli/scaffold/admin.js +34 -2
- package/dist/cms/runtime/api.d.ts +16 -12
- package/dist/cms/runtime/api.js +7 -6
- package/dist/cms/runtime/remote.js +2 -2
- package/dist/cms/runtime/schemas.d.ts +1 -1
- package/dist/cms/runtime/schemas.js +1 -1
- package/dist/cms/runtime/types.d.ts +118 -112
- package/dist/cms/runtime/types.js +0 -12
- package/dist/core/cms.d.ts +3 -1
- package/dist/core/cms.js +30 -0
- package/dist/core/fields/fieldSchemaToTs.js +9 -15
- package/dist/core/fields/formFieldSchemaToTs.js +7 -0
- package/dist/core/server/entries/operations/create.js +10 -4
- package/dist/core/server/entries/operations/get.d.ts +1 -0
- package/dist/core/server/entries/operations/get.js +186 -191
- package/dist/core/server/entries/operations/update.d.ts +6 -7
- package/dist/core/server/entries/operations/update.js +20 -38
- package/dist/core/server/fields/populateEntry.js +16 -52
- package/dist/core/server/fields/resolveImageFields.js +69 -120
- package/dist/core/server/fields/resolveRelationFields.js +30 -51
- package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
- package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
- package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
- package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
- package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
- package/dist/core/server/fields/resolveUrlFields.js +47 -56
- package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
- package/dist/core/server/fields/utils/fixOrphans.js +12 -0
- package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
- package/dist/core/server/fields/utils/imageStyles.js +41 -25
- package/dist/core/server/fields/utils/resolveMedia.js +1 -6
- package/dist/core/server/forms/submissions/operations/delete.js +26 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
- package/dist/core/server/generator/fields.d.ts +6 -0
- package/dist/core/server/generator/fields.js +43 -5
- package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
- package/dist/core/server/generator/formFields.js +1 -0
- package/dist/core/server/generator/generator.js +98 -30
- package/dist/core/server/media/operations/getFiles.d.ts +5 -0
- package/dist/core/server/media/operations/getFiles.js +6 -0
- package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
- package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
- package/dist/db-postgres/index.js +303 -37
- package/dist/db-postgres/schema/entry.d.ts +0 -94
- package/dist/db-postgres/schema/entry.js +0 -6
- package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
- package/dist/db-postgres/schema/entryVersion.js +1 -0
- package/dist/entity/index.d.ts +9 -4
- package/dist/entity/index.js +24 -24
- package/dist/files-local/index.js +43 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/preview.svelte +2 -326
- package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
- package/dist/sveltekit/server/index.d.ts +2 -1
- package/dist/sveltekit/server/index.js +2 -1
- package/dist/sveltekit/server/preview.js +4 -7
- package/dist/types/adapters/db.d.ts +15 -1
- package/dist/types/adapters/files.d.ts +6 -0
- package/dist/types/cms.d.ts +5 -0
- package/dist/types/entries.d.ts +54 -18
- package/dist/types/fields.d.ts +14 -24
- package/dist/types/formFields.d.ts +7 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/structured-content.d.ts +5 -0
- package/dist/updates/0.10.0/index.d.ts +2 -0
- package/dist/updates/0.10.0/index.js +15 -0
- package/dist/updates/0.11.0/index.d.ts +2 -0
- package/dist/updates/0.11.0/index.js +12 -0
- package/dist/updates/0.12.0/index.d.ts +2 -0
- package/dist/updates/0.12.0/index.js +12 -0
- package/dist/updates/0.13.0/index.d.ts +2 -0
- package/dist/updates/0.13.0/index.js +10 -0
- package/dist/updates/0.7.3/index.d.ts +2 -0
- package/dist/updates/0.7.3/index.js +10 -0
- package/dist/updates/0.8.0/index.d.ts +2 -0
- package/dist/updates/0.8.0/index.js +18 -0
- package/dist/updates/0.8.0/migrate.d.ts +2 -0
- package/dist/updates/0.8.0/migrate.js +101 -0
- package/dist/updates/0.9.0/index.d.ts +2 -0
- package/dist/updates/0.9.0/index.js +38 -0
- package/dist/updates/index.js +8 -1
- package/package.json +7 -6
- package/dist/admin/components/fields/image-field.svelte +0 -198
- package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
- package/dist/admin/components/fields/richtext-field.svelte +0 -13
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
- package/dist/admin/components/tiptap.svelte +0 -11
- package/dist/admin/components/tiptap.svelte.d.ts +0 -6
- package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
- package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -3,11 +3,12 @@ import { getCMS } from '../../../../core/cms.js';
|
|
|
3
3
|
import { getRawEntries } from '../../../../core/server/entries/operations/get.js';
|
|
4
4
|
import { createEntry } from '../../../../core/server/entries/operations/create.js';
|
|
5
5
|
import { upsertDraftVersion } from '../../../../core/server/entries/operations/update.js';
|
|
6
|
-
export async function GET(
|
|
6
|
+
export async function GET(event, slug) {
|
|
7
7
|
const cms = getCMS();
|
|
8
8
|
if (!cms.singles[slug]) {
|
|
9
9
|
return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
|
|
10
10
|
}
|
|
11
|
+
const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
|
|
11
12
|
const entries = await getRawEntries({ slug });
|
|
12
13
|
const entry = entries[0];
|
|
13
14
|
if (!entry) {
|
|
@@ -24,11 +25,10 @@ export async function GET(_event, slug) {
|
|
|
24
25
|
type: entry.type,
|
|
25
26
|
createdAt: entry.createdAt,
|
|
26
27
|
updatedAt: entry.updatedAt,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
publishedVersionId: entry.publishedVersion?.id ?? null
|
|
28
|
+
draftData: entry.draftVersions[lang]?.data ?? null,
|
|
29
|
+
publishedData: entry.publishedVersions[lang]?.data ?? null,
|
|
30
|
+
draftVersionId: entry.draftVersions[lang]?.id ?? null,
|
|
31
|
+
publishedVersionId: entry.publishedVersions[lang]?.id ?? null
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
export async function PUT(event, slug) {
|
|
@@ -36,6 +36,7 @@ export async function PUT(event, slug) {
|
|
|
36
36
|
if (!cms.singles[slug]) {
|
|
37
37
|
return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
|
|
38
38
|
}
|
|
39
|
+
const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
|
|
39
40
|
const body = await event.request.json().catch(() => null);
|
|
40
41
|
if (!body?.data || typeof body.data !== 'object') {
|
|
41
42
|
return json({ error: 'Request body must contain "data" object' }, { status: 400 });
|
|
@@ -51,7 +52,7 @@ export async function PUT(event, slug) {
|
|
|
51
52
|
return json({ error: 'Failed to create singleton entry' }, { status: 500 });
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
const version = await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
|
|
55
|
+
const version = await upsertDraftVersion(entry.id, body.data, lang, { skipValidation: true });
|
|
55
56
|
return json({
|
|
56
57
|
id: entry.id,
|
|
57
58
|
versionId: version.id,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { uploadFile } from '../../../../core/server/media/operations/uploadFile.js';
|
|
3
|
+
import { getCMS } from '../../../../core/cms.js';
|
|
4
|
+
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
|
|
5
|
+
export async function POST(event) {
|
|
6
|
+
const form = await event.request.formData();
|
|
7
|
+
const file = form.get('file');
|
|
8
|
+
const tagIdsRaw = form.get('tagIds');
|
|
9
|
+
if (!file || file.size === 0) {
|
|
10
|
+
return json({ error: 'No file provided' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
if (file.size > MAX_UPLOAD_SIZE) {
|
|
13
|
+
return json({ error: 'File too large (max 50MB)' }, { status: 413 });
|
|
14
|
+
}
|
|
15
|
+
const dbFile = await uploadFile(file);
|
|
16
|
+
if (tagIdsRaw) {
|
|
17
|
+
try {
|
|
18
|
+
const tagIds = JSON.parse(tagIdsRaw);
|
|
19
|
+
if (Array.isArray(tagIds) && tagIds.length > 0) {
|
|
20
|
+
await getCMS().databaseAdapter.setMediaFileTags(dbFile.id, tagIds);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore invalid tagIds JSON
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return json(dbFile, { status: 201 });
|
|
28
|
+
}
|
package/dist/admin/api/upload.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { requireAuth } from '../remote/middleware/auth.js';
|
|
2
2
|
import { uploadFile } from '../../core/server/media/operations/uploadFile.js';
|
|
3
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
4
|
import { json } from '@sveltejs/kit';
|
|
4
5
|
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
|
|
5
6
|
export const POST = async ({ request }) => {
|
|
6
7
|
requireAuth();
|
|
7
8
|
const form = await request.formData();
|
|
8
9
|
const file = form.get('file');
|
|
10
|
+
const tagIdsRaw = form.get('tagIds');
|
|
9
11
|
if (!file || file.size === 0) {
|
|
10
12
|
return new Response('No file', { status: 400 });
|
|
11
13
|
}
|
|
@@ -13,5 +15,16 @@ export const POST = async ({ request }) => {
|
|
|
13
15
|
return new Response('File too large', { status: 413 });
|
|
14
16
|
}
|
|
15
17
|
const dbFile = await uploadFile(file);
|
|
18
|
+
if (tagIdsRaw) {
|
|
19
|
+
try {
|
|
20
|
+
const tagIds = JSON.parse(tagIdsRaw);
|
|
21
|
+
if (Array.isArray(tagIds) && tagIds.length > 0) {
|
|
22
|
+
await getCMS().databaseAdapter.setMediaFileTags(dbFile.id, tagIds);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// ignore invalid tagIds JSON
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
return json(dbFile);
|
|
17
30
|
};
|
|
@@ -378,8 +378,20 @@
|
|
|
378
378
|
pendingDeleteId = null;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
function getVersionData(entry: RawEntry): Record<string, unknown> | undefined {
|
|
382
|
+
const lang = contentLanguage.current;
|
|
383
|
+
const draft = entry.draftVersions[lang];
|
|
384
|
+
const published = entry.publishedVersions[lang];
|
|
385
|
+
if (draft?.data) return draft.data as Record<string, unknown>;
|
|
386
|
+
if (published?.data) return published.data as Record<string, unknown>;
|
|
387
|
+
// Fallback: any lang
|
|
388
|
+
for (const v of Object.values(entry.draftVersions)) if (v?.data) return v.data as Record<string, unknown>;
|
|
389
|
+
for (const v of Object.values(entry.publishedVersions)) if (v?.data) return v.data as Record<string, unknown>;
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
381
393
|
function getSearchText(entry: RawEntry): string {
|
|
382
|
-
const data = entry
|
|
394
|
+
const data = getVersionData(entry);
|
|
383
395
|
if (!data) return '';
|
|
384
396
|
return extractTextValues(data).join(' ').toLowerCase();
|
|
385
397
|
}
|
|
@@ -404,7 +416,7 @@
|
|
|
404
416
|
|
|
405
417
|
function extractSlug(entry: RawEntry): string | undefined {
|
|
406
418
|
if (!seoField) return undefined;
|
|
407
|
-
const data = entry
|
|
419
|
+
const data = getVersionData(entry);
|
|
408
420
|
if (!data) return undefined;
|
|
409
421
|
const seoData = data[seoField.slug];
|
|
410
422
|
if (seoData && typeof seoData === 'object' && 'slug' in seoData) {
|
|
@@ -414,7 +426,7 @@
|
|
|
414
426
|
}
|
|
415
427
|
|
|
416
428
|
function countA11yWarnings(entry: RawEntry): number {
|
|
417
|
-
const data = entry
|
|
429
|
+
const data = getVersionData(entry);
|
|
418
430
|
if (!data) return 0;
|
|
419
431
|
const a11yLang = interfaceLanguage.current === 'pl' ? a11yLangPl : a11yLangEn;
|
|
420
432
|
const issues = validateA11y(data as Record<string, unknown>, collection.fields, a11yLang);
|
|
@@ -429,7 +441,7 @@
|
|
|
429
441
|
uuidsByCollection.set(field.collection, new Set());
|
|
430
442
|
}
|
|
431
443
|
for (const entry of entries) {
|
|
432
|
-
const data = entry
|
|
444
|
+
const data = getVersionData(entry);
|
|
433
445
|
if (!data) continue;
|
|
434
446
|
for (const field of relationListFields) {
|
|
435
447
|
const raw = (data as Record<string, unknown>)[field.slug];
|
|
@@ -454,9 +466,10 @@
|
|
|
454
466
|
}
|
|
455
467
|
|
|
456
468
|
function mapEntryToRow(entry: RawEntry, lookup: Record<string, string> = {}): CollectionDataTableRow {
|
|
457
|
-
const data = entry
|
|
469
|
+
const data = getVersionData(entry) || {};
|
|
458
470
|
// For custom columns, prefer published data (complete) over draft (may be partial)
|
|
459
|
-
const
|
|
471
|
+
const lang = contentLanguage.current;
|
|
472
|
+
const columnData = entry.publishedVersions[lang]?.data || entry.draftVersions[lang]?.data || data;
|
|
460
473
|
const customData: Record<string, unknown> = {};
|
|
461
474
|
if (collection.listColumns) {
|
|
462
475
|
for (const fieldSlug of collection.listColumns) {
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
|
|
29
29
|
import { onMount } from 'svelte';
|
|
30
30
|
import { get } from 'svelte/store';
|
|
31
|
-
import { computeTranslationStatus } from '../../utils/translationStatus.js';
|
|
32
|
-
|
|
33
31
|
const contentLanguage = getContentLanguage();
|
|
34
32
|
const remotes = getRemotes();
|
|
35
33
|
const interfaceLanguage = useInterfaceLanguage();
|
|
@@ -142,10 +140,11 @@
|
|
|
142
140
|
let { collection } = entry;
|
|
143
141
|
const isArchived = $derived(!!entry.archivedAt);
|
|
144
142
|
|
|
145
|
-
// Create form once at component level
|
|
143
|
+
// Create form once at component level — localized: false since data is flat single-language
|
|
146
144
|
const collectionSchema = generateZodSchemaFromFields(
|
|
147
145
|
getFieldsFromConfig(collection),
|
|
148
|
-
contentLanguage.all
|
|
146
|
+
contentLanguage.all,
|
|
147
|
+
{ localized: false }
|
|
149
148
|
);
|
|
150
149
|
const form = superForm(defaults(editingEntry.data, zod4(collectionSchema)), {
|
|
151
150
|
validators: zod4Client(collectionSchema),
|
|
@@ -166,15 +165,6 @@
|
|
|
166
165
|
|
|
167
166
|
const AUTOSAVE_DELAY = 30000; // 30 seconds
|
|
168
167
|
|
|
169
|
-
// Reactive translation status — must live here where we can subscribe to form.form store
|
|
170
|
-
const formStore = form.form;
|
|
171
|
-
const collectionFields = getFieldsFromConfig(collection);
|
|
172
|
-
const translationStatus = $derived.by(() => {
|
|
173
|
-
if (contentLanguage.all.length <= 1) return null;
|
|
174
|
-
const data = $formStore;
|
|
175
|
-
return computeTranslationStatus(data, collectionFields, contentLanguage.all);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
168
|
function scheduleAutosave() {
|
|
179
169
|
if (isArchived) return;
|
|
180
170
|
if (autosaveTimer) {
|
|
@@ -195,15 +185,18 @@
|
|
|
195
185
|
|
|
196
186
|
saveStatus = 'saving';
|
|
197
187
|
try {
|
|
188
|
+
const currentLang = contentLanguage.current;
|
|
198
189
|
const result = await remotes.updateEntryVersionCommand({
|
|
199
190
|
entryId: entry.id,
|
|
191
|
+
lang: currentLang,
|
|
200
192
|
data: currentFormData,
|
|
201
193
|
type: 'draft'
|
|
202
194
|
});
|
|
203
195
|
lastSavedData = currentData;
|
|
204
196
|
saveStatus = 'saved';
|
|
205
197
|
// If we're editing the published version and saved a draft, track it for the banner
|
|
206
|
-
|
|
198
|
+
const publishedVersion = entry.publishedVersions[currentLang];
|
|
199
|
+
if (publishedVersion && editingEntry.id === publishedVersion.id && result?.id) {
|
|
207
200
|
savedDraftVersionId = result.id;
|
|
208
201
|
}
|
|
209
202
|
// Reset to idle after 3s
|
|
@@ -270,6 +263,7 @@
|
|
|
270
263
|
try {
|
|
271
264
|
await remotes.updateEntryVersionCommand({
|
|
272
265
|
entryId: entry.id,
|
|
266
|
+
lang: contentLanguage.current,
|
|
273
267
|
data: get(form.form),
|
|
274
268
|
type
|
|
275
269
|
});
|
|
@@ -298,6 +292,7 @@
|
|
|
298
292
|
try {
|
|
299
293
|
await remotes.updateEntryVersionCommand({
|
|
300
294
|
entryId: entry.id,
|
|
295
|
+
lang: contentLanguage.current,
|
|
301
296
|
data: validatedForm.data,
|
|
302
297
|
type,
|
|
303
298
|
scheduledAt
|
|
@@ -501,21 +496,25 @@
|
|
|
501
496
|
}
|
|
502
497
|
});
|
|
503
498
|
|
|
499
|
+
// Per-language version lookups
|
|
500
|
+
const currentPublishedVersion = $derived(entry.publishedVersions[contentLanguage.current]);
|
|
501
|
+
const currentDraftVersion = $derived(entry.draftVersions[contentLanguage.current]);
|
|
502
|
+
|
|
504
503
|
// Banner: viewing published version + a separate draft exists
|
|
505
504
|
const showDraftBanner = $derived(
|
|
506
|
-
|
|
507
|
-
editingEntry.id ===
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
currentPublishedVersion != null &&
|
|
506
|
+
editingEntry.id === currentPublishedVersion.id &&
|
|
507
|
+
currentDraftVersion != null &&
|
|
508
|
+
currentDraftVersion.id !== currentPublishedVersion.id &&
|
|
509
|
+
currentDraftVersion.createdAt > currentPublishedVersion.createdAt
|
|
511
510
|
);
|
|
512
511
|
|
|
513
512
|
// Banner: viewing draft + a published version exists
|
|
514
513
|
const showPublishedBanner = $derived(
|
|
515
|
-
|
|
514
|
+
currentPublishedVersion != null && editingEntry.id !== currentPublishedVersion.id
|
|
516
515
|
);
|
|
517
516
|
|
|
518
|
-
const draftVersionId = $derived(
|
|
517
|
+
const draftVersionId = $derived(currentDraftVersion?.id ?? savedDraftVersionId);
|
|
519
518
|
|
|
520
519
|
const scrollToIssue = (fieldSlug: string) => {
|
|
521
520
|
const fieldEl = document.querySelector(`[data-field-path="${fieldSlug}"]`);
|
|
@@ -554,7 +553,6 @@
|
|
|
554
553
|
fields={getFieldsFromConfig(collection)}
|
|
555
554
|
getFormData={() => get(form.form)}
|
|
556
555
|
onScrollToIssue={scrollToIssue}
|
|
557
|
-
{translationStatus}
|
|
558
556
|
/>
|
|
559
557
|
|
|
560
558
|
{#if validationErrors.length > 0}
|
|
@@ -620,7 +618,7 @@
|
|
|
620
618
|
<button
|
|
621
619
|
type="button"
|
|
622
620
|
class="font-semibold text-[var(--primary)] hover:underline"
|
|
623
|
-
onclick={() => goto(`?version=${
|
|
621
|
+
onclick={() => goto(`?version=${currentPublishedVersion!.id}`)}
|
|
624
622
|
>
|
|
625
623
|
{lang[interfaceLanguage.current].switchToPublished}
|
|
626
624
|
</button>
|
|
@@ -198,7 +198,7 @@ function tryExtractDoc(val) {
|
|
|
198
198
|
function extractDocs(data, fields) {
|
|
199
199
|
const result = [];
|
|
200
200
|
for (const field of fields) {
|
|
201
|
-
if (field.type !== 'content'
|
|
201
|
+
if (field.type !== 'content')
|
|
202
202
|
continue;
|
|
203
203
|
const val = data[field.slug];
|
|
204
204
|
const docs = tryExtractDoc(val);
|
|
@@ -212,7 +212,7 @@ function extractDocs(data, fields) {
|
|
|
212
212
|
function extractDocsForLang(data, fields, language) {
|
|
213
213
|
const result = [];
|
|
214
214
|
for (const field of fields) {
|
|
215
|
-
if (field.type !== 'content'
|
|
215
|
+
if (field.type !== 'content')
|
|
216
216
|
continue;
|
|
217
217
|
const val = data[field.slug];
|
|
218
218
|
if (!val || typeof val !== 'object')
|
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
import InfoCircle from '@tabler/icons-svelte/icons/info-circle';
|
|
17
17
|
import Shield from '@tabler/icons-svelte/icons/shield';
|
|
18
18
|
import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
|
|
19
|
-
import {
|
|
19
|
+
import { getEntryStatusForLang } from '../utils.js';
|
|
20
20
|
import { validateA11y, a11yLangPl, a11yLangEn, type A11yIssue } from './a11y-validator.js';
|
|
21
21
|
import { getContentLanguage } from '../../../state/content-language.svelte.js';
|
|
22
|
-
import { computeTranslationStatus, type LangStatus } from '../../../utils/translationStatus.js';
|
|
23
22
|
import LanguageIcon from '@tabler/icons-svelte/icons/language';
|
|
24
23
|
|
|
25
24
|
const lang: Record<
|
|
@@ -131,19 +130,13 @@
|
|
|
131
130
|
let dateValue = $state('');
|
|
132
131
|
let timeValue = $state('');
|
|
133
132
|
let a11yIssues = $state<A11yIssue[]>([]);
|
|
134
|
-
let translationStatus = $state<Record<string, LangStatus> | null>(null);
|
|
135
133
|
|
|
136
|
-
|
|
134
|
+
// Status is per current language
|
|
135
|
+
const entryStatus = $derived(getEntryStatusForLang(entry, contentLanguage.current));
|
|
137
136
|
const t = $derived(lang[interfaceLanguage.current]);
|
|
138
137
|
|
|
139
138
|
const hasA11yWarnings = $derived(a11yIssues.some((i) => i.type === 'warning'));
|
|
140
|
-
const
|
|
141
|
-
translationStatus != null &&
|
|
142
|
-
Object.values(translationStatus).some((s) => s.status !== 'complete')
|
|
143
|
-
);
|
|
144
|
-
const showTranslationSection = $derived(
|
|
145
|
-
contentLanguage.all.length > 1 && translationStatus != null
|
|
146
|
-
);
|
|
139
|
+
const showLangStatusSection = $derived(contentLanguage.all.length > 1);
|
|
147
140
|
|
|
148
141
|
const scheduleLabel = $derived.by(() => {
|
|
149
142
|
switch (entryStatus) {
|
|
@@ -173,8 +166,9 @@
|
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
function setDefaultValues() {
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
const scheduledVersion = entry.scheduledVersions[contentLanguage.current];
|
|
170
|
+
if (entryStatus === 'scheduled' && scheduledVersion?.publishedAt) {
|
|
171
|
+
const scheduled = new Date(scheduledVersion.publishedAt);
|
|
178
172
|
dateValue = scheduled.toISOString().slice(0, 10);
|
|
179
173
|
timeValue = `${String(scheduled.getHours()).padStart(2, '0')}:${String(scheduled.getMinutes()).padStart(2, '0')}`;
|
|
180
174
|
} else {
|
|
@@ -229,20 +223,10 @@
|
|
|
229
223
|
a11yIssues = validateA11y(data, fields, a11yLang, contentLanguage.all);
|
|
230
224
|
}
|
|
231
225
|
|
|
232
|
-
function runTranslationCheck() {
|
|
233
|
-
if (contentLanguage.all.length <= 1 || !fields.length) {
|
|
234
|
-
translationStatus = null;
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const data = getFormData ? getFormData() : version.data;
|
|
238
|
-
translationStatus = computeTranslationStatus(data, fields, contentLanguage.all);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
226
|
$effect(() => {
|
|
242
227
|
if (open) {
|
|
243
228
|
setDefaultValues();
|
|
244
229
|
runA11yValidation();
|
|
245
|
-
runTranslationCheck();
|
|
246
230
|
}
|
|
247
231
|
});
|
|
248
232
|
</script>
|
|
@@ -284,92 +268,56 @@
|
|
|
284
268
|
</div>
|
|
285
269
|
</div>
|
|
286
270
|
|
|
287
|
-
{#if entryStatus === 'published' && entry.publishedAt}
|
|
271
|
+
{#if entryStatus === 'published' && entry.publishedVersions[contentLanguage.current]?.publishedAt}
|
|
288
272
|
<div class="publish-detail-row">
|
|
289
273
|
<span>{t.publishedAt}</span>
|
|
290
|
-
<span class="tabular-nums">{formatDateTime(entry.publishedAt)}</span>
|
|
274
|
+
<span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current]?.publishedAt)}</span>
|
|
291
275
|
</div>
|
|
292
276
|
{/if}
|
|
293
277
|
|
|
294
|
-
{#if entryStatus === 'scheduled' && entry.publishedAt}
|
|
278
|
+
{#if entryStatus === 'scheduled' && entry.publishedVersions[contentLanguage.current]?.publishedAt}
|
|
295
279
|
<div class="publish-detail-row">
|
|
296
280
|
<span>{t.scheduledFor}</span>
|
|
297
|
-
<span class="tabular-nums">{formatDateTime(entry.publishedAt)}</span>
|
|
281
|
+
<span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current]?.publishedAt)}</span>
|
|
298
282
|
</div>
|
|
299
283
|
{/if}
|
|
300
284
|
|
|
301
|
-
{#if entry.
|
|
285
|
+
{#if entry.publishedVersions[contentLanguage.current] && entry.publishedVersions[contentLanguage.current].id !== version.id}
|
|
302
286
|
<div class="publish-detail-row">
|
|
303
287
|
<span>{t.publishedVersion}</span>
|
|
304
|
-
<span class="tabular-nums">{formatDateTime(entry.
|
|
288
|
+
<span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current].createdAt)}</span>
|
|
305
289
|
</div>
|
|
306
290
|
{/if}
|
|
307
291
|
</div>
|
|
308
292
|
|
|
309
|
-
<!--
|
|
310
|
-
{#if
|
|
293
|
+
<!-- Per-language status section -->
|
|
294
|
+
{#if showLangStatusSection}
|
|
311
295
|
<div class="sheet-section">
|
|
312
296
|
<div class="sheet-section-title">
|
|
313
297
|
<LanguageIcon class="size-3.5" />
|
|
314
298
|
{t.translations}
|
|
315
299
|
</div>
|
|
316
300
|
|
|
317
|
-
{#each contentLanguage.all as
|
|
318
|
-
{@const
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
<div
|
|
329
|
-
class="translation-progress-fill {status.status === 'complete' ? 'complete' : status.status === 'partial' ? 'partial' : ''}"
|
|
330
|
-
style="width: {status.percentage}%"
|
|
331
|
-
></div>
|
|
332
|
-
</div>
|
|
301
|
+
{#each contentLanguage.all as langCode}
|
|
302
|
+
{@const langPub = entry.publishedVersions[langCode]}
|
|
303
|
+
{@const langSch = entry.scheduledVersions[langCode]}
|
|
304
|
+
{@const langDraft = entry.draftVersions[langCode]}
|
|
305
|
+
{@const langStatus = langPub ? 'published' : langSch ? 'scheduled' : 'draft'}
|
|
306
|
+
<div class="translation-lang-row">
|
|
307
|
+
<div class="translation-lang-header">
|
|
308
|
+
<span class="translation-lang-label">{langCode.toUpperCase()}</span>
|
|
309
|
+
<span class="translation-lang-pct {langStatus === 'published' ? 'complete' : langStatus === 'scheduled' ? 'partial' : 'empty'}">
|
|
310
|
+
{t.statusLabels[langStatus]}
|
|
311
|
+
</span>
|
|
333
312
|
</div>
|
|
334
|
-
|
|
313
|
+
<div class="translation-progress-track">
|
|
314
|
+
<div
|
|
315
|
+
class="translation-progress-fill {langStatus === 'published' ? 'complete' : langStatus === 'scheduled' ? 'partial' : ''}"
|
|
316
|
+
style="width: {langStatus === 'published' ? 100 : langStatus === 'scheduled' ? 66 : langDraft ? 33 : 0}%"
|
|
317
|
+
></div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
335
320
|
{/each}
|
|
336
|
-
|
|
337
|
-
{#if hasIncompleteTranslations}
|
|
338
|
-
{#each contentLanguage.all as lang}
|
|
339
|
-
{@const status = translationStatus[lang]}
|
|
340
|
-
{#if status && status.missingFields.length > 0}
|
|
341
|
-
<div class="translation-missing">
|
|
342
|
-
<div class="translation-missing-title">{t.missingFieldsLabel(lang)}</div>
|
|
343
|
-
{#each status.missingFields as field}
|
|
344
|
-
{#if onScrollToIssue}
|
|
345
|
-
<button
|
|
346
|
-
type="button"
|
|
347
|
-
class="a11y-item warning a11y-item-clickable"
|
|
348
|
-
onclick={() => {
|
|
349
|
-
contentLanguage.current = lang;
|
|
350
|
-
open = false;
|
|
351
|
-
setTimeout(() => onScrollToIssue!(field.slug, 0), 200);
|
|
352
|
-
}}
|
|
353
|
-
>
|
|
354
|
-
<div class="a11y-item-icon">
|
|
355
|
-
<AlertTriangle class="size-4" />
|
|
356
|
-
</div>
|
|
357
|
-
<span class="a11y-item-text">{field.label}</span>
|
|
358
|
-
</button>
|
|
359
|
-
{:else}
|
|
360
|
-
<div class="a11y-item warning">
|
|
361
|
-
<div class="a11y-item-icon">
|
|
362
|
-
<AlertTriangle class="size-4" />
|
|
363
|
-
</div>
|
|
364
|
-
<span class="a11y-item-text">{field.label}</span>
|
|
365
|
-
</div>
|
|
366
|
-
{/if}
|
|
367
|
-
{/each}
|
|
368
|
-
</div>
|
|
369
|
-
{/if}
|
|
370
|
-
{/each}
|
|
371
|
-
<p class="a11y-hint">{t.translationsHint}</p>
|
|
372
|
-
{/if}
|
|
373
321
|
</div>
|
|
374
322
|
{/if}
|
|
375
323
|
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
|
|
46
46
|
let { entry, version }: Props = $props();
|
|
47
47
|
|
|
48
|
-
const isPublishedVersion = $derived(entry.
|
|
49
|
-
const isScheduledVersion = $derived(entry.
|
|
48
|
+
const isPublishedVersion = $derived(entry.publishedVersions[version.lang]?.id === version.id);
|
|
49
|
+
const isScheduledVersion = $derived(entry.scheduledVersions[version.lang]?.id === version.id);
|
|
50
50
|
|
|
51
51
|
const currentStatus = $derived.by((): EntryVersionStatus => {
|
|
52
52
|
if (isPublishedVersion) return 'published';
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function getDotClass(version: DbEntryVersion): string {
|
|
92
|
-
const isPublished = entry.
|
|
93
|
-
const isScheduled = entry.
|
|
92
|
+
const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published';
|
|
93
|
+
const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled';
|
|
94
94
|
const isCurrent = version.id === currentVersionId;
|
|
95
95
|
|
|
96
96
|
if (isPublished) return 'dot-published';
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
// Always keep current version
|
|
110
110
|
if (version.id === currentVersionId) return true;
|
|
111
111
|
// Always keep published/scheduled
|
|
112
|
-
if (entry.
|
|
112
|
+
if (entry.publishedVersions[version.lang]?.id === version.id) return true;
|
|
113
113
|
// Keep versions with actual data changes vs previous
|
|
114
114
|
const prevVersion = sortedVersions[idx + 1];
|
|
115
115
|
if (!prevVersion) return true; // first version
|
|
@@ -132,8 +132,8 @@
|
|
|
132
132
|
const groups: Map<string, DbEntryVersion[]> = new Map();
|
|
133
133
|
|
|
134
134
|
for (const version of timelineVersions) {
|
|
135
|
-
const isPublished = entry.
|
|
136
|
-
const isScheduled = entry.
|
|
135
|
+
const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published';
|
|
136
|
+
const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled';
|
|
137
137
|
|
|
138
138
|
let key: string;
|
|
139
139
|
if (isPublished) {
|
|
@@ -193,12 +193,12 @@
|
|
|
193
193
|
<div class="vh-item-left">
|
|
194
194
|
<span class="vh-time">{formatTime(currentVersion.createdAt)}</span>
|
|
195
195
|
<Badge class="vh-badge-editing">{t.editing}</Badge>
|
|
196
|
-
{#if entry.
|
|
196
|
+
{#if entry.publishedVersions[currentVersion.lang]?.id === currentVersion.id && entryStatus === 'published'}
|
|
197
197
|
<Badge class="vh-badge-published">
|
|
198
198
|
<SquareCheckFilled class="size-2.5 mr-0.5" />
|
|
199
199
|
{t.published}
|
|
200
200
|
</Badge>
|
|
201
|
-
{:else if entry.
|
|
201
|
+
{:else if entry.publishedVersions[currentVersion.lang]?.id === currentVersion.id && entryStatus === 'scheduled'}
|
|
202
202
|
<Badge class="vh-badge-scheduled">
|
|
203
203
|
{t.scheduled}
|
|
204
204
|
</Badge>
|
|
@@ -233,8 +233,8 @@
|
|
|
233
233
|
<div class="vh-timeline-line"></div>
|
|
234
234
|
|
|
235
235
|
{#each versions as version}
|
|
236
|
-
{@const isPublished = entry.
|
|
237
|
-
{@const isScheduled = entry.
|
|
236
|
+
{@const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published'}
|
|
237
|
+
{@const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled'}
|
|
238
238
|
{@const prevVersion = sortedVersions.find(
|
|
239
239
|
(v) => v.versionNumber === version.versionNumber - 1
|
|
240
240
|
)}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getContentLanguage } from '../../../state/content-language.svelte.js';
|
|
2
3
|
import { useInterfaceLanguage } from '../../../state/interface-language.svelte.js';
|
|
3
4
|
import type { RawEntry } from '../../../../types/entries.js';
|
|
4
5
|
import type { InterfaceLanguage } from '../../../../types/languages.js';
|
|
5
6
|
import ClockFilled from '@tabler/icons-svelte/icons/clock-filled';
|
|
6
7
|
import FileFilled from '@tabler/icons-svelte/icons/file-filled';
|
|
7
8
|
import SquareCheckFilled from '@tabler/icons-svelte/icons/square-check-filled';
|
|
8
|
-
import {
|
|
9
|
+
import { getEntryStatusForLang } from '../utils.js';
|
|
9
10
|
|
|
10
11
|
const lang: Record<
|
|
11
12
|
InterfaceLanguage,
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const interfaceLanguage = useInterfaceLanguage();
|
|
38
|
+
const contentLanguage = getContentLanguage();
|
|
37
39
|
|
|
38
40
|
type Props = {
|
|
39
41
|
entry: RawEntry;
|
|
@@ -41,34 +43,38 @@
|
|
|
41
43
|
|
|
42
44
|
let { entry }: Props = $props();
|
|
43
45
|
|
|
44
|
-
const
|
|
46
|
+
const currentLang = $derived(contentLanguage.current);
|
|
47
|
+
const entryStatus = $derived(getEntryStatusForLang(entry, currentLang));
|
|
48
|
+
const publishedVersion = $derived(entry.publishedVersions[currentLang]);
|
|
49
|
+
const scheduledVersion = $derived(entry.scheduledVersions[currentLang]);
|
|
50
|
+
const draftVersion = $derived(entry.draftVersions[currentLang]);
|
|
45
51
|
</script>
|
|
46
52
|
|
|
47
53
|
<div class="flex items-center gap-1 text-sm">
|
|
48
54
|
<span class="text-muted-foreground"> {lang[interfaceLanguage.current].status}:</span>
|
|
49
55
|
|
|
50
|
-
{#if entryStatus === 'published' &&
|
|
56
|
+
{#if entryStatus === 'published' && publishedVersion}
|
|
51
57
|
<span class="flex items-center gap-1">
|
|
52
58
|
<SquareCheckFilled />
|
|
53
|
-
{lang[interfaceLanguage.current].published} (v{
|
|
59
|
+
{lang[interfaceLanguage.current].published} (v{publishedVersion.versionNumber}, {publishedVersion.publishedAt ? new Date(publishedVersion.publishedAt).toLocaleString(
|
|
54
60
|
interfaceLanguage.current
|
|
55
|
-
)})</span
|
|
61
|
+
) : ''})</span
|
|
56
62
|
>
|
|
57
63
|
{/if}
|
|
58
64
|
|
|
59
|
-
{#if entryStatus === 'scheduled' &&
|
|
65
|
+
{#if entryStatus === 'scheduled' && scheduledVersion}
|
|
60
66
|
<span class="flex items-center gap-1">
|
|
61
67
|
<ClockFilled />
|
|
62
|
-
{lang[interfaceLanguage.current].scheduled} v{
|
|
68
|
+
{lang[interfaceLanguage.current].scheduled} v{scheduledVersion.versionNumber}
|
|
63
69
|
{lang[interfaceLanguage.current].on}
|
|
64
|
-
{
|
|
70
|
+
{scheduledVersion.publishedAt ? new Date(scheduledVersion.publishedAt).toLocaleString(interfaceLanguage.current) : ''})</span
|
|
65
71
|
>
|
|
66
72
|
{/if}
|
|
67
73
|
|
|
68
|
-
{#if entryStatus === 'draft' &&
|
|
74
|
+
{#if entryStatus === 'draft' && draftVersion}
|
|
69
75
|
<span class="flex items-center gap-1">
|
|
70
76
|
<FileFilled />
|
|
71
|
-
{lang[interfaceLanguage.current].draft} (v{
|
|
77
|
+
{lang[interfaceLanguage.current].draft} (v{draftVersion.versionNumber})</span
|
|
72
78
|
>
|
|
73
79
|
{/if}
|
|
74
80
|
</div>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { DbEntryVersion, EntryStatus, EntryVersionStatus, RawEntry } from '../../../types/entries.js';
|
|
2
2
|
export declare function getEntryVersionStatus(version: DbEntryVersion): EntryVersionStatus;
|
|
3
|
+
/** Get entry status for a specific language */
|
|
4
|
+
export declare function getEntryStatusForLang(entry: RawEntry, lang: string): EntryStatus;
|
|
5
|
+
/** Get overall entry status — published if any lang is published */
|
|
3
6
|
export declare function getEntryStatus(entry: RawEntry): EntryStatus;
|