includio-cms 0.36.2 → 0.36.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -1,4 +1,4 @@
1
- # includio-cms — Public API v0.36.2
1
+ # includio-cms — Public API v0.36.4
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.36.4 — 2026-06-11
7
+
8
+ Lista wpisów kolekcji — fix: URL pod nazwą wpisu (oraz wyszukiwarka i ostrzeżenia a11y) znikał, gdy początkowy draft v1 różnił się od późniejszej opublikowanej treści (np. slug ustawiony dopiero po publikacji). Ujednolicono precedencję wyboru wersji w jednym helperze (published-first), spójnie z resztą admina.
9
+
10
+ ### Fixed
11
+ - `getVersionData` w liście kolekcji (`collection-entries.svelte`) preferowało draft przed published. `draftVersions[lang]` to najnowsza wersja z `publishedAt == null` — co może być przestarzałym początkowym draftem (v1), starszym niż najnowsza publikacja, gdy późniejsze edycje szły tylko przez publikację. Efekt: subtytuł URL czytał pusty/nieaktualny slug z v1 i znikał, mimo że opublikowana wersja miała slug. Naprawione przez nowy SSOT `getRawEntryVersionData(entry, language)` z precedencją published-first (requested lang published → draft, potem any lang published → draft) — spójną z `getRawCollectionEntryLabelWithLanguage`, custom columns i edytorem wpisu.
12
+ - Ten sam helper zasila też tekst wyszukiwarki (`getSearchText`) i ostrzeżenia a11y (`countA11yWarnings`) — wcześniej również czytały przestarzały draft. `columnData` uproszczone (redundantny published-first override usunięty).
13
+ - `extractEntryUrl` przekazuje teraz `language` do `resolveEntryUrl` — bez efektu dla płaskiego slug, ale poprawne dla projektów wielojęzycznych ze zlokalizowanym slugiem. Dodany regression test (`entryVersionData.spec.ts`) odtwarzający scenariusz pustego draftu v1 + opublikowanego sluga.
14
+
15
+ ## 0.36.3 — 2026-06-11
16
+
17
+ Relacje opcjonalne — fix: niewybrana relacja (pole bez `required`) blokowała zapis wpisu („Invalid input: expected string, received null") albo wywalała populację przy pustym stringu. Teraz „brak wyboru" działa w obie strony — Zod akceptuje null/""/undefined, a populacja pomija puste wartości.
18
+
19
+ ### Fixed
20
+ - Schemat Zod relacji niewymaganej (`generateZodSchemaFromField`) akceptuje teraz `null` obok `""`/`undefined` (`z.string().nullish().default("")`). Wcześniej `null` (znormalizowane dane lub wyczyszczenie pola w adminie) rzucał „Invalid input: expected string, received null" i blokował zapis wpisu. Bez nowej walidacji uuid — dowolny string nadal przechodzi, więc zero regresji dla istniejących danych.
21
+ - `resolveRelationFields` pomija puste stringi przy zbieraniu ID do populacji (relacje pojedyncze i multiple). Pusty string nie trafia już do zapytania `WHERE id IN ('')`, które Postgres odrzucał jako nieprawidłowy uuid (`invalid input syntax for type uuid: ""`).
22
+ - Efekt łączny: opcjonalna relacja bez wyboru działa w obie strony — zapis w adminie (Zod akceptuje null/"") oraz odczyt na froncie (populacja pomija puste). Dodany regression test dla akceptacji null/""/undefined.
23
+
6
24
  ## 0.36.2 — 2026-06-08
7
25
 
8
26
  Per-status overrides (`ShopConfig.orderStatuses`) + globalny rejestr subject placeholders (`formatSubject`/`ShopConfig.emailSubjectPlaceholders`) + nowy hook `ShopConfig.resolveStatusSubject` do czytania subject z CMS + Handlebars helper `{{{structured}}}` renderujący `StructuredContentDoc` (TipTap) jako email-safe HTML z automatycznym token replacement. Email context rozszerzony o billing fields (firma/NIP/adres/telefon). Dwa fixy bugów w resolverze CMS singletons w mailach. Zaprojektowane pod sklepy ze szkoleniami/wydarzeniami (stationary), gdzie statusy `preparing`/`sent` nie mają sensu. Wszystkie API generyczne, do reusu w innych consumer projects.
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.36.2)
1
+ # Includio CMS Documentation (v0.36.4)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/ROADMAP.md CHANGED
@@ -57,6 +57,7 @@
57
57
  ## Backlog
