@entifix/next-shell 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/back-office/back-office-shell.d.ts +1 -1
- package/dist/lib/back-office/back-office-shell.js.map +1 -1
- package/dist/lib/crud/entity-crud-form.d.ts +4 -1
- package/dist/lib/crud/entity-crud-form.d.ts.map +1 -1
- package/dist/lib/crud/entity-crud-form.js +10 -4
- package/dist/lib/crud/entity-crud-form.js.map +1 -1
- package/dist/lib/crud/index.d.ts +1 -0
- package/dist/lib/crud/index.d.ts.map +1 -1
- package/dist/lib/crud/index.js +1 -0
- package/dist/lib/crud/index.js.map +1 -1
- package/dist/lib/crud/make-entity-crud.d.ts.map +1 -1
- package/dist/lib/crud/make-entity-crud.js +27 -3
- package/dist/lib/crud/make-entity-crud.js.map +1 -1
- package/dist/lib/crud/use-crud-render-link.d.ts +13 -0
- package/dist/lib/crud/use-crud-render-link.d.ts.map +1 -0
- package/dist/lib/crud/use-crud-render-link.js +44 -0
- package/dist/lib/crud/use-crud-render-link.js.map +1 -0
- package/dist/lib/i18n/catalog/es.d.ts +1 -1
- package/dist/lib/i18n/catalog/es.js +1 -1
- package/dist/lib/i18n/catalog/es.js.map +1 -1
- package/dist/lib/session/account-links.d.ts +2 -2
- package/dist/lib/session/account-links.js +2 -2
- package/dist/lib/session/account-links.js.map +1 -1
- package/dist/lib/workspace/entity-nav.d.ts +18 -2
- package/dist/lib/workspace/entity-nav.d.ts.map +1 -1
- package/dist/lib/workspace/entity-nav.js +22 -4
- package/dist/lib/workspace/entity-nav.js.map +1 -1
- package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -1
- package/dist/lib/workspace/entity-tab-kind.js +20 -2
- package/dist/lib/workspace/entity-tab-kind.js.map +1 -1
- package/package.json +9 -12
|
@@ -22,7 +22,7 @@ export interface BackOfficeShellProps {
|
|
|
22
22
|
* sources carry entity constructors and handler functions, neither of which
|
|
23
23
|
* survives the server→client boundary, so it cannot be built from serializable
|
|
24
24
|
* props here. The host composes it in a client module of its own and hands it
|
|
25
|
-
* over — which also keeps this
|
|
25
|
+
* over — which also keeps this framework package from naming any domain's
|
|
26
26
|
* entities.
|
|
27
27
|
*/
|
|
28
28
|
commandPalette?: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/back-office/back-office-shell.tsx"],"sourcesContent":["'use client';\n\nimport {\n Button,\n Drawer,\n Sidebar,\n useT,\n useUiPreference,\n useViewportMode,\n} from '@entifix/react-controls';\nimport { usePathname } from 'next/navigation';\nimport { type ReactNode, useState } from 'react';\n\nimport { BackOfficeBreadcrumbs } from './breadcrumbs.js';\nimport type { NavSection } from './nav.js';\nimport { SidebarNav } from './sidebar-nav.js';\n\n/** `namespace:` is prepended by the store → `entifix-ui:back-office:sidebar-collapsed`. */\nconst SIDEBAR_PREF_KEY = 'back-office:sidebar-collapsed';\n\n/**\n * Which domain groups are collapsed, as one keyed record rather than a key per\n * group. `useUiPreference` resolves asynchronously after mount, so N keys would\n * be N first-paint transitions; one read settles the whole sidebar at once.\n */\nconst NAV_GROUPS_PREF_KEY = 'back-office:nav-collapsed-groups';\n\nexport interface BackOfficeShellProps {\n /** Grouped primary navigation. */\n nav: NavSection[];\n /** Brand mark shown at the top of the sidebar. */\n brand: ReactNode;\n children: ReactNode;\n /** Segment → label overrides for the breadcrumb trail. */\n breadcrumbLabels?: Record<string, string>;\n /**\n * Right-aligned slot in the top bar — the account menu. A slot rather than a\n * built-in, because what belongs in it differs per host: the label is the\n * signed-in subject where a layout has resolved a principal, and a bare\n * caption where it has not.\n */\n accountMenu?: ReactNode;\n /**\n * The command palette, trigger and all.\n *\n * A slot for a reason the account menu does not share: the palette's use-case\n * sources carry entity constructors and handler functions, neither of which\n * survives the server→client boundary, so it cannot be built from serializable\n * props here. The host composes it in a client module of its own and hands it\n * over — which also keeps this
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/back-office/back-office-shell.tsx"],"sourcesContent":["'use client';\n\nimport {\n Button,\n Drawer,\n Sidebar,\n useT,\n useUiPreference,\n useViewportMode,\n} from '@entifix/react-controls';\nimport { usePathname } from 'next/navigation';\nimport { type ReactNode, useState } from 'react';\n\nimport { BackOfficeBreadcrumbs } from './breadcrumbs.js';\nimport type { NavSection } from './nav.js';\nimport { SidebarNav } from './sidebar-nav.js';\n\n/** `namespace:` is prepended by the store → `entifix-ui:back-office:sidebar-collapsed`. */\nconst SIDEBAR_PREF_KEY = 'back-office:sidebar-collapsed';\n\n/**\n * Which domain groups are collapsed, as one keyed record rather than a key per\n * group. `useUiPreference` resolves asynchronously after mount, so N keys would\n * be N first-paint transitions; one read settles the whole sidebar at once.\n */\nconst NAV_GROUPS_PREF_KEY = 'back-office:nav-collapsed-groups';\n\nexport interface BackOfficeShellProps {\n /** Grouped primary navigation. */\n nav: NavSection[];\n /** Brand mark shown at the top of the sidebar. */\n brand: ReactNode;\n children: ReactNode;\n /** Segment → label overrides for the breadcrumb trail. */\n breadcrumbLabels?: Record<string, string>;\n /**\n * Right-aligned slot in the top bar — the account menu. A slot rather than a\n * built-in, because what belongs in it differs per host: the label is the\n * signed-in subject where a layout has resolved a principal, and a bare\n * caption where it has not.\n */\n accountMenu?: ReactNode;\n /**\n * The command palette, trigger and all.\n *\n * A slot for a reason the account menu does not share: the palette's use-case\n * sources carry entity constructors and handler functions, neither of which\n * survives the server→client boundary, so it cannot be built from serializable\n * props here. The host composes it in a client module of its own and hands it\n * over — which also keeps this framework package from naming any domain's\n * entities.\n */\n commandPalette?: ReactNode;\n}\n\n/**\n * Back-office page shell: a collapsible sidebar + a top bar carrying\n * breadcrumbs, wrapping the routed content. Composed from the agnostic\n * `Sidebar` primitive — the Next coupling (routing, breadcrumbs) lives here,\n * not in the primitive.\n *\n * **Collapse has two inputs, and only one of them is a preference.** The stored\n * choice is the person's; a `rail`-width viewport forces the compact form\n * whatever they chose. They are kept apart deliberately: writing the forced\n * value back would mean a single visit at a narrow width silently rewrites the\n * choice, and the sidebar comes back collapsed on the desktop the person set it\n * expanded on.\n *\n * Below `rail` the sidebar is not a sidebar at all — it is a drawer, built on\n * Headless UI's `Dialog` for the focus trap, `Escape` handling, backdrop click\n * and focus restoration that a hand-rolled one has to get right and usually does\n * not. There is exactly one `SidebarNav` in the tree either way; rendering a\n * second copy for the drawer is how the two drift.\n */\nexport function BackOfficeShell({\n nav,\n brand,\n children,\n breadcrumbLabels,\n accountMenu,\n commandPalette,\n}: BackOfficeShellProps) {\n const t = useT('shell');\n const mode = useViewportMode();\n const pathname = usePathname();\n const { value: preferCollapsed, setValue: setCollapsed } =\n useUiPreference<boolean>(SIDEBAR_PREF_KEY, false);\n const { value: collapsedGroups, setValue: setCollapsedGroups } =\n useUiPreference<Record<string, boolean>>(NAV_GROUPS_PREF_KEY, {});\n // The drawer remembers *where* it was opened, and is open only while the\n // visitor is still there. A drawer that survives navigation covers the page\n // they just asked for, and closing it from an effect keyed on the route would\n // set state during a passive effect — a cascading render for something that is\n // derivable, since \"still on the page I opened this from\" is a fact about the\n // current render rather than an event to react to.\n const [openedAt, setOpenedAt] = useState<string | null>(null);\n const isDrawerOpen = openedAt !== null && openedAt === pathname;\n const setDrawerOpen = (open: boolean) => setOpenedAt(open ? pathname : null);\n\n // Three modes, one answer. A drawer has the whole viewport's width, so it\n // shows labels whatever the stored preference says — that preference is about\n // how much room the sidebar should take beside the content, and in a drawer\n // there is no beside.\n const collapsed =\n mode === 'rail' ? true : mode === 'compact' ? false : preferCollapsed;\n const toggleGroup = (title: string) =>\n setCollapsedGroups({\n ...collapsedGroups,\n [title]: collapsedGroups[title] !== true,\n });\n\n const navTree = (\n <SidebarNav\n sections={nav}\n collapsed={collapsed}\n collapsedGroups={collapsedGroups}\n onToggleGroup={toggleGroup}\n />\n );\n\n const isCompact = mode === 'compact';\n\n return (\n <Sidebar gap=\"none\" className=\"min-h-screen\">\n {isCompact ? null : (\n <Sidebar.Side\n as=\"aside\"\n width={collapsed ? '4.5rem' : '16rem'}\n className=\"md:sticky md:top-0 md:h-screen md:overflow-y-auto flex flex-col gap-m border-r border-border bg-surface-elevated p-s\"\n >\n <div className=\"flex items-center justify-center px-2xs py-3xs text-step-1 font-semibold text-content\">\n {collapsed ? null : brand}\n </div>\n {navTree}\n </Sidebar.Side>\n )}\n\n <Sidebar.Main as=\"main\" className=\"flex flex-col\">\n <header className=\"flex items-center gap-s border-b border-border bg-surface-elevated px-m py-s\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label={\n isCompact\n ? t('sidebar.openMenu')\n : collapsed\n ? t('sidebar.expand')\n : t('sidebar.collapse')\n }\n // A drawer trigger is not a two-state toggle — it opens a thing that\n // closes itself — so it reports pressed state only where it is one.\n {...(isCompact ? {} : { 'aria-pressed': collapsed })}\n onClick={() =>\n isCompact ? setDrawerOpen(true) : setCollapsed(!preferCollapsed)\n }\n >\n <span aria-hidden=\"true\">☰</span>\n </Button>\n <BackOfficeBreadcrumbs labels={breadcrumbLabels} />\n <div className=\"ml-auto flex items-center gap-2xs\">\n {commandPalette}\n {accountMenu}\n </div>\n </header>\n <div className=\"flex-1 p-m\">{children}</div>\n </Sidebar.Main>\n\n {isCompact && (\n <Drawer\n open={isDrawerOpen}\n onClose={() => setDrawerOpen(false)}\n title={brand}\n closeLabel={t('sidebar.closeMenu')}\n >\n {navTree}\n </Drawer>\n )}\n </Sidebar>\n );\n}\n"],"names":["Button","Drawer","Sidebar","useT","useUiPreference","useViewportMode","usePathname","useState","BackOfficeBreadcrumbs","SidebarNav","SIDEBAR_PREF_KEY","NAV_GROUPS_PREF_KEY","BackOfficeShell","nav","brand","children","breadcrumbLabels","accountMenu","commandPalette","t","mode","pathname","value","preferCollapsed","setValue","setCollapsed","collapsedGroups","setCollapsedGroups","openedAt","setOpenedAt","isDrawerOpen","setDrawerOpen","open","collapsed","toggleGroup","title","navTree","sections","onToggleGroup","isCompact","gap","className","Side","as","width","div","Main","header","variant","size","aria-label","onClick","span","aria-hidden","labels","onClose","closeLabel"],"mappings":"AAAA;;;AAEA,SACEA,MAAM,EACNC,MAAM,EACNC,OAAO,EACPC,IAAI,EACJC,eAAe,EACfC,eAAe,QACV,0BAA0B;AACjC,SAASC,WAAW,QAAQ,kBAAkB;AAC9C,SAAyBC,QAAQ,QAAQ,QAAQ;AAEjD,SAASC,qBAAqB,QAAQ,mBAAmB;AAEzD,SAASC,UAAU,QAAQ,mBAAmB;AAE9C,yFAAyF,GACzF,MAAMC,mBAAmB;AAEzB;;;;CAIC,GACD,MAAMC,sBAAsB;AA8B5B;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,gBAAgB,EAC9BC,GAAG,EACHC,KAAK,EACLC,QAAQ,EACRC,gBAAgB,EAChBC,WAAW,EACXC,cAAc,EACO;IACrB,MAAMC,IAAIhB,KAAK;IACf,MAAMiB,OAAOf;IACb,MAAMgB,WAAWf;IACjB,MAAM,EAAEgB,OAAOC,eAAe,EAAEC,UAAUC,YAAY,EAAE,GACtDrB,gBAAyBM,kBAAkB;IAC7C,MAAM,EAAEY,OAAOI,eAAe,EAAEF,UAAUG,kBAAkB,EAAE,GAC5DvB,gBAAyCO,qBAAqB,CAAC;IACjE,yEAAyE;IACzE,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,8EAA8E;IAC9E,mDAAmD;IACnD,MAAM,CAACiB,UAAUC,YAAY,GAAGtB,SAAwB;IACxD,MAAMuB,eAAeF,aAAa,QAAQA,aAAaP;IACvD,MAAMU,gBAAgB,CAACC,OAAkBH,YAAYG,OAAOX,WAAW;IAEvE,0EAA0E;IAC1E,8EAA8E;IAC9E,4EAA4E;IAC5E,sBAAsB;IACtB,MAAMY,YACJb,SAAS,SAAS,OAAOA,SAAS,YAAY,QAAQG;IACxD,MAAMW,cAAc,CAACC,QACnBR,mBAAmB,aACdD;YACH,CAACS,MAAM,EAAET,eAAe,CAACS,MAAM,KAAK;;IAGxC,MAAMC,wBACJ,KAAC3B;QACC4B,UAAUxB;QACVoB,WAAWA;QACXP,iBAAiBA;QACjBY,eAAeJ;;IAInB,MAAMK,YAAYnB,SAAS;IAE3B,qBACE,MAAClB;QAAQsC,KAAI;QAAOC,WAAU;;YAC3BF,YAAY,qBACX,MAACrC,QAAQwC,IAAI;gBACXC,IAAG;gBACHC,OAAOX,YAAY,WAAW;gBAC9BQ,WAAU;;kCAEV,KAACI;wBAAIJ,WAAU;kCACZR,YAAY,OAAOnB;;oBAErBsB;;;0BAIL,MAAClC,QAAQ4C,IAAI;gBAACH,IAAG;gBAAOF,WAAU;;kCAChC,MAACM;wBAAON,WAAU;;0CAChB,KAACzC;gCACCgD,SAAQ;gCACRC,MAAK;gCACLC,cACEX,YACIpB,EAAE,sBACFc,YACEd,EAAE,oBACFA,EAAE;+BAILoB,YAAY,CAAC,IAAI;gCAAE,gBAAgBN;4BAAU;gCAClDkB,SAAS,IACPZ,YAAYR,cAAc,QAAQN,aAAa,CAACF;0CAGlD,cAAA,KAAC6B;oCAAKC,eAAY;8CAAO;;;0CAE3B,KAAC7C;gCAAsB8C,QAAQtC;;0CAC/B,MAAC6B;gCAAIJ,WAAU;;oCACZvB;oCACAD;;;;;kCAGL,KAAC4B;wBAAIJ,WAAU;kCAAc1B;;;;YAG9BwB,2BACC,KAACtC;gBACC+B,MAAMF;gBACNyB,SAAS,IAAMxB,cAAc;gBAC7BI,OAAOrB;gBACP0C,YAAYrC,EAAE;0BAEbiB;;;;AAKX"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EntifixError, type Entity, type EntityConstructor, type EntityMetadataSource } from '@entifix/core';
|
|
2
|
+
import { type RenderLink } from '@entifix/react-controls';
|
|
2
3
|
import { type EntityDraftStore } from '@entifix/react-integration';
|
|
3
4
|
import type { EntityCatalogKey, EntityCrudLinkSource } from './make-entity-crud.types.js';
|
|
4
5
|
export interface EntityCrudFormProps<TEntity extends Entity> {
|
|
@@ -16,6 +17,8 @@ export interface EntityCrudFormProps<TEntity extends Entity> {
|
|
|
16
17
|
/** Omitted for a create — there is nothing to delete yet. */
|
|
17
18
|
readonly onDelete?: () => void;
|
|
18
19
|
readonly backHref: string;
|
|
20
|
+
/** Renders the Back link; see `useCrudRenderLink`. */
|
|
21
|
+
readonly renderLink?: RenderLink;
|
|
19
22
|
/**
|
|
20
23
|
* Where to autosave this form's draft. Omitted by a route host, which is what
|
|
21
24
|
* keeps a plain route ephemeral; the workspace tab host supplies one.
|
|
@@ -45,5 +48,5 @@ export interface EntityCrudFormProps<TEntity extends Entity> {
|
|
|
45
48
|
* `useEntityForm` owns the draft and seeds it once, so the page keys this
|
|
46
49
|
* component by the record id to reseed when the record arrives.
|
|
47
50
|
*/
|
|
48
|
-
export declare function EntityCrudForm<TEntity extends Entity>({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading, isSaving, isDeleting, error, onSave, onDelete, backHref, draft, metadataSource, onUseCase, }: EntityCrudFormProps<TEntity>): import("react").JSX.Element;
|
|
51
|
+
export declare function EntityCrudForm<TEntity extends Entity>({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading, isSaving, isDeleting, error, onSave, onDelete, backHref, renderLink, draft, metadataSource, onUseCase, }: EntityCrudFormProps<TEntity>): import("react").JSX.Element;
|
|
49
52
|
//# sourceMappingURL=entity-crud-form.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAIrC,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,MAAM;IACzD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,EACrD,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,GACV,EAAE,mBAAmB,CAAC,OAAO,CAAC,+BA8H9B"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
3
3
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
4
|
import { reconstructEntity } from '@entifix/core';
|
|
5
|
-
import { EntityField, EntityForm, useT } from '@entifix/react-controls';
|
|
5
|
+
import { EntityField, EntityForm, useT, useTranslateKey } from '@entifix/react-controls';
|
|
6
6
|
import { useEntityForm } from '@entifix/react-integration';
|
|
7
7
|
import { useEffect, useMemo } from 'react';
|
|
8
8
|
import { useEntityAffordances } from './use-entity-affordances.js';
|
|
@@ -21,13 +21,13 @@ import { useEntityLinkSources } from './use-entity-link-sources.js';
|
|
|
21
21
|
*
|
|
22
22
|
* `useEntityForm` owns the draft and seeds it once, so the page keys this
|
|
23
23
|
* component by the record id to reseed when the record arrives.
|
|
24
|
-
*/ export function EntityCrudForm({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading = false, isSaving = false, isDeleting = false, error, onSave, onDelete, backHref, draft, metadataSource, onUseCase }) {
|
|
24
|
+
*/ export function EntityCrudForm({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading = false, isSaving = false, isDeleting = false, error, onSave, onDelete, backHref, renderLink, draft, metadataSource, onUseCase }) {
|
|
25
25
|
const et = useT('entity');
|
|
26
26
|
const ct = useT('controls');
|
|
27
27
|
// `controls` copy, resolved here and handed down. The integration package
|
|
28
28
|
// owns the validation *rules* and this shell owns the sentences, because a
|
|
29
|
-
// sideways import between the two
|
|
30
|
-
//
|
|
29
|
+
// sideways import between the two T3 packages is an edge the tier register
|
|
30
|
+
// does not declare.
|
|
31
31
|
const validationMessages = useMemo(()=>{
|
|
32
32
|
const say = (failure)=>(field)=>ct(`validation.${failure}`, {
|
|
33
33
|
field
|
|
@@ -41,10 +41,15 @@ import { useEntityLinkSources } from './use-entity-link-sources.js';
|
|
|
41
41
|
}, [
|
|
42
42
|
ct
|
|
43
43
|
]);
|
|
44
|
+
// A runtime-key translator, so a message names the field by its catalog word
|
|
45
|
+
// (`labelKey`) and a schema's keyed message resolves at all. Without it both
|
|
46
|
+
// fell back to untranslated text inside an otherwise Spanish sentence.
|
|
47
|
+
const translateKey = useTranslateKey();
|
|
44
48
|
const form = useEntityForm({
|
|
45
49
|
entityConstructor,
|
|
46
50
|
entity,
|
|
47
51
|
validationMessages,
|
|
52
|
+
translateKey,
|
|
48
53
|
draft,
|
|
49
54
|
onSubmit: (values)=>onSave(reconstructEntity(entityConstructor, values, {
|
|
50
55
|
existing: entity
|
|
@@ -120,6 +125,7 @@ import { useEntityLinkSources } from './use-entity-link-sources.js';
|
|
|
120
125
|
isDeleting: isDeleting,
|
|
121
126
|
error: error,
|
|
122
127
|
backHref: backHref,
|
|
128
|
+
renderLink: renderLink,
|
|
123
129
|
// `entity` is undefined until the record lands, so testing it alone
|
|
124
130
|
// titled a loading edit form "New" and then relabelled it (#139).
|
|
125
131
|
title: et(entity || isLoading ? `${catalogKey}.form.editTitle` : `${catalogKey}.form.newTitle`),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport { EntityField, EntityForm, useT } from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down. The integration package\n // owns the validation *rules* and this shell owns the sentences, because a\n // sideways import between the two `entifix:react` packages is what the\n // boundary rule forbids.\n const validationMessages = useMemo(() => {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n };\n }, [ct]);\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","draft","metadataSource","onUseCase","et","ct","validationMessages","say","failure","field","required","number","date","option","form","onSubmit","values","existing","linkSources","selection","hydrateLink","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport {\n EntityField,\n EntityForm,\n type RenderLink,\n useT,\n useTranslateKey,\n} from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /** Renders the Back link; see `useCrudRenderLink`. */\n readonly renderLink?: RenderLink;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n renderLink,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down. The integration package\n // owns the validation *rules* and this shell owns the sentences, because a\n // sideways import between the two T3 packages is an edge the tier register\n // does not declare.\n const validationMessages = useMemo(() => {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n };\n }, [ct]);\n\n // A runtime-key translator, so a message names the field by its catalog word\n // (`labelKey`) and a schema's keyed message resolves at all. Without it both\n // fell back to untranslated text inside an otherwise Spanish sentence.\n const translateKey = useTranslateKey();\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n translateKey,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n renderLink={renderLink}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useTranslateKey","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","renderLink","draft","metadataSource","onUseCase","et","ct","validationMessages","say","failure","field","required","number","date","option","translateKey","form","onSubmit","values","existing","linkSources","selection","hydrateLink","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
|
package/dist/lib/crud/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './hand-off-write.js';
|
|
|
3
3
|
export * from './make-entity-crud.js';
|
|
4
4
|
export * from './make-entity-crud.types.js';
|
|
5
5
|
export * from './slug.js';
|
|
6
|
+
export * from './use-crud-render-link.js';
|
|
6
7
|
export * from './use-entity-affordances.js';
|
|
7
8
|
export * from './use-entity-bulk.js';
|
|
8
9
|
export * from './use-entity-link-sources.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,WAAW,CAAC;AAC1B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,WAAW,CAAC;AAC1B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC"}
|
package/dist/lib/crud/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './hand-off-write.js';
|
|
|
3
3
|
export * from './make-entity-crud.js';
|
|
4
4
|
export * from './make-entity-crud.types.js';
|
|
5
5
|
export * from './slug.js';
|
|
6
|
+
export * from './use-crud-render-link.js';
|
|
6
7
|
export * from './use-entity-affordances.js';
|
|
7
8
|
export * from './use-entity-bulk.js';
|
|
8
9
|
export * from './use-entity-link-sources.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/crud/index.ts"],"sourcesContent":["export * from './entity-crud-form.js';\nexport * from './hand-off-write.js';\nexport * from './make-entity-crud.js';\nexport * from './make-entity-crud.types.js';\nexport * from './slug.js';\nexport * from './use-entity-affordances.js';\nexport * from './use-entity-bulk.js';\nexport * from './use-entity-link-sources.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB;AACtC,cAAc,sBAAsB;AACpC,cAAc,wBAAwB;AACtC,cAAc,8BAA8B;AAC5C,cAAc,YAAY;AAC1B,cAAc,8BAA8B;AAC5C,cAAc,uBAAuB;AACrC,cAAc,+BAA+B"}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/index.ts"],"sourcesContent":["export * from './entity-crud-form.js';\nexport * from './hand-off-write.js';\nexport * from './make-entity-crud.js';\nexport * from './make-entity-crud.types.js';\nexport * from './slug.js';\nexport * from './use-crud-render-link.js';\nexport * from './use-entity-affordances.js';\nexport * from './use-entity-bulk.js';\nexport * from './use-entity-link-sources.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB;AACtC,cAAc,sBAAsB;AACpC,cAAc,wBAAwB;AACtC,cAAc,8BAA8B;AAC5C,cAAc,YAAY;AAC1B,cAAc,4BAA4B;AAC1C,cAAc,8BAA8B;AAC5C,cAAc,uBAAuB;AACrC,cAAc,+BAA+B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-entity-crud.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/make-entity-crud.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAE/B,KAAK,mBAAmB,EAIzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAGvB,MAAM,eAAe,CAAC;AAQvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"make-entity-crud.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/make-entity-crud.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAE/B,KAAK,mBAAmB,EAIzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAGvB,MAAM,eAAe,CAAC;AAQvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAajC,OAAO,KAAK,EACV,UAAU,EAEV,iBAAiB,EAElB,MAAM,6BAA6B,CAAC;AAUrC;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GACrB,mBAAmB,GAAG,0BAA0B,GAAG,kBAAkB,CAAC;AAExE;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EACxC,QAAQ,EAAE,SAAS,EACnB,aAAa,EAAE,MAAM,SAAS,EAC9B,UAAU,EAAE,MAAM,SAAS,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAS9B;AAwCD,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,EAC9D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,EAAE,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,GAC7C,UAAU,CAAC,OAAO,CAAC,CAmWrB"}
|
|
@@ -11,11 +11,13 @@ import { Context } from 'effect';
|
|
|
11
11
|
import { useParams, useRouter } from 'next/navigation';
|
|
12
12
|
import { useState } from 'react';
|
|
13
13
|
import { useLocaleHref } from '../i18n/index.js';
|
|
14
|
+
import { useEntityNavHost } from '../workspace/entity-nav.js';
|
|
14
15
|
import { pendingFor, pendingRecordsFor, usePendingTransactions } from '../workspace/pending-transactions.js';
|
|
15
16
|
import { EntityCrudForm } from './entity-crud-form.js';
|
|
16
17
|
import { handOffWrite } from './hand-off-write.js';
|
|
17
18
|
import { PendingNotice } from './pending-notice.js';
|
|
18
19
|
import { CATALOG_NEW_SLUG, slugToEntityId } from './slug.js';
|
|
20
|
+
import { useCrudRenderLink } from './use-crud-render-link.js';
|
|
19
21
|
import { useEntityAffordances } from './use-entity-affordances.js';
|
|
20
22
|
import { useEntityBulk } from './use-entity-bulk.js';
|
|
21
23
|
/** What a picker defaults to reading off its target. */ const TARGET_NAME_PROPERTY = 'name';
|
|
@@ -111,6 +113,7 @@ export function makeEntityCrud(entityConstructor, options) {
|
|
|
111
113
|
const queryClient = useQueryClient();
|
|
112
114
|
const scope = entityQueryScope(entityConstructor);
|
|
113
115
|
const affordances = useEntityAffordances(entityConstructor, metadataSource);
|
|
116
|
+
const renderLink = useCrudRenderLink(catalogKey);
|
|
114
117
|
// Records this browser has created but the service has not finished
|
|
115
118
|
// writing. Prepended below rather than patched into the cache, which a
|
|
116
119
|
// refetch would undo.
|
|
@@ -188,7 +191,8 @@ export function makeEntityCrud(entityConstructor, options) {
|
|
|
188
191
|
],
|
|
189
192
|
totalItems: pager.totalItems + optimistic.length,
|
|
190
193
|
hrefFor: (id)=>withLocale(`${basePath}/${String(id)}`),
|
|
191
|
-
newHref: withLocale(`${basePath}/${CATALOG_NEW_SLUG}`)
|
|
194
|
+
newHref: withLocale(`${basePath}/${CATALOG_NEW_SLUG}`),
|
|
195
|
+
renderLink: renderLink
|
|
192
196
|
}, affordances, bulk.tableProps, {
|
|
193
197
|
onUseCase: handleRowUseCase,
|
|
194
198
|
error: useCaseError != null ? useCaseError : pager.error,
|
|
@@ -239,8 +243,25 @@ export function makeEntityCrud(entityConstructor, options) {
|
|
|
239
243
|
ctx: mergeCrudContext(adapters, configuration, plan.repository, pending)
|
|
240
244
|
}
|
|
241
245
|
}));
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
// Inside a workspace tab, "back to the list" is the list's tab, not a URL
|
|
247
|
+
// that would leave the workspace. A host's own callbacks still win.
|
|
248
|
+
const host = useEntityNavHost();
|
|
249
|
+
const renderLink = useCrudRenderLink(catalogKey);
|
|
250
|
+
const queryClient = useQueryClient();
|
|
251
|
+
// The list this page returns to has its page cached, and it is no longer
|
|
252
|
+
// true. Invalidated rather than patched, for the reason `handOffWrite`
|
|
253
|
+
// gives: a transactional create is not in the service yet, and the pending
|
|
254
|
+
// set, not the cache, is what keeps it visible until it is.
|
|
255
|
+
//
|
|
256
|
+
// ⚠️ Invisible while every Open was a document load, which threw the cache
|
|
257
|
+
// away anyway (#20): the first client-side navigation back to the list
|
|
258
|
+
// showed the record as it was before the save.
|
|
259
|
+
const refreshList = ()=>queryClient.invalidateQueries({
|
|
260
|
+
queryKey: entityQueryScope(entityConstructor)
|
|
261
|
+
});
|
|
262
|
+
const toList = ()=>host === undefined ? router.push(withLocale(basePath)) : host.toList(catalogKey);
|
|
263
|
+
const afterSave = onSaved != null ? onSaved : toList;
|
|
264
|
+
const afterDelete = onDeleted != null ? onDeleted : toList;
|
|
244
265
|
// The draft is spent once the write *commits*, and only here is that known:
|
|
245
266
|
// `useEntityForm` neither fetches nor saves, so it cannot clear its own.
|
|
246
267
|
// A failed mutation deliberately keeps the draft — the edit is still the
|
|
@@ -259,11 +280,13 @@ export function makeEntityCrud(entityConstructor, options) {
|
|
|
259
280
|
pending,
|
|
260
281
|
draft
|
|
261
282
|
});
|
|
283
|
+
void refreshList();
|
|
262
284
|
afterSave();
|
|
263
285
|
};
|
|
264
286
|
const handleDelete = async ()=>{
|
|
265
287
|
if (await remove(id)) {
|
|
266
288
|
draft == null ? void 0 : draft.clear();
|
|
289
|
+
void refreshList();
|
|
267
290
|
afterDelete();
|
|
268
291
|
}
|
|
269
292
|
};
|
|
@@ -301,6 +324,7 @@ export function makeEntityCrud(entityConstructor, options) {
|
|
|
301
324
|
onDelete: id == null ? undefined : handleDelete,
|
|
302
325
|
onUseCase: handleUseCase,
|
|
303
326
|
backHref: withLocale(basePath),
|
|
327
|
+
renderLink: renderLink,
|
|
304
328
|
draft: draft
|
|
305
329
|
}, String((_ref1 = entity == null ? void 0 : entity.id) != null ? _ref1 : CATALOG_NEW_SLUG));
|
|
306
330
|
}
|
|
@@ -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 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 {\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 { 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,\n runBulkUseCase,\n runUseCase,\n } = options;\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 affordances = useEntityAffordances(entityConstructor, metadataSource);\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 {...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 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 const afterSave = onSaved ?? (() => router.push(withLocale(basePath)));\n const afterDelete = onDeleted ?? (() => router.push(withLocale(basePath)));\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 afterSave();\n };\n\n const handleDelete = async () => {\n if (await remove(id)) {\n draft?.clear();\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 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","pendingFor","pendingRecordsFor","usePendingTransactions","EntityCrudForm","handOffWrite","PendingNotice","CATALOG_NEW_SLUG","slugToEntityId","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","runBulkUseCase","runUseCase","meta","key","name","labelKey","entityLabelKey","pluralKey","entityPluralKey","undefined","entityName","descriptors","linkPlans","map","link","descriptor","find","entry","field","linkLabelProperty","labelProperty","linkSearchProperty","searchProperty","ListPage","affordances","pending","withLocale","queryClient","scope","optimistic","pager","uc","ctx","queryKey","canRunBulk","metadata","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","afterSave","push","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 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 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,\n runBulkUseCase,\n runUseCase,\n } = options;\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 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 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","runBulkUseCase","runUseCase","meta","key","name","labelKey","entityLabelKey","pluralKey","entityPluralKey","undefined","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","metadata","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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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RenderLink } from '@entifix/react-controls';
|
|
2
|
+
/**
|
|
3
|
+
* How a generated page renders its Open, New and Back links (#20).
|
|
4
|
+
*
|
|
5
|
+
* A plain anchor was a full document load: the bundle rebuilt, every cache
|
|
6
|
+
* emptied, and inside a workspace tab the click left the workspace for the route
|
|
7
|
+
* page. So every link is `next/link` — a client-side navigation on a route host —
|
|
8
|
+
* and, when a tab host is mounted, a plain click opens the matching tab instead:
|
|
9
|
+
* the record, its create form (`<key>:new`), or the list. The `href` stays real
|
|
10
|
+
* either way, so a modified click still opens the route in a new browser tab.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useCrudRenderLink(entityKey: string): RenderLink;
|
|
13
|
+
//# sourceMappingURL=use-crud-render-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-crud-render-link.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/use-crud-render-link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAoB1D;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAyB/D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
import { useEntityNavHost } from '../workspace/entity-nav.js';
|
|
6
|
+
import { CATALOG_NEW_SLUG } from './slug.js';
|
|
7
|
+
/**
|
|
8
|
+
* A click the page may take over: the primary button with no modifier. Anything
|
|
9
|
+
* else — middle-click, ⌘/Ctrl-click, Shift-click — is the visitor asking for the
|
|
10
|
+
* link itself, in a new tab or window, and must be left to the browser.
|
|
11
|
+
*/ const isPlainClick = (event)=>!event.defaultPrevented && event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
12
|
+
/**
|
|
13
|
+
* How a generated page renders its Open, New and Back links (#20).
|
|
14
|
+
*
|
|
15
|
+
* A plain anchor was a full document load: the bundle rebuilt, every cache
|
|
16
|
+
* emptied, and inside a workspace tab the click left the workspace for the route
|
|
17
|
+
* page. So every link is `next/link` — a client-side navigation on a route host —
|
|
18
|
+
* and, when a tab host is mounted, a plain click opens the matching tab instead:
|
|
19
|
+
* the record, its create form (`<key>:new`), or the list. The `href` stays real
|
|
20
|
+
* either way, so a modified click still opens the route in a new browser tab.
|
|
21
|
+
*/ export function useCrudRenderLink(entityKey) {
|
|
22
|
+
const host = useEntityNavHost();
|
|
23
|
+
return useCallback(({ href, className, children, intent })=>/*#__PURE__*/ _jsx(Link, {
|
|
24
|
+
href: href,
|
|
25
|
+
className: className,
|
|
26
|
+
onClick: (event)=>{
|
|
27
|
+
if (host === undefined || !isPlainClick(event)) return;
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
if (intent.kind === 'open') {
|
|
30
|
+
host.toEntity(entityKey, String(intent.id));
|
|
31
|
+
} else if (intent.kind === 'new') {
|
|
32
|
+
host.toEntity(entityKey, CATALOG_NEW_SLUG);
|
|
33
|
+
} else {
|
|
34
|
+
host.toList(entityKey);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
children: children
|
|
38
|
+
}), [
|
|
39
|
+
host,
|
|
40
|
+
entityKey
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=use-crud-render-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/use-crud-render-link.tsx"],"sourcesContent":["'use client';\n\nimport type { RenderLink } from '@entifix/react-controls';\nimport Link from 'next/link';\nimport { type MouseEvent, useCallback } from 'react';\n\nimport { useEntityNavHost } from '../workspace/entity-nav.js';\nimport { CATALOG_NEW_SLUG } from './slug.js';\n\n/**\n * A click the page may take over: the primary button with no modifier. Anything\n * else — middle-click, ⌘/Ctrl-click, Shift-click — is the visitor asking for the\n * link itself, in a new tab or window, and must be left to the browser.\n */\nconst isPlainClick = (event: MouseEvent<HTMLAnchorElement>): boolean =>\n !event.defaultPrevented &&\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey;\n\n/**\n * How a generated page renders its Open, New and Back links (#20).\n *\n * A plain anchor was a full document load: the bundle rebuilt, every cache\n * emptied, and inside a workspace tab the click left the workspace for the route\n * page. So every link is `next/link` — a client-side navigation on a route host —\n * and, when a tab host is mounted, a plain click opens the matching tab instead:\n * the record, its create form (`<key>:new`), or the list. The `href` stays real\n * either way, so a modified click still opens the route in a new browser tab.\n */\nexport function useCrudRenderLink(entityKey: string): RenderLink {\n const host = useEntityNavHost();\n\n return useCallback<RenderLink>(\n ({ href, className, children, intent }) => (\n <Link\n href={href}\n className={className}\n onClick={event => {\n if (host === undefined || !isPlainClick(event)) return;\n event.preventDefault();\n if (intent.kind === 'open') {\n host.toEntity(entityKey, String(intent.id));\n } else if (intent.kind === 'new') {\n host.toEntity(entityKey, CATALOG_NEW_SLUG);\n } else {\n host.toList(entityKey);\n }\n }}\n >\n {children}\n </Link>\n ),\n [host, entityKey],\n );\n}\n"],"names":["Link","useCallback","useEntityNavHost","CATALOG_NEW_SLUG","isPlainClick","event","defaultPrevented","button","metaKey","ctrlKey","shiftKey","altKey","useCrudRenderLink","entityKey","host","href","className","children","intent","onClick","undefined","preventDefault","kind","toEntity","String","id","toList"],"mappings":"AAAA;;AAGA,OAAOA,UAAU,YAAY;AAC7B,SAA0BC,WAAW,QAAQ,QAAQ;AAErD,SAASC,gBAAgB,QAAQ,6BAA6B;AAC9D,SAASC,gBAAgB,QAAQ,YAAY;AAE7C;;;;CAIC,GACD,MAAMC,eAAe,CAACC,QACpB,CAACA,MAAMC,gBAAgB,IACvBD,MAAME,MAAM,KAAK,KACjB,CAACF,MAAMG,OAAO,IACd,CAACH,MAAMI,OAAO,IACd,CAACJ,MAAMK,QAAQ,IACf,CAACL,MAAMM,MAAM;AAEf;;;;;;;;;CASC,GACD,OAAO,SAASC,kBAAkBC,SAAiB;IACjD,MAAMC,OAAOZ;IAEb,OAAOD,YACL,CAAC,EAAEc,IAAI,EAAEC,SAAS,EAAEC,QAAQ,EAAEC,MAAM,EAAE,iBACpC,KAAClB;YACCe,MAAMA;YACNC,WAAWA;YACXG,SAASd,CAAAA;gBACP,IAAIS,SAASM,aAAa,CAAChB,aAAaC,QAAQ;gBAChDA,MAAMgB,cAAc;gBACpB,IAAIH,OAAOI,IAAI,KAAK,QAAQ;oBAC1BR,KAAKS,QAAQ,CAACV,WAAWW,OAAON,OAAOO,EAAE;gBAC3C,OAAO,IAAIP,OAAOI,IAAI,KAAK,OAAO;oBAChCR,KAAKS,QAAQ,CAACV,WAAWV;gBAC3B,OAAO;oBACLW,KAAKY,MAAM,CAACb;gBACd;YACF;sBAECI;YAGL;QAACH;QAAMD;KAAU;AAErB"}
|
|
@@ -74,7 +74,7 @@ export declare const shell: {
|
|
|
74
74
|
};
|
|
75
75
|
};
|
|
76
76
|
/**
|
|
77
|
-
* System management — the operator surface. Lives in
|
|
77
|
+
* System management — the operator surface. Lives in the framework shell so
|
|
78
78
|
* a dedicated management app can mount the same screens later, which is why the
|
|
79
79
|
* copy is here rather than in an app catalog.
|
|
80
80
|
*/
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
/**
|
|
75
|
-
* System management — the operator surface. Lives in
|
|
75
|
+
* System management — the operator surface. Lives in the framework shell so
|
|
76
76
|
* a dedicated management app can mount the same screens later, which is why the
|
|
77
77
|
* copy is here rather than in an app catalog.
|
|
78
78
|
*/ /**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/i18n/catalog/es.ts"],"sourcesContent":["/** Copy owned by the Next shells — back-office chrome and the tab workspace. */\nexport const shell = {\n /**\n * The identity surface — user administration and the signed-in\n * account. The screens live in `@r10c/shells-next-auth` so a second\n * host can mount them, and copy follows the code: an `app:` key is\n * lint-restricted to `apps/`, so a shell binding one fails the build.\n * What stays in `app:auth` is the sign-in page itself, which is the\n * host's own front door rather than the domain's surface.\n */\n auth: {\n nav: {\n identity: 'Identidad',\n users: 'Usuarios',\n accountSection: 'Cuenta',\n },\n account: {\n overline: 'Cuenta',\n title: 'Mi cuenta',\n subtitle: 'Tu identidad y tu acceso a la flota r10c.',\n menu: 'Cuenta',\n identifiers: 'Identificadores',\n noIdentifiers: 'Sin identificadores registrados.',\n userId: 'ID de usuario',\n subject: 'Sujeto',\n session: 'Sesión',\n roles: 'Roles',\n none: 'Ninguno',\n failed: 'No pudimos cargar tu cuenta.',\n },\n sessions: {\n overline: 'Seguridad',\n title: 'Sesiones activas',\n subtitle: 'Dónde has iniciado sesión. Cierra las que no reconozcas.',\n current: 'Este dispositivo',\n unknownDevice: 'Dispositivo desconocido',\n startedAt: 'Iniciada',\n expiresAt: 'Expira',\n lastIp: 'IP',\n revoke: 'Cerrar',\n revoking: 'Cerrando…',\n revokeOthers: 'Cerrar las demás',\n empty: 'No hay otras sesiones activas.',\n failed: 'No pudimos cargar tus sesiones.',\n loading: 'Cargando sesiones…',\n },\n security: {\n overline: 'Seguridad',\n title: 'Contraseña y acceso',\n subtitle:\n 'Tu contraseña, tu segundo factor y tus cuentas vinculadas viven en r10c Identity.',\n explain:\n 'r10c no guarda tu contraseña. Adminístrala, junto con la verificación en dos pasos y tus accesos sociales, en el proveedor de identidad.',\n manage: 'Administrar en r10c Identity',\n },\n fields: {\n displayName: 'Nombre visible',\n email: 'Correo',\n username: 'Usuario',\n role: 'Rol',\n },\n submit: {\n wait: 'Espera…',\n creating: 'Creando…',\n createUser: 'Crear usuario',\n },\n users: {\n title: 'Usuarios',\n newTitle: 'Nuevo usuario',\n fallbackName: 'Usuario',\n sessionsTitle: 'Sesiones de este usuario',\n sessionsEmpty: 'Este usuario no tiene sesiones activas.',\n sessionsFailed: 'No pudimos cargar las sesiones de este usuario.',\n kick: 'Cerrar todas sus sesiones',\n kicking: 'Cerrando…',\n },\n },\n /**\n * System management — the operator surface. Lives in a `scope:shared` shell so\n * a dedicated management app can mount the same screens later, which is why the\n * copy is here rather than in an app catalog.\n */\n /**\n * La superficie de existencias — las primeras pantallas de Operaciones del\n * back office.\n *\n * El *nombre del nivel* (\"Operaciones\") sale de `nav.screenType.operation`;\n * esto nombra el grupo dentro de él.\n */\n stock: {\n nav: {\n stock: 'Existencias',\n items: 'Existencias en almacén',\n movements: 'Libro de movimientos',\n reservations: 'Reservas',\n },\n },\n order: {\n nav: {\n orders: 'Pedidos',\n },\n },\n settlement: {\n nav: {\n // El mismo nombre de grupo en las dos gradas: un acuerdo se redacta\n // (Definiciones) y el resto lo produce un proceso (Operaciones). El\n // encabezado de cada grada sale de `nav.screenType`.\n settlement: 'Liquidaciones',\n agreements: 'Acuerdos',\n entries: 'Apuntes de comisión',\n runs: 'Liquidaciones',\n payouts: 'Pagos a vendedores',\n },\n agreement: {\n channelRatesHint:\n 'Déjalo en blanco para cobrar la comisión por defecto en ese canal. Un 0 significa que no se cobra nada.',\n channelRateDefault: 'Por defecto',\n noChannelRates: 'Sin tarifas por canal',\n },\n },\n sales: {\n nav: {\n sales: 'Ventas',\n channels: 'Canales de venta',\n guided: 'Ventas',\n counterSale: 'Vender en mostrador',\n },\n counterSale: {\n title: 'Venta en mostrador',\n steps: {\n channel: 'Canal',\n lines: 'Productos',\n payment: 'Cobro',\n summary: 'Resumen',\n },\n channelHint: 'Elige el canal por el que se hace esta venta.',\n linesHint: 'Agrega los productos publicados que estás vendiendo.',\n addLine: 'Agregar producto',\n removeLine: 'Quitar',\n quantity: 'Cantidad',\n offering: 'Producto',\n total: 'Total',\n paymentMethod: 'Forma de pago',\n charge: 'Cobrar',\n sold: 'Venta registrada',\n soldDetail: 'La existencia ya se descontó y el pedido quedó registrado.',\n newSale: 'Nueva venta',\n emptyLines: 'Agrega al menos un producto para cobrar.',\n },\n },\n marketplaceAdmin: {\n nav: {\n // Moved out of the `app:` catalog with the nav table itself: this copy\n // belongs to the shell that owns the screens, so a second host mounting\n // it does not re-translate them.\n catalog: 'Catálogo',\n products: 'Productos',\n brands: 'Marcas',\n categories: 'Categorías',\n offerings: 'Ofertas comerciales',\n offeringPrices: 'Precios de oferta',\n // La sección Asistentes. El *nombre del nivel* (\"Asistentes\") sale de\n // `nav.screenType.wizard`; esto nombra el grupo dentro de él.\n guided: 'Catálogo',\n },\n /**\n * El asistente de alta de producto. Los nombres de los pasos son de este\n * dominio, no del control — por eso viven acá y no en `controls`.\n */\n wizard: {\n productSetup: {\n title: 'Nuevo producto',\n launch: 'Nuevo producto guiado',\n steps: {\n start: 'Origen',\n source: 'Producto base',\n identity: 'Identificación',\n classification: 'Clasificación',\n summary: 'Resumen',\n },\n start: {\n question: '¿Cómo querés empezar?',\n blank: 'Desde cero',\n blankHint: 'Cargar los datos a mano.',\n duplicate: 'Duplicar uno existente',\n duplicateHint:\n 'Partir de un producto ya cargado. El código no se copia: identifica al original.',\n },\n source: {\n prompt: 'Elegí el producto del que querés partir.',\n chosen: 'Producto base: {{name}}',\n required: 'Elegí un producto para continuar.',\n },\n summary: {\n prompt: 'Revisá lo cargado antes de crearlo.',\n origin: 'Origen',\n originBlank: 'Desde cero',\n originDuplicate: 'Duplicado de {{name}}',\n empty: 'Sin definir',\n },\n recap: 'Ya habías respondido:',\n },\n },\n },\n systemManagement: {\n nav: {\n section: 'Sistema',\n configuration: 'Configuración',\n },\n configuration: {\n title: 'Configuración de la plataforma',\n description:\n 'Parámetros que cada servicio resuelve al arrancar. Editar un valor afecta al servicio en su próximo arranque.',\n secretPlaceholder: '•••••• (sin cambios)',\n secretHint:\n 'Dejar en blanco mantiene el valor guardado. Escribir uno nuevo lo reemplaza.',\n },\n },\n nav: {\n primary: 'Principal',\n openInWorkspace: 'Abrir {{label}} en el espacio de trabajo',\n openInWorkspaceShort: 'Abrir en el espacio de trabajo',\n openInNewTab: 'Abrir {{label}} en una pestaña nueva',\n openInNewTabShort: 'Abrir en una pestaña nueva',\n /**\n * The screen taxonomy (ADR 0033) — the sidebar's top tier, above the\n * domain. `Definiciones` rather than `Maestros` because the ERP term needs\n * the ERP background to parse, and rather than `Catálogos` or `Referencias`\n * because both already mean something else here: the product catalog, and a\n * product's code in `storefront.category.sortByCode`.\n */\n screenType: {\n master: 'Definiciones',\n operation: 'Operaciones',\n wizard: 'Asistentes',\n report: 'Consultas',\n },\n },\n /**\n * La paleta de comandos (ADR 0044). Vive en `shell:` y no en `controls:`\n * porque el control es agnóstico —recibe copia ya resuelta— y quien la\n * arma es el back office.\n */\n commandPalette: {\n /** El disparador visible, que existe justo porque un atajo puede fallar. */\n open: 'Buscar o ejecutar',\n /** El nombre accesible del diálogo y de su campo. */\n title: 'Buscar o ejecutar',\n placeholder: 'Busca un registro, una pantalla o un comando…',\n newPlaceholder: '¿Qué quieres crear?',\n newLabel: 'Nuevo…',\n /** El nombre de la página apilada, y también el del control para volver. */\n newTitle: 'Nuevo',\n back: 'Volver',\n empty: 'Sin resultados',\n loading: 'Buscando…',\n more_one: '{{count}} más',\n more_other: '{{count}} más',\n /** Escribe al menos dos caracteres — el suelo que impone `/api/search`. */\n typeMore: 'Escribe al menos {{count}} caracteres para buscar registros.',\n groups: {\n actions: 'Acciones',\n commands: 'Comandos',\n navigation: 'Navegación',\n tabs: 'Pestañas abiertas',\n records: 'Registros',\n },\n /**\n * Dos redacciones para dos cosas distintas: «no es tuyo» es el estado\n * **normal** de un operador sin organización, y pintarlo como avería en\n * cada tecleo enseña a ignorar el aviso que sí importa.\n */\n unavailableScope: 'No disponible aquí: {{reason}}',\n unavailableReach: 'No pudimos consultar este origen: {{reason}}',\n /** Sufijo del comando que abre un destino como pestaña del espacio. */\n openInWorkspaceHint: 'Pestaña',\n },\n sidebar: {\n expand: 'Expandir barra lateral',\n collapse: 'Contraer barra lateral',\n openMenu: 'Abrir el menú',\n closeMenu: 'Cerrar el menú',\n // The drawer is a dialog, so it needs a name of its own — the landmark\n // inside it is the nav, and a dialog with no accessible name is announced\n // as just \"dialog\".\n menu: 'Menú de navegación',\n expandGroup: 'Desplegar {{group}}',\n collapseGroup: 'Plegar {{group}}',\n },\n /**\n * The session strip, rendered by `SessionKeepalive` only inside the final\n * window before the absolute ceiling. One key, because the strip says one\n * thing: nothing here counts down, since a number that moves invites a person\n * to watch it rather than to save their work.\n */\n session: {\n expiringSoon:\n 'Tu sesión está por expirar. Guarda tu trabajo e inicia sesión de nuevo para continuar.',\n },\n breadcrumbs: {\n home: 'Inicio',\n // The `/new` segment, which every domain's create route shares. It was\n // an auth-specific string until one host started serving two domains,\n // at which point a catalog page's breadcrumb read \"New user\".\n new: 'Nuevo',\n },\n /**\n * The account surface. The auth shell owns the screens, but `ACCOUNT_DESTINATIONS`\n * — the one list every app builds its menu from — lives in the shell, so the\n * shell owns their names too and resolves them itself.\n */\n account: {\n profile: 'Perfil',\n security: 'Seguridad',\n sessions: 'Sesiones',\n signOut: 'Cerrar sesión',\n },\n workspace: {\n copyLink: 'Copiar enlace',\n empty:\n 'No hay pestañas abiertas. Elige un catálogo en la barra lateral para empezar.',\n unsupported: 'Esta pestaña no se puede abrir aquí.',\n // The confirmation that guards closing a dirty tab. The title carries the\n // question, so the message states the consequence rather than repeating it\n // — it was phrased as a question while `window.confirm` rendered it, which\n // had nowhere to put a title.\n discardTitle: 'Descartar cambios',\n discard:\n 'Esta pestaña tiene cambios sin guardar. Si la cierras, se perderán.',\n discardConfirm: 'Descartar',\n // What became of a write the server has not finished. `count` is the\n // number in flight; a failure names no reason here, because the only one a\n // transaction carries is a server exception string in whatever language it\n // was written in — that is rendered beside this, unlocalized and marked so.\n pendingLabel: 'Estado de los guardados',\n savePending_one: 'Guardando 1 registro…',\n savePending_other: 'Guardando {{count}} registros…',\n saveFailed: 'No se pudo guardar el registro.',\n saveFailedDismiss: 'Descartar',\n },\n /**\n * The storefront. Its pages live in `shells-next-marketplace`, so its copy\n * lives here and not in an app catalog — `app:` is reserved for `apps/`.\n */\n storefront: {\n nav: {\n catalog: 'Catálogo',\n },\n home: {\n overline: 'Tienda',\n heading: 'Marketplace r10c',\n lead: 'Objetos para la casa, elegidos de a poco.',\n featured: 'Destacados',\n },\n category: {\n overline: 'Categoría',\n empty: 'No hay productos en esta categoría todavía.',\n sort: 'Ordenar',\n sortByName: 'Nombre',\n sortByCode: 'Referencia',\n results: '{{count}} productos',\n previous: 'Anterior',\n next: 'Siguiente',\n pageOf: 'Página {{page}} de {{pages}}',\n },\n product: {\n addToCart: 'Añadir al carrito',\n view: 'Ver producto',\n brand: 'Marca',\n category: 'Categoría',\n reference: 'Referencia',\n related: 'También te puede interesar',\n /**\n * `availableHint` es una **pista**, y el nombre lo dice: el catálogo\n * publicado es consistente en diferido a propósito y la verdad es la\n * reserva del checkout (ADR 0009). Por eso la copia no promete\n * existencias concretas.\n */\n available: 'Disponible',\n unavailable: 'Sin existencias',\n },\n search: {\n heading: 'Buscar',\n label: 'Buscar productos',\n placeholder: 'Lámpara, taza, manta…',\n submit: 'Buscar',\n resultsFor: 'Resultados para «{{term}}»',\n empty: 'No encontramos nada para «{{term}}».',\n prompt: 'Escribe algo para empezar a buscar.',\n },\n cart: {\n heading: 'Carrito',\n empty: 'Tu carrito está vacío.',\n remove: 'Quitar',\n units: 'Cant. {{count}}',\n total: 'Artículos en total',\n keepShopping: 'Seguir comprando',\n checkout: 'Finalizar compra',\n },\n checkout: {\n placed: 'Tu pedido quedó registrado. Ya se notificó al vendedor.',\n unavailable:\n 'Uno de estos productos ya no está disponible en la cantidad que pediste. No se cobró nada y se liberaron todas las reservas.',\n empty: 'Ninguno de estos productos sigue a la venta.',\n failed: 'No se pudo completar la compra. Inténtalo de nuevo.',\n },\n order: {\n heading: '¡Gracias por tu compra!',\n placed: 'Tu pedido quedó registrado y el vendedor ya fue notificado.',\n reference: 'Número de pedido',\n units: 'Cant. {{count}}',\n total: 'Total',\n tooManyLines:\n 'Este pedido tiene {{count}} artículos, demasiados para mostrarlos aquí. El número de pedido de arriba los identifica a todos.',\n keepShopping: 'Seguir comprando',\n expiredHeading: 'No hay ningún pedido que mostrar',\n expired:\n 'El comprobante de tu última compra ya no está disponible en este navegador. Si acabas de comprar, revisa el correo de confirmación.',\n // El plazo se nombra en vez de dejar que el botón deje de funcionar solo:\n // el comprobante lleva la fecha que el servidor selló, y las dos\n // concuerdan porque `RECEIPT_TTL_SECONDS` y el ajuste del servicio son un\n // solo número (ADR 0058 §5).\n cancel: 'Cancelar pedido',\n cancelWindow: 'Puedes cancelar este pedido hasta las {{time}}.',\n cancelPending: 'Cancelando…',\n cancelledHeading: 'Pedido cancelado',\n cancelled:\n 'Cancelamos tu pedido y el reembolso ya va en camino. Puede tardar unos días en aparecer en tu estado de cuenta.',\n cancelFailed:\n 'No se pudo cancelar el pedido. Puede que el plazo haya terminado o que ya vaya en camino.',\n },\n footer: {\n note: 'Catálogo de demostración. Ningún pedido es real.',\n },\n },\n};\n"],"names":["shell","auth","nav","identity","users","accountSection","account","overline","title","subtitle","menu","identifiers","noIdentifiers","userId","subject","session","roles","none","failed","sessions","current","unknownDevice","startedAt","expiresAt","lastIp","revoke","revoking","revokeOthers","empty","loading","security","explain","manage","fields","displayName","email","username","role","submit","wait","creating","createUser","newTitle","fallbackName","sessionsTitle","sessionsEmpty","sessionsFailed","kick","kicking","stock","items","movements","reservations","order","orders","settlement","agreements","entries","runs","payouts","agreement","channelRatesHint","channelRateDefault","noChannelRates","sales","channels","guided","counterSale","steps","channel","lines","payment","summary","channelHint","linesHint","addLine","removeLine","quantity","offering","total","paymentMethod","charge","sold","soldDetail","newSale","emptyLines","marketplaceAdmin","catalog","products","brands","categories","offerings","offeringPrices","wizard","productSetup","launch","start","source","classification","question","blank","blankHint","duplicate","duplicateHint","prompt","chosen","required","origin","originBlank","originDuplicate","recap","systemManagement","section","configuration","description","secretPlaceholder","secretHint","primary","openInWorkspace","openInWorkspaceShort","openInNewTab","openInNewTabShort","screenType","master","operation","report","commandPalette","open","placeholder","newPlaceholder","newLabel","back","more_one","more_other","typeMore","groups","actions","commands","navigation","tabs","records","unavailableScope","unavailableReach","openInWorkspaceHint","sidebar","expand","collapse","openMenu","closeMenu","expandGroup","collapseGroup","expiringSoon","breadcrumbs","home","new","profile","signOut","workspace","copyLink","unsupported","discardTitle","discard","discardConfirm","pendingLabel","savePending_one","savePending_other","saveFailed","saveFailedDismiss","storefront","heading","lead","featured","category","sort","sortByName","sortByCode","results","previous","next","pageOf","product","addToCart","view","brand","reference","related","available","unavailable","search","label","resultsFor","cart","remove","units","keepShopping","checkout","placed","tooManyLines","expiredHeading","expired","cancel","cancelWindow","cancelPending","cancelledHeading","cancelled","cancelFailed","footer","note"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/i18n/catalog/es.ts"],"sourcesContent":["/** Copy owned by the Next shells — back-office chrome and the tab workspace. */\nexport const shell = {\n /**\n * The identity surface — user administration and the signed-in\n * account. The screens live in `@r10c/shells-next-auth` so a second\n * host can mount them, and copy follows the code: an `app:` key is\n * lint-restricted to `apps/`, so a shell binding one fails the build.\n * What stays in `app:auth` is the sign-in page itself, which is the\n * host's own front door rather than the domain's surface.\n */\n auth: {\n nav: {\n identity: 'Identidad',\n users: 'Usuarios',\n accountSection: 'Cuenta',\n },\n account: {\n overline: 'Cuenta',\n title: 'Mi cuenta',\n subtitle: 'Tu identidad y tu acceso a la flota r10c.',\n menu: 'Cuenta',\n identifiers: 'Identificadores',\n noIdentifiers: 'Sin identificadores registrados.',\n userId: 'ID de usuario',\n subject: 'Sujeto',\n session: 'Sesión',\n roles: 'Roles',\n none: 'Ninguno',\n failed: 'No pudimos cargar tu cuenta.',\n },\n sessions: {\n overline: 'Seguridad',\n title: 'Sesiones activas',\n subtitle: 'Dónde has iniciado sesión. Cierra las que no reconozcas.',\n current: 'Este dispositivo',\n unknownDevice: 'Dispositivo desconocido',\n startedAt: 'Iniciada',\n expiresAt: 'Expira',\n lastIp: 'IP',\n revoke: 'Cerrar',\n revoking: 'Cerrando…',\n revokeOthers: 'Cerrar las demás',\n empty: 'No hay otras sesiones activas.',\n failed: 'No pudimos cargar tus sesiones.',\n loading: 'Cargando sesiones…',\n },\n security: {\n overline: 'Seguridad',\n title: 'Contraseña y acceso',\n subtitle:\n 'Tu contraseña, tu segundo factor y tus cuentas vinculadas viven en r10c Identity.',\n explain:\n 'r10c no guarda tu contraseña. Adminístrala, junto con la verificación en dos pasos y tus accesos sociales, en el proveedor de identidad.',\n manage: 'Administrar en r10c Identity',\n },\n fields: {\n displayName: 'Nombre visible',\n email: 'Correo',\n username: 'Usuario',\n role: 'Rol',\n },\n submit: {\n wait: 'Espera…',\n creating: 'Creando…',\n createUser: 'Crear usuario',\n },\n users: {\n title: 'Usuarios',\n newTitle: 'Nuevo usuario',\n fallbackName: 'Usuario',\n sessionsTitle: 'Sesiones de este usuario',\n sessionsEmpty: 'Este usuario no tiene sesiones activas.',\n sessionsFailed: 'No pudimos cargar las sesiones de este usuario.',\n kick: 'Cerrar todas sus sesiones',\n kicking: 'Cerrando…',\n },\n },\n /**\n * System management — the operator surface. Lives in the framework shell so\n * a dedicated management app can mount the same screens later, which is why the\n * copy is here rather than in an app catalog.\n */\n /**\n * La superficie de existencias — las primeras pantallas de Operaciones del\n * back office.\n *\n * El *nombre del nivel* (\"Operaciones\") sale de `nav.screenType.operation`;\n * esto nombra el grupo dentro de él.\n */\n stock: {\n nav: {\n stock: 'Existencias',\n items: 'Existencias en almacén',\n movements: 'Libro de movimientos',\n reservations: 'Reservas',\n },\n },\n order: {\n nav: {\n orders: 'Pedidos',\n },\n },\n settlement: {\n nav: {\n // El mismo nombre de grupo en las dos gradas: un acuerdo se redacta\n // (Definiciones) y el resto lo produce un proceso (Operaciones). El\n // encabezado de cada grada sale de `nav.screenType`.\n settlement: 'Liquidaciones',\n agreements: 'Acuerdos',\n entries: 'Apuntes de comisión',\n runs: 'Liquidaciones',\n payouts: 'Pagos a vendedores',\n },\n agreement: {\n channelRatesHint:\n 'Déjalo en blanco para cobrar la comisión por defecto en ese canal. Un 0 significa que no se cobra nada.',\n channelRateDefault: 'Por defecto',\n noChannelRates: 'Sin tarifas por canal',\n },\n },\n sales: {\n nav: {\n sales: 'Ventas',\n channels: 'Canales de venta',\n guided: 'Ventas',\n counterSale: 'Vender en mostrador',\n },\n counterSale: {\n title: 'Venta en mostrador',\n steps: {\n channel: 'Canal',\n lines: 'Productos',\n payment: 'Cobro',\n summary: 'Resumen',\n },\n channelHint: 'Elige el canal por el que se hace esta venta.',\n linesHint: 'Agrega los productos publicados que estás vendiendo.',\n addLine: 'Agregar producto',\n removeLine: 'Quitar',\n quantity: 'Cantidad',\n offering: 'Producto',\n total: 'Total',\n paymentMethod: 'Forma de pago',\n charge: 'Cobrar',\n sold: 'Venta registrada',\n soldDetail: 'La existencia ya se descontó y el pedido quedó registrado.',\n newSale: 'Nueva venta',\n emptyLines: 'Agrega al menos un producto para cobrar.',\n },\n },\n marketplaceAdmin: {\n nav: {\n // Moved out of the `app:` catalog with the nav table itself: this copy\n // belongs to the shell that owns the screens, so a second host mounting\n // it does not re-translate them.\n catalog: 'Catálogo',\n products: 'Productos',\n brands: 'Marcas',\n categories: 'Categorías',\n offerings: 'Ofertas comerciales',\n offeringPrices: 'Precios de oferta',\n // La sección Asistentes. El *nombre del nivel* (\"Asistentes\") sale de\n // `nav.screenType.wizard`; esto nombra el grupo dentro de él.\n guided: 'Catálogo',\n },\n /**\n * El asistente de alta de producto. Los nombres de los pasos son de este\n * dominio, no del control — por eso viven acá y no en `controls`.\n */\n wizard: {\n productSetup: {\n title: 'Nuevo producto',\n launch: 'Nuevo producto guiado',\n steps: {\n start: 'Origen',\n source: 'Producto base',\n identity: 'Identificación',\n classification: 'Clasificación',\n summary: 'Resumen',\n },\n start: {\n question: '¿Cómo querés empezar?',\n blank: 'Desde cero',\n blankHint: 'Cargar los datos a mano.',\n duplicate: 'Duplicar uno existente',\n duplicateHint:\n 'Partir de un producto ya cargado. El código no se copia: identifica al original.',\n },\n source: {\n prompt: 'Elegí el producto del que querés partir.',\n chosen: 'Producto base: {{name}}',\n required: 'Elegí un producto para continuar.',\n },\n summary: {\n prompt: 'Revisá lo cargado antes de crearlo.',\n origin: 'Origen',\n originBlank: 'Desde cero',\n originDuplicate: 'Duplicado de {{name}}',\n empty: 'Sin definir',\n },\n recap: 'Ya habías respondido:',\n },\n },\n },\n systemManagement: {\n nav: {\n section: 'Sistema',\n configuration: 'Configuración',\n },\n configuration: {\n title: 'Configuración de la plataforma',\n description:\n 'Parámetros que cada servicio resuelve al arrancar. Editar un valor afecta al servicio en su próximo arranque.',\n secretPlaceholder: '•••••• (sin cambios)',\n secretHint:\n 'Dejar en blanco mantiene el valor guardado. Escribir uno nuevo lo reemplaza.',\n },\n },\n nav: {\n primary: 'Principal',\n openInWorkspace: 'Abrir {{label}} en el espacio de trabajo',\n openInWorkspaceShort: 'Abrir en el espacio de trabajo',\n openInNewTab: 'Abrir {{label}} en una pestaña nueva',\n openInNewTabShort: 'Abrir en una pestaña nueva',\n /**\n * The screen taxonomy (ADR 0033) — the sidebar's top tier, above the\n * domain. `Definiciones` rather than `Maestros` because the ERP term needs\n * the ERP background to parse, and rather than `Catálogos` or `Referencias`\n * because both already mean something else here: the product catalog, and a\n * product's code in `storefront.category.sortByCode`.\n */\n screenType: {\n master: 'Definiciones',\n operation: 'Operaciones',\n wizard: 'Asistentes',\n report: 'Consultas',\n },\n },\n /**\n * La paleta de comandos (ADR 0044). Vive en `shell:` y no en `controls:`\n * porque el control es agnóstico —recibe copia ya resuelta— y quien la\n * arma es el back office.\n */\n commandPalette: {\n /** El disparador visible, que existe justo porque un atajo puede fallar. */\n open: 'Buscar o ejecutar',\n /** El nombre accesible del diálogo y de su campo. */\n title: 'Buscar o ejecutar',\n placeholder: 'Busca un registro, una pantalla o un comando…',\n newPlaceholder: '¿Qué quieres crear?',\n newLabel: 'Nuevo…',\n /** El nombre de la página apilada, y también el del control para volver. */\n newTitle: 'Nuevo',\n back: 'Volver',\n empty: 'Sin resultados',\n loading: 'Buscando…',\n more_one: '{{count}} más',\n more_other: '{{count}} más',\n /** Escribe al menos dos caracteres — el suelo que impone `/api/search`. */\n typeMore: 'Escribe al menos {{count}} caracteres para buscar registros.',\n groups: {\n actions: 'Acciones',\n commands: 'Comandos',\n navigation: 'Navegación',\n tabs: 'Pestañas abiertas',\n records: 'Registros',\n },\n /**\n * Dos redacciones para dos cosas distintas: «no es tuyo» es el estado\n * **normal** de un operador sin organización, y pintarlo como avería en\n * cada tecleo enseña a ignorar el aviso que sí importa.\n */\n unavailableScope: 'No disponible aquí: {{reason}}',\n unavailableReach: 'No pudimos consultar este origen: {{reason}}',\n /** Sufijo del comando que abre un destino como pestaña del espacio. */\n openInWorkspaceHint: 'Pestaña',\n },\n sidebar: {\n expand: 'Expandir barra lateral',\n collapse: 'Contraer barra lateral',\n openMenu: 'Abrir el menú',\n closeMenu: 'Cerrar el menú',\n // The drawer is a dialog, so it needs a name of its own — the landmark\n // inside it is the nav, and a dialog with no accessible name is announced\n // as just \"dialog\".\n menu: 'Menú de navegación',\n expandGroup: 'Desplegar {{group}}',\n collapseGroup: 'Plegar {{group}}',\n },\n /**\n * The session strip, rendered by `SessionKeepalive` only inside the final\n * window before the absolute ceiling. One key, because the strip says one\n * thing: nothing here counts down, since a number that moves invites a person\n * to watch it rather than to save their work.\n */\n session: {\n expiringSoon:\n 'Tu sesión está por expirar. Guarda tu trabajo e inicia sesión de nuevo para continuar.',\n },\n breadcrumbs: {\n home: 'Inicio',\n // The `/new` segment, which every domain's create route shares. It was\n // an auth-specific string until one host started serving two domains,\n // at which point a catalog page's breadcrumb read \"New user\".\n new: 'Nuevo',\n },\n /**\n * The account surface. The auth shell owns the screens, but `ACCOUNT_DESTINATIONS`\n * — the one list every app builds its menu from — lives in the shell, so the\n * shell owns their names too and resolves them itself.\n */\n account: {\n profile: 'Perfil',\n security: 'Seguridad',\n sessions: 'Sesiones',\n signOut: 'Cerrar sesión',\n },\n workspace: {\n copyLink: 'Copiar enlace',\n empty:\n 'No hay pestañas abiertas. Elige un catálogo en la barra lateral para empezar.',\n unsupported: 'Esta pestaña no se puede abrir aquí.',\n // The confirmation that guards closing a dirty tab. The title carries the\n // question, so the message states the consequence rather than repeating it\n // — it was phrased as a question while `window.confirm` rendered it, which\n // had nowhere to put a title.\n discardTitle: 'Descartar cambios',\n discard:\n 'Esta pestaña tiene cambios sin guardar. Si la cierras, se perderán.',\n discardConfirm: 'Descartar',\n // What became of a write the server has not finished. `count` is the\n // number in flight; a failure names no reason here, because the only one a\n // transaction carries is a server exception string in whatever language it\n // was written in — that is rendered beside this, unlocalized and marked so.\n pendingLabel: 'Estado de los guardados',\n savePending_one: 'Guardando 1 registro…',\n savePending_other: 'Guardando {{count}} registros…',\n saveFailed: 'No se pudo guardar el registro.',\n saveFailedDismiss: 'Descartar',\n },\n /**\n * The storefront. Its pages live in `shells-next-marketplace`, so its copy\n * lives here and not in an app catalog — `app:` is reserved for `apps/`.\n */\n storefront: {\n nav: {\n catalog: 'Catálogo',\n },\n home: {\n overline: 'Tienda',\n heading: 'Marketplace r10c',\n lead: 'Objetos para la casa, elegidos de a poco.',\n featured: 'Destacados',\n },\n category: {\n overline: 'Categoría',\n empty: 'No hay productos en esta categoría todavía.',\n sort: 'Ordenar',\n sortByName: 'Nombre',\n sortByCode: 'Referencia',\n results: '{{count}} productos',\n previous: 'Anterior',\n next: 'Siguiente',\n pageOf: 'Página {{page}} de {{pages}}',\n },\n product: {\n addToCart: 'Añadir al carrito',\n view: 'Ver producto',\n brand: 'Marca',\n category: 'Categoría',\n reference: 'Referencia',\n related: 'También te puede interesar',\n /**\n * `availableHint` es una **pista**, y el nombre lo dice: el catálogo\n * publicado es consistente en diferido a propósito y la verdad es la\n * reserva del checkout (ADR 0009). Por eso la copia no promete\n * existencias concretas.\n */\n available: 'Disponible',\n unavailable: 'Sin existencias',\n },\n search: {\n heading: 'Buscar',\n label: 'Buscar productos',\n placeholder: 'Lámpara, taza, manta…',\n submit: 'Buscar',\n resultsFor: 'Resultados para «{{term}}»',\n empty: 'No encontramos nada para «{{term}}».',\n prompt: 'Escribe algo para empezar a buscar.',\n },\n cart: {\n heading: 'Carrito',\n empty: 'Tu carrito está vacío.',\n remove: 'Quitar',\n units: 'Cant. {{count}}',\n total: 'Artículos en total',\n keepShopping: 'Seguir comprando',\n checkout: 'Finalizar compra',\n },\n checkout: {\n placed: 'Tu pedido quedó registrado. Ya se notificó al vendedor.',\n unavailable:\n 'Uno de estos productos ya no está disponible en la cantidad que pediste. No se cobró nada y se liberaron todas las reservas.',\n empty: 'Ninguno de estos productos sigue a la venta.',\n failed: 'No se pudo completar la compra. Inténtalo de nuevo.',\n },\n order: {\n heading: '¡Gracias por tu compra!',\n placed: 'Tu pedido quedó registrado y el vendedor ya fue notificado.',\n reference: 'Número de pedido',\n units: 'Cant. {{count}}',\n total: 'Total',\n tooManyLines:\n 'Este pedido tiene {{count}} artículos, demasiados para mostrarlos aquí. El número de pedido de arriba los identifica a todos.',\n keepShopping: 'Seguir comprando',\n expiredHeading: 'No hay ningún pedido que mostrar',\n expired:\n 'El comprobante de tu última compra ya no está disponible en este navegador. Si acabas de comprar, revisa el correo de confirmación.',\n // El plazo se nombra en vez de dejar que el botón deje de funcionar solo:\n // el comprobante lleva la fecha que el servidor selló, y las dos\n // concuerdan porque `RECEIPT_TTL_SECONDS` y el ajuste del servicio son un\n // solo número (ADR 0058 §5).\n cancel: 'Cancelar pedido',\n cancelWindow: 'Puedes cancelar este pedido hasta las {{time}}.',\n cancelPending: 'Cancelando…',\n cancelledHeading: 'Pedido cancelado',\n cancelled:\n 'Cancelamos tu pedido y el reembolso ya va en camino. Puede tardar unos días en aparecer en tu estado de cuenta.',\n cancelFailed:\n 'No se pudo cancelar el pedido. Puede que el plazo haya terminado o que ya vaya en camino.',\n },\n footer: {\n note: 'Catálogo de demostración. Ningún pedido es real.',\n },\n },\n};\n"],"names":["shell","auth","nav","identity","users","accountSection","account","overline","title","subtitle","menu","identifiers","noIdentifiers","userId","subject","session","roles","none","failed","sessions","current","unknownDevice","startedAt","expiresAt","lastIp","revoke","revoking","revokeOthers","empty","loading","security","explain","manage","fields","displayName","email","username","role","submit","wait","creating","createUser","newTitle","fallbackName","sessionsTitle","sessionsEmpty","sessionsFailed","kick","kicking","stock","items","movements","reservations","order","orders","settlement","agreements","entries","runs","payouts","agreement","channelRatesHint","channelRateDefault","noChannelRates","sales","channels","guided","counterSale","steps","channel","lines","payment","summary","channelHint","linesHint","addLine","removeLine","quantity","offering","total","paymentMethod","charge","sold","soldDetail","newSale","emptyLines","marketplaceAdmin","catalog","products","brands","categories","offerings","offeringPrices","wizard","productSetup","launch","start","source","classification","question","blank","blankHint","duplicate","duplicateHint","prompt","chosen","required","origin","originBlank","originDuplicate","recap","systemManagement","section","configuration","description","secretPlaceholder","secretHint","primary","openInWorkspace","openInWorkspaceShort","openInNewTab","openInNewTabShort","screenType","master","operation","report","commandPalette","open","placeholder","newPlaceholder","newLabel","back","more_one","more_other","typeMore","groups","actions","commands","navigation","tabs","records","unavailableScope","unavailableReach","openInWorkspaceHint","sidebar","expand","collapse","openMenu","closeMenu","expandGroup","collapseGroup","expiringSoon","breadcrumbs","home","new","profile","signOut","workspace","copyLink","unsupported","discardTitle","discard","discardConfirm","pendingLabel","savePending_one","savePending_other","saveFailed","saveFailedDismiss","storefront","heading","lead","featured","category","sort","sortByName","sortByCode","results","previous","next","pageOf","product","addToCart","view","brand","reference","related","available","unavailable","search","label","resultsFor","cart","remove","units","keepShopping","checkout","placed","tooManyLines","expiredHeading","expired","cancel","cancelWindow","cancelPending","cancelledHeading","cancelled","cancelFailed","footer","note"],"mappings":"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"}
|
|
@@ -26,8 +26,8 @@ export interface AccountDestination {
|
|
|
26
26
|
* a screen is a one-line change here.
|
|
27
27
|
*
|
|
28
28
|
* It lives in the shared shell rather than in the auth shell because the menu
|
|
29
|
-
* is rendered by `BackOfficeShell`, which any host mounts — a
|
|
30
|
-
*
|
|
29
|
+
* is rendered by `BackOfficeShell`, which any host mounts — a framework
|
|
30
|
+
* shell cannot reach up into a host's own one.
|
|
31
31
|
*/
|
|
32
32
|
export declare const ACCOUNT_DESTINATIONS: readonly AccountDestination[];
|
|
33
33
|
export interface AccountLink {
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* a screen is a one-line change here.
|
|
7
7
|
*
|
|
8
8
|
* It lives in the shared shell rather than in the auth shell because the menu
|
|
9
|
-
* is rendered by `BackOfficeShell`, which any host mounts — a
|
|
10
|
-
*
|
|
9
|
+
* is rendered by `BackOfficeShell`, which any host mounts — a framework
|
|
10
|
+
* shell cannot reach up into a host's own one.
|
|
11
11
|
*/ export const ACCOUNT_DESTINATIONS = [
|
|
12
12
|
{
|
|
13
13
|
labelKey: 'account.profile',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/session/account-links.ts"],"sourcesContent":["import type { Locale } from '@entifix/core';\n\n/**\n * The catalog keys naming these destinations, in the `shell` namespace.\n *\n * They are keys of `shell` and not of `app` because this list is shell-owned:\n * the module that authors a string owns its catalog entry and resolves it. When\n * they lived under `app:auth.*`, every host app had to resolve a key belonging\n * to a *different* app's sub-tree just to render this menu.\n *\n * Typed as a union rather than `string` so `useT('shell')` still checks them —\n * no `useTranslateKey` escape hatch is needed to render the menu.\n */\nexport type AccountLabelKey =\n 'account.profile' | 'account.security' | 'account.sessions';\n\n/** An account destination: a catalog key plus the path it lives at. */\nexport interface AccountDestination {\n /** Key in the `shell` namespace, resolved by whichever control renders it. */\n readonly labelKey: AccountLabelKey;\n /** Path within the host serving the account surface, with no locale prefix. */\n readonly path: string;\n}\n\n/**\n * Every account screen, in one list.\n *\n * The sidebar section and the account menu are the same three destinations and\n * were drifting as two hand-kept lists; both build from this one now, so adding\n * a screen is a one-line change here.\n *\n * It lives in the shared shell rather than in the auth shell because the menu\n * is rendered by `BackOfficeShell`, which any host mounts — a
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/session/account-links.ts"],"sourcesContent":["import type { Locale } from '@entifix/core';\n\n/**\n * The catalog keys naming these destinations, in the `shell` namespace.\n *\n * They are keys of `shell` and not of `app` because this list is shell-owned:\n * the module that authors a string owns its catalog entry and resolves it. When\n * they lived under `app:auth.*`, every host app had to resolve a key belonging\n * to a *different* app's sub-tree just to render this menu.\n *\n * Typed as a union rather than `string` so `useT('shell')` still checks them —\n * no `useTranslateKey` escape hatch is needed to render the menu.\n */\nexport type AccountLabelKey =\n 'account.profile' | 'account.security' | 'account.sessions';\n\n/** An account destination: a catalog key plus the path it lives at. */\nexport interface AccountDestination {\n /** Key in the `shell` namespace, resolved by whichever control renders it. */\n readonly labelKey: AccountLabelKey;\n /** Path within the host serving the account surface, with no locale prefix. */\n readonly path: string;\n}\n\n/**\n * Every account screen, in one list.\n *\n * The sidebar section and the account menu are the same three destinations and\n * were drifting as two hand-kept lists; both build from this one now, so adding\n * a screen is a one-line change here.\n *\n * It lives in the shared shell rather than in the auth shell because the menu\n * is rendered by `BackOfficeShell`, which any host mounts — a framework\n * shell cannot reach up into a host's own one.\n */\nexport const ACCOUNT_DESTINATIONS: readonly AccountDestination[] = [\n { labelKey: 'account.profile', path: '/account' },\n // Not `/account/password` any more. r10c holds no password to change, so the\n // screen behind this is a set of links into the provider's own self-service —\n // which is also where a second factor is enrolled and a social account is\n // linked, neither of which had anywhere to live before.\n { labelKey: 'account.security', path: '/account/security' },\n { labelKey: 'account.sessions', path: '/account/sessions' },\n];\n\nexport interface AccountLink {\n readonly labelKey: AccountLabelKey;\n readonly href: string;\n}\n\n/**\n * Account links as locale-prefixed paths on the host that serves them.\n *\n * There is no absolute-URL variant any more. There used to be one, for the apps\n * that linked across to auth-app on another port — the account surface and the\n * back office share an origin now, so every one of these is an ordinary in-app\n * navigation and a cross-origin builder would have no caller.\n */\nexport const accountPaths = (locale: Locale): readonly AccountLink[] =>\n ACCOUNT_DESTINATIONS.map(destination => ({\n labelKey: destination.labelKey,\n href: `/${locale}${destination.path}`,\n }));\n"],"names":["ACCOUNT_DESTINATIONS","labelKey","path","accountPaths","locale","map","destination","href"],"mappings":"AAwBA;;;;;;;;;;CAUC,GACD,OAAO,MAAMA,uBAAsD;IACjE;QAAEC,UAAU;QAAmBC,MAAM;IAAW;IAChD,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,wDAAwD;IACxD;QAAED,UAAU;QAAoBC,MAAM;IAAoB;IAC1D;QAAED,UAAU;QAAoBC,MAAM;IAAoB;CAC3D,CAAC;AAOF;;;;;;;CAOC,GACD,OAAO,MAAMC,eAAe,CAACC,SAC3BJ,qBAAqBK,GAAG,CAACC,CAAAA,cAAgB,CAAA;YACvCL,UAAUK,YAAYL,QAAQ;YAC9BM,MAAM,CAAC,CAAC,EAAEH,SAASE,YAAYJ,IAAI,EAAE;QACvC,CAAA,GAAI"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ScreenType } from '@entifix/authz';
|
|
1
2
|
import { type ReactNode } from 'react';
|
|
2
3
|
/**
|
|
3
4
|
* Navigation intent, decoupled from the host. A page says "go to this list" or
|
|
@@ -11,12 +12,27 @@ export interface EntityNav {
|
|
|
11
12
|
}
|
|
12
13
|
/** Route host: navigation is a canonical URL push under `/catalog`. */
|
|
13
14
|
export declare function useRouteEntityNav(basePath?: string): EntityNav;
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Tab host: navigation opens or focuses a workspace tab of the given screen type.
|
|
17
|
+
*
|
|
18
|
+
* The type is a parameter because a record belongs to the tier its list is in:
|
|
19
|
+
* a stock item opened from an Operaciones list is `operation:stock-item:<id>`,
|
|
20
|
+
* and hardcoding `master` opened it under the wrong kind — which the registry
|
|
21
|
+
* cannot resolve, and whose draft nothing reads back.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useTabEntityNav(type?: ScreenType): EntityNav;
|
|
16
24
|
export declare function EntityNavProvider({ value, children, }: {
|
|
17
25
|
value: EntityNav;
|
|
18
26
|
children: ReactNode;
|
|
19
27
|
}): import("react").JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* The {@link EntityNav} a host mounted, or `undefined` on a plain route.
|
|
30
|
+
*
|
|
31
|
+
* For a page that must behave differently inside a tab — follow a link's `href`
|
|
32
|
+
* on a route, but open a tab instead of leaving the workspace — where
|
|
33
|
+
* {@link useEntityNav}'s route fallback would hide which host it is in.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useEntityNavHost(): EntityNav | undefined;
|
|
20
36
|
/**
|
|
21
37
|
* The active {@link EntityNav}. Falls back to route navigation when no provider
|
|
22
38
|
* is mounted, so a page rendered as a plain route (its default host) navigates
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-nav.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-nav.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entity-nav.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-nav.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAAiB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAI3E;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAID,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,QAAQ,SAAa,GAAG,SAAS,CAUlE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,UAAqB,GAAG,SAAS,CAiBtE;AAED,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAMA;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,SAAS,GAAG,SAAS,CAExD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAIxC"}
|
|
@@ -15,26 +15,34 @@ const EntityNavContext = /*#__PURE__*/ createContext(null);
|
|
|
15
15
|
basePath
|
|
16
16
|
]);
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Tab host: navigation opens or focuses a workspace tab of the given screen type.
|
|
20
|
+
*
|
|
21
|
+
* The type is a parameter because a record belongs to the tier its list is in:
|
|
22
|
+
* a stock item opened from an Operaciones list is `operation:stock-item:<id>`,
|
|
23
|
+
* and hardcoding `master` opened it under the wrong kind — which the registry
|
|
24
|
+
* cannot resolve, and whose draft nothing reads back.
|
|
25
|
+
*/ export function useTabEntityNav(type = 'master') {
|
|
19
26
|
const open = useTabsState((state)=>state.open);
|
|
20
27
|
return useMemo(()=>({
|
|
21
28
|
toList: (entityKey)=>open({
|
|
22
29
|
param: screenAddress({
|
|
23
|
-
type
|
|
30
|
+
type,
|
|
24
31
|
key: entityKey
|
|
25
32
|
}),
|
|
26
33
|
title: entityKey
|
|
27
34
|
}),
|
|
28
35
|
toEntity: (entityKey, id)=>open({
|
|
29
36
|
param: screenAddress({
|
|
30
|
-
type
|
|
37
|
+
type,
|
|
31
38
|
key: entityKey,
|
|
32
39
|
id
|
|
33
40
|
}),
|
|
34
41
|
title: `${entityKey} #${id}`
|
|
35
42
|
})
|
|
36
43
|
}), [
|
|
37
|
-
open
|
|
44
|
+
open,
|
|
45
|
+
type
|
|
38
46
|
]);
|
|
39
47
|
}
|
|
40
48
|
export function EntityNavProvider({ value, children }) {
|
|
@@ -43,6 +51,16 @@ export function EntityNavProvider({ value, children }) {
|
|
|
43
51
|
children: children
|
|
44
52
|
});
|
|
45
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The {@link EntityNav} a host mounted, or `undefined` on a plain route.
|
|
56
|
+
*
|
|
57
|
+
* For a page that must behave differently inside a tab — follow a link's `href`
|
|
58
|
+
* on a route, but open a tab instead of leaving the workspace — where
|
|
59
|
+
* {@link useEntityNav}'s route fallback would hide which host it is in.
|
|
60
|
+
*/ export function useEntityNavHost() {
|
|
61
|
+
var _useContext;
|
|
62
|
+
return (_useContext = useContext(EntityNavContext)) != null ? _useContext : undefined;
|
|
63
|
+
}
|
|
46
64
|
/**
|
|
47
65
|
* The active {@link EntityNav}. Falls back to route navigation when no provider
|
|
48
66
|
* is mounted, so a page rendered as a plain route (its default host) navigates
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/workspace/entity-nav.tsx"],"sourcesContent":["'use client';\n\nimport { screenAddress } from '@entifix/authz';\nimport { useRouter } from 'next/navigation';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\n\nimport { useTabsState } from './tabs-state.js';\n\n/**\n * Navigation intent, decoupled from the host. A page says \"go to this list\" or\n * \"open this entity\"; the host decides how. In a route the destination is a URL\n * push; in the workspace it is a tab open/focus. A page calls {@link useEntityNav}\n * and never learns which host it is in.\n */\nexport interface EntityNav {\n toList(entityKey: string): void;\n toEntity(entityKey: string, id: string): void;\n}\n\nconst EntityNavContext = createContext<EntityNav | null>(null);\n\n/** Route host: navigation is a canonical URL push under `/catalog`. */\nexport function useRouteEntityNav(basePath = '/catalog'): EntityNav {\n const router = useRouter();\n return useMemo(\n () => ({\n toList: entityKey => router.push(`${basePath}/${entityKey}`),\n toEntity: (entityKey, id) =>\n router.push(`${basePath}/${entityKey}/${id}`),\n }),\n [router, basePath],\n );\n}\n\n
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/workspace/entity-nav.tsx"],"sourcesContent":["'use client';\n\nimport { screenAddress, type ScreenType } from '@entifix/authz';\nimport { useRouter } from 'next/navigation';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\n\nimport { useTabsState } from './tabs-state.js';\n\n/**\n * Navigation intent, decoupled from the host. A page says \"go to this list\" or\n * \"open this entity\"; the host decides how. In a route the destination is a URL\n * push; in the workspace it is a tab open/focus. A page calls {@link useEntityNav}\n * and never learns which host it is in.\n */\nexport interface EntityNav {\n toList(entityKey: string): void;\n toEntity(entityKey: string, id: string): void;\n}\n\nconst EntityNavContext = createContext<EntityNav | null>(null);\n\n/** Route host: navigation is a canonical URL push under `/catalog`. */\nexport function useRouteEntityNav(basePath = '/catalog'): EntityNav {\n const router = useRouter();\n return useMemo(\n () => ({\n toList: entityKey => router.push(`${basePath}/${entityKey}`),\n toEntity: (entityKey, id) =>\n router.push(`${basePath}/${entityKey}/${id}`),\n }),\n [router, basePath],\n );\n}\n\n/**\n * Tab host: navigation opens or focuses a workspace tab of the given screen type.\n *\n * The type is a parameter because a record belongs to the tier its list is in:\n * a stock item opened from an Operaciones list is `operation:stock-item:<id>`,\n * and hardcoding `master` opened it under the wrong kind — which the registry\n * cannot resolve, and whose draft nothing reads back.\n */\nexport function useTabEntityNav(type: ScreenType = 'master'): EntityNav {\n const open = useTabsState(state => state.open);\n return useMemo(\n () => ({\n toList: entityKey =>\n open({\n param: screenAddress({ type, key: entityKey }),\n title: entityKey,\n }),\n toEntity: (entityKey, id) =>\n open({\n param: screenAddress({ type, key: entityKey, id }),\n title: `${entityKey} #${id}`,\n }),\n }),\n [open, type],\n );\n}\n\nexport function EntityNavProvider({\n value,\n children,\n}: {\n value: EntityNav;\n children: ReactNode;\n}) {\n return (\n <EntityNavContext.Provider value={value}>\n {children}\n </EntityNavContext.Provider>\n );\n}\n\n/**\n * The {@link EntityNav} a host mounted, or `undefined` on a plain route.\n *\n * For a page that must behave differently inside a tab — follow a link's `href`\n * on a route, but open a tab instead of leaving the workspace — where\n * {@link useEntityNav}'s route fallback would hide which host it is in.\n */\nexport function useEntityNavHost(): EntityNav | undefined {\n return useContext(EntityNavContext) ?? undefined;\n}\n\n/**\n * The active {@link EntityNav}. Falls back to route navigation when no provider\n * is mounted, so a page rendered as a plain route (its default host) navigates\n * by URL without any wiring; the workspace wraps its tabs in a tab-nav provider.\n */\nexport function useEntityNav(): EntityNav {\n const provided = useContext(EntityNavContext);\n const routeNav = useRouteEntityNav();\n return provided ?? routeNav;\n}\n"],"names":["screenAddress","useRouter","createContext","useContext","useMemo","useTabsState","EntityNavContext","useRouteEntityNav","basePath","router","toList","entityKey","push","toEntity","id","useTabEntityNav","type","open","state","param","key","title","EntityNavProvider","value","children","Provider","useEntityNavHost","undefined","useEntityNav","provided","routeNav"],"mappings":"AAAA;;AAEA,SAASA,aAAa,QAAyB,iBAAiB;AAChE,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,aAAa,EAAkBC,UAAU,EAAEC,OAAO,QAAQ,QAAQ;AAE3E,SAASC,YAAY,QAAQ,kBAAkB;AAa/C,MAAMC,iCAAmBJ,cAAgC;AAEzD,qEAAqE,GACrE,OAAO,SAASK,kBAAkBC,WAAW,UAAU;IACrD,MAAMC,SAASR;IACf,OAAOG,QACL,IAAO,CAAA;YACLM,QAAQC,CAAAA,YAAaF,OAAOG,IAAI,CAAC,GAAGJ,SAAS,CAAC,EAAEG,WAAW;YAC3DE,UAAU,CAACF,WAAWG,KACpBL,OAAOG,IAAI,CAAC,GAAGJ,SAAS,CAAC,EAAEG,UAAU,CAAC,EAAEG,IAAI;QAChD,CAAA,GACA;QAACL;QAAQD;KAAS;AAEtB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASO,gBAAgBC,OAAmB,QAAQ;IACzD,MAAMC,OAAOZ,aAAaa,CAAAA,QAASA,MAAMD,IAAI;IAC7C,OAAOb,QACL,IAAO,CAAA;YACLM,QAAQC,CAAAA,YACNM,KAAK;oBACHE,OAAOnB,cAAc;wBAAEgB;wBAAMI,KAAKT;oBAAU;oBAC5CU,OAAOV;gBACT;YACFE,UAAU,CAACF,WAAWG,KACpBG,KAAK;oBACHE,OAAOnB,cAAc;wBAAEgB;wBAAMI,KAAKT;wBAAWG;oBAAG;oBAChDO,OAAO,GAAGV,UAAU,EAAE,EAAEG,IAAI;gBAC9B;QACJ,CAAA,GACA;QAACG;QAAMD;KAAK;AAEhB;AAEA,OAAO,SAASM,kBAAkB,EAChCC,KAAK,EACLC,QAAQ,EAIT;IACC,qBACE,KAAClB,iBAAiBmB,QAAQ;QAACF,OAAOA;kBAC/BC;;AAGP;AAEA;;;;;;CAMC,GACD,OAAO,SAASE;QACPvB;IAAP,QAAOA,cAAAA,WAAWG,6BAAXH,cAAgCwB;AACzC;AAEA;;;;CAIC,GACD,OAAO,SAASC;IACd,MAAMC,WAAW1B,WAAWG;IAC5B,MAAMwB,WAAWvB;IACjB,OAAOsB,mBAAAA,WAAYC;AACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-tab-kind.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"entity-tab-kind.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAiB7C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,MAAM,IAAI,SAAS,CAAC;CACrB;AAED,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BvC"}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
import { parseScreenPayload } from '@entifix/authz';
|
|
4
|
+
import { CATALOG_NEW_SLUG } from '../crud/slug.js';
|
|
5
|
+
import { EntityNavProvider, useTabEntityNav } from './entity-nav.js';
|
|
6
|
+
/**
|
|
7
|
+
* Makes every page rendered in a tab of this kind navigate by tab.
|
|
8
|
+
*
|
|
9
|
+
* A generated page asks {@link useEntityNavHost} whether a host is mounted; with
|
|
10
|
+
* this around it, opening a record or going back opens a tab of the same kind
|
|
11
|
+
* rather than following the link out of the workspace.
|
|
12
|
+
*/ function TabNav({ type, children }) {
|
|
13
|
+
return /*#__PURE__*/ _jsx(EntityNavProvider, {
|
|
14
|
+
value: useTabEntityNav(type),
|
|
15
|
+
children: children
|
|
16
|
+
});
|
|
17
|
+
}
|
|
3
18
|
/**
|
|
4
19
|
* An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one
|
|
5
20
|
* record.
|
|
@@ -39,8 +54,11 @@ import { parseScreenPayload } from '@entifix/authz';
|
|
|
39
54
|
return known ? parsed : null;
|
|
40
55
|
},
|
|
41
56
|
toParam: (addr)=>addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,
|
|
42
|
-
title: (addr, translate)=>addr.id === undefined ? translate(screens.lists[addr.key].titleKey) : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,
|
|
43
|
-
render: (addr)
|
|
57
|
+
title: (addr, translate)=>addr.id === undefined ? translate(screens.lists[addr.key].titleKey) : addr.id === CATALOG_NEW_SLUG ? `${translate(screens.records[addr.key].labelKey)} · ${translate('shell:breadcrumbs.new')}` : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,
|
|
58
|
+
render: (addr)=>/*#__PURE__*/ _jsx(TabNav, {
|
|
59
|
+
type: type,
|
|
60
|
+
children: addr.id === undefined ? screens.lists[addr.key].render() : screens.records[addr.key].render(addr.id)
|
|
61
|
+
})
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
64
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"sourcesContent":["'use client';\n\nimport { parseScreenPayload, type ScreenType } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nimport type { TabKind } from './tab-kind.js';\n\n/** One list a workspace may open — the collection half of a screen. */\nexport interface EntityTabList {\n /** The tab caption's catalog key. `@entity({ pluralKey })` for a generated screen. */\n readonly titleKey: string;\n render(): ReactNode;\n}\n\n/** One record a workspace may open — the same screen, with an id. */\nexport interface EntityTabRecord {\n /** The tab caption's catalog key. `@entity({ labelKey })` for a generated screen. */\n readonly labelKey: string;\n render(id: string): ReactNode;\n}\n\nexport interface EntityTabScreens {\n readonly lists: Record<string, EntityTabList>;\n readonly records: Record<string, EntityTabRecord>;\n}\n\n/**\n * An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one\n * record.\n *\n * **The list and the record are not different kinds.** They are the same screen\n * with and without a record, which is exactly what the optional id in the\n * payload says — three kinds (`catalog:`, `entity:`, `system:`) collapsed into\n * one for that reason, and the prefix names the *taxonomy* now\n * ([ADR 0042](../../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).\n *\n * A factory over the {@link ScreenType} rather than a constant, and that is the\n * whole of what makes an Operaciones screen addressable. It was written inline\n * in back-office-app as a `masterKind`, so `master:` was the only entity\n * address any host could resolve and `operation:` matched nothing at all — a\n * second copy of this parser beside it is exactly the drift the registry was\n * consolidated to stop, and the two would have had to agree about a grammar\n * neither owns.\n *\n * Which screens a host offers stays the host's decision — a second host\n * mounting the same shells may want a different set — so the screens arrive as\n * an argument. What no longer varies is how one is addressed.\n *\n * `render` takes the id rather than a component, so the caller decides what\n * hosting a record means: in a workspace that is an editor wired to an\n * address-keyed autosaved draft, and the address it keys on has to carry this\n * same `type`.\n */\nexport function entityTabKind(\n type: ScreenType,\n screens: EntityTabScreens,\n): TabKind<{ key: string; id?: string }> {\n return {\n kind: type,\n match: payload => {\n const parsed = parseScreenPayload(payload);\n if (parsed === null) return null;\n // A key is valid for the half it names: a record address for a screen\n // with a list and no record editor resolves to nothing, and the workspace\n // answers with its fallback rather than rendering an empty editor.\n const known =\n parsed.id === undefined\n ? parsed.key in screens.lists\n : parsed.key in screens.records;\n return known ? parsed : null;\n },\n toParam: addr =>\n addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,\n title: (addr, translate) =>\n addr.id === undefined\n ? translate(screens.lists[addr.key].titleKey)\n : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,\n render: addr
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"sourcesContent":["'use client';\n\nimport { parseScreenPayload, type ScreenType } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nimport { CATALOG_NEW_SLUG } from '../crud/slug.js';\nimport { EntityNavProvider, useTabEntityNav } from './entity-nav.js';\nimport type { TabKind } from './tab-kind.js';\n\n/**\n * Makes every page rendered in a tab of this kind navigate by tab.\n *\n * A generated page asks {@link useEntityNavHost} whether a host is mounted; with\n * this around it, opening a record or going back opens a tab of the same kind\n * rather than following the link out of the workspace.\n */\nfunction TabNav({ type, children }: { type: ScreenType; children: ReactNode }) {\n return (\n <EntityNavProvider value={useTabEntityNav(type)}>\n {children}\n </EntityNavProvider>\n );\n}\n\n/** One list a workspace may open — the collection half of a screen. */\nexport interface EntityTabList {\n /** The tab caption's catalog key. `@entity({ pluralKey })` for a generated screen. */\n readonly titleKey: string;\n render(): ReactNode;\n}\n\n/** One record a workspace may open — the same screen, with an id. */\nexport interface EntityTabRecord {\n /** The tab caption's catalog key. `@entity({ labelKey })` for a generated screen. */\n readonly labelKey: string;\n render(id: string): ReactNode;\n}\n\nexport interface EntityTabScreens {\n readonly lists: Record<string, EntityTabList>;\n readonly records: Record<string, EntityTabRecord>;\n}\n\n/**\n * An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one\n * record.\n *\n * **The list and the record are not different kinds.** They are the same screen\n * with and without a record, which is exactly what the optional id in the\n * payload says — three kinds (`catalog:`, `entity:`, `system:`) collapsed into\n * one for that reason, and the prefix names the *taxonomy* now\n * ([ADR 0042](../../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).\n *\n * A factory over the {@link ScreenType} rather than a constant, and that is the\n * whole of what makes an Operaciones screen addressable. It was written inline\n * in back-office-app as a `masterKind`, so `master:` was the only entity\n * address any host could resolve and `operation:` matched nothing at all — a\n * second copy of this parser beside it is exactly the drift the registry was\n * consolidated to stop, and the two would have had to agree about a grammar\n * neither owns.\n *\n * Which screens a host offers stays the host's decision — a second host\n * mounting the same shells may want a different set — so the screens arrive as\n * an argument. What no longer varies is how one is addressed.\n *\n * `render` takes the id rather than a component, so the caller decides what\n * hosting a record means: in a workspace that is an editor wired to an\n * address-keyed autosaved draft, and the address it keys on has to carry this\n * same `type`.\n */\nexport function entityTabKind(\n type: ScreenType,\n screens: EntityTabScreens,\n): TabKind<{ key: string; id?: string }> {\n return {\n kind: type,\n match: payload => {\n const parsed = parseScreenPayload(payload);\n if (parsed === null) return null;\n // A key is valid for the half it names: a record address for a screen\n // with a list and no record editor resolves to nothing, and the workspace\n // answers with its fallback rather than rendering an empty editor.\n const known =\n parsed.id === undefined\n ? parsed.key in screens.lists\n : parsed.key in screens.records;\n return known ? parsed : null;\n },\n toParam: addr =>\n addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,\n title: (addr, translate) =>\n addr.id === undefined\n ? translate(screens.lists[addr.key].titleKey)\n : addr.id === CATALOG_NEW_SLUG\n ? `${translate(screens.records[addr.key].labelKey)} · ${translate('shell:breadcrumbs.new')}`\n : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,\n render: addr => (\n <TabNav type={type}>\n {addr.id === undefined\n ? screens.lists[addr.key].render()\n : screens.records[addr.key].render(addr.id)}\n </TabNav>\n ),\n };\n}\n"],"names":["parseScreenPayload","CATALOG_NEW_SLUG","EntityNavProvider","useTabEntityNav","TabNav","type","children","value","entityTabKind","screens","kind","match","payload","parsed","known","id","undefined","key","lists","records","toParam","addr","title","translate","titleKey","labelKey","render"],"mappings":"AAAA;;AAEA,SAASA,kBAAkB,QAAyB,iBAAiB;AAGrE,SAASC,gBAAgB,QAAQ,kBAAkB;AACnD,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,kBAAkB;AAGrE;;;;;;CAMC,GACD,SAASC,OAAO,EAAEC,IAAI,EAAEC,QAAQ,EAA6C;IAC3E,qBACE,KAACJ;QAAkBK,OAAOJ,gBAAgBE;kBACvCC;;AAGP;AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASE,cACdH,IAAgB,EAChBI,OAAyB;IAEzB,OAAO;QACLC,MAAML;QACNM,OAAOC,CAAAA;YACL,MAAMC,SAASb,mBAAmBY;YAClC,IAAIC,WAAW,MAAM,OAAO;YAC5B,sEAAsE;YACtE,0EAA0E;YAC1E,mEAAmE;YACnE,MAAMC,QACJD,OAAOE,EAAE,KAAKC,YACVH,OAAOI,GAAG,IAAIR,QAAQS,KAAK,GAC3BL,OAAOI,GAAG,IAAIR,QAAQU,OAAO;YACnC,OAAOL,QAAQD,SAAS;QAC1B;QACAO,SAASC,CAAAA,OACPA,KAAKN,EAAE,KAAKC,YAAYK,KAAKJ,GAAG,GAAG,GAAGI,KAAKJ,GAAG,CAAC,CAAC,EAAEI,KAAKN,EAAE,EAAE;QAC7DO,OAAO,CAACD,MAAME,YACZF,KAAKN,EAAE,KAAKC,YACRO,UAAUd,QAAQS,KAAK,CAACG,KAAKJ,GAAG,CAAC,CAACO,QAAQ,IAC1CH,KAAKN,EAAE,KAAKd,mBACV,GAAGsB,UAAUd,QAAQU,OAAO,CAACE,KAAKJ,GAAG,CAAC,CAACQ,QAAQ,EAAE,GAAG,EAAEF,UAAU,0BAA0B,GAC1F,GAAGA,UAAUd,QAAQU,OAAO,CAACE,KAAKJ,GAAG,CAAC,CAACQ,QAAQ,EAAE,EAAE,EAAEJ,KAAKN,EAAE,EAAE;QACtEW,QAAQL,CAAAA,qBACN,KAACjB;gBAAOC,MAAMA;0BACXgB,KAAKN,EAAE,KAAKC,YACTP,QAAQS,KAAK,CAACG,KAAKJ,GAAG,CAAC,CAACS,MAAM,KAC9BjB,QAAQU,OAAO,CAACE,KAAKJ,GAAG,CAAC,CAACS,MAAM,CAACL,KAAKN,EAAE;;IAGlD;AACF"}
|
package/package.json
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
"name": "@entifix/next-shell",
|
|
3
3
|
"nx": {
|
|
4
4
|
"tags": [
|
|
5
|
-
"layer:shell",
|
|
6
|
-
"scope:shared",
|
|
7
|
-
"shell:base",
|
|
8
5
|
"tier:4"
|
|
9
6
|
],
|
|
10
7
|
"sourceRoot": "packages/next/shell/src",
|
|
@@ -24,7 +21,7 @@
|
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
},
|
|
27
|
-
"version": "0.1.
|
|
24
|
+
"version": "0.1.3",
|
|
28
25
|
"description": "Next.js application shell: back-office chrome, workspace tabs, CRUD screens derived from entity metadata, and session and proxy routes.",
|
|
29
26
|
"license": "MIT",
|
|
30
27
|
"repository": {
|
|
@@ -62,17 +59,17 @@
|
|
|
62
59
|
"react": "19.2.8",
|
|
63
60
|
"react-dom": "19.2.8",
|
|
64
61
|
"zustand": "^5.0.15",
|
|
65
|
-
"@entifix/testing-unit": "0.1.
|
|
62
|
+
"@entifix/testing-unit": "0.1.3"
|
|
66
63
|
},
|
|
67
64
|
"dependencies": {
|
|
68
65
|
"idb": "^8.0.3",
|
|
69
|
-
"@entifix/
|
|
70
|
-
"@entifix/
|
|
71
|
-
"@entifix/
|
|
72
|
-
"@entifix/
|
|
73
|
-
"@entifix/
|
|
74
|
-
"@entifix/
|
|
75
|
-
"@entifix/
|
|
66
|
+
"@entifix/react-controls": "0.1.3",
|
|
67
|
+
"@entifix/authz": "0.1.3",
|
|
68
|
+
"@entifix/transactions": "0.1.3",
|
|
69
|
+
"@entifix/react-integration": "0.1.3",
|
|
70
|
+
"@entifix/core": "0.1.3",
|
|
71
|
+
"@entifix/i18n": "0.1.3",
|
|
72
|
+
"@entifix/business": "0.1.3"
|
|
76
73
|
},
|
|
77
74
|
"files": [
|
|
78
75
|
"dist",
|