includio-cms 0.7.2 → 0.13.1

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 (185) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/ROADMAP.md +54 -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 +35 -13
  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/client/users/users-page.svelte +5 -6
  35. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  36. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  37. package/dist/admin/components/fields/blocks-field.svelte +40 -19
  38. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  39. package/dist/admin/components/fields/object-field.svelte +7 -12
  40. package/dist/admin/components/fields/select-field.svelte +8 -2
  41. package/dist/admin/components/fields/seo-field.svelte +40 -93
  42. package/dist/admin/components/fields/simple-array-field.svelte +27 -16
  43. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  44. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  45. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  46. package/dist/admin/components/fields/url-field.svelte +61 -72
  47. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  48. package/dist/admin/components/media/file-preview.svelte +10 -1
  49. package/dist/admin/components/media/file-upload.svelte +5 -1
  50. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  51. package/dist/admin/components/media/files-list.svelte +12 -3
  52. package/dist/admin/components/media/media-library.svelte +109 -37
  53. package/dist/admin/components/media/media-selector.svelte +90 -16
  54. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  55. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  56. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  57. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
  58. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  59. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  60. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  61. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  62. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  63. package/dist/admin/components/tiptap/lang.js +170 -0
  64. package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
  65. package/dist/admin/components/tiptap/slash-command.js +27 -23
  66. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  67. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  68. package/dist/admin/remote/email.remote.d.ts +1 -0
  69. package/dist/admin/remote/email.remote.js +5 -0
  70. package/dist/admin/remote/entry.remote.d.ts +2 -5
  71. package/dist/admin/remote/entry.remote.js +23 -28
  72. package/dist/admin/remote/index.d.ts +1 -0
  73. package/dist/admin/remote/index.js +1 -0
  74. package/dist/admin/remote/media.remote.d.ts +15 -0
  75. package/dist/admin/remote/media.remote.js +18 -2
  76. package/dist/admin/remote/preview.remote.js +3 -1
  77. package/dist/admin/utils/entryLabel.js +9 -6
  78. package/dist/admin/utils/translationStatus.js +1 -2
  79. package/dist/cli/scaffold/admin.js +34 -2
  80. package/dist/cms/runtime/api.d.ts +16 -12
  81. package/dist/cms/runtime/api.js +7 -6
  82. package/dist/cms/runtime/remote.js +2 -2
  83. package/dist/cms/runtime/schemas.d.ts +1 -1
  84. package/dist/cms/runtime/schemas.js +1 -1
  85. package/dist/cms/runtime/types.d.ts +118 -112
  86. package/dist/cms/runtime/types.js +0 -12
  87. package/dist/core/cms.d.ts +3 -1
  88. package/dist/core/cms.js +30 -0
  89. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  90. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  91. package/dist/core/server/entries/operations/create.js +10 -4
  92. package/dist/core/server/entries/operations/get.d.ts +1 -0
  93. package/dist/core/server/entries/operations/get.js +186 -191
  94. package/dist/core/server/entries/operations/update.d.ts +6 -7
  95. package/dist/core/server/entries/operations/update.js +20 -38
  96. package/dist/core/server/fields/populateEntry.js +16 -52
  97. package/dist/core/server/fields/resolveImageFields.js +69 -120
  98. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  99. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  100. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  101. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  102. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  103. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  104. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  105. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  106. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  107. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  108. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  109. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  110. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  111. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  112. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  113. package/dist/core/server/generator/fields.d.ts +6 -0
  114. package/dist/core/server/generator/fields.js +43 -5
  115. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  116. package/dist/core/server/generator/formFields.js +1 -0
  117. package/dist/core/server/generator/generator.js +98 -30
  118. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  119. package/dist/core/server/media/operations/getFiles.js +6 -0
  120. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  121. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  122. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  123. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  124. package/dist/db-postgres/index.js +303 -37
  125. package/dist/db-postgres/schema/entry.d.ts +0 -94
  126. package/dist/db-postgres/schema/entry.js +0 -6
  127. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  128. package/dist/db-postgres/schema/entryVersion.js +1 -0
  129. package/dist/entity/index.d.ts +9 -4
  130. package/dist/entity/index.js +24 -24
  131. package/dist/files-local/index.js +43 -0
  132. package/dist/paraglide/messages/_index.d.ts +36 -3
  133. package/dist/paraglide/messages/_index.js +71 -3
  134. package/dist/paraglide/messages/en.d.ts +5 -0
  135. package/dist/paraglide/messages/en.js +14 -0
  136. package/dist/paraglide/messages/pl.d.ts +5 -0
  137. package/dist/paraglide/messages/pl.js +14 -0
  138. package/dist/sveltekit/components/preview.svelte +2 -326
  139. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  140. package/dist/sveltekit/server/index.d.ts +2 -1
  141. package/dist/sveltekit/server/index.js +2 -1
  142. package/dist/sveltekit/server/preview.js +4 -7
  143. package/dist/types/adapters/db.d.ts +15 -1
  144. package/dist/types/adapters/files.d.ts +6 -0
  145. package/dist/types/cms.d.ts +5 -0
  146. package/dist/types/entries.d.ts +54 -18
  147. package/dist/types/fields.d.ts +14 -24
  148. package/dist/types/formFields.d.ts +7 -2
  149. package/dist/types/index.d.ts +2 -2
  150. package/dist/types/layout.d.ts +0 -1
  151. package/dist/types/structured-content.d.ts +5 -0
  152. package/dist/updates/0.10.0/index.d.ts +2 -0
  153. package/dist/updates/0.10.0/index.js +15 -0
  154. package/dist/updates/0.11.0/index.d.ts +2 -0
  155. package/dist/updates/0.11.0/index.js +12 -0
  156. package/dist/updates/0.12.0/index.d.ts +2 -0
  157. package/dist/updates/0.12.0/index.js +12 -0
  158. package/dist/updates/0.13.0/index.d.ts +2 -0
  159. package/dist/updates/0.13.0/index.js +10 -0
  160. package/dist/updates/0.13.1/index.d.ts +2 -0
  161. package/dist/updates/0.13.1/index.js +20 -0
  162. package/dist/updates/0.7.3/index.d.ts +2 -0
  163. package/dist/updates/0.7.3/index.js +10 -0
  164. package/dist/updates/0.8.0/index.d.ts +2 -0
  165. package/dist/updates/0.8.0/index.js +18 -0
  166. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  167. package/dist/updates/0.8.0/migrate.js +101 -0
  168. package/dist/updates/0.9.0/index.d.ts +2 -0
  169. package/dist/updates/0.9.0/index.js +38 -0
  170. package/dist/updates/index.js +9 -1
  171. package/package.json +7 -6
  172. package/dist/admin/components/fields/image-field.svelte +0 -198
  173. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  174. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  175. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  176. package/dist/admin/components/tiptap.svelte +0 -11
  177. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  178. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  179. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
  180. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  181. package/dist/paraglide/messages/hello_world.js +0 -33
  182. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  183. package/dist/paraglide/messages/login_hello.js +0 -34
  184. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  185. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -10,9 +10,161 @@ function serializeField(field) {
10
10
  function serializeFields(fields) {
11
11
  return fields.map(serializeField);
12
12
  }
13
+ function localize(value, languages) {
14
+ const obj = {};
15
+ for (const lang of languages) {
16
+ obj[lang] = value;
17
+ }
18
+ return obj;
19
+ }
20
+ function generateFieldTemplate(field, languages) {
21
+ const wrap = (v, hint) => {
22
+ if (field.localized) {
23
+ return { value: localize(v, languages), hint: `${hint} (localized)` };
24
+ }
25
+ return { value: v, hint };
26
+ };
27
+ switch (field.type) {
28
+ case 'text':
29
+ case 'slug':
30
+ return wrap('', 'string');
31
+ case 'content':
32
+ return wrap({ type: 'doc', content: [] }, 'ProseMirror JSON document');
33
+ case 'number':
34
+ return wrap(0, 'number');
35
+ case 'boolean':
36
+ return { value: false, hint: 'boolean' };
37
+ case 'date':
38
+ return wrap('2024-01-01', 'ISO date string');
39
+ case 'datetime':
40
+ return wrap('2024-01-01T00:00:00Z', 'ISO datetime string');
41
+ case 'file':
42
+ if (field.multiple) {
43
+ return { value: ['<media-uuid-from-upload>'], hint: 'array of media UUIDs from POST /upload' };
44
+ }
45
+ return { value: '<media-uuid-from-upload>', hint: 'media UUID from POST /upload' };
46
+ case 'media':
47
+ if (field.multiple) {
48
+ return { value: ['<media-uuid-from-upload>'], hint: 'array of media UUIDs (image or video)' };
49
+ }
50
+ return { value: '<media-uuid-from-upload>', hint: 'media UUID (image or video) from POST /upload' };
51
+ case 'select': {
52
+ const sf = field;
53
+ const opts = sf.options.map((o) => o.value);
54
+ if (sf.multiple) {
55
+ return { value: opts.length ? [opts[0]] : [], hint: `array of: ${opts.join(' | ')}` };
56
+ }
57
+ return { value: opts[0] ?? '', hint: `one of: ${opts.join(' | ')}` };
58
+ }
59
+ case 'radio': {
60
+ const rf = field;
61
+ const opts = rf.options.map((o) => o.value);
62
+ return { value: opts[0] ?? '', hint: `one of: ${opts.join(' | ')}` };
63
+ }
64
+ case 'checkboxes': {
65
+ const cf = field;
66
+ const opts = cf.options.map((o) => o.value);
67
+ return { value: opts.length ? [opts[0]] : [], hint: `array of: ${opts.join(' | ')}` };
68
+ }
69
+ case 'relation': {
70
+ const rf = field;
71
+ if (rf.multiple) {
72
+ return { value: ['<entry-uuid>'], hint: `array of ${rf.collection} entry UUIDs` };
73
+ }
74
+ return { value: '<entry-uuid>', hint: `${rf.collection} entry UUID` };
75
+ }
76
+ case 'object': {
77
+ const of = field;
78
+ const { template, meta } = generateTemplate(of.fields, languages);
79
+ return { value: template, hint: `object — ${JSON.stringify(meta)}` };
80
+ }
81
+ case 'blocks': {
82
+ const bf = field;
83
+ const blocks = bf.of.map((block) => {
84
+ const { template } = generateTemplate(block.fields, languages);
85
+ return { _slug: block.slug, ...template };
86
+ });
87
+ return { value: blocks, hint: 'array of block objects with _slug' };
88
+ }
89
+ case 'array': {
90
+ const af = field;
91
+ const examples = { text: '', number: 0, url: { url: { [languages[0]]: '' } } };
92
+ return { value: [examples[af.of] ?? ''], hint: `array of ${af.of}` };
93
+ }
94
+ case 'seo':
95
+ return {
96
+ value: {
97
+ slug: localize('', languages),
98
+ title: localize('', languages),
99
+ description: localize('', languages),
100
+ ogImage: '',
101
+ keywords: localize('', languages),
102
+ canonicalUrl: localize('', languages),
103
+ customCode: localize('', languages)
104
+ },
105
+ hint: 'SEO data — slug/title required, ogImage = media UUID or empty'
106
+ };
107
+ case 'url':
108
+ return {
109
+ value: {
110
+ url: localize('', languages),
111
+ text: localize('', languages),
112
+ newTab: false
113
+ },
114
+ hint: 'URL field with optional text and newTab'
115
+ };
116
+ case 'custom':
117
+ return { value: null, hint: 'custom field — check plugin docs' };
118
+ default:
119
+ return { value: null, hint: 'unknown field type' };
120
+ }
121
+ }
122
+ export function generateTemplate(fields, languages) {
123
+ const template = {};
124
+ const meta = {};
125
+ for (const field of fields) {
126
+ const { value, hint } = generateFieldTemplate(field, languages);
127
+ template[field.slug] = value;
128
+ meta[field.slug] = hint;
129
+ }
130
+ return { template, meta };
131
+ }
13
132
  export async function GET(event) {
14
133
  const cms = getCMS();
15
134
  const path = event.params.restPath || '';
135
+ // GET /schema/collections/:slug/template
136
+ const templateMatch = path.match(/^collections\/([^/]+)\/template$/);
137
+ if (templateMatch) {
138
+ const slug = templateMatch[1];
139
+ const collection = cms.collections[slug];
140
+ if (!collection) {
141
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
142
+ }
143
+ const fields = getFieldsFromConfig(collection);
144
+ const { template, meta } = generateTemplate(fields, cms.languages);
145
+ return json({
146
+ data: template,
147
+ publish: true,
148
+ _meta: meta,
149
+ _notes: 'POST this to /collections/' + slug + ' — set publish:true to auto-publish'
150
+ });
151
+ }
152
+ // GET /schema/singletons/:slug/template
153
+ const singletonTemplateMatch = path.match(/^singletons\/([^/]+)\/template$/);
154
+ if (singletonTemplateMatch) {
155
+ const slug = singletonTemplateMatch[1];
156
+ const single = cms.singles[slug];
157
+ if (!single) {
158
+ return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
159
+ }
160
+ const fields = getFieldsFromConfig(single);
161
+ const { template, meta } = generateTemplate(fields, cms.languages);
162
+ return json({
163
+ data: template,
164
+ _meta: meta,
165
+ _notes: 'PUT this to /singletons/' + slug
166
+ });
167
+ }
16
168
  // GET /schema/collections/:slug
17
169
  const collectionMatch = path.match(/^collections\/([^/]+)$/);
18
170
  if (collectionMatch) {
@@ -1,3 +1,3 @@
1
1
  import { type RequestEvent } from '@sveltejs/kit';
2
- export declare function GET(_event: RequestEvent, slug: string): Promise<Response>;
2
+ export declare function GET(event: RequestEvent, slug: string): Promise<Response>;
3
3
  export declare function PUT(event: RequestEvent, slug: string): Promise<Response>;
@@ -3,11 +3,12 @@ import { getCMS } from '../../../../core/cms.js';
3
3
  import { getRawEntries } from '../../../../core/server/entries/operations/get.js';
4
4
  import { createEntry } from '../../../../core/server/entries/operations/create.js';
5
5
  import { upsertDraftVersion } from '../../../../core/server/entries/operations/update.js';
6
- export async function GET(_event, slug) {
6
+ export async function GET(event, slug) {
7
7
  const cms = getCMS();
8
8
  if (!cms.singles[slug]) {
9
9
  return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
10
10
  }
11
+ const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
11
12
  const entries = await getRawEntries({ slug });
12
13
  const entry = entries[0];
13
14
  if (!entry) {
@@ -24,11 +25,10 @@ export async function GET(_event, slug) {
24
25
  type: entry.type,
25
26
  createdAt: entry.createdAt,
26
27
  updatedAt: entry.updatedAt,
27
- publishedAt: entry.publishedAt,
28
- draftData: entry.draftVersion?.data ?? null,
29
- publishedData: entry.publishedVersion?.data ?? null,
30
- draftVersionId: entry.draftVersion?.id ?? null,
31
- publishedVersionId: entry.publishedVersion?.id ?? null
28
+ draftData: entry.draftVersions[lang]?.data ?? null,
29
+ publishedData: entry.publishedVersions[lang]?.data ?? null,
30
+ draftVersionId: entry.draftVersions[lang]?.id ?? null,
31
+ publishedVersionId: entry.publishedVersions[lang]?.id ?? null
32
32
  });
33
33
  }
34
34
  export async function PUT(event, slug) {
@@ -36,6 +36,7 @@ export async function PUT(event, slug) {
36
36
  if (!cms.singles[slug]) {
37
37
  return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
38
38
  }
39
+ const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
39
40
  const body = await event.request.json().catch(() => null);
40
41
  if (!body?.data || typeof body.data !== 'object') {
41
42
  return json({ error: 'Request body must contain "data" object' }, { status: 400 });
@@ -51,7 +52,7 @@ export async function PUT(event, slug) {
51
52
  return json({ error: 'Failed to create singleton entry' }, { status: 500 });
52
53
  }
53
54
  }
54
- const version = await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
55
+ const version = await upsertDraftVersion(entry.id, body.data, lang, { skipValidation: true });
55
56
  return json({
56
57
  id: entry.id,
57
58
  versionId: version.id,
@@ -0,0 +1,2 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function POST(event: RequestEvent): Promise<Response>;
@@ -0,0 +1,28 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { uploadFile } from '../../../../core/server/media/operations/uploadFile.js';
3
+ import { getCMS } from '../../../../core/cms.js';
4
+ const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
5
+ export async function POST(event) {
6
+ const form = await event.request.formData();
7
+ const file = form.get('file');
8
+ const tagIdsRaw = form.get('tagIds');
9
+ if (!file || file.size === 0) {
10
+ return json({ error: 'No file provided' }, { status: 400 });
11
+ }
12
+ if (file.size > MAX_UPLOAD_SIZE) {
13
+ return json({ error: 'File too large (max 50MB)' }, { status: 413 });
14
+ }
15
+ const dbFile = await uploadFile(file);
16
+ if (tagIdsRaw) {
17
+ try {
18
+ const tagIds = JSON.parse(tagIdsRaw);
19
+ if (Array.isArray(tagIds) && tagIds.length > 0) {
20
+ await getCMS().databaseAdapter.setMediaFileTags(dbFile.id, tagIds);
21
+ }
22
+ }
23
+ catch {
24
+ // ignore invalid tagIds JSON
25
+ }
26
+ }
27
+ return json(dbFile, { status: 201 });
28
+ }
@@ -1,11 +1,13 @@
1
1
  import { requireAuth } from '../remote/middleware/auth.js';
2
2
  import { uploadFile } from '../../core/server/media/operations/uploadFile.js';
3
+ import { getCMS } from '../../core/cms.js';
3
4
  import { json } from '@sveltejs/kit';
4
5
  const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
5
6
  export const POST = async ({ request }) => {
6
7
  requireAuth();
7
8
  const form = await request.formData();
8
9
  const file = form.get('file');
10
+ const tagIdsRaw = form.get('tagIds');
9
11
  if (!file || file.size === 0) {
10
12
  return new Response('No file', { status: 400 });
11
13
  }
@@ -13,5 +15,16 @@ export const POST = async ({ request }) => {
13
15
  return new Response('File too large', { status: 413 });
14
16
  }
15
17
  const dbFile = await uploadFile(file);
18
+ if (tagIdsRaw) {
19
+ try {
20
+ const tagIds = JSON.parse(tagIdsRaw);
21
+ if (Array.isArray(tagIds) && tagIds.length > 0) {
22
+ await getCMS().databaseAdapter.setMediaFileTags(dbFile.id, tagIds);
23
+ }
24
+ }
25
+ catch {
26
+ // ignore invalid tagIds JSON
27
+ }
28
+ }
16
29
  return json(dbFile);
17
30
  };
@@ -378,8 +378,20 @@
378
378
  pendingDeleteId = null;
379
379
  }
380
380
 
381
+ function getVersionData(entry: RawEntry): Record<string, unknown> | undefined {
382
+ const lang = contentLanguage.current;
383
+ const draft = entry.draftVersions[lang];
384
+ const published = entry.publishedVersions[lang];
385
+ if (draft?.data) return draft.data as Record<string, unknown>;
386
+ if (published?.data) return published.data as Record<string, unknown>;
387
+ // Fallback: any lang
388
+ for (const v of Object.values(entry.draftVersions)) if (v?.data) return v.data as Record<string, unknown>;
389
+ for (const v of Object.values(entry.publishedVersions)) if (v?.data) return v.data as Record<string, unknown>;
390
+ return undefined;
391
+ }
392
+
381
393
  function getSearchText(entry: RawEntry): string {
382
- const data = entry.draftVersion?.data || entry.publishedVersion?.data;
394
+ const data = getVersionData(entry);
383
395
  if (!data) return '';
384
396
  return extractTextValues(data).join(' ').toLowerCase();
385
397
  }
@@ -404,7 +416,7 @@
404
416
 
405
417
  function extractSlug(entry: RawEntry): string | undefined {
406
418
  if (!seoField) return undefined;
407
- const data = entry.draftVersion?.data || entry.publishedVersion?.data;
419
+ const data = getVersionData(entry);
408
420
  if (!data) return undefined;
409
421
  const seoData = data[seoField.slug];
410
422
  if (seoData && typeof seoData === 'object' && 'slug' in seoData) {
@@ -414,13 +426,15 @@
414
426
  }
415
427
 
416
428
  function countA11yWarnings(entry: RawEntry): number {
417
- const data = entry.draftVersion?.data || entry.publishedVersion?.data;
429
+ const data = getVersionData(entry);
418
430
  if (!data) return 0;
419
431
  const a11yLang = interfaceLanguage.current === 'pl' ? a11yLangPl : a11yLangEn;
420
432
  const issues = validateA11y(data as Record<string, unknown>, collection.fields, a11yLang);
421
433
  return issues.filter((i) => i.type === 'warning').length;
422
434
  }
423
435
 
436
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
437
+
424
438
  async function fetchRelationLabels(entries: RawEntry[]): Promise<Record<string, string>> {
425
439
  if (relationListFields.length === 0) return {};
426
440
 
@@ -429,14 +443,21 @@
429
443
  uuidsByCollection.set(field.collection, new Set());
430
444
  }
431
445
  for (const entry of entries) {
432
- const data = entry.publishedVersion?.data || entry.draftVersion?.data;
433
- if (!data) continue;
434
- for (const field of relationListFields) {
435
- const raw = (data as Record<string, unknown>)[field.slug];
436
- if (!raw) continue;
437
- const set = uuidsByCollection.get(field.collection)!;
438
- if (typeof raw === 'string') set.add(raw);
439
- if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && set.add(v));
446
+ // Collect UUIDs from all version sources (draft + published) to match mapEntryToRow
447
+ const lang = contentLanguage.current;
448
+ const dataSources = [
449
+ entry.publishedVersions[lang]?.data,
450
+ entry.draftVersions[lang]?.data,
451
+ getVersionData(entry)
452
+ ].filter(Boolean) as Record<string, unknown>[];
453
+ for (const data of dataSources) {
454
+ for (const field of relationListFields) {
455
+ const raw = data[field.slug];
456
+ if (!raw) continue;
457
+ const set = uuidsByCollection.get(field.collection)!;
458
+ if (typeof raw === 'string' && UUID_RE.test(raw)) set.add(raw);
459
+ if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && UUID_RE.test(v) && set.add(v));
460
+ }
440
461
  }
441
462
  }
442
463
 
@@ -454,9 +475,10 @@
454
475
  }
455
476
 
456
477
  function mapEntryToRow(entry: RawEntry, lookup: Record<string, string> = {}): CollectionDataTableRow {
457
- const data = entry.draftVersion?.data || entry.publishedVersion?.data || {};
478
+ const data = getVersionData(entry) || {};
458
479
  // For custom columns, prefer published data (complete) over draft (may be partial)
459
- const columnData = entry.publishedVersion?.data || entry.draftVersion?.data || {};
480
+ const lang = contentLanguage.current;
481
+ const columnData = entry.publishedVersions[lang]?.data || entry.draftVersions[lang]?.data || data;
460
482
  const customData: Record<string, unknown> = {};
461
483
  if (collection.listColumns) {
462
484
  for (const fieldSlug of collection.listColumns) {
@@ -28,8 +28,6 @@
28
28
  import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
29
29
  import { onMount } from 'svelte';
30
30
  import { get } from 'svelte/store';
31
- import { computeTranslationStatus } from '../../utils/translationStatus.js';
32
-
33
31
  const contentLanguage = getContentLanguage();
34
32
  const remotes = getRemotes();
35
33
  const interfaceLanguage = useInterfaceLanguage();
@@ -142,10 +140,11 @@
142
140
  let { collection } = entry;
143
141
  const isArchived = $derived(!!entry.archivedAt);
144
142
 
145
- // Create form once at component level
143
+ // Create form once at component level — localized: false since data is flat single-language
146
144
  const collectionSchema = generateZodSchemaFromFields(
147
145
  getFieldsFromConfig(collection),
148
- contentLanguage.all
146
+ contentLanguage.all,
147
+ { localized: false }
149
148
  );
150
149
  const form = superForm(defaults(editingEntry.data, zod4(collectionSchema)), {
151
150
  validators: zod4Client(collectionSchema),
@@ -166,15 +165,6 @@
166
165
 
167
166
  const AUTOSAVE_DELAY = 30000; // 30 seconds
168
167
 
169
- // Reactive translation status — must live here where we can subscribe to form.form store
170
- const formStore = form.form;
171
- const collectionFields = getFieldsFromConfig(collection);
172
- const translationStatus = $derived.by(() => {
173
- if (contentLanguage.all.length <= 1) return null;
174
- const data = $formStore;
175
- return computeTranslationStatus(data, collectionFields, contentLanguage.all);
176
- });
177
-
178
168
  function scheduleAutosave() {
179
169
  if (isArchived) return;
180
170
  if (autosaveTimer) {
@@ -195,15 +185,18 @@
195
185
 
196
186
  saveStatus = 'saving';
197
187
  try {
188
+ const currentLang = contentLanguage.current;
198
189
  const result = await remotes.updateEntryVersionCommand({
199
190
  entryId: entry.id,
191
+ lang: currentLang,
200
192
  data: currentFormData,
201
193
  type: 'draft'
202
194
  });
203
195
  lastSavedData = currentData;
204
196
  saveStatus = 'saved';
205
197
  // If we're editing the published version and saved a draft, track it for the banner
206
- if (entry.publishedVersion && editingEntry.id === entry.publishedVersion.id && result?.id) {
198
+ const publishedVersion = entry.publishedVersions[currentLang];
199
+ if (publishedVersion && editingEntry.id === publishedVersion.id && result?.id) {
207
200
  savedDraftVersionId = result.id;
208
201
  }
209
202
  // Reset to idle after 3s
@@ -270,6 +263,7 @@
270
263
  try {
271
264
  await remotes.updateEntryVersionCommand({
272
265
  entryId: entry.id,
266
+ lang: contentLanguage.current,
273
267
  data: get(form.form),
274
268
  type
275
269
  });
@@ -298,6 +292,7 @@
298
292
  try {
299
293
  await remotes.updateEntryVersionCommand({
300
294
  entryId: entry.id,
295
+ lang: contentLanguage.current,
301
296
  data: validatedForm.data,
302
297
  type,
303
298
  scheduledAt
@@ -501,21 +496,25 @@
501
496
  }
502
497
  });
503
498
 
499
+ // Per-language version lookups
500
+ const currentPublishedVersion = $derived(entry.publishedVersions[contentLanguage.current]);
501
+ const currentDraftVersion = $derived(entry.draftVersions[contentLanguage.current]);
502
+
504
503
  // Banner: viewing published version + a separate draft exists
505
504
  const showDraftBanner = $derived(
506
- entry.publishedVersion != null &&
507
- editingEntry.id === entry.publishedVersion.id &&
508
- entry.draftVersion != null &&
509
- entry.draftVersion.id !== entry.publishedVersion.id &&
510
- entry.draftVersion.createdAt > entry.publishedVersion.createdAt
505
+ currentPublishedVersion != null &&
506
+ editingEntry.id === currentPublishedVersion.id &&
507
+ currentDraftVersion != null &&
508
+ currentDraftVersion.id !== currentPublishedVersion.id &&
509
+ currentDraftVersion.createdAt > currentPublishedVersion.createdAt
511
510
  );
512
511
 
513
512
  // Banner: viewing draft + a published version exists
514
513
  const showPublishedBanner = $derived(
515
- entry.publishedVersion != null && editingEntry.id !== entry.publishedVersion.id
514
+ currentPublishedVersion != null && editingEntry.id !== currentPublishedVersion.id
516
515
  );
517
516
 
518
- const draftVersionId = $derived(entry.draftVersion?.id ?? savedDraftVersionId);
517
+ const draftVersionId = $derived(currentDraftVersion?.id ?? savedDraftVersionId);
519
518
 
520
519
  const scrollToIssue = (fieldSlug: string) => {
521
520
  const fieldEl = document.querySelector(`[data-field-path="${fieldSlug}"]`);
@@ -554,7 +553,6 @@
554
553
  fields={getFieldsFromConfig(collection)}
555
554
  getFormData={() => get(form.form)}
556
555
  onScrollToIssue={scrollToIssue}
557
- {translationStatus}
558
556
  />
559
557
 
560
558
  {#if validationErrors.length > 0}
@@ -620,7 +618,7 @@
620
618
  <button
621
619
  type="button"
622
620
  class="font-semibold text-[var(--primary)] hover:underline"
623
- onclick={() => goto(`?version=${entry.publishedVersion!.id}`)}
621
+ onclick={() => goto(`?version=${currentPublishedVersion!.id}`)}
624
622
  >
625
623
  {lang[interfaceLanguage.current].switchToPublished}
626
624
  </button>
@@ -198,7 +198,7 @@ function tryExtractDoc(val) {
198
198
  function extractDocs(data, fields) {
199
199
  const result = [];
200
200
  for (const field of fields) {
201
- if (field.type !== 'content' && field.type !== 'richtext')
201
+ if (field.type !== 'content')
202
202
  continue;
203
203
  const val = data[field.slug];
204
204
  const docs = tryExtractDoc(val);
@@ -212,7 +212,7 @@ function extractDocs(data, fields) {
212
212
  function extractDocsForLang(data, fields, language) {
213
213
  const result = [];
214
214
  for (const field of fields) {
215
- if (field.type !== 'content' && field.type !== 'richtext')
215
+ if (field.type !== 'content')
216
216
  continue;
217
217
  const val = data[field.slug];
218
218
  if (!val || typeof val !== 'object')