includio-cms 0.36.1 → 0.36.3

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,8 +1,8 @@
1
- # includio-cms — Public API v0.36.1
1
+ # includio-cms — Public API v0.36.3
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
5
- Entry points: **19** · Stable: **492** · Experimental: **4**
5
+ Entry points: **19** · Stable: **495** · Experimental: **4**
6
6
 
7
7
  Tags:
8
8
  - `@public` — frozen for v1.0; semver-protected.
@@ -446,6 +446,7 @@ Tags:
446
446
  - `type Currency = 'PLN'`
447
447
  - `defineShop(config: ShopConfig): ResolvedShopConfig`
448
448
  - `type DepositAmount = { type: 'percent'; value: number } | { type: 'amount'; value: number }` — Deposit amount specifier. `percent` charges `floor(base * value / 100)` of
449
+ - `interface EmailContext` — Context passed to a {@link SubjectPlaceholderResolver}. Currently carries
449
450
  - `fakturowniaAdapter(opts: FakturowniaAdapterOptions): InvoicingAdapter` — Invoicing adapter backed by Fakturownia (fakturownia.pl). Issues a paid VAT
450
451
  - `interface FakturowniaAdapterOptions`
451
452
  - `filterUpcoming(variants: T[], config: VariantExpiryConfig | null, now?: Date): T[]` — Return the subset of `variants` that have not yet expired. Order is
@@ -471,6 +472,7 @@ Tags:
471
472
  - `type Order = typeof shopOrdersTable.$inferSelect` — Public row type for a single shop order — mirrors the Drizzle `shop_orders`
472
473
  - `interface OrderRef`
473
474
  - `type OrderStatus = | 'new' | 'awaitingPayment' | 'paid' | 'preparing' | 'sent' | 'done' | 'cancelled' | 'paymentReje...`
475
+ - `interface OrderStatusConfig` — Per-status configuration. Lets consumer projects hide a built-in status from
474
476
  - `interface PartialPayment` — Persisted partial-payment summary on `order.partialPayment` when the order
475
477
  - `interface PaymentAdapter`
476
478
  - `interface PaymentCreateContext`
@@ -490,6 +492,7 @@ Tags:
490
492
  - `interface ShopFeatures`
491
493
  - `stripeAdapter(opts: StripeAdapterOptions): PaymentAdapter` — Stripe payment adapter (Checkout Session flow).
492
494
  - `interface StripeAdapterOptions`
495
+ - `type SubjectPlaceholderResolver = (order: Order, ctx: EmailContext) => string` — Resolves one `{placeholder}` token in an email subject template to a string.
493
496
  - `type VariantAttribute = | VariantAttributeText | VariantAttributeNumber | VariantAttributeDatetime | VariantAttributeSele...` — Schema descriptor for one product-variant attribute (city, startsAt, …).
494
497
  - `interface VariantAttributeBoolean`
495
498
  - `interface VariantAttributeDatetime`
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
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.3 — 2026-06-11
7
+
8
+ 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.
9
+
10
+ ### Fixed
11
+ - 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.
12
+ - `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: ""`).
13
+ - 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.
14
+
15
+ ## 0.36.2 — 2026-06-08
16
+
17
+ 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.
18
+
19
+ ### Added
20
+ - **`ShopConfig.orderStatuses?: OrderStatusConfig[]`** — opcjonalna lista per-status overrides. `hidden: true` chowa status z admin dropdownu (`shop-order-detail-page.svelte`) i blokuje manual transitions z admina rzucając nowy `HiddenStatusTransitionError` (kod `HIDDEN_STATUS_TRANSITION`). `sendEmail: false` powoduje, że `sendOrderStatusEmail` early-returnuje przy wejściu w ten status. Webhook / carrier-driven transitions BYPASSują guard — provider-driven update musi przejść bez względu na config (rola engine fundamentalna). Nowy parametr `updateOrderStatus(orderId, status, { source: "admin" })` rozróżnia kontekst; `source: "webhook" | "system"` (default) zawsze pomija check. Pure helpery: `assertManualStatusAllowed(status, orderStatuses)` (rzuca) i `shouldSendStatusEmail(status, orderStatuses)` (zwraca bool).
21
+ - **`formatSubject` + `builtInSubjectPlaceholders`** (`$lib/shop/server/emailSubject.js`) — generyczna interpolacja `{key}` w subject mailach. Built-in: `{order_number}`, `{customer_name}`, `{customer_email}`, `{total_gross}` (formatted PLN currency), `{currency}`. Nowy `ShopConfig.emailSubjectPlaceholders?: Record<string, SubjectPlaceholderResolver>` — projektowe rozszerzenia (np. `{event_date}` dla szkoleń), mergowane na built-in (custom wins). `sendOrderStatusEmail` przepuszcza fallback subject (`STATUS_SUBJECTS[status][lang] · {order_number}`) przez `formatSubject` zanim wyśle. Nieznane placeholdery są lenient — zostają w stringu.
22
+ - **`ShopConfig.resolveStatusSubject?(status, ctx) => string | null | undefined`** — async hook pozwalający consumerowi pobrać template subject z dowolnego źródła (typically CMS singleton). Wywoływany w `sendOrderStatusEmail` przed `formatSubject`, więc tokeny działają tak samo jak w defaultach. Pusta wartość / błąd → fallback do `STATUS_SUBJECTS`. Paczka pozostaje agnostyczna o CMS schemie consumera.
23
+ - **Handlebars helper `{{{structured doc}}}`** (`emailTemplateRegistry.ts`) — renderuje `StructuredContentDoc` (output TipTap fielda `content`) jako inline HTML dla maili. Używa `structuredToHtml()` z core; zwraca `SafeString`. Po render — automatyczne **token replacement** przez `formatSubject` (built-in + `emailSubjectPlaceholders`) jeśli `order` jest w Handlebars context. Pozwala wpisać `{order_number}` w polu TipTap w CMSie i mieć je podmienione w wysłanym mailu. Pusty/undefined doc → "".
24
+ - **Email context rozszerzony o billing fields** (`email.ts`): `order.customerCompanyName`, `customerNip`, `customerPhone`, `billingAddress` (`{street, postalCode, city, country?}`) + flagi `hasCompany` / `hasBillingAddress` dla wygodnych `{{#if}}` w templates. Pozwala renderować WooCommerce-style billing block bez custom JOIN.
25
+
26
+ ### Fixed
27
+ - **`extractCmsSlugs` skanuje partials** — wcześniej AST tylko głównego templatu był parsowany, więc `cms.<slug>.*` referencje w partials (np. `cms.globalSettings.legalEntity` w `_partials/footer.pl.html`) były ignorowane → singleton nie był prefetchowany → context był pusty → fallbacki renderowane mimo wypełnionego CMSa. Teraz dodatkowo iteruje przez wszystkie zarejestrowane `Handlebars.partials` i merguje slugi.
28
+ - **`resolveCmsContext` poprawnie spłaszcza entry** — wcześniej używał `entry?.data ?? {}`, ale `resolveEntry` zwraca obiekt spreadowany (`{ _id, _slug, _type, _publishedAt, _url, ...fields }`) bez pola `.data`. Efekt: każdy `cms.<slug>` w mailu był pustym obiektem. Teraz strip-uje meta keys (`_*`) i zwraca user fields. Mock w testach był niezgodny z prawdziwym API, dlatego bug przetrwał — naprawiony, dodany regression test dla nested path.
29
+
6
30
  ## 0.36.1 — 2026-06-08