58
58
 
59
59
  - [ ] `[feature]` `[P2]` Date/datetime field — przebudowa na shadcn-svelte (bits-ui Calendar/DatePicker) zamiast natywnego inputu. Zgłoszone w QA Etap 5a (4/5); funkcjonalnie OK, odłożone jako osobny redesign pola daty. <!-- files: src/lib/admin/components/fields/date-field.svelte, datetime-field.svelte -->
60
+ - [ ] `[feature]` `[P2]` `<Video>` — eksponować ref wewnętrznego `<video>` (np. bindable `element` prop lub forward `bind:this`). Obecnie konsument potrzebujący programowej kontroli (play/pause, scrub, mute toggle) musi owijać w `<div bind:this>` + `querySelector('video')` — boilerplate i kruche. Zgłoszone przy customowym hero-wideo (autoplay + przycisk pauzy). <!-- files: src/lib/sveltekit/components/video.svelte -->
60
61
  - [ ] `[feature]` `[P1]` Re-introduce entry autosave with strict draft-only guard (opt-in via `cms.config.ts`, never touch published versions, debounced + visual countdown). Removed in 0.26.0 / S8 because old impl could touch published data ambiguously.
61
62
  - [ ] `[feature]` `[P2]` Migrate `shipping-method-form` na sveltekit-superforms + formsnap (S8 zostawił hand-rolled validation + FormErrorSummary; pełna migracja wymaga schema dla multi-lang record + dynamic carrier config + price net/gross toggle).
62
63
  - [ ] `[feature]` `[P2]` Storybook story dla `entry-page` (Default/Saving/Saved/Error/Draft/WithErrors/Confirmation) — wymaga mock context: `setRemotes` (5 commands), `setBreadcrumbs`, `setContentLanguage`, RawEntry/DbEntryVersion fixtures. S8 odłożone do S10 a11y sweep.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getCollectionEntryDisplayLabel } from '../../utils/entryLabel.js';
3
+ import { getRawEntryVersionData } from '../../utils/entryVersionData.js';
3
4
  import { getEntryThumbnail, type MediaThumbnailLookup } from '../../utils/entryThumbnail.js';
4
5
  import { arrayMove } from '../../utils/arrayMove.js';
5
6
  import { getRemotes } from '../../../sveltekit/index.js';
@@ -426,15 +427,7 @@
426
427
  }
427
428
 
428
429
  function getVersionData(entry: RawEntry): Record<string, unknown> | undefined {
429
- const lang = interfaceLanguage.current;
430
- const draft = entry.draftVersions[lang];
431
- const published = entry.publishedVersions[lang];
432
- if (draft?.data) return draft.data as Record<string, unknown>;
433
- if (published?.data) return published.data as Record<string, unknown>;
434
- // Fallback: any lang
435
- for (const v of Object.values(entry.draftVersions)) if (v?.data) return v.data as Record<string, unknown>;
436
- for (const v of Object.values(entry.publishedVersions)) if (v?.data) return v.data as Record<string, unknown>;
437
- return undefined;
430
+ return getRawEntryVersionData(entry, interfaceLanguage.current);
438
431
  }
439
432
 
440
433
  function getSearchText(entry: RawEntry): string {
@@ -467,7 +460,8 @@
467
460
  return resolveEntryUrl({
468
461
  slugField: collection.slugField,
469
462
  pathTemplate: collection.pathTemplate,
470
- data: data as Record<string, unknown>
463
+ data: data as Record<string, unknown>,
464
+ language: interfaceLanguage.current
471
465
  });
472
466
  }
473
467
 
@@ -560,9 +554,9 @@
560
554
 
