includio-cms 0.13.0 → 0.13.2
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 +36 -0
- package/ROADMAP.md +24 -0
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/replace.js +2 -1
- package/dist/admin/api/rest/routes/upload.js +2 -1
- package/dist/admin/api/upload-limit.d.ts +2 -0
- package/dist/admin/api/upload-limit.js +7 -0
- package/dist/admin/api/upload.js +2 -1
- package/dist/admin/client/collection/collection-entries.svelte +58 -11
- 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 +31 -9
- package/dist/admin/components/fields/simple-array-field.svelte +22 -11
- 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 +66 -9
- package/dist/admin/components/media/files-list.svelte +12 -3
- package/dist/admin/components/media/media-selector.svelte +11 -5
- package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +32 -1
- 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/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +22 -18
- package/dist/admin/components/tiptap/slash-command.js +26 -22
- 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 +1 -0
- package/dist/admin/remote/entry.remote.js +6 -4
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/reorder.d.ts +1 -0
- package/dist/admin/remote/reorder.js +33 -0
- package/dist/core/server/entries/operations/get.js +15 -3
- package/dist/core/server/fields/utils/imageStyles.js +7 -3
- package/dist/core/server/generator/fields.js +2 -2
- package/dist/core/server/generator/generator.js +4 -2
- package/dist/core/server/media/styles/operations/createMediaStyle.d.ts +2 -2
- package/dist/core/server/media/styles/operations/createMediaStyle.js +5 -3
- package/dist/core/server/media/styles/operations/generateDefaultStyles.js +33 -6
- package/dist/core/server/media/styles/operations/getImageStyle.d.ts +1 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +3 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +2 -1
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +5 -3
- package/dist/core/server/media/uploadLimit.d.ts +2 -0
- package/dist/core/server/media/uploadLimit.js +26 -0
- package/dist/types/entries.d.ts +1 -0
- package/dist/types/layout.d.ts +0 -1
- 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.13.2/index.d.ts +2 -0
- package/dist/updates/0.13.2/index.js +20 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,42 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.13.2 — 2026-03-20
|
|
7
|
+
|
|
8
|
+
Upload limits, stable reordering, relation filters, image style optimization
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Configurable upload size limit via BODY_SIZE_LIMIT env var (supports K/M/G suffixes, default 50M)
|
|
12
|
+
- File upload pre-validation — rejects oversized files before upload with localized error messages
|
|
13
|
+
- Stable slot reordering — preserves positions of filtered-out entries during DnD reorder
|
|
14
|
+
- Collection relation field filtering — filter entries by relation field values
|
|
15
|
+
- Expose _sortOrder on entries for orderable collections
|
|
16
|
+
- Codegen: _sortOrder field in generated TypeScript interfaces for orderable collections
|
|
17
|
+
- getImageStyleIfExists — non-blocking image style lookup (returns null if missing)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Concurrent image style generation with buffer reuse — download original once, generate styles in parallel (limit 3)
|
|
21
|
+
- Image style resolution gracefully skips missing styles instead of blocking
|
|
22
|
+
- Upload endpoints use configurable size limit instead of hardcoded 50MB
|
|
23
|
+
|
|
24
|
+
## 0.13.1 — 2026-03-20
|
|
25
|
+
|
|
26
|
+
Admin UI i18n, codegen cleanup, docs rewrite
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Admin UI i18n — TipTap editor, blocks field, array field, media components support pl/en interface language
|
|
30
|
+
- Inline block accordion labels — show field value in collapsed block header
|
|
31
|
+
- Blocks field UrlFieldData label support
|
|
32
|
+
- Email configuration remote endpoint
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Codegen: remove unused FlatImageFieldData/FlatVideoFieldData, use ImageFieldData/VideoFieldData
|
|
36
|
+
- Layout renderer: hide card header when label is empty
|
|
37
|
+
- Collection entries: improved relation label fetching with UUID validation and multi-version support
|
|
38
|
+
- Users page: client-side email config check, remove server load dependency
|
|
39
|
+
- Entry remote: stricter UUID validation for ids parameter
|
|
40
|
+
- Layout type: remove unused label property from layout node
|
|
41
|
+
|
|
6
42
|
## 0.13.0 — 2026-03-19
|
|
7
43
|
|
|
8
44
|
Private file uploads for form submissions
|
package/ROADMAP.md
CHANGED
|
@@ -205,6 +205,30 @@
|
|
|
205
205
|
|
|
206
206
|
- [x] `[feature]` `[P1]` Private file uploads — form submissions can upload files to non-public directory
|
|
207
207
|
|
|
208
|
+
## 0.13.1 — Admin i18n, codegen cleanup, docs
|
|
209
|
+
|
|
210
|
+
- [x] `[feature]` `[P1]` Admin UI i18n — TipTap, blocks, array, media components support pl/en <!-- files: src/lib/admin/components/tiptap/lang.ts -->
|
|
211
|
+
- [x] `[feature]` `[P2]` Inline block accordion labels — show field value in collapsed header <!-- files: src/lib/admin/components/tiptap/InlineBlockNodeView.svelte -->
|
|
212
|
+
- [x] `[feature]` `[P2]` Blocks field UrlFieldData label support <!-- files: src/lib/admin/components/fields/blocks-field.svelte -->
|
|
213
|
+
- [x] `[feature]` `[P2]` Email configuration remote endpoint <!-- files: src/lib/admin/remote/email.remote.ts -->
|
|
214
|
+
- [x] `[fix]` `[P1]` Codegen: remove unused Flat*FieldData types, use ImageFieldData/VideoFieldData <!-- files: src/lib/core/server/generator/fields.ts, src/lib/core/server/generator/generator.ts -->
|
|
215
|
+
- [x] `[fix]` `[P1]` Layout renderer: hide card header when label empty <!-- files: src/lib/admin/components/layout/layout-renderer.svelte -->
|
|
216
|
+
- [x] `[fix]` `[P1]` Collection entries: improved relation label fetching (UUID validation, multi-version) <!-- files: src/lib/admin/client/collection/collection-entries.svelte -->
|
|
217
|
+
- [x] `[fix]` `[P1]` Users page: client-side email config, remove server load <!-- files: src/lib/admin/client/users/users-page.svelte -->
|
|
218
|
+
- [x] `[fix]` `[P2]` Entry remote: stricter UUID validation for ids <!-- files: src/lib/admin/remote/entry.remote.ts -->
|
|
219
|
+
- [x] `[fix]` `[P2]` Layout type: remove unused label property <!-- files: src/lib/types/layout.ts -->
|
|
220
|
+
- [x] `[chore]` `[P1]` Documentation rewrite — new pages for blocks, content, media-field, layout; expanded API, auth, entries, forms, plugins docs
|
|
221
|
+
|
|
222
|
+
## 0.13.2 — Upload limits, stable reordering, image style optimization
|
|
223
|
+
|
|
224
|
+
- [x] `[feature]` `[P1]` Configurable upload size limit — `BODY_SIZE_LIMIT` env var, pre-validation in UI <!-- files: src/lib/core/server/media/uploadLimit.ts, src/lib/admin/api/upload-limit.ts, src/lib/admin/components/media/file-upload.svelte -->
|
|
225
|
+
- [x] `[feature]` `[P1]` Stable slot reordering — preserve filtered-out entry positions during DnD <!-- files: src/lib/admin/remote/reorder.ts, src/lib/admin/remote/entry.remote.ts -->
|
|
226
|
+
- [x] `[feature]` `[P1]` Collection relation field filtering — filter entries by relation values <!-- files: src/lib/admin/client/collection/collection-entries.svelte -->
|
|
227
|
+
- [x] `[feature]` `[P2]` Expose `_sortOrder` on entries + codegen for orderable collections <!-- files: src/lib/core/server/entries/operations/get.ts, src/lib/core/server/generator/generator.ts, src/lib/types/entries.ts -->
|
|
228
|
+
- [x] `[feature]` `[P2]` `getImageStyleIfExists` — non-blocking image style lookup <!-- files: src/lib/core/server/media/styles/operations/getImageStyle.ts -->
|
|
229
|
+
- [x] `[fix]` `[P1]` Concurrent image style generation with buffer reuse (download once, parallel limit 3) <!-- files: src/lib/core/server/media/styles/operations/generateDefaultStyles.ts, src/lib/core/server/media/styles/sharp/generateImageStyle.ts -->
|
|
230
|
+
- [x] `[fix]` `[P1]` Image style resolution gracefully skips missing styles <!-- files: src/lib/core/server/fields/utils/imageStyles.ts -->
|
|
231
|
+
|
|
208
232
|
## 0.14.0 — SEO module
|
|
209
233
|
|
|
210
234
|
- [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
|
|
@@ -6,6 +6,7 @@ import * as inviteHandlers from './invite.js';
|
|
|
6
6
|
import * as acceptInviteHandlers from './accept-invite.js';
|
|
7
7
|
import * as mediaGcHandlers from './media-gc.js';
|
|
8
8
|
import * as generateStylesHandlers from './generate-styles.js';
|
|
9
|
+
import * as uploadLimitHandlers from './upload-limit.js';
|
|
9
10
|
import { requireAuth } from '../remote/middleware/auth.js';
|
|
10
11
|
import { getCMS } from '../../core/cms.js';
|
|
11
12
|
import { lookup } from 'mrmime';
|
|
@@ -18,6 +19,7 @@ export function createAdminApiHandler(options) {
|
|
|
18
19
|
'accept-invite': acceptInviteHandlers,
|
|
19
20
|
'media-gc': mediaGcHandlers,
|
|
20
21
|
'generate-styles': generateStylesHandlers,
|
|
22
|
+
'upload-limit': uploadLimitHandlers,
|
|
21
23
|
...options?.extraRoutes
|
|
22
24
|
};
|
|
23
25
|
const privateMediaGet = async (event) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { requireAuth } from '../remote/middleware/auth.js';
|
|
2
2
|
import { replaceFile } from '../../core/server/media/operations/replaceFile.js';
|
|
3
3
|
import { json } from '@sveltejs/kit';
|
|
4
|
-
|
|
4
|
+
import { getMaxUploadSize } from '../../core/server/media/uploadLimit.js';
|
|
5
|
+
const MAX_UPLOAD_SIZE = getMaxUploadSize();
|
|
5
6
|
export const POST = async ({ request }) => {
|
|
6
7
|
requireAuth();
|
|
7
8
|
const form = await request.formData();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { json } from '@sveltejs/kit';
|
|
2
2
|
import { uploadFile } from '../../../../core/server/media/operations/uploadFile.js';
|
|
3
3
|
import { getCMS } from '../../../../core/cms.js';
|
|
4
|
-
|
|
4
|
+
import { getMaxUploadSize } from '../../../../core/server/media/uploadLimit.js';
|
|
5
|
+
const MAX_UPLOAD_SIZE = getMaxUploadSize();
|
|
5
6
|
export async function POST(event) {
|
|
6
7
|
const form = await event.request.formData();
|
|
7
8
|
const file = form.get('file');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireAuth } from '../remote/middleware/auth.js';
|
|
2
|
+
import { getMaxUploadSize } from '../../core/server/media/uploadLimit.js';
|
|
3
|
+
import { json } from '@sveltejs/kit';
|
|
4
|
+
export const GET = async () => {
|
|
5
|
+
requireAuth();
|
|
6
|
+
return json({ maxUploadSize: getMaxUploadSize() });
|
|
7
|
+
};
|
package/dist/admin/api/upload.js
CHANGED
|
@@ -2,7 +2,8 @@ import { requireAuth } from '../remote/middleware/auth.js';
|
|
|
2
2
|
import { uploadFile } from '../../core/server/media/operations/uploadFile.js';
|
|
3
3
|
import { getCMS } from '../../core/cms.js';
|
|
4
4
|
import { json } from '@sveltejs/kit';
|
|
5
|
-
|
|
5
|
+
import { getMaxUploadSize } from '../../core/server/media/uploadLimit.js';
|
|
6
|
+
const MAX_UPLOAD_SIZE = getMaxUploadSize();
|
|
6
7
|
export const POST = async ({ request }) => {
|
|
7
8
|
requireAuth();
|
|
8
9
|
const form = await request.formData();
|
|
@@ -144,6 +144,21 @@
|
|
|
144
144
|
);
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
// Relation filter options fetched from related collections
|
|
148
|
+
let relationFilterOptions = $state<Record<string, { label: string; value: string }[]>>({});
|
|
149
|
+
|
|
150
|
+
$effect(() => {
|
|
151
|
+
if (relationListFields.length === 0) return;
|
|
152
|
+
for (const field of relationListFields) {
|
|
153
|
+
remotes.getEntryLabels({ slug: field.collection }).then((labels) => {
|
|
154
|
+
relationFilterOptions = {
|
|
155
|
+
...relationFilterOptions,
|
|
156
|
+
[field.slug]: labels.map((l) => ({ value: l.id, label: l.label }))
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
147
162
|
// Data filter configs from select/radio fields in listColumns
|
|
148
163
|
const dataFilterConfigs = $derived(
|
|
149
164
|
(collection.listColumns ?? [])
|
|
@@ -163,6 +178,15 @@
|
|
|
163
178
|
return null;
|
|
164
179
|
})
|
|
165
180
|
.filter((f): f is NonNullable<typeof f> => f !== null)
|
|
181
|
+
.concat(
|
|
182
|
+
relationListFields
|
|
183
|
+
.filter((f) => relationFilterOptions[f.slug]?.length)
|
|
184
|
+
.map((f) => ({
|
|
185
|
+
slug: f.slug,
|
|
186
|
+
label: getLocalizedLabel(f.label, interfaceLanguage.current),
|
|
187
|
+
options: relationFilterOptions[f.slug]
|
|
188
|
+
}))
|
|
189
|
+
)
|
|
166
190
|
);
|
|
167
191
|
|
|
168
192
|
let activeDataFilters = $state<Record<string, string | null>>({});
|
|
@@ -433,6 +457,8 @@
|
|
|
433
457
|
return issues.filter((i) => i.type === 'warning').length;
|
|
434
458
|
}
|
|
435
459
|
|
|
460
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
461
|
+
|
|
436
462
|
async function fetchRelationLabels(entries: RawEntry[]): Promise<Record<string, string>> {
|
|
437
463
|
if (relationListFields.length === 0) return {};
|
|
438
464
|
|
|
@@ -441,14 +467,21 @@
|
|
|
441
467
|
uuidsByCollection.set(field.collection, new Set());
|
|
442
468
|
}
|
|
443
469
|
for (const entry of entries) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
470
|
+
// Collect UUIDs from all version sources (draft + published) to match mapEntryToRow
|
|
471
|
+
const lang = contentLanguage.current;
|
|
472
|
+
const dataSources = [
|
|
473
|
+
entry.publishedVersions[lang]?.data,
|
|
474
|
+
entry.draftVersions[lang]?.data,
|
|
475
|
+
getVersionData(entry)
|
|
476
|
+
].filter(Boolean) as Record<string, unknown>[];
|
|
477
|
+
for (const data of dataSources) {
|
|
478
|
+
for (const field of relationListFields) {
|
|
479
|
+
const raw = data[field.slug];
|
|
480
|
+
if (!raw) continue;
|
|
481
|
+
const set = uuidsByCollection.get(field.collection)!;
|
|
482
|
+
if (typeof raw === 'string' && UUID_RE.test(raw)) set.add(raw);
|
|
483
|
+
if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && UUID_RE.test(v) && set.add(v));
|
|
484
|
+
}
|
|
452
485
|
}
|
|
453
486
|
}
|
|
454
487
|
|
|
@@ -476,6 +509,8 @@
|
|
|
476
509
|
const fieldData = (columnData as Record<string, unknown>)[fieldSlug];
|
|
477
510
|
const isRelation = relationListFields.some((f) => f.slug === fieldSlug);
|
|
478
511
|
if (isRelation) {
|
|
512
|
+
// Store raw UUID(s) for filtering
|
|
513
|
+
customData[`__raw_${fieldSlug}`] = fieldData;
|
|
479
514
|
// Resolve relation UUID(s) to labels
|
|
480
515
|
if (typeof fieldData === 'string') {
|
|
481
516
|
customData[fieldSlug] = lookup[fieldData] ?? '';
|
|
@@ -590,7 +625,7 @@
|
|
|
590
625
|
...old,
|
|
591
626
|
entries: orderedIds.map((id) => old.entries.find((e: RawEntry) => e.id === id)).filter(Boolean)
|
|
592
627
|
}));
|
|
593
|
-
await remotes.reorderEntriesCommand({ orderedIds });
|
|
628
|
+
await remotes.reorderEntriesCommand({ orderedIds, collectionSlug: collection.slug });
|
|
594
629
|
await entriesQuery.refresh();
|
|
595
630
|
override.release();
|
|
596
631
|
}
|
|
@@ -631,8 +666,20 @@
|
|
|
631
666
|
rows = rows.filter((row) => {
|
|
632
667
|
for (const [slug, value] of Object.entries(activeDataFilters)) {
|
|
633
668
|
if (value === null) continue;
|
|
634
|
-
|
|
635
|
-
|
|
669
|
+
// For relation fields, compare against raw UUID(s)
|
|
670
|
+
const rawValue = row.customData[`__raw_${slug}`];
|
|
671
|
+
if (rawValue !== undefined) {
|
|
672
|
+
if (typeof rawValue === 'string') {
|
|
673
|
+
if (rawValue !== value) return false;
|
|
674
|
+
} else if (Array.isArray(rawValue)) {
|
|
675
|
+
if (!rawValue.includes(value)) return false;
|
|
676
|
+
} else {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
const rowValue = row.customData[slug];
|
|
681
|
+
if (String(rowValue ?? '') !== value) return false;
|
|
682
|
+
}
|
|
636
683
|
}
|
|
637
684
|
return true;
|
|
638
685
|
});
|
|
@@ -32,12 +32,7 @@
|
|
|
32
32
|
import UserSessionsSheet from './user-sessions-sheet.svelte';
|
|
33
33
|
import InviteUserDialog from './invite-user-dialog.svelte';
|
|
34
34
|
import PendingInvitations from './pending-invitations.svelte';
|
|
35
|
-
|
|
36
|
-
type Props = {
|
|
37
|
-
emailConfigured?: boolean;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
let { emailConfigured = false }: Props = $props();
|
|
35
|
+
import { getRemotes } from '../../helpers/index.js';
|
|
41
36
|
|
|
42
37
|
type User = {
|
|
43
38
|
id: string;
|
|
@@ -47,6 +42,10 @@
|
|
|
47
42
|
createdAt: Date;
|
|
48
43
|
};
|
|
49
44
|
|
|
45
|
+
const remotes = getRemotes();
|
|
46
|
+
const emailQuery = $derived(remotes.getEmailConfigured());
|
|
47
|
+
const emailConfigured = $derived(emailQuery.data === true);
|
|
48
|
+
|
|
50
49
|
const interfaceLanguage = useInterfaceLanguage();
|
|
51
50
|
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
52
51
|
const session = authClient.useSession();
|
|
@@ -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();
|
|
@@ -174,6 +184,18 @@
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
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
|
+
|
|
177
199
|
const objectLabel = label as Record<string, string>;
|
|
178
200
|
return `${objectLabel[contentLanguage.current]}`;
|
|
179
201
|
}
|
|
@@ -229,7 +251,7 @@
|
|
|
229
251
|
>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel
|
|
230
252
|
>
|
|
231
253
|
{#if isFixedLength}
|
|
232
|
-
<span class="text-muted-foreground text-xs">{fixedCount}
|
|
254
|
+
<span class="text-muted-foreground text-xs">{fixedCount} {bt.elements}</span>
|
|
233
255
|
{:else if field.maxItems !== undefined}
|
|
234
256
|
<span class="text-xs {atMax ? 'text-destructive' : 'text-muted-foreground'}"
|
|
235
257
|
>{$value?.length ?? 0} / {field.maxItems}</span
|
|
@@ -244,7 +266,7 @@
|
|
|
244
266
|
variant="ghost"
|
|
245
267
|
onclick={() => {
|
|
246
268
|
accordionOpenState = [];
|
|
247
|
-
}}>
|
|
269
|
+
}}>{bt.collapseAll}</Button
|
|
248
270
|
>
|
|
249
271
|
<Button
|
|
250
272
|
size="sm"
|
|
@@ -254,7 +276,7 @@
|
|
|
254
276
|
if ($value) {
|
|
255
277
|
accordionOpenState = $value.map((_, i) => i.toString());
|
|
256
278
|
}
|
|
257
|
-
}}>
|
|
279
|
+
}}>{bt.showAll}</Button
|
|
258
280
|
>
|
|
259
281
|
</div>
|
|
260
282
|
</div>
|
|
@@ -334,7 +356,7 @@
|
|
|
334
356
|
{#snippet child({ props })}
|
|
335
357
|
<Button variant="ghost" size="icon" {...props}>
|
|
336
358
|
<DotsVerticalIcon />
|
|
337
|
-
<span class="sr-only">
|
|
359
|
+
<span class="sr-only">{bt.openMenu}</span>
|
|
338
360
|
</Button>
|
|
339
361
|
{/snippet}
|
|
340
362
|
</DropdownMenu.Trigger>
|
|
@@ -344,27 +366,27 @@
|
|
|
344
366
|
onclick={() => duplicateItem(index)}
|
|
345
367
|
disabled={atMax}
|
|
346
368
|
>
|
|
347
|
-
|
|
369
|
+
{bt.duplicate}
|
|
348
370
|
</DropdownMenu.Item>
|
|
349
371
|
{/if}
|
|
350
372
|
<DropdownMenu.Item
|
|
351
373
|
onclick={() => moveItemUp(index)}
|
|
352
374
|
disabled={index === 0}
|
|
353
375
|
>
|
|
354
|
-
|
|
376
|
+
{bt.moveUp}
|
|
355
377
|
</DropdownMenu.Item>
|
|
356
378
|
<DropdownMenu.Item
|
|
357
379
|
onclick={() => moveItemDown(index)}
|
|
358
380
|
disabled={$value && index === $value.length - 1}
|
|
359
381
|
>
|
|
360
|
-
|
|
382
|
+
{bt.moveDown}
|
|
361
383
|
</DropdownMenu.Item>
|
|
362
384
|
{#if !isFixedLength}
|
|
363
385
|
<DropdownMenu.Item
|
|
364
386
|
variant="destructive"
|
|
365
387
|
onclick={() => removeItem(index)}
|
|
366
388
|
>
|
|
367
|
-
|
|
389
|
+
{bt.delete}
|
|
368
390
|
</DropdownMenu.Item>
|
|
369
391
|
{/if}
|
|
370
392
|
</DropdownMenu.Content>
|
|
@@ -413,7 +435,7 @@
|
|
|
413
435
|
onclick={() => (blockPickerOpen = true)}
|
|
414
436
|
>
|
|
415
437
|
<CirclePlus />
|
|
416
|
-
|
|
438
|
+
{bt.addBlock}
|
|
417
439
|
</Button>
|
|
418
440
|
</div>
|
|
419
441
|
<BlockPickerModal
|
|
@@ -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;
|
|
@@ -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,7 +220,7 @@
|
|
|
209
220
|
<div class="flex-1 space-y-2">
|
|
210
221
|
<Input
|
|
211
222
|
type="url"
|
|
212
|
-
placeholder=
|
|
223
|
+
placeholder={at.urlPlaceholder}
|
|
213
224
|
value={typeof urlItem.url === 'string' ? urlItem.url : ''}
|
|
214
225
|
oninput={(e) => {
|
|
215
226
|
const val = e.currentTarget.value;
|
|
@@ -222,7 +233,7 @@
|
|
|
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
238
|
value={typeof urlItem.text === 'string' ? urlItem.text : ''}
|
|
228
239
|
oninput={(e) => {
|
|
@@ -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}
|
|
@@ -150,10 +150,12 @@
|
|
|
150
150
|
</div>
|
|
151
151
|
|
|
152
152
|
{:else if node.type === 'card'}
|
|
153
|
-
<div role="group" aria-label={getLabel(node)} class="layout-card">
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
<div role="group" aria-label={getLabel(node) || undefined} class="layout-card" class:no-header={!getLabel(node)}>
|
|
154
|
+
{#if getLabel(node)}
|
|
155
|
+
<div class="layout-card-header">
|
|
156
|
+
{getLabel(node)}
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
157
159
|
<div class="layout-card-body">
|
|
158
160
|
{#if isLayoutLeaf(node) && node.autoGrid}
|
|
159
161
|
<div class="layout-auto-grid">
|
|
@@ -268,6 +270,10 @@
|
|
|
268
270
|
padding: 10px 16px 16px;
|
|
269
271
|
}
|
|
270
272
|
|
|
273
|
+
.layout-card.no-header .layout-card-body {
|
|
274
|
+
padding-top: 16px;
|
|
275
|
+
}
|
|
276
|
+
|
|
271
277
|
/* ═══════════ ACCORDION ═══════════ */
|
|
272
278
|
.layout-accordion-wrapper {
|
|
273
279
|
background: var(--card);
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
import VideoOff from '@tabler/icons-svelte/icons/video-off';
|
|
6
6
|
|
|
7
7
|
import { getRemotes } from '../../context/remotes.js';
|
|
8
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
9
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
10
|
+
|
|
11
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
12
|
+
const previewLang: Record<InterfaceLanguage, { loading: string }> = {
|
|
13
|
+
pl: { loading: 'Ładowanie...' },
|
|
14
|
+
en: { loading: 'Loading...' }
|
|
15
|
+
};
|
|
16
|
+
const pvt = $derived(previewLang[interfaceLanguage.current]);
|
|
8
17
|
|
|
9
18
|
const remotes = getRemotes();
|
|
10
19
|
|
|
@@ -20,7 +29,7 @@
|
|
|
20
29
|
|
|
21
30
|
<div class="border">
|
|
22
31
|
{#if !fileQuery.ready}
|
|
23
|
-
|
|
32
|
+
{pvt.loading}
|
|
24
33
|
{:else if fileQuery.current}
|
|
25
34
|
{@const file = fileQuery.current}
|
|
26
35
|
{#if file.type === 'image'}
|