includio-cms 0.13.2 → 0.13.4
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 +32 -0
- package/ROADMAP.md +19 -2
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/media-gc.js +8 -3
- package/dist/admin/api/regenerate-posters.d.ts +2 -0
- package/dist/admin/api/regenerate-posters.js +32 -0
- package/dist/admin/api/replace.js +4 -0
- package/dist/admin/api/rest/middleware/apiKey.js +7 -1
- package/dist/admin/api/upload.js +4 -0
- package/dist/admin/client/collection/collection-entries.svelte +8 -4
- package/dist/admin/client/collection/grid-view.svelte +1 -1
- package/dist/admin/client/entry/entry-header.svelte +37 -44
- package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -2
- package/dist/admin/client/entry/entry-version.svelte +9 -3
- package/dist/admin/client/entry/entry.svelte +20 -1
- package/dist/admin/client/maintenance/maintenance-page.svelte +153 -0
- package/dist/admin/components/fields/seo-field.svelte +30 -16
- package/dist/admin/remote/entry.remote.js +3 -4
- package/dist/admin/state/content-language.svelte.d.ts +0 -3
- package/dist/admin/state/content-language.svelte.js +7 -11
- package/dist/admin/utils/entryLabel.js +2 -3
- package/dist/cms/runtime/api.d.ts +5 -0
- package/dist/cms/runtime/types.d.ts +13 -8
- package/dist/core/cms.js +3 -0
- package/dist/core/fields/layoutUtils.d.ts +2 -2
- package/dist/core/fields/layoutUtils.js +3 -10
- package/dist/core/server/entries/operations/get.js +2 -2
- package/dist/core/server/media/mimeBlocklist.d.ts +1 -0
- package/dist/core/server/media/mimeBlocklist.js +31 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.d.ts +15 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.js +112 -0
- package/dist/files-local/index.d.ts +1 -0
- package/dist/files-local/index.js +3 -140
- package/dist/files-local/sanitizeFilename.js +2 -1
- package/dist/files-local/video.d.ts +9 -0
- package/dist/files-local/video.js +145 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/sveltekit/server/handle.js +8 -0
- package/dist/updates/0.13.3/index.d.ts +2 -0
- package/dist/updates/0.13.3/index.js +21 -0
- package/dist/updates/0.13.4/index.d.ts +2 -0
- package/dist/updates/0.13.4/index.js +14 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/admin/utils/translationStatus.d.ts +0 -17
- package/dist/admin/utils/translationStatus.js +0 -133
- package/dist/demo/reset.d.ts +0 -1
- package/dist/demo/reset.js +0 -26
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import PlayerPlay from '@tabler/icons-svelte/icons/player-play';
|
|
9
9
|
import PlayerStop from '@tabler/icons-svelte/icons/player-stop';
|
|
10
10
|
import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
|
|
11
|
+
import Video from '@tabler/icons-svelte/icons/video';
|
|
11
12
|
import Button from '../../../components/ui/button/button.svelte';
|
|
12
13
|
import * as Card from '../../../components/ui/card/index.js';
|
|
13
14
|
import { toast } from 'svelte-sonner';
|
|
@@ -19,6 +20,9 @@
|
|
|
19
20
|
missingStylesCount: number;
|
|
20
21
|
orphanedDiskFiles: string[];
|
|
21
22
|
missingDiskRecords: { table: string; id: string; url: string }[];
|
|
23
|
+
videosCount: number;
|
|
24
|
+
videosWithPosters: number;
|
|
25
|
+
videosMissingPosters: number;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
let report = $state<GcReport | null>(null);
|
|
@@ -38,6 +42,18 @@
|
|
|
38
42
|
|
|
39
43
|
let genPercent = $derived(genTotal > 0 ? Math.round((genProcessed / genTotal) * 100) : 0);
|
|
40
44
|
|
|
45
|
+
// Video poster generation state
|
|
46
|
+
let posterGenerating = $state(false);
|
|
47
|
+
let posterTotal = $state(0);
|
|
48
|
+
let posterProcessed = $state(0);
|
|
49
|
+
let posterCreated = $state(0);
|
|
50
|
+
let posterSkipped = $state(0);
|
|
51
|
+
let posterCurrentFile = $state('');
|
|
52
|
+
let posterErrors = $state(0);
|
|
53
|
+
let posterAbort: AbortController | null = null;
|
|
54
|
+
|
|
55
|
+
let posterPercent = $derived(posterTotal > 0 ? Math.round((posterProcessed / posterTotal) * 100) : 0);
|
|
56
|
+
|
|
41
57
|
async function loadReport() {
|
|
42
58
|
loading = true;
|
|
43
59
|
try {
|
|
@@ -152,6 +168,77 @@
|
|
|
152
168
|
genAbort?.abort();
|
|
153
169
|
}
|
|
154
170
|
|
|
171
|
+
async function startPosterGenerate() {
|
|
172
|
+
posterGenerating = true;
|
|
173
|
+
posterTotal = 0;
|
|
174
|
+
posterProcessed = 0;
|
|
175
|
+
posterCreated = 0;
|
|
176
|
+
posterSkipped = 0;
|
|
177
|
+
posterCurrentFile = '';
|
|
178
|
+
posterErrors = 0;
|
|
179
|
+
posterAbort = new AbortController();
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch('/admin/api/regenerate-posters', {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
signal: posterAbort.signal
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!res.ok) throw new Error('Failed to start');
|
|
188
|
+
if (!res.body) throw new Error('No response body');
|
|
189
|
+
|
|
190
|
+
const reader = res.body.getReader();
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
let buffer = '';
|
|
193
|
+
|
|
194
|
+
while (true) {
|
|
195
|
+
const { done, value } = await reader.read();
|
|
196
|
+
if (done) break;
|
|
197
|
+
|
|
198
|
+
buffer += decoder.decode(value, { stream: true });
|
|
199
|
+
const chunks = buffer.split('\n\n');
|
|
200
|
+
buffer = chunks.pop() || '';
|
|
201
|
+
|
|
202
|
+
for (const chunk of chunks) {
|
|
203
|
+
if (!chunk.startsWith('data: ')) continue;
|
|
204
|
+
const event = JSON.parse(chunk.slice(6));
|
|
205
|
+
|
|
206
|
+
posterTotal = event.total ?? posterTotal;
|
|
207
|
+
posterProcessed = event.processed ?? posterProcessed;
|
|
208
|
+
posterCreated = event.created ?? posterCreated;
|
|
209
|
+
posterSkipped = event.skipped ?? posterSkipped;
|
|
210
|
+
posterCurrentFile = event.currentFile ?? posterCurrentFile;
|
|
211
|
+
|
|
212
|
+
if (event.type === 'error') {
|
|
213
|
+
posterErrors++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (event.type === 'done') {
|
|
217
|
+
const parts = [`Przetworzono ${posterTotal} filmów`];
|
|
218
|
+
if (posterCreated > 0) parts.push(`utworzono ${posterCreated} posterów`);
|
|
219
|
+
if (posterSkipped > 0) parts.push(`pominięto ${posterSkipped} (już istnieją)`);
|
|
220
|
+
if (posterErrors > 0) parts.push(`${posterErrors} błędów`);
|
|
221
|
+
toast.success(parts.join(', '));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
227
|
+
toast.info(`Przerwano po ${posterProcessed}/${posterTotal} filmów`);
|
|
228
|
+
} else {
|
|
229
|
+
toast.error('Błąd podczas generowania posterów');
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
posterGenerating = false;
|
|
233
|
+
posterAbort = null;
|
|
234
|
+
await loadReport();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cancelPosterGenerate() {
|
|
239
|
+
posterAbort?.abort();
|
|
240
|
+
}
|
|
241
|
+
|
|
155
242
|
$effect(() => {
|
|
156
243
|
loadReport();
|
|
157
244
|
});
|
|
@@ -260,6 +347,72 @@
|
|
|
260
347
|
</Card.Content>
|
|
261
348
|
</Card.Root>
|
|
262
349
|
|
|
350
|
+
<!-- Video posters -->
|
|
351
|
+
<Card.Root>
|
|
352
|
+
<Card.Header>
|
|
353
|
+
<div class="flex items-center gap-2">
|
|
354
|
+
<Video class="size-5" style="color: var(--primary);" />
|
|
355
|
+
<Card.Title>Postery video</Card.Title>
|
|
356
|
+
</div>
|
|
357
|
+
<Card.Description>
|
|
358
|
+
Miniaturki i postery generowane z plików wideo (ffmpeg)
|
|
359
|
+
</Card.Description>
|
|
360
|
+
</Card.Header>
|
|
361
|
+
<Card.Content>
|
|
362
|
+
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
363
|
+
{report.videosWithPosters}
|
|
364
|
+
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.videosCount}</span>
|
|
365
|
+
</p>
|
|
366
|
+
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
367
|
+
{report.videosCount} filmów, {report.videosMissingPosters} bez posterów
|
|
368
|
+
</p>
|
|
369
|
+
|
|
370
|
+
{#if posterGenerating}
|
|
371
|
+
<div class="mb-4">
|
|
372
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
373
|
+
<span>{posterProcessed}/{posterTotal} filmów</span>
|
|
374
|
+
<span>{posterPercent}%</span>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="h-2 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
377
|
+
<div
|
|
378
|
+
class="h-full rounded-full transition-all duration-300"
|
|
379
|
+
style="width: {posterPercent}%; background: var(--primary);"
|
|
380
|
+
></div>
|
|
381
|
+
</div>
|
|
382
|
+
<p class="mt-1 text-xs" style="color: var(--muted-foreground);">
|
|
383
|
+
Utworzono: {posterCreated}, pominięto: {posterSkipped}{posterErrors > 0 ? `, błędów: ${posterErrors}` : ''}
|
|
384
|
+
</p>
|
|
385
|
+
<p class="mt-0.5 truncate text-xs" style="color: var(--muted-foreground);">
|
|
386
|
+
{posterCurrentFile}
|
|
387
|
+
</p>
|
|
388
|
+
<Button
|
|
389
|
+
variant="outline"
|
|
390
|
+
size="sm"
|
|
391
|
+
onclick={cancelPosterGenerate}
|
|
392
|
+
class="mt-2"
|
|
393
|
+
>
|
|
394
|
+
<PlayerStop class="size-4" />
|
|
395
|
+
Anuluj
|
|
396
|
+
</Button>
|
|
397
|
+
</div>
|
|
398
|
+
{:else if report.videosCount > 0}
|
|
399
|
+
<Button
|
|
400
|
+
variant="default"
|
|
401
|
+
size="sm"
|
|
402
|
+
onclick={startPosterGenerate}
|
|
403
|
+
>
|
|
404
|
+
<PlayerPlay class="size-4" />
|
|
405
|
+
Generuj brakujące postery
|
|
406
|
+
</Button>
|
|
407
|
+
{:else}
|
|
408
|
+
<div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
|
|
409
|
+
<CircleCheck class="size-4" />
|
|
410
|
+
Brak plików wideo
|
|
411
|
+
</div>
|
|
412
|
+
{/if}
|
|
413
|
+
</Card.Content>
|
|
414
|
+
</Card.Root>
|
|
415
|
+
|
|
263
416
|
<!-- Orphaned disk files -->
|
|
264
417
|
<Card.Root>
|
|
265
418
|
<Card.Header>
|
|
@@ -8,15 +8,22 @@
|
|
|
8
8
|
MediaField,
|
|
9
9
|
SeoField,
|
|
10
10
|
SeoFieldData,
|
|
11
|
+
SlugField as SlugFieldType,
|
|
11
12
|
TextField
|
|
12
13
|
} from '../../../types/fields.js';
|
|
13
|
-
import {
|
|
14
|
+
import { formFieldProxy, type FormPathLeaves } from 'sveltekit-superforms';
|
|
15
|
+
import Input from '../../../components/ui/input/input.svelte';
|
|
16
|
+
import * as Form from '../../../components/ui/form/index.js';
|
|
17
|
+
import { getContext, untrack } from 'svelte';
|
|
14
18
|
import slugify from '../../imports/slugify.js';
|
|
15
19
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
16
20
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
17
21
|
import { Switch } from '../../../components/ui/switch/index.js';
|
|
18
22
|
|
|
19
23
|
const interfaceLanguage = useInterfaceLanguage();
|
|
24
|
+
const pathTemplate = getContext<string | null>('cms-path-template');
|
|
25
|
+
const pathPrefix = pathTemplate ? pathTemplate.replace('{slug}', '') : '/';
|
|
26
|
+
const wasPublished = getContext<boolean>('cms-entry-published') ?? false;
|
|
20
27
|
|
|
21
28
|
type Props = {
|
|
22
29
|
field: SeoField;
|
|
@@ -172,6 +179,10 @@
|
|
|
172
179
|
return 'text-destructive';
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
// Slug field proxy for direct input binding
|
|
183
|
+
const slugPath = joinPath(String(path), 'slug');
|
|
184
|
+
const { value: slugValue } = formFieldProxy(form, slugPath as FormPathLeaves<Record<string, unknown>>);
|
|
185
|
+
|
|
175
186
|
// Auto-gen: track last auto-generated value
|
|
176
187
|
let lastAutoSlug = '';
|
|
177
188
|
let lastAutoTitle = '';
|
|
@@ -179,6 +190,7 @@
|
|
|
179
190
|
// Auto slug toggle
|
|
180
191
|
let autoSlug = $state((() => {
|
|
181
192
|
if (!field.slugSource) return false;
|
|
193
|
+
if (wasPublished) return false;
|
|
182
194
|
const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
|
|
183
195
|
if (!sourceRaw || typeof sourceRaw !== 'string') return true;
|
|
184
196
|
const slugPath = joinPath(String(path), 'slug');
|
|
@@ -238,21 +250,23 @@
|
|
|
238
250
|
</script>
|
|
239
251
|
|
|
240
252
|
<div class="space-y-4">
|
|
241
|
-
<!-- Slug field with auto/manual toggle -->
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
<!-- Slug field with auto/manual toggle + path prefix -->
|
|
254
|
+
<Form.Field {form} name={slugPath} class="space-y-1">
|
|
255
|
+
<div class="flex items-center justify-between">
|
|
256
|
+
<Form.Label>{getLocalizedLabel(labels.slug.label, interfaceLanguage.current)}</Form.Label>
|
|
257
|
+
{#if field.slugSource}
|
|
258
|
+
<div class="flex items-center gap-2">
|
|
259
|
+
<span class="text-sm font-medium text-muted-foreground">Auto</span>
|
|
260
|
+
<Switch bind:checked={autoSlug} onCheckedChange={onAutoSlugToggle} />
|
|
261
|
+
</div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
264
|
+
<div class="flex">
|
|
265
|
+
<span class="border-input bg-muted text-muted-foreground flex h-9 shrink-0 items-center rounded-l-md border border-r-0 px-2.5 font-mono text-sm">{pathPrefix}</span>
|
|
266
|
+
<Input bind:value={$slugValue} readonly={autoSlug} class="rounded-l-none border-l-0" />
|
|
267
|
+
</div>
|
|
268
|
+
<Form.Description>{getLocalizedLabel(labels.slug.description, interfaceLanguage.current)}</Form.Description>
|
|
269
|
+
</Form.Field>
|
|
256
270
|
{#each fields as f}
|
|
257
271
|
<div>
|
|
258
272
|
<FieldRenderer
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { command, query } from '$app/server';
|
|
2
|
+
import { getAtPath } from '../utils/objectPath.js';
|
|
2
3
|
import { createEntry as createEntryOperation, createEntrySchema, createEntryVersion } from '../../core/server/entries/operations/create.js';
|
|
3
4
|
import { getRawEntries as getRawEntriesOperation, countRawEntries as countRawEntriesOperation, getRawEntry as getRawEntryOperation, getRawEntryOrThrow, getDbEntry, getDbEntryOrThrow, getEntries as getEntriesOperation, getEntry as getEntryOperation, getEntryVersion as getEntryVersionOperation, getEntryLabels as getEntryLabelsOperation } from '../../core/server/entries/operations/get.js';
|
|
4
5
|
import { getCMS } from '../../core/cms.js';
|
|
@@ -208,8 +209,7 @@ export const getRecentEntries = query(z.number().default(6), async (limit) => {
|
|
|
208
209
|
const latestVersion = entry.versions[0];
|
|
209
210
|
let label = null;
|
|
210
211
|
if (config && config.type === 'collection' && config.entryAdminTitle && latestVersion) {
|
|
211
|
-
const titleData = latestVersion.data
|
|
212
|
-
// Data is flat — titleData is the string directly
|
|
212
|
+
const titleData = getAtPath(latestVersion.data, config.entryAdminTitle);
|
|
213
213
|
if (typeof titleData === 'string') {
|
|
214
214
|
label = titleData || '';
|
|
215
215
|
}
|
|
@@ -257,8 +257,7 @@ export const getRecentActivity = query(z.number().default(10), async (limit) =>
|
|
|
257
257
|
const config = getCMS().getBySlug(entry.slug);
|
|
258
258
|
let label = null;
|
|
259
259
|
if (config && config.type === 'collection' && config.entryAdminTitle) {
|
|
260
|
-
const titleData = latestVersion.data
|
|
261
|
-
// Data is flat — titleData is the string directly
|
|
260
|
+
const titleData = getAtPath(latestVersion.data, config.entryAdminTitle);
|
|
262
261
|
if (typeof titleData === 'string') {
|
|
263
262
|
label = titleData || null;
|
|
264
263
|
}
|
|
@@ -2,7 +2,6 @@ export declare const getContentLanguage: () => ContentLanguage, setContentLangua
|
|
|
2
2
|
type _ContentLanguage = {
|
|
3
3
|
all: string[];
|
|
4
4
|
current: string;
|
|
5
|
-
referenceMode: boolean;
|
|
6
5
|
};
|
|
7
6
|
export declare class ContentLanguage {
|
|
8
7
|
#private;
|
|
@@ -10,7 +9,5 @@ export declare class ContentLanguage {
|
|
|
10
9
|
get all(): string[];
|
|
11
10
|
get current(): _ContentLanguage["current"];
|
|
12
11
|
set current(value: _ContentLanguage['current']);
|
|
13
|
-
get referenceMode(): boolean;
|
|
14
|
-
set referenceMode(value: boolean);
|
|
15
12
|
}
|
|
16
13
|
export {};
|
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
import { createContext } from 'svelte';
|
|
2
|
+
import { PersistedState } from 'runed';
|
|
2
3
|
export const [getContentLanguage, setContentLanguage] = createContext();
|
|
3
4
|
export class ContentLanguage {
|
|
4
5
|
#all;
|
|
5
6
|
#current;
|
|
6
|
-
#referenceMode;
|
|
7
7
|
constructor(all, current) {
|
|
8
8
|
this.#all = $state(all);
|
|
9
|
-
this.#current =
|
|
10
|
-
this.#
|
|
9
|
+
this.#current = new PersistedState('content-language', current);
|
|
10
|
+
if (!all.includes(this.#current.current)) {
|
|
11
|
+
this.#current.current = current;
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
get all() {
|
|
13
15
|
return this.#all;
|
|
14
16
|
}
|
|
15
17
|
get current() {
|
|
16
|
-
return this.#current;
|
|
18
|
+
return this.#current.current;
|
|
17
19
|
}
|
|
18
20
|
set current(value) {
|
|
19
|
-
this.#current = value;
|
|
20
|
-
}
|
|
21
|
-
get referenceMode() {
|
|
22
|
-
return this.#referenceMode;
|
|
23
|
-
}
|
|
24
|
-
set referenceMode(value) {
|
|
25
|
-
this.#referenceMode = value;
|
|
21
|
+
this.#current.current = value;
|
|
26
22
|
}
|
|
27
23
|
}
|
|
@@ -2,15 +2,14 @@ import { getAtPath } from './objectPath.js';
|
|
|
2
2
|
export function getRawCollectionEntryLabel(entry, config, language) {
|
|
3
3
|
const publishedVersion = entry.publishedVersions[language];
|
|
4
4
|
if (publishedVersion) {
|
|
5
|
-
// Data is flat — entryAdminTitle value is directly a string
|
|
6
5
|
return config.entryAdminTitle
|
|
7
|
-
? String(publishedVersion.data
|
|
6
|
+
? String(getAtPath(publishedVersion.data, config.entryAdminTitle) || entry.id)
|
|
8
7
|
: entry.id;
|
|
9
8
|
}
|
|
10
9
|
const draftVersion = entry.draftVersions[language];
|
|
11
10
|
if (draftVersion) {
|
|
12
11
|
return config.entryAdminTitle
|
|
13
|
-
? String(draftVersion.data
|
|
12
|
+
? String(getAtPath(draftVersion.data, config.entryAdminTitle) || entry.id)
|
|
14
13
|
: entry.id;
|
|
15
14
|
}
|
|
16
15
|
return entry.id;
|
|
@@ -8,10 +8,15 @@ interface GetEntryOptions {
|
|
|
8
8
|
interface GetEntriesOptions extends GetEntryOptions {
|
|
9
9
|
ids?: string[];
|
|
10
10
|
dataLike?: Record<string, unknown>;
|
|
11
|
+
dataILikeOr?: Record<string, unknown>;
|
|
11
12
|
orderBy?: {
|
|
12
13
|
column: 'createdAt' | 'updatedAt' | 'sortOrder';
|
|
13
14
|
direction: 'asc' | 'desc';
|
|
14
15
|
};
|
|
16
|
+
dataOrderBy?: {
|
|
17
|
+
field: string;
|
|
18
|
+
direction: 'asc' | 'desc';
|
|
19
|
+
};
|
|
15
20
|
limit?: number;
|
|
16
21
|
offset?: number;
|
|
17
22
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ImageFieldData, VideoFieldData, StructuredContentDoc } from 'includio-cms/types';
|
|
2
2
|
export type SingleSlug = "settings" | "image-showcase";
|
|
3
3
|
export interface Settings {
|
|
4
4
|
_id: string;
|
|
5
5
|
_slug: string;
|
|
6
6
|
_type: string;
|
|
7
7
|
_publishedAt: Date | null;
|
|
8
|
+
_url?: string;
|
|
8
9
|
siteName: string;
|
|
9
10
|
description?: string;
|
|
10
|
-
logo?:
|
|
11
|
-
favicon?:
|
|
11
|
+
logo?: ImageFieldData | VideoFieldData | null;
|
|
12
|
+
favicon?: ImageFieldData | VideoFieldData | null;
|
|
12
13
|
socialLinks?: ({
|
|
13
14
|
_slug: 'socialLink';
|
|
14
15
|
platform: string;
|
|
@@ -25,7 +26,8 @@ export interface ImageShowcase {
|
|
|
25
26
|
_slug: string;
|
|
26
27
|
_type: string;
|
|
27
28
|
_publishedAt: Date | null;
|
|
28
|
-
|
|
29
|
+
_url?: string;
|
|
30
|
+
photo?: ImageFieldData | VideoFieldData | null;
|
|
29
31
|
}
|
|
30
32
|
export type SingleEntryMap = {
|
|
31
33
|
settings: Settings;
|
|
@@ -37,13 +39,14 @@ export interface BlogPost {
|
|
|
37
39
|
_slug: string;
|
|
38
40
|
_type: string;
|
|
39
41
|
_publishedAt: Date | null;
|
|
42
|
+
_url?: string;
|
|
40
43
|
title: string;
|
|
41
44
|
slug?: string;
|
|
42
|
-
cover?:
|
|
45
|
+
cover?: ImageFieldData | VideoFieldData | null;
|
|
43
46
|
rating: number;
|
|
44
47
|
category?: string;
|
|
45
48
|
publishedAt?: string;
|
|
46
|
-
thumbnail?:
|
|
49
|
+
thumbnail?: ImageFieldData | VideoFieldData | null;
|
|
47
50
|
content?: StructuredContentDoc;
|
|
48
51
|
tags?: string[];
|
|
49
52
|
seo: {
|
|
@@ -65,6 +68,7 @@ export interface Project {
|
|
|
65
68
|
_slug: string;
|
|
66
69
|
_type: string;
|
|
67
70
|
_publishedAt: Date | null;
|
|
71
|
+
_url?: string;
|
|
68
72
|
title: string;
|
|
69
73
|
description?: string;
|
|
70
74
|
status?: string;
|
|
@@ -72,7 +76,7 @@ export interface Project {
|
|
|
72
76
|
url?: {
|
|
73
77
|
url: string;
|
|
74
78
|
};
|
|
75
|
-
image?:
|
|
79
|
+
image?: ImageFieldData | VideoFieldData | null;
|
|
76
80
|
techStack?: ({
|
|
77
81
|
_slug: 'tech';
|
|
78
82
|
name: string;
|
|
@@ -92,6 +96,7 @@ export interface ArrayTest {
|
|
|
92
96
|
_slug: string;
|
|
93
97
|
_type: string;
|
|
94
98
|
_publishedAt: Date | null;
|
|
99
|
+
_url?: string;
|
|
95
100
|
name: string;
|
|
96
101
|
tags?: string[];
|
|
97
102
|
localizedTags?: Record<string, string>[];
|
|
@@ -104,7 +109,7 @@ export interface ArrayTest {
|
|
|
104
109
|
body?: StructuredContentDoc;
|
|
105
110
|
} | {
|
|
106
111
|
_slug: 'image-block';
|
|
107
|
-
image:
|
|
112
|
+
image: ImageFieldData | VideoFieldData;
|
|
108
113
|
caption?: string;
|
|
109
114
|
})[];
|
|
110
115
|
highlights?: ({
|
package/dist/core/cms.js
CHANGED
|
@@ -51,6 +51,9 @@ export class CMS {
|
|
|
51
51
|
};
|
|
52
52
|
});
|
|
53
53
|
this.languages = config.languages || [];
|
|
54
|
+
if (this.languages.length === 0) {
|
|
55
|
+
throw new Error('CMS config must include at least one language.');
|
|
56
|
+
}
|
|
54
57
|
this.apiKeys = config.apiKeys || [];
|
|
55
58
|
if (config.plugins) {
|
|
56
59
|
this.plugins = config.plugins;
|
|
@@ -25,8 +25,8 @@ export declare function collectAllLeafPaths(fields: Field[], prefix?: string): s
|
|
|
25
25
|
export declare function getDistributedObjectSlugs(nodes: LayoutNode[], fields: Field[]): Set<string>;
|
|
26
26
|
/**
|
|
27
27
|
* Build SuperForm-compatible path for a dot-notation field reference.
|
|
28
|
-
* 'hero.title' → 'hero.
|
|
29
|
-
* 'hero.contact.email' → 'hero.
|
|
28
|
+
* 'hero.title' → 'hero.title'
|
|
29
|
+
* 'hero.contact.email' → 'hero.contact.email'
|
|
30
30
|
* 'title' → 'title' (no change for top-level)
|
|
31
31
|
*/
|
|
32
32
|
export declare function buildFormPath(dotPath: string): string;
|
|
@@ -98,19 +98,12 @@ export function getDistributedObjectSlugs(nodes, fields) {
|
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
100
|
* Build SuperForm-compatible path for a dot-notation field reference.
|
|
101
|
-
* 'hero.title' → 'hero.
|
|
102
|
-
* 'hero.contact.email' → 'hero.
|
|
101
|
+
* 'hero.title' → 'hero.title'
|
|
102
|
+
* 'hero.contact.email' → 'hero.contact.email'
|
|
103
103
|
* 'title' → 'title' (no change for top-level)
|
|
104
104
|
*/
|
|
105
105
|
export function buildFormPath(dotPath) {
|
|
106
|
-
|
|
107
|
-
if (parts.length <= 1)
|
|
108
|
-
return dotPath;
|
|
109
|
-
const result = [parts[0]];
|
|
110
|
-
for (let i = 1; i < parts.length; i++) {
|
|
111
|
-
result.push('data', parts[i]);
|
|
112
|
-
}
|
|
113
|
-
return result.join('.');
|
|
106
|
+
return dotPath;
|
|
114
107
|
}
|
|
115
108
|
/** Count columns expected by a ratio string */
|
|
116
109
|
function columnCount(ratio) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
|
+
import { getAtPath } from '../../../../admin/utils/objectPath.js';
|
|
2
3
|
import { populateEntryData } from '../../fields/populateEntry.js';
|
|
3
4
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
4
5
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
|
|
@@ -303,8 +304,7 @@ export const getEntryLabels = async (options) => {
|
|
|
303
304
|
const latestVersion = publishedVersion ?? sorted[0] ?? null;
|
|
304
305
|
let label = entry.id;
|
|
305
306
|
if (entryAdminTitle && latestVersion) {
|
|
306
|
-
const titleData = latestVersion.data
|
|
307
|
-
// Data is now flat — titleData is the string directly
|
|
307
|
+
const titleData = getAtPath(latestVersion.data, entryAdminTitle);
|
|
308
308
|
if (typeof titleData === 'string') {
|
|
309
309
|
label = titleData || entry.id;
|
|
310
310
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isBlockedMimeType(mimeType: string, fileName?: string): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const BLOCKED_MIME_TYPES = new Set([
|
|
2
|
+
'application/x-msdownload',
|
|
3
|
+
'application/x-executable',
|
|
4
|
+
'application/x-msdos-program',
|
|
5
|
+
'application/x-sh',
|
|
6
|
+
'application/x-shellscript',
|
|
7
|
+
'application/x-bat',
|
|
8
|
+
'application/x-msi',
|
|
9
|
+
'application/java-archive',
|
|
10
|
+
'application/x-httpd-php',
|
|
11
|
+
'text/x-php',
|
|
12
|
+
'application/hta'
|
|
13
|
+
]);
|
|
14
|
+
const BLOCKED_EXTENSIONS = new Set([
|
|
15
|
+
'.exe', '.bat', '.cmd', '.com', '.msi', '.scr', '.pif',
|
|
16
|
+
'.sh', '.bash', '.csh', '.ksh',
|
|
17
|
+
'.php', '.php3', '.php4', '.php5', '.phtml',
|
|
18
|
+
'.jsp', '.asp', '.aspx',
|
|
19
|
+
'.jar', '.class',
|
|
20
|
+
'.hta', '.vbs', '.vbe', '.wsf', '.wsh', '.ps1'
|
|
21
|
+
]);
|
|
22
|
+
export function isBlockedMimeType(mimeType, fileName) {
|
|
23
|
+
if (BLOCKED_MIME_TYPES.has(mimeType.toLowerCase()))
|
|
24
|
+
return true;
|
|
25
|
+
if (fileName) {
|
|
26
|
+
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
|
|
27
|
+
if (BLOCKED_EXTENSIONS.has(ext))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type PosterBatchProgress = {
|
|
2
|
+
type: 'progress' | 'error' | 'done';
|
|
3
|
+
total: number;
|
|
4
|
+
processed: number;
|
|
5
|
+
created: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
currentFile?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function batchRegenerateVideoPosters(signal?: AbortSignal): AsyncGenerator<PosterBatchProgress>;
|
|
11
|
+
export declare function getVideoPosterStatus(): Promise<{
|
|
12
|
+
videosCount: number;
|
|
13
|
+
videosWithPosters: number;
|
|
14
|
+
videosMissingPosters: number;
|
|
15
|
+
}>;
|