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.
Files changed (59) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/ROADMAP.md +19 -2
  3. package/dist/admin/api/handler.js +2 -0
  4. package/dist/admin/api/media-gc.js +8 -3
  5. package/dist/admin/api/regenerate-posters.d.ts +2 -0
  6. package/dist/admin/api/regenerate-posters.js +32 -0
  7. package/dist/admin/api/replace.js +4 -0
  8. package/dist/admin/api/rest/middleware/apiKey.js +7 -1
  9. package/dist/admin/api/upload.js +4 -0
  10. package/dist/admin/client/collection/collection-entries.svelte +8 -4
  11. package/dist/admin/client/collection/grid-view.svelte +1 -1
  12. package/dist/admin/client/entry/entry-header.svelte +37 -44
  13. package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -2
  14. package/dist/admin/client/entry/entry-version.svelte +9 -3
  15. package/dist/admin/client/entry/entry.svelte +20 -1
  16. package/dist/admin/client/maintenance/maintenance-page.svelte +153 -0
  17. package/dist/admin/components/fields/seo-field.svelte +30 -16
  18. package/dist/admin/remote/entry.remote.js +3 -4
  19. package/dist/admin/state/content-language.svelte.d.ts +0 -3
  20. package/dist/admin/state/content-language.svelte.js +7 -11
  21. package/dist/admin/utils/entryLabel.js +2 -3
  22. package/dist/cms/runtime/api.d.ts +5 -0
  23. package/dist/cms/runtime/types.d.ts +13 -8
  24. package/dist/core/cms.js +3 -0
  25. package/dist/core/fields/layoutUtils.d.ts +2 -2
  26. package/dist/core/fields/layoutUtils.js +3 -10
  27. package/dist/core/server/entries/operations/get.js +2 -2
  28. package/dist/core/server/media/mimeBlocklist.d.ts +1 -0
  29. package/dist/core/server/media/mimeBlocklist.js +31 -0
  30. package/dist/core/server/media/operations/batchRegenerateVideoPosters.d.ts +15 -0
  31. package/dist/core/server/media/operations/batchRegenerateVideoPosters.js +112 -0
  32. package/dist/files-local/index.d.ts +1 -0
  33. package/dist/files-local/index.js +3 -140
  34. package/dist/files-local/sanitizeFilename.js +2 -1
  35. package/dist/files-local/video.d.ts +9 -0
  36. package/dist/files-local/video.js +145 -0
  37. package/dist/paraglide/messages/_index.d.ts +3 -36
  38. package/dist/paraglide/messages/_index.js +3 -71
  39. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  40. package/dist/paraglide/messages/hello_world.js +33 -0
  41. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  42. package/dist/paraglide/messages/login_hello.js +34 -0
  43. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  44. package/dist/paraglide/messages/login_please_login.js +34 -0
  45. package/dist/sveltekit/server/handle.js +8 -0
  46. package/dist/updates/0.13.3/index.d.ts +2 -0
  47. package/dist/updates/0.13.3/index.js +21 -0
  48. package/dist/updates/0.13.4/index.d.ts +2 -0
  49. package/dist/updates/0.13.4/index.js +14 -0
  50. package/dist/updates/index.js +3 -1
  51. package/package.json +1 -1
  52. package/dist/admin/utils/translationStatus.d.ts +0 -17
  53. package/dist/admin/utils/translationStatus.js +0 -133
  54. package/dist/demo/reset.d.ts +0 -1
  55. package/dist/demo/reset.js +0 -26
  56. package/dist/paraglide/messages/en.d.ts +0 -5
  57. package/dist/paraglide/messages/en.js +0 -14
  58. package/dist/paraglide/messages/pl.d.ts +0 -5
  59. 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 { untrack } from 'svelte';
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
- <div>
243
- {#if field.slugSource}
244
- <div class="mb-1.5 flex items-center gap-2">
245
- <span class="text-sm font-medium text-muted-foreground">Auto</span>
246
- <Switch bind:checked={autoSlug} onCheckedChange={onAutoSlugToggle} />
247
- </div>
248
- {/if}
249
- <FieldRenderer
250
- field={slugField}
251
- {form}
252
- path={joinPath(path, 'slug')}
253
- readonly={autoSlug}
254
- />
255
- </div>
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[config.entryAdminTitle];
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[config.entryAdminTitle];
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 = $state(current);
10
- this.#referenceMode = $state(false);
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[config.entryAdminTitle] || entry.id)
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[config.entryAdminTitle] || entry.id)
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 { FlatImageFieldData, FlatVideoFieldData, StructuredContentDoc } from 'includio-cms/types';
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?: FlatImageFieldData | FlatVideoFieldData | null;
11
- favicon?: FlatImageFieldData | FlatVideoFieldData | null;
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
- photo?: FlatImageFieldData | FlatVideoFieldData | null;
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?: FlatImageFieldData | FlatVideoFieldData | null;
45
+ cover?: ImageFieldData | VideoFieldData | null;
43
46
  rating: number;
44
47
  category?: string;
45
48
  publishedAt?: string;
46
- thumbnail?: FlatImageFieldData | FlatVideoFieldData | null;
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?: FlatImageFieldData | FlatVideoFieldData | null;
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: FlatImageFieldData | FlatVideoFieldData;
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.data.title'
29
- * 'hero.contact.email' → 'hero.data.contact.data.email'
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.data.title'
102
- * 'hero.contact.email' → 'hero.data.contact.data.email'
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
- const parts = dotPath.split('.');
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[entryAdminTitle];
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
+ }>;