7
31
 
8
32
  Fakturownia adapter — fix B2C: gdy brak `buyer.nip` i `buyer.companyName`, payload zawiera `buyer_first_name` + `buyer_last_name` (rozdzielone przez `splitFullName` — spójnie z PayU/InPost). Bez tej zmiany Fakturownia API odrzuca fakturę dla osoby fizycznej z 422 `buyer_tax_no — nie może być puste`.
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.36.1)
1
+ # Includio CMS Documentation (v0.36.3)
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.
@@ -148,6 +148,13 @@
148
148
  { value: 'refunded', label: 'Zwrócone' }
149
149
  ];
150
150
 
151
+ // Statuses the consumer marked `hidden: true` in defineShop({ orderStatuses })
152
+ // — never shown in the dropdown (manual transitions blocked server-side too).
153
+ const hiddenStatuses = $derived(
154
+ new Set<OrderStatus>(((query.current?.hiddenStatuses as OrderStatus[] | undefined) ?? []))
155
+ );
156
+ const visibleStatuses = $derived(STATUSES.filter((s) => !hiddenStatuses.has(s.value)));
157
+
151
158
  let newStatus = $state<OrderStatus | ''>('');
152
159
  let note = $state('');
153
160
  let saving = $state(false);
@@ -482,7 +489,7 @@
482
489
  class="border-border w-full rounded-lg border px-3 py-2 text-sm"
483
490
  >
484
491
  <option value="">— wybierz —</option>
485
- {#each STATUSES as s (s.value)}
492
+ {#each visibleStatuses as s (s.value)}
486
493
  <option value={s.value} disabled={s.value === order.status}>{s.label}</option>
487
494
  {/each}
488
495
  </select>
@@ -211,6 +211,7 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
211
211
  code: string;
212
212
  discountAmount: number;
213
213
  } | null;
214
+ hiddenStatuses: import("../../shop/types.js").OrderStatus[];
214
215
  } | null>;
215
216
  export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand<{
216
217
  orderId: string;
@@ -171,7 +171,8 @@ export const getOrderForAdmin = query(z.string(), async (id) => {
171
171
  getOrderStatusHistory(id),
172
172
  getOrderCoupon(id).catch(() => null)
173
173
  ]);
174
- return { order, items, history, coupon };
174
+ const hiddenStatuses = getCMS().shopConfig?.orderStatuses?.filter((s) => s.hidden).map((s) => s.key) ?? [];
175
+ return { order, items, history, coupon, hiddenStatuses };
175
176
  });
