@entifix/next-shell 0.4.2 → 0.6.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/back-office/group-by-screen-type.d.ts +2 -2
- package/dist/lib/back-office/group-by-screen-type.js +2 -2
- package/dist/lib/back-office/group-by-screen-type.js.map +1 -1
- package/dist/lib/back-office/nav.d.ts +1 -2
- package/dist/lib/back-office/nav.d.ts.map +1 -1
- package/dist/lib/back-office/nav.js.map +1 -1
- package/dist/lib/back-office/sidebar-nav.d.ts +2 -2
- package/dist/lib/back-office/sidebar-nav.js +2 -2
- package/dist/lib/back-office/sidebar-nav.js.map +1 -1
- package/dist/lib/command-palette/command-palette-host.js +1 -1
- package/dist/lib/command-palette/command-palette-host.js.map +1 -1
- package/dist/lib/command-palette/use-command-recency.d.ts +1 -1
- package/dist/lib/command-palette/use-command-recency.js +1 -1
- package/dist/lib/command-palette/use-command-recency.js.map +1 -1
- package/dist/lib/command-palette/use-nav-source.d.ts +4 -3
- package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -1
- package/dist/lib/command-palette/use-nav-source.js +4 -3
- package/dist/lib/command-palette/use-nav-source.js.map +1 -1
- package/dist/lib/command-palette/use-records-source.d.ts +1 -1
- package/dist/lib/command-palette/use-records-source.js +2 -2
- package/dist/lib/command-palette/use-records-source.js.map +1 -1
- package/dist/lib/command-palette/use-tabs-source.d.ts +1 -1
- package/dist/lib/command-palette/use-tabs-source.js +1 -1
- package/dist/lib/command-palette/use-tabs-source.js.map +1 -1
- package/dist/lib/command-palette/use-use-cases-source.js +2 -2
- package/dist/lib/command-palette/use-use-cases-source.js.map +1 -1
- package/dist/lib/config/config-route.d.ts +6 -19
- package/dist/lib/config/config-route.d.ts.map +1 -1
- package/dist/lib/config/config-route.js +6 -23
- package/dist/lib/config/config-route.js.map +1 -1
- package/dist/lib/config/configuration-source.d.ts +18 -0
- package/dist/lib/config/configuration-source.d.ts.map +1 -0
- package/dist/lib/config/configuration-source.js +15 -0
- package/dist/lib/config/configuration-source.js.map +1 -0
- package/dist/lib/config/service-proxy-route.d.ts +11 -2
- package/dist/lib/config/service-proxy-route.d.ts.map +1 -1
- package/dist/lib/config/service-proxy-route.js +27 -4
- package/dist/lib/config/service-proxy-route.js.map +1 -1
- package/dist/lib/crud/entity-crud-form.js +3 -3
- package/dist/lib/crud/entity-crud-form.js.map +1 -1
- package/dist/lib/crud/hand-off-write.d.ts +1 -1
- package/dist/lib/crud/hand-off-write.js +1 -1
- package/dist/lib/crud/hand-off-write.js.map +1 -1
- package/dist/lib/crud/make-entity-crud.d.ts +7 -9
- package/dist/lib/crud/make-entity-crud.d.ts.map +1 -1
- package/dist/lib/crud/make-entity-crud.js +15 -18
- package/dist/lib/crud/make-entity-crud.js.map +1 -1
- package/dist/lib/crud/make-entity-crud.types.d.ts +9 -12
- package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -1
- package/dist/lib/crud/make-entity-crud.types.js.map +1 -1
- package/dist/lib/crud/pending-notice.d.ts +2 -2
- package/dist/lib/crud/pending-notice.js +2 -2
- package/dist/lib/crud/pending-notice.js.map +1 -1
- package/dist/lib/crud/use-entity-affordances.d.ts +1 -1
- package/dist/lib/crud/use-entity-affordances.js +1 -1
- package/dist/lib/crud/use-entity-affordances.js.map +1 -1
- package/dist/lib/crud/use-entity-bulk.d.ts +2 -2
- package/dist/lib/crud/use-entity-bulk.js +2 -2
- package/dist/lib/crud/use-entity-bulk.js.map +1 -1
- package/dist/lib/health/health-routes.d.ts +3 -4
- package/dist/lib/health/health-routes.d.ts.map +1 -1
- package/dist/lib/health/health-routes.js +7 -9
- package/dist/lib/health/health-routes.js.map +1 -1
- package/dist/lib/i18n/catalog/en.d.ts.map +1 -1
- package/dist/lib/i18n/catalog/en.js +1 -264
- package/dist/lib/i18n/catalog/en.js.map +1 -1
- package/dist/lib/i18n/catalog/es.d.ts +2 -290
- package/dist/lib/i18n/catalog/es.d.ts.map +1 -1
- package/dist/lib/i18n/catalog/es.js +2 -296
- package/dist/lib/i18n/catalog/es.js.map +1 -1
- package/dist/lib/i18n/catalog/index.d.ts +0 -508
- package/dist/lib/i18n/catalog/index.d.ts.map +1 -1
- package/dist/lib/i18n/spec-catalog.js +4 -4
- package/dist/lib/i18n/spec-catalog.js.map +1 -1
- package/dist/lib/search/record-search-route.d.ts +1 -1
- package/dist/lib/search/record-search-route.js +1 -1
- package/dist/lib/search/record-search-route.js.map +1 -1
- package/dist/lib/search/record-search-source.d.ts +1 -2
- package/dist/lib/search/record-search-source.d.ts.map +1 -1
- package/dist/lib/search/record-search-source.js.map +1 -1
- package/dist/lib/search/record-search.types.d.ts +1 -1
- package/dist/lib/search/record-search.types.js +1 -1
- package/dist/lib/search/record-search.types.js.map +1 -1
- package/dist/lib/session/account-links.js +1 -1
- package/dist/lib/session/account-links.js.map +1 -1
- package/dist/lib/session/session-keepalive.d.ts +2 -4
- package/dist/lib/session/session-keepalive.d.ts.map +1 -1
- package/dist/lib/session/session-keepalive.js +2 -4
- package/dist/lib/session/session-keepalive.js.map +1 -1
- package/dist/lib/session/use-session-refresh.js +1 -1
- package/dist/lib/session/use-session-refresh.js.map +1 -1
- package/dist/lib/wizard/wizard-tab-kind.d.ts +1 -2
- package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -1
- package/dist/lib/wizard/wizard-tab-kind.js +1 -2
- package/dist/lib/wizard/wizard-tab-kind.js.map +1 -1
- package/dist/lib/workspace/drafts-state.d.ts.map +1 -1
- package/dist/lib/workspace/drafts-state.js +3 -2
- package/dist/lib/workspace/drafts-state.js.map +1 -1
- package/dist/lib/workspace/entity-tab-kind.d.ts +1 -2
- package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -1
- package/dist/lib/workspace/entity-tab-kind.js +1 -2
- package/dist/lib/workspace/entity-tab-kind.js.map +1 -1
- package/dist/lib/workspace/pending-state.d.ts +5 -4
- package/dist/lib/workspace/pending-state.d.ts.map +1 -1
- package/dist/lib/workspace/pending-state.js +1 -1
- package/dist/lib/workspace/pending-state.js.map +1 -1
- package/dist/lib/workspace/tabs-state.js +1 -1
- package/dist/lib/workspace/tabs-state.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/package.json +9 -9
- package/dist/lib/config/service-token.d.ts +0 -5
- package/dist/lib/config/service-token.d.ts.map +0 -1
- package/dist/lib/config/service-token.js +0 -18
- package/dist/lib/config/service-token.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/make-entity-crud.tsx"],"sourcesContent":["'use client';\n\nimport {\n type ConfigurationRepositoryTag,\n deleteUCFactory,\n type EntityRepositoryTag,\n getUCFactory,\n loadUCFactory,\n saveUCFactory,\n} from '@entifix/business';\nimport {\n describeEntityColumns,\n EntifixBuildError,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n envelopeEntityName,\n extractMetaEntity,\n} from '@entifix/core';\nimport { EntityTable, useCasesForSurface } from '@entifix/react-controls';\nimport {\n entityQueryScope,\n useDataLoading,\n useEntityMutation,\n useEntityRecord,\n} from '@entifix/react-integration';\nimport type { TransactionSink } from '@entifix/transactions';\nimport { TransactionSinkTag } from '@entifix/transactions';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { Context } from 'effect';\nimport { useParams, useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport { useEntityNavHost } from '../workspace/entity-nav.js';\nimport {\n pendingFor,\n pendingRecordsFor,\n usePendingTransactions,\n} from '../workspace/pending-transactions.js';\nimport { EntityCrudForm } from './entity-crud-form.js';\nimport { handOffWrite } from './hand-off-write.js';\nimport type {\n EntityCrud,\n EntityCrudLinkSource,\n EntityCrudOptions,\n EntityCrudSingleViewProps,\n} from './make-entity-crud.types.js';\nimport { PendingNotice } from './pending-notice.js';\nimport { CATALOG_NEW_SLUG, slugToEntityId } from './slug.js';\nimport { useCrudRenderLink } from './use-crud-render-link.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityBulk } from './use-entity-bulk.js';\n\n/** What a picker defaults to reading off its target. */\nconst TARGET_NAME_PROPERTY = 'name';\n\n/**\n * What the generated pages' use-cases run against.\n *\n * Exported because a hand-built screen needs the same three: a wizard reads\n * records, resolves pickers and writes through exactly these ports, and\n * rebuilding the union at each such call site is how one of them ends up\n * missing `TransactionSinkTag` and quietly losing its optimistic treatment.\n */\nexport type CrudContext =\n EntityRepositoryTag | ConfigurationRepositoryTag | TransactionSinkTag;\n\n/**\n * Merges the configuration adapter with one repository adapter into the context\n * the use-cases run against.\n *\n * The casts are the price of naming adapters by key: `AdapterKey` narrows which\n * names may be *offered*, but TypeScript will not carry that narrowing through\n * an index access on a generic record. The alternative is every call site\n * passing two `Context` values per entity — the duplication this factory exists\n * to remove.\n */\nexport function mergeCrudContext<TAdapters>(\n adapters: TAdapters,\n configuration: keyof TAdapters,\n repository: keyof TAdapters,\n sink: TransactionSink,\n): Context.Context<CrudContext> {\n return Context.add(\n Context.merge(\n adapters[configuration] as Context.Context<ConfigurationRepositoryTag>,\n adapters[repository] as Context.Context<EntityRepositoryTag>,\n ),\n TransactionSinkTag,\n sink,\n );\n}\n\n/**\n * Builds the list page and the single-record page for one entity.\n *\n * Every catalog entity used to cost ~10 files and ~300 lines whose only variable\n * was a class name: two organisms with a `.types.ts` each, two client pages, two\n * route files. The route files stay (Next needs a module per path); everything\n * between them is derived from the entity's own metadata, so a new reference\n * entity is this call plus a nav entry.\n *\n * Two things are checked here rather than at the call site, because both fail\n * silently otherwise. The catalog key must be the entity's own `@entity({ key })`\n * — they are the same string by convention, and a drifted one titles the form\n * after a different entity in a way only a reader of both catalogs would catch.\n * And a `links` entry must name a real member — a picker aimed at nothing renders\n * identically to a read-only field.\n *\n * **Serves compile-time `@entity()` classes only.** Operator reference data ships\n * with the code, so making this spec-driven would trade type safety for nothing;\n * a vendor-authored `EntitySpecification` renders through a separate path\n * (ADR 0014).\n */\n/**\n * A rejected verb, as something the form's error slot can render.\n *\n * `useErrorMessage` resolves `details.code` through the shared `errors`\n * catalog, so a shell that throws an `EntifixError` carrying one gets its\n * translated sentence; anything else — a network failure, a thrown string —\n * keeps its message rather than being relabelled with a code it never had.\n */\nfunction asEntifixError(failure: unknown): EntifixError {\n return failure instanceof EntifixLogicError ||\n failure instanceof EntifixBuildError\n ? failure\n : new EntifixLogicError(\n failure instanceof Error ? failure.message : String(failure),\n );\n}\n\nexport function makeEntityCrud<TEntity extends Entity, TAdapters>(\n entityConstructor: EntityConstructor<TEntity>,\n options: EntityCrudOptions<TAdapters, TEntity>,\n): EntityCrud<TEntity> {\n const {\n useAdapters,\n basePath,\n catalogKey,\n repository,\n configuration,\n hiddenFields = [],\n columns,\n toolbar,\n links = [],\n metadataSource: givenMetadataSource,\n metadata,\n runBulkUseCase,\n runUseCase,\n } = options;\n\n if (givenMetadataSource !== undefined && metadata !== undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} was given both metadataSource and metadata; pass one`,\n );\n }\n // An adapter value, so it is resolved per render from the same `useAdapters()`\n // the records come from — the host's composition, never the caller's module.\n const metadataSourceOf = (adapters: TAdapters) =>\n metadata === undefined\n ? givenMetadataSource\n : (adapters[metadata] as EntityMetadataSource);\n\n const meta = extractMetaEntity(entityConstructor);\n if (meta.key !== catalogKey) {\n throw new EntifixBuildError(\n `${entityConstructor.name} declares key \"${String(meta.key)}\" but was given catalog key \"${catalogKey}\"`,\n );\n }\n // Read off `@entity()` rather than rebuilt from `catalogKey`. Both spellings\n // produce the same string today, and that is exactly the problem: a second\n // place that knows how an entity's catalog subtree is laid out is a second\n // place to fix when one moves. `MetaEntityOptions` makes them optional, so an\n // entity that never declared them fails here — where a screen is generated —\n // rather than on the render that first tries to title a tab with `undefined`.\n const { labelKey: entityLabelKey, pluralKey: entityPluralKey } = meta;\n if (entityLabelKey === undefined || entityPluralKey === undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} must declare labelKey and pluralKey on @entity() to be generated`,\n );\n }\n\n // Computed once: a descriptor is a property of the class, so nothing about it\n // depends on the record being edited. `linkLabelProperty`/`linkSearchProperty`\n // are overridden here because a scalar foreign key's `@accessor()` cannot name\n // the target's members — it may not import the target at all.\n // The wire name, which is also what a pending entry records and what\n // `entityQueryScopeFor` keys on — one derivation, so a notice and an\n // invalidation can never disagree about which entity they mean.\n const entityName = envelopeEntityName(entityConstructor);\n const descriptors = describeEntityColumns(entityConstructor);\n const linkPlans = links.map(link => {\n const descriptor = descriptors.find(entry => entry.name === link.field);\n if (descriptor === undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} has no member \"${link.field}\" for a link source`,\n );\n }\n return {\n ...link,\n descriptor: {\n ...descriptor,\n linkLabelProperty: link.labelProperty ?? TARGET_NAME_PROPERTY,\n linkSearchProperty: link.searchProperty ?? TARGET_NAME_PROPERTY,\n },\n };\n });\n\n function ListPage() {\n const adapters = useAdapters();\n // Outside a `PendingTransactionsProvider` this watches nothing, so a host\n // that has not mounted one behaves exactly as it did before.\n const pending = usePendingTransactions();\n // Every internal href carries the locale. An unprefixed one still resolves —\n // the middleware redirects it — but the visitor pays a round trip per click.\n const withLocale = useLocaleHref();\n\n const queryClient = useQueryClient();\n const scope = entityQueryScope(entityConstructor);\n const metadataSource = metadataSourceOf(adapters);\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n const renderLink = useCrudRenderLink(catalogKey);\n\n // Records this browser has created but the service has not finished\n // writing. Prepended below rather than patched into the cache, which a\n // refetch would undo.\n const optimistic = pendingRecordsFor<TEntity>(pending, entityName);\n\n const pager = useDataLoading<TEntity, CrudContext>({\n uc: loadUCFactory<TEntity>(),\n ctx: mergeCrudContext(adapters, configuration, repository, pending),\n // Scoped rather than left to the per-instance fallback, which is correct\n // but unshared: with the entity's own scope one invalidation refreshes\n // every page and filter of it, which is what a bulk run needs — and it is\n // the same prefix `useReactiveInvalidation`already targets, so a generated\n // list now also refreshes on a reactive change event.\n queryKey: scope,\n });\n\n // Only when this caller may actually run something over a selection.\n //\n // Measured live: an `admin` holds `catalog-reference:*:read` and no\n // `retire`, so the service filters the verb out of `$metadata` — but the\n // selection column still rendered, offering a set that no action could be\n // taken on. A checkbox that can lead nowhere is worse than no checkbox: it\n // reads as a permission the user does not have.\n //\n // Decided from the served document rather than from the runner, because\n // the runner is a property of the *shell* (it knows the route) and the\n // grant is a property of the *caller*. The column therefore appears when\n // the document lands, which is the asynchrony ADR 0026 already accepts for\n // every action surface.\n const canRunBulk =\n useCasesForSurface('bulk-bar', affordances.metadata?.useCases).length > 0;\n\n const bulk = useEntityBulk<TEntity>({\n run: canRunBulk ? runBulkUseCase : undefined,\n // A bulk write changed the rows underneath the listing, so the page is\n // re-read; the selection deliberately survives it (#121 — \"the selection\n // is still there afterwards\"), because the operator's next act is usually\n // to retry the failures or run a second verb on the same rows.\n onCompleted: () => {\n void queryClient.invalidateQueries({ queryKey: scope });\n },\n });\n\n // What a row's overflow menu runs, and the one place the list reports a\n // verb that failed.\n //\n // ⚠️ **A row verb and the form's are the same runner with different\n // arguments.** `EntityTable` hands back the whole record, because that is\n // what it has; `runUseCase` takes an id, because that is what a route needs\n // (#216). The list has no error slot of its own, so a failure lands in the\n // table's own error region — the same one a failed page read uses.\n const [useCaseError, setUseCaseError] = useState<EntifixError | undefined>(\n undefined,\n );\n // Called unconditionally and the *result* gated, the way the surfaces are\n // read in `EntityForm`: the name begins with `use`, so short-circuiting it\n // reads to `react-hooks/rules-of-hooks` as a conditional hook.\n const rowUseCases = useCasesForSurface(\n 'row-menu',\n affordances.metadata?.useCases,\n );\n const canRunRowUseCase = runUseCase !== undefined && rowUseCases.length > 0;\n\n const handleRowUseCase = canRunRowUseCase\n ? (key: string, item: TEntity) => {\n if (item.id == null) return;\n setUseCaseError(undefined);\n void runUseCase(key, item.id).then(\n () => queryClient.invalidateQueries({ queryKey: scope }),\n (failure: unknown) => setUseCaseError(asEntifixError(failure)),\n );\n }\n : undefined;\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/* Above the table, because this is where `afterSave()` leaves the\n operator after a create — and a create is the only transactional\n write there is, so this is the surface the pending state has. */}\n <PendingNotice\n entries={pendingFor(pending, entityName)}\n onDismiss={pending.dismiss}\n />\n <EntityTable\n entityConstructor={entityConstructor}\n {...pager}\n items={[...optimistic, ...pager.items]}\n totalItems={pager.totalItems + optimistic.length}\n hrefFor={id => withLocale(`${basePath}/${String(id)}`)}\n newHref={withLocale(`${basePath}/${CATALOG_NEW_SLUG}`)}\n renderLink={renderLink}\n {...affordances}\n {...bulk.tableProps}\n onUseCase={handleRowUseCase}\n error={useCaseError ?? pager.error}\n >\n {columns}\n {toolbar}\n </EntityTable>\n </div>\n );\n }\n\n /**\n * Dual-host: as a route it reads its slug from the URL and returns to the list\n * on save; in a workspace tab it takes the slug and the post-save action as\n * props.\n */\n function SingleViewPage({\n slug,\n onSaved,\n onDeleted,\n draft,\n }: EntityCrudSingleViewProps = {}) {\n const adapters = useAdapters();\n const metadataSource = metadataSourceOf(adapters);\n const pending = usePendingTransactions();\n const router = useRouter();\n const withLocale = useLocaleHref();\n const params = useParams<{ slug: string }>();\n // A verb's failure is this page's, not the record fetch's or the mutation's,\n // so it needs a slot of its own to reach the form's error area.\n const [useCaseError, setUseCaseError] = useState<EntifixError | undefined>(\n undefined,\n );\n const id = slugToEntityId(slug ?? params.slug);\n\n const ctx = mergeCrudContext(adapters, configuration, repository, pending);\n\n const {\n entity,\n isLoading,\n error: loadError,\n reload,\n } = useEntityRecord<TEntity, CrudContext>({\n uc: getUCFactory<TEntity>(),\n ctx,\n id,\n });\n\n const {\n save,\n remove,\n isSaving,\n isDeleting,\n error: writeError,\n } = useEntityMutation<TEntity, CrudContext>({\n saveUc: saveUCFactory<TEntity>(),\n deleteUc: deleteUCFactory<TEntity>(),\n ctx,\n });\n\n // Rebuilt inline on every render on purpose: `useEntityLinkSource` holds\n // these in a ref and keeps them out of its query keys, precisely so a caller\n // does not have to memoise them.\n const linkSources: EntityCrudLinkSource[] = linkPlans.map(plan => ({\n field: plan.field,\n descriptor: plan.descriptor,\n config: {\n entityConstructor: plan.entityConstructor,\n loadUc: loadUCFactory(),\n getUc: getUCFactory(),\n ctx: mergeCrudContext(\n adapters,\n configuration,\n plan.repository,\n pending,\n ),\n },\n }));\n\n // Inside a workspace tab, \"back to the list\" is the list's tab, not a URL\n // that would leave the workspace. A host's own callbacks still win.\n const host = useEntityNavHost();\n const renderLink = useCrudRenderLink(catalogKey);\n const queryClient = useQueryClient();\n // The list this page returns to has its page cached, and it is no longer\n // true. Invalidated rather than patched, for the reason `handOffWrite`\n // gives: a transactional create is not in the service yet, and the pending\n // set, not the cache, is what keeps it visible until it is.\n //\n // ⚠️ Invisible while every Open was a document load, which threw the cache\n // away anyway (#20): the first client-side navigation back to the list\n // showed the record as it was before the save.\n const refreshList = () =>\n queryClient.invalidateQueries({\n queryKey: entityQueryScope(entityConstructor),\n });\n const toList = () =>\n host === undefined\n ? router.push(withLocale(basePath))\n : host.toList(catalogKey);\n const afterSave = onSaved ?? toList;\n const afterDelete = onDeleted ?? toList;\n\n // The draft is spent once the write *commits*, and only here is that known:\n // `useEntityForm` neither fetches nor saves, so it cannot clear its own.\n // A failed mutation deliberately keeps the draft — the edit is still the\n // user's only copy of what they typed.\n const handleSave = async (next: TEntity) => {\n const saved = await save(next);\n if (saved === undefined) return;\n\n // ⚠️ Deliberately *not* a `setQueriesData` patch. The list refetches on\n // mount — precisely when the operator arrives, having just been navigated\n // here — and the server legitimately does not hold the record yet, so the\n // refetch would replace the patched page and the row would vanish a\n // moment after appearing. The pending set outlives refetches.\n handOffWrite({ id: String(saved.id), record: saved, pending, draft });\n void refreshList();\n afterSave();\n };\n\n const handleDelete = async () => {\n if (await remove(id)) {\n draft?.clear();\n void refreshList();\n afterDelete();\n }\n };\n\n /**\n * An `entity`-bound verb, on the record being shown.\n *\n * It **reloads** rather than patching what it has: the service decides the\n * resulting state — a `publish` may land on `published` from four different\n * places — and a client that assumed the new value would show the wrong one\n * the first time the rule changed. `id == null` means the create form,\n * where there is no record to act on and the form renders no verbs anyway.\n *\n * ⚠️ The rejection is **caught and shown**, never left to the promise.\n * `EntityForm`'s `onUseCase` returns `void`, so an uncaught rejection here\n * reaches the console and nothing else — measured on an offering refusing\n * `unpublish` from `draft`: the service answered `409` with a coded body,\n * the record correctly did not move, and the operator saw a button that\n * appeared to do nothing at all.\n */\n const handleUseCase =\n runUseCase === undefined || id == null\n ? undefined\n : (key: string) => {\n setUseCaseError(undefined);\n void runUseCase(key, id).then(\n () => reload(),\n (failure: unknown) => setUseCaseError(asEntifixError(failure)),\n );\n };\n\n return (\n <EntityCrudForm<TEntity>\n // Remounts (and reseeds the fields) once the record arrives.\n key={String(entity?.id ?? CATALOG_NEW_SLUG)}\n entityConstructor={entityConstructor}\n catalogKey={catalogKey}\n metadataSource={metadataSource}\n hiddenFields={hiddenFields}\n links={linkSources}\n entity={entity}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={loadError ?? writeError ?? useCaseError}\n onSave={handleSave}\n onDelete={id == null ? undefined : handleDelete}\n onUseCase={handleUseCase}\n backHref={withLocale(basePath)}\n renderLink={renderLink}\n draft={draft}\n />\n );\n }\n\n return {\n entityConstructor,\n entityKey: catalogKey,\n entityLabelKey,\n entityPluralKey,\n basePath,\n ListPage,\n SingleViewPage,\n };\n}\n"],"names":["deleteUCFactory","getUCFactory","loadUCFactory","saveUCFactory","describeEntityColumns","EntifixBuildError","EntifixLogicError","envelopeEntityName","extractMetaEntity","EntityTable","useCasesForSurface","entityQueryScope","useDataLoading","useEntityMutation","useEntityRecord","TransactionSinkTag","useQueryClient","Context","useParams","useRouter","useState","useLocaleHref","useEntityNavHost","pendingFor","pendingRecordsFor","usePendingTransactions","EntityCrudForm","handOffWrite","PendingNotice","CATALOG_NEW_SLUG","slugToEntityId","useCrudRenderLink","useEntityAffordances","useEntityBulk","TARGET_NAME_PROPERTY","mergeCrudContext","adapters","configuration","repository","sink","add","merge","asEntifixError","failure","Error","message","String","makeEntityCrud","entityConstructor","options","useAdapters","basePath","catalogKey","hiddenFields","columns","toolbar","links","metadataSource","givenMetadataSource","metadata","runBulkUseCase","runUseCase","undefined","name","metadataSourceOf","meta","key","labelKey","entityLabelKey","pluralKey","entityPluralKey","entityName","descriptors","linkPlans","map","link","descriptor","find","entry","field","linkLabelProperty","labelProperty","linkSearchProperty","searchProperty","ListPage","affordances","pending","withLocale","queryClient","scope","renderLink","optimistic","pager","uc","ctx","queryKey","canRunBulk","useCases","length","bulk","run","onCompleted","invalidateQueries","useCaseError","setUseCaseError","rowUseCases","canRunRowUseCase","handleRowUseCase","item","id","then","div","className","entries","onDismiss","dismiss","items","totalItems","hrefFor","newHref","tableProps","onUseCase","error","SingleViewPage","slug","onSaved","onDeleted","draft","loadError","router","params","entity","isLoading","reload","save","remove","isSaving","isDeleting","writeError","saveUc","deleteUc","linkSources","plan","config","loadUc","getUc","host","refreshList","toList","push","afterSave","afterDelete","handleSave","next","saved","record","handleDelete","clear","handleUseCase","onSave","onDelete","backHref","entityKey"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/make-entity-crud.tsx"],"sourcesContent":["'use client';\n\nimport {\n deleteUCFactory,\n type EntityRepositoryTag,\n getUCFactory,\n loadUCFactory,\n saveUCFactory,\n} from '@entifix/business';\nimport {\n describeEntityColumns,\n EntifixBuildError,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n envelopeEntityName,\n extractMetaEntity,\n} from '@entifix/core';\nimport { EntityTable, useCasesForSurface } from '@entifix/react-controls';\nimport {\n entityQueryScope,\n useDataLoading,\n useEntityMutation,\n useEntityRecord,\n} from '@entifix/react-integration';\nimport type { TransactionSink } from '@entifix/transactions';\nimport { TransactionSinkTag } from '@entifix/transactions';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { Context } from 'effect';\nimport { useParams, useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport { useEntityNavHost } from '../workspace/entity-nav.js';\nimport {\n pendingFor,\n pendingRecordsFor,\n usePendingTransactions,\n} from '../workspace/pending-transactions.js';\nimport { EntityCrudForm } from './entity-crud-form.js';\nimport { handOffWrite } from './hand-off-write.js';\nimport type {\n EntityCrud,\n EntityCrudLinkSource,\n EntityCrudOptions,\n EntityCrudSingleViewProps,\n} from './make-entity-crud.types.js';\nimport { PendingNotice } from './pending-notice.js';\nimport { CATALOG_NEW_SLUG, slugToEntityId } from './slug.js';\nimport { useCrudRenderLink } from './use-crud-render-link.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityBulk } from './use-entity-bulk.js';\n\n/** What a picker defaults to reading off its target. */\nconst TARGET_NAME_PROPERTY = 'name';\n\n/**\n * What the generated pages' use-cases run against.\n *\n * Exported because a hand-built screen needs the same three: a wizard reads\n * records, resolves pickers and writes through exactly these ports, and\n * rebuilding the union at each such call site is how one of them ends up\n * missing `TransactionSinkTag` and quietly losing its optimistic treatment.\n */\nexport type CrudContext = EntityRepositoryTag | TransactionSinkTag;\n\n/**\n * Adds the pending-write sink to one repository adapter, giving the context the\n * use-cases run against.\n *\n * The cast is the price of naming adapters by key: `AdapterKey` narrows which\n * names may be *offered*, but TypeScript will not carry that narrowing through\n * an index access on a generic record.\n */\nexport function mergeCrudContext<TAdapters>(\n adapters: TAdapters,\n repository: keyof TAdapters,\n sink: TransactionSink,\n): Context.Context<CrudContext> {\n return Context.add(\n adapters[repository] as Context.Context<EntityRepositoryTag>,\n TransactionSinkTag,\n sink,\n );\n}\n\n/**\n * Builds the list page and the single-record page for one entity.\n *\n * Every catalog entity used to cost ~10 files and ~300 lines whose only variable\n * was a class name: two organisms with a `.types.ts` each, two client pages, two\n * route files. The route files stay (Next needs a module per path); everything\n * between them is derived from the entity's own metadata, so a new reference\n * entity is this call plus a nav entry.\n *\n * Two things are checked here rather than at the call site, because both fail\n * silently otherwise. The catalog key must be the entity's own `@entity({ key })`\n * — they are the same string by convention, and a drifted one titles the form\n * after a different entity in a way only a reader of both catalogs would catch.\n * And a `links` entry must name a real member — a picker aimed at nothing renders\n * identically to a read-only field.\n *\n * **Serves compile-time `@entity()` classes only.** Operator reference data ships\n * with the code, so making this spec-driven would trade type safety for nothing;\n * a vendor-authored `EntitySpecification` renders through a separate path.\n */\n/**\n * A rejected verb, as something the form's error slot can render.\n *\n * `useErrorMessage` resolves `details.code` through the shared `errors`\n * catalog, so a shell that throws an `EntifixError` carrying one gets its\n * translated sentence; anything else — a network failure, a thrown string —\n * keeps its message rather than being relabelled with a code it never had.\n */\nfunction asEntifixError(failure: unknown): EntifixError {\n return failure instanceof EntifixLogicError ||\n failure instanceof EntifixBuildError\n ? failure\n : new EntifixLogicError(\n failure instanceof Error ? failure.message : String(failure),\n );\n}\n\nexport function makeEntityCrud<TEntity extends Entity, TAdapters>(\n entityConstructor: EntityConstructor<TEntity>,\n options: EntityCrudOptions<TAdapters, TEntity>,\n): EntityCrud<TEntity> {\n const {\n useAdapters,\n basePath,\n catalogKey,\n repository,\n hiddenFields = [],\n columns,\n toolbar,\n links = [],\n metadataSource: givenMetadataSource,\n metadata,\n runBulkUseCase,\n runUseCase,\n } = options;\n\n if (givenMetadataSource !== undefined && metadata !== undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} was given both metadataSource and metadata; pass one`,\n );\n }\n // An adapter value, so it is resolved per render from the same `useAdapters()`\n // the records come from — the host's composition, never the caller's module.\n const metadataSourceOf = (adapters: TAdapters) =>\n metadata === undefined\n ? givenMetadataSource\n : (adapters[metadata] as EntityMetadataSource);\n\n const meta = extractMetaEntity(entityConstructor);\n if (meta.key !== catalogKey) {\n throw new EntifixBuildError(\n `${entityConstructor.name} declares key \"${String(meta.key)}\" but was given catalog key \"${catalogKey}\"`,\n );\n }\n // Read off `@entity()` rather than rebuilt from `catalogKey`. Both spellings\n // produce the same string today, and that is exactly the problem: a second\n // place that knows how an entity's catalog subtree is laid out is a second\n // place to fix when one moves. `MetaEntityOptions` makes them optional, so an\n // entity that never declared them fails here — where a screen is generated —\n // rather than on the render that first tries to title a tab with `undefined`.\n const { labelKey: entityLabelKey, pluralKey: entityPluralKey } = meta;\n if (entityLabelKey === undefined || entityPluralKey === undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} must declare labelKey and pluralKey on @entity() to be generated`,\n );\n }\n\n // Computed once: a descriptor is a property of the class, so nothing about it\n // depends on the record being edited. `linkLabelProperty`/`linkSearchProperty`\n // are overridden here because a scalar foreign key's `@accessor()` cannot name\n // the target's members — it may not import the target at all.\n // The wire name, which is also what a pending entry records and what\n // `entityQueryScopeFor` keys on — one derivation, so a notice and an\n // invalidation can never disagree about which entity they mean.\n const entityName = envelopeEntityName(entityConstructor);\n const descriptors = describeEntityColumns(entityConstructor);\n const linkPlans = links.map(link => {\n const descriptor = descriptors.find(entry => entry.name === link.field);\n if (descriptor === undefined) {\n throw new EntifixBuildError(\n `${entityConstructor.name} has no member \"${link.field}\" for a link source`,\n );\n }\n return {\n ...link,\n descriptor: {\n ...descriptor,\n linkLabelProperty: link.labelProperty ?? TARGET_NAME_PROPERTY,\n linkSearchProperty: link.searchProperty ?? TARGET_NAME_PROPERTY,\n },\n };\n });\n\n function ListPage() {\n const adapters = useAdapters();\n // Outside a `PendingTransactionsProvider` this watches nothing, so a host\n // that has not mounted one behaves exactly as it did before.\n const pending = usePendingTransactions();\n // Every internal href carries the locale. An unprefixed one still resolves —\n // the middleware redirects it — but the visitor pays a round trip per click.\n const withLocale = useLocaleHref();\n\n const queryClient = useQueryClient();\n const scope = entityQueryScope(entityConstructor);\n const metadataSource = metadataSourceOf(adapters);\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n const renderLink = useCrudRenderLink(catalogKey);\n\n // Records this browser has created but the service has not finished\n // writing. Prepended below rather than patched into the cache, which a\n // refetch would undo.\n const optimistic = pendingRecordsFor<TEntity>(pending, entityName);\n\n const pager = useDataLoading<TEntity, CrudContext>({\n uc: loadUCFactory<TEntity>(),\n ctx: mergeCrudContext(adapters, repository, pending),\n // Scoped rather than left to the per-instance fallback, which is correct\n // but unshared: with the entity's own scope one invalidation refreshes\n // every page and filter of it, which is what a bulk run needs — and it is\n // the same prefix `useReactiveInvalidation`already targets, so a generated\n // list now also refreshes on a reactive change event.\n queryKey: scope,\n });\n\n // Only when this caller may actually run something over a selection.\n //\n // Measured live: an `admin` holds `catalog-reference:*:read` and no\n // `retire`, so the service filters the verb out of `$metadata` — but the\n // selection column still rendered, offering a set that no action could be\n // taken on. A checkbox that can lead nowhere is worse than no checkbox: it\n // reads as a permission the user does not have.\n //\n // Decided from the served document rather than from the runner, because\n // the runner is a property of the *shell* (it knows the route) and the\n // grant is a property of the *caller*. The column therefore appears when\n // the document lands, which is the asynchrony serving affordances accepts\n // for every action surface.\n const canRunBulk =\n useCasesForSurface('bulk-bar', affordances.metadata?.useCases).length > 0;\n\n const bulk = useEntityBulk<TEntity>({\n run: canRunBulk ? runBulkUseCase : undefined,\n // A bulk write changed the rows underneath the listing, so the page is\n // re-read; the selection deliberately survives it (\"the selection\n // is still there afterwards\"), because the operator's next act is usually\n // to retry the failures or run a second verb on the same rows.\n onCompleted: () => {\n void queryClient.invalidateQueries({ queryKey: scope });\n },\n });\n\n // What a row's overflow menu runs, and the one place the list reports a\n // verb that failed.\n //\n // ⚠️ **A row verb and the form's are the same runner with different\n // arguments.** `EntityTable` hands back the whole record, because that is\n // what it has; `runUseCase` takes an id, because that is what a route needs\n // The list has no error slot of its own, so a failure lands in the\n // table's own error region — the same one a failed page read uses.\n const [useCaseError, setUseCaseError] = useState<EntifixError | undefined>(\n undefined,\n );\n // Called unconditionally and the *result* gated, the way the surfaces are\n // read in `EntityForm`: the name begins with `use`, so short-circuiting it\n // reads to `react-hooks/rules-of-hooks` as a conditional hook.\n const rowUseCases = useCasesForSurface(\n 'row-menu',\n affordances.metadata?.useCases,\n );\n const canRunRowUseCase = runUseCase !== undefined && rowUseCases.length > 0;\n\n const handleRowUseCase = canRunRowUseCase\n ? (key: string, item: TEntity) => {\n if (item.id == null) return;\n setUseCaseError(undefined);\n void runUseCase(key, item.id).then(\n () => queryClient.invalidateQueries({ queryKey: scope }),\n (failure: unknown) => setUseCaseError(asEntifixError(failure)),\n );\n }\n : undefined;\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/* Above the table, because this is where `afterSave()` leaves the\n operator after a create — and a create is the only transactional\n write there is, so this is the surface the pending state has. */}\n <PendingNotice\n entries={pendingFor(pending, entityName)}\n onDismiss={pending.dismiss}\n />\n <EntityTable\n entityConstructor={entityConstructor}\n {...pager}\n items={[...optimistic, ...pager.items]}\n totalItems={pager.totalItems + optimistic.length}\n hrefFor={id => withLocale(`${basePath}/${String(id)}`)}\n newHref={withLocale(`${basePath}/${CATALOG_NEW_SLUG}`)}\n renderLink={renderLink}\n {...affordances}\n {...bulk.tableProps}\n onUseCase={handleRowUseCase}\n error={useCaseError ?? pager.error}\n >\n {columns}\n {toolbar}\n </EntityTable>\n </div>\n );\n }\n\n /**\n * Dual-host: as a route it reads its slug from the URL and returns to the list\n * on save; in a workspace tab it takes the slug and the post-save action as\n * props.\n */\n function SingleViewPage({\n slug,\n onSaved,\n onDeleted,\n draft,\n }: EntityCrudSingleViewProps = {}) {\n const adapters = useAdapters();\n const metadataSource = metadataSourceOf(adapters);\n const pending = usePendingTransactions();\n const router = useRouter();\n const withLocale = useLocaleHref();\n const params = useParams<{ slug: string }>();\n // A verb's failure is this page's, not the record fetch's or the mutation's,\n // so it needs a slot of its own to reach the form's error area.\n const [useCaseError, setUseCaseError] = useState<EntifixError | undefined>(\n undefined,\n );\n const id = slugToEntityId(slug ?? params.slug);\n\n const ctx = mergeCrudContext(adapters, repository, pending);\n\n const {\n entity,\n isLoading,\n error: loadError,\n reload,\n } = useEntityRecord<TEntity, CrudContext>({\n uc: getUCFactory<TEntity>(),\n ctx,\n id,\n });\n\n const {\n save,\n remove,\n isSaving,\n isDeleting,\n error: writeError,\n } = useEntityMutation<TEntity, CrudContext>({\n saveUc: saveUCFactory<TEntity>(),\n deleteUc: deleteUCFactory<TEntity>(),\n ctx,\n });\n\n // Rebuilt inline on every render on purpose: `useEntityLinkSource` holds\n // these in a ref and keeps them out of its query keys, precisely so a caller\n // does not have to memoise them.\n const linkSources: EntityCrudLinkSource[] = linkPlans.map(plan => ({\n field: plan.field,\n descriptor: plan.descriptor,\n config: {\n entityConstructor: plan.entityConstructor,\n loadUc: loadUCFactory(),\n getUc: getUCFactory(),\n ctx: mergeCrudContext(adapters, plan.repository, pending),\n },\n }));\n\n // Inside a workspace tab, \"back to the list\" is the list's tab, not a URL\n // that would leave the workspace. A host's own callbacks still win.\n const host = useEntityNavHost();\n const renderLink = useCrudRenderLink(catalogKey);\n const queryClient = useQueryClient();\n // The list this page returns to has its page cached, and it is no longer\n // true. Invalidated rather than patched, for the reason `handOffWrite`\n // gives: a transactional create is not in the service yet, and the pending\n // set, not the cache, is what keeps it visible until it is.\n //\n // ⚠️ Invisible while every Open was a document load, which threw the cache\n // away anyway (#20): the first client-side navigation back to the list\n // showed the record as it was before the save.\n const refreshList = () =>\n queryClient.invalidateQueries({\n queryKey: entityQueryScope(entityConstructor),\n });\n const toList = () =>\n host === undefined\n ? router.push(withLocale(basePath))\n : host.toList(catalogKey);\n const afterSave = onSaved ?? toList;\n const afterDelete = onDeleted ?? toList;\n\n // The draft is spent once the write *commits*, and only here is that known:\n // `useEntityForm` neither fetches nor saves, so it cannot clear its own.\n // A failed mutation deliberately keeps the draft — the edit is still the\n // user's only copy of what they typed.\n const handleSave = async (next: TEntity) => {\n const saved = await save(next);\n if (saved === undefined) return;\n\n // ⚠️ Deliberately *not* a `setQueriesData` patch. The list refetches on\n // mount — precisely when the operator arrives, having just been navigated\n // here — and the server legitimately does not hold the record yet, so the\n // refetch would replace the patched page and the row would vanish a\n // moment after appearing. The pending set outlives refetches.\n handOffWrite({ id: String(saved.id), record: saved, pending, draft });\n void refreshList();\n afterSave();\n };\n\n const handleDelete = async () => {\n if (await remove(id)) {\n draft?.clear();\n void refreshList();\n afterDelete();\n }\n };\n\n /**\n * An `entity`-bound verb, on the record being shown.\n *\n * It **reloads** rather than patching what it has: the service decides the\n * resulting state — a `publish` may land on `published` from four different\n * places — and a client that assumed the new value would show the wrong one\n * the first time the rule changed. `id == null` means the create form,\n * where there is no record to act on and the form renders no verbs anyway.\n *\n * ⚠️ The rejection is **caught and shown**, never left to the promise.\n * `EntityForm`'s `onUseCase` returns `void`, so an uncaught rejection here\n * reaches the console and nothing else — measured on an offering refusing\n * `unpublish` from `draft`: the service answered `409` with a coded body,\n * the record correctly did not move, and the operator saw a button that\n * appeared to do nothing at all.\n */\n const handleUseCase =\n runUseCase === undefined || id == null\n ? undefined\n : (key: string) => {\n setUseCaseError(undefined);\n void runUseCase(key, id).then(\n () => reload(),\n (failure: unknown) => setUseCaseError(asEntifixError(failure)),\n );\n };\n\n return (\n <EntityCrudForm<TEntity>\n // Remounts (and reseeds the fields) once the record arrives.\n key={String(entity?.id ?? CATALOG_NEW_SLUG)}\n entityConstructor={entityConstructor}\n catalogKey={catalogKey}\n metadataSource={metadataSource}\n hiddenFields={hiddenFields}\n links={linkSources}\n entity={entity}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={loadError ?? writeError ?? useCaseError}\n onSave={handleSave}\n onDelete={id == null ? undefined : handleDelete}\n onUseCase={handleUseCase}\n backHref={withLocale(basePath)}\n renderLink={renderLink}\n draft={draft}\n />\n );\n }\n\n return {\n entityConstructor,\n entityKey: catalogKey,\n entityLabelKey,\n entityPluralKey,\n basePath,\n ListPage,\n SingleViewPage,\n };\n}\n"],"names":["deleteUCFactory","getUCFactory","loadUCFactory","saveUCFactory","describeEntityColumns","EntifixBuildError","EntifixLogicError","envelopeEntityName","extractMetaEntity","EntityTable","useCasesForSurface","entityQueryScope","useDataLoading","useEntityMutation","useEntityRecord","TransactionSinkTag","useQueryClient","Context","useParams","useRouter","useState","useLocaleHref","useEntityNavHost","pendingFor","pendingRecordsFor","usePendingTransactions","EntityCrudForm","handOffWrite","PendingNotice","CATALOG_NEW_SLUG","slugToEntityId","useCrudRenderLink","useEntityAffordances","useEntityBulk","TARGET_NAME_PROPERTY","mergeCrudContext","adapters","repository","sink","add","asEntifixError","failure","Error","message","String","makeEntityCrud","entityConstructor","options","useAdapters","basePath","catalogKey","hiddenFields","columns","toolbar","links","metadataSource","givenMetadataSource","metadata","runBulkUseCase","runUseCase","undefined","name","metadataSourceOf","meta","key","labelKey","entityLabelKey","pluralKey","entityPluralKey","entityName","descriptors","linkPlans","map","link","descriptor","find","entry","field","linkLabelProperty","labelProperty","linkSearchProperty","searchProperty","ListPage","affordances","pending","withLocale","queryClient","scope","renderLink","optimistic","pager","uc","ctx","queryKey","canRunBulk","useCases","length","bulk","run","onCompleted","invalidateQueries","useCaseError","setUseCaseError","rowUseCases","canRunRowUseCase","handleRowUseCase","item","id","then","div","className","entries","onDismiss","dismiss","items","totalItems","hrefFor","newHref","tableProps","onUseCase","error","SingleViewPage","slug","onSaved","onDeleted","draft","loadError","router","params","entity","isLoading","reload","save","remove","isSaving","isDeleting","writeError","saveUc","deleteUc","linkSources","plan","config","loadUc","getUc","host","refreshList","toList","push","afterSave","afterDelete","handleSave","next","saved","record","handleDelete","clear","handleUseCase","onSave","onDelete","backHref","entityKey"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityRepositoryTag } from '@entifix/business';
|
|
2
2
|
import type { BulkOutcome, Entity, EntityConstructor, EntityFieldDescriptor, EntityId, EntityMetadataSource, EntitySelection } from '@entifix/core';
|
|
3
3
|
import type { EntityDraftStore, EntityLinkSourceConfig } from '@entifix/react-integration';
|
|
4
4
|
import type { Context } from 'effect/Context';
|
|
@@ -12,9 +12,9 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
12
12
|
* catalog: an entity whose subtree has no `form.editTitle`/`form.newTitle`
|
|
13
13
|
* cannot be handed to {@link makeEntityCrud} at all, and adding a reference
|
|
14
14
|
* entity therefore starts by adding its copy to both locales. That is the whole
|
|
15
|
-
* i18n gate for this factory —
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* i18n gate for this factory — a host's catalog check typically scans its
|
|
16
|
+
* domain packages for `@useCase()` decorators and never `.tsx`, so a runtime
|
|
17
|
+
* key here would be checked by nothing.
|
|
18
18
|
*
|
|
19
19
|
* `label`/`plural` joined the condition when {@link EntityCrud} began carrying
|
|
20
20
|
* them, so that a workspace tab and a nav entry derived from this descriptor
|
|
@@ -106,7 +106,6 @@ export interface EntityCrudOptions<TAdapters, TEntity extends Entity> {
|
|
|
106
106
|
readonly catalogKey: EntityCatalogKey;
|
|
107
107
|
/** Which adapter holds this entity's own repository. */
|
|
108
108
|
readonly repository: AdapterKey<TAdapters, Context<EntityRepositoryTag>>;
|
|
109
|
-
readonly configuration: AdapterKey<TAdapters, Context<ConfigurationRepositoryTag>>;
|
|
110
109
|
/**
|
|
111
110
|
* Members the form keeps but does not show — `id` always, plus anything a
|
|
112
111
|
* transaction assigns. A hidden member still round-trips: it is dropped from
|
|
@@ -122,8 +121,7 @@ export interface EntityCrudOptions<TAdapters, TEntity extends Entity> {
|
|
|
122
121
|
* Its own option rather than more `columns`, though both end up as
|
|
123
122
|
* `EntityTable` children and the slots are sorted by component identity: a
|
|
124
123
|
* toolbar filed under a columns prop is a name that lies, and the first thing
|
|
125
|
-
* to go here is a link into a wizard, which is not a column by any reading
|
|
126
|
-
* ([ADR 0045](../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).
|
|
124
|
+
* to go here is a link into a wizard, which is not a column by any reading.
|
|
127
125
|
*/
|
|
128
126
|
readonly toolbar?: ReactNode;
|
|
129
127
|
/** Frozen at factory time — see `use-entity-link-sources.ts`. */
|
|
@@ -131,7 +129,7 @@ export interface EntityCrudOptions<TAdapters, TEntity extends Entity> {
|
|
|
131
129
|
/**
|
|
132
130
|
* Where the entity's served affordances come from — `GET /api/<entity>/$metadata`.
|
|
133
131
|
*
|
|
134
|
-
* Supplying it is what turns on
|
|
132
|
+
* Supplying it is what turns on every served affordance:
|
|
135
133
|
* Save and Delete filtered against the caller's real grants, the declared
|
|
136
134
|
* verbs on the form, the row overflow menu, and the bulk bar. Omitting it
|
|
137
135
|
* keeps the pre-0026 behaviour exactly, which is what every generated catalog
|
|
@@ -167,11 +165,11 @@ export interface EntityCrudOptions<TAdapters, TEntity extends Entity> {
|
|
|
167
165
|
* Runs an `entity`-bound verb on the record the single view is showing.
|
|
168
166
|
*
|
|
169
167
|
* The sibling of {@link runBulkUseCase}, and it had to exist for the same
|
|
170
|
-
* reason: `EntityForm` has rendered these buttons
|
|
168
|
+
* reason: `EntityForm` has rendered these buttons for a long time, and
|
|
171
169
|
* `EntityCrudForm` has accepted an `onUseCase` for as long — but nothing ever
|
|
172
170
|
* passed one, so a generated screen showed a declared, granted, served verb
|
|
173
171
|
* that did nothing when clicked. Measured on `ProductOffering`'s `publish`,
|
|
174
|
-
* which passed every
|
|
172
|
+
* which passed every check its host's register ran and appeared nowhere.
|
|
175
173
|
*
|
|
176
174
|
* Resolving means the record changed, and the page reloads it: a verb that
|
|
177
175
|
* moves a lifecycle leaves the form showing the old state otherwise, which
|
|
@@ -206,8 +204,7 @@ export interface EntityCrudSingleViewProps {
|
|
|
206
204
|
* What the factory returns: a **named descriptor**, not four loose components.
|
|
207
205
|
*
|
|
208
206
|
* The identity fields are here so the workspace registry and the nav are derived
|
|
209
|
-
* from a list of these rather than from the hand-written const maps they replace
|
|
210
|
-
* (#141).
|
|
207
|
+
* from a list of these rather than from the hand-written const maps they replace.
|
|
211
208
|
*/
|
|
212
209
|
export interface EntityCrud<TEntity extends Entity> {
|
|
213
210
|
readonly entityConstructor: EntityConstructor<TEntity>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-entity-crud.types.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/make-entity-crud.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"make-entity-crud.types.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/make-entity-crud.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/make-entity-crud.types.ts"],"sourcesContent":["import type {\n ConfigurationRepositoryTag,\n EntityRepositoryTag,\n} from '@entifix/business';\nimport type {\n BulkOutcome,\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityMetadataSource,\n EntitySelection,\n} from '@entifix/core';\nimport type {\n EntityDraftStore,\n EntityLinkSourceConfig,\n} from '@entifix/react-integration';\nimport type { Context } from 'effect/Context';\nimport type { CustomTypeOptions } from 'i18next';\nimport type { ReactElement, ReactNode } from 'react';\n\n/**\n * The `entity` catalog keys that carry **both** form titles, a `label` and a\n * `plural`.\n *\n * Derived from the Spanish catalog rather than written out, so the union is the\n * catalog: an entity whose subtree has no `form.editTitle`/`form.newTitle`\n * cannot be handed to {@link makeEntityCrud} at all, and adding a reference\n * entity therefore starts by adding its copy to both locales. That is the whole\n * i18n gate for this factory — `@r10c/i18n-check` scans only\n * `packages/business/ts` for `@useCase()` decorators and never `.tsx`, so a\n * runtime key here would be checked by nothing.\n *\n * `label`/`plural` joined the condition when {@link EntityCrud} began carrying\n * them, so that a workspace tab and a nav entry derived from this descriptor\n * name the entity in the catalog's own words. They are part of the **type**\n * rather than a runtime assertion in the factory, because the catalog is\n * statically known here: a missing `plural` should fail at the call site that\n * generates the screens, not on the render that first tries to title a tab.\n *\n * Keeping the key typed is what lets the generated form call `useT('entity')`\n * with a template literal and still fail the build on a typo, instead of\n * reaching for the `useTranslateKey` escape hatch that authored copy must not\n * use. `AccountLabelKey` in `../session/account-links.ts` is the same trick\n * written out by hand; this one cannot drift because it is computed.\n */\nexport type EntityCatalogKey<\n TEntityCatalog = EntityCatalogOf<TypeOptionsResources>,\n> = [TEntityCatalog] extends [never]\n ? // No host augmentation in this compilation — this package's own specs, or\n // an adopter who has not declared one. The factory still works; the key is\n // simply unchecked, which is the same trade `useTranslateKey` makes.\n string\n : {\n [K in keyof TEntityCatalog]: TEntityCatalog[K] extends {\n form: { editTitle: string; newTitle: string };\n label: string;\n plural: string;\n }\n ? K\n : never;\n }[keyof TEntityCatalog] &\n string;\n\n/**\n * The host's `entity` namespace, read back out of its own `i18next`\n * augmentation.\n *\n * ⚠️ **The `entity` catalog is the host's, not entifix's.** It names *their*\n * entities — a parameter, an offering, a sales channel — so a framework that\n * shipped one would be shipping an application's product copy. Reading it back\n * through `CustomTypeOptions` keeps the key union computed rather than written\n * out, which is what stops it drifting, while leaving the catalog where it\n * belongs. A host that declares no augmentation gets `string`, and the factory\n * still works with the gate relaxed.\n */\ntype TypeOptionsResources = CustomTypeOptions extends { resources: infer R }\n ? R\n : never;\n\ntype EntityCatalogOf<TResources> = TResources extends { entity: infer E }\n ? E\n : never;\n\n/**\n * A relation the generated form edits with a picker.\n *\n * The target is named here rather than derived, because the common case is a\n * **plain foreign key**: when the target lives in another slice's store a typed\n * `EntityLink` is both an illegal import and a cross-store join, so the member\n * is a bare `string` (`ProductSpecification.brandId` into `catalog-reference`)\n * and its `@accessor()` has no vocabulary for \"the target's name\". Hence\n * `labelProperty`/`searchProperty` are stated at the call site instead of being\n * left to `describeEntityColumns`' `'name'` default, which happens to be right\n * today and states nothing.\n *\n * `searchProperty` must be `filterable` on the **target** or the service answers\n * `400`; `useEntityLinkSource` throws at render rather than letting that surface\n * as an empty suggestion list.\n */\nexport interface EntityCrudLink<TAdapters> {\n /** The owning entity's accessor name, e.g. `'brandId'`. */\n readonly field: string;\n readonly entityConstructor: EntityConstructor<Entity>;\n /**\n * Which adapter holds the **target's** repository. Often a different service\n * than the record's own: `catalog-reference` is another slice's store, so\n * resolving an id goes through that domain's own read path.\n */\n readonly repository: AdapterKey<TAdapters, Context<EntityRepositoryTag>>;\n /** Defaults to `'name'`. */\n readonly labelProperty?: string;\n /** Defaults to `'name'`. Must be `filterable` on the target. */\n readonly searchProperty?: string;\n}\n\n/** The keys of `TAdapters` whose value is assignable to `TValue`. */\nexport type AdapterKey<TAdapters, TValue> = {\n [K in keyof TAdapters]: TAdapters[K] extends TValue ? K : never;\n}[keyof TAdapters];\n\nexport interface EntityCrudOptions<TAdapters, TEntity extends Entity> {\n /** The domain shell's adapters hook, e.g. `useMarketplaceAdminAdapters`. */\n readonly useAdapters: () => TAdapters;\n /** Route prefix for both pages, e.g. `'/catalog/product-brand'`. */\n readonly basePath: string;\n /**\n * The entity's `entity` catalog key. Asserted against `@entity({ key })` at\n * factory time — the two are the same string by convention\n * (`entity:product-brand.label` mirrors `key: 'product-brand'`), and letting\n * them drift would title a form after a different entity.\n */\n readonly catalogKey: EntityCatalogKey;\n /** Which adapter holds this entity's own repository. */\n readonly repository: AdapterKey<TAdapters, Context<EntityRepositoryTag>>;\n readonly configuration: AdapterKey<\n TAdapters,\n Context<ConfigurationRepositoryTag>\n >;\n /**\n * Members the form keeps but does not show — `id` always, plus anything a\n * transaction assigns. A hidden member still round-trips: it is dropped from\n * the rendered fields, not from the draft, so `reconstructEntity` carries it\n * back out of the seeded values.\n */\n readonly hiddenFields?: readonly string[];\n /** `<EntityColumn>` overrides for the list, for presentation metadata cannot express. */\n readonly columns?: ReactNode;\n /**\n * Extra controls beside the table's own — an `<EntityTableToolbar>`.\n *\n * Its own option rather than more `columns`, though both end up as\n * `EntityTable` children and the slots are sorted by component identity: a\n * toolbar filed under a columns prop is a name that lies, and the first thing\n * to go here is a link into a wizard, which is not a column by any reading\n * ([ADR 0045](../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).\n */\n readonly toolbar?: ReactNode;\n /** Frozen at factory time — see `use-entity-link-sources.ts`. */\n readonly links?: readonly EntityCrudLink<TAdapters>[];\n /**\n * Where the entity's served affordances come from — `GET /api/<entity>/$metadata`.\n *\n * Supplying it is what turns on everything ADR 0026 and ADR 0035 describe:\n * Save and Delete filtered against the caller's real grants, the declared\n * verbs on the form, the row overflow menu, and the bulk bar. Omitting it\n * keeps the pre-0026 behaviour exactly, which is what every generated catalog\n * rendered before this option existed.\n *\n * A `EntityMetadataSource` rather than a URL because the two hosts reach\n * their service differently — one through the config-driven REST adapters,\n * one through a shell's own same-origin routes.\n */\n readonly metadataSource?: EntityMetadataSource;\n /**\n * Which adapter holds this entity's {@link EntityMetadataSource} — the same\n * source as {@link metadataSource}, resolved from the host's composition\n * instead of built by the caller.\n *\n * Prefer it: a source is a transport, and naming it by key keeps the module\n * that generates the screens free of the adapter that fetches, exactly as\n * {@link repository} already does for records. Supplying both is a build\n * error — two answers to where the document comes from.\n */\n readonly metadata?: AdapterKey<TAdapters, EntityMetadataSource>;\n /**\n * Runs a `collection`-bound verb over a selection, and reports **per row**.\n *\n * Supplying it opts the list into the selection column and the bulk bar.\n * It is separate from `metadataSource` because the two are independent: an\n * entity can declare no collection verb and still want permission-filtered\n * Save, and the endpoint a verb posts to is the shell's knowledge, not\n * metadata's.\n */\n readonly runBulkUseCase?: (\n key: string,\n selection: EntitySelection<TEntity>,\n ) => Promise<readonly BulkOutcome[]>;\n /**\n * Runs an `entity`-bound verb on the record the single view is showing.\n *\n * The sibling of {@link runBulkUseCase}, and it had to exist for the same\n * reason: `EntityForm` has rendered these buttons since ADR 0035, and\n * `EntityCrudForm` has accepted an `onUseCase` for as long — but nothing ever\n * passed one, so a generated screen showed a declared, granted, served verb\n * that did nothing when clicked. Measured on `ProductOffering`'s `publish`,\n * which passed every `@r10c/slices` invariant and appeared nowhere.\n *\n * Resolving means the record changed, and the page reloads it: a verb that\n * moves a lifecycle leaves the form showing the old state otherwise, which\n * reads as \"the button did nothing\". Rejecting surfaces through the form's\n * own error slot, so a `409` from an illegal transition renders as the coded\n * message the catalog holds rather than a thrown promise nobody catches.\n */\n readonly runUseCase?: (key: string, id: EntityId) => Promise<void>;\n}\n\n/**\n * Props both hosts pass. A route host passes none — the slug comes from the URL\n * and a save returns to the list; the workspace tab host passes all of them.\n */\nexport interface EntityCrudSingleViewProps {\n /** Defaults to the `[slug]` route param. */\n readonly slug?: string;\n /** Defaults to navigating back to the list. */\n readonly onSaved?: () => void;\n readonly onDeleted?: () => void;\n /**\n * Where to autosave the edit. Supplying it is the whole opt-in: the form\n * seeds from the persisted draft, writes to it on every edit, and the page\n * clears it once a save or delete commits.\n *\n * A route host omits it and stays ephemeral, which is the rule stated as a\n * missing prop rather than as a flag — there is no value of \"absent\" that\n * accidentally starts persisting.\n */\n readonly draft?: EntityDraftStore;\n}\n\n/**\n * What the factory returns: a **named descriptor**, not four loose components.\n *\n * The identity fields are here so the workspace registry and the nav are derived\n * from a list of these rather than from the hand-written const maps they replace\n * (#141).\n */\nexport interface EntityCrud<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** `@entity({ key })`, which is also `catalogKey` and the tab address payload. */\n readonly entityKey: EntityCatalogKey;\n /**\n * `@entity({ labelKey })` — how one record is named. A record tab's title.\n *\n * Read off the decorator rather than passed, so the tab strip, the nav and the\n * search results all name the entity from the one place it names itself.\n */\n readonly entityLabelKey: string;\n /** `@entity({ pluralKey })` — how the collection is named. A list tab's title. */\n readonly entityPluralKey: string;\n readonly basePath: string;\n readonly ListPage: () => ReactElement;\n readonly SingleViewPage: (props?: EntityCrudSingleViewProps) => ReactElement;\n}\n\n/**\n * A picker's transport with its context tags erased.\n *\n * The factory cannot preserve one type parameter per link — the pages that\n * build these know their adapters, the generated form only forwards them — and\n * `EntityForm.linkSources` is already `Record<string, EntityLinkSource<any>>`\n * for the same reason.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyEntityLinkSourceConfig = EntityLinkSourceConfig<Entity, any>;\n\n/**\n * One picker, resolved: the descriptor computed once at factory time, and the\n * use-cases plus adapter context the page builds per render.\n */\nexport interface EntityCrudLinkSource {\n readonly field: string;\n readonly descriptor: EntityFieldDescriptor;\n readonly config: AnyEntityLinkSourceConfig;\n}\n"],"names":[],"mappings":"AAmRA;;;CAGC,GACD,WAIC"}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/make-entity-crud.types.ts"],"sourcesContent":["import type { EntityRepositoryTag } from '@entifix/business';\nimport type {\n BulkOutcome,\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityMetadataSource,\n EntitySelection,\n} from '@entifix/core';\nimport type {\n EntityDraftStore,\n EntityLinkSourceConfig,\n} from '@entifix/react-integration';\nimport type { Context } from 'effect/Context';\nimport type { CustomTypeOptions } from 'i18next';\nimport type { ReactElement, ReactNode } from 'react';\n\n/**\n * The `entity` catalog keys that carry **both** form titles, a `label` and a\n * `plural`.\n *\n * Derived from the Spanish catalog rather than written out, so the union is the\n * catalog: an entity whose subtree has no `form.editTitle`/`form.newTitle`\n * cannot be handed to {@link makeEntityCrud} at all, and adding a reference\n * entity therefore starts by adding its copy to both locales. That is the whole\n * i18n gate for this factory — a host's catalog check typically scans its\n * domain packages for `@useCase()` decorators and never `.tsx`, so a runtime\n * key here would be checked by nothing.\n *\n * `label`/`plural` joined the condition when {@link EntityCrud} began carrying\n * them, so that a workspace tab and a nav entry derived from this descriptor\n * name the entity in the catalog's own words. They are part of the **type**\n * rather than a runtime assertion in the factory, because the catalog is\n * statically known here: a missing `plural` should fail at the call site that\n * generates the screens, not on the render that first tries to title a tab.\n *\n * Keeping the key typed is what lets the generated form call `useT('entity')`\n * with a template literal and still fail the build on a typo, instead of\n * reaching for the `useTranslateKey` escape hatch that authored copy must not\n * use. `AccountLabelKey` in `../session/account-links.ts` is the same trick\n * written out by hand; this one cannot drift because it is computed.\n */\nexport type EntityCatalogKey<\n TEntityCatalog = EntityCatalogOf<TypeOptionsResources>,\n> = [TEntityCatalog] extends [never]\n ? // No host augmentation in this compilation — this package's own specs, or\n // an adopter who has not declared one. The factory still works; the key is\n // simply unchecked, which is the same trade `useTranslateKey` makes.\n string\n : {\n [K in keyof TEntityCatalog]: TEntityCatalog[K] extends {\n form: { editTitle: string; newTitle: string };\n label: string;\n plural: string;\n }\n ? K\n : never;\n }[keyof TEntityCatalog] &\n string;\n\n/**\n * The host's `entity` namespace, read back out of its own `i18next`\n * augmentation.\n *\n * ⚠️ **The `entity` catalog is the host's, not entifix's.** It names *their*\n * entities — a parameter, an offering, a sales channel — so a framework that\n * shipped one would be shipping an application's product copy. Reading it back\n * through `CustomTypeOptions` keeps the key union computed rather than written\n * out, which is what stops it drifting, while leaving the catalog where it\n * belongs. A host that declares no augmentation gets `string`, and the factory\n * still works with the gate relaxed.\n */\ntype TypeOptionsResources = CustomTypeOptions extends { resources: infer R }\n ? R\n : never;\n\ntype EntityCatalogOf<TResources> = TResources extends { entity: infer E }\n ? E\n : never;\n\n/**\n * A relation the generated form edits with a picker.\n *\n * The target is named here rather than derived, because the common case is a\n * **plain foreign key**: when the target lives in another slice's store a typed\n * `EntityLink` is both an illegal import and a cross-store join, so the member\n * is a bare `string` (`ProductSpecification.brandId` into `catalog-reference`)\n * and its `@accessor()` has no vocabulary for \"the target's name\". Hence\n * `labelProperty`/`searchProperty` are stated at the call site instead of being\n * left to `describeEntityColumns`' `'name'` default, which happens to be right\n * today and states nothing.\n *\n * `searchProperty` must be `filterable` on the **target** or the service answers\n * `400`; `useEntityLinkSource` throws at render rather than letting that surface\n * as an empty suggestion list.\n */\nexport interface EntityCrudLink<TAdapters> {\n /** The owning entity's accessor name, e.g. `'brandId'`. */\n readonly field: string;\n readonly entityConstructor: EntityConstructor<Entity>;\n /**\n * Which adapter holds the **target's** repository. Often a different service\n * than the record's own: `catalog-reference` is another slice's store, so\n * resolving an id goes through that domain's own read path.\n */\n readonly repository: AdapterKey<TAdapters, Context<EntityRepositoryTag>>;\n /** Defaults to `'name'`. */\n readonly labelProperty?: string;\n /** Defaults to `'name'`. Must be `filterable` on the target. */\n readonly searchProperty?: string;\n}\n\n/** The keys of `TAdapters` whose value is assignable to `TValue`. */\nexport type AdapterKey<TAdapters, TValue> = {\n [K in keyof TAdapters]: TAdapters[K] extends TValue ? K : never;\n}[keyof TAdapters];\n\nexport interface EntityCrudOptions<TAdapters, TEntity extends Entity> {\n /** The domain shell's adapters hook, e.g. `useMarketplaceAdminAdapters`. */\n readonly useAdapters: () => TAdapters;\n /** Route prefix for both pages, e.g. `'/catalog/product-brand'`. */\n readonly basePath: string;\n /**\n * The entity's `entity` catalog key. Asserted against `@entity({ key })` at\n * factory time — the two are the same string by convention\n * (`entity:product-brand.label` mirrors `key: 'product-brand'`), and letting\n * them drift would title a form after a different entity.\n */\n readonly catalogKey: EntityCatalogKey;\n /** Which adapter holds this entity's own repository. */\n readonly repository: AdapterKey<TAdapters, Context<EntityRepositoryTag>>;\n /**\n * Members the form keeps but does not show — `id` always, plus anything a\n * transaction assigns. A hidden member still round-trips: it is dropped from\n * the rendered fields, not from the draft, so `reconstructEntity` carries it\n * back out of the seeded values.\n */\n readonly hiddenFields?: readonly string[];\n /** `<EntityColumn>` overrides for the list, for presentation metadata cannot express. */\n readonly columns?: ReactNode;\n /**\n * Extra controls beside the table's own — an `<EntityTableToolbar>`.\n *\n * Its own option rather than more `columns`, though both end up as\n * `EntityTable` children and the slots are sorted by component identity: a\n * toolbar filed under a columns prop is a name that lies, and the first thing\n * to go here is a link into a wizard, which is not a column by any reading.\n */\n readonly toolbar?: ReactNode;\n /** Frozen at factory time — see `use-entity-link-sources.ts`. */\n readonly links?: readonly EntityCrudLink<TAdapters>[];\n /**\n * Where the entity's served affordances come from — `GET /api/<entity>/$metadata`.\n *\n * Supplying it is what turns on every served affordance:\n * Save and Delete filtered against the caller's real grants, the declared\n * verbs on the form, the row overflow menu, and the bulk bar. Omitting it\n * keeps the pre-0026 behaviour exactly, which is what every generated catalog\n * rendered before this option existed.\n *\n * A `EntityMetadataSource` rather than a URL because the two hosts reach\n * their service differently — one through the config-driven REST adapters,\n * one through a shell's own same-origin routes.\n */\n readonly metadataSource?: EntityMetadataSource;\n /**\n * Which adapter holds this entity's {@link EntityMetadataSource} — the same\n * source as {@link metadataSource}, resolved from the host's composition\n * instead of built by the caller.\n *\n * Prefer it: a source is a transport, and naming it by key keeps the module\n * that generates the screens free of the adapter that fetches, exactly as\n * {@link repository} already does for records. Supplying both is a build\n * error — two answers to where the document comes from.\n */\n readonly metadata?: AdapterKey<TAdapters, EntityMetadataSource>;\n /**\n * Runs a `collection`-bound verb over a selection, and reports **per row**.\n *\n * Supplying it opts the list into the selection column and the bulk bar.\n * It is separate from `metadataSource` because the two are independent: an\n * entity can declare no collection verb and still want permission-filtered\n * Save, and the endpoint a verb posts to is the shell's knowledge, not\n * metadata's.\n */\n readonly runBulkUseCase?: (\n key: string,\n selection: EntitySelection<TEntity>,\n ) => Promise<readonly BulkOutcome[]>;\n /**\n * Runs an `entity`-bound verb on the record the single view is showing.\n *\n * The sibling of {@link runBulkUseCase}, and it had to exist for the same\n * reason: `EntityForm` has rendered these buttons for a long time, and\n * `EntityCrudForm` has accepted an `onUseCase` for as long — but nothing ever\n * passed one, so a generated screen showed a declared, granted, served verb\n * that did nothing when clicked. Measured on `ProductOffering`'s `publish`,\n * which passed every check its host's register ran and appeared nowhere.\n *\n * Resolving means the record changed, and the page reloads it: a verb that\n * moves a lifecycle leaves the form showing the old state otherwise, which\n * reads as \"the button did nothing\". Rejecting surfaces through the form's\n * own error slot, so a `409` from an illegal transition renders as the coded\n * message the catalog holds rather than a thrown promise nobody catches.\n */\n readonly runUseCase?: (key: string, id: EntityId) => Promise<void>;\n}\n\n/**\n * Props both hosts pass. A route host passes none — the slug comes from the URL\n * and a save returns to the list; the workspace tab host passes all of them.\n */\nexport interface EntityCrudSingleViewProps {\n /** Defaults to the `[slug]` route param. */\n readonly slug?: string;\n /** Defaults to navigating back to the list. */\n readonly onSaved?: () => void;\n readonly onDeleted?: () => void;\n /**\n * Where to autosave the edit. Supplying it is the whole opt-in: the form\n * seeds from the persisted draft, writes to it on every edit, and the page\n * clears it once a save or delete commits.\n *\n * A route host omits it and stays ephemeral, which is the rule stated as a\n * missing prop rather than as a flag — there is no value of \"absent\" that\n * accidentally starts persisting.\n */\n readonly draft?: EntityDraftStore;\n}\n\n/**\n * What the factory returns: a **named descriptor**, not four loose components.\n *\n * The identity fields are here so the workspace registry and the nav are derived\n * from a list of these rather than from the hand-written const maps they replace.\n */\nexport interface EntityCrud<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** `@entity({ key })`, which is also `catalogKey` and the tab address payload. */\n readonly entityKey: EntityCatalogKey;\n /**\n * `@entity({ labelKey })` — how one record is named. A record tab's title.\n *\n * Read off the decorator rather than passed, so the tab strip, the nav and the\n * search results all name the entity from the one place it names itself.\n */\n readonly entityLabelKey: string;\n /** `@entity({ pluralKey })` — how the collection is named. A list tab's title. */\n readonly entityPluralKey: string;\n readonly basePath: string;\n readonly ListPage: () => ReactElement;\n readonly SingleViewPage: (props?: EntityCrudSingleViewProps) => ReactElement;\n}\n\n/**\n * A picker's transport with its context tags erased.\n *\n * The factory cannot preserve one type parameter per link — the pages that\n * build these know their adapters, the generated form only forwards them — and\n * `EntityForm.linkSources` is already `Record<string, EntityLinkSource<any>>`\n * for the same reason.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyEntityLinkSourceConfig = EntityLinkSourceConfig<Entity, any>;\n\n/**\n * One picker, resolved: the descriptor computed once at factory time, and the\n * use-cases plus adapter context the page builds per render.\n */\nexport interface EntityCrudLinkSource {\n readonly field: string;\n readonly descriptor: EntityFieldDescriptor;\n readonly config: AnyEntityLinkSourceConfig;\n}\n"],"names":[],"mappings":"AA0QA;;;CAGC,GACD,WAIC"}
|
|
@@ -16,7 +16,7 @@ export interface PendingNoticeProps {
|
|
|
16
16
|
* `master:<key>` is the list and there is no address for a record that does not
|
|
17
17
|
* exist yet. A badge keyed on a pending record id would therefore be unreachable
|
|
18
18
|
* by construction, which is exactly the "declared, styled, never produced"
|
|
19
|
-
* defect
|
|
19
|
+
* defect the optimistic mutation contract exists to prevent. It renders here instead, where
|
|
20
20
|
* `afterSave()` actually puts the operator.
|
|
21
21
|
*
|
|
22
22
|
* `role="status"`, not `role="alert"` — the `BulkResult` reasoning: an outcome
|
|
@@ -25,7 +25,7 @@ export interface PendingNoticeProps {
|
|
|
25
25
|
* The failure headline is a catalog key and never `entry.reason`, which is free
|
|
26
26
|
* text from whatever threw, in whatever language that code was written in. The
|
|
27
27
|
* reason is kept as secondary, explicitly-unlocalized detail; a translatable
|
|
28
|
-
* failure needs an error *code* on the event, which
|
|
28
|
+
* failure needs an error *code* on the event, which the event does not carry yet.
|
|
29
29
|
*/
|
|
30
30
|
export declare function PendingNotice({ entries, onDismiss, className, }: PendingNoticeProps): import("react").JSX.Element | null;
|
|
31
31
|
//# sourceMappingURL=pending-notice.d.ts.map
|
|
@@ -11,7 +11,7 @@ import { Button, cn, useT } from '@entifix/react-controls/primitives';
|
|
|
11
11
|
* `master:<key>` is the list and there is no address for a record that does not
|
|
12
12
|
* exist yet. A badge keyed on a pending record id would therefore be unreachable
|
|
13
13
|
* by construction, which is exactly the "declared, styled, never produced"
|
|
14
|
-
* defect
|
|
14
|
+
* defect the optimistic mutation contract exists to prevent. It renders here instead, where
|
|
15
15
|
* `afterSave()` actually puts the operator.
|
|
16
16
|
*
|
|
17
17
|
* `role="status"`, not `role="alert"` — the `BulkResult` reasoning: an outcome
|
|
@@ -20,7 +20,7 @@ import { Button, cn, useT } from '@entifix/react-controls/primitives';
|
|
|
20
20
|
* The failure headline is a catalog key and never `entry.reason`, which is free
|
|
21
21
|
* text from whatever threw, in whatever language that code was written in. The
|
|
22
22
|
* reason is kept as secondary, explicitly-unlocalized detail; a translatable
|
|
23
|
-
* failure needs an error *code* on the event, which
|
|
23
|
+
* failure needs an error *code* on the event, which the event does not carry yet.
|
|
24
24
|
*/ export function PendingNotice({ entries, onDismiss, className }) {
|
|
25
25
|
// The `shell` namespace: this copy lives in `shell.ts`, and `useT()` with no
|
|
26
26
|
// argument reads `controls` — which resolves nothing and renders the key.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/pending-notice.tsx"],"sourcesContent":["'use client';\n\nimport { Button, cn, useT } from '@entifix/react-controls/primitives';\nimport type { PendingEntry } from '@entifix/transactions';\n\nexport interface PendingNoticeProps {\n /** This entity's in-flight and failed writes, newest last. */\n entries: readonly PendingEntry[];\n /** Retires a failure the operator has read. */\n onDismiss: (transactionId: string) => void;\n className?: string;\n}\n\n/**\n * What became of the writes this browser started but the server has not\n * finished.\n *\n * ⚠️ **This is the list's surface because the tab strip cannot be.** Only\n * creates are transactional (`create: 'command'` fires on `isCreate` alone), and\n * a create has no workspace tab — `EntityEditorTab` takes a required id, so\n * `master:<key>` is the list and there is no address for a record that does not\n * exist yet. A badge keyed on a pending record id would therefore be unreachable\n * by construction, which is exactly the \"declared, styled, never produced\"\n * defect
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/pending-notice.tsx"],"sourcesContent":["'use client';\n\nimport { Button, cn, useT } from '@entifix/react-controls/primitives';\nimport type { PendingEntry } from '@entifix/transactions';\n\nexport interface PendingNoticeProps {\n /** This entity's in-flight and failed writes, newest last. */\n entries: readonly PendingEntry[];\n /** Retires a failure the operator has read. */\n onDismiss: (transactionId: string) => void;\n className?: string;\n}\n\n/**\n * What became of the writes this browser started but the server has not\n * finished.\n *\n * ⚠️ **This is the list's surface because the tab strip cannot be.** Only\n * creates are transactional (`create: 'command'` fires on `isCreate` alone), and\n * a create has no workspace tab — `EntityEditorTab` takes a required id, so\n * `master:<key>` is the list and there is no address for a record that does not\n * exist yet. A badge keyed on a pending record id would therefore be unreachable\n * by construction, which is exactly the \"declared, styled, never produced\"\n * defect the optimistic mutation contract exists to prevent. It renders here instead, where\n * `afterSave()` actually puts the operator.\n *\n * `role=\"status\"`, not `role=\"alert\"` — the `BulkResult` reasoning: an outcome\n * to read, not an interruption.\n *\n * The failure headline is a catalog key and never `entry.reason`, which is free\n * text from whatever threw, in whatever language that code was written in. The\n * reason is kept as secondary, explicitly-unlocalized detail; a translatable\n * failure needs an error *code* on the event, which the event does not carry yet.\n */\nexport function PendingNotice({\n entries,\n onDismiss,\n className,\n}: PendingNoticeProps) {\n // The `shell` namespace: this copy lives in `shell.ts`, and `useT()` with no\n // argument reads `controls` — which resolves nothing and renders the key.\n const t = useT('shell');\n\n const saving = entries.filter(entry => entry.state === 'pending');\n const failed = entries.filter(entry => entry.state === 'failed');\n\n if (saving.length === 0 && failed.length === 0) return null;\n\n return (\n <div\n role=\"status\"\n aria-label={t('workspace.pendingLabel')}\n data-testid=\"pending-notice\"\n className={cn(\n 'flex flex-col gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n {saving.length > 0 && (\n <span className=\"text-step-sm text-content-muted\">\n {t('workspace.savePending', { count: saving.length })}\n </span>\n )}\n\n {failed.map(entry => (\n <div\n key={entry.transactionId}\n className=\"flex flex-wrap items-center gap-2xs\"\n >\n <span className=\"text-step-sm text-danger\">\n {t('workspace.saveFailed')}\n </span>\n {entry.reason !== undefined && (\n // Untranslated on purpose, and marked as such: it is a server\n // exception string, shown because a reason nobody can read is worse\n // than one in the wrong language.\n <span\n lang=\"und\"\n className=\"text-step-sm text-content-muted\"\n data-testid=\"pending-notice-reason\"\n >\n {entry.reason}\n </span>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onDismiss(entry.transactionId)}\n >\n {t('workspace.saveFailedDismiss')}\n </Button>\n </div>\n ))}\n </div>\n );\n}\n"],"names":["Button","cn","useT","PendingNotice","entries","onDismiss","className","t","saving","filter","entry","state","failed","length","div","role","aria-label","data-testid","span","count","map","reason","undefined","lang","type","variant","size","onClick","transactionId"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,EAAE,EAAEC,IAAI,QAAQ,qCAAqC;AAWtE;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,cAAc,EAC5BC,OAAO,EACPC,SAAS,EACTC,SAAS,EACU;IACnB,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAMC,IAAIL,KAAK;IAEf,MAAMM,SAASJ,QAAQK,MAAM,CAACC,CAAAA,QAASA,MAAMC,KAAK,KAAK;IACvD,MAAMC,SAASR,QAAQK,MAAM,CAACC,CAAAA,QAASA,MAAMC,KAAK,KAAK;IAEvD,IAAIH,OAAOK,MAAM,KAAK,KAAKD,OAAOC,MAAM,KAAK,GAAG,OAAO;IAEvD,qBACE,MAACC;QACCC,MAAK;QACLC,cAAYT,EAAE;QACdU,eAAY;QACZX,WAAWL,GACT,yFACAK;;YAGDE,OAAOK,MAAM,GAAG,mBACf,KAACK;gBAAKZ,WAAU;0BACbC,EAAE,yBAAyB;oBAAEY,OAAOX,OAAOK,MAAM;gBAAC;;YAItDD,OAAOQ,GAAG,CAACV,CAAAA,sBACV,MAACI;oBAECR,WAAU;;sCAEV,KAACY;4BAAKZ,WAAU;sCACbC,EAAE;;wBAEJG,MAAMW,MAAM,KAAKC,aAChB,8DAA8D;wBAC9D,oEAAoE;wBACpE,kCAAkC;sCAClC,KAACJ;4BACCK,MAAK;4BACLjB,WAAU;4BACVW,eAAY;sCAEXP,MAAMW,MAAM;;sCAGjB,KAACrB;4BACCwB,MAAK;4BACLC,SAAQ;4BACRC,MAAK;4BACLC,SAAS,IAAMtB,UAAUK,MAAMkB,aAAa;sCAE3CrB,EAAE;;;mBAxBAG,MAAMkB,aAAa;;;AA8BlC"}
|
|
@@ -13,7 +13,7 @@ export interface EntityAffordances {
|
|
|
13
13
|
* two lines at every call site. An absent source is the hook's own business.
|
|
14
14
|
*
|
|
15
15
|
* Absent metadata is **not** a security posture: it is the shape both surfaces
|
|
16
|
-
* read as "behave exactly as before
|
|
16
|
+
* read as "behave exactly as before affordances were served". The route guard is the
|
|
17
17
|
* authorization boundary.
|
|
18
18
|
*/
|
|
19
19
|
export declare function useEntityAffordances<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, source: EntityMetadataSource | undefined): EntityAffordances;
|
|
@@ -9,7 +9,7 @@ import { useEntityUseCases } from '@entifix/react-integration';
|
|
|
9
9
|
* two lines at every call site. An absent source is the hook's own business.
|
|
10
10
|
*
|
|
11
11
|
* Absent metadata is **not** a security posture: it is the shape both surfaces
|
|
12
|
-
* read as "behave exactly as before
|
|
12
|
+
* read as "behave exactly as before affordances were served". The route guard is the
|
|
13
13
|
* authorization boundary.
|
|
14
14
|
*/ export function useEntityAffordances(entityConstructor, source) {
|
|
15
15
|
const { metadata, isLoading } = useEntityUseCases(entityConstructor, source);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/use-entity-affordances.ts"],"sourcesContent":["'use client';\n\nimport type {\n Entity,\n EntityConstructor,\n EntityMetadataDocument,\n EntityMetadataSource,\n} from '@entifix/core';\nimport { useEntityUseCases } from '@entifix/react-integration';\n\n/** What a surface needs to render an entity's served affordances. */\nexport interface EntityAffordances {\n metadata?: EntityMetadataDocument;\n isMetadataLoading: boolean;\n}\n\n/**\n * Reads what this caller may do with an entity, in the shape a surface spreads.\n *\n * A thin rename over `useEntityUseCases` — `isLoading` becomes\n * `isMetadataLoading` — so a page can spread the result straight into\n * `EntityForm` or `EntityTable` beside its other props, rather than restating\n * two lines at every call site. An absent source is the hook's own business.\n *\n * Absent metadata is **not** a security posture: it is the shape both surfaces\n * read as \"behave exactly as before
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/use-entity-affordances.ts"],"sourcesContent":["'use client';\n\nimport type {\n Entity,\n EntityConstructor,\n EntityMetadataDocument,\n EntityMetadataSource,\n} from '@entifix/core';\nimport { useEntityUseCases } from '@entifix/react-integration';\n\n/** What a surface needs to render an entity's served affordances. */\nexport interface EntityAffordances {\n metadata?: EntityMetadataDocument;\n isMetadataLoading: boolean;\n}\n\n/**\n * Reads what this caller may do with an entity, in the shape a surface spreads.\n *\n * A thin rename over `useEntityUseCases` — `isLoading` becomes\n * `isMetadataLoading` — so a page can spread the result straight into\n * `EntityForm` or `EntityTable` beside its other props, rather than restating\n * two lines at every call site. An absent source is the hook's own business.\n *\n * Absent metadata is **not** a security posture: it is the shape both surfaces\n * read as \"behave exactly as before affordances were served\". The route guard is the\n * authorization boundary.\n */\nexport function useEntityAffordances<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n source: EntityMetadataSource | undefined,\n): EntityAffordances {\n const { metadata, isLoading } = useEntityUseCases(entityConstructor, source);\n\n return { metadata, isMetadataLoading: isLoading };\n}\n"],"names":["useEntityUseCases","useEntityAffordances","entityConstructor","source","metadata","isLoading","isMetadataLoading"],"mappings":"AAAA;AAQA,SAASA,iBAAiB,QAAQ,6BAA6B;AAQ/D;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,qBACdC,iBAA6C,EAC7CC,MAAwC;IAExC,MAAM,EAAEC,QAAQ,EAAEC,SAAS,EAAE,GAAGL,kBAAkBE,mBAAmBC;IAErE,OAAO;QAAEC;QAAUE,mBAAmBD;IAAU;AAClD"}
|
|
@@ -28,8 +28,8 @@ export interface EntityBulkTableProps<TEntity extends Entity> {
|
|
|
28
28
|
* very navigation used to add to it would make "select rows across pages"
|
|
29
29
|
* impossible.
|
|
30
30
|
*
|
|
31
|
-
* Two behaviours worth not re-deriving. **The selection survives the action
|
|
32
|
-
*
|
|
31
|
+
* Two behaviours worth not re-deriving. **The selection survives the action**:
|
|
32
|
+
* after a run the rows stay ticked, because the operator's next act is
|
|
33
33
|
* usually to retry the failures or to run a second verb on the same rows, and
|
|
34
34
|
* re-selecting forty rows to do it is the whole cost the bulk bar exists to
|
|
35
35
|
* remove. And **a retry re-runs only the failures**, as an `ids` selection
|
|
@@ -15,8 +15,8 @@ import { useCallback, useState } from 'react';
|
|
|
15
15
|
* very navigation used to add to it would make "select rows across pages"
|
|
16
16
|
* impossible.
|
|
17
17
|
*
|
|
18
|
-
* Two behaviours worth not re-deriving. **The selection survives the action
|
|
19
|
-
*
|
|
18
|
+
* Two behaviours worth not re-deriving. **The selection survives the action**:
|
|
19
|
+
* after a run the rows stay ticked, because the operator's next act is
|
|
20
20
|
* usually to retry the failures or to run a second verb on the same rows, and
|
|
21
21
|
* re-selecting forty rows to do it is the whole cost the bulk bar exists to
|
|
22
22
|
* remove. And **a retry re-runs only the failures**, as an `ids` selection
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/use-entity-bulk.ts"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n emptySelection,\n type Entity,\n type EntityId,\n type EntitySelection,\n} from '@entifix/core';\nimport { useCallback, useState } from 'react';\n\n/**\n * The code a row carries when the *request* failed rather than the row.\n *\n * `unexpected` is the shared fallback the `errors` catalog already ships, and\n * it is the honest label: what went wrong is not something this row did.\n */\nconst BULK_REQUEST_FAILED = 'unexpected';\n\nexport interface UseEntityBulkOptions<TEntity extends Entity> {\n /**\n * Runs one `collection`-bound verb. Absent means this listing has no bulk\n * actions, and no selection column is rendered at all.\n */\n readonly run?: (\n key: string,\n selection: EntitySelection<TEntity>,\n ) => Promise<readonly BulkOutcome[]>;\n /** Called after a run, so the listing can re-read the rows it just changed. */\n readonly onCompleted?: () => void;\n /** The whole request failed — a transport error, not a per-row outcome. */\n readonly onError?: (error: unknown) => void;\n}\n\n/** The props {@link useEntityBulk} contributes to `EntityTable`. */\nexport interface EntityBulkTableProps<TEntity extends Entity> {\n selection?: EntitySelection<TEntity>;\n onSelectionChange?: (selection: EntitySelection<TEntity>) => void;\n onBulkUseCase?: (key: string, selection: EntitySelection<TEntity>) => void;\n bulkOutcomes?: readonly BulkOutcome[];\n onBulkDismiss?: () => void;\n onBulkRetry?: (ids: EntityId[]) => void;\n isBulkRunning?: boolean;\n}\n\n/**\n * Owns a listing's selection and the result of the last bulk run.\n *\n * The selection lives here rather than inside `EntityTable` because it has to\n * survive pagination, and the page owns the pager — a selection reset by the\n * very navigation used to add to it would make \"select rows across pages\"\n * impossible.\n *\n * Two behaviours worth not re-deriving. **The selection survives the action
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/use-entity-bulk.ts"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n emptySelection,\n type Entity,\n type EntityId,\n type EntitySelection,\n} from '@entifix/core';\nimport { useCallback, useState } from 'react';\n\n/**\n * The code a row carries when the *request* failed rather than the row.\n *\n * `unexpected` is the shared fallback the `errors` catalog already ships, and\n * it is the honest label: what went wrong is not something this row did.\n */\nconst BULK_REQUEST_FAILED = 'unexpected';\n\nexport interface UseEntityBulkOptions<TEntity extends Entity> {\n /**\n * Runs one `collection`-bound verb. Absent means this listing has no bulk\n * actions, and no selection column is rendered at all.\n */\n readonly run?: (\n key: string,\n selection: EntitySelection<TEntity>,\n ) => Promise<readonly BulkOutcome[]>;\n /** Called after a run, so the listing can re-read the rows it just changed. */\n readonly onCompleted?: () => void;\n /** The whole request failed — a transport error, not a per-row outcome. */\n readonly onError?: (error: unknown) => void;\n}\n\n/** The props {@link useEntityBulk} contributes to `EntityTable`. */\nexport interface EntityBulkTableProps<TEntity extends Entity> {\n selection?: EntitySelection<TEntity>;\n onSelectionChange?: (selection: EntitySelection<TEntity>) => void;\n onBulkUseCase?: (key: string, selection: EntitySelection<TEntity>) => void;\n bulkOutcomes?: readonly BulkOutcome[];\n onBulkDismiss?: () => void;\n onBulkRetry?: (ids: EntityId[]) => void;\n isBulkRunning?: boolean;\n}\n\n/**\n * Owns a listing's selection and the result of the last bulk run.\n *\n * The selection lives here rather than inside `EntityTable` because it has to\n * survive pagination, and the page owns the pager — a selection reset by the\n * very navigation used to add to it would make \"select rows across pages\"\n * impossible.\n *\n * Two behaviours worth not re-deriving. **The selection survives the action**:\n * after a run the rows stay ticked, because the operator's next act is\n * usually to retry the failures or to run a second verb on the same rows, and\n * re-selecting forty rows to do it is the whole cost the bulk bar exists to\n * remove. And **a retry re-runs only the failures**, as an `ids` selection\n * built from the outcomes — never the original selection, which would redo the\n * successful rows and, for a `matching` selection, re-resolve a filter whose\n * answer has just changed underneath it.\n */\nexport function useEntityBulk<TEntity extends Entity>({\n run,\n onCompleted,\n onError,\n}: UseEntityBulkOptions<TEntity>): {\n tableProps: EntityBulkTableProps<TEntity>;\n} {\n const [selection, setSelection] = useState<EntitySelection<TEntity>>(() =>\n emptySelection<TEntity>(),\n );\n const [outcomes, setOutcomes] = useState<readonly BulkOutcome[] | undefined>(\n undefined,\n );\n const [isRunning, setRunning] = useState(false);\n\n const execute = useCallback(\n async (key: string, target: EntitySelection<TEntity>) => {\n // Unreachable through the UI: the handlers that call this are only\n // exposed when `run` exists. It is here because a `useCallback` cannot\n // be declared conditionally and TypeScript still has to narrow.\n /* v8 ignore next */\n if (!run) return;\n setRunning(true);\n // The previous result is cleared as the new run starts: leaving it up\n // would show last run's failures beside this run's spinner, and they\n // are not the same rows.\n setOutcomes(undefined);\n try {\n setOutcomes(await run(key, target));\n } catch (error) {\n // The *request* failed — the network, a `403`, a `500` — which is not a\n // per-row outcome. Caught rather than left to reject: `void execute(…)`\n // at the call site would otherwise make every failed bulk run an\n // unhandled promise rejection, which is invisible in the UI and noisy\n // in the console.\n //\n // When the rows are known it is reported as every row failing, because\n // that is what happened, and it reuses the surface that already\n // attributes failures per row. A `matching` selection has no id list to\n // attribute to — that is the whole point of the mode — so it reports\n // nothing here and the error goes to the caller instead.\n if (target.mode === 'ids') {\n setOutcomes(\n // `Array.from`, never a spread — see `toWireSelection`: the loose\n // SWC helper wraps a `Set` instead of iterating it.\n Array.from(target.ids).map(id => ({\n id,\n ok: false,\n code: BULK_REQUEST_FAILED,\n })),\n );\n }\n onError?.(error);\n } finally {\n setRunning(false);\n onCompleted?.();\n }\n },\n [run, onCompleted, onError],\n );\n\n const [lastKey, setLastKey] = useState<string | undefined>(undefined);\n\n const onBulkUseCase = useCallback(\n (key: string, target: EntitySelection<TEntity>) => {\n setLastKey(key);\n void execute(key, target);\n },\n [execute],\n );\n\n const onBulkRetry = useCallback(\n (ids: EntityId[]) => {\n if (lastKey === undefined) return;\n void execute(lastKey, { mode: 'ids', ids: new Set(ids) });\n },\n [execute, lastKey],\n );\n\n if (!run) return { tableProps: {} };\n\n return {\n tableProps: {\n selection,\n onSelectionChange: setSelection,\n onBulkUseCase,\n bulkOutcomes: outcomes,\n onBulkDismiss: () => setOutcomes(undefined),\n onBulkRetry,\n isBulkRunning: isRunning,\n },\n };\n}\n"],"names":["emptySelection","useCallback","useState","BULK_REQUEST_FAILED","useEntityBulk","run","onCompleted","onError","selection","setSelection","outcomes","setOutcomes","undefined","isRunning","setRunning","execute","key","target","error","mode","Array","from","ids","map","id","ok","code","lastKey","setLastKey","onBulkUseCase","onBulkRetry","Set","tableProps","onSelectionChange","bulkOutcomes","onBulkDismiss","isBulkRunning"],"mappings":"AAAA;AAEA,SAEEA,cAAc,QAIT,gBAAgB;AACvB,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAQ;AAE9C;;;;;CAKC,GACD,MAAMC,sBAAsB;AA4B5B;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,cAAsC,EACpDC,GAAG,EACHC,WAAW,EACXC,OAAO,EACuB;IAG9B,MAAM,CAACC,WAAWC,aAAa,GAAGP,SAAmC,IACnEF;IAEF,MAAM,CAACU,UAAUC,YAAY,GAAGT,SAC9BU;IAEF,MAAM,CAACC,WAAWC,WAAW,GAAGZ,SAAS;IAEzC,MAAMa,UAAUd,YACd,OAAOe,KAAaC;QAClB,mEAAmE;QACnE,uEAAuE;QACvE,gEAAgE;QAChE,kBAAkB,GAClB,IAAI,CAACZ,KAAK;QACVS,WAAW;QACX,sEAAsE;QACtE,qEAAqE;QACrE,yBAAyB;QACzBH,YAAYC;QACZ,IAAI;YACFD,YAAY,MAAMN,IAAIW,KAAKC;QAC7B,EAAE,OAAOC,OAAO;YACd,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,sEAAsE;YACtE,kBAAkB;YAClB,EAAE;YACF,uEAAuE;YACvE,gEAAgE;YAChE,wEAAwE;YACxE,qEAAqE;YACrE,yDAAyD;YACzD,IAAID,OAAOE,IAAI,KAAK,OAAO;gBACzBR,YACE,kEAAkE;gBAClE,oDAAoD;gBACpDS,MAAMC,IAAI,CAACJ,OAAOK,GAAG,EAAEC,GAAG,CAACC,CAAAA,KAAO,CAAA;wBAChCA;wBACAC,IAAI;wBACJC,MAAMvB;oBACR,CAAA;YAEJ;YACAI,2BAAAA,QAAUW;QACZ,SAAU;YACRJ,WAAW;YACXR,+BAAAA;QACF;IACF,GACA;QAACD;QAAKC;QAAaC;KAAQ;IAG7B,MAAM,CAACoB,SAASC,WAAW,GAAG1B,SAA6BU;IAE3D,MAAMiB,gBAAgB5B,YACpB,CAACe,KAAaC;QACZW,WAAWZ;QACX,KAAKD,QAAQC,KAAKC;IACpB,GACA;QAACF;KAAQ;IAGX,MAAMe,cAAc7B,YAClB,CAACqB;QACC,IAAIK,YAAYf,WAAW;QAC3B,KAAKG,QAAQY,SAAS;YAAER,MAAM;YAAOG,KAAK,IAAIS,IAAIT;QAAK;IACzD,GACA;QAACP;QAASY;KAAQ;IAGpB,IAAI,CAACtB,KAAK,OAAO;QAAE2B,YAAY,CAAC;IAAE;IAElC,OAAO;QACLA,YAAY;YACVxB;YACAyB,mBAAmBxB;YACnBoB;YACAK,cAAcxB;YACdyB,eAAe,IAAMxB,YAAYC;YACjCkB;YACAM,eAAevB;QACjB;IACF;AACF"}
|
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
* They ship from `@entifix/next-shell/server` because a route handler must
|
|
6
6
|
* not come from the `"use client"` bundle.
|
|
7
7
|
*/
|
|
8
|
+
import type { ConfigurationSource } from '../config/configuration-source.js';
|
|
8
9
|
export interface HealthRouteOptions {
|
|
9
10
|
/** The app's package name, echoed back so a probe says what it reached. */
|
|
10
11
|
readonly app: string;
|
|
11
|
-
/**
|
|
12
|
-
readonly
|
|
13
|
-
/** The app's config key, i.e. the `:service` in `GET /api/config/:service`. */
|
|
14
|
-
readonly configKey: string;
|
|
12
|
+
/** The configuration readiness checks — the same source `/api/config` reads. */
|
|
13
|
+
readonly configuration: ConfigurationSource;
|
|
15
14
|
}
|
|
16
15
|
/**
|
|
17
16
|
* Build the `GET` handlers for `/api/health`, `/api/health/live` and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../../../src/lib/health/health-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../../../src/lib/health/health-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AA+B7E,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;CAC7C;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,kBAAkB;IAqB1D,4DAA4D;;IAG5D,6BAA6B;;IAQ7B,8BAA8B;;CAejC,CAAC"}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* They ship from `@entifix/next-shell/server` because a route handler must
|
|
6
6
|
* not come from the `"use client"` bundle.
|
|
7
|
-
*/
|
|
8
|
-
/** How long a readiness result is reused, so probes cannot hammer config-service. */ const READY_CACHE_MS = 1000;
|
|
7
|
+
*/ /** How long a readiness result is reused, so probes cannot hammer config-service. */ const READY_CACHE_MS = 1000;
|
|
9
8
|
/** A readiness check that hangs is a readiness check that failed. */ const READY_TIMEOUT_MS = 2000;
|
|
10
9
|
/**
|
|
11
10
|
* Which build is answering: `next dev` or `next start`.
|
|
@@ -38,15 +37,14 @@
|
|
|
38
37
|
let cached = null;
|
|
39
38
|
const checkConfig = async ()=>{
|
|
40
39
|
try {
|
|
41
|
-
// The
|
|
42
|
-
//
|
|
43
|
-
// reads as "degraded" — permanently, for an app that is in fact fine.
|
|
44
|
-
const
|
|
40
|
+
// The source's headers are not optional here: a configuration service
|
|
41
|
+
// that gates its lookup answers `401` without them, which a readiness
|
|
42
|
+
// probe reads as "degraded" — permanently, for an app that is in fact fine.
|
|
43
|
+
const { url, headers } = options.configuration();
|
|
44
|
+
const response = await fetch(url, {
|
|
45
45
|
signal: AbortSignal.timeout(READY_TIMEOUT_MS),
|
|
46
46
|
cache: 'no-store',
|
|
47
|
-
headers
|
|
48
|
-
[SERVICE_TOKEN_HEADER]: serviceToken()
|
|
49
|
-
}
|
|
47
|
+
headers
|
|
50
48
|
});
|
|
51
49
|
return response.ok;
|
|
52
50
|
} catch (unused) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/health/health-routes.ts"],"sourcesContent":["/**\n * Probe route handlers for the Next apps, mirroring what\n * `@entifix/service-shell` mounts on the backends.\n *\n * They ship from `@entifix/next-shell/server` because a route handler must\n * not come from the `\"use client\"` bundle.\n */\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/health/health-routes.ts"],"sourcesContent":["/**\n * Probe route handlers for the Next apps, mirroring what\n * `@entifix/service-shell` mounts on the backends.\n *\n * They ship from `@entifix/next-shell/server` because a route handler must\n * not come from the `\"use client\"` bundle.\n */\n\nimport type { ConfigurationSource } from '../config/configuration-source.js';\n\n/** How long a readiness result is reused, so probes cannot hammer config-service. */\nconst READY_CACHE_MS = 1_000;\n\n/** A readiness check that hangs is a readiness check that failed. */\nconst READY_TIMEOUT_MS = 2_000;\n\n/**\n * Which build is answering: `next dev` or `next start`.\n *\n * Liveness carries it because an e2e run needs to know *which artifact* it\n * attached to. Playwright reuses whatever already listens on the app's port, so\n * a suite started while a dev server is up tests a different bundle against a\n * different backend than the one it claims to — see `assertExpectedServer` in\n * `@entifix/testing-e2e/playwright`.\n *\n * On liveness rather than readiness because liveness is the one endpoint that\n * answers from the process alone, before config-service or any backend is up.\n *\n * Not an information leak worth avoiding: a dev build already announces itself\n * in its asset paths long before anything asks this endpoint.\n */\nconst buildMode = (): 'development' | 'production' =>\n process.env.NODE_ENV === 'production' ? 'production' : 'development';\n\ninterface ReadyState {\n readonly at: number;\n readonly ready: boolean;\n}\n\nexport interface HealthRouteOptions {\n /** The app's package name, echoed back so a probe says what it reached. */\n readonly app: string;\n /** The configuration readiness checks — the same source `/api/config` reads. */\n readonly configuration: ConfigurationSource;\n}\n\n/**\n * Build the `GET` handlers for `/api/health`, `/api/health/live` and\n * `/api/health/ready`.\n *\n * **Liveness** answers from the process alone — no config-service, no backend,\n * no session. That is what makes it usable as Playwright's `readyPath` and what\n * keeps a dependency blip from getting the app killed by Kubernetes later.\n *\n * **Readiness** checks the one dependency the app cannot render without: its\n * configuration. It deliberately does *not* chain to the domain backend —\n * cascading readiness turns one degraded service into a fleet-wide outage, and\n * a page that renders with a degraded backend is still worth serving.\n */\nexport const createHealthRoutes = (options: HealthRouteOptions) => {\n let cached: ReadyState | null = null;\n\n const checkConfig = async (): Promise<boolean> => {\n try {\n // The source's headers are not optional here: a configuration service\n // that gates its lookup answers `401` without them, which a readiness\n // probe reads as \"degraded\" — permanently, for an app that is in fact fine.\n const { url, headers } = options.configuration();\n const response = await fetch(url, {\n signal: AbortSignal.timeout(READY_TIMEOUT_MS),\n cache: 'no-store',\n headers,\n });\n return response.ok;\n } catch {\n return false;\n }\n };\n\n return {\n /** `GET /api/health` — the original endpoint, unchanged. */\n health: () => Response.json({ status: 'ok', app: options.app }),\n\n /** `GET /api/health/live` */\n live: () =>\n Response.json({\n status: 'live',\n app: options.app,\n mode: buildMode(),\n }),\n\n /** `GET /api/health/ready` */\n ready: async () => {\n const now = Date.now();\n if (cached === null || now - cached.at >= READY_CACHE_MS) {\n cached = { at: now, ready: await checkConfig() };\n }\n\n return cached.ready\n ? Response.json({ status: 'ready', app: options.app })\n : Response.json(\n { status: 'degraded', app: options.app, failing: ['config'] },\n { status: 503 },\n );\n },\n };\n};\n"],"names":["READY_CACHE_MS","READY_TIMEOUT_MS","buildMode","process","env","NODE_ENV","createHealthRoutes","options","cached","checkConfig","url","headers","configuration","response","fetch","signal","AbortSignal","timeout","cache","ok","health","Response","json","status","app","live","mode","ready","now","Date","at","failing"],"mappings":"AAAA;;;;;;CAMC,GAID,mFAAmF,GACnF,MAAMA,iBAAiB;AAEvB,mEAAmE,GACnE,MAAMC,mBAAmB;AAEzB;;;;;;;;;;;;;;CAcC,GACD,MAAMC,YAAY,IAChBC,QAAQC,GAAG,CAACC,QAAQ,KAAK,eAAe,eAAe;AAczD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAACC;IACjC,IAAIC,SAA4B;IAEhC,MAAMC,cAAc;QAClB,IAAI;YACF,sEAAsE;YACtE,sEAAsE;YACtE,4EAA4E;YAC5E,MAAM,EAAEC,GAAG,EAAEC,OAAO,EAAE,GAAGJ,QAAQK,aAAa;YAC9C,MAAMC,WAAW,MAAMC,MAAMJ,KAAK;gBAChCK,QAAQC,YAAYC,OAAO,CAAChB;gBAC5BiB,OAAO;gBACPP;YACF;YACA,OAAOE,SAASM,EAAE;QACpB,EAAE,eAAM;YACN,OAAO;QACT;IACF;IAEA,OAAO;QACL,0DAA0D,GAC1DC,QAAQ,IAAMC,SAASC,IAAI,CAAC;gBAAEC,QAAQ;gBAAMC,KAAKjB,QAAQiB,GAAG;YAAC;QAE7D,2BAA2B,GAC3BC,MAAM,IACJJ,SAASC,IAAI,CAAC;gBACZC,QAAQ;gBACRC,KAAKjB,QAAQiB,GAAG;gBAChBE,MAAMxB;YACR;QAEF,4BAA4B,GAC5ByB,OAAO;YACL,MAAMC,MAAMC,KAAKD,GAAG;YACpB,IAAIpB,WAAW,QAAQoB,MAAMpB,OAAOsB,EAAE,IAAI9B,gBAAgB;gBACxDQ,SAAS;oBAAEsB,IAAIF;oBAAKD,OAAO,MAAMlB;gBAAc;YACjD;YAEA,OAAOD,OAAOmB,KAAK,GACfN,SAASC,IAAI,CAAC;gBAAEC,QAAQ;gBAASC,KAAKjB,QAAQiB,GAAG;YAAC,KAClDH,SAASC,IAAI,CACX;gBAAEC,QAAQ;gBAAYC,KAAKjB,QAAQiB,GAAG;gBAAEO,SAAS;oBAAC;iBAAS;YAAC,GAC5D;gBAAER,QAAQ;YAAI;QAEtB;IACF;AACF,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/lib/i18n/catalog/en.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,cAAc,SAAS,EAAE,KAAK,CAAC;AAE9C,eAAO,MAAM,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/lib/i18n/catalog/en.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,cAAc,SAAS,EAAE,KAAK,CAAC;AAE9C,eAAO,MAAM,KAAK,EAAE,OAmFnB,CAAC"}
|