includio-cms 0.7.1 → 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.
Files changed (157) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/ROADMAP.md +44 -2
  3. package/dist/admin/api/generate-styles.d.ts +2 -0
  4. package/dist/admin/api/generate-styles.js +32 -0
  5. package/dist/admin/api/handler.js +33 -0
  6. package/dist/admin/api/media-gc.js +10 -4
  7. package/dist/admin/api/rest/handler.js +17 -0
  8. package/dist/admin/api/rest/routes/collections.js +25 -13
  9. package/dist/admin/api/rest/routes/entries.d.ts +1 -1
  10. package/dist/admin/api/rest/routes/entries.js +10 -10
  11. package/dist/admin/api/rest/routes/media.d.ts +2 -0
  12. package/dist/admin/api/rest/routes/media.js +9 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +5 -0
  14. package/dist/admin/api/rest/routes/schema.js +152 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
  16. package/dist/admin/api/rest/routes/singletons.js +8 -7
  17. package/dist/admin/api/rest/routes/upload.d.ts +2 -0
  18. package/dist/admin/api/rest/routes/upload.js +28 -0
  19. package/dist/admin/api/upload.js +13 -0
  20. package/dist/admin/client/collection/collection-entries.svelte +19 -6
  21. package/dist/admin/client/entry/entry.svelte +21 -23
  22. package/dist/admin/client/entry/header/a11y-validator.js +2 -2
  23. package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
  24. package/dist/admin/client/entry/header/status-badge.svelte +2 -2
  25. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
  26. package/dist/admin/client/entry/header/visibility.svelte +16 -10
  27. package/dist/admin/client/entry/utils.d.ts +3 -0
  28. package/dist/admin/client/entry/utils.js +22 -4
  29. package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
  30. package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
  31. package/dist/admin/client/index.d.ts +1 -0
  32. package/dist/admin/client/index.js +1 -0
  33. package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
  34. package/dist/admin/components/fields/blocks-field.svelte +9 -10
  35. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  36. package/dist/admin/components/fields/object-field.svelte +7 -12
  37. package/dist/admin/components/fields/select-field.svelte +8 -2
  38. package/dist/admin/components/fields/seo-field.svelte +40 -93
  39. package/dist/admin/components/fields/simple-array-field.svelte +5 -5
  40. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  41. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  42. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  43. package/dist/admin/components/fields/url-field.svelte +61 -72
  44. package/dist/admin/components/media/file-upload.svelte +5 -1
  45. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  46. package/dist/admin/components/media/media-library.svelte +109 -37
  47. package/dist/admin/components/media/media-selector.svelte +79 -11
  48. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  49. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  50. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +21 -93
  51. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  52. package/dist/admin/components/tiptap/link-dialog.svelte +10 -11
  53. package/dist/admin/components/tiptap/slash-command.js +1 -1
  54. package/dist/admin/remote/entry.remote.d.ts +2 -5
  55. package/dist/admin/remote/entry.remote.js +22 -27
  56. package/dist/admin/remote/media.remote.d.ts +15 -0
  57. package/dist/admin/remote/media.remote.js +18 -2
  58. package/dist/admin/remote/preview.remote.js +3 -1
  59. package/dist/admin/utils/entryLabel.js +9 -6
  60. package/dist/admin/utils/translationStatus.js +1 -2
  61. package/dist/cli/create-user.d.ts +2 -0
  62. package/dist/cli/create-user.js +81 -0
  63. package/dist/cli/index.js +5 -0
  64. package/dist/cli/scaffold/admin.js +34 -2
  65. package/dist/cms/runtime/api.d.ts +16 -12
  66. package/dist/cms/runtime/api.js +7 -6
  67. package/dist/cms/runtime/remote.js +2 -2
  68. package/dist/cms/runtime/schemas.d.ts +1 -1
  69. package/dist/cms/runtime/schemas.js +1 -1
  70. package/dist/cms/runtime/types.d.ts +118 -112
  71. package/dist/cms/runtime/types.js +0 -12
  72. package/dist/core/cms.d.ts +3 -1
  73. package/dist/core/cms.js +30 -0
  74. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  75. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  76. package/dist/core/server/entries/operations/create.js +10 -4
  77. package/dist/core/server/entries/operations/get.d.ts +1 -0
  78. package/dist/core/server/entries/operations/get.js +186 -191
  79. package/dist/core/server/entries/operations/update.d.ts +6 -7
  80. package/dist/core/server/entries/operations/update.js +20 -38
  81. package/dist/core/server/fields/populateEntry.js +16 -52
  82. package/dist/core/server/fields/resolveImageFields.js +69 -120
  83. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  84. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  85. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  86. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  87. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  88. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  89. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  90. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  91. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  92. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  93. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  94. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  95. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  96. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  97. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  98. package/dist/core/server/generator/fields.d.ts +6 -0
  99. package/dist/core/server/generator/fields.js +43 -5
  100. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  101. package/dist/core/server/generator/formFields.js +1 -0
  102. package/dist/core/server/generator/generator.js +98 -30
  103. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  104. package/dist/core/server/media/operations/getFiles.js +6 -0
  105. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  106. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  107. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  108. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  109. package/dist/db-postgres/index.js +303 -37
  110. package/dist/db-postgres/schema/entry.d.ts +0 -94
  111. package/dist/db-postgres/schema/entry.js +0 -6
  112. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  113. package/dist/db-postgres/schema/entryVersion.js +1 -0
  114. package/dist/entity/index.d.ts +9 -4
  115. package/dist/entity/index.js +24 -24
  116. package/dist/files-local/index.js +43 -0
  117. package/dist/sveltekit/components/preview.svelte +2 -326
  118. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  119. package/dist/sveltekit/server/index.d.ts +2 -1
  120. package/dist/sveltekit/server/index.js +2 -1
  121. package/dist/sveltekit/server/preview.js +4 -7
  122. package/dist/types/adapters/db.d.ts +15 -1
  123. package/dist/types/adapters/files.d.ts +6 -0
  124. package/dist/types/cms.d.ts +5 -0
  125. package/dist/types/entries.d.ts +54 -18
  126. package/dist/types/fields.d.ts +14 -24
  127. package/dist/types/formFields.d.ts +7 -2
  128. package/dist/types/index.d.ts +2 -2
  129. package/dist/types/structured-content.d.ts +5 -0
  130. package/dist/updates/0.10.0/index.d.ts +2 -0
  131. package/dist/updates/0.10.0/index.js +15 -0
  132. package/dist/updates/0.11.0/index.d.ts +2 -0
  133. package/dist/updates/0.11.0/index.js +12 -0
  134. package/dist/updates/0.12.0/index.d.ts +2 -0
  135. package/dist/updates/0.12.0/index.js +12 -0
  136. package/dist/updates/0.13.0/index.d.ts +2 -0
  137. package/dist/updates/0.13.0/index.js +10 -0
  138. package/dist/updates/0.7.2/index.d.ts +2 -0
  139. package/dist/updates/0.7.2/index.js +10 -0
  140. package/dist/updates/0.7.3/index.d.ts +2 -0
  141. package/dist/updates/0.7.3/index.js +10 -0
  142. package/dist/updates/0.8.0/index.d.ts +2 -0
  143. package/dist/updates/0.8.0/index.js +18 -0
  144. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  145. package/dist/updates/0.8.0/migrate.js +101 -0
  146. package/dist/updates/0.9.0/index.d.ts +2 -0
  147. package/dist/updates/0.9.0/index.js +38 -0
  148. package/dist/updates/index.js +9 -1
  149. package/package.json +7 -6
  150. package/dist/admin/components/fields/image-field.svelte +0 -198
  151. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  152. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  153. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  154. package/dist/admin/components/tiptap.svelte +0 -11
  155. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  156. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  157. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
