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
@@ -16,10 +16,9 @@
16
16
  import InfoCircle from '@tabler/icons-svelte/icons/info-circle';
17
17
  import Shield from '@tabler/icons-svelte/icons/shield';
18
18
  import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
19
- import { getEntryStatus } from '../utils.js';
19
+ import { getEntryStatusForLang } from '../utils.js';
20
20
  import { validateA11y, a11yLangPl, a11yLangEn, type A11yIssue } from './a11y-validator.js';
21
21
  import { getContentLanguage } from '../../../state/content-language.svelte.js';
22
- import { computeTranslationStatus, type LangStatus } from '../../../utils/translationStatus.js';
23
22
  import LanguageIcon from '@tabler/icons-svelte/icons/language';
24
23
 
25
24
  const lang: Record<
@@ -131,19 +130,13 @@
131
130
  let dateValue = $state('');
132
131
  let timeValue = $state('');
133
132
  let a11yIssues = $state<A11yIssue[]>([]);
134
- let translationStatus = $state<Record<string, LangStatus> | null>(null);
135
133
 
136
- const entryStatus = $derived(getEntryStatus(entry));
134
+ // Status is per current language
135
+ const entryStatus = $derived(getEntryStatusForLang(entry, contentLanguage.current));
137
136
  const t = $derived(lang[interfaceLanguage.current]);
138
137
 
139
138
  const hasA11yWarnings = $derived(a11yIssues.some((i) => i.type === 'warning'));
140
- const hasIncompleteTranslations = $derived(
141
- translationStatus != null &&
142
- Object.values(translationStatus).some((s) => s.status !== 'complete')
143
- );
144
- const showTranslationSection = $derived(
145
- contentLanguage.all.length > 1 && translationStatus != null
146
- );
139
+ const showLangStatusSection = $derived(contentLanguage.all.length > 1);
147
140
 