176
177
  export const updateOrderStatusCmd = command(z.object({
177
178
  orderId: z.string(),
@@ -179,7 +180,11 @@ export const updateOrderStatusCmd = command(z.object({
179
180
  note: z.string().optional()
180
181
  }), async ({ orderId, status, note }) => {
181
182
  requireAuth();
182
- const updated = await updateOrderStatus(orderId, status, { note, changedBy: 'admin' });
183
+ const updated = await updateOrderStatus(orderId, status, {
184
+ note,
185
+ changedBy: 'admin',
186
+ source: 'admin'
187
+ });
183
188
  return updated;
184
189
  });
185
190
  export const deleteOrderCmd = command(z.object({ orderId: z.string() }), async ({ orderId }) => {
@@ -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;
@@ -12,6 +12,6 @@ export type { InpostAdapterOptions, InpostSenderAddress, GeowidgetConfigPreset,
12
12
  export { fakturowniaAdapter } from './adapters/fakturownia/index.js';
13
13
  export type { FakturowniaAdapterOptions } from './adapters/fakturownia/index.js';
14
14
  export { isValidNip } from './nip.js';
15
- export type { ShopConfig, ResolvedShopConfig, Currency, Order, OrderStatus, PaymentAdapter, PaymentCreateContext, PaymentRefundInput, PaymentRefundResult, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, CouponRef, I18nText, VariantAttribute, VariantAttributeText, VariantAttributeNumber, VariantAttributeDatetime, VariantAttributeSelect, VariantAttributeBoolean, VariantAttributeImage, VariantAttributeEntry, VariantAttributeSlug, VariantLabelConfig, VariantExpiryConfig, PaymentPolicy, DepositAmount, PartialPayment, InvoicingAdapter, InvoiceIssuePolicy, InvoiceBuyer, InvoiceLineItem, InvoicePayload, InvoiceCreateResult, InvoiceContext } from './types.js';
15
+ export type { ShopConfig, ResolvedShopConfig, Currency, Order, OrderStatus, OrderStatusConfig, SubjectPlaceholderResolver, EmailContext, PaymentAdapter, PaymentCreateContext, PaymentRefundInput, PaymentRefundResult, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, CouponRef, I18nText, VariantAttribute, VariantAttributeText, VariantAttributeNumber, VariantAttributeDatetime, VariantAttributeSelect, VariantAttributeBoolean, VariantAttributeImage, VariantAttributeEntry, VariantAttributeSlug, VariantLabelConfig, VariantExpiryConfig, PaymentPolicy, DepositAmount, PartialPayment, InvoicingAdapter, InvoiceIssuePolicy, InvoiceBuyer, InvoiceLineItem, InvoicePayload, InvoiceCreateResult, InvoiceContext } from './types.js';
16
16
  export { interpolateTemplate, renderShopName } from './template.js';
17
17
  export type { I18nTemplate } from './template.js';
@@ -1,4 +1,12 @@
1
- import type { OrderStatus } from '../types.js';
1
+ import type { OrderStatus, OrderStatusConfig } from '../types.js';
2
+ /**
3
+ * Decide whether a status-change email should be sent for a given status,
4
+ * given the consumer-supplied per-status config (`ShopConfig.orderStatuses`).
5
+ * Defaults to `true` — only an entry with `sendEmail === false` suppresses
6
+ * the email. Pure helper, exported for testing + reuse.
7
+ * @public
8
+ */
9
+ export declare function shouldSendStatusEmail(status: OrderStatus, orderStatuses: OrderStatusConfig[] | undefined): boolean;
2
10
  /**
3
11
  * List of template names a shop install must ship in `dist/shop/templates/`.
4
12
  * Consumed by `validateBuiltinTemplates` at CMS bootstrap.
@@ -4,6 +4,18 @@ import { getOrderCoupon } from './coupons.js';
4
4
  import { requireShopConfig } from './db.js';
5
5
  import { buildOrderViewUrl } from './order-access-url.js';
6
6
  import { renderEmailTemplate } from './emailTemplateRegistry.js';
7
+ import { formatSubject } from './emailSubject.js';
8
+ /**
9
+ * Decide whether a status-change email should be sent for a given status,
10
+ * given the consumer-supplied per-status config (`ShopConfig.orderStatuses`).
11
+ * Defaults to `true` — only an entry with `sendEmail === false` suppresses
12
+ * the email. Pure helper, exported for testing + reuse.
13
+ * @public
14
+ */
15
+ export function shouldSendStatusEmail(status, orderStatuses) {
16
+ const cfg = orderStatuses?.find((s) => s.key === status);
17
+ return cfg?.sendEmail !== false;
18
+ }
7
19
  const STATUS_SUBJECTS = {
8
20
  new: { pl: 'Zamówienie przyjęte', en: 'Order received' },
9
21
  awaitingPayment: { pl: 'Oczekiwanie na płatność', en: 'Awaiting payment' },
@@ -53,6 +65,8 @@ export const REQUIRED_TEMPLATE_NAMES = [
53
65
  export async function sendOrderStatusEmail(orderId, status) {
54
66
  const cms = getCMS();
55
67
  const shop = requireShopConfig();
68
+ if (!shouldSendStatusEmail(status, shop.orderStatuses))
69
+ return;
56
70
  const emailAdapter = cms.emailAdapter;
57
71
  if (!emailAdapter) {
58
72
  console.warn('[shop] No email adapter configured — skipping status email.');
@@ -134,6 +148,12 @@ export async function sendOrderStatusEmail(orderId, status) {
134
148
  status: order.status,
135
149
  customerName: order.customerName ?? '',
136
150
  customerEmail: order.customerEmail,
151
+ customerPhone: order.customerPhone ?? null,
152
+ customerCompanyName: order.customerCompanyName ?? null,
153
+ customerNip: order.customerNip ?? null,
154
+ billingAddress: (order.billingAddress ?? null),
155
+ hasCompany: Boolean(order.customerCompanyName || order.customerNip),
156
+ hasBillingAddress: Boolean(order.billingAddress && order.billingAddress.street),
137
157
  language: order.language,
138
158
  totalGross: formatPrice(order.totalGross, order.currency),
139
159
  totalNet: formatPrice(order.totalNet, order.currency),
@@ -167,7 +187,21 @@ export async function sendOrderStatusEmail(orderId, status) {
167
187
  adminEmail: shop.adminEmail ?? null
168
188
  }
169
189
  };
170
- const subject = `${STATUS_SUBJECTS[status][subjectKey]} · ${order.number}`;
190
+ // Subject template lookup: project hook (typically reads CMS) → package default.
191
+ // Both feed through `formatSubject` so `{order_number}` etc. are formatted the same way.
192
+ let subjectTemplate;
193
+ if (shop.resolveStatusSubject) {
194
+ try {
195
+ subjectTemplate = await shop.resolveStatusSubject(status, { order, language: lang });
196
+ }
197
+ catch (err) {
198
+ console.error('[shop] resolveStatusSubject threw — falling back to default:', err);
199
+ }
200
+ }
201
+ if (!subjectTemplate || typeof subjectTemplate !== 'string' || !subjectTemplate.trim()) {
202
+ subjectTemplate = `${STATUS_SUBJECTS[status][subjectKey]} · {order_number}`;
203
+ }
204
+ const subject = formatSubject(subjectTemplate, order, { language: lang }, shop.emailSubjectPlaceholders);
171
205
  const templateName = STATUS_TO_TEMPLATE[status];
172
206
  let html;
173
207
  try {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Subject placeholder registry — generic `{key}`-style interpolation for the
3
+ * email subject string fed to {@link sendOrderStatusEmail}. Built-in keys
4
+ * cover the data common to every shop order; consumer projects merge custom
5
+ * resolvers via `ShopConfig.emailSubjectPlaceholders` to add domain-specific
6
+ * tokens (e.g. `{event_date}`).
7
+ *
8
+ * Unknown placeholders are left untouched (lenient), so a typo in CMS-managed
9
+ * copy never breaks delivery.
10
+ */
11
+ import type { EmailContext, Order, SubjectPlaceholderResolver } from '../types.js';
12
+ /**
13
+ * Default subject-placeholder resolvers, available in every shop install.
14
+ * Merged with `ShopConfig.emailSubjectPlaceholders` in {@link formatSubject};
15
+ * custom resolvers take precedence on key collision.
16
+ * @public
17
+ */
18
+ export declare const builtInSubjectPlaceholders: Record<string, SubjectPlaceholderResolver>;
19
+ /**
20
+ * Replace every `{key}` token in `template` with the result of the matching
21
+ * resolver. Custom resolvers (e.g. from `ShopConfig.emailSubjectPlaceholders`)
22
+ * are merged on top of {@link builtInSubjectPlaceholders} — colliding keys
23
+ * are overridden. Unknown keys are left untouched.
24
+ *
25
+ * @param template Subject template (CMS string or hardcoded fallback).
26
+ * @param order Resolved order row passed to each resolver.
27
+ * @param ctx Minimal email context (currently just `language`).
28
+ * @param custom Optional per-project resolvers merged into the registry.
29
+ * @public
30
+ */
31
+ export declare function formatSubject(template: string, order: Order, ctx: EmailContext, custom?: Record<string, SubjectPlaceholderResolver>): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Subject placeholder registry — generic `{key}`-style interpolation for the
3
+ * email subject string fed to {@link sendOrderStatusEmail}. Built-in keys
4
+ * cover the data common to every shop order; consumer projects merge custom
5
+ * resolvers via `ShopConfig.emailSubjectPlaceholders` to add domain-specific
6
+ * tokens (e.g. `{event_date}`).
7
+ *
8
+ * Unknown placeholders are left untouched (lenient), so a typo in CMS-managed
9
+ * copy never breaks delivery.
10
+ */
11
+ function formatMoney(minorUnits, currency) {
12
+ const cur = typeof currency === 'string' && currency.length > 0 ? currency : 'PLN';
13
+ return new Intl.NumberFormat('pl-PL', {
14
+ style: 'currency',
15
+ currency: cur,
16
+ minimumFractionDigits: 2
17
+ }).format(minorUnits / 100);
18
+ }
19
+ /**
20
+ * Default subject-placeholder resolvers, available in every shop install.
21
+ * Merged with `ShopConfig.emailSubjectPlaceholders` in {@link formatSubject};
22
+ * custom resolvers take precedence on key collision.
23
+ * @public
24
+ */
25
+ export const builtInSubjectPlaceholders = {
26
+ order_number: (o) => o.number,
27
+ customer_name: (o) => o.customerName ?? '',
28
+ customer_email: (o) => o.customerEmail,
29
+ total_gross: (o) => formatMoney(o.totalGross, o.currency),
30
+ currency: (o) => o.currency
31
+ };
32
+ const PLACEHOLDER_RE = /\{(\w+)\}/g;
33
+ /**
34
+ * Replace every `{key}` token in `template` with the result of the matching
35
+ * resolver. Custom resolvers (e.g. from `ShopConfig.emailSubjectPlaceholders`)
36
+ * are merged on top of {@link builtInSubjectPlaceholders} — colliding keys
37
+ * are overridden. Unknown keys are left untouched.
38
+ *
39
+ * @param template Subject template (CMS string or hardcoded fallback).
40
+ * @param order Resolved order row passed to each resolver.
41
+ * @param ctx Minimal email context (currently just `language`).
42
+ * @param custom Optional per-project resolvers merged into the registry.
43
+ * @public
44
+ */
45
+ export function formatSubject(template, order, ctx, custom) {
46
+ const resolvers = custom
47
+ ? { ...builtInSubjectPlaceholders, ...custom }
48
+ : builtInSubjectPlaceholders;
49
+ return template.replace(PLACEHOLDER_RE, (match, key) => {
50
+ const fn = resolvers[key];
51
+ return fn ? fn(order, ctx) : match;
52
+ });
53
+ }
@@ -20,7 +20,9 @@ interface ResolveResult {
20
20
  declare function resolveTemplatePath(projectDir: string, name: string, lang: Lang, fallbackLang: Lang): ResolveResult | null;
21
21
  /**
22
22
  * Traverse Handlebars AST and collect all `cms.<slug>.*` accessor slugs.
23
- * Returns the unique set so the renderer can prefetch them.
23
+ * Also scans all currently-registered partials so `cms.*` refs inside
24
+ * `{{> footer}}` etc. are also prefetched. Returns the unique set so the
25
+ * renderer can prefetch them.
24
26
  */
25
27
  declare function extractCmsSlugs(ast: hbs.AST.Program): Set<string>;
26
28
  /**
@@ -12,6 +12,9 @@ import { fileURLToPath } from 'node:url';
12
12
  import Handlebars from 'handlebars';
13
13
  import { getCMS } from '../../core/cms.js';
14
14
  import { resolveEntry } from '../../sveltekit/server/index.js';
15
+ import { structuredToHtml } from '../../core/fields/structuredToHtml.js';
16
+ import { formatSubject } from './emailSubject.js';
17
+ import { requireShopConfig } from './db.js';
15
18
  const PACKAGE_TEMPLATES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
16
19
  const cache = new Map();
17
20
  let helpersRegistered = false;
@@ -50,6 +53,32 @@ function registerHelpers() {
50
53
  return new Intl.DateTimeFormat('pl-PL').format(d);
51
54
  });
52
55
  Handlebars.registerHelper('eq', (a, b) => a === b);
56
+ // `{{{structured doc}}}` — render a TipTap `StructuredContentDoc` (output of
57
+ // AriaCMS `content` fields) as inline HTML for email. Returns a SafeString
58
+ // so the caller's triple-stache passes the markup through; pre-rendered text
59
+ // inside `doc` is still HTML-escaped by `structuredToHtml`.
60
+ //
61
+ // Token replacement: any `{key}` token found inside the rendered HTML is
62
+ // substituted via `formatSubject` (built-in placeholders + project
63
+ // `emailSubjectPlaceholders`). Requires `order` in the rendering context.
64
+ // Uses `function` (not arrow) so `this` is the Handlebars root context.
65
+ Handlebars.registerHelper('structured', function (doc) {
66
+ if (!doc || typeof doc !== 'object')
67
+ return '';
68
+ let html = structuredToHtml(doc);
69
+ // Best-effort token replacement — silently skip when no order in scope.
70
+ const order = this?.order;
71
+ if (order) {
72
+ try {
73
+ const shop = requireShopConfig();
74
+ html = formatSubject(html, order, { language: order.language ?? 'pl' }, shop.emailSubjectPlaceholders);
75
+ }
76
+ catch {
77
+ // shop config not initialized — leave HTML as-is
78
+ }
79
+ }
80
+ return new Handlebars.SafeString(html);
81
+ });
53
82
  helpersRegistered = true;
54
83
  }
55
84
  /**
@@ -77,7 +106,9 @@ function resolveTemplatePath(projectDir, name, lang, fallbackLang) {
77
106
  }
78
107
  /**
79
108
  * Traverse Handlebars AST and collect all `cms.<slug>.*` accessor slugs.
80
- * Returns the unique set so the renderer can prefetch them.
109
+ * Also scans all currently-registered partials so `cms.*` refs inside
110
+ * `{{> footer}}` etc. are also prefetched. Returns the unique set so the
111
+ * renderer can prefetch them.
81
112
  */
82
113
  function extractCmsSlugs(ast) {
83
114
  const slugs = new Set();
@@ -114,6 +145,19 @@ function extractCmsSlugs(ast) {
114
145
  }
115
146
  };
116
147
  visit(ast);
148
+ // Also visit ASTs of every registered partial — `cms.*` paths in shared
149
+ // partials like footer/legal_notice would otherwise be missed and the
150
+ // underlying singleton would never be prefetched.
151
+ for (const partial of Object.values(Handlebars.partials)) {
152
+ if (typeof partial !== 'string')
153
+ continue;
154
+ try {
155
+ visit(Handlebars.parse(partial));
156
+ }
157
+ catch {
158
+ // malformed partial — already logged elsewhere when it tries to render
159
+ }
160
+ }
117
161
  return slugs;
118
162
  }
119
163
  /**
@@ -185,7 +229,16 @@ async function resolveCmsContext(slugs) {
185
229
  const entries = await Promise.all(slugList.map(async (slug) => {
186
230
  try {
187
231
  const entry = await resolveEntry({ collection: slug });
188
- return [slug, entry?.data ?? {}];
232
+ if (!entry)
233
+ return [slug, {}];
234
+ // `resolveEntry` returns a flat object: `{ _id, _slug, _type, _publishedAt, _url, ...fields }`.
235
+ // Strip leading-underscore meta keys so `cms.<slug>.<field>` matches the user-defined schema.
236
+ const data = {};
237
+ for (const [k, v] of Object.entries(entry)) {
238
+ if (!k.startsWith('_'))
239
+ data[k] = v;
240
+ }
241
+ return [slug, data];
189
242
  }
190
243
  catch {
191
244
  return [slug, {}];
@@ -1,6 +1,6 @@
1
1
  import { shopOrderItemsTable, shopOrderStatusHistoryTable, shopOrdersTable } from '../../db-postgres/schema/shop/index.js';
2
2
  import type { CartItemRef } from '../cart/types.js';
3
- import type { OrderStatus } from '../types.js';
3
+ import type { OrderStatus, OrderStatusConfig } from '../types.js';
4
4
  /**
5
5
  * @public
6
6
  * Thrown by `createOrderFromCart` when the cart contains items from multiple
@@ -40,6 +40,25 @@ export type OrderDeletionDecision = {
40
40
  export declare function decideOrderDeletion(status: OrderStatus, invoice: {
41
41
  status: 'pending' | 'issued' | 'sent' | 'failed';
42
42
  } | null): OrderDeletionDecision;
43
+ /**
44
+ * @public
45
+ * Thrown by {@link assertManualStatusAllowed} (and {@link updateOrderStatus}
46
+ * when called from the admin UI) for a manual transition into a status the
47
+ * consumer marked as hidden in `ShopConfig.orderStatuses`. Webhook /
48
+ * carrier-driven transitions bypass this check — `source: 'webhook'`.
49
+ */
50
+ export declare class HiddenStatusTransitionError extends Error {
51
+ readonly status: OrderStatus;
52
+ readonly code = "HIDDEN_STATUS_TRANSITION";
53
+ constructor(status: OrderStatus);
54
+ }
55
+ /**
56
+ * @public
57
+ * Throw a {@link HiddenStatusTransitionError} when the target status is marked
58
+ * `hidden: true` in the consumer config. No-op when no config / no matching
59
+ * entry. Pure check, exported for testing + reuse by admin pre-flight UI.
60
+ */
61
+ export declare function assertManualStatusAllowed(status: OrderStatus, orderStatuses: OrderStatusConfig[] | undefined): void;
43
62
  /**
44
63
  * @public
45
64
  * Thrown by `softDeleteOrder` when the order can't be hidden: either its status
@@ -98,6 +117,13 @@ export declare function updateOrderStatus(orderId: string, status: OrderStatus,
98
117
  note?: string;
99
118
  changedBy?: string;
100
119
  paymentKind?: 'full' | 'deposit' | 'balance';
120
+ /**
121
+ * Caller context. `'admin'` enforces the `orderStatuses[].hidden` guard
122
+ * (throws {@link HiddenStatusTransitionError}). `'webhook'` and `'system'`
123
+ * (default) bypass it — provider-driven and engine-internal transitions
124
+ * are always allowed so a hidden status never silently drops a webhook.
125
+ */
126
+ source?: 'admin' | 'webhook' | 'system';
101
127
  }): Promise<OrderRow>;
102
128
  /**
103
129
  * @internal
@@ -60,6 +60,33 @@ export function decideOrderDeletion(status, invoice) {
60
60
  }
61
61
  return { ok: true };
62
62
  }
63
+ /**
64
+ * @public
65
+ * Thrown by {@link assertManualStatusAllowed} (and {@link updateOrderStatus}
66
+ * when called from the admin UI) for a manual transition into a status the
67
+ * consumer marked as hidden in `ShopConfig.orderStatuses`. Webhook /
68
+ * carrier-driven transitions bypass this check — `source: 'webhook'`.
69
+ */
70
+ export class HiddenStatusTransitionError extends Error {
71
+ status;
72
+ code = 'HIDDEN_STATUS_TRANSITION';
73
+ constructor(status) {
74
+ super(`Manual transition into hidden status "${status}" is not allowed.`);
75
+ this.status = status;
76
+ this.name = 'HiddenStatusTransitionError';
77
+ }
78
+ }
79
+ /**
80
+ * @public
81
+ * Throw a {@link HiddenStatusTransitionError} when the target status is marked
82
+ * `hidden: true` in the consumer config. No-op when no config / no matching
83
+ * entry. Pure check, exported for testing + reuse by admin pre-flight UI.
84
+ */
85
+ export function assertManualStatusAllowed(status, orderStatuses) {
86
+ const cfg = orderStatuses?.find((s) => s.key === status);
87
+ if (cfg?.hidden === true)
88
+ throw new HiddenStatusTransitionError(status);
89
+ }
63
90
  /**
64
91
  * @public
65
92
  * Thrown by `softDeleteOrder` when the order can't be hidden: either its status
@@ -385,6 +412,9 @@ export async function updateOrderStatus(orderId, status, opts = {}) {
385
412
  if (order.status === status)
386
413
  return order;
387
414
  const shop = requireShopConfig();
415
+ if (opts.source === 'admin') {
416
+ assertManualStatusAllowed(status, shop.orderStatuses);
417
+ }
388
418
  // Deposit-kind transition to `paid`: bump partialPayment.paidAmount to the
389
419
  // deposit amount, stamp paidAt, and flag balanceOwed = true. Stock is
390
420
  // permanently confirmed by the existing `paid` branch below — no TTL
@@ -8,6 +8,43 @@ export type Currency = 'PLN';
8
8
  */
9
9
  export type Order = typeof shopOrdersTable.$inferSelect;
10
10
  export type OrderStatus = 'new' | 'awaitingPayment' | 'paid' | 'preparing' | 'sent' | 'done' | 'cancelled' | 'paymentRejected' | 'refunded';
11
+ /**
12
+ * Per-status configuration. Lets consumer projects hide a built-in status from
13
+ * the admin status dropdown (`hidden`) and / or skip the customer status email
14
+ * when the order enters that status (`sendEmail: false`). Useful when the
15
+ * product is stationary (events, courses) and the shipping-oriented states
16
+ * (`preparing`, `sent`) don't apply.
17
+ *
18
+ * `hidden: true` blocks manual transitions from the admin UI, but webhook /
19
+ * carrier driven transitions still go through — those updates are
20
+ * engine-fundamental and the consumer can't always foresee what a provider
21
+ * will send.
22
+ * @public
23
+ */
24
+ export interface OrderStatusConfig {
25
+ key: OrderStatus;
26
+ /** Hide from admin dropdown + block manual admin transition. Default `false`. */
27
+ hidden?: boolean;
28
+ /** Send status email when the order enters this status. Default `true`. */
29
+ sendEmail?: boolean;
30
+ }
31
+ /**
32
+ * Context passed to a {@link SubjectPlaceholderResolver}. Currently carries
33
+ * just the resolved order language; expand as new built-in placeholders need
34
+ * extra data.
35
+ * @public
36
+ */
37
+ export interface EmailContext {
38
+ language: string;
39
+ }
40
+ /**
41
+ * Resolves one `{placeholder}` token in an email subject template to a string.
42
+ * Registered globally via {@link ShopConfig.emailSubjectPlaceholders} —
43
+ * consumer projects add their own (e.g. `{event_date}`) without touching the
44
+ * shop engine. Receives the full order row + a minimal context object.
45
+ * @public
46
+ */
47
+ export type SubjectPlaceholderResolver = (order: Order, ctx: EmailContext) => string;
11
48
  export type I18nText = {
12
49
  [lang: string]: string;
13
50
  };
@@ -437,6 +474,56 @@ export interface ShopConfig {
437
474
  /** Throw on missing tokens / unknown CMS slugs. Default `false` (lenient → ""). */
438
475
  strict?: boolean;
439
476
  };
477
+ /**
478
+ * Per-status overrides — hide a status from the admin dropdown and / or
479
+ * skip the customer status email when the order enters that status. Useful
480
+ * for product types that don't ship physically (stationary events, digital
481
+ * services). Each entry targets one of the built-in {@link OrderStatus}
482
+ * keys; built-in status semantics are preserved, only UI visibility +
483
+ * email-send are toggled.
484
+ *
485
+ * `hidden: true` blocks admin manual transitions but webhook-driven
486
+ * transitions are unaffected.
487
+ * @public
488
+ */
489
+ orderStatuses?: OrderStatusConfig[];
490
+ /**
491
+ * Project-specific subject placeholder resolvers, merged on top of the
492
+ * built-in registry (`order_number`, `customer_name`, `customer_email`,
493
+ * `total_gross`, `currency`). Use to add tokens specific to the consumer's
494
+ * domain (e.g. `{event_date}` for course shops).
495
+ *
496
+ * Templates use `{key}` syntax in any subject string (CMS or hardcoded
497
+ * fallback); unknown keys are left untouched (lenient).
498
+ * @public
499
+ */
500
+ emailSubjectPlaceholders?: Record<string, SubjectPlaceholderResolver>;
501
+ /**
502
+ * Optional resolver for the email subject template per order status.
503
+ * Receives the resolved status + order language and returns a subject
504
+ * template string (with `{placeholder}` tokens), or `null`/`undefined`
505
+ * to fall back to the package default.
506
+ *
507
+ * The returned string is fed through {@link formatSubject} just like the
508
+ * default, so built-in and {@link emailSubjectPlaceholders} tokens both work.
509
+ *
510
+ * Use this to source subjects from a CMS singleton (the package itself stays
511
+ * agnostic about your CMS schema). Async — you can hit the DB here.
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * resolveStatusSubject: async (status, { language }) => {
516
+ * const entry = await resolveEntry({ collection: 'shopEmails' });
517
+ * const key = STATUS_TO_CMS_KEY[status];
518
+ * return entry?.statuses?.[key]?.subject;
519
+ * }
520
+ * ```
521
+ * @public
522
+ */
523
+ resolveStatusSubject?: (status: OrderStatus, ctx: {
524
+ order: Order;
525
+ language: string;
526
+ }) => Promise<string | null | undefined> | string | null | undefined;
440
527
  /**
441
528
  * Optional callback fired after an order transitions to `paid`.
442
529
  *
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,17 @@
1
+ export const update = {
2
+ version: '0.36.2',
3
+ date: '2026-06-08',
4
+ description: '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.',
5
+ features: [
6
+ '**`ShopConfig.orderStatuses?: OrderStatusConfig[]`** — opcjonalna lista per-status overrides. `hidden: true` chowa status z admin dropdownu (`shop-order-detail-page.svelte`) i blokuje manual transitions z admina rzucając nowy `HiddenStatusTransitionError` (kod `HIDDEN_STATUS_TRANSITION`). `sendEmail: false` powoduje, że `sendOrderStatusEmail` early-returnuje przy wejściu w ten status. Webhook / carrier-driven transitions BYPASSują guard — provider-driven update musi przejść bez względu na config (rola engine fundamentalna). Nowy parametr `updateOrderStatus(orderId, status, { source: "admin" })` rozróżnia kontekst; `source: "webhook" | "system"` (default) zawsze pomija check. Pure helpery: `assertManualStatusAllowed(status, orderStatuses)` (rzuca) i `shouldSendStatusEmail(status, orderStatuses)` (zwraca bool).',
7
+ '**`formatSubject` + `builtInSubjectPlaceholders`** (`$lib/shop/server/emailSubject.js`) — generyczna interpolacja `{key}` w subject mailach. Built-in: `{order_number}`, `{customer_name}`, `{customer_email}`, `{total_gross}` (formatted PLN currency), `{currency}`. Nowy `ShopConfig.emailSubjectPlaceholders?: Record<string, SubjectPlaceholderResolver>` — projektowe rozszerzenia (np. `{event_date}` dla szkoleń), mergowane na built-in (custom wins). `sendOrderStatusEmail` przepuszcza fallback subject (`STATUS_SUBJECTS[status][lang] · {order_number}`) przez `formatSubject` zanim wyśle. Nieznane placeholdery są lenient — zostają w stringu.',
8
+ '**`ShopConfig.resolveStatusSubject?(status, ctx) => string | null | undefined`** — async hook pozwalający consumerowi pobrać template subject z dowolnego źródła (typically CMS singleton). Wywoływany w `sendOrderStatusEmail` przed `formatSubject`, więc tokeny działają tak samo jak w defaultach. Pusta wartość / błąd → fallback do `STATUS_SUBJECTS`. Paczka pozostaje agnostyczna o CMS schemie consumera.',
9
+ '**Handlebars helper `{{{structured doc}}}`** (`emailTemplateRegistry.ts`) — renderuje `StructuredContentDoc` (output TipTap fielda `content`) jako inline HTML dla maili. Używa `structuredToHtml()` z core; zwraca `SafeString`. Po render — automatyczne **token replacement** przez `formatSubject` (built-in + `emailSubjectPlaceholders`) jeśli `order` jest w Handlebars context. Pozwala wpisać `{order_number}` w polu TipTap w CMSie i mieć je podmienione w wysłanym mailu. Pusty/undefined doc → "".',
10
+ '**Email context rozszerzony o billing fields** (`email.ts`): `order.customerCompanyName`, `customerNip`, `customerPhone`, `billingAddress` (`{street, postalCode, city, country?}`) + flagi `hasCompany` / `hasBillingAddress` dla wygodnych `{{#if}}` w templates. Pozwala renderować WooCommerce-style billing block bez custom JOIN.'
11
+ ],
12
+ fixes: [
13
+ '**`extractCmsSlugs` skanuje partials** — wcześniej AST tylko głównego templatu był parsowany, więc `cms.<slug>.*` referencje w partials (np. `cms.globalSettings.legalEntity` w `_partials/footer.pl.html`) były ignorowane → singleton nie był prefetchowany → context był pusty → fallbacki renderowane mimo wypełnionego CMSa. Teraz dodatkowo iteruje przez wszystkie zarejestrowane `Handlebars.partials` i merguje slugi.',
14
+ '**`resolveCmsContext` poprawnie spłaszcza entry** — wcześniej używał `entry?.data ?? {}`, ale `resolveEntry` zwraca obiekt spreadowany (`{ _id, _slug, _type, _publishedAt, _url, ...fields }`) bez pola `.data`. Efekt: każdy `cms.<slug>` w mailu był pustym obiektem. Teraz strip-uje meta keys (`_*`) i zwraca user fields. Mock w testach był niezgodny z prawdziwym API, dlatego bug przetrwał — naprawiony, dodany regression test dla nested path.'
15
+ ],
16
+ breakingChanges: []
17
+ };
@@ -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
+ };
@@ -68,6 +68,8 @@ import { update as update0341 } from './0.34.1/index.js';
68
68
  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
+ import { update as update0362 } from './0.36.2/index.js';
72
+ import { update as update0363 } from './0.36.3/index.js';
71
73
  export const updates = [
72
74
  update0065,
73
75
  update0066,
@@ -138,7 +140,9 @@ export const updates = [
138
140
  update0341,
139
141
  update0350,
140
142
  update0360,
141
- update0361
143
+ update0361,
144
+ update0362,
145
+ update0363
142
146
  ];
143
147
  export const getUpdatesFrom = (fromVersion) => {
144
148
  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.1",
3
+ "version": "0.36.3",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",