includio-cms 0.7.2 → 0.13.1
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 +128 -0
- package/ROADMAP.md +54 -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 +35 -13
- 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/client/users/users-page.svelte +5 -6
- package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
- package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
- package/dist/admin/components/fields/blocks-field.svelte +40 -19
- 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 +27 -16
- 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/layout/layout-renderer.svelte +10 -4
- package/dist/admin/components/media/file-preview.svelte +10 -1
- 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/files-list.svelte +12 -3
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +90 -16
- 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/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
- package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
- package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
- package/dist/admin/components/tiptap/slash-command.js +27 -23
- package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
- package/dist/admin/remote/email.remote.d.ts +1 -0
- package/dist/admin/remote/email.remote.js +5 -0
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +23 -28
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- 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/layout.d.ts +0 -1
- 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.13.1/index.d.ts +2 -0
- package/dist/updates/0.13.1/index.js +20 -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 +9 -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
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
emailConfigured?: boolean;
|
|
3
|
-
};
|
|
4
|
-
declare const UsersPage: import("svelte").Component<Props, {}, "">;
|
|
1
|
+
declare const UsersPage: import("svelte").Component<Record<string, never>, {}, "">;
|
|
5
2
|
type UsersPage = ReturnType<typeof UsersPage>;
|
|
6
3
|
export default UsersPage;
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
const interfaceLanguage = useInterfaceLanguage();
|
|
11
11
|
|
|
12
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
13
|
+
const pickerLang: Record<InterfaceLanguage, {
|
|
14
|
+
addBlock: string; chooseBlockType: string; searchBlocks: string; noBlocksFound: string;
|
|
15
|
+
}> = {
|
|
16
|
+
pl: { addBlock: 'Dodaj blok', chooseBlockType: 'Wybierz typ bloku', searchBlocks: 'Szukaj bloków...', noBlocksFound: 'Nie znaleziono bloków' },
|
|
17
|
+
en: { addBlock: 'Add block', chooseBlockType: 'Choose a block type to add', searchBlocks: 'Search blocks...', noBlocksFound: 'No blocks found' }
|
|
18
|
+
};
|
|
19
|
+
const pt = $derived(pickerLang[interfaceLanguage.current]);
|
|
20
|
+
|
|
12
21
|
type Props = {
|
|
13
22
|
open: boolean;
|
|
14
23
|
options: ObjectField[];
|
|
@@ -39,15 +48,15 @@
|
|
|
39
48
|
<Dialog.Root bind:open>
|
|
40
49
|
<Dialog.Content class="max-w-3xl">
|
|
41
50
|
<Dialog.Header>
|
|
42
|
-
<Dialog.Title>
|
|
43
|
-
<Dialog.Description>
|
|
51
|
+
<Dialog.Title>{pt.addBlock}</Dialog.Title>
|
|
52
|
+
<Dialog.Description>{pt.chooseBlockType}</Dialog.Description>
|
|
44
53
|
</Dialog.Header>
|
|
45
54
|
|
|
46
55
|
<div class="relative">
|
|
47
56
|
<SearchIcon class="text-muted-foreground absolute left-3 top-1/2 size-4 -translate-y-1/2" />
|
|
48
57
|
<Input
|
|
49
58
|
type="text"
|
|
50
|
-
placeholder=
|
|
59
|
+
placeholder={pt.searchBlocks}
|
|
51
60
|
class="pl-10"
|
|
52
61
|
bind:value={searchQuery}
|
|
53
62
|
/>
|
|
@@ -90,7 +99,7 @@
|
|
|
90
99
|
|
|
91
100
|
{#if filteredOptions.length === 0}
|
|
92
101
|
<div class="py-8 text-center">
|
|
93
|
-
<p class="text-muted-foreground">
|
|
102
|
+
<p class="text-muted-foreground">{pt.noBlocksFound}</p>
|
|
94
103
|
</div>
|
|
95
104
|
{/if}
|
|
96
105
|
</Dialog.Content>
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
const contentLanguage = getContentLanguage();
|
|
33
33
|
const interfaceLanguage = useInterfaceLanguage();
|
|
34
34
|
|
|
35
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
36
|
+
const blocksLang: Record<InterfaceLanguage, {
|
|
37
|
+
collapseAll: string; showAll: string; openMenu: string; duplicate: string;
|
|
38
|
+
moveUp: string; moveDown: string; delete: string; addBlock: string; elements: string;
|
|
39
|
+
}> = {
|
|
40
|
+
pl: { collapseAll: 'Zwiń wszystko', showAll: 'Rozwiń wszystko', openMenu: 'Otwórz menu', duplicate: 'Duplikuj', moveUp: 'Przenieś wyżej', moveDown: 'Przenieś niżej', delete: 'Usuń', addBlock: 'Dodaj blok', elements: 'elementów' },
|
|
41
|
+
en: { collapseAll: 'Collapse all', showAll: 'Show all', openMenu: 'Open menu', duplicate: 'Duplicate', moveUp: 'Move up', moveDown: 'Move down', delete: 'Delete', addBlock: 'Add block', elements: 'elements' }
|
|
42
|
+
};
|
|
43
|
+
const bt = $derived(blocksLang[interfaceLanguage.current]);
|
|
44
|
+
|
|
35
45
|
function generateId(): string {
|
|
36
46
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
37
47
|
return crypto.randomUUID();
|
|
@@ -66,8 +76,8 @@
|
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
$value = $value.reduce((acc: ObjectFieldData[], item: ObjectFieldData) => {
|
|
69
|
-
if (!item.
|
|
70
|
-
if (field.of.find((o) => o.slug === item.
|
|
79
|
+
if (!item._slug) return acc;
|
|
80
|
+
if (field.of.find((o) => o.slug === item._slug) === undefined) return acc;
|
|
71
81
|
acc.push(item);
|
|
72
82
|
return acc;
|
|
73
83
|
}, [] as ObjectFieldData[]);
|
|
@@ -81,7 +91,7 @@
|
|
|
81
91
|
if (defaults && defaults[i]) {
|
|
82
92
|
current.push({ _id: generateId(), ...JSON.parse(JSON.stringify(defaults[i])) });
|
|
83
93
|
} else {
|
|
84
|
-
current.push({ _id: generateId(),
|
|
94
|
+
current.push({ _id: generateId(), _slug: field.of[0].slug });
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
} else {
|
|
@@ -97,8 +107,7 @@
|
|
|
97
107
|
...($value ?? []),
|
|
98
108
|
{
|
|
99
109
|
_id: generateId(),
|
|
100
|
-
|
|
101
|
-
data: {}
|
|
110
|
+
_slug: field.slug
|
|
102
111
|
}
|
|
103
112
|
];
|
|
104
113
|
|
|
@@ -161,20 +170,32 @@
|
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
function getAccordionLabel(item: ObjectFieldData) {
|
|
164
|
-
const objectConfig = field.of.find((o) => o.slug === item.
|
|
173
|
+
const objectConfig = field.of.find((o) => o.slug === item._slug) as ObjectFieldType;
|
|
165
174
|
|
|
166
175
|
if (
|
|
167
176
|
objectConfig.accordionLabelField &&
|
|
168
177
|
typeof objectConfig.accordionLabelField === 'string' &&
|
|
169
178
|
objectConfig.accordionLabelField.trim().length > 0
|
|
170
179
|
) {
|
|
171
|
-
const label = item
|
|
180
|
+
const label = item[objectConfig.accordionLabelField] as string | Record<string, string>;
|
|
172
181
|
|
|
173
182
|
if (typeof label === 'string' && label.trim().length > 0) {
|
|
174
183
|
return `${label}`;
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
if (typeof label === 'object' && label !== null) {
|
|
187
|
+
// UrlFieldData — has `url` property
|
|
188
|
+
if ('url' in label) {
|
|
189
|
+
const urlData = label as { url: string | Record<string, string>; text?: string | Record<string, string> };
|
|
190
|
+
const displayValue = urlData.text || urlData.url;
|
|
191
|
+
if (typeof displayValue === 'string' && displayValue.trim().length > 0) {
|
|
192
|
+
return displayValue;
|
|
193
|
+
}
|
|
194
|
+
if (typeof displayValue === 'object' && displayValue !== null) {
|
|
195
|
+
return displayValue[contentLanguage.current] ?? '';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
178
199
|
const objectLabel = label as Record<string, string>;
|
|
179
200
|
return `${objectLabel[contentLanguage.current]}`;
|
|
180
201
|
}
|
|
@@ -230,7 +251,7 @@
|
|
|
230
251
|
>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel
|
|
231
252
|
>
|
|
232
253
|
{#if isFixedLength}
|
|
233
|
-
<span class="text-muted-foreground text-xs">{fixedCount}
|
|
254
|
+
<span class="text-muted-foreground text-xs">{fixedCount} {bt.elements}</span>
|
|
234
255
|
{:else if field.maxItems !== undefined}
|
|
235
256
|
<span class="text-xs {atMax ? 'text-destructive' : 'text-muted-foreground'}"
|
|
236
257
|
>{$value?.length ?? 0} / {field.maxItems}</span
|
|
@@ -245,7 +266,7 @@
|
|
|
245
266
|
variant="ghost"
|
|
246
267
|
onclick={() => {
|
|
247
268
|
accordionOpenState = [];
|
|
248
|
-
}}>
|
|
269
|
+
}}>{bt.collapseAll}</Button
|
|
249
270
|
>
|
|
250
271
|
<Button
|
|
251
272
|
size="sm"
|
|
@@ -255,7 +276,7 @@
|
|
|
255
276
|
if ($value) {
|
|
256
277
|
accordionOpenState = $value.map((_, i) => i.toString());
|
|
257
278
|
}
|
|
258
|
-
}}>
|
|
279
|
+
}}>{bt.showAll}</Button
|
|
259
280
|
>
|
|
260
281
|
</div>
|
|
261
282
|
</div>
|
|
@@ -294,9 +315,9 @@
|
|
|
294
315
|
animate:flip={{ duration: 200 }}
|
|
295
316
|
>
|
|
296
317
|
{#key index}
|
|
297
|
-
{#if $value[index].
|
|
318
|
+
{#if $value[index]._slug}
|
|
298
319
|
{@const item = $value[index]}
|
|
299
|
-
{@const objectField = field.of.find((option) => option.slug === item.
|
|
320
|
+
{@const objectField = field.of.find((option) => option.slug === item._slug)}
|
|
300
321
|
|
|
301
322
|
{#if objectField}
|
|
302
323
|
<Accordion.Item value={index.toString()} class="overflow-hidden rounded-md border-0" data-depth={depth + 1}>
|
|
@@ -335,7 +356,7 @@
|
|
|
335
356
|
{#snippet child({ props })}
|
|
336
357
|
<Button variant="ghost" size="icon" {...props}>
|
|
337
358
|
<DotsVerticalIcon />
|
|
338
|
-
<span class="sr-only">
|
|
359
|
+
<span class="sr-only">{bt.openMenu}</span>
|
|
339
360
|
</Button>
|
|
340
361
|
{/snippet}
|
|
341
362
|
</DropdownMenu.Trigger>
|
|
@@ -345,27 +366,27 @@
|
|
|
345
366
|
onclick={() => duplicateItem(index)}
|
|
346
367
|
disabled={atMax}
|
|
347
368
|
>
|
|
348
|
-
|
|
369
|
+
{bt.duplicate}
|
|
349
370
|
</DropdownMenu.Item>
|
|
350
371
|
{/if}
|
|
351
372
|
<DropdownMenu.Item
|
|
352
373
|
onclick={() => moveItemUp(index)}
|
|
353
374
|
disabled={index === 0}
|
|
354
375
|
>
|
|
355
|
-
|
|
376
|
+
{bt.moveUp}
|
|
356
377
|
</DropdownMenu.Item>
|
|
357
378
|
<DropdownMenu.Item
|
|
358
379
|
onclick={() => moveItemDown(index)}
|
|
359
380
|
disabled={$value && index === $value.length - 1}
|
|
360
381
|
>
|
|
361
|
-
|
|
382
|
+
{bt.moveDown}
|
|
362
383
|
</DropdownMenu.Item>
|
|
363
384
|
{#if !isFixedLength}
|
|
364
385
|
<DropdownMenu.Item
|
|
365
386
|
variant="destructive"
|
|
366
387
|
onclick={() => removeItem(index)}
|
|
367
388
|
>
|
|
368
|
-
|
|
389
|
+
{bt.delete}
|
|
369
390
|
</DropdownMenu.Item>
|
|
370
391
|
{/if}
|
|
371
392
|
</DropdownMenu.Content>
|
|
@@ -391,7 +412,7 @@
|
|
|
391
412
|
{:else}
|
|
392
413
|
<p class="text-red-500">
|
|
393
414
|
Invalid field configuration. Unknown slug:
|
|
394
|
-
{$value[index].
|
|
415
|
+
{$value[index]._slug}
|
|
395
416
|
</p>
|
|
396
417
|
{/if}
|
|
397
418
|
{:else}
|
|
@@ -414,7 +435,7 @@
|
|
|
414
435
|
onclick={() => (blockPickerOpen = true)}
|
|
415
436
|
>
|
|
416
437
|
<CirclePlus />
|
|
417
|
-
|
|
438
|
+
{bt.addBlock}
|
|
418
439
|
</Button>
|
|
419
440
|
</div>
|
|
420
441
|
<BlockPickerModal
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
const fieldsWithNoDescription: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
|
|
42
42
|
const fieldsWithNoLabel: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
|
|
43
43
|
|
|
44
|
-
const fieldsWithAlternativeDescription: FieldType[] = ['
|
|
44
|
+
const fieldsWithAlternativeDescription: FieldType[] = ['media', 'object', 'blocks'];
|
|
45
45
|
|
|
46
|
-
function isTextField(field: Field): field is Extract<Field, { type: 'text' | '
|
|
47
|
-
return ['text', '
|
|
46
|
+
function isTextField(field: Field): field is Extract<Field, { type: 'text' | 'content' }> {
|
|
47
|
+
return ['text', 'content'].includes(field.type);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function isRadioField(field: Field): field is Extract<Field, { type: 'radio' }> {
|
|
@@ -121,11 +121,7 @@
|
|
|
121
121
|
<Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
|
|
122
122
|
{/if}
|
|
123
123
|
|
|
124
|
-
{#if field.type === '
|
|
125
|
-
{#await import('./image-field.svelte') then { default: ImageField }}
|
|
126
|
-
<ImageField {field} bind:value={$value} />
|
|
127
|
-
{/await}
|
|
128
|
-
{:else if field.type === 'media'}
|
|
124
|
+
{#if field.type === 'media'}
|
|
129
125
|
{#await import('./media-field.svelte') then { default: MediaField }}
|
|
130
126
|
<MediaField {field} bind:value={$value} />
|
|
131
127
|
{/await}
|
|
@@ -32,21 +32,16 @@
|
|
|
32
32
|
|
|
33
33
|
onMount(() => {
|
|
34
34
|
if (!$value) {
|
|
35
|
-
$value = {
|
|
35
|
+
$value = { _slug: field.slug };
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (!$value.
|
|
40
|
-
$value.
|
|
39
|
+
if (!$value._slug) {
|
|
40
|
+
$value._slug = field.slug;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
if ($value.
|
|
44
|
-
$value
|
|
45
|
-
$value.data = {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (typeof $value.data !== 'object' || $value.data === null) {
|
|
49
|
-
$value.data = {};
|
|
43
|
+
if ($value._slug !== field.slug) {
|
|
44
|
+
$value = { _slug: field.slug };
|
|
50
45
|
}
|
|
51
46
|
});
|
|
52
47
|
|
|
@@ -59,8 +54,8 @@
|
|
|
59
54
|
{#snippet content()}
|
|
60
55
|
<div class="space-y-4">
|
|
61
56
|
{#each field.fields as f}
|
|
62
|
-
{#if evaluateCondition(f.showWhen, (slug) => $value?.
|
|
63
|
-
{@const fieldPath = joinPath(path,
|
|
57
|
+
{#if evaluateCondition(f.showWhen, (slug) => $value?.[slug] ?? $formData[slug])}
|
|
58
|
+
{@const fieldPath = joinPath(path, f.slug)}
|
|
64
59
|
{@const showFlash = isFlashing(fieldPath)}
|
|
65
60
|
<div
|
|
66
61
|
data-field-path={fieldPath}
|
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
onMount(() => {
|
|
33
|
-
if (
|
|
34
|
-
value
|
|
33
|
+
if (field.multiple) {
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
value = [];
|
|
36
|
+
} else if (typeof value === 'string') {
|
|
37
|
+
value = value ? [value] : [];
|
|
38
|
+
}
|
|
39
|
+
} else if (value === undefined) {
|
|
40
|
+
value = '';
|
|
35
41
|
}
|
|
36
42
|
});
|
|
37
43
|
</script>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { getAtPath, setAtPath } from '../../utils/objectPath.js';
|
|
6
6
|
import type {
|
|
7
7
|
Field,
|
|
8
|
-
|
|
8
|
+
MediaField,
|
|
9
9
|
SeoField,
|
|
10
10
|
SeoFieldData,
|
|
11
11
|
TextField
|
|
@@ -13,12 +13,10 @@
|
|
|
13
13
|
import { untrack } from 'svelte';
|
|
14
14
|
import slugify from '../../imports/slugify.js';
|
|
15
15
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
16
|
-
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
17
16
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
18
17
|
import { Switch } from '../../../components/ui/switch/index.js';
|
|
19
18
|
|
|
20
19
|
const interfaceLanguage = useInterfaceLanguage();
|
|
21
|
-
const contentLanguage = getContentLanguage();
|
|
22
20
|
|
|
23
21
|
type Props = {
|
|
24
22
|
field: SeoField;
|
|
@@ -124,11 +122,12 @@
|
|
|
124
122
|
multiline: true
|
|
125
123
|
};
|
|
126
124
|
|
|
127
|
-
const ogImageField:
|
|
128
|
-
type: '
|
|
125
|
+
const ogImageField: MediaField = {
|
|
126
|
+
type: 'media',
|
|
129
127
|
slug: 'ogImage',
|
|
130
128
|
label: labels.ogImage.label,
|
|
131
129
|
description: labels.ogImage.description,
|
|
130
|
+
accept: 'image/*',
|
|
132
131
|
required: false
|
|
133
132
|
};
|
|
134
133
|
|
|
@@ -150,29 +149,19 @@
|
|
|
150
149
|
customCodeField
|
|
151
150
|
];
|
|
152
151
|
|
|
153
|
-
// Helper: read a value from formData, handling both localized (object) and plain (string) values
|
|
154
|
-
function readSourceValue(sourcePath: string): string | undefined {
|
|
155
|
-
const raw = ($formData as Record<string, unknown>)[sourcePath];
|
|
156
|
-
if (typeof raw === 'string') return raw;
|
|
157
|
-
if (raw && typeof raw === 'object') {
|
|
158
|
-
return (raw as Record<string, string>)[contentLanguage.current];
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
152
|
// Helper: read value at a dot-path from formData
|
|
164
153
|
function readPath(dotPath: string): string | undefined {
|
|
165
154
|
const val = getAtPath($formData as Record<string, unknown>, dotPath);
|
|
166
155
|
return typeof val === 'string' ? val : undefined;
|
|
167
156
|
}
|
|
168
157
|
|
|
169
|
-
// Character count
|
|
158
|
+
// Character count — data is flat, no lang nesting
|
|
170
159
|
let titleLength = $derived.by(() => {
|
|
171
|
-
const val = readPath(joinPath(String(path), 'title'
|
|
160
|
+
const val = readPath(joinPath(String(path), 'title'));
|
|
172
161
|
return val?.length ?? 0;
|
|
173
162
|
});
|
|
174
163
|
let descLength = $derived.by(() => {
|
|
175
|
-
const val = readPath(joinPath(String(path), 'description'
|
|
164
|
+
const val = readPath(joinPath(String(path), 'description'));
|
|
176
165
|
return val?.length ?? 0;
|
|
177
166
|
});
|
|
178
167
|
|
|
@@ -183,109 +172,67 @@
|
|
|
183
172
|
return 'text-destructive';
|
|
184
173
|
}
|
|
185
174
|
|
|
186
|
-
// Auto-gen: track last auto-generated
|
|
187
|
-
let
|
|
188
|
-
let
|
|
175
|
+
// Auto-gen: track last auto-generated value
|
|
176
|
+
let lastAutoSlug = '';
|
|
177
|
+
let lastAutoTitle = '';
|
|
189
178
|
|
|
190
|
-
// Auto slug toggle
|
|
191
|
-
// Initializes to false if existing slug differs from what auto-gen would produce
|
|
179
|
+
// Auto slug toggle
|
|
192
180
|
let autoSlug = $state((() => {
|
|
193
181
|
if (!field.slugSource) return false;
|
|
194
182
|
const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
|
|
195
|
-
if (!sourceRaw) return true;
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
|
|
201
|
-
: [];
|
|
202
|
-
for (const [lang, text] of pairs) {
|
|
203
|
-
const targetPath = joinPath(String(path), 'slug', lang);
|
|
204
|
-
const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
|
|
205
|
-
const expected = slugify(String(text), { lower: true, strict: true, trim: true });
|
|
206
|
-
if (current != null && current !== expected) return false;
|
|
207
|
-
}
|
|
183
|
+
if (!sourceRaw || typeof sourceRaw !== 'string') return true;
|
|
184
|
+
const slugPath = joinPath(String(path), 'slug');
|
|
185
|
+
const current = getAtPath($formData as Record<string, unknown>, slugPath) as string | undefined;
|
|
186
|
+
const expected = slugify(sourceRaw, { lower: true, strict: true, trim: true });
|
|
187
|
+
if (current != null && current !== expected) return false;
|
|
208
188
|
return true;
|
|
209
189
|
})());
|
|
210
190
|
|
|
211
|
-
// slugSource → auto-gen seo.slug per
|
|
191
|
+
// slugSource → auto-gen seo.slug (flat, no per-lang)
|
|
212
192
|
$effect(() => {
|
|
213
193
|
if (!field.slugSource || !autoSlug) return;
|
|
214
194
|
const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
|
|
215
|
-
if (!sourceRaw) return;
|
|
195
|
+
if (!sourceRaw || typeof sourceRaw !== 'string') return;
|
|
216
196
|
|
|
217
197
|
untrack(() => {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
let changed = false;
|
|
226
|
-
for (const [lang, text] of pairs) {
|
|
227
|
-
const targetPath = joinPath(String(path), 'slug', lang);
|
|
228
|
-
const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
|
|
229
|
-
const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
|
|
230
|
-
if (newSlug !== current) {
|
|
231
|
-
setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
|
|
232
|
-
changed = true;
|
|
233
|
-
}
|
|
234
|
-
lastAutoSlugs[lang] = newSlug;
|
|
198
|
+
const slugPath = joinPath(String(path), 'slug');
|
|
199
|
+
const current = getAtPath($formData as Record<string, unknown>, slugPath) as string | undefined;
|
|
200
|
+
const newSlug = slugify(sourceRaw, { lower: true, strict: true, trim: true });
|
|
201
|
+
if (newSlug !== current) {
|
|
202
|
+
setAtPath($formData as Record<string, unknown>, slugPath, newSlug);
|
|
203
|
+
$formData = $formData;
|
|
235
204
|
}
|
|
236
|
-
|
|
205
|
+
lastAutoSlug = newSlug;
|
|
237
206
|
});
|
|
238
207
|
});
|
|
239
208
|
|
|
240
209
|
function onAutoSlugToggle(checked: boolean) {
|
|
241
210
|
if (!checked || !field.slugSource) return;
|
|
242
|
-
// Regenerate slug from current source when toggling ON
|
|
243
211
|
const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
|
|
244
|
-
if (!sourceRaw) return;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
: [];
|
|
251
|
-
let changed = false;
|
|
252
|
-
for (const [lang, text] of pairs) {
|
|
253
|
-
const targetPath = joinPath(String(path), 'slug', lang);
|
|
254
|
-
const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
|
|
255
|
-
setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
|
|
256
|
-
lastAutoSlugs[lang] = newSlug;
|
|
257
|
-
changed = true;
|
|
258
|
-
}
|
|
259
|
-
if (changed) $formData = $formData;
|
|
212
|
+
if (!sourceRaw || typeof sourceRaw !== 'string') return;
|
|
213
|
+
const slugPath = joinPath(String(path), 'slug');
|
|
214
|
+
const newSlug = slugify(sourceRaw, { lower: true, strict: true, trim: true });
|
|
215
|
+
setAtPath($formData as Record<string, unknown>, slugPath, newSlug);
|
|
216
|
+
lastAutoSlug = newSlug;
|
|
217
|
+
$formData = $formData;
|
|
260
218
|
}
|
|
261
219
|
|
|
262
|
-
// titleSource → auto-fill seo.title
|
|
220
|
+
// titleSource → auto-fill seo.title (flat)
|
|
263
221
|
$effect(() => {
|
|
264
222
|
if (!field.titleSource) return;
|
|
265
223
|
const sourceRaw = ($formData as Record<string, unknown>)[field.titleSource];
|
|
266
|
-
if (!sourceRaw) return;
|
|
224
|
+
if (!sourceRaw || typeof sourceRaw !== 'string') return;
|
|
267
225
|
|
|
268
226
|
untrack(() => {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
let changed = false;
|
|
277
|
-
for (const [lang, text] of pairs) {
|
|
278
|
-
const targetPath = joinPath(String(path), 'title', lang);
|
|
279
|
-
const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
|
|
280
|
-
if (!current || current === lastAutoTitles[lang]) {
|
|
281
|
-
if (text !== current) {
|
|
282
|
-
setAtPath($formData as Record<string, unknown>, targetPath, text);
|
|
283
|
-
changed = true;
|
|
284
|
-
}
|
|
285
|
-
lastAutoTitles[lang] = text;
|
|
227
|
+
const titlePath = joinPath(String(path), 'title');
|
|
228
|
+
const current = getAtPath($formData as Record<string, unknown>, titlePath) as string | undefined;
|
|
229
|
+
if (!current || current === lastAutoTitle) {
|
|
230
|
+
if (sourceRaw !== current) {
|
|
231
|
+
setAtPath($formData as Record<string, unknown>, titlePath, sourceRaw);
|
|
232
|
+
$formData = $formData;
|
|
286
233
|
}
|
|
234
|
+
lastAutoTitle = sourceRaw;
|
|
287
235
|
}
|
|
288
|
-
if (changed) $formData = $formData;
|
|
289
236
|
});
|
|
290
237
|
});
|
|
291
238
|
</script>
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
const contentLanguage = getContentLanguage();
|
|
12
12
|
const interfaceLanguage = useInterfaceLanguage();
|
|
13
13
|
|
|
14
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
15
|
+
const arrayLang: Record<InterfaceLanguage, {
|
|
16
|
+
typeAndEnter: string; add: string; typeNumber: string; addLink: string;
|
|
17
|
+
urlPlaceholder: string; linkText: string; newTab: string;
|
|
18
|
+
removeItem: string; removeLink: string;
|
|
19
|
+
}> = {
|
|
20
|
+
pl: { typeAndEnter: 'Wpisz i naciśnij Enter...', add: 'Dodaj', typeNumber: 'Wpisz liczbę...', addLink: 'Dodaj link', urlPlaceholder: 'URL...', linkText: 'Tekst linku...', newTab: 'Nowa karta', removeItem: 'Usuń element', removeLink: 'Usuń link' },
|
|
21
|
+
en: { typeAndEnter: 'Type and press Enter...', add: 'Add', typeNumber: 'Enter a number...', addLink: 'Add link', urlPlaceholder: 'URL...', linkText: 'Link text...', newTab: 'New tab', removeItem: 'Remove item', removeLink: 'Remove link' }
|
|
22
|
+
};
|
|
23
|
+
const at = $derived(arrayLang[interfaceLanguage.current]);
|
|
24
|
+
|
|
14
25
|
type Props = {
|
|
15
26
|
field: ArrayField;
|
|
16
27
|
value: unknown[] | undefined;
|
|
@@ -68,7 +79,7 @@
|
|
|
68
79
|
// --- URL ---
|
|
69
80
|
function addUrlItem() {
|
|
70
81
|
if (atMax) return;
|
|
71
|
-
const item: UrlFieldData = { url:
|
|
82
|
+
const item: UrlFieldData = { url: '', text: '', newTab: false };
|
|
72
83
|
value = [...(value ?? []), item];
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -122,7 +133,7 @@
|
|
|
122
133
|
type="button"
|
|
123
134
|
class="text-[#555566] hover:text-[#C44B4B] transition-colors"
|
|
124
135
|
onclick={() => removeItem(index)}
|
|
125
|
-
aria-label=
|
|
136
|
+
aria-label={at.removeItem}
|
|
126
137
|
>
|
|
127
138
|
<X class="h-3.5 w-3.5" />
|
|
128
139
|
</button>
|
|
@@ -134,7 +145,7 @@
|
|
|
134
145
|
<div class="flex items-center gap-2">
|
|
135
146
|
<Input
|
|
136
147
|
type="text"
|
|
137
|
-
placeholder=
|
|
148
|
+
placeholder={at.typeAndEnter}
|
|
138
149
|
bind:value={textInput}
|
|
139
150
|
onkeydown={handleTextKeydown}
|
|
140
151
|
disabled={atMax}
|
|
@@ -142,7 +153,7 @@
|
|
|
142
153
|
/>
|
|
143
154
|
<Button size="sm" type="button" variant="outline" disabled={atMax || !textInput.trim()} onclick={addTextItem}>
|
|
144
155
|
<CirclePlus class="h-4 w-4" />
|
|
145
|
-
|
|
156
|
+
{at.add}
|
|
146
157
|
</Button>
|
|
147
158
|
</div>
|
|
148
159
|
|
|
@@ -167,7 +178,7 @@
|
|
|
167
178
|
type="button"
|
|
168
179
|
class="text-[#555566] hover:text-[#C44B4B] transition-colors"
|
|
169
180
|
onclick={() => removeItem(index)}
|
|
170
|
-
aria-label=
|
|
181
|
+
aria-label={at.removeItem}
|
|
171
182
|
>
|
|
172
183
|
<X class="h-3.5 w-3.5" />
|
|
173
184
|
</button>
|
|
@@ -179,7 +190,7 @@
|
|
|
179
190
|
<div class="flex items-center gap-2">
|
|
180
191
|
<Input
|
|
181
192
|
type="number"
|
|
182
|
-
placeholder=
|
|
193
|
+
placeholder={at.typeNumber}
|
|
183
194
|
bind:value={numberInput}
|
|
184
195
|
onkeydown={handleNumberKeydown}
|
|
185
196
|
disabled={atMax}
|
|
@@ -187,7 +198,7 @@
|
|
|
187
198
|
/>
|
|
188
199
|
<Button size="sm" type="button" variant="outline" disabled={atMax || numberInput === ''} onclick={addNumberItem}>
|
|
189
200
|
<CirclePlus class="h-4 w-4" />
|
|
190
|
-
|
|
201
|
+
{at.add}
|
|
191
202
|
</Button>
|
|
192
203
|
</div>
|
|
193
204
|
|
|
@@ -209,11 +220,11 @@
|
|
|
209
220
|
<div class="flex-1 space-y-2">
|
|
210
221
|
<Input
|
|
211
222
|
type="url"
|
|
212
|
-
placeholder=
|
|
213
|
-
value={urlItem.url
|
|
223
|
+
placeholder={at.urlPlaceholder}
|
|
224
|
+
value={typeof urlItem.url === 'string' ? urlItem.url : ''}
|
|
214
225
|
oninput={(e) => {
|
|
215
226
|
const val = e.currentTarget.value;
|
|
216
|
-
const updated = { ...urlItem, url:
|
|
227
|
+
const updated = { ...urlItem, url: val };
|
|
217
228
|
const arr = [...(value ?? [])];
|
|
218
229
|
arr[index] = updated;
|
|
219
230
|
value = arr;
|
|
@@ -222,12 +233,12 @@
|
|
|
222
233
|
<div class="flex items-center gap-2">
|
|
223
234
|
<Input
|
|
224
235
|
type="text"
|
|
225
|
-
placeholder=
|
|
236
|
+
placeholder={at.linkText}
|
|
226
237
|
class="flex-1"
|
|
227
|
-
value={urlItem.text
|
|
238
|
+
value={typeof urlItem.text === 'string' ? urlItem.text : ''}
|
|
228
239
|
oninput={(e) => {
|
|
229
240
|
const val = e.currentTarget.value;
|
|
230
|
-
const updated = { ...urlItem, text:
|
|
241
|
+
const updated = { ...urlItem, text: val };
|
|
231
242
|
const arr = [...(value ?? [])];
|
|
232
243
|
arr[index] = updated;
|
|
233
244
|
value = arr;
|
|
@@ -245,7 +256,7 @@
|
|
|
245
256
|
}}
|
|
246
257
|
class="accent-[#5B4A9E]"
|
|
247
258
|
/>
|
|
248
|
-
|
|
259
|
+
{at.newTab}
|
|
249
260
|
</label>
|
|
250
261
|
</div>
|
|
251
262
|
</div>
|
|
@@ -253,7 +264,7 @@
|
|
|
253
264
|
type="button"
|
|
254
265
|
class="mt-1.5 text-[#555566] hover:text-[#C44B4B] transition-colors"
|
|
255
266
|
onclick={() => removeItem(index)}
|
|
256
|
-
aria-label=
|
|
267
|
+
aria-label={at.removeLink}
|
|
257
268
|
>
|
|
258
269
|
<X class="h-4 w-4" />
|
|
259
270
|
</button>
|
|
@@ -264,7 +275,7 @@
|
|
|
264
275
|
|
|
265
276
|
<Button size="sm" type="button" variant="outline" disabled={atMax} onclick={addUrlItem}>
|
|
266
277
|
<CirclePlus class="h-4 w-4" />
|
|
267
|
-
|
|
278
|
+
{at.addLink}
|
|
268
279
|
</Button>
|
|
269
280
|
|
|
270
281
|
{#if field.maxItems !== undefined}
|