148
141
  const scheduleLabel = $derived.by(() => {
149
142
  switch (entryStatus) {
@@ -173,8 +166,9 @@
173
166
  }
174
167
 
175
168
  function setDefaultValues() {
176
- if (entryStatus === 'scheduled' && entry.publishedAt) {
177
- const scheduled = new Date(entry.publishedAt);
169
+ const scheduledVersion = entry.scheduledVersions[contentLanguage.current];
170
+ if (entryStatus === 'scheduled' && scheduledVersion?.publishedAt) {
171
+ const scheduled = new Date(scheduledVersion.publishedAt);
178
172
  dateValue = scheduled.toISOString().slice(0, 10);
179
173
  timeValue = `${String(scheduled.getHours()).padStart(2, '0')}:${String(scheduled.getMinutes()).padStart(2, '0')}`;
180
174
  } else {
@@ -229,20 +223,10 @@
229
223
  a11yIssues = validateA11y(data, fields, a11yLang, contentLanguage.all);
230
224
  }
231
225
 
232
- function runTranslationCheck() {
233
- if (contentLanguage.all.length <= 1 || !fields.length) {
234
- translationStatus = null;
235
- return;
236
- }
237
- const data = getFormData ? getFormData() : version.data;
238
- translationStatus = computeTranslationStatus(data, fields, contentLanguage.all);
239
- }
240
-
241
226
  $effect(() => {
242
227
  if (open) {
243
228
  setDefaultValues();
244
229
  runA11yValidation();
245
- runTranslationCheck();
246
230
  }
247
231
  });
248
232
  </script>
@@ -284,92 +268,56 @@
284
268
  </div>
285
269
  </div>
286
270
 
287
- {#if entryStatus === 'published' && entry.publishedAt}
271
+ {#if entryStatus === 'published' && entry.publishedVersions[contentLanguage.current]?.publishedAt}
288
272
  <div class="publish-detail-row">
289
273
  <span>{t.publishedAt}</span>
290
- <span class="tabular-nums">{formatDateTime(entry.publishedAt)}</span>
274
+ <span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current]?.publishedAt)}</span>
291
275
  </div>
292
276
  {/if}
293
277
 
294
- {#if entryStatus === 'scheduled' && entry.publishedAt}
278
+ {#if entryStatus === 'scheduled' && entry.publishedVersions[contentLanguage.current]?.publishedAt}
295
279
  <div class="publish-detail-row">
296
280
  <span>{t.scheduledFor}</span>
297
- <span class="tabular-nums">{formatDateTime(entry.publishedAt)}</span>
281
+ <span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current]?.publishedAt)}</span>
298
282
  </div>
299
283
  {/if}
300
284
 
301
- {#if entry.publishedVersion && entry.publishedVersion.id !== version.id}
285
+ {#if entry.publishedVersions[contentLanguage.current] && entry.publishedVersions[contentLanguage.current].id !== version.id}
302
286
  <div class="publish-detail-row">
303
287
  <span>{t.publishedVersion}</span>
304
- <span class="tabular-nums">{formatDateTime(entry.publishedVersion.createdAt)}</span>
288
+ <span class="tabular-nums">{formatDateTime(entry.publishedVersions[contentLanguage.current].createdAt)}</span>
305
289
  </div>
306
290
  {/if}
307
291
  </div>
308
292
 
309
- <!-- Translation section -->
310
- {#if showTranslationSection && translationStatus}
293
+ <!-- Per-language status section -->
294
+ {#if showLangStatusSection}
311
295
  <div class="sheet-section">
312
296
  <div class="sheet-section-title">
313
297
  <LanguageIcon class="size-3.5" />
314
298
  {t.translations}
315
299
  </div>
316
300
 
317
- {#each contentLanguage.all as lang}
318
- {@const status = translationStatus[lang]}
319
- {#if status}
320
- <div class="translation-lang-row">
321
- <div class="translation-lang-header">
322
- <span class="translation-lang-label">{lang.toUpperCase()}</span>
323
- <span class="translation-lang-pct {status.status === 'complete' ? 'complete' : status.status === 'partial' ? 'partial' : 'empty'}">
324
- {#if status.status === 'complete'}&#10003;{:else}{status.percentage}%{/if}
325
- </span>
326
- </div>
327
- <div class="translation-progress-track">
328
- <div
329
- class="translation-progress-fill {status.status === 'complete' ? 'complete' : status.status === 'partial' ? 'partial' : ''}"
330
- style="width: {status.percentage}%"
331
- ></div>
332
- </div>
301
+ {#each contentLanguage.all as langCode}
302
+ {@const langPub = entry.publishedVersions[langCode]}
303
+ {@const langSch = entry.scheduledVersions[langCode]}
304
+ {@const langDraft = entry.draftVersions[langCode]}
305
+ {@const langStatus = langPub ? 'published' : langSch ? 'scheduled' : 'draft'}
306
+ <div class="translation-lang-row">
307
+ <div class="translation-lang-header">
308
+ <span class="translation-lang-label">{langCode.toUpperCase()}</span>
309
+ <span class="translation-lang-pct {langStatus === 'published' ? 'complete' : langStatus === 'scheduled' ? 'partial' : 'empty'}">
310
+ {t.statusLabels[langStatus]}
311
+ </span>
333
312
  </div>
334
- {/if}
313
+ <div class="translation-progress-track">
314
+ <div
315
+ class="translation-progress-fill {langStatus === 'published' ? 'complete' : langStatus === 'scheduled' ? 'partial' : ''}"
316
+ style="width: {langStatus === 'published' ? 100 : langStatus === 'scheduled' ? 66 : langDraft ? 33 : 0}%"
317
+ ></div>
318
+ </div>
319
+ </div>
335
320
  {/each}
336
-
337
- {#if hasIncompleteTranslations}
338
- {#each contentLanguage.all as lang}
339
- {@const status = translationStatus[lang]}
340
- {#if status && status.missingFields.length > 0}
341
- <div class="translation-missing">
342
- <div class="translation-missing-title">{t.missingFieldsLabel(lang)}</div>
343
- {#each status.missingFields as field}
344
- {#if onScrollToIssue}
345
- <button
346
- type="button"
347
- class="a11y-item warning a11y-item-clickable"
348
- onclick={() => {
349
- contentLanguage.current = lang;
350
- open = false;
351
- setTimeout(() => onScrollToIssue!(field.slug, 0), 200);
352
- }}
353
- >
354
- <div class="a11y-item-icon">
355
- <AlertTriangle class="size-4" />
356
- </div>
357
- <span class="a11y-item-text">{field.label}</span>
358
- </button>
359
- {:else}
360
- <div class="a11y-item warning">
361
- <div class="a11y-item-icon">
362
- <AlertTriangle class="size-4" />
363
- </div>
364
- <span class="a11y-item-text">{field.label}</span>
365
- </div>
366
- {/if}
367
- {/each}
368
- </div>
369
- {/if}
370
- {/each}
371
- <p class="a11y-hint">{t.translationsHint}</p>
372
- {/if}
373
321
  </div>
374
322
  {/if}
375
323
 
@@ -45,8 +45,8 @@
45
45
 
46
46
  let { entry, version }: Props = $props();
47
47
 
48
- const isPublishedVersion = $derived(entry.publishedVersion?.id === version.id);
49
- const isScheduledVersion = $derived(entry.scheduledVersion?.id === version.id);
48
+ const isPublishedVersion = $derived(entry.publishedVersions[version.lang]?.id === version.id);
49
+ const isScheduledVersion = $derived(entry.scheduledVersions[version.lang]?.id === version.id);
50
50
 
51
51
  const currentStatus = $derived.by((): EntryVersionStatus => {
52
52
  if (isPublishedVersion) return 'published';
@@ -89,8 +89,8 @@
89
89
  }
90
90
 
91
91
  function getDotClass(version: DbEntryVersion): string {
92
- const isPublished = entry.publishedVersionId === version.id && entryStatus === 'published';
93
- const isScheduled = entry.publishedVersionId === version.id && entryStatus === 'scheduled';
92
+ const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published';
93
+ const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled';
94
94
  const isCurrent = version.id === currentVersionId;
95
95
 
96
96
  if (isPublished) return 'dot-published';
@@ -109,7 +109,7 @@
109
109
  // Always keep current version
110
110
  if (version.id === currentVersionId) return true;
111
111
  // Always keep published/scheduled
112
- if (entry.publishedVersionId === version.id) return true;
112
+ if (entry.publishedVersions[version.lang]?.id === version.id) return true;
113
113
  // Keep versions with actual data changes vs previous
114
114
  const prevVersion = sortedVersions[idx + 1];
115
115
  if (!prevVersion) return true; // first version
@@ -132,8 +132,8 @@
132
132
  const groups: Map<string, DbEntryVersion[]> = new Map();
133
133
 
134
134
  for (const version of timelineVersions) {
135
- const isPublished = entry.publishedVersionId === version.id && entryStatus === 'published';
136
- const isScheduled = entry.publishedVersionId === version.id && entryStatus === 'scheduled';
135
+ const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published';
136
+ const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled';
137
137
 
138
138
  let key: string;
139
139
  if (isPublished) {
@@ -193,12 +193,12 @@
193
193
  <div class="vh-item-left">
194
194
  <span class="vh-time">{formatTime(currentVersion.createdAt)}</span>
195
195
  <Badge class="vh-badge-editing">{t.editing}</Badge>
196
- {#if entry.publishedVersionId === currentVersion.id && entryStatus === 'published'}
196
+ {#if entry.publishedVersions[currentVersion.lang]?.id === currentVersion.id && entryStatus === 'published'}
197
197
  <Badge class="vh-badge-published">
198
198
  <SquareCheckFilled class="size-2.5 mr-0.5" />
199
199
  {t.published}
200
200
  </Badge>
201
- {:else if entry.publishedVersionId === currentVersion.id && entryStatus === 'scheduled'}
201
+ {:else if entry.publishedVersions[currentVersion.lang]?.id === currentVersion.id && entryStatus === 'scheduled'}
202
202
  <Badge class="vh-badge-scheduled">
203
203
  {t.scheduled}
204
204
  </Badge>
@@ -233,8 +233,8 @@
233
233
  <div class="vh-timeline-line"></div>
234
234
 
235
235
  {#each versions as version}
236
- {@const isPublished = entry.publishedVersionId === version.id && entryStatus === 'published'}
237
- {@const isScheduled = entry.publishedVersionId === version.id && entryStatus === 'scheduled'}
236
+ {@const isPublished = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'published'}
237
+ {@const isScheduled = entry.publishedVersions[version.lang]?.id === version.id && entryStatus === 'scheduled'}
238
238
  {@const prevVersion = sortedVersions.find(
239
239
  (v) => v.versionNumber === version.versionNumber - 1
240
240
  )}
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
+ import { getContentLanguage } from '../../../state/content-language.svelte.js';
2
3
  import { useInterfaceLanguage } from '../../../state/interface-language.svelte.js';
3
4
  import type { RawEntry } from '../../../../types/entries.js';
4
5
  import type { InterfaceLanguage } from '../../../../types/languages.js';
5
6
  import ClockFilled from '@tabler/icons-svelte/icons/clock-filled';
6
7
  import FileFilled from '@tabler/icons-svelte/icons/file-filled';
7
8
  import SquareCheckFilled from '@tabler/icons-svelte/icons/square-check-filled';
8
- import { getEntryStatus } from '../utils.js';
9
+ import { getEntryStatusForLang } from '../utils.js';
9
10
 
10
11
  const lang: Record<
11
12
  InterfaceLanguage,
@@ -34,6 +35,7 @@
34
35
  };
35
36
 
36
37
  const interfaceLanguage = useInterfaceLanguage();
38
+ const contentLanguage = getContentLanguage();
37
39
 
38
40
  type Props = {
39
41
  entry: RawEntry;
@@ -41,34 +43,38 @@
41
43
 
42
44
  let { entry }: Props = $props();
43
45
 
44
- const entryStatus = $derived(getEntryStatus(entry));
46
+ const currentLang = $derived(contentLanguage.current);
47
+ const entryStatus = $derived(getEntryStatusForLang(entry, currentLang));
48
+ const publishedVersion = $derived(entry.publishedVersions[currentLang]);
49
+ const scheduledVersion = $derived(entry.scheduledVersions[currentLang]);
50
+ const draftVersion = $derived(entry.draftVersions[currentLang]);
45
51
  </script>
46
52
 
47
53
  <div class="flex items-center gap-1 text-sm">
48
54
  <span class="text-muted-foreground"> {lang[interfaceLanguage.current].status}:</span>
49
55
 
50
- {#if entryStatus === 'published' && entry.publishedVersion}
56
+ {#if entryStatus === 'published' && publishedVersion}
51
57
  <span class="flex items-center gap-1">
52
58
  <SquareCheckFilled />
53
- {lang[interfaceLanguage.current].published} (v{entry.publishedVersion.versionNumber}, {entry.publishedAt?.toLocaleString(
59
+ {lang[interfaceLanguage.current].published} (v{publishedVersion.versionNumber}, {publishedVersion.publishedAt ? new Date(publishedVersion.publishedAt).toLocaleString(
54
60
  interfaceLanguage.current
55
- )})</span
61
+ ) : ''})</span
56
62
  >
57
63
  {/if}
58
64
 
59
- {#if entryStatus === 'scheduled' && entry.scheduledVersion}
65
+ {#if entryStatus === 'scheduled' && scheduledVersion}
60
66
  <span class="flex items-center gap-1">
61
67
  <ClockFilled />
62
- {lang[interfaceLanguage.current].scheduled} v{entry.scheduledVersion.versionNumber}
68
+ {lang[interfaceLanguage.current].scheduled} v{scheduledVersion.versionNumber}
63
69
  {lang[interfaceLanguage.current].on}
64
- {entry.publishedAt?.toLocaleString(interfaceLanguage.current)})</span
70
+ {scheduledVersion.publishedAt ? new Date(scheduledVersion.publishedAt).toLocaleString(interfaceLanguage.current) : ''})</span
65
71
  >
66
72
  {/if}
67
73
 
68
- {#if entryStatus === 'draft' && entry.draftVersion}
74
+ {#if entryStatus === 'draft' && draftVersion}
69
75
  <span class="flex items-center gap-1">
70
76
  <FileFilled />
71
- {lang[interfaceLanguage.current].draft} (v{entry.draftVersion.versionNumber})</span
77
+ {lang[interfaceLanguage.current].draft} (v{draftVersion.versionNumber})</span
72
78
  >
73
79
  {/if}
74
80
  </div>
@@ -1,3 +1,6 @@
1
1
  import type { DbEntryVersion, EntryStatus, EntryVersionStatus, RawEntry } from '../../../types/entries.js';
2
2
  export declare function getEntryVersionStatus(version: DbEntryVersion): EntryVersionStatus;
3
+ /** Get entry status for a specific language */
4
+ export declare function getEntryStatusForLang(entry: RawEntry, lang: string): EntryStatus;
5
+ /** Get overall entry status — published if any lang is published */
3
6
  export declare function getEntryStatus(entry: RawEntry): EntryStatus;
@@ -7,15 +7,33 @@ export function getEntryVersionStatus(version) {
7
7
  }
8
8
  return 'published';
9
9
  }
10
+ /** Get entry status for a specific language */
11
+ export function getEntryStatusForLang(entry, lang) {
12
+ if (entry.archivedAt) {
13
+ return 'archived';
14
+ }
15
+ const publishedVersion = entry.publishedVersions[lang];
16
+ const scheduledVersion = entry.scheduledVersions[lang];
17
+ if (publishedVersion) {
18
+ return 'published';
19
+ }
20
+ if (scheduledVersion) {
21
+ return 'scheduled';
22
+ }
23
+ return 'draft';
24
+ }
25
+ /** Get overall entry status — published if any lang is published */
10
26
  export function getEntryStatus(entry) {
11
27
  if (entry.archivedAt) {
12
28
  return 'archived';
13
29
  }
14
- if (entry.publishedVersionId) {
15
- if (entry.publishedAt && new Date(entry.publishedAt) > new Date()) {
16
- return 'scheduled';
17
- }
30
+ const hasPublished = Object.values(entry.publishedVersions).some((v) => v != null);
31
+ if (hasPublished) {
18
32
  return 'published';
19
33
  }
34
+ const hasScheduled = Object.values(entry.scheduledVersions).some((v) => v != null);
35
+ if (hasScheduled) {
36
+ return 'scheduled';
37
+ }
20
38
  return 'draft';
21
39
  }
@@ -46,6 +46,8 @@
46
46
 
47
47
  const formSubmissionQuery = $derived(submissionId ? remotes.getFormSubmission(submissionId) : null);
48
48
 
49
+ let markedAsRead = false;
50
+
49
51
  $effect(() => {
50
52
  const submission = formSubmissionQuery?.current;
51
53
  if (submission) {
@@ -65,7 +67,8 @@
65
67
  ];
66
68
  }
67
69
 
68
- if (!submission.read) {
70
+ if (!submission.read && !markedAsRead) {
71
+ markedAsRead = true;
69
72
  remotes.updateFormSubmission({ id: submission.id, read: true });
70
73
  }
71
74
  }
@@ -5,6 +5,7 @@
5
5
  import Phone from '@tabler/icons-svelte/icons/phone';
6
6
  import MessageSquare from '@tabler/icons-svelte/icons/message';
7
7
  import FileText from '@tabler/icons-svelte/icons/file-text';
8
+ import Download from '@tabler/icons-svelte/icons/download';
8
9
  import CheckCircle from '@tabler/icons-svelte/icons/circle-check';
9
10
  import CircleX from '@tabler/icons-svelte/icons/circle-x';
10
11
  import Hash from '@tabler/icons-svelte/icons/hash';
@@ -28,11 +29,13 @@
28
29
  });
29
30
 
30
31
  const isBoolean = $derived(typeof value === 'boolean');
32
+ const isFile = $derived(fieldType === 'file' && typeof value === 'string' && value.length > 0);
31
33
  const isEmail = $derived(fieldType === 'email' || (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)));
32
34
  const isPhone = $derived(fieldType === 'phone' || (typeof value === 'string' && /^[\d\s\-+()]+$/.test(value) && String(value).length >= 9));
33
35
 
34
36
  const iconType = $derived.by(() => {
35
37
  if (isBoolean) return value ? 'check' : 'x';
38
+ if (isFile) return 'download';
36
39
  if (isEmail) return 'mail';
37
40
  if (isPhone) return 'phone';
38
41
  if (fieldType === 'textarea') return 'message';
@@ -64,6 +67,8 @@
64
67
  <Hash class="size-4 text-muted-foreground" />
65
68
  {:else if iconType === 'calendar'}
66
69
  <Calendar class="size-4 text-muted-foreground" />
70
+ {:else if iconType === 'download'}
71
+ <Download class="size-4 text-muted-foreground" />
67
72
  {:else}
68
73
  <FileText class="size-4 text-muted-foreground" />
69
74
  {/if}
@@ -81,6 +86,11 @@
81
86
  <span class="text-red-600">Nie</span>
82
87
  {/if}
83
88
  </div>
89
+ {:else if isFile}
90
+ <a href={String(value)} target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 text-primary hover:underline">
91
+ <Download class="size-4" />
92
+ Download
93
+ </a>
84
94
  {:else if isEmail}
85
95
  <a href="mailto:{value}" class="text-primary hover:underline break-all">{displayValue()}</a>
86
96
  {:else if isPhone}
@@ -9,5 +9,6 @@ export { default as FormPage } from './form/form-page.svelte';
9
9
  export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
10
10
  export { default as UsersPage } from './users/users-page.svelte';
11
11
  export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
12
+ export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
12
13
  export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
13
14
  export { default as MediaSelector } from '../components/media/media-selector.svelte';
@@ -9,5 +9,6 @@ export { default as FormPage } from './form/form-page.svelte';
9
9
  export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
10
10
  export { default as UsersPage } from './users/users-page.svelte';
11
11
  export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
12
+ export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
12
13
  export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
13
14
  export { default as MediaSelector } from '../components/media/media-selector.svelte';
@@ -5,12 +5,18 @@
5
5
  import FileSearch from '@tabler/icons-svelte/icons/file-search';
6
6
  import Photo from '@tabler/icons-svelte/icons/photo';
7
7
  import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
8
+ import PlayerPlay from '@tabler/icons-svelte/icons/player-play';
9
+ import PlayerStop from '@tabler/icons-svelte/icons/player-stop';
10
+ import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
8
11
  import Button from '../../../components/ui/button/button.svelte';
9
12
  import * as Card from '../../../components/ui/card/index.js';
10
13
  import { toast } from 'svelte-sonner';
11
14
 
12
15
  interface GcReport {
13
16
  imageStylesCount: number;
17
+ processableImagesCount: number;
18
+ expectedStylesCount: number;
19
+ missingStylesCount: number;
14
20
  orphanedDiskFiles: string[];
15
21
  missingDiskRecords: { table: string; id: string; url: string }[];
16
22
  }
@@ -20,6 +26,18 @@
20
26
  let purging = $state(false);
21
27
  let cleaningOrphans = $state(false);
22
28
 
29
+ // Batch generation state
30
+ let generating = $state(false);
31
+ let genTotal = $state(0);
32
+ let genProcessed = $state(0);
33
+ let genCreated = $state(0);
34
+ let genSkipped = $state(0);
35
+ let genCurrentFile = $state('');
36
+ let genErrors = $state(0);
37
+ let genAbort: AbortController | null = null;
38
+
39
+ let genPercent = $derived(genTotal > 0 ? Math.round((genProcessed / genTotal) * 100) : 0);
40
+
23
41
  async function loadReport() {
24
42
  loading = true;
25
43
  try {
@@ -63,6 +81,77 @@
63
81
  }
64
82
  }
65
83
 
84
+ async function startBatchGenerate() {
85
+ generating = true;
86
+ genTotal = 0;
87
+ genProcessed = 0;
88
+ genCreated = 0;
89
+ genSkipped = 0;
90
+ genCurrentFile = '';
91
+ genErrors = 0;
92
+ genAbort = new AbortController();
93
+
94
+ try {
95
+ const res = await fetch('/admin/api/generate-styles', {
96
+ method: 'POST',
97
+ signal: genAbort.signal
98
+ });
99
+
100
+ if (!res.ok) throw new Error('Failed to start');
101
+ if (!res.body) throw new Error('No response body');
102
+
103
+ const reader = res.body.getReader();
104
+ const decoder = new TextDecoder();
105
+ let buffer = '';
106
+
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done) break;
110
+
111
+ buffer += decoder.decode(value, { stream: true });
112
+ const chunks = buffer.split('\n\n');
113
+ buffer = chunks.pop() || '';
114
+
115
+ for (const chunk of chunks) {
116
+ if (!chunk.startsWith('data: ')) continue;
117
+ const event = JSON.parse(chunk.slice(6));
118
+
119
+ genTotal = event.total ?? genTotal;
120
+ genProcessed = event.processed ?? genProcessed;
121
+ genCreated = event.created ?? genCreated;
122
+ genSkipped = event.skipped ?? genSkipped;
123
+ genCurrentFile = event.currentFile ?? genCurrentFile;
124
+
125
+ if (event.type === 'error') {
126
+ genErrors++;
127
+ }
128
+
129
+ if (event.type === 'done') {
130
+ const parts = [`Przetworzono ${genTotal} obrazów`];
131
+ if (genCreated > 0) parts.push(`utworzono ${genCreated} styli`);
132
+ if (genSkipped > 0) parts.push(`pominięto ${genSkipped} (już istnieją)`);
133
+ if (genErrors > 0) parts.push(`${genErrors} błędów`);
134
+ toast.success(parts.join(', '));
135
+ }
136
+ }
137
+ }
138
+ } catch (e) {
139
+ if (e instanceof DOMException && e.name === 'AbortError') {
140
+ toast.info(`Przerwano po ${genProcessed}/${genTotal} obrazów`);
141
+ } else {
142
+ toast.error('Błąd podczas generowania styli');
143
+ }
144
+ } finally {
145
+ generating = false;
146
+ genAbort = null;
147
+ await loadReport();
148
+ }
149
+ }
150
+
151
+ function cancelBatchGenerate() {
152
+ genAbort?.abort();
153
+ }
154
+
66
155
  $effect(() => {
67
156
  loadReport();
68
157
  });
@@ -94,14 +183,69 @@
94
183
  </Card.Description>
95
184
  </Card.Header>
96
185
  <Card.Content>
97
- <p class="mb-4 text-3xl font-bold" style="color: var(--primary);">
186
+ <p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
98
187
  {report.imageStylesCount}
188
+ <span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.expectedStylesCount}</span>
189
+ </p>
190
+ <p class="mb-4 text-xs" style="color: var(--muted-foreground);">
191
+ {report.processableImagesCount} obrazów, {report.expectedStylesCount} oczekiwanych styli
99
192
  </p>
193
+
194
+ <!-- Batch generate -->
195
+ {#if generating}
196
+ <div class="mb-4">
197
+ <div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
198
+ <span>{genProcessed}/{genTotal} obrazów</span>
199
+ <span>{genPercent}%</span>
200
+ </div>
201
+ <div class="h-2 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
202
+ <div
203
+ class="h-full rounded-full transition-all duration-300"
204
+ style="width: {genPercent}%; background: var(--primary);"
205
+ ></div>
206
+ </div>
207
+ <p class="mt-1 text-xs" style="color: var(--muted-foreground);">
208
+ Utworzono: {genCreated}, pominięto: {genSkipped}{genErrors > 0 ? `, błędów: ${genErrors}` : ''}
209
+ </p>
210
+ <p class="mt-0.5 truncate text-xs" style="color: var(--muted-foreground);">
211
+ {genCurrentFile}
212
+ </p>
213
+ <Button
214
+ variant="outline"
215
+ size="sm"
216
+ onclick={cancelBatchGenerate}
217
+ class="mt-2"
218
+ >
219
+ <PlayerStop class="size-4" />
220
+ Anuluj
221
+ </Button>
222
+ </div>
223
+ {:else if report.missingStylesCount > 0}
224
+ <div class="mb-3">
225
+ <p class="mb-2 text-sm" style="color: var(--warning, #C4893A);">
226
+ Brakuje {report.missingStylesCount} styli — generowanie odbywa się przy wyświetleniu, co może spowalniać aplikację.
227
+ </p>
228
+ <Button
229
+ variant="default"
230
+ size="sm"
231
+ onclick={startBatchGenerate}
232
+ >
233
+ <PlayerPlay class="size-4" />
234
+ Generuj brakujące style
235
+ </Button>
236
+ </div>
237
+ {:else}
238
+ <div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
239
+ <CircleCheck class="size-4" />
240
+ Wszystkie style wygenerowane
241
+ </div>
242
+ {/if}
243
+
100
244
  <Button
101
245
  variant="destructive"
102
246
  size="sm"
103
247
  onclick={purgeStyles}
104
- disabled={purging || report.imageStylesCount === 0}
248
+ disabled={purging || generating || report.imageStylesCount === 0}
105
249
  >
106
250
  {#if purging}
107
251
  <Loader2 class="size-4 animate-spin" />
@@ -32,12 +32,7 @@
32
32
  import UserSessionsSheet from './user-sessions-sheet.svelte';
33
33
  import InviteUserDialog from './invite-user-dialog.svelte';
34
34
  import PendingInvitations from './pending-invitations.svelte';
35
-
36
- type Props = {
37
- emailConfigured?: boolean;
38
- };
39
-
40
- let { emailConfigured = false }: Props = $props();
35
+ import { getRemotes } from '../../helpers/index.js';
41
36
 
42
37
  type User = {
43
38
  id: string;
@@ -47,6 +42,10 @@
47
42
  createdAt: Date;
48
43
  };
49
44
 
45
+ const remotes = getRemotes();
46
+ const emailQuery = $derived(remotes.getEmailConfigured());
47
+ const emailConfigured = $derived(emailQuery.data === true);
48
+
50
49
  const interfaceLanguage = useInterfaceLanguage();
51
50
  const lang = $derived(usersLang[interfaceLanguage.current]);
52
51
  const session = authClient.useSession();