@@ -6,6 +6,7 @@ export const entryVersionsTable = pgTable('entry_version', {
6
6
  entryId: uuid('entry_id')
7
7
  .notNull()
8
8
  .references(() => entriesTable.id, { onDelete: 'cascade' }),
9
+ lang: text('lang').notNull(),
9
10
  versionNumber: integer('version_number').notNull(),
10
11
  data: jsonb('data').notNull().$type(),
11
12
  createdAt: timestamp('created_at').defaultNow().notNull(),
@@ -8,12 +8,15 @@ interface CreateOptions {
8
8
  sortOrder?: number;
9
9
  }
10
10
  export declare function createEntityAPI(cms: CMS, opts?: EntityAPIOptions): {
11
- create(slug: string, data?: EntryData, options?: CreateOptions): Promise<DbEntry>;
11
+ create(slug: string, data?: EntryData, options?: CreateOptions & {
12
+ lang?: string;
13
+ }): Promise<DbEntry>;
12
14
  update(entryId: string, data: EntryData, options?: {
13
15
  skipValidation?: boolean;
16
+ lang?: string;
14
17
  }): Promise<DbEntryVersion>;
15
- publish(entryId: string): Promise<void>;
16
- unpublish(entryId: string): Promise<void>;
18
+ publish(entryId: string, lang?: string): Promise<void>;
19
+ unpublish(entryId: string, lang?: string): Promise<void>;
17
20
  archive(entryId: string): Promise<void>;
18
21
  unarchive(entryId: string): Promise<void>;
19
22
  delete(entryId: string): Promise<void>;
@@ -21,6 +24,8 @@ export declare function createEntityAPI(cms: CMS, opts?: EntityAPIOptions): {
21
24
  includeArchived?: boolean;
22
25
  onlyArchived?: boolean;
23
26
  }): Promise<import("../types/entries.js").RawEntry[]>;
24
- createAndPublish(slug: string, data: EntryData, options?: CreateOptions): Promise<DbEntry>;
27
+ createAndPublish(slug: string, data: EntryData, options?: CreateOptions & {
28
+ lang?: string;
29
+ }): Promise<DbEntry>;
25
30
  };
26
31
  export {};
@@ -4,6 +4,7 @@ import { getRawEntries } from '../core/server/entries/operations/get.js';
4
4
  export function createEntityAPI(cms, opts) {
5
5
  const db = cms.databaseAdapter;
6
6
  const userId = opts?.userId ?? 'system';
7
+ const defaultLang = cms.languages[0];
7
8
  function validate(slug, data) {
8
9
  const config = cms.getBySlug(slug);
9
10
  const schema = generateZodSchemaFromFields(getFieldsFromConfig(config), cms.languages);
@@ -22,24 +23,27 @@ export function createEntityAPI(cms, opts) {
22
23
  type,
23
24
  sortOrder: options?.sortOrder ?? null
24
25
  });
26
+ const lang = options?.lang ?? defaultLang;
25
27
  const entryData = data ?? {};
26
28
  const validatedData = data && !options?.skipValidation ? validate(slug, entryData) : entryData;
27
29
  await db.createEntryVersion({
28
30
  entryId: entry.id,
31
+ lang,
29
32
  data: validatedData,
30
33
  createdBy: userId
31
34
  });
32
35
  return entry;
33
36
  },
34
37
  async update(entryId, data, options) {
38
+ const lang = options?.lang ?? defaultLang;
35
39
  const entries = await db.getEntries({ ids: [entryId] });
36
40
  const entry = entries[0];
37
41
  if (!entry)
38
42
  throw Error('Entry not found');
39
43
  const validatedData = !options?.skipValidation ? validate(entry.slug, data) : data;
40
- const versions = await db.getEntryVersions({ entryIds: [entryId] });
44
+ const versions = await db.getEntryVersions({ entryIds: [entryId], lang });
41
45
  const sorted = versions.sort((a, b) => b.versionNumber - a.versionNumber);
42
- const latestDraft = sorted.find((v) => v.id !== entry.publishedVersionId);
46
+ const latestDraft = sorted.find((v) => v.publishedAt == null);
43
47
  if (latestDraft) {
44
48
  if (JSON.stringify(latestDraft.data) === JSON.stringify(validatedData)) {
45
49
  return latestDraft;
@@ -51,40 +55,36 @@ export function createEntityAPI(cms, opts) {
51
55
  }
52
56
  return db.createEntryVersion({
53
57
  entryId,
58
+ lang,
54
59
  data: validatedData,
55
60
  createdBy: userId
56
61
  });
57
62
  },
58
- async publish(entryId) {
59
- const versions = await db.getEntryVersions({ entryIds: [entryId] });
60
- const entries = await db.getEntries({ ids: [entryId] });
61
- const entry = entries[0];
62
- if (!entry)
63
- throw Error('Entry not found');
63
+ async publish(entryId, lang) {
64
+ const targetLang = lang ?? defaultLang;
65
+ const versions = await db.getEntryVersions({ entryIds: [entryId], lang: targetLang });
64
66
  const draft = versions
65
- .filter((v) => v.id !== entry.publishedVersionId)
67
+ .filter((v) => v.publishedAt == null)
66
68
  .sort((a, b) => b.versionNumber - a.versionNumber)[0];
67
69
  if (!draft)
68
70
  throw Error('No draft to publish');
69
- await db.updateEntry({
70
- id: entryId,
71
+ await db.updateEntryVersion({
72
+ id: draft.id,
71
73
  data: {
72
- publishedVersionId: draft.id,
73
74
  publishedAt: new Date(),
74
- publishedBy: userId,
75
- archivedAt: null
75
+ publishedBy: userId
76
76
  }
77
77
  });
78
78
  },
79
- async unpublish(entryId) {
80
- await db.updateEntry({
81
- id: entryId,
82
- data: {
83
- publishedVersionId: null,
84
- publishedAt: null,
85
- publishedBy: null
86
- }
87
- });
79
+ async unpublish(entryId, lang) {
80
+ const targetLang = lang ?? defaultLang;
81
+ const versions = await db.getEntryVersions({ entryIds: [entryId], lang: targetLang });
82
+ await Promise.all(versions
83
+ .filter((v) => v.publishedAt !== null)
84
+ .map((v) => db.updateEntryVersion({
85
+ id: v.id,
86
+ data: { publishedAt: null, publishedBy: null }
87
+ })));
88
88
  },
89
89
  async archive(entryId) {
90
90
  await db.updateEntry({
@@ -106,7 +106,7 @@ export function createEntityAPI(cms, opts) {
106
106
  },
107
107
  async createAndPublish(slug, data, options) {
108
108
  const entry = await this.create(slug, data, options);
109
- await this.publish(entry.id);
109
+ await this.publish(entry.id, options?.lang);
110
110
  return entry;
111
111
  }
112
112
  };
@@ -10,6 +10,7 @@ const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
  global.__dirname = __dirname ?? '';
12
12
  const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
13
+ const privateDir = process.env.NODE_ENV === 'production' ? `/data/private-uploads` : `./data/private-uploads`;
13
14
  // Ustawienie ścieżek do ffmpeg
14
15
  const ffmpegPath = process.env.FFMPEG_PATH || '/usr/bin/ffmpeg';
15
16
  const ffprobePath = process.env.FFPROBE_PATH || '/usr/bin/ffprobe';
@@ -311,6 +312,48 @@ export function local() {
311
312
  catch {
312
313
  return [];
313
314
  }
315
+ },
316
+ uploadPrivateFile: async (file) => {
317
+ await ensureDir(privateDir);
318
+ const ext = extname(file.name);
319
+ const rawName = basename(file.name, ext);
320
+ const safeName = sanitizeFilename(rawName);
321
+ const filename = `${randomUUID().slice(0, 8)}-${safeName}${ext}`;
322
+ const filepath = resolve(privateDir, filename);
323
+ if (!filepath.startsWith(resolve(privateDir))) {
324
+ throw new Error('Invalid filename');
325
+ }
326
+ const arrayBuffer = await file.arrayBuffer();
327
+ await writeFile(filepath, Buffer.from(arrayBuffer));
328
+ return {
329
+ filename,
330
+ url: `/admin/api/media/private/${filename}`
331
+ };
332
+ },
333
+ downloadPrivateFile: async (filename) => {
334
+ const filepath = resolve(privateDir, filename);
335
+ if (!filepath.startsWith(resolve(privateDir))) {
336
+ return null;
337
+ }
338
+ try {
339
+ const data = await readFile(filepath);
340
+ return new File([data], filename);
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ },
346
+ deletePrivateFile: async (filename) => {
347
+ const filepath = resolve(privateDir, filename);
348
+ if (!filepath.startsWith(resolve(privateDir))) {
349
+ throw new Error('Invalid filename');
350
+ }
351
+ try {
352
+ await unlink(filepath);
353
+ }
354
+ catch {
355
+ // File may already be deleted
356
+ }
314
357
  }
315
358
  };
316
359
  }
@@ -1,9 +1,5 @@
1
- <script lang="ts" generics="T extends { data: PopulatedEntryData }">
2
- import type { PopulatedEntryData } from '../../types/entries.js';
1
+ <script lang="ts" generics="T extends Record<string, unknown>">
3
2
  import { onMount, type Snippet } from 'svelte';
4
- import { enableHybridEditing } from './hybrid-context.js';
5
-
6
- enableHybridEditing();
7
3
 
8
4
  type Props = {
9
5
  entry: T;
@@ -13,230 +9,10 @@
13
9
  let { entry, child }: Props = $props();
14
10
 
15
11
  let previewData: T = $state(entry);
16
- let hybridModeEnabled = $state(false);
17
- let highlightedPath = $state<string | null>(null);
18
12
  let parentOrigin: string | null = null;
19
13
 
20
- /**
21
- * Checks if a value looks like a UUID reference
22
- */
23
- function isUUID(value: unknown): value is string {
24
- if (typeof value !== 'string') return false;
25
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
26
- }
27
-
28
- /**
29
- * Checks if a value is a block with slug and data (e.g., {slug: "card", data: {...}})
30
- */
31
- function isBlock(value: unknown): value is { slug: string; data: Record<string, unknown> } {
32
- return (
33
- typeof value === 'object' &&
34
- value !== null &&
35
- 'slug' in value &&
36
- 'data' in value &&
37
- typeof (value as Record<string, unknown>).slug === 'string'
38
- );
39
- }
40
-
41
- /**
42
- * Checks if a value is a resolved media/file object with nested data
43
- * e.g., { data: { id: "...", url: "...", ... }, styles: {} }
44
- */
45
- function isResolvedMediaBlock(
46
- value: unknown
47
- ): value is { data: { id: string; [key: string]: unknown }; [key: string]: unknown } {
48
- return (
49
- typeof value === 'object' &&
50
- value !== null &&
51
- 'data' in value &&
52
- typeof (value as Record<string, unknown>).data === 'object' &&
53
- (value as Record<string, unknown>).data !== null &&
54
- 'id' in ((value as Record<string, unknown>).data as Record<string, unknown>) &&
55
- 'url' in ((value as Record<string, unknown>).data as Record<string, unknown>)
56
- );
57
- }
58
-
59
- /**
60
- * Checks if a value is a simple resolved reference (has id UUID at top level)
61
- * e.g., { id: "...", slug: "...", type: "collection" } or { id: "...", url: "...", width: 800 }
62
- */
63
- function isResolvedReference(
64
- value: unknown
65
- ): value is { id: string; [key: string]: unknown } {
66
- return (
67
- typeof value === 'object' &&
68
- value !== null &&
69
- 'id' in value &&
70
- typeof (value as Record<string, unknown>).id === 'string' &&
71
- isUUID((value as Record<string, unknown>).id)
72
- );
73
- }
74
-
75
- /**
76
- * Checks if a value looks like a localized object (has language keys like 'pl', 'en')
77
- */
78
- function isLocalizedObject(value: unknown): value is Record<string, unknown> {
79
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
80
- const keys = Object.keys(value);
81
- // Common language codes
82
- const langCodes = ['pl', 'en', 'de', 'fr', 'es', 'it', 'uk', 'cs', 'sk'];
83
- return keys.length > 0 && keys.every((k) => langCodes.includes(k));
84
- }
85
-
86
- /**
87
- * Deep merges form data with original populated data, preserving resolved references
88
- * when the ID hasn't changed. This allows live preview to work without losing
89
- * populated image/file/relation data.
90
- */
91
- function deepMergeWithReferences(
92
- original: PopulatedEntryData,
93
- updates: Record<string, unknown>
94
- ): PopulatedEntryData {
95
- const result: PopulatedEntryData = { ...original };
96
-
97
- for (const key in updates) {
98
- const updateValue = updates[key];
99
- const originalValue = original[key];
100
-
101
- // Case 1: Update is a UUID string, original is a resolved media block { data: { id, url, ... }, styles: {} }
102
- if (isUUID(updateValue) && isResolvedMediaBlock(originalValue)) {
103
- if (originalValue.data.id === updateValue) {
104
- // Same ID - keep the fully populated original
105
- continue;
106
- } else {
107
- // Different ID - mark as pending
108
- result[key] = { data: { id: updateValue, _pending: true }, styles: {} };
109
- }
110
- continue;
111
- }
112
-
113
- // Case 2: Update is a UUID string, original is a simple resolved reference { id, url, ... }
114
- if (isUUID(updateValue) && isResolvedReference(originalValue)) {
115
- if (originalValue.id === updateValue) {
116
- // Same ID - keep the fully populated original
117
- continue;
118
- } else {
119
- // Different ID - mark as pending
120
- result[key] = { id: updateValue, _pending: true };
121
- }
122
- continue;
123
- }
124
-
125
- // Case 3: Both are arrays - merge based on index and slug matching for blocks
126
- if (Array.isArray(updateValue) && Array.isArray(originalValue)) {
127
- const mergedArray: unknown[] = [];
128
-
129
- for (let i = 0; i < updateValue.length; i++) {
130
- const updateItem = updateValue[i];
131
-
132
- // Handle UUID in array
133
- if (isUUID(updateItem)) {
134
- // Find matching resolved object in original by ID
135
- const matchingOriginal = originalValue.find((orig) => {
136
- if (isResolvedMediaBlock(orig)) return orig.data.id === updateItem;
137
- if (isResolvedReference(orig)) return orig.id === updateItem;
138
- return false;
139
- });
140
- if (matchingOriginal) {
141
- mergedArray.push(matchingOriginal);
142
- } else {
143
- mergedArray.push({ id: updateItem, _pending: true });
144
- }
145
- continue;
146
- }
147
-
148
- // Handle blocks with slug and data
149
- if (isBlock(updateItem)) {
150
- // Try to find matching block in original by same index and slug
151
- const originalItem = originalValue[i];
152
- if (isBlock(originalItem) && originalItem.slug === updateItem.slug) {
153
- // Recursively merge the data inside the block
154
- mergedArray.push({
155
- slug: updateItem.slug,
156
- data: deepMergeWithReferences(
157
- originalItem.data as PopulatedEntryData,
158
- updateItem.data
159
- )
160
- });
161
- } else {
162
- // No matching block, use update as-is
163
- mergedArray.push(updateItem);
164
- }
165
- continue;
166
- }
167
-
168
- // Handle other nested objects
169
- if (typeof updateItem === 'object' && updateItem !== null) {
170
- const originalItem = originalValue[i];
171
- if (typeof originalItem === 'object' && originalItem !== null) {
172
- mergedArray.push(
173
- deepMergeWithReferences(
174
- originalItem as PopulatedEntryData,
175
- updateItem as Record<string, unknown>
176
- )
177
- );
178
- } else {
179
- mergedArray.push(updateItem);
180
- }
181
- continue;
182
- }
183
-
184
- // Primitive value
185
- mergedArray.push(updateItem);
186
- }
187
-
188
- result[key] = mergedArray;
189
- continue;
190
- }
191
-
192
- // Case 4: Update is localized object {pl: "..."}, original is plain string
193
- // Use the current language value or first available
194
- if (isLocalizedObject(updateValue) && typeof originalValue === 'string') {
195
- const langs = Object.keys(updateValue);
196
- // Try 'pl' first, then first available
197
- result[key] = updateValue['pl'] ?? updateValue[langs[0]] ?? originalValue;
198
- continue;
199
- }
200
-
201
- // Case 5: Both are blocks with slug/data - merge recursively
202
- if (
203
- isBlock(updateValue) &&
204
- isBlock(originalValue) &&
205
- updateValue.slug === originalValue.slug
206
- ) {
207
- result[key] = {
208
- slug: updateValue.slug,
209
- data: deepMergeWithReferences(originalValue.data as PopulatedEntryData, updateValue.data)
210
- };
211
- continue;
212
- }
213
-
214
- // Case 6: Both are nested objects (but not arrays) - recursively merge
215
- if (
216
- typeof updateValue === 'object' &&
217
- updateValue !== null &&
218
- !Array.isArray(updateValue) &&
219
- typeof originalValue === 'object' &&
220
- originalValue !== null &&
221
- !Array.isArray(originalValue)
222
- ) {
223
- result[key] = deepMergeWithReferences(
224
- originalValue as PopulatedEntryData,
225
- updateValue as Record<string, unknown>
226
- );
227
- continue;
228
- }
229
-
230
- // Case 7: Simple value replacement (strings, numbers, booleans, etc.)
231
- result[key] = updateValue;
232
- }
233
-
234
- return result;
235
- }
236
-
237
14
  onMount(() => {
238
15
  function handleMessage(e: MessageEvent) {
239
- // Accept first message from any origin (handshake), then lock to that origin
240
16
  if (!parentOrigin) {
241
17
  parentOrigin = e.origin;
242
18
  } else if (e.origin !== parentOrigin) {
@@ -244,103 +20,17 @@
244
20
  }
245
21
 
246
22
  if (e.data.type === 'preview-update') {
247
- // Merge form data with current preview data, preserving resolved references
248
- previewData = {
249
- ...previewData,
250
- data: deepMergeWithReferences(previewData.data as PopulatedEntryData, e.data.data)
251
- } as T;
252
- }
253
-
254
- if (e.data.type === 'hybrid-mode-enable') {
255
- hybridModeEnabled = e.data.enabled;
256
- updateHybridClickHandlers();
257
- }
258
-
259
- // Highlight from form focus
260
- if (e.data.type === 'hybrid-highlight') {
261
- highlightedPath = e.data.path;
262
- updateHighlight();
23
+ previewData = { ...previewData, ...e.data.data } as T;
263
24
  }
264
25
  }
265
26
 
266
27
  window.addEventListener('message', handleMessage);
267
- // Block link clicks in hybrid mode
268
- document.addEventListener('click', handleLinkClick, true);
269
-
270
- // Signal to parent that preview is ready to receive data
271
28
  window.parent.postMessage({ type: 'preview-ready' }, parentOrigin || '*');
272
29
 
273
30
  return () => {
274
31
  window.removeEventListener('message', handleMessage);
275
- document.removeEventListener('click', handleLinkClick, true);
276
32
  };
277
33
  });
278
-
279
- function updateHybridClickHandlers() {
280
- // Remove existing handlers and styles
281
- document.querySelectorAll('[data-hybrid-path]').forEach((el) => {
282
- el.removeEventListener('click', handleHybridClick as EventListener);
283
- (el as HTMLElement).classList.remove('hybrid-editable');
284
- });
285
-
286
- if (hybridModeEnabled) {
287
- // Add click handlers and styles for hybrid elements
288
- document.querySelectorAll('[data-hybrid-path]').forEach((el) => {
289
- el.addEventListener('click', handleHybridClick as EventListener);
290
- (el as HTMLElement).classList.add('hybrid-editable');
291
- });
292
- }
293
- }
294
-
295
- // Block all link navigation when in hybrid mode
296
- function handleLinkClick(e: MouseEvent) {
297
- if (!hybridModeEnabled) return;
298
-
299
- const link = (e.target as HTMLElement).closest('a');
300
- if (link && link.href) {
301
- e.preventDefault();
302
- e.stopPropagation();
303
- }
304
- }
305
-
306
- function updateHighlight() {
307
- // Remove all highlights
308
- document.querySelectorAll('[data-hybrid-path]').forEach((el) => {
309
- (el as HTMLElement).classList.remove('hybrid-highlighted');
310
- });
311
-
312
- // Add highlight to matching element
313
- if (highlightedPath) {
314
- const el = document.querySelector(`[data-hybrid-path="${highlightedPath}"]`) as HTMLElement;
315
- if (el) {
316
- el.classList.add('hybrid-highlighted');
317
- }
318
- }
319
- }
320
-
321
- function handleHybridClick(e: MouseEvent) {
322
- e.preventDefault();
323
- e.stopPropagation();
324
-
325
- // Find the closest element with data-hybrid-path
326
- const target = (e.target as HTMLElement).closest('[data-hybrid-path]') as HTMLElement;
327
- if (!target) return;
328
-
329
- const path = target.dataset.hybridPath;
330
- if (path) {
331
- // Normalize to dot notation (hero[0].data → hero.0.data)
332
- const normalized = path.replace(/\[(\d+)\]/g, '.$1');
333
- window.parent.postMessage({ type: 'hybrid-focus', path: normalized }, parentOrigin || '*');
334
- }
335
- }
336
-
337
- // Re-apply handlers when previewData changes
338
- $effect(() => {
339
- if (previewData && hybridModeEnabled) {
340
- // Small delay to ensure DOM is updated
341
- setTimeout(updateHybridClickHandlers, 100);
342
- }
343
- });
344
34
  </script>
345
35
 
346
36
  {#if child}
@@ -350,17 +40,3 @@
350
40
  {:else}
351
41
  <pre>{JSON.stringify(previewData, null, 2)}</pre>
352
42
  {/if}
353
-
354
- <style>
355
- :global(.hybrid-editable) {
356
- cursor: pointer;
357
- outline: 2px dashed transparent;
358
- outline-offset: 4px;
359
- transition: outline-color 0.15s ease;
360
- }
361
-
362
- :global(.hybrid-editable:hover),
363
- :global(.hybrid-highlighted) {
364
- outline-color: #3b82f6;
365
- }
366
- </style>
@@ -1,8 +1,5 @@
1
- import type { PopulatedEntryData } from '../../types/entries.js';
2
1
  import { type Snippet } from 'svelte';
3
- declare function $$render<T extends {
4
- data: PopulatedEntryData;
5
- }>(): {
2
+ declare function $$render<T extends Record<string, unknown>>(): {
6
3
  props: {
7
4
  entry: T;
8
5
  child?: Snippet<[{
@@ -14,9 +11,7 @@ declare function $$render<T extends {
14
11
  slots: {};
15
12
  events: {};
16
13
  };
17
- declare class __sveltets_Render<T extends {
18
- data: PopulatedEntryData;
19
- }> {
14
+ declare class __sveltets_Render<T extends Record<string, unknown>> {
20
15
  props(): ReturnType<typeof $$render<T>>['props'];
21
16
  events(): ReturnType<typeof $$render<T>>['events'];
22
17
  slots(): ReturnType<typeof $$render<T>>['slots'];
@@ -24,18 +19,12 @@ declare class __sveltets_Render<T extends {
24
19
  exports(): {};
25
20
  }
26
21
  interface $$IsomorphicComponent {
27
- new <T extends {
28
- data: PopulatedEntryData;
29
- }>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
22
+ new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
30
23
  $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
31
24
  } & ReturnType<__sveltets_Render<T>['exports']>;
32
- <T extends {
33
- data: PopulatedEntryData;
34
- }>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
25
+ <T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
35
26
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
36
27
  }
37
28
  declare const Preview: $$IsomorphicComponent;
38
- type Preview<T extends {
39
- data: PopulatedEntryData;
40
- }> = InstanceType<typeof Preview<T>>;
29
+ type Preview<T extends Record<string, unknown>> = InstanceType<typeof Preview<T>>;
41
30
  export default Preview;
@@ -1,5 +1,6 @@
1
1
  export { includioCMS } from './handle.js';
2
- export { getEntry, getEntries } from '../../core/server/entries/operations/get.js';
2
+ export { getEntry, getEntries, countEntries } from '../../core/server/entries/operations/get.js';
3
3
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
4
+ export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
4
5
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
5
6
  export { getPreviewEntry } from './preview.js';
@@ -1,5 +1,6 @@
1
1
  export { includioCMS } from './handle.js';
2
- export { getEntry, getEntries } from '../../core/server/entries/operations/get.js';
2
+ export { getEntry, getEntries, countEntries } from '../../core/server/entries/operations/get.js';
3
3
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
4
+ export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
4
5
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
5
6
  export { getPreviewEntry } from './preview.js';
@@ -9,11 +9,8 @@ export async function getPreviewEntry(event, options) {
9
9
  if (!authLocals.user || !authLocals.session) {
10
10
  throw new Error('Unauthorized');
11
11
  }
12
- if (preview) {
13
- return getEntryVersion({
14
- id: preview,
15
- language: options.language
16
- });
17
- }
18
- return null;
12
+ return getEntryVersion({
13
+ id: preview,
14
+ language: options.language
15
+ });
19
16
  }