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
|
@@ -7,15 +7,33 @@ export function getEntryVersionStatus(version) {
|
|
|
7
7
|
}
|
|
8
8
|
return 'published';
|
|
9
9
|
}
|
|
10
|
+
/** Get entry status for a specific language */
|
|
11
|
+
export function getEntryStatusForLang(entry, lang) {
|
|
12
|
+
if (entry.archivedAt) {
|
|
13
|
+
return 'archived';
|
|
14
|
+
}
|
|
15
|
+
const publishedVersion = entry.publishedVersions[lang];
|
|
16
|
+
const scheduledVersion = entry.scheduledVersions[lang];
|
|
17
|
+
if (publishedVersion) {
|
|
18
|
+
return 'published';
|
|
19
|
+
}
|
|
20
|
+
if (scheduledVersion) {
|
|
21
|
+
return 'scheduled';
|
|
22
|
+
}
|
|
23
|
+
return 'draft';
|
|
24
|
+
}
|
|
25
|
+
/** Get overall entry status — published if any lang is published */
|
|
10
26
|
export function getEntryStatus(entry) {
|
|
11
27
|
if (entry.archivedAt) {
|
|
12
28
|
return 'archived';
|
|
13
29
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return 'scheduled';
|
|
17
|
-
}
|
|
30
|
+
const hasPublished = Object.values(entry.publishedVersions).some((v) => v != null);
|
|
31
|
+
if (hasPublished) {
|
|
18
32
|
return 'published';
|
|
19
33
|
}
|
|
34
|
+
const hasScheduled = Object.values(entry.scheduledVersions).some((v) => v != null);
|
|
35
|
+
if (hasScheduled) {
|
|
36
|
+
return 'scheduled';
|
|
37
|
+
}
|
|
20
38
|
return 'draft';
|
|
21
39
|
}
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
|
|
47
47
|
const formSubmissionQuery = $derived(submissionId ? remotes.getFormSubmission(submissionId) : null);
|
|
48
48
|
|
|
49
|
+
let markedAsRead = false;
|
|
50
|
+
|
|
49
51
|
$effect(() => {
|
|
50
52
|
const submission = formSubmissionQuery?.current;
|
|
51
53
|
if (submission) {
|
|
@@ -65,7 +67,8 @@
|
|
|
65
67
|
];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
if (!submission.read) {
|
|
70
|
+
if (!submission.read && !markedAsRead) {
|
|
71
|
+
markedAsRead = true;
|
|
69
72
|
remotes.updateFormSubmission({ id: submission.id, read: true });
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import Phone from '@tabler/icons-svelte/icons/phone';
|
|
6
6
|
import MessageSquare from '@tabler/icons-svelte/icons/message';
|
|
7
7
|
import FileText from '@tabler/icons-svelte/icons/file-text';
|
|
8
|
+
import Download from '@tabler/icons-svelte/icons/download';
|
|
8
9
|
import CheckCircle from '@tabler/icons-svelte/icons/circle-check';
|
|
9
10
|
import CircleX from '@tabler/icons-svelte/icons/circle-x';
|
|
10
11
|
import Hash from '@tabler/icons-svelte/icons/hash';
|
|
@@ -28,11 +29,13 @@
|
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
const isBoolean = $derived(typeof value === 'boolean');
|
|
32
|
+
const isFile = $derived(fieldType === 'file' && typeof value === 'string' && value.length > 0);
|
|
31
33
|
const isEmail = $derived(fieldType === 'email' || (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)));
|
|
32
34
|
const isPhone = $derived(fieldType === 'phone' || (typeof value === 'string' && /^[\d\s\-+()]+$/.test(value) && String(value).length >= 9));
|
|
33
35
|
|
|
34
36
|
const iconType = $derived.by(() => {
|
|
35
37
|
if (isBoolean) return value ? 'check' : 'x';
|
|
38
|
+
if (isFile) return 'download';
|
|
36
39
|
if (isEmail) return 'mail';
|
|
37
40
|
if (isPhone) return 'phone';
|
|
38
41
|
if (fieldType === 'textarea') return 'message';
|
|
@@ -64,6 +67,8 @@
|
|
|
64
67
|
<Hash class="size-4 text-muted-foreground" />
|
|
65
68
|
{:else if iconType === 'calendar'}
|
|
66
69
|
<Calendar class="size-4 text-muted-foreground" />
|
|
70
|
+
{:else if iconType === 'download'}
|
|
71
|
+
<Download class="size-4 text-muted-foreground" />
|
|
67
72
|
{:else}
|
|
68
73
|
<FileText class="size-4 text-muted-foreground" />
|
|
69
74
|
{/if}
|
|
@@ -81,6 +86,11 @@
|
|
|
81
86
|
<span class="text-red-600">Nie</span>
|
|
82
87
|
{/if}
|
|
83
88
|
</div>
|
|
89
|
+
{:else if isFile}
|
|
90
|
+
<a href={String(value)} target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 text-primary hover:underline">
|
|
91
|
+
<Download class="size-4" />
|
|
92
|
+
Download
|
|
93
|
+
</a>
|
|
84
94
|
{:else if isEmail}
|
|
85
95
|
<a href="mailto:{value}" class="text-primary hover:underline break-all">{displayValue()}</a>
|
|
86
96
|
{:else if isPhone}
|
|
@@ -9,5 +9,6 @@ export { default as FormPage } from './form/form-page.svelte';
|
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
10
|
export { default as UsersPage } from './users/users-page.svelte';
|
|
11
11
|
export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
|
|
12
|
+
export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
|
|
12
13
|
export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
|
|
13
14
|
export { default as MediaSelector } from '../components/media/media-selector.svelte';
|
|
@@ -9,5 +9,6 @@ export { default as FormPage } from './form/form-page.svelte';
|
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
10
|
export { default as UsersPage } from './users/users-page.svelte';
|
|
11
11
|
export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
|
|
12
|
+
export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
|
|
12
13
|
export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
|
|
13
14
|
export { default as MediaSelector } from '../components/media/media-selector.svelte';
|
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
import FileSearch from '@tabler/icons-svelte/icons/file-search';
|
|
6
6
|
import Photo from '@tabler/icons-svelte/icons/photo';
|
|
7
7
|
import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
|
|
8
|
+
import PlayerPlay from '@tabler/icons-svelte/icons/player-play';
|
|
9
|
+
import PlayerStop from '@tabler/icons-svelte/icons/player-stop';
|
|
10
|
+
import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
|
|
8
11
|
import Button from '../../../components/ui/button/button.svelte';
|
|
9
12
|
import * as Card from '../../../components/ui/card/index.js';
|
|
10
13
|
import { toast } from 'svelte-sonner';
|
|
11
14
|
|
|
12
15
|
interface GcReport {
|
|
13
16
|
imageStylesCount: number;
|
|
17
|
+
processableImagesCount: number;
|
|
18
|
+
expectedStylesCount: number;
|
|
19
|
+
missingStylesCount: number;
|
|
14
20
|
orphanedDiskFiles: string[];
|
|
15
21
|
missingDiskRecords: { table: string; id: string; url: string }[];
|
|
16
22
|
}
|
|
@@ -20,6 +26,18 @@
|
|
|
20
26
|
let purging = $state(false);
|
|
21
27
|
let cleaningOrphans = $state(false);
|
|
22
28
|
|
|
29
|
+
// Batch generation state
|
|
30
|
+
let generating = $state(false);
|
|
31
|
+
let genTotal = $state(0);
|
|
32
|
+
let genProcessed = $state(0);
|
|
33
|
+
let genCreated = $state(0);
|
|
34
|
+
let genSkipped = $state(0);
|
|
35
|
+
let genCurrentFile = $state('');
|
|
36
|
+
let genErrors = $state(0);
|
|
37
|
+
let genAbort: AbortController | null = null;
|
|
38
|
+
|
|
39
|
+
let genPercent = $derived(genTotal > 0 ? Math.round((genProcessed / genTotal) * 100) : 0);
|
|
40
|
+
|
|
23
41
|
async function loadReport() {
|
|
24
42
|
loading = true;
|
|
25
43
|
try {
|
|
@@ -63,6 +81,77 @@
|
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
async function startBatchGenerate() {
|
|
85
|
+
generating = true;
|
|
86
|
+
genTotal = 0;
|
|
87
|
+
genProcessed = 0;
|
|
88
|
+
genCreated = 0;
|
|
89
|
+
genSkipped = 0;
|
|
90
|
+
genCurrentFile = '';
|
|
91
|
+
genErrors = 0;
|
|
92
|
+
genAbort = new AbortController();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch('/admin/api/generate-styles', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
signal: genAbort.signal
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) throw new Error('Failed to start');
|
|
101
|
+
if (!res.body) throw new Error('No response body');
|
|
102
|
+
|
|
103
|
+
const reader = res.body.getReader();
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
let buffer = '';
|
|
106
|
+
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
|
|
111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
112
|
+
const chunks = buffer.split('\n\n');
|
|
113
|
+
buffer = chunks.pop() || '';
|
|
114
|
+
|
|
115
|
+
for (const chunk of chunks) {
|
|
116
|
+
if (!chunk.startsWith('data: ')) continue;
|
|
117
|
+
const event = JSON.parse(chunk.slice(6));
|
|
118
|
+
|
|
119
|
+
genTotal = event.total ?? genTotal;
|
|
120
|
+
genProcessed = event.processed ?? genProcessed;
|
|
121
|
+
genCreated = event.created ?? genCreated;
|
|
122
|
+
genSkipped = event.skipped ?? genSkipped;
|
|
123
|
+
genCurrentFile = event.currentFile ?? genCurrentFile;
|
|
124
|
+
|
|
125
|
+
if (event.type === 'error') {
|
|
126
|
+
genErrors++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (event.type === 'done') {
|
|
130
|
+
const parts = [`Przetworzono ${genTotal} obrazów`];
|
|
131
|
+
if (genCreated > 0) parts.push(`utworzono ${genCreated} styli`);
|
|
132
|
+
if (genSkipped > 0) parts.push(`pominięto ${genSkipped} (już istnieją)`);
|
|
133
|
+
if (genErrors > 0) parts.push(`${genErrors} błędów`);
|
|
134
|
+
toast.success(parts.join(', '));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
140
|
+
toast.info(`Przerwano po ${genProcessed}/${genTotal} obrazów`);
|
|
141
|
+
} else {
|
|
142
|
+
toast.error('Błąd podczas generowania styli');
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
generating = false;
|
|
146
|
+
genAbort = null;
|
|
147
|
+
await loadReport();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cancelBatchGenerate() {
|
|
152
|
+
genAbort?.abort();
|
|
153
|
+
}
|
|
154
|
+
|
|
66
155
|
$effect(() => {
|
|
67
156
|
loadReport();
|
|
68
157
|
});
|
|
@@ -94,14 +183,69 @@
|
|
|
94
183
|
</Card.Description>
|
|
95
184
|
</Card.Header>
|
|
96
185
|
<Card.Content>
|
|
97
|
-
<p class="mb-
|
|
186
|
+
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
98
187
|
{report.imageStylesCount}
|
|
188
|
+
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.expectedStylesCount}</span>
|
|
189
|
+
</p>
|
|
190
|
+
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
191
|
+
{report.processableImagesCount} obrazów, {report.expectedStylesCount} oczekiwanych styli
|
|
99
192
|
</p>
|
|
193
|
+
|
|
194
|
+
<!-- Batch generate -->
|
|
195
|
+
{#if generating}
|
|
196
|
+
<div class="mb-4">
|
|
197
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
198
|
+
<span>{genProcessed}/{genTotal} obrazów</span>
|
|
199
|
+
<span>{genPercent}%</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="h-2 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
202
|
+
<div
|
|
203
|
+
class="h-full rounded-full transition-all duration-300"
|
|
204
|
+
style="width: {genPercent}%; background: var(--primary);"
|
|
205
|
+
></div>
|
|
206
|
+
</div>
|
|
207
|
+
<p class="mt-1 text-xs" style="color: var(--muted-foreground);">
|
|
208
|
+
Utworzono: {genCreated}, pominięto: {genSkipped}{genErrors > 0 ? `, błędów: ${genErrors}` : ''}
|
|
209
|
+
</p>
|
|
210
|
+
<p class="mt-0.5 truncate text-xs" style="color: var(--muted-foreground);">
|
|
211
|
+
{genCurrentFile}
|
|
212
|
+
</p>
|
|
213
|
+
<Button
|
|
214
|
+
variant="outline"
|
|
215
|
+
size="sm"
|
|
216
|
+
onclick={cancelBatchGenerate}
|
|
217
|
+
class="mt-2"
|
|
218
|
+
>
|
|
219
|
+
<PlayerStop class="size-4" />
|
|
220
|
+
Anuluj
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
{:else if report.missingStylesCount > 0}
|
|
224
|
+
<div class="mb-3">
|
|
225
|
+
<p class="mb-2 text-sm" style="color: var(--warning, #C4893A);">
|
|
226
|
+
Brakuje {report.missingStylesCount} styli — generowanie odbywa się przy wyświetleniu, co może spowalniać aplikację.
|
|
227
|
+
</p>
|
|
228
|
+
<Button
|
|
229
|
+
variant="default"
|
|
230
|
+
size="sm"
|
|
231
|
+
onclick={startBatchGenerate}
|
|
232
|
+
>
|
|
233
|
+
<PlayerPlay class="size-4" />
|
|
234
|
+
Generuj brakujące style
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
{:else}
|
|
238
|
+
<div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
|
|
239
|
+
<CircleCheck class="size-4" />
|
|
240
|
+
Wszystkie style wygenerowane
|
|
241
|
+
</div>
|
|
242
|
+
{/if}
|
|
243
|
+
|
|
100
244
|
<Button
|
|
101
245
|
variant="destructive"
|
|
102
246
|
size="sm"
|
|
103
247
|
onclick={purgeStyles}
|
|
104
|
-
disabled={purging || report.imageStylesCount === 0}
|
|
248
|
+
disabled={purging || generating || report.imageStylesCount === 0}
|
|
105
249
|
>
|
|
106
250
|
{#if purging}
|
|
107
251
|
<Loader2 class="size-4 animate-spin" />
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
$value = $value.reduce((acc: ObjectFieldData[], item: ObjectFieldData) => {
|
|
69
|
-
if (!item.
|
|
70
|
-
if (field.of.find((o) => o.slug === item.
|
|
69
|
+
if (!item._slug) return acc;
|
|
70
|
+
if (field.of.find((o) => o.slug === item._slug) === undefined) return acc;
|
|
71
71
|
acc.push(item);
|
|
72
72
|
return acc;
|
|
73
73
|
}, [] as ObjectFieldData[]);
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
if (defaults && defaults[i]) {
|
|
82
82
|
current.push({ _id: generateId(), ...JSON.parse(JSON.stringify(defaults[i])) });
|
|
83
83
|
} else {
|
|
84
|
-
current.push({ _id: generateId(),
|
|
84
|
+
current.push({ _id: generateId(), _slug: field.of[0].slug });
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
} else {
|
|
@@ -97,8 +97,7 @@
|
|
|
97
97
|
...($value ?? []),
|
|
98
98
|
{
|
|
99
99
|
_id: generateId(),
|
|
100
|
-
|
|
101
|
-
data: {}
|
|
100
|
+
_slug: field.slug
|
|
102
101
|
}
|
|
103
102
|
];
|
|
104
103
|
|
|
@@ -161,14 +160,14 @@
|
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
function getAccordionLabel(item: ObjectFieldData) {
|
|
164
|
-
const objectConfig = field.of.find((o) => o.slug === item.
|
|
163
|
+
const objectConfig = field.of.find((o) => o.slug === item._slug) as ObjectFieldType;
|
|
165
164
|
|
|
166
165
|
if (
|
|
167
166
|
objectConfig.accordionLabelField &&
|
|
168
167
|
typeof objectConfig.accordionLabelField === 'string' &&
|
|
169
168
|
objectConfig.accordionLabelField.trim().length > 0
|
|
170
169
|
) {
|
|
171
|
-
const label = item
|
|
170
|
+
const label = item[objectConfig.accordionLabelField] as string | Record<string, string>;
|
|
172
171
|
|
|
173
172
|
if (typeof label === 'string' && label.trim().length > 0) {
|
|
174
173
|
return `${label}`;
|
|
@@ -294,9 +293,9 @@
|
|
|
294
293
|
animate:flip={{ duration: 200 }}
|
|
295
294
|
>
|
|
296
295
|
{#key index}
|
|
297
|
-
{#if $value[index].
|
|
296
|
+
{#if $value[index]._slug}
|
|
298
297
|
{@const item = $value[index]}
|
|
299
|
-
{@const objectField = field.of.find((option) => option.slug === item.
|
|
298
|
+
{@const objectField = field.of.find((option) => option.slug === item._slug)}
|
|
300
299
|
|
|
301
300
|
{#if objectField}
|
|
302
301
|
<Accordion.Item value={index.toString()} class="overflow-hidden rounded-md border-0" data-depth={depth + 1}>
|
|
@@ -391,7 +390,7 @@
|
|
|
391
390
|
{:else}
|
|
392
391
|
<p class="text-red-500">
|
|
393
392
|
Invalid field configuration. Unknown slug:
|
|
394
|
-
{$value[index].
|
|
393
|
+
{$value[index]._slug}
|
|
395
394
|
</p>
|
|
396
395
|
{/if}
|
|
397
396
|
{:else}
|
|
@@ -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>
|