@entifix/next-shell 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAIrC,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,MAAM;IACzD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,EACrD,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,GACV,EAAE,mBAAmB,CAAC,OAAO,CAAC,+BA8H9B"}
1
+ {"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAKrC,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,MAAM;IACzD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,EACrD,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,GACV,EAAE,mBAAmB,CAAC,OAAO,CAAC,+BAiH9B"}
@@ -7,6 +7,7 @@ import { useEntityForm } from '@entifix/react-integration';
7
7
  import { useEffect, useMemo } from 'react';
8
8
  import { useEntityAffordances } from './use-entity-affordances.js';
9
9
  import { useEntityLinkSources } from './use-entity-link-sources.js';
10
+ import { validationMessagesFor } from './validation-messages.js';
10
11
  /**
11
12
  * The generated create/update form. A thin wrapper over the agnostic
12
13
  * {@link EntityForm}: every field, label and validation rule comes from the
@@ -24,21 +25,8 @@ import { useEntityLinkSources } from './use-entity-link-sources.js';
24
25
  */ export function EntityCrudForm({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading = false, isSaving = false, isDeleting = false, error, onSave, onDelete, backHref, renderLink, draft, metadataSource, onUseCase }) {
25
26
  const et = useT('entity');
26
27
  const ct = useT('controls');
27
- // `controls` copy, resolved here and handed down. The integration package
28
- // owns the validation *rules* and this shell owns the sentences, because a
29
- // sideways import between the two T3 packages is an edge the tier register
30
- // does not declare.
31
- const validationMessages = useMemo(()=>{
32
- const say = (failure)=>(field)=>ct(`validation.${failure}`, {
33
- field
34
- });
35
- return {
36
- required: say('required'),
37
- number: say('number'),
38
- date: say('date'),
39
- option: say('option')
40
- };
41
- }, [
28
+ // `controls` copy, resolved here and handed down; see validationMessagesFor.
29
+ const validationMessages = useMemo(()=>validationMessagesFor(ct), [
42
30
  ct
43
31
  ]);
44
32
  // A runtime-key translator, so a message names the field by its catalog word
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport {\n EntityField,\n EntityForm,\n type RenderLink,\n useT,\n useTranslateKey,\n} from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /** Renders the Back link; see `useCrudRenderLink`. */\n readonly renderLink?: RenderLink;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n renderLink,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down. The integration package\n // owns the validation *rules* and this shell owns the sentences, because a\n // sideways import between the two T3 packages is an edge the tier register\n // does not declare.\n const validationMessages = useMemo(() => {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n };\n }, [ct]);\n\n // A runtime-key translator, so a message names the field by its catalog word\n // (`labelKey`) and a schema's keyed message resolves at all. Without it both\n // fell back to untranslated text inside an otherwise Spanish sentence.\n const translateKey = useTranslateKey();\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n translateKey,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n renderLink={renderLink}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useTranslateKey","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","renderLink","draft","metadataSource","onUseCase","et","ct","validationMessages","say","failure","field","required","number","date","option","translateKey","form","onSubmit","values","existing","linkSources","selection","hydrateLink","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport {\n EntityField,\n EntityForm,\n type RenderLink,\n useT,\n useTranslateKey,\n} from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\nimport { validationMessagesFor } from './validation-messages.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /** Renders the Back link; see `useCrudRenderLink`. */\n readonly renderLink?: RenderLink;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n renderLink,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down; see validationMessagesFor.\n const validationMessages = useMemo(() => validationMessagesFor(ct), [ct]);\n\n // A runtime-key translator, so a message names the field by its catalog word\n // (`labelKey`) and a schema's keyed message resolves at all. Without it both\n // fell back to untranslated text inside an otherwise Spanish sentence.\n const translateKey = useTranslateKey();\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n translateKey,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n renderLink={renderLink}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useTranslateKey","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","validationMessagesFor","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","renderLink","draft","metadataSource","onUseCase","et","ct","validationMessages","translateKey","form","onSubmit","values","existing","linkSources","selection","hydrateLink","field","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { EntityDraftMessages } from '@entifix/react-integration';
2
+ /** A translate function bound to the `controls` namespace. */
3
+ export type ControlsTranslate = (key: string, params?: Record<string, unknown>) => string;
4
+ /**
5
+ * The form's validation sentences, in the `controls` catalog's words.
6
+ *
7
+ * The integration package owns the validation *rules* and this shell owns the
8
+ * sentences, because a sideways import between the two T3 packages is an edge
9
+ * the tier register does not declare. `count` doubles as i18next's plural
10
+ * selector, so `minItems` reads "1 item" and "2 items".
11
+ */
12
+ export declare function validationMessagesFor(ct: ControlsTranslate): EntityDraftMessages;
13
+ //# sourceMappingURL=validation-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-messages.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/validation-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,iBAAiB,GACpB,mBAAmB,CAerB"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The form's validation sentences, in the `controls` catalog's words.
3
+ *
4
+ * The integration package owns the validation *rules* and this shell owns the
5
+ * sentences, because a sideways import between the two T3 packages is an edge
6
+ * the tier register does not declare. `count` doubles as i18next's plural
7
+ * selector, so `minItems` reads "1 item" and "2 items".
8
+ */ export function validationMessagesFor(ct) {
9
+ const say = (failure)=>(field)=>ct(`validation.${failure}`, {
10
+ field
11
+ });
12
+ return {
13
+ required: say('required'),
14
+ number: say('number'),
15
+ date: say('date'),
16
+ option: say('option'),
17
+ minItems: (field, count)=>ct('validation.minItems', {
18
+ field,
19
+ count
20
+ }),
21
+ maxItems: (field, count)=>ct('validation.maxItems', {
22
+ field,
23
+ count
24
+ }),
25
+ notBefore: (field, other)=>ct('validation.notBefore', {
26
+ field,
27
+ other
28
+ }),
29
+ locale: (field, locale)=>ct('validation.locale', {
30
+ field,
31
+ locale: locale.toUpperCase()
32
+ })
33
+ };
34
+ }
35
+
36
+ //# sourceMappingURL=validation-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/crud/validation-messages.ts"],"sourcesContent":["import type { EntityDraftMessages } from '@entifix/react-integration';\n\n/** A translate function bound to the `controls` namespace. */\nexport type ControlsTranslate = (\n key: string,\n params?: Record<string, unknown>,\n) => string;\n\n/**\n * The form's validation sentences, in the `controls` catalog's words.\n *\n * The integration package owns the validation *rules* and this shell owns the\n * sentences, because a sideways import between the two T3 packages is an edge\n * the tier register does not declare. `count` doubles as i18next's plural\n * selector, so `minItems` reads \"1 item\" and \"2 items\".\n */\nexport function validationMessagesFor(\n ct: ControlsTranslate,\n): EntityDraftMessages {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n minItems: (field, count) => ct('validation.minItems', { field, count }),\n maxItems: (field, count) => ct('validation.maxItems', { field, count }),\n notBefore: (field, other) => ct('validation.notBefore', { field, other }),\n locale: (field, locale) =>\n ct('validation.locale', { field, locale: locale.toUpperCase() }),\n };\n}\n"],"names":["validationMessagesFor","ct","say","failure","field","required","number","date","option","minItems","count","maxItems","notBefore","other","locale","toUpperCase"],"mappings":"AAQA;;;;;;;CAOC,GACD,OAAO,SAASA,sBACdC,EAAqB;IAErB,MAAMC,MACJ,CAACC,UAAuD,CAACC,QACvDH,GAAG,CAAC,WAAW,EAAEE,SAAS,EAAE;gBAAEC;YAAM;IACxC,OAAO;QACLC,UAAUH,IAAI;QACdI,QAAQJ,IAAI;QACZK,MAAML,IAAI;QACVM,QAAQN,IAAI;QACZO,UAAU,CAACL,OAAOM,QAAUT,GAAG,uBAAuB;gBAAEG;gBAAOM;YAAM;QACrEC,UAAU,CAACP,OAAOM,QAAUT,GAAG,uBAAuB;gBAAEG;gBAAOM;YAAM;QACrEE,WAAW,CAACR,OAAOS,QAAUZ,GAAG,wBAAwB;gBAAEG;gBAAOS;YAAM;QACvEC,QAAQ,CAACV,OAAOU,SACdb,GAAG,qBAAqB;gBAAEG;gBAAOU,QAAQA,OAAOC,WAAW;YAAG;IAClE;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"record-search-source.d.ts","sourceRoot":"","sources":["../../../src/lib/search/record-search-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,CACb,IAAI,EAAE,OAAO,KACV;QAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjE;AAoBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAAE,uGAS9D,wBAAwB,CAAC,OAAO,CAAC,KAAG,kBAiItC,CAAC"}
1
+ {"version":3,"file":"record-search-source.d.ts","sourceRoot":"","sources":["../../../src/lib/search/record-search-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,CACb,IAAI,EAAE,OAAO,KACV;QAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjE;AAoBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAAE,uGAS9D,wBAAwB,CAAC,OAAO,CAAC,KAAG,kBA2ItC,CAAC"}
@@ -45,6 +45,11 @@ const rejection = (entityName, member, because, fix)=>new EntifixLogicError(`Can
45
45
  // the service rejects the sort and the group degrades whole. Unsorted was the
46
46
  // alternative and is worse — the services apply no default order, so results
47
47
  // would arrive in storage order and shuffle as records are rewritten.
48
+ // A localized label sorts one locale at a time, and a search that fans out
49
+ // across services holds none: the bare member would answer `400`.
50
+ if (label.type === 'localized') {
51
+ throw rejection(entityName, labelProperty, 'the label member is localized, and the search has no locale to sort it in.', 'Label records by a string member.');
52
+ }
48
53
  if (!label.sortable) {
49
54
  throw rejection(entityName, labelProperty, 'the label member is not sortable, so the service would reject the sort.', 'Declare it `sortable` on the entity, or label records by another member.');
50
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/search/record-search-source.ts"],"sourcesContent":["import {\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityLoadRequest,\n envelopeEntityName,\n readEntityPageEnvelope,\n serializeLoadRequestParams,\n} from '@entifix/core';\nimport { Effect, Exit } from 'effect';\n\nimport type { RecordSearchOption } from './record-search.types.js';\n\n/**\n * What a host declares in order to make one entity searchable from the palette\n * (ADR 0040).\n *\n * The search and label members are **declared**, not derived, and that is not an\n * oversight in the metadata: `linkSearchProperty`/`linkLabelProperty` are\n * properties of a *referring* accessor — they say how the owner of a relation\n * looks up its target — and an entity has no member that says \"this is what I am\n * called\". So the host that mounts the screens says it, next to the route those\n * screens live at.\n */\nexport interface RecordSearchSourceConfig<TEntity extends Entity> {\n /** Stable id, unique per host. Defaults to the entity's wire name. */\n readonly key?: string;\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** Where the owning service listens, **without** the `/api` suffix. */\n readonly baseUrl: string;\n /** The `filterable` string member a term is matched against with `like`. */\n readonly searchProperty: string;\n /** The `sortable` member read as a record's label. */\n readonly labelProperty: string;\n /** An optional secondary line — a code, a role. */\n readonly sublabelProperty?: string;\n /** The group heading, as the entity's own `pluralKey`. */\n readonly labelKey: string;\n /** Where selecting a record lands. Locale-free; the caller prefixes it. */\n readonly href: (id: string) => string;\n}\n\n/** A declared source, reduced to what the fan-out actually calls. */\nexport interface RecordSearchSource {\n readonly key: string;\n readonly entity: string;\n readonly labelKey: string;\n /** The upstream URL for one term. */\n readonly url: (term: string, limit: number) => string;\n /** `undefined` when the body is not a readable `entityPage` envelope. */\n readonly read: (\n body: unknown,\n ) => { items: RecordSearchOption[]; total: number } | undefined;\n}\n\nconst descriptorOf = (\n columns: EntityFieldDescriptor[],\n name: string,\n): EntityFieldDescriptor | undefined =>\n columns.find(column => column.name === name);\n\nconst rejection = (\n entityName: string,\n member: string,\n because: string,\n fix: string,\n): EntifixLogicError =>\n new EntifixLogicError(\n `Cannot search ${entityName} by \"${member}\": ${because} ${fix}`,\n undefined,\n { entity: entityName, member },\n );\n\n/**\n * Declare a record search source, validating it against the entity's own\n * metadata **now** rather than on the first keystroke.\n *\n * Every check here guards a failure that is otherwise silent at both ends: the\n * service answers `400`, and a caller renders that as a group it could not\n * reach — which reads as \"there are no products\". `assertSearchable` took the\n * same posture for the relation picker, for the same reason.\n *\n * ⚠️ This throws at **module load**, so a bad declaration fails the whole app at\n * boot rather than just search. That is deliberate — a source silently missing\n * from a palette is indistinguishable from a permission the caller lacks — but\n * the blast radius is wider than the picker's, which fails only its own render.\n */\nexport const defineRecordSearchSource = <TEntity extends Entity>({\n key,\n entityConstructor,\n baseUrl,\n searchProperty,\n labelProperty,\n sublabelProperty,\n labelKey,\n href,\n}: RecordSearchSourceConfig<TEntity>): RecordSearchSource => {\n const entity = envelopeEntityName(entityConstructor);\n const entityName = entityConstructor.name;\n const columns = describeEntityColumns(entityConstructor);\n\n const search = descriptorOf(columns, searchProperty);\n if (search === undefined) {\n throw rejection(\n entityName,\n searchProperty,\n 'the entity declares no such member.',\n 'Name a member the entity actually has.',\n );\n }\n // `filterable` is simultaneously the server-side RSQL allowlist.\n if (!search.filterable) {\n throw rejection(\n entityName,\n searchProperty,\n 'the member is not filterable, so the service would reject the query.',\n 'Declare it `filterable` on the entity, or search another member.',\n );\n }\n // A `like` against anything else reaches `coerceValue`, which rejects a\n // partial term — so an enum member reads as declarable and is permanently\n // broken: every keystroke short of a whole value answers `400`.\n if (search.type !== 'string') {\n throw rejection(\n entityName,\n searchProperty,\n `the member is a ${search.type}, and a partial term is not a valid value for it.`,\n 'Search a string member instead.',\n );\n }\n\n const label = descriptorOf(columns, labelProperty);\n if (label === undefined) {\n throw rejection(\n entityName,\n labelProperty,\n 'the entity declares no such label member.',\n 'Name a member the entity actually has.',\n );\n }\n // The group is sorted by the label, so the flag is load-bearing: without it\n // the service rejects the sort and the group degrades whole. Unsorted was the\n // alternative and is worse — the services apply no default order, so results\n // would arrive in storage order and shuffle as records are rewritten.\n if (!label.sortable) {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is not sortable, so the service would reject the sort.',\n 'Declare it `sortable` on the entity, or label records by another member.',\n );\n }\n\n let sublabel: EntityFieldDescriptor | undefined;\n if (sublabelProperty !== undefined) {\n sublabel = descriptorOf(columns, sublabelProperty);\n if (sublabel === undefined) {\n throw rejection(\n entityName,\n sublabelProperty,\n 'the entity declares no such sublabel member.',\n 'Name a member the entity actually has, or drop `sublabelProperty`.',\n );\n }\n }\n\n const read = (\n body: unknown,\n ): { items: RecordSearchOption[]; total: number } | undefined => {\n const page = Effect.runSyncExit(\n readEntityPageEnvelope(entityConstructor, body),\n );\n if (Exit.isFailure(page)) return undefined;\n\n const items = page.value.items\n .filter(record => (record as Entity).id != null)\n .map(record => {\n const row = record as unknown as Record<string, unknown>;\n const raw = row[labelProperty];\n const id = String((record as Entity).id);\n return {\n id,\n // A blank label is as useless as a missing one, so both fall back to\n // the id — an unnamed record must stay distinguishable from the one\n // above it. Same rule the relation picker's `labelOf` applies.\n label: raw == null || raw === '' ? id : String(raw),\n ...(sublabel !== undefined && row[sublabel.name] != null\n ? { sublabel: String(row[sublabel.name]) }\n : {}),\n entity,\n href: href(id),\n } satisfies RecordSearchOption;\n });\n\n return { items, total: page.value.total };\n };\n\n return {\n key: key ?? entity,\n entity,\n labelKey,\n /**\n * Built through `serializeLoadRequestParams`, never by hand. That is\n * correctness rather than tidiness: `encodeRsqlValue` quotes a term carrying\n * any of `;,()'\"=!<>\\` or whitespace, so `Acme, Inc.` stays one value\n * instead of splitting the expression into two comparisons.\n */\n url: (term, limit) => {\n const request = {\n filtering: [\n {\n property: search.key,\n operator: 'like' as const,\n value: term,\n },\n ],\n sorting: [{ 0: { property: label.key, type: 'asc' as const } }],\n page: 1,\n pageSize: limit,\n } as unknown as EntityLoadRequest;\n\n return `${baseUrl}/api/${entity}?${serializeLoadRequestParams(request).toString()}`;\n },\n read,\n };\n};\n"],"names":["describeEntityColumns","EntifixLogicError","envelopeEntityName","readEntityPageEnvelope","serializeLoadRequestParams","Effect","Exit","descriptorOf","columns","name","find","column","rejection","entityName","member","because","fix","undefined","entity","defineRecordSearchSource","key","entityConstructor","baseUrl","searchProperty","labelProperty","sublabelProperty","labelKey","href","search","filterable","type","label","sortable","sublabel","read","body","page","runSyncExit","isFailure","items","value","filter","record","id","map","row","raw","String","total","url","term","limit","request","filtering","property","operator","sorting","pageSize","toString"],"mappings":";AAAA,SACEA,qBAAqB,EACrBC,iBAAiB,EAKjBC,kBAAkB,EAClBC,sBAAsB,EACtBC,0BAA0B,QACrB,gBAAgB;AACvB,SAASC,MAAM,EAAEC,IAAI,QAAQ,SAAS;AA8CtC,MAAMC,eAAe,CACnBC,SACAC,OAEAD,QAAQE,IAAI,CAACC,CAAAA,SAAUA,OAAOF,IAAI,KAAKA;AAEzC,MAAMG,YAAY,CAChBC,YACAC,QACAC,SACAC,MAEA,IAAIf,kBACF,CAAC,cAAc,EAAEY,WAAW,KAAK,EAAEC,OAAO,GAAG,EAAEC,QAAQ,CAAC,EAAEC,KAAK,EAC/DC,WACA;QAAEC,QAAQL;QAAYC;IAAO;AAGjC;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMK,2BAA2B,CAAyB,EAC/DC,GAAG,EACHC,iBAAiB,EACjBC,OAAO,EACPC,cAAc,EACdC,aAAa,EACbC,gBAAgB,EAChBC,QAAQ,EACRC,IAAI,EAC8B;IAClC,MAAMT,SAAShB,mBAAmBmB;IAClC,MAAMR,aAAaQ,kBAAkBZ,IAAI;IACzC,MAAMD,UAAUR,sBAAsBqB;IAEtC,MAAMO,SAASrB,aAAaC,SAASe;IACrC,IAAIK,WAAWX,WAAW;QACxB,MAAML,UACJC,YACAU,gBACA,uCACA;IAEJ;IACA,iEAAiE;IACjE,IAAI,CAACK,OAAOC,UAAU,EAAE;QACtB,MAAMjB,UACJC,YACAU,gBACA,wEACA;IAEJ;IACA,wEAAwE;IACxE,0EAA0E;IAC1E,gEAAgE;IAChE,IAAIK,OAAOE,IAAI,KAAK,UAAU;QAC5B,MAAMlB,UACJC,YACAU,gBACA,CAAC,gBAAgB,EAAEK,OAAOE,IAAI,CAAC,iDAAiD,CAAC,EACjF;IAEJ;IAEA,MAAMC,QAAQxB,aAAaC,SAASgB;IACpC,IAAIO,UAAUd,WAAW;QACvB,MAAML,UACJC,YACAW,eACA,6CACA;IAEJ;IACA,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,IAAI,CAACO,MAAMC,QAAQ,EAAE;QACnB,MAAMpB,UACJC,YACAW,eACA,2EACA;IAEJ;IAEA,IAAIS;IACJ,IAAIR,qBAAqBR,WAAW;QAClCgB,WAAW1B,aAAaC,SAASiB;QACjC,IAAIQ,aAAahB,WAAW;YAC1B,MAAML,UACJC,YACAY,kBACA,gDACA;QAEJ;IACF;IAEA,MAAMS,OAAO,CACXC;QAEA,MAAMC,OAAO/B,OAAOgC,WAAW,CAC7BlC,uBAAuBkB,mBAAmBc;QAE5C,IAAI7B,KAAKgC,SAAS,CAACF,OAAO,OAAOnB;QAEjC,MAAMsB,QAAQH,KAAKI,KAAK,CAACD,KAAK,CAC3BE,MAAM,CAACC,CAAAA,SAAU,AAACA,OAAkBC,EAAE,IAAI,MAC1CC,GAAG,CAACF,CAAAA;YACH,MAAMG,MAAMH;YACZ,MAAMI,MAAMD,GAAG,CAACrB,cAAc;YAC9B,MAAMmB,KAAKI,OAAO,AAACL,OAAkBC,EAAE;YACvC,OAAO;gBACLA;gBACA,qEAAqE;gBACrE,oEAAoE;gBACpE,+DAA+D;gBAC/DZ,OAAOe,OAAO,QAAQA,QAAQ,KAAKH,KAAKI,OAAOD;eAC3Cb,aAAahB,aAAa4B,GAAG,CAACZ,SAASxB,IAAI,CAAC,IAAI,OAChD;gBAAEwB,UAAUc,OAAOF,GAAG,CAACZ,SAASxB,IAAI,CAAC;YAAE,IACvC,CAAC;gBACLS;gBACAS,MAAMA,KAAKgB;;QAEf;QAEF,OAAO;YAAEJ;YAAOS,OAAOZ,KAAKI,KAAK,CAACQ,KAAK;QAAC;IAC1C;IAEA,OAAO;QACL5B,GAAG,EAAEA,cAAAA,MAAOF;QACZA;QACAQ;QACA;;;;;KAKC,GACDuB,KAAK,CAACC,MAAMC;YACV,MAAMC,UAAU;gBACdC,WAAW;oBACT;wBACEC,UAAU1B,OAAOR,GAAG;wBACpBmC,UAAU;wBACVf,OAAOU;oBACT;iBACD;gBACDM,SAAS;oBAAC;wBAAE,GAAG;4BAAEF,UAAUvB,MAAMX,GAAG;4BAAEU,MAAM;wBAAe;oBAAE;iBAAE;gBAC/DM,MAAM;gBACNqB,UAAUN;YACZ;YAEA,OAAO,GAAG7B,QAAQ,KAAK,EAAEJ,OAAO,CAAC,EAAEd,2BAA2BgD,SAASM,QAAQ,IAAI;QACrF;QACAxB;IACF;AACF,EAAE"}
1
+ {"version":3,"sources":["../../../src/lib/search/record-search-source.ts"],"sourcesContent":["import {\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityLoadRequest,\n envelopeEntityName,\n readEntityPageEnvelope,\n serializeLoadRequestParams,\n} from '@entifix/core';\nimport { Effect, Exit } from 'effect';\n\nimport type { RecordSearchOption } from './record-search.types.js';\n\n/**\n * What a host declares in order to make one entity searchable from the palette\n * (ADR 0040).\n *\n * The search and label members are **declared**, not derived, and that is not an\n * oversight in the metadata: `linkSearchProperty`/`linkLabelProperty` are\n * properties of a *referring* accessor — they say how the owner of a relation\n * looks up its target — and an entity has no member that says \"this is what I am\n * called\". So the host that mounts the screens says it, next to the route those\n * screens live at.\n */\nexport interface RecordSearchSourceConfig<TEntity extends Entity> {\n /** Stable id, unique per host. Defaults to the entity's wire name. */\n readonly key?: string;\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** Where the owning service listens, **without** the `/api` suffix. */\n readonly baseUrl: string;\n /** The `filterable` string member a term is matched against with `like`. */\n readonly searchProperty: string;\n /** The `sortable` member read as a record's label. */\n readonly labelProperty: string;\n /** An optional secondary line — a code, a role. */\n readonly sublabelProperty?: string;\n /** The group heading, as the entity's own `pluralKey`. */\n readonly labelKey: string;\n /** Where selecting a record lands. Locale-free; the caller prefixes it. */\n readonly href: (id: string) => string;\n}\n\n/** A declared source, reduced to what the fan-out actually calls. */\nexport interface RecordSearchSource {\n readonly key: string;\n readonly entity: string;\n readonly labelKey: string;\n /** The upstream URL for one term. */\n readonly url: (term: string, limit: number) => string;\n /** `undefined` when the body is not a readable `entityPage` envelope. */\n readonly read: (\n body: unknown,\n ) => { items: RecordSearchOption[]; total: number } | undefined;\n}\n\nconst descriptorOf = (\n columns: EntityFieldDescriptor[],\n name: string,\n): EntityFieldDescriptor | undefined =>\n columns.find(column => column.name === name);\n\nconst rejection = (\n entityName: string,\n member: string,\n because: string,\n fix: string,\n): EntifixLogicError =>\n new EntifixLogicError(\n `Cannot search ${entityName} by \"${member}\": ${because} ${fix}`,\n undefined,\n { entity: entityName, member },\n );\n\n/**\n * Declare a record search source, validating it against the entity's own\n * metadata **now** rather than on the first keystroke.\n *\n * Every check here guards a failure that is otherwise silent at both ends: the\n * service answers `400`, and a caller renders that as a group it could not\n * reach — which reads as \"there are no products\". `assertSearchable` took the\n * same posture for the relation picker, for the same reason.\n *\n * ⚠️ This throws at **module load**, so a bad declaration fails the whole app at\n * boot rather than just search. That is deliberate — a source silently missing\n * from a palette is indistinguishable from a permission the caller lacks — but\n * the blast radius is wider than the picker's, which fails only its own render.\n */\nexport const defineRecordSearchSource = <TEntity extends Entity>({\n key,\n entityConstructor,\n baseUrl,\n searchProperty,\n labelProperty,\n sublabelProperty,\n labelKey,\n href,\n}: RecordSearchSourceConfig<TEntity>): RecordSearchSource => {\n const entity = envelopeEntityName(entityConstructor);\n const entityName = entityConstructor.name;\n const columns = describeEntityColumns(entityConstructor);\n\n const search = descriptorOf(columns, searchProperty);\n if (search === undefined) {\n throw rejection(\n entityName,\n searchProperty,\n 'the entity declares no such member.',\n 'Name a member the entity actually has.',\n );\n }\n // `filterable` is simultaneously the server-side RSQL allowlist.\n if (!search.filterable) {\n throw rejection(\n entityName,\n searchProperty,\n 'the member is not filterable, so the service would reject the query.',\n 'Declare it `filterable` on the entity, or search another member.',\n );\n }\n // A `like` against anything else reaches `coerceValue`, which rejects a\n // partial term — so an enum member reads as declarable and is permanently\n // broken: every keystroke short of a whole value answers `400`.\n if (search.type !== 'string') {\n throw rejection(\n entityName,\n searchProperty,\n `the member is a ${search.type}, and a partial term is not a valid value for it.`,\n 'Search a string member instead.',\n );\n }\n\n const label = descriptorOf(columns, labelProperty);\n if (label === undefined) {\n throw rejection(\n entityName,\n labelProperty,\n 'the entity declares no such label member.',\n 'Name a member the entity actually has.',\n );\n }\n // The group is sorted by the label, so the flag is load-bearing: without it\n // the service rejects the sort and the group degrades whole. Unsorted was the\n // alternative and is worse — the services apply no default order, so results\n // would arrive in storage order and shuffle as records are rewritten.\n // A localized label sorts one locale at a time, and a search that fans out\n // across services holds none: the bare member would answer `400`.\n if (label.type === 'localized') {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is localized, and the search has no locale to sort it in.',\n 'Label records by a string member.',\n );\n }\n if (!label.sortable) {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is not sortable, so the service would reject the sort.',\n 'Declare it `sortable` on the entity, or label records by another member.',\n );\n }\n\n let sublabel: EntityFieldDescriptor | undefined;\n if (sublabelProperty !== undefined) {\n sublabel = descriptorOf(columns, sublabelProperty);\n if (sublabel === undefined) {\n throw rejection(\n entityName,\n sublabelProperty,\n 'the entity declares no such sublabel member.',\n 'Name a member the entity actually has, or drop `sublabelProperty`.',\n );\n }\n }\n\n const read = (\n body: unknown,\n ): { items: RecordSearchOption[]; total: number } | undefined => {\n const page = Effect.runSyncExit(\n readEntityPageEnvelope(entityConstructor, body),\n );\n if (Exit.isFailure(page)) return undefined;\n\n const items = page.value.items\n .filter(record => (record as Entity).id != null)\n .map(record => {\n const row = record as unknown as Record<string, unknown>;\n const raw = row[labelProperty];\n const id = String((record as Entity).id);\n return {\n id,\n // A blank label is as useless as a missing one, so both fall back to\n // the id — an unnamed record must stay distinguishable from the one\n // above it. Same rule the relation picker's `labelOf` applies.\n label: raw == null || raw === '' ? id : String(raw),\n ...(sublabel !== undefined && row[sublabel.name] != null\n ? { sublabel: String(row[sublabel.name]) }\n : {}),\n entity,\n href: href(id),\n } satisfies RecordSearchOption;\n });\n\n return { items, total: page.value.total };\n };\n\n return {\n key: key ?? entity,\n entity,\n labelKey,\n /**\n * Built through `serializeLoadRequestParams`, never by hand. That is\n * correctness rather than tidiness: `encodeRsqlValue` quotes a term carrying\n * any of `;,()'\"=!<>\\` or whitespace, so `Acme, Inc.` stays one value\n * instead of splitting the expression into two comparisons.\n */\n url: (term, limit) => {\n const request = {\n filtering: [\n {\n property: search.key,\n operator: 'like' as const,\n value: term,\n },\n ],\n sorting: [{ 0: { property: label.key, type: 'asc' as const } }],\n page: 1,\n pageSize: limit,\n } as unknown as EntityLoadRequest;\n\n return `${baseUrl}/api/${entity}?${serializeLoadRequestParams(request).toString()}`;\n },\n read,\n };\n};\n"],"names":["describeEntityColumns","EntifixLogicError","envelopeEntityName","readEntityPageEnvelope","serializeLoadRequestParams","Effect","Exit","descriptorOf","columns","name","find","column","rejection","entityName","member","because","fix","undefined","entity","defineRecordSearchSource","key","entityConstructor","baseUrl","searchProperty","labelProperty","sublabelProperty","labelKey","href","search","filterable","type","label","sortable","sublabel","read","body","page","runSyncExit","isFailure","items","value","filter","record","id","map","row","raw","String","total","url","term","limit","request","filtering","property","operator","sorting","pageSize","toString"],"mappings":";AAAA,SACEA,qBAAqB,EACrBC,iBAAiB,EAKjBC,kBAAkB,EAClBC,sBAAsB,EACtBC,0BAA0B,QACrB,gBAAgB;AACvB,SAASC,MAAM,EAAEC,IAAI,QAAQ,SAAS;AA8CtC,MAAMC,eAAe,CACnBC,SACAC,OAEAD,QAAQE,IAAI,CAACC,CAAAA,SAAUA,OAAOF,IAAI,KAAKA;AAEzC,MAAMG,YAAY,CAChBC,YACAC,QACAC,SACAC,MAEA,IAAIf,kBACF,CAAC,cAAc,EAAEY,WAAW,KAAK,EAAEC,OAAO,GAAG,EAAEC,QAAQ,CAAC,EAAEC,KAAK,EAC/DC,WACA;QAAEC,QAAQL;QAAYC;IAAO;AAGjC;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMK,2BAA2B,CAAyB,EAC/DC,GAAG,EACHC,iBAAiB,EACjBC,OAAO,EACPC,cAAc,EACdC,aAAa,EACbC,gBAAgB,EAChBC,QAAQ,EACRC,IAAI,EAC8B;IAClC,MAAMT,SAAShB,mBAAmBmB;IAClC,MAAMR,aAAaQ,kBAAkBZ,IAAI;IACzC,MAAMD,UAAUR,sBAAsBqB;IAEtC,MAAMO,SAASrB,aAAaC,SAASe;IACrC,IAAIK,WAAWX,WAAW;QACxB,MAAML,UACJC,YACAU,gBACA,uCACA;IAEJ;IACA,iEAAiE;IACjE,IAAI,CAACK,OAAOC,UAAU,EAAE;QACtB,MAAMjB,UACJC,YACAU,gBACA,wEACA;IAEJ;IACA,wEAAwE;IACxE,0EAA0E;IAC1E,gEAAgE;IAChE,IAAIK,OAAOE,IAAI,KAAK,UAAU;QAC5B,MAAMlB,UACJC,YACAU,gBACA,CAAC,gBAAgB,EAAEK,OAAOE,IAAI,CAAC,iDAAiD,CAAC,EACjF;IAEJ;IAEA,MAAMC,QAAQxB,aAAaC,SAASgB;IACpC,IAAIO,UAAUd,WAAW;QACvB,MAAML,UACJC,YACAW,eACA,6CACA;IAEJ;IACA,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,2EAA2E;IAC3E,kEAAkE;IAClE,IAAIO,MAAMD,IAAI,KAAK,aAAa;QAC9B,MAAMlB,UACJC,YACAW,eACA,8EACA;IAEJ;IACA,IAAI,CAACO,MAAMC,QAAQ,EAAE;QACnB,MAAMpB,UACJC,YACAW,eACA,2EACA;IAEJ;IAEA,IAAIS;IACJ,IAAIR,qBAAqBR,WAAW;QAClCgB,WAAW1B,aAAaC,SAASiB;QACjC,IAAIQ,aAAahB,WAAW;YAC1B,MAAML,UACJC,YACAY,kBACA,gDACA;QAEJ;IACF;IAEA,MAAMS,OAAO,CACXC;QAEA,MAAMC,OAAO/B,OAAOgC,WAAW,CAC7BlC,uBAAuBkB,mBAAmBc;QAE5C,IAAI7B,KAAKgC,SAAS,CAACF,OAAO,OAAOnB;QAEjC,MAAMsB,QAAQH,KAAKI,KAAK,CAACD,KAAK,CAC3BE,MAAM,CAACC,CAAAA,SAAU,AAACA,OAAkBC,EAAE,IAAI,MAC1CC,GAAG,CAACF,CAAAA;YACH,MAAMG,MAAMH;YACZ,MAAMI,MAAMD,GAAG,CAACrB,cAAc;YAC9B,MAAMmB,KAAKI,OAAO,AAACL,OAAkBC,EAAE;YACvC,OAAO;gBACLA;gBACA,qEAAqE;gBACrE,oEAAoE;gBACpE,+DAA+D;gBAC/DZ,OAAOe,OAAO,QAAQA,QAAQ,KAAKH,KAAKI,OAAOD;eAC3Cb,aAAahB,aAAa4B,GAAG,CAACZ,SAASxB,IAAI,CAAC,IAAI,OAChD;gBAAEwB,UAAUc,OAAOF,GAAG,CAACZ,SAASxB,IAAI,CAAC;YAAE,IACvC,CAAC;gBACLS;gBACAS,MAAMA,KAAKgB;;QAEf;QAEF,OAAO;YAAEJ;YAAOS,OAAOZ,KAAKI,KAAK,CAACQ,KAAK;QAAC;IAC1C;IAEA,OAAO;QACL5B,GAAG,EAAEA,cAAAA,MAAOF;QACZA;QACAQ;QACA;;;;;KAKC,GACDuB,KAAK,CAACC,MAAMC;YACV,MAAMC,UAAU;gBACdC,WAAW;oBACT;wBACEC,UAAU1B,OAAOR,GAAG;wBACpBmC,UAAU;wBACVf,OAAOU;oBACT;iBACD;gBACDM,SAAS;oBAAC;wBAAE,GAAG;4BAAEF,UAAUvB,MAAMX,GAAG;4BAAEU,MAAM;wBAAe;oBAAE;iBAAE;gBAC/DM,MAAM;gBACNqB,UAAUN;YACZ;YAEA,OAAO,GAAG7B,QAAQ,KAAK,EAAEJ,OAAO,CAAC,EAAEd,2BAA2BgD,SAASM,QAAQ,IAAI;QACrF;QACAxB;IACF;AACF,EAAE"}
@@ -15,7 +15,7 @@ import { WORKSPACE_DB } from './tabs-state.js';
15
15
  */ export function persistedPending(store) {
16
16
  const pending = {};
17
17
  for (const _ref of Object.entries(store.pending)){
18
- let [transactionId, _ref1] = _ref, { record: _dropped } = _ref1, rest = _object_without_properties_loose(_ref1, [
18
+ const [transactionId, _ref1] = _ref, { record: _dropped } = _ref1, rest = _object_without_properties_loose(_ref1, [
19
19
  "record"
20
20
  ]);
21
21
  pending[transactionId] = rest;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/workspace/pending-state.ts"],"sourcesContent":["'use client';\n\nimport type { PendingEntry, PendingTransaction } from '@entifix/transactions';\nimport { create } from 'zustand';\nimport { createJSONStorage, persist } from 'zustand/middleware';\n\nimport { makeIndexedDbStateStorage } from './idb-state-storage.js';\nimport { WORKSPACE_DB } from './tabs-state.js';\n\n/**\n * Writes the browser started and the server has not finished, keyed by\n * transaction id.\n *\n * ⚠️ **This is not workspace state, and the distinction is load-bearing.** Tabs\n * and drafts belong to the workspace and are scoped by `WorkspaceShell`; a\n * pending write is not, because a create happens at `/<basePath>/new` on the\n * plain route, outside any `WorkspaceShell` (a generated list still uses plain\n * `href`s — ADR 0042's own residual). A store mounted inside the workspace would\n * be written to by nothing, or — worse — written to *before* `persist.setOptions`\n * had scoped it, which is the unscoped cross-account restore ADR 0032 exists to\n * prevent. So `PendingTransactionsProvider` sits at the authenticated layout\n * instead, above both.\n *\n * It shares the workspace's IndexedDB database because it is the same browser\n * profile and the same scope key, not because it is the same concern.\n */\nexport interface PendingState {\n pending: Record<string, PendingEntry>;\n began(transaction: PendingTransaction): void;\n attach(transactionId: string, record: unknown): boolean;\n settle(transactionId: string): void;\n fail(transactionId: string, reason?: string): void;\n dismiss(transactionId: string): void;\n}\n\n/**\n * ⚠️ **`record` is stripped before persisting.** It is a class instance, and a\n * draft-style JSON round trip does not preserve one — it returns as something\n * else, silently (ADR 0032). So a refresh keeps the *watch* and loses the row,\n * which is the documented behaviour: past a refresh, server truth plus an honest\n * notice beats a record rebuilt from a blob.\n */\nexport function persistedPending(\n store: PendingState,\n): Pick<PendingState, 'pending'> {\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, { record: _dropped, ...rest }] of Object.entries(\n store.pending,\n )) {\n pending[transactionId] = rest;\n }\n return { pending };\n}\n\nconst PENDING_STORE = 'stores';\n\n/**\n * How many pending writes are watched at once, oldest evicted first.\n *\n * The same reasoning as {@link MAX_TABS}: an unbounded set persisted to\n * IndexedDB grows without anything ever pruning it, and reconciliation asks the\n * tracker once per entry on every reconnect — so the cap is also what keeps that\n * fan-out a handful of requests rather than an unbounded one.\n *\n * An evicted entry is dropped silently. The record is still on the server and\n * the list's own query is the fallback; what is lost is only the watching.\n */\nexport const MAX_PENDING = 12;\n\n/**\n * Bump when the *shape* of an entry changes, and every persisted pending write\n * is discarded on the next load.\n *\n * Discarding is right here for a reason the other two stores do not share: an\n * entry is a claim that something is still in flight, and it stops being true on\n * its own. A stale one restored from a previous build watches a transaction that\n * settled hours ago, and the reconcile that would correct it is keyed on a shape\n * this build may no longer understand.\n */\nexport const PENDING_VERSION = 1;\n\nconst emptyPending: Pick<PendingState, 'pending'> = { pending: {} };\n\n/**\n * Discard, never guess — written out rather than left to zustand's default,\n * which discards too but logs an error for a decision that was made on purpose.\n * Same call as `migrateDrafts` and `migrateTabs`.\n */\nexport function migratePending(): Pick<PendingState, 'pending'> {\n return emptyPending;\n}\n\n/** Whether a restored value is an entry this build can act on. */\nfunction isPendingEntry(value: unknown): value is PendingEntry {\n if (value === null || typeof value !== 'object') return false;\n const entry = value as Partial<PendingEntry>;\n return (\n typeof entry.transactionId === 'string' &&\n typeof entry.entity === 'string' &&\n typeof entry.at === 'string' &&\n (entry.state === 'pending' || entry.state === 'failed')\n );\n}\n\n/**\n * Restores only the entries that are actually entries — per entry, so one bad\n * record does not take the rest with it. The `mergeDrafts` rule, for the same\n * reason: this is what happens when something else was written by a build that\n * predates the shape.\n */\nexport function mergePending(\n persistedState: unknown,\n currentState: PendingState,\n): PendingState {\n const restored = (persistedState as Partial<PendingState> | undefined)\n ?.pending;\n if (restored === null || typeof restored !== 'object') return currentState;\n\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, value] of Object.entries(restored)) {\n if (isPendingEntry(value)) pending[transactionId] = value;\n }\n return { ...currentState, pending };\n}\n\n/** Drops the oldest entries until the set fits, by announcement time. */\nfunction capPending(\n pending: Record<string, PendingEntry>,\n): Record<string, PendingEntry> {\n const entries = Object.entries(pending);\n if (entries.length <= MAX_PENDING) return pending;\n\n return Object.fromEntries(\n entries\n .sort(([, a], [, b]) => a.at.localeCompare(b.at))\n .slice(entries.length - MAX_PENDING),\n );\n}\n\nexport const usePendingState = create<PendingState>()(\n persist(\n (set, get) => ({\n pending: {},\n began: transaction =>\n set(state => ({\n pending: capPending({\n ...state.pending,\n [transaction.transactionId]: { ...transaction, state: 'pending' },\n }),\n })),\n // Reads current state rather than taking it from the caller, so a save\n // handler cannot ask about a store snapshot older than its own `await`.\n attach: (transactionId, record) => {\n const entry = get().pending[transactionId];\n if (entry === undefined) return false;\n set(state => ({\n pending: { ...state.pending, [transactionId]: { ...entry, record } },\n }));\n return true;\n },\n // Settling drops the entry rather than marking it done: the server's copy\n // is the truth now, and the list query that just refetched is showing it.\n settle: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _settled, ...rest } = state.pending;\n return { pending: rest };\n }),\n // A failure is *kept*, marked, until someone reads it. Dropping it here\n // would un-render the optimistic row and say nothing about why.\n fail: (transactionId, reason) =>\n set(state => {\n const entry = state.pending[transactionId];\n if (entry === undefined) return state;\n return {\n pending: {\n ...state.pending,\n [transactionId]: { ...entry, state: 'failed', reason },\n },\n };\n }),\n dismiss: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _dismissed, ...rest } = state.pending;\n return { pending: rest };\n }),\n }),\n {\n // Replaced with a scoped name before rehydrating, exactly as the tab and\n // draft stores are. This literal is only what an unscoped store would\n // write, and nothing rehydrates without passing through that call.\n name: 'pending',\n version: PENDING_VERSION,\n migrate: migratePending,\n merge: mergePending,\n storage: createJSONStorage(() =>\n makeIndexedDbStateStorage(WORKSPACE_DB, PENDING_STORE),\n ),\n partialize: persistedPending,\n skipHydration: true,\n },\n ),\n);\n"],"names":["create","createJSONStorage","persist","makeIndexedDbStateStorage","WORKSPACE_DB","persistedPending","store","pending","Object","entries","transactionId","record","_dropped","rest","PENDING_STORE","MAX_PENDING","PENDING_VERSION","emptyPending","migratePending","isPendingEntry","value","entry","entity","at","state","mergePending","persistedState","currentState","restored","capPending","length","fromEntries","sort","a","b","localeCompare","slice","usePendingState","set","get","began","transaction","attach","undefined","settle","_settled","fail","reason","dismiss","_dismissed","name","version","migrate","merge","storage","partialize","skipHydration"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/workspace/pending-state.ts"],"sourcesContent":["'use client';\n\nimport type { PendingEntry, PendingTransaction } from '@entifix/transactions';\nimport { create } from 'zustand';\nimport { createJSONStorage, persist } from 'zustand/middleware';\n\nimport { makeIndexedDbStateStorage } from './idb-state-storage.js';\nimport { WORKSPACE_DB } from './tabs-state.js';\n\n/**\n * Writes the browser started and the server has not finished, keyed by\n * transaction id.\n *\n * ⚠️ **This is not workspace state, and the distinction is load-bearing.** Tabs\n * and drafts belong to the workspace and are scoped by `WorkspaceShell`; a\n * pending write is not, because a create happens at `/<basePath>/new` on the\n * plain route, outside any `WorkspaceShell` (a generated list still uses plain\n * `href`s — ADR 0042's own residual). A store mounted inside the workspace would\n * be written to by nothing, or — worse — written to *before* `persist.setOptions`\n * had scoped it, which is the unscoped cross-account restore ADR 0032 exists to\n * prevent. So `PendingTransactionsProvider` sits at the authenticated layout\n * instead, above both.\n *\n * It shares the workspace's IndexedDB database because it is the same browser\n * profile and the same scope key, not because it is the same concern.\n */\nexport interface PendingState {\n pending: Record<string, PendingEntry>;\n began(transaction: PendingTransaction): void;\n attach(transactionId: string, record: unknown): boolean;\n settle(transactionId: string): void;\n fail(transactionId: string, reason?: string): void;\n dismiss(transactionId: string): void;\n}\n\n/**\n * ⚠️ **`record` is stripped before persisting.** It is a class instance, and a\n * draft-style JSON round trip does not preserve one — it returns as something\n * else, silently (ADR 0032). So a refresh keeps the *watch* and loses the row,\n * which is the documented behaviour: past a refresh, server truth plus an honest\n * notice beats a record rebuilt from a blob.\n */\nexport function persistedPending(\n store: PendingState,\n): Pick<PendingState, 'pending'> {\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, { record: _dropped, ...rest }] of Object.entries(\n store.pending,\n )) {\n pending[transactionId] = rest;\n }\n return { pending };\n}\n\nconst PENDING_STORE = 'stores';\n\n/**\n * How many pending writes are watched at once, oldest evicted first.\n *\n * The same reasoning as {@link MAX_TABS}: an unbounded set persisted to\n * IndexedDB grows without anything ever pruning it, and reconciliation asks the\n * tracker once per entry on every reconnect — so the cap is also what keeps that\n * fan-out a handful of requests rather than an unbounded one.\n *\n * An evicted entry is dropped silently. The record is still on the server and\n * the list's own query is the fallback; what is lost is only the watching.\n */\nexport const MAX_PENDING = 12;\n\n/**\n * Bump when the *shape* of an entry changes, and every persisted pending write\n * is discarded on the next load.\n *\n * Discarding is right here for a reason the other two stores do not share: an\n * entry is a claim that something is still in flight, and it stops being true on\n * its own. A stale one restored from a previous build watches a transaction that\n * settled hours ago, and the reconcile that would correct it is keyed on a shape\n * this build may no longer understand.\n */\nexport const PENDING_VERSION = 1;\n\nconst emptyPending: Pick<PendingState, 'pending'> = { pending: {} };\n\n/**\n * Discard, never guess — written out rather than left to zustand's default,\n * which discards too but logs an error for a decision that was made on purpose.\n * Same call as `migrateDrafts` and `migrateTabs`.\n */\nexport function migratePending(): Pick<PendingState, 'pending'> {\n return emptyPending;\n}\n\n/** Whether a restored value is an entry this build can act on. */\nfunction isPendingEntry(value: unknown): value is PendingEntry {\n if (value === null || typeof value !== 'object') return false;\n const entry = value as Partial<PendingEntry>;\n return (\n typeof entry.transactionId === 'string' &&\n typeof entry.entity === 'string' &&\n typeof entry.at === 'string' &&\n (entry.state === 'pending' || entry.state === 'failed')\n );\n}\n\n/**\n * Restores only the entries that are actually entries — per entry, so one bad\n * record does not take the rest with it. The `mergeDrafts` rule, for the same\n * reason: this is what happens when something else was written by a build that\n * predates the shape.\n */\nexport function mergePending(\n persistedState: unknown,\n currentState: PendingState,\n): PendingState {\n const restored = (persistedState as Partial<PendingState> | undefined)\n ?.pending;\n if (restored === null || typeof restored !== 'object') return currentState;\n\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, value] of Object.entries(restored)) {\n if (isPendingEntry(value)) pending[transactionId] = value;\n }\n return { ...currentState, pending };\n}\n\n/** Drops the oldest entries until the set fits, by announcement time. */\nfunction capPending(\n pending: Record<string, PendingEntry>,\n): Record<string, PendingEntry> {\n const entries = Object.entries(pending);\n if (entries.length <= MAX_PENDING) return pending;\n\n return Object.fromEntries(\n entries\n .sort(([, a], [, b]) => a.at.localeCompare(b.at))\n .slice(entries.length - MAX_PENDING),\n );\n}\n\nexport const usePendingState = create<PendingState>()(\n persist(\n (set, get) => ({\n pending: {},\n began: transaction =>\n set(state => ({\n pending: capPending({\n ...state.pending,\n [transaction.transactionId]: { ...transaction, state: 'pending' },\n }),\n })),\n // Reads current state rather than taking it from the caller, so a save\n // handler cannot ask about a store snapshot older than its own `await`.\n attach: (transactionId, record) => {\n const entry = get().pending[transactionId];\n if (entry === undefined) return false;\n set(state => ({\n pending: { ...state.pending, [transactionId]: { ...entry, record } },\n }));\n return true;\n },\n // Settling drops the entry rather than marking it done: the server's copy\n // is the truth now, and the list query that just refetched is showing it.\n settle: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _settled, ...rest } = state.pending;\n return { pending: rest };\n }),\n // A failure is *kept*, marked, until someone reads it. Dropping it here\n // would un-render the optimistic row and say nothing about why.\n fail: (transactionId, reason) =>\n set(state => {\n const entry = state.pending[transactionId];\n if (entry === undefined) return state;\n return {\n pending: {\n ...state.pending,\n [transactionId]: { ...entry, state: 'failed', reason },\n },\n };\n }),\n dismiss: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _dismissed, ...rest } = state.pending;\n return { pending: rest };\n }),\n }),\n {\n // Replaced with a scoped name before rehydrating, exactly as the tab and\n // draft stores are. This literal is only what an unscoped store would\n // write, and nothing rehydrates without passing through that call.\n name: 'pending',\n version: PENDING_VERSION,\n migrate: migratePending,\n merge: mergePending,\n storage: createJSONStorage(() =>\n makeIndexedDbStateStorage(WORKSPACE_DB, PENDING_STORE),\n ),\n partialize: persistedPending,\n skipHydration: true,\n },\n ),\n);\n"],"names":["create","createJSONStorage","persist","makeIndexedDbStateStorage","WORKSPACE_DB","persistedPending","store","pending","Object","entries","transactionId","record","_dropped","rest","PENDING_STORE","MAX_PENDING","PENDING_VERSION","emptyPending","migratePending","isPendingEntry","value","entry","entity","at","state","mergePending","persistedState","currentState","restored","capPending","length","fromEntries","sort","a","b","localeCompare","slice","usePendingState","set","get","began","transaction","attach","undefined","settle","_settled","fail","reason","dismiss","_dismissed","name","version","migrate","merge","storage","partialize","skipHydration"],"mappings":"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"}
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  }
22
22
  }
23
23
  },
24
- "version": "0.2.0",
24
+ "version": "0.4.0",
25
25
  "description": "Next.js application shell: back-office chrome, workspace tabs, CRUD screens derived from entity metadata, and session and proxy routes.",
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -39,37 +39,35 @@
39
39
  "exports": {
40
40
  "./package.json": "./package.json",
41
41
  ".": {
42
- "@entifix/source": "./src/index.ts",
43
42
  "types": "./dist/index.d.ts",
44
43
  "import": "./dist/index.js",
45
44
  "default": "./dist/index.js"
46
45
  },
47
46
  "./server": {
48
- "@entifix/source": "./src/server.ts",
49
47
  "types": "./dist/server.d.ts",
50
48
  "import": "./dist/server.js",
51
49
  "default": "./dist/server.js"
52
50
  }
53
51
  },
54
52
  "devDependencies": {
55
- "@tanstack/react-query": "^5.101.4",
53
+ "@tanstack/react-query": "^5.104.5",
56
54
  "effect": "4.0.5",
57
- "i18next": "26.3.6",
58
- "next": "16.2.10",
59
- "react": "19.2.8",
60
- "react-dom": "19.2.8",
55
+ "i18next": "26.5.3",
56
+ "next": "16.4.2",
57
+ "react": "19.3.1",
58
+ "react-dom": "19.3.1",
61
59
  "zustand": "^5.0.15",
62
- "@entifix/testing-unit": "0.2.0"
60
+ "@entifix/testing-unit": "0.4.0"
63
61
  },
64
62
  "dependencies": {
65
- "idb": "^8.0.3",
66
- "@entifix/authz": "0.2.0",
67
- "@entifix/react-controls": "0.2.0",
68
- "@entifix/react-integration": "0.2.0",
69
- "@entifix/business": "0.2.0",
70
- "@entifix/transactions": "0.2.0",
71
- "@entifix/i18n": "0.2.0",
72
- "@entifix/core": "0.2.0"
63
+ "idb": "^8.0.4",
64
+ "@entifix/authz": "0.4.0",
65
+ "@entifix/business": "0.4.0",
66
+ "@entifix/core": "0.4.0",
67
+ "@entifix/i18n": "0.4.0",
68
+ "@entifix/react-controls": "0.4.0",
69
+ "@entifix/react-integration": "0.4.0",
70
+ "@entifix/transactions": "0.4.0"
73
71
  },
74
72
  "files": [
75
73
  "dist",