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
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
// --- URL ---
|
|
69
69
|
function addUrlItem() {
|
|
70
70
|
if (atMax) return;
|
|
71
|
-
const item: UrlFieldData = { url:
|
|
71
|
+
const item: UrlFieldData = { url: '', text: '', newTab: false };
|
|
72
72
|
value = [...(value ?? []), item];
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -210,10 +210,10 @@
|
|
|
210
210
|
<Input
|
|
211
211
|
type="url"
|
|
212
212
|
placeholder="URL..."
|
|
213
|
-
value={urlItem.url
|
|
213
|
+
value={typeof urlItem.url === 'string' ? urlItem.url : ''}
|
|
214
214
|
oninput={(e) => {
|
|
215
215
|
const val = e.currentTarget.value;
|
|
216
|
-
const updated = { ...urlItem, url:
|
|
216
|
+
const updated = { ...urlItem, url: val };
|
|
217
217
|
const arr = [...(value ?? [])];
|
|
218
218
|
arr[index] = updated;
|
|
219
219
|
value = arr;
|
|
@@ -224,10 +224,10 @@
|
|
|
224
224
|
type="text"
|
|
225
225
|
placeholder="Tekst linku..."
|
|
226
226
|
class="flex-1"
|
|
227
|
-
value={urlItem.text
|
|
227
|
+
value={typeof urlItem.text === 'string' ? urlItem.text : ''}
|
|
228
228
|
oninput={(e) => {
|
|
229
229
|
const val = e.currentTarget.value;
|
|
230
|
-
const updated = { ...urlItem, text:
|
|
230
|
+
const updated = { ...urlItem, text: val };
|
|
231
231
|
const arr = [...(value ?? [])];
|
|
232
232
|
arr[index] = updated;
|
|
233
233
|
value = arr;
|
|
@@ -4,30 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
<script lang="ts" generics="T extends Record<string, unknown>">
|
|
6
6
|
import type {
|
|
7
|
-
RichtextField as RichtextFieldType,
|
|
8
7
|
ContentField as ContentFieldType,
|
|
9
8
|
TextField as TextFieldType
|
|
10
9
|
} from '../../../types/fields.js';
|
|
11
|
-
import * as Tabs from '../../../components/ui/tabs/index.js';
|
|
12
10
|
import * as Form from '../../../components/ui/form/index.js';
|
|
13
11
|
import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
|
|
14
12
|
import TextField from './text-field.svelte';
|
|
15
|
-
import { joinPath, setAtPath } from '../../utils/objectPath.js';
|
|
16
|
-
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
17
13
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
18
|
-
import { getContext } from 'svelte';
|
|
19
14
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
20
15
|
import RequiredLabel from './required-label.svelte';
|
|
21
|
-
import ClipboardIcon from '@tabler/icons-svelte/icons/clipboard';
|
|
22
|
-
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
23
16
|
import FieldValueBridge from './field-value-bridge.svelte';
|
|
24
17
|
|
|
25
|
-
const contentLanguage = getContentLanguage();
|
|
26
18
|
const interfaceLanguage = useInterfaceLanguage();
|
|
27
|
-
const inInlineBlock = getContext<boolean>('inInlineBlock') ?? false;
|
|
28
19
|
|
|
29
20
|
type Props = {
|
|
30
|
-
field: TextFieldType |
|
|
21
|
+
field: TextFieldType | ContentFieldType;
|
|
31
22
|
form: SuperForm<T>;
|
|
32
23
|
path: FormPathLeaves<T, string | undefined>;
|
|
33
24
|
};
|
|
@@ -40,75 +31,11 @@
|
|
|
40
31
|
const hasConstraints = $derived(
|
|
41
32
|
isText && (field.minLength !== undefined || field.maxLength !== undefined || field.pattern !== undefined)
|
|
42
33
|
);
|
|
43
|
-
const isMultiLang = $derived(contentLanguage.all.length > 1);
|
|
44
|
-
const defaultLang = $derived(contentLanguage.all[0]);
|
|
45
|
-
|
|
46
|
-
const copyLang: Record<InterfaceLanguage, { copyFrom: (lang: string) => string }> = {
|
|
47
|
-
en: { copyFrom: (lang: string) => `Copy from ${lang.toUpperCase()}` },
|
|
48
|
-
pl: { copyFrom: (lang: string) => `Kopiuj z ${lang.toUpperCase()}` }
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const refLang: Record<InterfaceLanguage, { reference: (lang: string) => string }> = {
|
|
52
|
-
en: { reference: (lang: string) => `${lang.toUpperCase()} (reference)` },
|
|
53
|
-
pl: { reference: (lang: string) => `${lang.toUpperCase()} (referencja)` }
|
|
54
|
-
};
|
|
55
34
|
|
|
56
35
|
function resolvePathValue(data: Record<string, unknown>, dotPath: string): unknown {
|
|
57
36
|
return dotPath.split('.').reduce<unknown>((obj, key) => (obj as Record<string, unknown>)?.[key], data);
|
|
58
37
|
}
|
|
59
38
|
|
|
60
|
-
function isValueEmpty(value: unknown): boolean {
|
|
61
|
-
if (value == null) return true;
|
|
62
|
-
if (typeof value === 'string') return value.length === 0;
|
|
63
|
-
if (typeof value === 'object' && 'type' in (value as Record<string, unknown>)) {
|
|
64
|
-
const doc = value as { type: string; content?: unknown[] };
|
|
65
|
-
return doc.type === 'doc' && (!doc.content || doc.content.length === 0);
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function getSourceValue(lang: string): unknown {
|
|
71
|
-
return resolvePathValue($formData, joinPath(path, lang));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function findSourceLang(currentLang: string): string | null {
|
|
75
|
-
for (const l of contentLanguage.all) {
|
|
76
|
-
if (l === currentLang) continue;
|
|
77
|
-
const val = getSourceValue(l);
|
|
78
|
-
if (!isValueEmpty(val)) return l;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function copyFrom(sourceLang: string, targetLang: string) {
|
|
84
|
-
const sourceVal = getSourceValue(sourceLang);
|
|
85
|
-
if (sourceVal == null) return;
|
|
86
|
-
|
|
87
|
-
const targetPath = joinPath(path, targetLang);
|
|
88
|
-
const value =
|
|
89
|
-
field.type === 'content' && typeof sourceVal === 'object'
|
|
90
|
-
? structuredClone(sourceVal)
|
|
91
|
-
: sourceVal;
|
|
92
|
-
setAtPath($formData as Record<string, unknown>, targetPath, value);
|
|
93
|
-
$formData = $formData;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getReferenceText(lang: string): string {
|
|
97
|
-
const val = getSourceValue(lang);
|
|
98
|
-
if (typeof val === 'string') return val;
|
|
99
|
-
if (val && typeof val === 'object' && 'type' in (val as Record<string, unknown>)) {
|
|
100
|
-
return interfaceLanguage.current === 'pl'
|
|
101
|
-
? '(treść w edytorze)'
|
|
102
|
-
: '(editor content)';
|
|
103
|
-
}
|
|
104
|
-
return '';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getLangFillStatus(lang: string): boolean {
|
|
108
|
-
const val = getSourceValue(lang);
|
|
109
|
-
return !isValueEmpty(val);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
39
|
function constraintHint(): string {
|
|
113
40
|
if (field.type !== 'text') return '';
|
|
114
41
|
const parts: string[] = [];
|
|
@@ -122,137 +49,65 @@
|
|
|
122
49
|
return parts.join('');
|
|
123
50
|
}
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
const parts = contentLanguage.all.map((l) => {
|
|
127
|
-
const filled = getLangFillStatus(l);
|
|
128
|
-
const statusText =
|
|
129
|
-
interfaceLanguage.current === 'pl'
|
|
130
|
-
? filled ? 'uzupełnione' : 'puste'
|
|
131
|
-
: filled ? 'filled' : 'empty';
|
|
132
|
-
return `${l.toUpperCase()}: ${statusText}`;
|
|
133
|
-
});
|
|
134
|
-
return parts.join(', ');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Lazy-load richtext and content field components
|
|
138
|
-
let RichtextField: typeof TextField | null = $state(null);
|
|
52
|
+
// Lazy-load content field component
|
|
139
53
|
let ContentField: typeof TextField | null = $state(null);
|
|
140
54
|
|
|
141
55
|
$effect(() => {
|
|
142
|
-
if (field.type === 'richtext' && !RichtextField) {
|
|
143
|
-
import('./richtext-field.svelte').then((m) => { RichtextField = m.default; });
|
|
144
|
-
}
|
|
145
56
|
if (field.type === 'content' && !ContentField) {
|
|
146
57
|
import('./content-field.svelte').then((m) => { ContentField = m.default; });
|
|
147
58
|
}
|
|
148
59
|
});
|
|
149
60
|
</script>
|
|
150
61
|
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
{#if contentLanguage.referenceMode && isMultiLang && !inInlineBlock}
|
|
182
|
-
{@const refSource = findSourceLang(lang)}
|
|
183
|
-
{#if refSource}
|
|
184
|
-
{@const refText = getReferenceText(refSource)}
|
|
185
|
-
{#if refText}
|
|
186
|
-
<div class="mb-2 rounded-lg border-l-2 border-[var(--primary)]/20 bg-[var(--muted)]/50 p-3 text-sm text-[var(--muted-foreground)]">
|
|
187
|
-
<div class="mb-1 text-xs font-semibold text-[var(--text-light)]">{refLang[interfaceLanguage.current].reference(refSource)}</div>
|
|
188
|
-
<div class="whitespace-pre-wrap">{refText}</div>
|
|
189
|
-
</div>
|
|
190
|
-
{/if}
|
|
191
|
-
{/if}
|
|
192
|
-
{/if}
|
|
193
|
-
|
|
194
|
-
{#if field.type === 'text'}
|
|
195
|
-
<FieldValueBridge {form} path={joinPath(path, lang) as FormPathLeaves<T>} component={TextField} {field} {...props} />
|
|
196
|
-
{:else if field.type === 'richtext' && RichtextField}
|
|
197
|
-
<FieldValueBridge {form} path={joinPath(path, lang) as FormPathLeaves<T>} component={RichtextField} {field} {...props} />
|
|
198
|
-
{:else if field.type === 'content' && ContentField}
|
|
199
|
-
<FieldValueBridge {form} path={joinPath(path, lang) as FormPathLeaves<T>} component={ContentField} {field} {...props} />
|
|
200
|
-
{:else}
|
|
201
|
-
<div class="h-32 animate-pulse rounded-md bg-accent"></div>
|
|
202
|
-
{/if}
|
|
203
|
-
|
|
204
|
-
{#if isMultiLang && !inInlineBlock}
|
|
205
|
-
{@const currentVal = resolvePathValue($formData, joinPath(path, lang))}
|
|
206
|
-
{@const sourceLang = isValueEmpty(currentVal) ? findSourceLang(lang) : null}
|
|
207
|
-
{#if sourceLang}
|
|
208
|
-
<button
|
|
209
|
-
type="button"
|
|
210
|
-
class="mt-1 inline-flex items-center gap-1 text-xs text-[var(--muted-foreground)] transition-colors hover:text-[var(--primary)]"
|
|
211
|
-
onclick={() => copyFrom(sourceLang, lang)}
|
|
212
|
-
>
|
|
213
|
-
<ClipboardIcon class="size-3" />
|
|
214
|
-
{copyLang[interfaceLanguage.current].copyFrom(sourceLang)}
|
|
215
|
-
</button>
|
|
216
|
-
{/if}
|
|
217
|
-
{/if}
|
|
218
|
-
{/snippet}
|
|
219
|
-
</Form.Control>
|
|
220
|
-
|
|
221
|
-
{#if isText}
|
|
222
|
-
{@const val = resolvePathValue($formData, joinPath(path, lang))}
|
|
223
|
-
{@const charCount = typeof val === 'string' ? val.length : 0}
|
|
224
|
-
{@const atLimit = field.type === 'text' && field.maxLength !== undefined && charCount >= field.maxLength}
|
|
225
|
-
<div class="flex items-start justify-between gap-4">
|
|
226
|
-
{#if field.description || hasConstraints}
|
|
227
|
-
<Form.Description class="flex-1">
|
|
228
|
-
{#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
|
|
229
|
-
{#if hasConstraints}
|
|
230
|
-
{#if field.description && constraintHint()}<br />{/if}
|
|
231
|
-
{#if constraintHint()}{constraintHint()}{/if}
|
|
232
|
-
{#if field.type === 'text' && field.pattern}
|
|
233
|
-
{#if constraintHint()} · {/if}Format: <code class="text-xs bg-muted px-1 py-0.5 rounded">{field.pattern}</code>
|
|
234
|
-
{/if}
|
|
235
|
-
{/if}
|
|
236
|
-
</Form.Description>
|
|
237
|
-
{:else}
|
|
238
|
-
<div></div>
|
|
62
|
+
<Form.Field {form} name={path as FormPathLeaves<T, string | undefined>}>
|
|
63
|
+
<Form.Control>
|
|
64
|
+
{#snippet children({ props })}
|
|
65
|
+
{#if field.label}
|
|
66
|
+
<RequiredLabel required={field.required}>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel>
|
|
67
|
+
{/if}
|
|
68
|
+
|
|
69
|
+
{#if field.type === 'text'}
|
|
70
|
+
<FieldValueBridge {form} {path} component={TextField} {field} {...props} />
|
|
71
|
+
{:else if field.type === 'content' && ContentField}
|
|
72
|
+
<FieldValueBridge {form} {path} component={ContentField} {field} {...props} />
|
|
73
|
+
{:else}
|
|
74
|
+
<div class="h-32 animate-pulse rounded-md bg-accent"></div>
|
|
75
|
+
{/if}
|
|
76
|
+
{/snippet}
|
|
77
|
+
</Form.Control>
|
|
78
|
+
|
|
79
|
+
{#if isText}
|
|
80
|
+
{@const val = resolvePathValue($formData, path)}
|
|
81
|
+
{@const charCount = typeof val === 'string' ? val.length : 0}
|
|
82
|
+
{@const atLimit = field.type === 'text' && field.maxLength !== undefined && charCount >= field.maxLength}
|
|
83
|
+
<div class="flex items-start justify-between gap-4">
|
|
84
|
+
{#if field.description || hasConstraints}
|
|
85
|
+
<Form.Description class="flex-1">
|
|
86
|
+
{#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
|
|
87
|
+
{#if hasConstraints}
|
|
88
|
+
{#if field.description && constraintHint()}<br />{/if}
|
|
89
|
+
{#if constraintHint()}{constraintHint()}{/if}
|
|
90
|
+
{#if field.type === 'text' && field.pattern}
|
|
91
|
+
{#if constraintHint()} · {/if}Format: <code class="text-xs bg-muted px-1 py-0.5 rounded">{field.pattern}</code>
|
|
239
92
|
{/if}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{:else
|
|
251
|
-
|
|
93
|
+
{/if}
|
|
94
|
+
</Form.Description>
|
|
95
|
+
{:else}
|
|
96
|
+
<div></div>
|
|
97
|
+
{/if}
|
|
98
|
+
<span class="shrink-0 text-xs {atLimit ? 'text-destructive' : 'text-muted-foreground'}" aria-live="polite">
|
|
99
|
+
{#if field.type === 'text' && field.maxLength !== undefined}
|
|
100
|
+
{charCount} / {field.maxLength}
|
|
101
|
+
{:else if field.type === 'text' && field.minLength !== undefined}
|
|
102
|
+
{charCount} (min. {field.minLength})
|
|
103
|
+
{:else}
|
|
104
|
+
{charCount}
|
|
252
105
|
{/if}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
</
|
|
257
|
-
{/
|
|
258
|
-
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
{:else if field.description}
|
|
109
|
+
<Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
112
|
+
<Form.FieldErrors />
|
|
113
|
+
</Form.Field>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContentField as ContentFieldType, TextField as TextFieldType } from '../../../types/fields.js';
|
|
2
2
|
import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
|
|
3
3
|
declare function $$render<T extends Record<string, unknown>>(): {
|
|
4
4
|
props: {
|
|
5
|
-
field: TextFieldType |
|
|
5
|
+
field: TextFieldType | ContentFieldType;
|
|
6
6
|
form: SuperForm<T>;
|
|
7
7
|
path: FormPathLeaves<T, string | undefined>;
|
|
8
8
|
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts" generics="T extends Record<string, unknown>">
|
|
6
|
-
import { urlFieldDataSchema } from '../../../schemas/field/url.js';
|
|
7
6
|
import type { UrlField, UrlFieldData } from '../../../types/fields.js';
|
|
8
7
|
import { onMount } from 'svelte';
|
|
9
8
|
import {
|
|
@@ -25,22 +24,23 @@
|
|
|
25
24
|
const { value } = formFieldProxy(form, path) satisfies FormFieldProxy<UrlFieldData | undefined>;
|
|
26
25
|
|
|
27
26
|
onMount(() => {
|
|
28
|
-
if (!$value) {
|
|
29
|
-
$value = { id: '', url:
|
|
30
|
-
} else if (typeof $value !== 'object' || Array.isArray($value)) {
|
|
31
|
-
$value = { id: '', url: {} };
|
|
27
|
+
if (!$value || typeof $value !== 'object' || Array.isArray($value)) {
|
|
28
|
+
$value = { id: '', url: '' };
|
|
32
29
|
} else if (!('url' in $value)) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: { id: '', url: {} };
|
|
30
|
+
$value = { id: '', url: '' };
|
|
31
|
+
} else if (typeof ($value as Record<string, unknown>).url === 'object') {
|
|
32
|
+
// Migrate old { url: { pl: "..." } } → flat string
|
|
33
|
+
const urlObj = ($value as Record<string, unknown>).url as Record<string, string>;
|
|
34
|
+
const firstVal = Object.values(urlObj)[0] ?? '';
|
|
35
|
+
$value = { ...$value, url: firstVal };
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
if (field.text &&
|
|
43
|
-
$value = { ...$value, text:
|
|
38
|
+
if (field.text && $value.text == null) {
|
|
39
|
+
$value = { ...$value, text: '' };
|
|
40
|
+
} else if (field.text && typeof $value.text === 'object') {
|
|
41
|
+
// Migrate old { text: { pl: "..." } } → flat string
|
|
42
|
+
const textObj = $value.text as unknown as Record<string, string>;
|
|
43
|
+
$value = { ...$value, text: Object.values(textObj)[0] ?? '' };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (field.newTab && $value.newTab === undefined) {
|
|
@@ -51,17 +51,7 @@
|
|
|
51
51
|
$value = { ...$value, rel: '' };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
$value = {
|
|
56
|
-
id: '',
|
|
57
|
-
url: {},
|
|
58
|
-
...(field.text ? { text: {} } : {}),
|
|
59
|
-
...(field.newTab ? { newTab: false } : {}),
|
|
60
|
-
...(field.rel ? { rel: '' } : {})
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
fieldValid = urlFieldDataSchema.safeParse($value).success;
|
|
54
|
+
fieldValid = true;
|
|
65
55
|
});
|
|
66
56
|
|
|
67
57
|
let fieldValid = $state(false);
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import Badge from '../../../components/ui/badge/badge.svelte';
|
|
7
7
|
import Checkbox from '../../../components/ui/checkbox/checkbox.svelte';
|
|
8
8
|
import Label from '../../../components/ui/label/label.svelte';
|
|
9
|
-
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
10
9
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
11
10
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
12
11
|
import { isExternalUrl } from '../../../core/fields/urlUtils.js';
|
|
@@ -19,7 +18,6 @@
|
|
|
19
18
|
import ChevronRight from '@tabler/icons-svelte/icons/chevron-right';
|
|
20
19
|
|
|
21
20
|
const remotes = getRemotes();
|
|
22
|
-
const contentLanguage = getContentLanguage();
|
|
23
21
|
const interfaceLanguage = useInterfaceLanguage();
|
|
24
22
|
|
|
25
23
|
const labels = {
|
|
@@ -39,11 +37,10 @@
|
|
|
39
37
|
internal: { en: 'Internal', pl: 'Wewnętrzny' }
|
|
40
38
|
};
|
|
41
39
|
|
|
42
|
-
type EntryWithSeo =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
40
|
+
type EntryWithSeo = {
|
|
41
|
+
_id: string;
|
|
42
|
+
_slug: string;
|
|
43
|
+
seo?: SeoFieldData;
|
|
47
44
|
};
|
|
48
45
|
|
|
49
46
|
type Props = {
|
|
@@ -55,8 +52,8 @@
|
|
|
55
52
|
|
|
56
53
|
let isLinked = $derived(value.id != null && value.id.length > 0);
|
|
57
54
|
|
|
58
|
-
//
|
|
59
|
-
let currentUrl = $derived(value.url
|
|
55
|
+
// URL is now a flat string
|
|
56
|
+
let currentUrl = $derived(typeof value.url === 'string' ? value.url : '');
|
|
60
57
|
let currentIsExternal = $derived(currentUrl ? isExternalUrl(currentUrl) : false);
|
|
61
58
|
|
|
62
59
|
// Autocomplete state
|
|
@@ -64,7 +61,7 @@
|
|
|
64
61
|
let popoverOpen = $state(false);
|
|
65
62
|
let activeIndex = $state(-1);
|
|
66
63
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
-
let linkedEntry = $state<
|
|
64
|
+
let linkedEntry = $state<EntryWithSeo | null>(null);
|
|
68
65
|
|
|
69
66
|
// Rel collapsible
|
|
70
67
|
let relOpen = $state(false);
|
|
@@ -81,14 +78,14 @@
|
|
|
81
78
|
}
|
|
82
79
|
});
|
|
83
80
|
|
|
84
|
-
async function fetchLinkedEntry(id: string | undefined) {
|
|
81
|
+
async function fetchLinkedEntry(id: string | undefined): Promise<EntryWithSeo | null> {
|
|
85
82
|
if (!id) return null;
|
|
86
|
-
return remotes.getEntry({ id })
|
|
83
|
+
return remotes.getEntry({ id }) as Promise<EntryWithSeo | null>;
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
function searchEntries() {
|
|
90
87
|
if (searchTimeout) clearTimeout(searchTimeout);
|
|
91
|
-
const url = value.url
|
|
88
|
+
const url = typeof value.url === 'string' ? value.url : '';
|
|
92
89
|
if (isLinked || url.length < 3) {
|
|
93
90
|
autocompleteSuggestions = [];
|
|
94
91
|
popoverOpen = false;
|
|
@@ -101,7 +98,7 @@
|
|
|
101
98
|
]) as [EntryWithSeo[], EntryWithSeo[]];
|
|
102
99
|
const combined = [...slugResults, ...titleResults];
|
|
103
100
|
const deduped = combined.filter(
|
|
104
|
-
(entry, index, self) => index === self.findIndex((e) => e.
|
|
101
|
+
(entry, index, self) => index === self.findIndex((e) => e._id === entry._id)
|
|
105
102
|
);
|
|
106
103
|
autocompleteSuggestions = deduped.slice(0, 5);
|
|
107
104
|
popoverOpen = autocompleteSuggestions.length > 0;
|
|
@@ -110,7 +107,7 @@
|
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
function linkEntry(entry: EntryWithSeo) {
|
|
113
|
-
value.id = entry.
|
|
110
|
+
value.id = entry._id;
|
|
114
111
|
linkedEntry = entry;
|
|
115
112
|
autocompleteSuggestions = [];
|
|
116
113
|
popoverOpen = false;
|
|
@@ -170,11 +167,11 @@
|
|
|
170
167
|
<div class="flex min-w-0 flex-1 items-center gap-2 py-1.5">
|
|
171
168
|
<Badge variant="secondary" class="max-w-full gap-1.5 truncate">
|
|
172
169
|
<span class="truncate font-medium">
|
|
173
|
-
{linkedEntry.
|
|
170
|
+
{linkedEntry.seo?.title || linkedEntry._id}
|
|
174
171
|
</span>
|
|
175
|
-
{#if linkedEntry.
|
|
172
|
+
{#if linkedEntry.seo?.slug}
|
|
176
173
|
<span class="text-muted-foreground truncate text-[10px] font-normal">
|
|
177
|
-
{linkedEntry.
|
|
174
|
+
{linkedEntry.seo.slug}
|
|
178
175
|
</span>
|
|
179
176
|
{/if}
|
|
180
177
|
</Badge>
|
|
@@ -191,42 +188,38 @@
|
|
|
191
188
|
</InputGroup.Addon>
|
|
192
189
|
</InputGroup.Root>
|
|
193
190
|
{:else}
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{#if
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
{getLocalizedLabel(labels.internal, interfaceLanguage.current)}
|
|
223
|
-
</Badge>
|
|
224
|
-
{/if}
|
|
225
|
-
</InputGroup.Addon>
|
|
191
|
+
<InputGroup.Root>
|
|
192
|
+
<InputGroup.Addon align="inline-start">
|
|
193
|
+
<GlobeIcon class="size-4" />
|
|
194
|
+
</InputGroup.Addon>
|
|
195
|
+
<InputGroup.Input
|
|
196
|
+
bind:value={value.url}
|
|
197
|
+
type="text"
|
|
198
|
+
placeholder={getLocalizedLabel(labels.placeholder, interfaceLanguage.current)}
|
|
199
|
+
oninput={searchEntries}
|
|
200
|
+
onkeydown={handleUrlKeydown}
|
|
201
|
+
role="combobox"
|
|
202
|
+
aria-expanded={popoverOpen}
|
|
203
|
+
aria-controls="url-suggestions"
|
|
204
|
+
aria-autocomplete="list"
|
|
205
|
+
aria-activedescendant={activeIndex >= 0 ? `url-suggestion-${activeIndex}` : undefined}
|
|
206
|
+
/>
|
|
207
|
+
{#if currentUrl}
|
|
208
|
+
<InputGroup.Addon align="inline-end">
|
|
209
|
+
{#if currentIsExternal}
|
|
210
|
+
<Badge variant="outline" class="text-[10px]">
|
|
211
|
+
<ExternalLinkIcon class="size-3" />
|
|
212
|
+
{getLocalizedLabel(labels.external, interfaceLanguage.current)}
|
|
213
|
+
</Badge>
|
|
214
|
+
{:else if currentUrl.length > 0}
|
|
215
|
+
<Badge variant="secondary" class="text-[10px]">
|
|
216
|
+
<LinkIcon class="size-3" />
|
|
217
|
+
{getLocalizedLabel(labels.internal, interfaceLanguage.current)}
|
|
218
|
+
</Badge>
|
|
226
219
|
{/if}
|
|
227
|
-
</InputGroup.
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
</InputGroup.Addon>
|
|
221
|
+
{/if}
|
|
222
|
+
</InputGroup.Root>
|
|
230
223
|
{/if}
|
|
231
224
|
|
|
232
225
|
<!-- Floating autocomplete dropdown -->
|
|
@@ -247,10 +240,10 @@
|
|
|
247
240
|
>
|
|
248
241
|
<LinkIcon class="text-muted-foreground size-3.5 shrink-0" />
|
|
249
242
|
<span class="truncate text-sm">
|
|
250
|
-
{suggestion.
|
|
243
|
+
{suggestion.seo?.title || getLocalizedLabel(labels.noTitle, interfaceLanguage.current)}
|
|
251
244
|
</span>
|
|
252
245
|
<span class="text-muted-foreground ml-auto shrink-0 text-xs">
|
|
253
|
-
{suggestion.
|
|
246
|
+
{suggestion.seo?.slug || ''}
|
|
254
247
|
</span>
|
|
255
248
|
</button>
|
|
256
249
|
{/each}
|
|
@@ -259,24 +252,20 @@
|
|
|
259
252
|
</div>
|
|
260
253
|
|
|
261
254
|
<!-- Options (text, newTab, rel) -->
|
|
262
|
-
{#if
|
|
255
|
+
{#if field.text || field.newTab || field.rel}
|
|
263
256
|
<div class="mt-2 space-y-2">
|
|
264
|
-
<!-- Link text -->
|
|
265
|
-
{#if field.text
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
/>
|
|
277
|
-
</InputGroup.Root>
|
|
278
|
-
</div>
|
|
279
|
-
{/each}
|
|
257
|
+
<!-- Link text (now flat string) -->
|
|
258
|
+
{#if field.text}
|
|
259
|
+
<InputGroup.Root>
|
|
260
|
+
<InputGroup.Addon align="inline-start">
|
|
261
|
+
<TextIcon class="size-4" />
|
|
262
|
+
</InputGroup.Addon>
|
|
263
|
+
<InputGroup.Input
|
|
264
|
+
bind:value={value.text}
|
|
265
|
+
type="text"
|
|
266
|
+
placeholder={getLocalizedLabel(labels.linkText, interfaceLanguage.current)}
|
|
267
|
+
/>
|
|
268
|
+
</InputGroup.Root>
|
|
280
269
|
{/if}
|
|
281
270
|
|
|
282
271
|
<!-- New tab + rel row -->
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
accept?: string;
|
|
32
32
|
onUpload?: () => void;
|
|
33
33
|
dropZoneRef?: HTMLElement | null;
|
|
34
|
+
tagIds?: string[];
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
let { accept, onUpload, dropZoneRef = $bindable(null) }: Props =
|
|
37
|
+
let { accept, onUpload, dropZoneRef = $bindable(null), tagIds }: Props =
|
|
37
38
|
$props();
|
|
38
39
|
|
|
39
40
|
let inputElement: HTMLInputElement;
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
async function uploadFile(file: File, index: number) {
|
|
44
45
|
const form = new FormData();
|
|
45
46
|
form.append('file', file);
|
|
47
|
+
if (tagIds && tagIds.length > 0) {
|
|
48
|
+
form.append('tagIds', JSON.stringify(tagIds));
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
return new Promise<void>((resolve) => {
|
|
48
52
|
const xhr = new XMLHttpRequest();
|