561
555
  function mapEntryToRow(entry: RawEntry, lookup: Record<string, string> = {}, mediaThumbs: MediaThumbnailLookup = {}): CollectionDataTableRow {
562
556
  const data = getVersionData(entry) || {};
563
- // For custom columns, prefer published data (complete) over draft (may be partial)
564
- const lang = interfaceLanguage.current;
565
- const columnData = entry.publishedVersions[lang]?.data || entry.draftVersions[lang]?.data || data;
557
+ // getVersionData is already published-first (see getRawEntryVersionData),
558
+ // so it doubles as the source for custom columns.
559
+ const columnData = data;
566
560
  const customData: Record<string, unknown> = {};
567
561
  if (collection.listColumns) {
568
562
  for (const fieldSlug of collection.listColumns) {
@@ -0,0 +1,17 @@
1
+ import type { RawEntry } from '../../types/entries.js';
2
+ /**
3
+ * Effective entry data for the admin collection list.
4
+ *
5
+ * Resolves the version whose data the list should display for a raw entry,
6
+ * with **published-first** precedence: requested language published → draft,
7
+ * then any other language published → draft. Returns `undefined` only when the
8
+ * entry has no usable version data at all.
9
+ *
10
+ * Why published-first: `draftVersions[lang]` is the latest version with
11
+ * `publishedAt == null`, which can be an *older* lingering draft (e.g. the
12
+ * initial v1) when later edits were published without re-saving a draft. The
13
+ * rest of the admin (entry label, custom columns, entry editor) already prefers
14
+ * published — this keeps the URL subtitle, search text and a11y warnings
15
+ * consistent with it. Mirrors {@link getRawCollectionEntryLabelWithLanguage}.
16
+ */
17
+ export declare function getRawEntryVersionData(entry: RawEntry, language: string): Record<string, unknown> | undefined;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Effective entry data for the admin collection list.
3
+ *
4
+ * Resolves the version whose data the list should display for a raw entry,
5
+ * with **published-first** precedence: requested language published → draft,
6
+ * then any other language published → draft. Returns `undefined` only when the
7
+ * entry has no usable version data at all.
8
+ *
9
+ * Why published-first: `draftVersions[lang]` is the latest version with
10
+ * `publishedAt == null`, which can be an *older* lingering draft (e.g. the
11
+ * initial v1) when later edits were published without re-saving a draft. The
12
+ * rest of the admin (entry label, custom columns, entry editor) already prefers
13
+ * published — this keeps the URL subtitle, search text and a11y warnings
14
+ * consistent with it. Mirrors {@link getRawCollectionEntryLabelWithLanguage}.
15
+ */
16
+ export function getRawEntryVersionData(entry, language) {
17
+ for (const bucket of [entry.publishedVersions, entry.draftVersions]) {
18
+ const data = bucket?.[language]?.data;
19
+ if (data)
20
+ return data;
21
+ }
22
+ for (const bucket of [entry.publishedVersions, entry.draftVersions]) {
23
+ for (const version of Object.values(bucket ?? {})) {
24
+ if (version?.data)
25
+ return version.data;
26
+ }
27
+ }
28
+ return undefined;
29
+ }
@@ -214,7 +214,11 @@ export function generateZodSchemaFromField(field, languages, options = {
214
214
  if (field.required) {
215
215
  return z.string().uuid({ message: msg.required });
216
216
  }
217
- return z.string().optional().default('');
217
+ // Non-required: a relation id, or "no selection". The admin (and legacy
218
+ // data) may send '', null or undefined for an unset relation — accept
219
+ // all of them (`.nullish()` adds null + undefined) so an empty optional
220
+ // relation never blocks save. Empty values are skipped at populate time.
221
+ return z.string().nullish().default('');
218
222
  }
219
223
  case 'object': {
220
224
  // Children's `required` is enforced when the object itself is required OR
@@ -30,13 +30,15 @@ export async function resolveRelationFields(data, fields, ctx) {
30
30
  }
31
31
  switch (field.type) {
32
32
  case 'relation': {
33
+ // Skip empty strings — an unset optional relation stores '' and must
34
+ // never reach the id query (Postgres rejects '' as uuid).
33
35
  if (field.multiple && Array.isArray(val)) {
34
36
  for (const id of val) {
35
- if (typeof id === 'string')
37
+ if (typeof id === 'string' && id !== '')
36
38
  entriesIds.push(id);
37
39
  }
38
40
  }
39
- else if (typeof val === 'string') {
41
+ else if (typeof val === 'string' && val !== '') {
40
42
  entriesIds.push(val);
41
43
  }
42
44
  break;
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,12 @@
1
+ export const update = {
2
+ version: '0.36.3',
3
+ date: '2026-06-11',
4
+ description: 'Relacje opcjonalne — fix: niewybrana relacja (pole bez `required`) blokowała zapis wpisu („Invalid input: expected string, received null") albo wywalała populację przy pustym stringu. Teraz „brak wyboru" działa w obie strony — Zod akceptuje null/""/undefined, a populacja pomija puste wartości.',
5
+ features: [],
6
+ fixes: [
7
+ 'Schemat Zod relacji niewymaganej (`generateZodSchemaFromField`) akceptuje teraz `null` obok `""`/`undefined` (`z.string().nullish().default("")`). Wcześniej `null` (znormalizowane dane lub wyczyszczenie pola w adminie) rzucał „Invalid input: expected string, received null" i blokował zapis wpisu. Bez nowej walidacji uuid — dowolny string nadal przechodzi, więc zero regresji dla istniejących danych.',
8
+ '`resolveRelationFields` pomija puste stringi przy zbieraniu ID do populacji (relacje pojedyncze i multiple). Pusty string nie trafia już do zapytania `WHERE id IN (\'\')`, które Postgres odrzucał jako nieprawidłowy uuid (`invalid input syntax for type uuid: ""`).',
9
+ 'Efekt łączny: opcjonalna relacja bez wyboru działa w obie strony — zapis w adminie (Zod akceptuje null/"") oraz odczyt na froncie (populacja pomija puste). Dodany regression test dla akceptacji null/""/undefined.'
10
+ ],
11
+ breakingChanges: []
12
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,12 @@
1
+ export const update = {
2
+ version: '0.36.4',
3
+ date: '2026-06-11',
4
+ description: 'Lista wpisów kolekcji — fix: URL pod nazwą wpisu (oraz wyszukiwarka i ostrzeżenia a11y) znikał, gdy początkowy draft v1 różnił się od późniejszej opublikowanej treści (np. slug ustawiony dopiero po publikacji). Ujednolicono precedencję wyboru wersji w jednym helperze (published-first), spójnie z resztą admina.',
5
+ features: [],
6
+ fixes: [
7
+ '`getVersionData` w liście kolekcji (`collection-entries.svelte`) preferowało draft przed published. `draftVersions[lang]` to najnowsza wersja z `publishedAt == null` — co może być przestarzałym początkowym draftem (v1), starszym niż najnowsza publikacja, gdy późniejsze edycje szły tylko przez publikację. Efekt: subtytuł URL czytał pusty/nieaktualny slug z v1 i znikał, mimo że opublikowana wersja miała slug. Naprawione przez nowy SSOT `getRawEntryVersionData(entry, language)` z precedencją published-first (requested lang published → draft, potem any lang published → draft) — spójną z `getRawCollectionEntryLabelWithLanguage`, custom columns i edytorem wpisu.',
8
+ 'Ten sam helper zasila też tekst wyszukiwarki (`getSearchText`) i ostrzeżenia a11y (`countA11yWarnings`) — wcześniej również czytały przestarzały draft. `columnData` uproszczone (redundantny published-first override usunięty).',
9
+ '`extractEntryUrl` przekazuje teraz `language` do `resolveEntryUrl` — bez efektu dla płaskiego slug, ale poprawne dla projektów wielojęzycznych ze zlokalizowanym slugiem. Dodany regression test (`entryVersionData.spec.ts`) odtwarzający scenariusz pustego draftu v1 + opublikowanego sluga.'
10
+ ],
11
+ breakingChanges: []
12
+ };
@@ -69,6 +69,8 @@ import { update as update0350 } from './0.35.0/index.js';
69
69
  import { update as update0360 } from './0.36.0/index.js';
70
70
  import { update as update0361 } from './0.36.1/index.js';
71
71
  import { update as update0362 } from './0.36.2/index.js';
72
+ import { update as update0363 } from './0.36.3/index.js';
73
+ import { update as update0364 } from './0.36.4/index.js';
72
74
  export const updates = [
73
75
  update0065,
74
76
  update0066,
@@ -140,7 +142,9 @@ export const updates = [
140
142
  update0350,
141
143
  update0360,
142
144
  update0361,
143
- update0362
145
+ update0362,
146
+ update0363,
147
+ update0364
144
148
  ];
145
149
  export const getUpdatesFrom = (fromVersion) => {
146
150
  const fromParts = fromVersion.split('.').map(Number);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.36.2",
3
+ "version": "0.36.4",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",