@burojs/ui-shadcn 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/{bulk-summary-2HLgz7Il.cjs → bulk-summary-Chc61Pyw.cjs} +9 -7
  2. package/dist/bulk-summary-Chc61Pyw.cjs.map +1 -0
  3. package/dist/{bulk-summary-D6A0ua21.js → bulk-summary-DpSQD2Fq.js} +9 -7
  4. package/dist/bulk-summary-DpSQD2Fq.js.map +1 -0
  5. package/dist/{candidates-relation-picker-QvoY2bOQ.js → candidates-relation-picker-DRsHKe6N.js} +3 -3
  6. package/dist/{candidates-relation-picker-QvoY2bOQ.js.map → candidates-relation-picker-DRsHKe6N.js.map} +1 -1
  7. package/dist/{candidates-relation-picker-pEAWKQEY.cjs → candidates-relation-picker-GIxsp7D_.cjs} +3 -3
  8. package/dist/{candidates-relation-picker-pEAWKQEY.cjs.map → candidates-relation-picker-GIxsp7D_.cjs.map} +1 -1
  9. package/dist/{check-confirm-dialog-W-8WOmGh.cjs → check-confirm-dialog-BMSb7wFv.cjs} +17 -9
  10. package/dist/check-confirm-dialog-BMSb7wFv.cjs.map +1 -0
  11. package/dist/{check-confirm-dialog-B-CbJTln.js → check-confirm-dialog-B_u3YVHS.js} +18 -10
  12. package/dist/{check-confirm-dialog-B-CbJTln.js.map → check-confirm-dialog-B_u3YVHS.js.map} +1 -1
  13. package/dist/{filter-builder-04nas3eq.js → filter-builder-DX-xC3Yd.js} +2 -2
  14. package/dist/{filter-builder-04nas3eq.js.map → filter-builder-DX-xC3Yd.js.map} +1 -1
  15. package/dist/{filter-builder-DQX-TMFh.cjs → filter-builder-Dt5E8PSX.cjs} +2 -2
  16. package/dist/{filter-builder-DQX-TMFh.cjs.map → filter-builder-Dt5E8PSX.cjs.map} +1 -1
  17. package/dist/{flow-runtime-DyNIYfUP.cjs → flow-runtime-CQ4wf8co.cjs} +2 -2
  18. package/dist/{flow-runtime-DyNIYfUP.cjs.map → flow-runtime-CQ4wf8co.cjs.map} +1 -1
  19. package/dist/{flow-runtime-BEIyym7d.js → flow-runtime-Q2vD7R3c.js} +2 -2
  20. package/dist/{flow-runtime-BEIyym7d.js.map → flow-runtime-Q2vD7R3c.js.map} +1 -1
  21. package/dist/{flow-screen-DY1Z9PbG.js → flow-screen-B1y2yizP.js} +18 -18
  22. package/dist/flow-screen-B1y2yizP.js.map +1 -0
  23. package/dist/{flow-screen-FE5Xhb3t.cjs → flow-screen-sw2cP70t.cjs} +18 -18
  24. package/dist/flow-screen-sw2cP70t.cjs.map +1 -0
  25. package/dist/index.cjs +2183 -109
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +76 -8
  28. package/dist/index.d.ts +76 -8
  29. package/dist/index.js +2095 -24
  30. package/dist/index.js.map +1 -1
  31. package/dist/{inline-flow-host-yVLo6-sM.cjs → inline-flow-host-CkhoA69R.cjs} +3 -3
  32. package/dist/{inline-flow-host-yVLo6-sM.cjs.map → inline-flow-host-CkhoA69R.cjs.map} +1 -1
  33. package/dist/{inline-flow-host-D6u-RlsV.js → inline-flow-host-DqbXqcPS.js} +3 -3
  34. package/dist/{inline-flow-host-D6u-RlsV.js.map → inline-flow-host-DqbXqcPS.js.map} +1 -1
  35. package/dist/{lazy-relation-picker-dialog-BaORufxQ.js → lazy-relation-picker-dialog-BaIEWLbF.js} +3 -52
  36. package/dist/lazy-relation-picker-dialog-BaIEWLbF.js.map +1 -0
  37. package/dist/{lazy-relation-picker-dialog-C_VlGlDz.cjs → lazy-relation-picker-dialog-CeRzOAi0.cjs} +3 -52
  38. package/dist/lazy-relation-picker-dialog-CeRzOAi0.cjs.map +1 -0
  39. package/dist/{page-flow-route-CyMzUidK.js → page-flow-route-CP7cS7FY.js} +5 -5
  40. package/dist/page-flow-route-CP7cS7FY.js.map +1 -0
  41. package/dist/{page-flow-route-OFx7h7Ja.cjs → page-flow-route-UDx8qvdM.cjs} +9 -9
  42. package/dist/page-flow-route-UDx8qvdM.cjs.map +1 -0
  43. package/dist/{place-node-BZF5Af6x.cjs → place-node-C5ZmuHaN.cjs} +149 -40
  44. package/dist/place-node-C5ZmuHaN.cjs.map +1 -0
  45. package/dist/{place-node-DRjmJGQ_.js → place-node-DwWAigv0.js} +146 -43
  46. package/dist/place-node-DwWAigv0.js.map +1 -0
  47. package/dist/record-trail-store-Cjo9vtod.cjs +126 -0
  48. package/dist/record-trail-store-Cjo9vtod.cjs.map +1 -0
  49. package/dist/record-trail-store-Dn6bVry4.js +91 -0
  50. package/dist/record-trail-store-Dn6bVry4.js.map +1 -0
  51. package/dist/{related-list-cards-xVvgSlu-.cjs → related-list-cards-BypKfm9t.cjs} +3 -3
  52. package/dist/{related-list-cards-xVvgSlu-.cjs.map → related-list-cards-BypKfm9t.cjs.map} +1 -1
  53. package/dist/{related-list-cards-2hLitVkg.js → related-list-cards-Dcmruy09.js} +3 -3
  54. package/dist/{related-list-cards-2hLitVkg.js.map → related-list-cards-Dcmruy09.js.map} +1 -1
  55. package/dist/{related-list-timeline-Dsf5WewG.cjs → related-list-timeline-D-KClgKb.cjs} +4 -4
  56. package/dist/{related-list-timeline-Dsf5WewG.cjs.map → related-list-timeline-D-KClgKb.cjs.map} +1 -1
  57. package/dist/{related-list-timeline-CxJqe6eK.js → related-list-timeline-DZON_qSc.js} +4 -4
  58. package/dist/{related-list-timeline-CxJqe6eK.js.map → related-list-timeline-DZON_qSc.js.map} +1 -1
  59. package/dist/{related-rows-DDhp9axl.cjs → related-rows-DgF05ssi.cjs} +4 -4
  60. package/dist/{related-rows-DDhp9axl.cjs.map → related-rows-DgF05ssi.cjs.map} +1 -1
  61. package/dist/{related-rows-DL6CWsrj.js → related-rows-ETZl5CB9.js} +4 -4
  62. package/dist/{related-rows-DL6CWsrj.js.map → related-rows-ETZl5CB9.js.map} +1 -1
  63. package/dist/{relation-create-_SFuPQGM.cjs → relation-create-B8zngAcA.cjs} +46 -11
  64. package/dist/relation-create-B8zngAcA.cjs.map +1 -0
  65. package/dist/{relation-create-6WicROWA.js → relation-create-CeCWyxIb.js} +47 -12
  66. package/dist/relation-create-CeCWyxIb.js.map +1 -0
  67. package/dist/{relation-many-picker-mGEQDWo2.js → relation-many-picker-CT7nKfE2.js} +4 -4
  68. package/dist/{relation-many-picker-mGEQDWo2.js.map → relation-many-picker-CT7nKfE2.js.map} +1 -1
  69. package/dist/{relation-many-picker-BBNsmkaX.cjs → relation-many-picker-Cq0EHY5I.cjs} +4 -4
  70. package/dist/{relation-many-picker-BBNsmkaX.cjs.map → relation-many-picker-Cq0EHY5I.cjs.map} +1 -1
  71. package/dist/{relation-picker-BbO41cFV.js → relation-picker-CCyuMeGq.js} +6 -6
  72. package/dist/{relation-picker-BbO41cFV.js.map → relation-picker-CCyuMeGq.js.map} +1 -1
  73. package/dist/{relation-picker-jk9D_uwn.cjs → relation-picker-DmwTzgsd.cjs} +6 -6
  74. package/dist/{relation-picker-jk9D_uwn.cjs.map → relation-picker-DmwTzgsd.cjs.map} +1 -1
  75. package/dist/{relation-picker-dialog-BgSG_8bU.js → relation-picker-dialog-CoWKjNv8.js} +4 -4
  76. package/dist/{relation-picker-dialog-BgSG_8bU.js.map → relation-picker-dialog-CoWKjNv8.js.map} +1 -1
  77. package/dist/{relation-picker-dialog-Duqy2r0n.cjs → relation-picker-dialog-mbwyNK43.cjs} +4 -4
  78. package/dist/{relation-picker-dialog-Duqy2r0n.cjs.map → relation-picker-dialog-mbwyNK43.cjs.map} +1 -1
  79. package/dist/{relation-select-C3aV18kT.cjs → relation-select-CguIo2M5.cjs} +2 -2
  80. package/dist/{relation-select-C3aV18kT.cjs.map → relation-select-CguIo2M5.cjs.map} +1 -1
  81. package/dist/{relation-select-CjQT9LpS.js → relation-select-nAYxhopN.js} +2 -2
  82. package/dist/{relation-select-CjQT9LpS.js.map → relation-select-nAYxhopN.js.map} +1 -1
  83. package/dist/{use-dialog-filter-state-lsTJxrB4.cjs → use-dialog-filter-state-CpWblz3C.cjs} +2 -2
  84. package/dist/{use-dialog-filter-state-lsTJxrB4.cjs.map → use-dialog-filter-state-CpWblz3C.cjs.map} +1 -1
  85. package/dist/{use-dialog-filter-state-CJoeY-1j.js → use-dialog-filter-state-ZYxOp5WO.js} +2 -2
  86. package/dist/{use-dialog-filter-state-CJoeY-1j.js.map → use-dialog-filter-state-ZYxOp5WO.js.map} +1 -1
  87. package/dist/{use-record-subtitle-BJ9rnj6F.cjs → use-record-subtitle-B4lTR7pf.cjs} +2 -2
  88. package/dist/{use-record-subtitle-BJ9rnj6F.cjs.map → use-record-subtitle-B4lTR7pf.cjs.map} +1 -1
  89. package/dist/{use-record-subtitle-w3ZhXNlf.js → use-record-subtitle-pWMBqQx9.js} +2 -2
  90. package/dist/{use-record-subtitle-w3ZhXNlf.js.map → use-record-subtitle-pWMBqQx9.js.map} +1 -1
  91. package/package.json +9 -9
  92. package/dist/bulk-summary-2HLgz7Il.cjs.map +0 -1
  93. package/dist/bulk-summary-D6A0ua21.js.map +0 -1
  94. package/dist/check-confirm-dialog-W-8WOmGh.cjs.map +0 -1
  95. package/dist/flow-screen-DY1Z9PbG.js.map +0 -1
  96. package/dist/flow-screen-FE5Xhb3t.cjs.map +0 -1
  97. package/dist/lazy-relation-picker-dialog-BaORufxQ.js.map +0 -1
  98. package/dist/lazy-relation-picker-dialog-C_VlGlDz.cjs.map +0 -1
  99. package/dist/page-flow-route-CyMzUidK.js.map +0 -1
  100. package/dist/page-flow-route-OFx7h7Ja.cjs.map +0 -1
  101. package/dist/place-node-BZF5Af6x.cjs.map +0 -1
  102. package/dist/place-node-DRjmJGQ_.js.map +0 -1
  103. package/dist/relation-create-6WicROWA.js.map +0 -1
  104. package/dist/relation-create-_SFuPQGM.cjs.map +0 -1
  105. package/dist/resource-page-router-CzaNATSp.js +0 -2010
  106. package/dist/resource-page-router-CzaNATSp.js.map +0 -1
  107. package/dist/resource-page-router-dmVIeu5s.cjs +0 -2231
  108. package/dist/resource-page-router-dmVIeu5s.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"relation-picker-dialog-BgSG_8bU.js","names":[],"sources":["../src/fields/relation-picker-dialog.tsx"],"sourcesContent":["'use client';\nimport * as React from 'react';\nimport {\n IconArrowDown,\n IconArrowUp,\n IconCheck,\n IconChevronRight,\n IconSelector,\n IconLoader2,\n IconSearch,\n} from '@tabler/icons-react';\nimport {\n type FixedFilterInput,\n type ScopeSource,\n declaredField,\n declaredText,\n enumNamespaceOf,\n enumValueText,\n fieldOptionText,\n resolveFieldLabel,\n resolveResourceLabel,\n routeResourceKey,\n type ResourceNameOrRef,\n columnName,\n} from '@burojs/core';\nimport {\n recordKeyOf,\n useApp,\n useChromeT,\n useI18nCaps,\n useList,\n useListAccess,\n useOptionalT,\n} from '@burojs/react';\nimport { useRowTitle } from '../resource/use-row-title.js';\nimport { ResolvedFieldDisplay } from './resolved-field.js';\nimport { cn } from '../lib/cn.js';\nimport { Button } from '../primitives/button.js';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog.js';\nimport { Input } from '../primitives/input.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '../primitives/table.js';\nimport { FilterBuilder } from '../list-kit/filter-builder/filter-builder.js';\nimport { useDialogFilterState } from './use-dialog-filter-state.js';\nimport { RelationCreateButton, type SelectCreate } from './relation-create.js';\n\nconst noop = (): void => undefined;\n\n/**\n * Advanced-search modal for the relation picker (#11 / #9). The inline popover\n * handles the quick \"type a few letters\" case; this dialog is the escalation:\n * the full target list with SEARCH, per-column FILTERS, SORTING and PAGINATION,\n * plus a \"Create\" shortcut. Resolved generically from the registry so it works\n * for any resource with no per-field wiring.\n *\n * #9 — compact, and the table area is a FIXED height so paging never resizes /\n * jumps the dialog; responsive down to mobile.\n */\nexport interface RelationPickerDialogProps<N extends string = string> {\n to: ResourceNameOrRef<N>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (value: string | number) => void;\n /** Optional: receives the FULL chosen row (for prefilling sibling fields). */\n onSelectRow?: (row: Record<string, unknown>) => void;\n /**\n * Fallback search field — an OVERRIDE, not a required prop. The search box\n * expands through `useDialogFilterState`, which resolves what to search in\n * this order:\n * 1. the TARGET resource's own `views.list.search` declaration, if any —\n * always wins, regardless of this prop;\n * 2. this `searchField` prop, when the target declares no search;\n * 3. the target's `displayField` (itself falling back to `'name'`), when\n * neither of the above is available.\n *\n * A resolver-shaped `SearchDecl` has no single field that could be named\n * here, and a target with no search declaration at all should still be\n * searchable somehow — hence the two-step fallback instead of requiring\n * this prop outright.\n */\n searchField?: string;\n /**\n * Which field of the chosen row `onSelect` returns. Defaults to the target's id\n * field (`data.idField`, else `id`: a normal FK relation). Set to another column (e.g. `gtin`) for \"relate by a non-id\n * value\" pickers — the core mechanic behind the playground GTIN field.\n */\n valueField?: string;\n /**\n * Optional extra filters ANDed into every query (e.g. scope by product\n * group) — the same `FixedFilterInput` shape `useResourceList` and\n * `RelationPickerProps.fixedFilters` take: a bare AST expression\n * (`@burojs/core`'s `FilterExpression`, defaulting to `lock: true`, no\n * badge), or `{ filter, lock?, label? }` for finer control. `lock: false`\n * leaves the field open in the builder instead of hiding it; `label`\n * renders it as an undeletable badge.\n */\n fixedFilters?: readonly FixedFilterInput[];\n /**\n * The read scope of the dialog's list, as the select's own option reads take it. Default\n * `'context'`: the record around the select narrows the rows.\n */\n scope?: ScopeSource;\n /**\n * The record field the select writes (`useList`'s `scopeField`): the record's value of it never\n * narrows the rows offered for it. Absent: every field of the record narrows.\n */\n scopeField?: string;\n /**\n * The select's one pending create (P6-34), shared with its popover. With it the footer offers\n * \"+ Create\" (the target's `quick.create`, handed back through the select's own query); without\n * it the dialog offers none. The dialog is not a dismiss scope and holds no form: the sheet a\n * create opens stacks above it.\n */\n create?: SelectCreate;\n /** The trusted `defaults` of that create: the registry field's static `eq` literals (P6-31). */\n createSeeds?: Readonly<Record<string, unknown>>;\n /** The form field holding the key (the create button's test id). */\n fieldName?: string;\n /** Runs before a create opens: the select returns focus to its trigger. */\n onBeforeCreate?: () => void;\n pageSize?: number;\n /**\n * A to-many select's choice (P7 3.5): a click toggles the row in `selected` through `onToggle`\n * and the dialog stays open until the user is done. Without it, a click picks one row and closes.\n */\n multiple?: boolean;\n /** The keys already chosen (`multiple` only): their rows are marked. */\n selected?: readonly (string | number)[];\n /** A row toggled in `multiple` mode: its value, and the row itself. */\n onToggle?: (value: string | number, row: Record<string, unknown>) => void;\n /**\n * `multiple` only: the Select button confirms the choice through this instead of closing the\n * dialog with `onOpenChange(false)`, so a confirm and a dismissal can be told apart.\n */\n onDone?: () => void;\n /**\n * Asked once per shown page with the keys of its rows: rows whose key is in the resolved set\n * are drawn checked and disabled (a link panel's already-linked targets). Until it answers the\n * page's rows cannot be picked and the footer says the rows are being checked; when it fails\n * none of them can, and the footer says so with a Retry.\n */\n disabledKeys?: (\n keys: readonly (string | number)[],\n signal: AbortSignal,\n ) => Promise<ReadonlySet<string>>;\n /**\n * With `disabledKeys`: the footer's note for the page's disabled rows, given how many there\n * are (the caller says what they are, \"3 already linked\"). Absent: no note.\n */\n disabledLabel?: (count: number) => React.ReactNode;\n}\n\nconst DEBOUNCE_MS = 250;\n\n/** The disabled rows of a page: the keys asked (and the attempt), and the answer. */\ninterface DisabledState {\n readonly asked: string;\n readonly attempt: number;\n readonly keys: ReadonlySet<string> | 'failed';\n}\n\ntype EnumOption = { value: string; label: string };\n\nexport function RelationPickerDialog<const N extends string = string>({\n to: toInput,\n open,\n onOpenChange,\n onSelect,\n onSelectRow,\n searchField,\n valueField: valueFieldProp,\n fixedFilters,\n scope = 'context',\n scopeField,\n create,\n createSeeds,\n fieldName,\n onBeforeCreate,\n pageSize = 8,\n multiple = false,\n selected,\n onToggle,\n onDone,\n disabledKeys,\n disabledLabel,\n}: RelationPickerDialogProps<N>): React.ReactElement {\n const to = routeResourceKey(toInput);\n const app = useApp();\n const caps = useI18nCaps();\n // The target's own caps: a column header reads `resources.<to>.fields.<f>.label` first.\n const targetCaps = useI18nCaps(to);\n const t = useOptionalT();\n const chromeT = useChromeT();\n const tr = (key: string, params?: Record<string, unknown>, fallback?: string) =>\n t?.(key, { params, defaultValue: fallback }) ?? fallback ?? key;\n const targetDef = app.registry.get(to);\n const rowTitle = useRowTitle(to, targetDef?.displayField, { recordTitle: true });\n const declaredIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof declaredIdField === 'string' && declaredIdField.length > 0 ? declaredIdField : 'id';\n const valueField = valueFieldProp ?? idField;\n\n const [search, setSearchInput] = React.useState('');\n const [debounced, setDebounced] = React.useState('');\n const [sort, setSort] = React.useState<{ field: string; dir: 'asc' | 'desc' }>({\n field: 'id',\n dir: 'desc',\n });\n\n const displayField = targetDef?.displayField ?? 'name';\n\n // Resolution order (see `RelationPickerDialogProps.searchField`'s doc):\n // the target's own `views.list.search` declaration always wins inside\n // `useDialogFilterState`; this `searchField` prop is only consulted THERE\n // when the target declares no search at all, and `displayField` is what\n // this dialog hands the hook when the caller didn't override either — so\n // a target with no declaration and no override is still searchable.\n //\n // `enabled: open` — `RelationPickerDialog` is always mounted by\n // `<RelationPicker>` (it's a controlled dialog, not conditionally\n // rendered), so without this gate the field-derivation walk inside\n // `useFilterFields` (`categorizeField` over every field of the target)\n // would run on every mount and every `fixedFilters`-identity change of\n // EVERY relation field on a form, whether its dialog ever opens or not.\n // Gating recomputes in full the moment `open` flips true — same render,\n // before the dialog shows anything, so nothing it displays changes.\n const filterState = useDialogFilterState(to, {\n fixedFilters,\n searchField: searchField ?? displayField,\n enabled: open,\n });\n\n React.useEffect(() => {\n const h = setTimeout(() => setDebounced(search), DEBOUNCE_MS);\n return () => clearTimeout(h);\n }, [search]);\n\n // The debounced text is the one thing this dialog composes itself — the\n // rest of the filter (fixed scope, the builder's group, and the actual\n // `fixed → search → group` composition) all live in `useDialogFilterState`\n // now, via the SAME `composeListFilter` core the list page uses.\n React.useEffect(() => {\n filterState.setSearch(debounced);\n // `filterState.setSearch` is stable (useCallback, empty deps) — only\n // `debounced` driving this is real; including the whole `filterState`\n // object would refire on every recompute of its OWN memo (its `filter`\n // depends on this very effect's result), which is not what \"debounced\n // changed\" means.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debounced]);\n\n // Reset volatile state each time the dialog opens.\n React.useEffect(() => {\n if (open) {\n setSearchInput('');\n setDebounced('');\n filterState.reset();\n }\n // `filterState.reset` is stable (useCallback, empty deps); `filterState`\n // itself is excluded for the same reason as the effect above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const listAccess = useListAccess(to);\n const extraColumns = React.useMemo(() => {\n const declared = (targetDef?.views?.list?.columns ?? []).map(columnName);\n // The same predicate as the list table: a pending decision shows no extra column.\n return declared\n .filter((c) => c !== 'id' && c !== displayField)\n .filter((c) => listAccess.status === 'ready' && listAccess.columnVisible(c))\n .slice(0, 2);\n }, [targetDef, displayField, listAccess]);\n const titleSortable = listAccess.status === 'ready' && listAccess.fieldVisible(displayField);\n // A sort on a field the user may not view is an ordering oracle: it is dropped, never sent.\n const effectiveSortField =\n sort.field === 'id' ||\n (sort.field === displayField ? titleSortable : extraColumns.includes(sort.field))\n ? sort.field\n : 'id';\n\n const query = useList(to, {\n ...(filterState.filter ? { filter: filterState.filter } : {}),\n sort: [\n {\n field: effectiveSortField,\n direction: effectiveSortField === sort.field ? sort.dir : 'desc',\n },\n ],\n pagination: { kind: 'offset', page: filterState.page, pageSize },\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n enabled: open && targetDef != null && !filterState.blocked,\n });\n\n const rows = (query.data?.data ?? []) as Array<{ id: unknown } & Record<string, unknown>>;\n // Unknown-total contract: a provider that cannot count omits `total` (Hasura\n // `noAggregate` targets such as `users` / `code_code`). Deriving `totalPages`\n // from a `?? 0` fallback pinned it to 1 and disabled Next forever. With an\n // unknown total we page forward while the page is full.\n const total = query.data?.total;\n const knownTotal = typeof total === 'number';\n const totalPages = knownTotal ? Math.max(1, Math.ceil(total / pageSize)) : undefined;\n const pageIsFull = rows.length >= pageSize;\n const hasNextPage = knownTotal ? filterState.page < (totalPages as number) : pageIsFull;\n const isLoading = query.isLoading || query.isFetching;\n\n // The key a row is chosen, selected and disabled by: the target's idField unless a value field\n // names another column.\n const keyOf = (row: Record<string, unknown>): unknown =>\n valueField === idField ? recordKeyOf(targetDef, row) : row[valueField];\n // The page's keys, asked of `disabledKeys` once per distinct set.\n const pageKeys = rows\n .map(keyOf)\n .filter((key): key is string | number => typeof key === 'string' || typeof key === 'number');\n const asked = JSON.stringify(pageKeys);\n const [disabled, setDisabled] = React.useState<DisabledState | undefined>(undefined);\n // A Retry after a failed answer asks the same keys again.\n const [attempt, setAttempt] = React.useState(0);\n // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by the page's keys, not the rows' identity\n React.useEffect(() => {\n if (disabledKeys === undefined || pageKeys.length === 0) return;\n const controller = new AbortController();\n disabledKeys(pageKeys, controller.signal).then(\n (keys) => {\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys });\n },\n () => {\n // Fails closed: a row that may already be linked is never offered.\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys: 'failed' });\n },\n );\n return () => controller.abort();\n }, [asked, attempt, disabledKeys]);\n // The answer for the page shown now; undefined while it is being asked.\n const answer =\n disabledKeys === undefined ||\n pageKeys.length === 0 ||\n disabled === undefined ||\n disabled.asked !== asked ||\n disabled.attempt !== attempt\n ? undefined\n : disabled.keys;\n const checking = disabledKeys !== undefined && pageKeys.length > 0 && answer === undefined;\n const checkFailed = answer === 'failed';\n const isDisabled = (row: Record<string, unknown>): boolean => {\n if (disabledKeys === undefined) return false;\n if (answer === undefined || answer === 'failed') return true;\n return answer.has(String(keyOf(row)));\n };\n const disabledCount =\n answer === undefined || answer === 'failed' ? 0 : rows.filter(isDisabled).length;\n\n const toggleSort = (field: string) => {\n setSort((prev) =>\n prev.field === field\n ? { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' }\n : { field, dir: 'asc' },\n );\n };\n\n const targetLabel = resolveResourceLabel(caps, to, 'single', targetDef);\n const colCount = 2 + extraColumns.length;\n\n const pick = (row: { id: unknown } & Record<string, unknown>) => {\n if (isDisabled(row)) return;\n const picked = keyOf(row);\n // A row without the value the field stores is never written as its id instead (spec 4.2).\n if (picked === null || picked === undefined || picked === '') return;\n if (multiple) {\n onToggle?.(picked as string | number, row as Record<string, unknown>);\n return;\n }\n onSelect(picked as string | number);\n onSelectRow?.(row as Record<string, unknown>);\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange} modal={create?.pending !== true}>\n <DialogContent\n // While a create of this select is open its sheet stacks above this dialog and stays\n // interactive: the dialog neither traps focus nor closes on a click outside, and Escape\n // closes only the sheet (P6-67).\n {...(create?.pending === true\n ? {\n onInteractOutside: (event: Event) => event.preventDefault(),\n onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),\n }\n : {})}\n className=\"flex max-h-[85vh] w-[calc(100vw-1.5rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0\"\n data-testid={`relation-picker-dialog:${to}`}\n >\n <DialogHeader className=\"border-b px-4 py-3\">\n <DialogTitle className=\"text-base\">\n {tr('relationPicker.title', { name: targetLabel }, `Select: ${targetLabel}`)}\n </DialogTitle>\n <DialogDescription className=\"sr-only\">\n {tr('relationPicker.advancedSearch', undefined, 'Advanced search')}\n </DialogDescription>\n </DialogHeader>\n\n {/* Search + the filter builder (#9 / Task 5 SP3) */}\n <div className=\"flex flex-wrap items-center gap-2 border-b px-4 py-2.5\">\n {filterState.searchEnabled && (\n <div className=\"relative min-w-[12rem] flex-1\">\n <IconSearch className=\"pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n autoFocus\n value={search}\n onChange={(e) => setSearchInput(e.target.value)}\n placeholder={tr('toolbar.search', { name: targetLabel }, `Search ${targetLabel}…`)}\n data-testid={`relation-picker-dialog:${to}:search`}\n className=\"h-9 pl-8\"\n />\n </div>\n )}\n {/*\n No `pinnedFields`/`pinnedValue`/`setPinnedValue` — this dialog has\n no pinned slot (`useDialogFilterState`'s doc: a modal has no\n spare real estate for always-visible facets), so `FilterBuilder`\n falls back to its own stable empty-array defaults.\n */}\n <FilterBuilder\n group={filterState.group}\n setGroup={filterState.setGroup}\n available={filterState.available}\n fixed={filterState.fixed}\n setPage={filterState.setPage}\n testIdBase={`relation-picker-dialog:${to}:filter`}\n />\n </div>\n\n {/* Fixed-height table area — constant height so paging never jumps. */}\n <div className=\"h-[46vh] min-h-[16rem] overflow-auto\">\n <Table>\n <TableHeader className=\"sticky top-0 z-10 bg-background shadow-[inset_0_-1px_0_var(--border)]\">\n <TableRow className=\"hover:bg-transparent\">\n <TableHead\n className=\"h-9 w-14 cursor-pointer select-none\"\n onClick={() => toggleSort('id')}\n >\n {tr('relationPicker.idColumn', undefined, 'ID')}{' '}\n <SortIcon field=\"id\" sort={sort} />\n </TableHead>\n <TableHead\n className={cn('h-9 select-none', titleSortable && 'cursor-pointer')}\n {...(titleSortable ? { onClick: () => toggleSort(displayField) } : {})}\n >\n {targetLabel} {titleSortable && <SortIcon field={displayField} sort={sort} />}\n </TableHead>\n {extraColumns.map((c) => (\n <TableHead\n key={c}\n className=\"h-9 cursor-pointer select-none\"\n onClick={() => toggleSort(c)}\n >\n {resolveFieldLabel(\n targetCaps,\n c,\n declaredText('field', targetDef?.fields?.[c], 'label'),\n )}\n <SortIcon field={c} sort={sort} />\n </TableHead>\n ))}\n <TableHead className=\"h-9 w-8\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {isLoading ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount + 1} className=\"h-64 text-center\">\n <IconLoader2 className=\"mx-auto size-4 animate-spin text-muted-foreground\" />\n </TableCell>\n </TableRow>\n ) : rows.length === 0 ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell\n colSpan={colCount + 1}\n className=\"h-64 text-center text-sm text-muted-foreground\"\n >\n {tr('relation.noResults', undefined, 'No results.')}\n </TableCell>\n </TableRow>\n ) : (\n rows.map((row) => {\n const idStr = String(recordKeyOf(targetDef, row));\n const rowDisabled = isDisabled(row);\n // A disabled row is drawn checked: it is already chosen elsewhere.\n const checked =\n rowDisabled ||\n (selected ?? []).some((key) => String(key) === String(keyOf(row)));\n return (\n <TableRow\n key={idStr}\n className={cn(\n rowDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',\n )}\n data-testid={`relation-picker-dialog:${to}:row:${idStr}`}\n {...(multiple ? { 'aria-selected': checked } : {})}\n {...(rowDisabled ? { 'aria-disabled': true } : {})}\n onClick={() => pick(row)}\n >\n <TableCell className=\"py-2 tabular-nums text-muted-foreground\">\n {tr('relationPicker.recordNumber', { id: idStr }, `#${idStr}`)}\n </TableCell>\n <TableCell className=\"py-2 font-medium\">{rowTitle(row)}</TableCell>\n {extraColumns.map((c) => (\n <TableCell key={c} className=\"py-2 text-muted-foreground\">\n {/* The list's display renderers: locale numbers and dates, enum labels. */}\n <ResolvedFieldDisplay\n resource={targetDef?.ref ?? { namespace: 'default', id: to }}\n name={c}\n field={\n (targetDef === undefined ? undefined : declaredField(targetDef, c)) ??\n (targetDef?.fields?.[c] as never) ?? { type: 'unknown' }\n }\n value={row[c]}\n record={row}\n fallback={formatCell(row[c], targetDef?.fields?.[c], (ns) =>\n app.registry.enums.get(ns),\n )}\n />\n </TableCell>\n ))}\n <TableCell className=\"py-2 text-right\">\n {multiple ? (\n <IconCheck\n className={cn('ml-auto size-4', checked ? 'opacity-100' : 'opacity-0')}\n aria-hidden\n />\n ) : (\n <IconChevronRight className=\"ml-auto size-4 text-muted-foreground/50\" />\n )}\n </TableCell>\n </TableRow>\n );\n })\n )}\n </TableBody>\n </Table>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-between gap-2 border-t px-4 py-2.5 sm:justify-between\">\n {/*\n A wrapper that is always there: the footer is `justify-between`, so dropping its first\n child (no create offered) would slide the pager to the left edge.\n */}\n <div className=\"flex items-center gap-3\">\n {checking && (\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-checking`}\n >\n <IconLoader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />\n {tr('relationPicker.checking', undefined, 'Checking these rows…')}\n </span>\n )}\n {checkFailed && (\n <span\n role=\"alert\"\n className=\"inline-flex items-center gap-2 text-sm text-destructive\"\n data-testid={`relation-picker-dialog:${to}:disabled-failed`}\n >\n {tr(\n 'relationPicker.checkFailed',\n undefined,\n 'These rows could not be checked, so none can be chosen.',\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7\"\n onClick={() => setAttempt((n) => n + 1)}\n data-testid={`relation-picker-dialog:${to}:disabled-retry`}\n >\n {chromeT('actions.retry', { defaultValue: 'Retry' })}\n </Button>\n </span>\n )}\n {disabledCount > 0 && disabledLabel !== undefined && (\n <span\n className=\"text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-count`}\n >\n {disabledLabel(disabledCount)}\n </span>\n )}\n {create !== undefined && (\n <RelationCreateButton\n to={to}\n fieldName={fieldName ?? to}\n create={create}\n seeds={createSeeds}\n search={search}\n beforeOpen={onBeforeCreate ?? noop}\n className=\"inline-flex items-center gap-1 whitespace-nowrap text-sm text-foreground hover:underline disabled:opacity-50\"\n />\n )}\n </div>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <span className=\"hidden tabular-nums sm:inline\">\n {knownTotal\n ? tr(\n 'pagination.pageOf',\n { page: filterState.page, total: totalPages },\n `${filterState.page} / ${totalPages}`,\n )\n : tr('pagination.pageOnly', { page: filterState.page }, `${filterState.page}`)}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={filterState.page <= 1}\n onClick={() => filterState.setPage(Math.max(1, filterState.page - 1))}\n data-testid={`relation-picker-dialog:${to}:prev`}\n >\n {tr('relationPicker.prev', undefined, 'Prev')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={!hasNextPage}\n onClick={() => filterState.setPage(filterState.page + 1)}\n data-testid={`relation-picker-dialog:${to}:next`}\n >\n {tr('relationPicker.next', undefined, 'Next')}\n </Button>\n {multiple && (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => (onDone === undefined ? onOpenChange(false) : onDone())}\n data-testid={`relation-picker-dialog:${to}:done`}\n >\n {tr('relationPicker.select', undefined, 'Select')}\n </Button>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * A column's sort arrow. Declared here, not inside the dialog: a component made in a render is a\n * new type on every render, so React replaced these icons each time the dialog rendered — and a\n * node removed from the dialog while focus was crossing into the filter popover (which is portaled\n * outside it, so focus sits on `body` for that moment) makes the dialog's focus scope take focus\n * back, which closed the popover on any click inside it.\n */\nfunction SortIcon({\n field,\n sort,\n}: {\n readonly field: string;\n readonly sort: { readonly field: string; readonly dir: 'asc' | 'desc' };\n}): React.ReactElement {\n if (sort.field !== field) return <IconSelector className=\"ml-1 inline size-3 opacity-40\" />;\n return sort.dir === 'asc' ? (\n <IconArrowUp className=\"ml-1 inline size-3\" />\n ) : (\n <IconArrowDown className=\"ml-1 inline size-3\" />\n );\n}\n\nfunction formatCell(\n value: unknown,\n field: unknown,\n enumOf: (namespace: string) => Parameters<typeof enumValueText>[0],\n): string {\n if (value == null || value === '') return '—';\n if (typeof value === 'boolean') return value ? '✓' : '—';\n // Map an option value → its human label: the field's `texts.options`, the bound enum's\n // `texts`, else a label the field's `meta.options` carries (adopter data).\n const namespace =\n field !== null && typeof field === 'object'\n ? enumNamespaceOf(field as Parameters<typeof enumNamespaceOf>[0])\n : undefined;\n const declared =\n fieldOptionText(field, String(value)) ??\n (namespace === undefined ? undefined : enumValueText(enumOf(namespace), String(value)));\n if (declared !== undefined) return declared;\n const options = (field as { meta?: { options?: EnumOption[] } } | undefined)?.meta?.options;\n if (Array.isArray(options)) {\n const hit = options.find((o) => o.value === String(value));\n if (hit && typeof hit.label === 'string') return hit.label;\n }\n const s = String(value);\n return s.length > 40 ? `${s.slice(0, 40)}…` : s;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"relation-picker-dialog-CoWKjNv8.js","names":[],"sources":["../src/fields/relation-picker-dialog.tsx"],"sourcesContent":["'use client';\nimport * as React from 'react';\nimport {\n IconArrowDown,\n IconArrowUp,\n IconCheck,\n IconChevronRight,\n IconSelector,\n IconLoader2,\n IconSearch,\n} from '@tabler/icons-react';\nimport {\n type FixedFilterInput,\n type ScopeSource,\n declaredField,\n declaredText,\n enumNamespaceOf,\n enumValueText,\n fieldOptionText,\n resolveFieldLabel,\n resolveResourceLabel,\n routeResourceKey,\n type ResourceNameOrRef,\n columnName,\n} from '@burojs/core';\nimport {\n recordKeyOf,\n useApp,\n useChromeT,\n useI18nCaps,\n useList,\n useListAccess,\n useOptionalT,\n} from '@burojs/react';\nimport { useRowTitle } from '../resource/use-row-title.js';\nimport { ResolvedFieldDisplay } from './resolved-field.js';\nimport { cn } from '../lib/cn.js';\nimport { Button } from '../primitives/button.js';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog.js';\nimport { Input } from '../primitives/input.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '../primitives/table.js';\nimport { FilterBuilder } from '../list-kit/filter-builder/filter-builder.js';\nimport { useDialogFilterState } from './use-dialog-filter-state.js';\nimport { RelationCreateButton, type SelectCreate } from './relation-create.js';\n\nconst noop = (): void => undefined;\n\n/**\n * Advanced-search modal for the relation picker (#11 / #9). The inline popover\n * handles the quick \"type a few letters\" case; this dialog is the escalation:\n * the full target list with SEARCH, per-column FILTERS, SORTING and PAGINATION,\n * plus a \"Create\" shortcut. Resolved generically from the registry so it works\n * for any resource with no per-field wiring.\n *\n * #9 — compact, and the table area is a FIXED height so paging never resizes /\n * jumps the dialog; responsive down to mobile.\n */\nexport interface RelationPickerDialogProps<N extends string = string> {\n to: ResourceNameOrRef<N>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (value: string | number) => void;\n /** Optional: receives the FULL chosen row (for prefilling sibling fields). */\n onSelectRow?: (row: Record<string, unknown>) => void;\n /**\n * Fallback search field — an OVERRIDE, not a required prop. The search box\n * expands through `useDialogFilterState`, which resolves what to search in\n * this order:\n * 1. the TARGET resource's own `views.list.search` declaration, if any —\n * always wins, regardless of this prop;\n * 2. this `searchField` prop, when the target declares no search;\n * 3. the target's `displayField` (itself falling back to `'name'`), when\n * neither of the above is available.\n *\n * A resolver-shaped `SearchDecl` has no single field that could be named\n * here, and a target with no search declaration at all should still be\n * searchable somehow — hence the two-step fallback instead of requiring\n * this prop outright.\n */\n searchField?: string;\n /**\n * Which field of the chosen row `onSelect` returns. Defaults to the target's id\n * field (`data.idField`, else `id`: a normal FK relation). Set to another column (e.g. `gtin`) for \"relate by a non-id\n * value\" pickers — the core mechanic behind the playground GTIN field.\n */\n valueField?: string;\n /**\n * Optional extra filters ANDed into every query (e.g. scope by product\n * group) — the same `FixedFilterInput` shape `useResourceList` and\n * `RelationPickerProps.fixedFilters` take: a bare AST expression\n * (`@burojs/core`'s `FilterExpression`, defaulting to `lock: true`, no\n * badge), or `{ filter, lock?, label? }` for finer control. `lock: false`\n * leaves the field open in the builder instead of hiding it; `label`\n * renders it as an undeletable badge.\n */\n fixedFilters?: readonly FixedFilterInput[];\n /**\n * The read scope of the dialog's list, as the select's own option reads take it. Default\n * `'context'`: the record around the select narrows the rows.\n */\n scope?: ScopeSource;\n /**\n * The record field the select writes (`useList`'s `scopeField`): the record's value of it never\n * narrows the rows offered for it. Absent: every field of the record narrows.\n */\n scopeField?: string;\n /**\n * The select's one pending create (P6-34), shared with its popover. With it the footer offers\n * \"+ Create\" (the target's `quick.create`, handed back through the select's own query); without\n * it the dialog offers none. The dialog is not a dismiss scope and holds no form: the sheet a\n * create opens stacks above it.\n */\n create?: SelectCreate;\n /** The trusted `defaults` of that create: the registry field's static `eq` literals (P6-31). */\n createSeeds?: Readonly<Record<string, unknown>>;\n /** The form field holding the key (the create button's test id). */\n fieldName?: string;\n /** Runs before a create opens: the select returns focus to its trigger. */\n onBeforeCreate?: () => void;\n pageSize?: number;\n /**\n * A to-many select's choice (P7 3.5): a click toggles the row in `selected` through `onToggle`\n * and the dialog stays open until the user is done. Without it, a click picks one row and closes.\n */\n multiple?: boolean;\n /** The keys already chosen (`multiple` only): their rows are marked. */\n selected?: readonly (string | number)[];\n /** A row toggled in `multiple` mode: its value, and the row itself. */\n onToggle?: (value: string | number, row: Record<string, unknown>) => void;\n /**\n * `multiple` only: the Select button confirms the choice through this instead of closing the\n * dialog with `onOpenChange(false)`, so a confirm and a dismissal can be told apart.\n */\n onDone?: () => void;\n /**\n * Asked once per shown page with the keys of its rows: rows whose key is in the resolved set\n * are drawn checked and disabled (a link panel's already-linked targets). Until it answers the\n * page's rows cannot be picked and the footer says the rows are being checked; when it fails\n * none of them can, and the footer says so with a Retry.\n */\n disabledKeys?: (\n keys: readonly (string | number)[],\n signal: AbortSignal,\n ) => Promise<ReadonlySet<string>>;\n /**\n * With `disabledKeys`: the footer's note for the page's disabled rows, given how many there\n * are (the caller says what they are, \"3 already linked\"). Absent: no note.\n */\n disabledLabel?: (count: number) => React.ReactNode;\n}\n\nconst DEBOUNCE_MS = 250;\n\n/** The disabled rows of a page: the keys asked (and the attempt), and the answer. */\ninterface DisabledState {\n readonly asked: string;\n readonly attempt: number;\n readonly keys: ReadonlySet<string> | 'failed';\n}\n\ntype EnumOption = { value: string; label: string };\n\nexport function RelationPickerDialog<const N extends string = string>({\n to: toInput,\n open,\n onOpenChange,\n onSelect,\n onSelectRow,\n searchField,\n valueField: valueFieldProp,\n fixedFilters,\n scope = 'context',\n scopeField,\n create,\n createSeeds,\n fieldName,\n onBeforeCreate,\n pageSize = 8,\n multiple = false,\n selected,\n onToggle,\n onDone,\n disabledKeys,\n disabledLabel,\n}: RelationPickerDialogProps<N>): React.ReactElement {\n const to = routeResourceKey(toInput);\n const app = useApp();\n const caps = useI18nCaps();\n // The target's own caps: a column header reads `resources.<to>.fields.<f>.label` first.\n const targetCaps = useI18nCaps(to);\n const t = useOptionalT();\n const chromeT = useChromeT();\n const tr = (key: string, params?: Record<string, unknown>, fallback?: string) =>\n t?.(key, { params, defaultValue: fallback }) ?? fallback ?? key;\n const targetDef = app.registry.get(to);\n const rowTitle = useRowTitle(to, targetDef?.displayField, { recordTitle: true });\n const declaredIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof declaredIdField === 'string' && declaredIdField.length > 0 ? declaredIdField : 'id';\n const valueField = valueFieldProp ?? idField;\n\n const [search, setSearchInput] = React.useState('');\n const [debounced, setDebounced] = React.useState('');\n const [sort, setSort] = React.useState<{ field: string; dir: 'asc' | 'desc' }>({\n field: 'id',\n dir: 'desc',\n });\n\n const displayField = targetDef?.displayField ?? 'name';\n\n // Resolution order (see `RelationPickerDialogProps.searchField`'s doc):\n // the target's own `views.list.search` declaration always wins inside\n // `useDialogFilterState`; this `searchField` prop is only consulted THERE\n // when the target declares no search at all, and `displayField` is what\n // this dialog hands the hook when the caller didn't override either — so\n // a target with no declaration and no override is still searchable.\n //\n // `enabled: open` — `RelationPickerDialog` is always mounted by\n // `<RelationPicker>` (it's a controlled dialog, not conditionally\n // rendered), so without this gate the field-derivation walk inside\n // `useFilterFields` (`categorizeField` over every field of the target)\n // would run on every mount and every `fixedFilters`-identity change of\n // EVERY relation field on a form, whether its dialog ever opens or not.\n // Gating recomputes in full the moment `open` flips true — same render,\n // before the dialog shows anything, so nothing it displays changes.\n const filterState = useDialogFilterState(to, {\n fixedFilters,\n searchField: searchField ?? displayField,\n enabled: open,\n });\n\n React.useEffect(() => {\n const h = setTimeout(() => setDebounced(search), DEBOUNCE_MS);\n return () => clearTimeout(h);\n }, [search]);\n\n // The debounced text is the one thing this dialog composes itself — the\n // rest of the filter (fixed scope, the builder's group, and the actual\n // `fixed → search → group` composition) all live in `useDialogFilterState`\n // now, via the SAME `composeListFilter` core the list page uses.\n React.useEffect(() => {\n filterState.setSearch(debounced);\n // `filterState.setSearch` is stable (useCallback, empty deps) — only\n // `debounced` driving this is real; including the whole `filterState`\n // object would refire on every recompute of its OWN memo (its `filter`\n // depends on this very effect's result), which is not what \"debounced\n // changed\" means.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debounced]);\n\n // Reset volatile state each time the dialog opens.\n React.useEffect(() => {\n if (open) {\n setSearchInput('');\n setDebounced('');\n filterState.reset();\n }\n // `filterState.reset` is stable (useCallback, empty deps); `filterState`\n // itself is excluded for the same reason as the effect above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const listAccess = useListAccess(to);\n const extraColumns = React.useMemo(() => {\n const declared = (targetDef?.views?.list?.columns ?? []).map(columnName);\n // The same predicate as the list table: a pending decision shows no extra column.\n return declared\n .filter((c) => c !== 'id' && c !== displayField)\n .filter((c) => listAccess.status === 'ready' && listAccess.columnVisible(c))\n .slice(0, 2);\n }, [targetDef, displayField, listAccess]);\n const titleSortable = listAccess.status === 'ready' && listAccess.fieldVisible(displayField);\n // A sort on a field the user may not view is an ordering oracle: it is dropped, never sent.\n const effectiveSortField =\n sort.field === 'id' ||\n (sort.field === displayField ? titleSortable : extraColumns.includes(sort.field))\n ? sort.field\n : 'id';\n\n const query = useList(to, {\n ...(filterState.filter ? { filter: filterState.filter } : {}),\n sort: [\n {\n field: effectiveSortField,\n direction: effectiveSortField === sort.field ? sort.dir : 'desc',\n },\n ],\n pagination: { kind: 'offset', page: filterState.page, pageSize },\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n enabled: open && targetDef != null && !filterState.blocked,\n });\n\n const rows = (query.data?.data ?? []) as Array<{ id: unknown } & Record<string, unknown>>;\n // Unknown-total contract: a provider that cannot count omits `total` (Hasura\n // `noAggregate` targets such as `users` / `code_code`). Deriving `totalPages`\n // from a `?? 0` fallback pinned it to 1 and disabled Next forever. With an\n // unknown total we page forward while the page is full.\n const total = query.data?.total;\n const knownTotal = typeof total === 'number';\n const totalPages = knownTotal ? Math.max(1, Math.ceil(total / pageSize)) : undefined;\n const pageIsFull = rows.length >= pageSize;\n const hasNextPage = knownTotal ? filterState.page < (totalPages as number) : pageIsFull;\n const isLoading = query.isLoading || query.isFetching;\n\n // The key a row is chosen, selected and disabled by: the target's idField unless a value field\n // names another column.\n const keyOf = (row: Record<string, unknown>): unknown =>\n valueField === idField ? recordKeyOf(targetDef, row) : row[valueField];\n // The page's keys, asked of `disabledKeys` once per distinct set.\n const pageKeys = rows\n .map(keyOf)\n .filter((key): key is string | number => typeof key === 'string' || typeof key === 'number');\n const asked = JSON.stringify(pageKeys);\n const [disabled, setDisabled] = React.useState<DisabledState | undefined>(undefined);\n // A Retry after a failed answer asks the same keys again.\n const [attempt, setAttempt] = React.useState(0);\n // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by the page's keys, not the rows' identity\n React.useEffect(() => {\n if (disabledKeys === undefined || pageKeys.length === 0) return;\n const controller = new AbortController();\n disabledKeys(pageKeys, controller.signal).then(\n (keys) => {\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys });\n },\n () => {\n // Fails closed: a row that may already be linked is never offered.\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys: 'failed' });\n },\n );\n return () => controller.abort();\n }, [asked, attempt, disabledKeys]);\n // The answer for the page shown now; undefined while it is being asked.\n const answer =\n disabledKeys === undefined ||\n pageKeys.length === 0 ||\n disabled === undefined ||\n disabled.asked !== asked ||\n disabled.attempt !== attempt\n ? undefined\n : disabled.keys;\n const checking = disabledKeys !== undefined && pageKeys.length > 0 && answer === undefined;\n const checkFailed = answer === 'failed';\n const isDisabled = (row: Record<string, unknown>): boolean => {\n if (disabledKeys === undefined) return false;\n if (answer === undefined || answer === 'failed') return true;\n return answer.has(String(keyOf(row)));\n };\n const disabledCount =\n answer === undefined || answer === 'failed' ? 0 : rows.filter(isDisabled).length;\n\n const toggleSort = (field: string) => {\n setSort((prev) =>\n prev.field === field\n ? { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' }\n : { field, dir: 'asc' },\n );\n };\n\n const targetLabel = resolveResourceLabel(caps, to, 'single', targetDef);\n const colCount = 2 + extraColumns.length;\n\n const pick = (row: { id: unknown } & Record<string, unknown>) => {\n if (isDisabled(row)) return;\n const picked = keyOf(row);\n // A row without the value the field stores is never written as its id instead (spec 4.2).\n if (picked === null || picked === undefined || picked === '') return;\n if (multiple) {\n onToggle?.(picked as string | number, row as Record<string, unknown>);\n return;\n }\n onSelect(picked as string | number);\n onSelectRow?.(row as Record<string, unknown>);\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange} modal={create?.pending !== true}>\n <DialogContent\n // While a create of this select is open its sheet stacks above this dialog and stays\n // interactive: the dialog neither traps focus nor closes on a click outside, and Escape\n // closes only the sheet (P6-67).\n {...(create?.pending === true\n ? {\n onInteractOutside: (event: Event) => event.preventDefault(),\n onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),\n }\n : {})}\n className=\"flex max-h-[85vh] w-[calc(100vw-1.5rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0\"\n data-testid={`relation-picker-dialog:${to}`}\n >\n <DialogHeader className=\"border-b px-4 py-3\">\n <DialogTitle className=\"text-base\">\n {tr('relationPicker.title', { name: targetLabel }, `Select: ${targetLabel}`)}\n </DialogTitle>\n <DialogDescription className=\"sr-only\">\n {tr('relationPicker.advancedSearch', undefined, 'Advanced search')}\n </DialogDescription>\n </DialogHeader>\n\n {/* Search + the filter builder (#9 / Task 5 SP3) */}\n <div className=\"flex flex-wrap items-center gap-2 border-b px-4 py-2.5\">\n {filterState.searchEnabled && (\n <div className=\"relative min-w-[12rem] flex-1\">\n <IconSearch className=\"pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n autoFocus\n value={search}\n onChange={(e) => setSearchInput(e.target.value)}\n placeholder={tr('toolbar.search', { name: targetLabel }, `Search ${targetLabel}…`)}\n data-testid={`relation-picker-dialog:${to}:search`}\n className=\"h-9 pl-8\"\n />\n </div>\n )}\n {/*\n No `pinnedFields`/`pinnedValue`/`setPinnedValue` — this dialog has\n no pinned slot (`useDialogFilterState`'s doc: a modal has no\n spare real estate for always-visible facets), so `FilterBuilder`\n falls back to its own stable empty-array defaults.\n */}\n <FilterBuilder\n group={filterState.group}\n setGroup={filterState.setGroup}\n available={filterState.available}\n fixed={filterState.fixed}\n setPage={filterState.setPage}\n testIdBase={`relation-picker-dialog:${to}:filter`}\n />\n </div>\n\n {/* Fixed-height table area — constant height so paging never jumps. */}\n <div className=\"h-[46vh] min-h-[16rem] overflow-auto\">\n <Table>\n <TableHeader className=\"sticky top-0 z-10 bg-background shadow-[inset_0_-1px_0_var(--border)]\">\n <TableRow className=\"hover:bg-transparent\">\n <TableHead\n className=\"h-9 w-14 cursor-pointer select-none\"\n onClick={() => toggleSort('id')}\n >\n {tr('relationPicker.idColumn', undefined, 'ID')}{' '}\n <SortIcon field=\"id\" sort={sort} />\n </TableHead>\n <TableHead\n className={cn('h-9 select-none', titleSortable && 'cursor-pointer')}\n {...(titleSortable ? { onClick: () => toggleSort(displayField) } : {})}\n >\n {targetLabel} {titleSortable && <SortIcon field={displayField} sort={sort} />}\n </TableHead>\n {extraColumns.map((c) => (\n <TableHead\n key={c}\n className=\"h-9 cursor-pointer select-none\"\n onClick={() => toggleSort(c)}\n >\n {resolveFieldLabel(\n targetCaps,\n c,\n declaredText('field', targetDef?.fields?.[c], 'label'),\n )}\n <SortIcon field={c} sort={sort} />\n </TableHead>\n ))}\n <TableHead className=\"h-9 w-8\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {isLoading ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount + 1} className=\"h-64 text-center\">\n <IconLoader2 className=\"mx-auto size-4 animate-spin text-muted-foreground\" />\n </TableCell>\n </TableRow>\n ) : rows.length === 0 ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell\n colSpan={colCount + 1}\n className=\"h-64 text-center text-sm text-muted-foreground\"\n >\n {tr('relation.noResults', undefined, 'No results.')}\n </TableCell>\n </TableRow>\n ) : (\n rows.map((row) => {\n const idStr = String(recordKeyOf(targetDef, row));\n const rowDisabled = isDisabled(row);\n // A disabled row is drawn checked: it is already chosen elsewhere.\n const checked =\n rowDisabled ||\n (selected ?? []).some((key) => String(key) === String(keyOf(row)));\n return (\n <TableRow\n key={idStr}\n className={cn(\n rowDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',\n )}\n data-testid={`relation-picker-dialog:${to}:row:${idStr}`}\n {...(multiple ? { 'aria-selected': checked } : {})}\n {...(rowDisabled ? { 'aria-disabled': true } : {})}\n onClick={() => pick(row)}\n >\n <TableCell className=\"py-2 tabular-nums text-muted-foreground\">\n {tr('relationPicker.recordNumber', { id: idStr }, `#${idStr}`)}\n </TableCell>\n <TableCell className=\"py-2 font-medium\">{rowTitle(row)}</TableCell>\n {extraColumns.map((c) => (\n <TableCell key={c} className=\"py-2 text-muted-foreground\">\n {/* The list's display renderers: locale numbers and dates, enum labels. */}\n <ResolvedFieldDisplay\n resource={targetDef?.ref ?? { namespace: 'default', id: to }}\n name={c}\n field={\n (targetDef === undefined ? undefined : declaredField(targetDef, c)) ??\n (targetDef?.fields?.[c] as never) ?? { type: 'unknown' }\n }\n value={row[c]}\n record={row}\n fallback={formatCell(row[c], targetDef?.fields?.[c], (ns) =>\n app.registry.enums.get(ns),\n )}\n />\n </TableCell>\n ))}\n <TableCell className=\"py-2 text-right\">\n {multiple ? (\n <IconCheck\n className={cn('ml-auto size-4', checked ? 'opacity-100' : 'opacity-0')}\n aria-hidden\n />\n ) : (\n <IconChevronRight className=\"ml-auto size-4 text-muted-foreground/50\" />\n )}\n </TableCell>\n </TableRow>\n );\n })\n )}\n </TableBody>\n </Table>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-between gap-2 border-t px-4 py-2.5 sm:justify-between\">\n {/*\n A wrapper that is always there: the footer is `justify-between`, so dropping its first\n child (no create offered) would slide the pager to the left edge.\n */}\n <div className=\"flex items-center gap-3\">\n {checking && (\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-checking`}\n >\n <IconLoader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />\n {tr('relationPicker.checking', undefined, 'Checking these rows…')}\n </span>\n )}\n {checkFailed && (\n <span\n role=\"alert\"\n className=\"inline-flex items-center gap-2 text-sm text-destructive\"\n data-testid={`relation-picker-dialog:${to}:disabled-failed`}\n >\n {tr(\n 'relationPicker.checkFailed',\n undefined,\n 'These rows could not be checked, so none can be chosen.',\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7\"\n onClick={() => setAttempt((n) => n + 1)}\n data-testid={`relation-picker-dialog:${to}:disabled-retry`}\n >\n {chromeT('actions.retry', { defaultValue: 'Retry' })}\n </Button>\n </span>\n )}\n {disabledCount > 0 && disabledLabel !== undefined && (\n <span\n className=\"text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-count`}\n >\n {disabledLabel(disabledCount)}\n </span>\n )}\n {create !== undefined && (\n <RelationCreateButton\n to={to}\n fieldName={fieldName ?? to}\n create={create}\n seeds={createSeeds}\n search={search}\n beforeOpen={onBeforeCreate ?? noop}\n className=\"inline-flex items-center gap-1 whitespace-nowrap text-sm text-foreground hover:underline disabled:opacity-50\"\n />\n )}\n </div>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <span className=\"hidden tabular-nums sm:inline\">\n {knownTotal\n ? tr(\n 'pagination.pageOf',\n { page: filterState.page, total: totalPages },\n `${filterState.page} / ${totalPages}`,\n )\n : tr('pagination.pageOnly', { page: filterState.page }, `${filterState.page}`)}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={filterState.page <= 1}\n onClick={() => filterState.setPage(Math.max(1, filterState.page - 1))}\n data-testid={`relation-picker-dialog:${to}:prev`}\n >\n {tr('relationPicker.prev', undefined, 'Prev')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={!hasNextPage}\n onClick={() => filterState.setPage(filterState.page + 1)}\n data-testid={`relation-picker-dialog:${to}:next`}\n >\n {tr('relationPicker.next', undefined, 'Next')}\n </Button>\n {multiple && (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => (onDone === undefined ? onOpenChange(false) : onDone())}\n data-testid={`relation-picker-dialog:${to}:done`}\n >\n {tr('relationPicker.select', undefined, 'Select')}\n </Button>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * A column's sort arrow. Declared here, not inside the dialog: a component made in a render is a\n * new type on every render, so React replaced these icons each time the dialog rendered — and a\n * node removed from the dialog while focus was crossing into the filter popover (which is portaled\n * outside it, so focus sits on `body` for that moment) makes the dialog's focus scope take focus\n * back, which closed the popover on any click inside it.\n */\nfunction SortIcon({\n field,\n sort,\n}: {\n readonly field: string;\n readonly sort: { readonly field: string; readonly dir: 'asc' | 'desc' };\n}): React.ReactElement {\n if (sort.field !== field) return <IconSelector className=\"ml-1 inline size-3 opacity-40\" />;\n return sort.dir === 'asc' ? (\n <IconArrowUp className=\"ml-1 inline size-3\" />\n ) : (\n <IconArrowDown className=\"ml-1 inline size-3\" />\n );\n}\n\nfunction formatCell(\n value: unknown,\n field: unknown,\n enumOf: (namespace: string) => Parameters<typeof enumValueText>[0],\n): string {\n if (value == null || value === '') return '—';\n if (typeof value === 'boolean') return value ? '✓' : '—';\n // Map an option value → its human label: the field's `texts.options`, the bound enum's\n // `texts`, else a label the field's `meta.options` carries (adopter data).\n const namespace =\n field !== null && typeof field === 'object'\n ? enumNamespaceOf(field as Parameters<typeof enumNamespaceOf>[0])\n : undefined;\n const declared =\n fieldOptionText(field, String(value)) ??\n (namespace === undefined ? undefined : enumValueText(enumOf(namespace), String(value)));\n if (declared !== undefined) return declared;\n const options = (field as { meta?: { options?: EnumOption[] } } | undefined)?.meta?.options;\n if (Array.isArray(options)) {\n const hit = options.find((o) => o.value === String(value));\n if (hit && typeof hit.label === 'string') return hit.label;\n }\n const s = String(value);\n return s.length > 40 ? `${s.slice(0, 40)}…` : s;\n}\n"],"mappings":"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"}
@@ -2,9 +2,9 @@ const require_use_theme = require("./use-theme-DFlpXils.cjs");
2
2
  const require_button = require("./button-BxnnE28H.cjs");
3
3
  const require_input = require("./input-BI0czGyI.cjs");
4
4
  const require_dialog = require("./dialog-vmMdfUWs.cjs");
5
- const require_relation_create = require("./relation-create-_SFuPQGM.cjs");
6
- const require_filter_builder = require("./filter-builder-DQX-TMFh.cjs");
7
- const require_use_dialog_filter_state = require("./use-dialog-filter-state-lsTJxrB4.cjs");
5
+ const require_relation_create = require("./relation-create-B8zngAcA.cjs");
6
+ const require_filter_builder = require("./filter-builder-Dt5E8PSX.cjs");
7
+ const require_use_dialog_filter_state = require("./use-dialog-filter-state-CpWblz3C.cjs");
8
8
  let _burojs_core = require("@burojs/core");
9
9
  let _burojs_react = require("@burojs/react");
10
10
  let _tabler_icons_react = require("@tabler/icons-react");
@@ -399,4 +399,4 @@ function formatCell(value, field, enumOf) {
399
399
  //#endregion
400
400
  exports.RelationPickerDialog = RelationPickerDialog;
401
401
 
402
- //# sourceMappingURL=relation-picker-dialog-Duqy2r0n.cjs.map
402
+ //# sourceMappingURL=relation-picker-dialog-mbwyNK43.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"relation-picker-dialog-Duqy2r0n.cjs","names":["routeResourceKey","useApp","useI18nCaps","useOptionalT","useChromeT","useRowTitle","React","useDialogFilterState","useListAccess","columnName","useList","recordKeyOf","resolveResourceLabel","Dialog","DialogContent","DialogHeader","DialogTitle","DialogDescription","IconSearch","Input","FilterBuilder","Table","TableHeader","TableRow","TableHead","cn","resolveFieldLabel","declaredText","TableBody","TableCell","IconLoader2","ResolvedFieldDisplay","declaredField","IconCheck","IconChevronRight","DialogFooter","Button","RelationCreateButton","IconSelector","IconArrowUp","IconArrowDown","enumNamespaceOf","fieldOptionText","enumValueText"],"sources":["../src/fields/relation-picker-dialog.tsx"],"sourcesContent":["'use client';\nimport * as React from 'react';\nimport {\n IconArrowDown,\n IconArrowUp,\n IconCheck,\n IconChevronRight,\n IconSelector,\n IconLoader2,\n IconSearch,\n} from '@tabler/icons-react';\nimport {\n type FixedFilterInput,\n type ScopeSource,\n declaredField,\n declaredText,\n enumNamespaceOf,\n enumValueText,\n fieldOptionText,\n resolveFieldLabel,\n resolveResourceLabel,\n routeResourceKey,\n type ResourceNameOrRef,\n columnName,\n} from '@burojs/core';\nimport {\n recordKeyOf,\n useApp,\n useChromeT,\n useI18nCaps,\n useList,\n useListAccess,\n useOptionalT,\n} from '@burojs/react';\nimport { useRowTitle } from '../resource/use-row-title.js';\nimport { ResolvedFieldDisplay } from './resolved-field.js';\nimport { cn } from '../lib/cn.js';\nimport { Button } from '../primitives/button.js';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog.js';\nimport { Input } from '../primitives/input.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '../primitives/table.js';\nimport { FilterBuilder } from '../list-kit/filter-builder/filter-builder.js';\nimport { useDialogFilterState } from './use-dialog-filter-state.js';\nimport { RelationCreateButton, type SelectCreate } from './relation-create.js';\n\nconst noop = (): void => undefined;\n\n/**\n * Advanced-search modal for the relation picker (#11 / #9). The inline popover\n * handles the quick \"type a few letters\" case; this dialog is the escalation:\n * the full target list with SEARCH, per-column FILTERS, SORTING and PAGINATION,\n * plus a \"Create\" shortcut. Resolved generically from the registry so it works\n * for any resource with no per-field wiring.\n *\n * #9 — compact, and the table area is a FIXED height so paging never resizes /\n * jumps the dialog; responsive down to mobile.\n */\nexport interface RelationPickerDialogProps<N extends string = string> {\n to: ResourceNameOrRef<N>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (value: string | number) => void;\n /** Optional: receives the FULL chosen row (for prefilling sibling fields). */\n onSelectRow?: (row: Record<string, unknown>) => void;\n /**\n * Fallback search field — an OVERRIDE, not a required prop. The search box\n * expands through `useDialogFilterState`, which resolves what to search in\n * this order:\n * 1. the TARGET resource's own `views.list.search` declaration, if any —\n * always wins, regardless of this prop;\n * 2. this `searchField` prop, when the target declares no search;\n * 3. the target's `displayField` (itself falling back to `'name'`), when\n * neither of the above is available.\n *\n * A resolver-shaped `SearchDecl` has no single field that could be named\n * here, and a target with no search declaration at all should still be\n * searchable somehow — hence the two-step fallback instead of requiring\n * this prop outright.\n */\n searchField?: string;\n /**\n * Which field of the chosen row `onSelect` returns. Defaults to the target's id\n * field (`data.idField`, else `id`: a normal FK relation). Set to another column (e.g. `gtin`) for \"relate by a non-id\n * value\" pickers — the core mechanic behind the playground GTIN field.\n */\n valueField?: string;\n /**\n * Optional extra filters ANDed into every query (e.g. scope by product\n * group) — the same `FixedFilterInput` shape `useResourceList` and\n * `RelationPickerProps.fixedFilters` take: a bare AST expression\n * (`@burojs/core`'s `FilterExpression`, defaulting to `lock: true`, no\n * badge), or `{ filter, lock?, label? }` for finer control. `lock: false`\n * leaves the field open in the builder instead of hiding it; `label`\n * renders it as an undeletable badge.\n */\n fixedFilters?: readonly FixedFilterInput[];\n /**\n * The read scope of the dialog's list, as the select's own option reads take it. Default\n * `'context'`: the record around the select narrows the rows.\n */\n scope?: ScopeSource;\n /**\n * The record field the select writes (`useList`'s `scopeField`): the record's value of it never\n * narrows the rows offered for it. Absent: every field of the record narrows.\n */\n scopeField?: string;\n /**\n * The select's one pending create (P6-34), shared with its popover. With it the footer offers\n * \"+ Create\" (the target's `quick.create`, handed back through the select's own query); without\n * it the dialog offers none. The dialog is not a dismiss scope and holds no form: the sheet a\n * create opens stacks above it.\n */\n create?: SelectCreate;\n /** The trusted `defaults` of that create: the registry field's static `eq` literals (P6-31). */\n createSeeds?: Readonly<Record<string, unknown>>;\n /** The form field holding the key (the create button's test id). */\n fieldName?: string;\n /** Runs before a create opens: the select returns focus to its trigger. */\n onBeforeCreate?: () => void;\n pageSize?: number;\n /**\n * A to-many select's choice (P7 3.5): a click toggles the row in `selected` through `onToggle`\n * and the dialog stays open until the user is done. Without it, a click picks one row and closes.\n */\n multiple?: boolean;\n /** The keys already chosen (`multiple` only): their rows are marked. */\n selected?: readonly (string | number)[];\n /** A row toggled in `multiple` mode: its value, and the row itself. */\n onToggle?: (value: string | number, row: Record<string, unknown>) => void;\n /**\n * `multiple` only: the Select button confirms the choice through this instead of closing the\n * dialog with `onOpenChange(false)`, so a confirm and a dismissal can be told apart.\n */\n onDone?: () => void;\n /**\n * Asked once per shown page with the keys of its rows: rows whose key is in the resolved set\n * are drawn checked and disabled (a link panel's already-linked targets). Until it answers the\n * page's rows cannot be picked and the footer says the rows are being checked; when it fails\n * none of them can, and the footer says so with a Retry.\n */\n disabledKeys?: (\n keys: readonly (string | number)[],\n signal: AbortSignal,\n ) => Promise<ReadonlySet<string>>;\n /**\n * With `disabledKeys`: the footer's note for the page's disabled rows, given how many there\n * are (the caller says what they are, \"3 already linked\"). Absent: no note.\n */\n disabledLabel?: (count: number) => React.ReactNode;\n}\n\nconst DEBOUNCE_MS = 250;\n\n/** The disabled rows of a page: the keys asked (and the attempt), and the answer. */\ninterface DisabledState {\n readonly asked: string;\n readonly attempt: number;\n readonly keys: ReadonlySet<string> | 'failed';\n}\n\ntype EnumOption = { value: string; label: string };\n\nexport function RelationPickerDialog<const N extends string = string>({\n to: toInput,\n open,\n onOpenChange,\n onSelect,\n onSelectRow,\n searchField,\n valueField: valueFieldProp,\n fixedFilters,\n scope = 'context',\n scopeField,\n create,\n createSeeds,\n fieldName,\n onBeforeCreate,\n pageSize = 8,\n multiple = false,\n selected,\n onToggle,\n onDone,\n disabledKeys,\n disabledLabel,\n}: RelationPickerDialogProps<N>): React.ReactElement {\n const to = routeResourceKey(toInput);\n const app = useApp();\n const caps = useI18nCaps();\n // The target's own caps: a column header reads `resources.<to>.fields.<f>.label` first.\n const targetCaps = useI18nCaps(to);\n const t = useOptionalT();\n const chromeT = useChromeT();\n const tr = (key: string, params?: Record<string, unknown>, fallback?: string) =>\n t?.(key, { params, defaultValue: fallback }) ?? fallback ?? key;\n const targetDef = app.registry.get(to);\n const rowTitle = useRowTitle(to, targetDef?.displayField, { recordTitle: true });\n const declaredIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof declaredIdField === 'string' && declaredIdField.length > 0 ? declaredIdField : 'id';\n const valueField = valueFieldProp ?? idField;\n\n const [search, setSearchInput] = React.useState('');\n const [debounced, setDebounced] = React.useState('');\n const [sort, setSort] = React.useState<{ field: string; dir: 'asc' | 'desc' }>({\n field: 'id',\n dir: 'desc',\n });\n\n const displayField = targetDef?.displayField ?? 'name';\n\n // Resolution order (see `RelationPickerDialogProps.searchField`'s doc):\n // the target's own `views.list.search` declaration always wins inside\n // `useDialogFilterState`; this `searchField` prop is only consulted THERE\n // when the target declares no search at all, and `displayField` is what\n // this dialog hands the hook when the caller didn't override either — so\n // a target with no declaration and no override is still searchable.\n //\n // `enabled: open` — `RelationPickerDialog` is always mounted by\n // `<RelationPicker>` (it's a controlled dialog, not conditionally\n // rendered), so without this gate the field-derivation walk inside\n // `useFilterFields` (`categorizeField` over every field of the target)\n // would run on every mount and every `fixedFilters`-identity change of\n // EVERY relation field on a form, whether its dialog ever opens or not.\n // Gating recomputes in full the moment `open` flips true — same render,\n // before the dialog shows anything, so nothing it displays changes.\n const filterState = useDialogFilterState(to, {\n fixedFilters,\n searchField: searchField ?? displayField,\n enabled: open,\n });\n\n React.useEffect(() => {\n const h = setTimeout(() => setDebounced(search), DEBOUNCE_MS);\n return () => clearTimeout(h);\n }, [search]);\n\n // The debounced text is the one thing this dialog composes itself — the\n // rest of the filter (fixed scope, the builder's group, and the actual\n // `fixed → search → group` composition) all live in `useDialogFilterState`\n // now, via the SAME `composeListFilter` core the list page uses.\n React.useEffect(() => {\n filterState.setSearch(debounced);\n // `filterState.setSearch` is stable (useCallback, empty deps) — only\n // `debounced` driving this is real; including the whole `filterState`\n // object would refire on every recompute of its OWN memo (its `filter`\n // depends on this very effect's result), which is not what \"debounced\n // changed\" means.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debounced]);\n\n // Reset volatile state each time the dialog opens.\n React.useEffect(() => {\n if (open) {\n setSearchInput('');\n setDebounced('');\n filterState.reset();\n }\n // `filterState.reset` is stable (useCallback, empty deps); `filterState`\n // itself is excluded for the same reason as the effect above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const listAccess = useListAccess(to);\n const extraColumns = React.useMemo(() => {\n const declared = (targetDef?.views?.list?.columns ?? []).map(columnName);\n // The same predicate as the list table: a pending decision shows no extra column.\n return declared\n .filter((c) => c !== 'id' && c !== displayField)\n .filter((c) => listAccess.status === 'ready' && listAccess.columnVisible(c))\n .slice(0, 2);\n }, [targetDef, displayField, listAccess]);\n const titleSortable = listAccess.status === 'ready' && listAccess.fieldVisible(displayField);\n // A sort on a field the user may not view is an ordering oracle: it is dropped, never sent.\n const effectiveSortField =\n sort.field === 'id' ||\n (sort.field === displayField ? titleSortable : extraColumns.includes(sort.field))\n ? sort.field\n : 'id';\n\n const query = useList(to, {\n ...(filterState.filter ? { filter: filterState.filter } : {}),\n sort: [\n {\n field: effectiveSortField,\n direction: effectiveSortField === sort.field ? sort.dir : 'desc',\n },\n ],\n pagination: { kind: 'offset', page: filterState.page, pageSize },\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n enabled: open && targetDef != null && !filterState.blocked,\n });\n\n const rows = (query.data?.data ?? []) as Array<{ id: unknown } & Record<string, unknown>>;\n // Unknown-total contract: a provider that cannot count omits `total` (Hasura\n // `noAggregate` targets such as `users` / `code_code`). Deriving `totalPages`\n // from a `?? 0` fallback pinned it to 1 and disabled Next forever. With an\n // unknown total we page forward while the page is full.\n const total = query.data?.total;\n const knownTotal = typeof total === 'number';\n const totalPages = knownTotal ? Math.max(1, Math.ceil(total / pageSize)) : undefined;\n const pageIsFull = rows.length >= pageSize;\n const hasNextPage = knownTotal ? filterState.page < (totalPages as number) : pageIsFull;\n const isLoading = query.isLoading || query.isFetching;\n\n // The key a row is chosen, selected and disabled by: the target's idField unless a value field\n // names another column.\n const keyOf = (row: Record<string, unknown>): unknown =>\n valueField === idField ? recordKeyOf(targetDef, row) : row[valueField];\n // The page's keys, asked of `disabledKeys` once per distinct set.\n const pageKeys = rows\n .map(keyOf)\n .filter((key): key is string | number => typeof key === 'string' || typeof key === 'number');\n const asked = JSON.stringify(pageKeys);\n const [disabled, setDisabled] = React.useState<DisabledState | undefined>(undefined);\n // A Retry after a failed answer asks the same keys again.\n const [attempt, setAttempt] = React.useState(0);\n // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by the page's keys, not the rows' identity\n React.useEffect(() => {\n if (disabledKeys === undefined || pageKeys.length === 0) return;\n const controller = new AbortController();\n disabledKeys(pageKeys, controller.signal).then(\n (keys) => {\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys });\n },\n () => {\n // Fails closed: a row that may already be linked is never offered.\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys: 'failed' });\n },\n );\n return () => controller.abort();\n }, [asked, attempt, disabledKeys]);\n // The answer for the page shown now; undefined while it is being asked.\n const answer =\n disabledKeys === undefined ||\n pageKeys.length === 0 ||\n disabled === undefined ||\n disabled.asked !== asked ||\n disabled.attempt !== attempt\n ? undefined\n : disabled.keys;\n const checking = disabledKeys !== undefined && pageKeys.length > 0 && answer === undefined;\n const checkFailed = answer === 'failed';\n const isDisabled = (row: Record<string, unknown>): boolean => {\n if (disabledKeys === undefined) return false;\n if (answer === undefined || answer === 'failed') return true;\n return answer.has(String(keyOf(row)));\n };\n const disabledCount =\n answer === undefined || answer === 'failed' ? 0 : rows.filter(isDisabled).length;\n\n const toggleSort = (field: string) => {\n setSort((prev) =>\n prev.field === field\n ? { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' }\n : { field, dir: 'asc' },\n );\n };\n\n const targetLabel = resolveResourceLabel(caps, to, 'single', targetDef);\n const colCount = 2 + extraColumns.length;\n\n const pick = (row: { id: unknown } & Record<string, unknown>) => {\n if (isDisabled(row)) return;\n const picked = keyOf(row);\n // A row without the value the field stores is never written as its id instead (spec 4.2).\n if (picked === null || picked === undefined || picked === '') return;\n if (multiple) {\n onToggle?.(picked as string | number, row as Record<string, unknown>);\n return;\n }\n onSelect(picked as string | number);\n onSelectRow?.(row as Record<string, unknown>);\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange} modal={create?.pending !== true}>\n <DialogContent\n // While a create of this select is open its sheet stacks above this dialog and stays\n // interactive: the dialog neither traps focus nor closes on a click outside, and Escape\n // closes only the sheet (P6-67).\n {...(create?.pending === true\n ? {\n onInteractOutside: (event: Event) => event.preventDefault(),\n onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),\n }\n : {})}\n className=\"flex max-h-[85vh] w-[calc(100vw-1.5rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0\"\n data-testid={`relation-picker-dialog:${to}`}\n >\n <DialogHeader className=\"border-b px-4 py-3\">\n <DialogTitle className=\"text-base\">\n {tr('relationPicker.title', { name: targetLabel }, `Select: ${targetLabel}`)}\n </DialogTitle>\n <DialogDescription className=\"sr-only\">\n {tr('relationPicker.advancedSearch', undefined, 'Advanced search')}\n </DialogDescription>\n </DialogHeader>\n\n {/* Search + the filter builder (#9 / Task 5 SP3) */}\n <div className=\"flex flex-wrap items-center gap-2 border-b px-4 py-2.5\">\n {filterState.searchEnabled && (\n <div className=\"relative min-w-[12rem] flex-1\">\n <IconSearch className=\"pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n autoFocus\n value={search}\n onChange={(e) => setSearchInput(e.target.value)}\n placeholder={tr('toolbar.search', { name: targetLabel }, `Search ${targetLabel}…`)}\n data-testid={`relation-picker-dialog:${to}:search`}\n className=\"h-9 pl-8\"\n />\n </div>\n )}\n {/*\n No `pinnedFields`/`pinnedValue`/`setPinnedValue` — this dialog has\n no pinned slot (`useDialogFilterState`'s doc: a modal has no\n spare real estate for always-visible facets), so `FilterBuilder`\n falls back to its own stable empty-array defaults.\n */}\n <FilterBuilder\n group={filterState.group}\n setGroup={filterState.setGroup}\n available={filterState.available}\n fixed={filterState.fixed}\n setPage={filterState.setPage}\n testIdBase={`relation-picker-dialog:${to}:filter`}\n />\n </div>\n\n {/* Fixed-height table area — constant height so paging never jumps. */}\n <div className=\"h-[46vh] min-h-[16rem] overflow-auto\">\n <Table>\n <TableHeader className=\"sticky top-0 z-10 bg-background shadow-[inset_0_-1px_0_var(--border)]\">\n <TableRow className=\"hover:bg-transparent\">\n <TableHead\n className=\"h-9 w-14 cursor-pointer select-none\"\n onClick={() => toggleSort('id')}\n >\n {tr('relationPicker.idColumn', undefined, 'ID')}{' '}\n <SortIcon field=\"id\" sort={sort} />\n </TableHead>\n <TableHead\n className={cn('h-9 select-none', titleSortable && 'cursor-pointer')}\n {...(titleSortable ? { onClick: () => toggleSort(displayField) } : {})}\n >\n {targetLabel} {titleSortable && <SortIcon field={displayField} sort={sort} />}\n </TableHead>\n {extraColumns.map((c) => (\n <TableHead\n key={c}\n className=\"h-9 cursor-pointer select-none\"\n onClick={() => toggleSort(c)}\n >\n {resolveFieldLabel(\n targetCaps,\n c,\n declaredText('field', targetDef?.fields?.[c], 'label'),\n )}\n <SortIcon field={c} sort={sort} />\n </TableHead>\n ))}\n <TableHead className=\"h-9 w-8\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {isLoading ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount + 1} className=\"h-64 text-center\">\n <IconLoader2 className=\"mx-auto size-4 animate-spin text-muted-foreground\" />\n </TableCell>\n </TableRow>\n ) : rows.length === 0 ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell\n colSpan={colCount + 1}\n className=\"h-64 text-center text-sm text-muted-foreground\"\n >\n {tr('relation.noResults', undefined, 'No results.')}\n </TableCell>\n </TableRow>\n ) : (\n rows.map((row) => {\n const idStr = String(recordKeyOf(targetDef, row));\n const rowDisabled = isDisabled(row);\n // A disabled row is drawn checked: it is already chosen elsewhere.\n const checked =\n rowDisabled ||\n (selected ?? []).some((key) => String(key) === String(keyOf(row)));\n return (\n <TableRow\n key={idStr}\n className={cn(\n rowDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',\n )}\n data-testid={`relation-picker-dialog:${to}:row:${idStr}`}\n {...(multiple ? { 'aria-selected': checked } : {})}\n {...(rowDisabled ? { 'aria-disabled': true } : {})}\n onClick={() => pick(row)}\n >\n <TableCell className=\"py-2 tabular-nums text-muted-foreground\">\n {tr('relationPicker.recordNumber', { id: idStr }, `#${idStr}`)}\n </TableCell>\n <TableCell className=\"py-2 font-medium\">{rowTitle(row)}</TableCell>\n {extraColumns.map((c) => (\n <TableCell key={c} className=\"py-2 text-muted-foreground\">\n {/* The list's display renderers: locale numbers and dates, enum labels. */}\n <ResolvedFieldDisplay\n resource={targetDef?.ref ?? { namespace: 'default', id: to }}\n name={c}\n field={\n (targetDef === undefined ? undefined : declaredField(targetDef, c)) ??\n (targetDef?.fields?.[c] as never) ?? { type: 'unknown' }\n }\n value={row[c]}\n record={row}\n fallback={formatCell(row[c], targetDef?.fields?.[c], (ns) =>\n app.registry.enums.get(ns),\n )}\n />\n </TableCell>\n ))}\n <TableCell className=\"py-2 text-right\">\n {multiple ? (\n <IconCheck\n className={cn('ml-auto size-4', checked ? 'opacity-100' : 'opacity-0')}\n aria-hidden\n />\n ) : (\n <IconChevronRight className=\"ml-auto size-4 text-muted-foreground/50\" />\n )}\n </TableCell>\n </TableRow>\n );\n })\n )}\n </TableBody>\n </Table>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-between gap-2 border-t px-4 py-2.5 sm:justify-between\">\n {/*\n A wrapper that is always there: the footer is `justify-between`, so dropping its first\n child (no create offered) would slide the pager to the left edge.\n */}\n <div className=\"flex items-center gap-3\">\n {checking && (\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-checking`}\n >\n <IconLoader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />\n {tr('relationPicker.checking', undefined, 'Checking these rows…')}\n </span>\n )}\n {checkFailed && (\n <span\n role=\"alert\"\n className=\"inline-flex items-center gap-2 text-sm text-destructive\"\n data-testid={`relation-picker-dialog:${to}:disabled-failed`}\n >\n {tr(\n 'relationPicker.checkFailed',\n undefined,\n 'These rows could not be checked, so none can be chosen.',\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7\"\n onClick={() => setAttempt((n) => n + 1)}\n data-testid={`relation-picker-dialog:${to}:disabled-retry`}\n >\n {chromeT('actions.retry', { defaultValue: 'Retry' })}\n </Button>\n </span>\n )}\n {disabledCount > 0 && disabledLabel !== undefined && (\n <span\n className=\"text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-count`}\n >\n {disabledLabel(disabledCount)}\n </span>\n )}\n {create !== undefined && (\n <RelationCreateButton\n to={to}\n fieldName={fieldName ?? to}\n create={create}\n seeds={createSeeds}\n search={search}\n beforeOpen={onBeforeCreate ?? noop}\n className=\"inline-flex items-center gap-1 whitespace-nowrap text-sm text-foreground hover:underline disabled:opacity-50\"\n />\n )}\n </div>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <span className=\"hidden tabular-nums sm:inline\">\n {knownTotal\n ? tr(\n 'pagination.pageOf',\n { page: filterState.page, total: totalPages },\n `${filterState.page} / ${totalPages}`,\n )\n : tr('pagination.pageOnly', { page: filterState.page }, `${filterState.page}`)}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={filterState.page <= 1}\n onClick={() => filterState.setPage(Math.max(1, filterState.page - 1))}\n data-testid={`relation-picker-dialog:${to}:prev`}\n >\n {tr('relationPicker.prev', undefined, 'Prev')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={!hasNextPage}\n onClick={() => filterState.setPage(filterState.page + 1)}\n data-testid={`relation-picker-dialog:${to}:next`}\n >\n {tr('relationPicker.next', undefined, 'Next')}\n </Button>\n {multiple && (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => (onDone === undefined ? onOpenChange(false) : onDone())}\n data-testid={`relation-picker-dialog:${to}:done`}\n >\n {tr('relationPicker.select', undefined, 'Select')}\n </Button>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * A column's sort arrow. Declared here, not inside the dialog: a component made in a render is a\n * new type on every render, so React replaced these icons each time the dialog rendered — and a\n * node removed from the dialog while focus was crossing into the filter popover (which is portaled\n * outside it, so focus sits on `body` for that moment) makes the dialog's focus scope take focus\n * back, which closed the popover on any click inside it.\n */\nfunction SortIcon({\n field,\n sort,\n}: {\n readonly field: string;\n readonly sort: { readonly field: string; readonly dir: 'asc' | 'desc' };\n}): React.ReactElement {\n if (sort.field !== field) return <IconSelector className=\"ml-1 inline size-3 opacity-40\" />;\n return sort.dir === 'asc' ? (\n <IconArrowUp className=\"ml-1 inline size-3\" />\n ) : (\n <IconArrowDown className=\"ml-1 inline size-3\" />\n );\n}\n\nfunction formatCell(\n value: unknown,\n field: unknown,\n enumOf: (namespace: string) => Parameters<typeof enumValueText>[0],\n): string {\n if (value == null || value === '') return '—';\n if (typeof value === 'boolean') return value ? '✓' : '—';\n // Map an option value → its human label: the field's `texts.options`, the bound enum's\n // `texts`, else a label the field's `meta.options` carries (adopter data).\n const namespace =\n field !== null && typeof field === 'object'\n ? enumNamespaceOf(field as Parameters<typeof enumNamespaceOf>[0])\n : undefined;\n const declared =\n fieldOptionText(field, String(value)) ??\n (namespace === undefined ? undefined : enumValueText(enumOf(namespace), String(value)));\n if (declared !== undefined) return declared;\n const options = (field as { meta?: { options?: EnumOption[] } } | undefined)?.meta?.options;\n if (Array.isArray(options)) {\n const hit = options.find((o) => o.value === String(value));\n if (hit && typeof hit.label === 'string') return hit.label;\n }\n const s = String(value);\n return s.length > 40 ? `${s.slice(0, 40)}…` : s;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"relation-picker-dialog-mbwyNK43.cjs","names":["routeResourceKey","useApp","useI18nCaps","useOptionalT","useChromeT","useRowTitle","React","useDialogFilterState","useListAccess","columnName","useList","recordKeyOf","resolveResourceLabel","Dialog","DialogContent","DialogHeader","DialogTitle","DialogDescription","IconSearch","Input","FilterBuilder","Table","TableHeader","TableRow","TableHead","cn","resolveFieldLabel","declaredText","TableBody","TableCell","IconLoader2","ResolvedFieldDisplay","declaredField","IconCheck","IconChevronRight","DialogFooter","Button","RelationCreateButton","IconSelector","IconArrowUp","IconArrowDown","enumNamespaceOf","fieldOptionText","enumValueText"],"sources":["../src/fields/relation-picker-dialog.tsx"],"sourcesContent":["'use client';\nimport * as React from 'react';\nimport {\n IconArrowDown,\n IconArrowUp,\n IconCheck,\n IconChevronRight,\n IconSelector,\n IconLoader2,\n IconSearch,\n} from '@tabler/icons-react';\nimport {\n type FixedFilterInput,\n type ScopeSource,\n declaredField,\n declaredText,\n enumNamespaceOf,\n enumValueText,\n fieldOptionText,\n resolveFieldLabel,\n resolveResourceLabel,\n routeResourceKey,\n type ResourceNameOrRef,\n columnName,\n} from '@burojs/core';\nimport {\n recordKeyOf,\n useApp,\n useChromeT,\n useI18nCaps,\n useList,\n useListAccess,\n useOptionalT,\n} from '@burojs/react';\nimport { useRowTitle } from '../resource/use-row-title.js';\nimport { ResolvedFieldDisplay } from './resolved-field.js';\nimport { cn } from '../lib/cn.js';\nimport { Button } from '../primitives/button.js';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog.js';\nimport { Input } from '../primitives/input.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '../primitives/table.js';\nimport { FilterBuilder } from '../list-kit/filter-builder/filter-builder.js';\nimport { useDialogFilterState } from './use-dialog-filter-state.js';\nimport { RelationCreateButton, type SelectCreate } from './relation-create.js';\n\nconst noop = (): void => undefined;\n\n/**\n * Advanced-search modal for the relation picker (#11 / #9). The inline popover\n * handles the quick \"type a few letters\" case; this dialog is the escalation:\n * the full target list with SEARCH, per-column FILTERS, SORTING and PAGINATION,\n * plus a \"Create\" shortcut. Resolved generically from the registry so it works\n * for any resource with no per-field wiring.\n *\n * #9 — compact, and the table area is a FIXED height so paging never resizes /\n * jumps the dialog; responsive down to mobile.\n */\nexport interface RelationPickerDialogProps<N extends string = string> {\n to: ResourceNameOrRef<N>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (value: string | number) => void;\n /** Optional: receives the FULL chosen row (for prefilling sibling fields). */\n onSelectRow?: (row: Record<string, unknown>) => void;\n /**\n * Fallback search field — an OVERRIDE, not a required prop. The search box\n * expands through `useDialogFilterState`, which resolves what to search in\n * this order:\n * 1. the TARGET resource's own `views.list.search` declaration, if any —\n * always wins, regardless of this prop;\n * 2. this `searchField` prop, when the target declares no search;\n * 3. the target's `displayField` (itself falling back to `'name'`), when\n * neither of the above is available.\n *\n * A resolver-shaped `SearchDecl` has no single field that could be named\n * here, and a target with no search declaration at all should still be\n * searchable somehow — hence the two-step fallback instead of requiring\n * this prop outright.\n */\n searchField?: string;\n /**\n * Which field of the chosen row `onSelect` returns. Defaults to the target's id\n * field (`data.idField`, else `id`: a normal FK relation). Set to another column (e.g. `gtin`) for \"relate by a non-id\n * value\" pickers — the core mechanic behind the playground GTIN field.\n */\n valueField?: string;\n /**\n * Optional extra filters ANDed into every query (e.g. scope by product\n * group) — the same `FixedFilterInput` shape `useResourceList` and\n * `RelationPickerProps.fixedFilters` take: a bare AST expression\n * (`@burojs/core`'s `FilterExpression`, defaulting to `lock: true`, no\n * badge), or `{ filter, lock?, label? }` for finer control. `lock: false`\n * leaves the field open in the builder instead of hiding it; `label`\n * renders it as an undeletable badge.\n */\n fixedFilters?: readonly FixedFilterInput[];\n /**\n * The read scope of the dialog's list, as the select's own option reads take it. Default\n * `'context'`: the record around the select narrows the rows.\n */\n scope?: ScopeSource;\n /**\n * The record field the select writes (`useList`'s `scopeField`): the record's value of it never\n * narrows the rows offered for it. Absent: every field of the record narrows.\n */\n scopeField?: string;\n /**\n * The select's one pending create (P6-34), shared with its popover. With it the footer offers\n * \"+ Create\" (the target's `quick.create`, handed back through the select's own query); without\n * it the dialog offers none. The dialog is not a dismiss scope and holds no form: the sheet a\n * create opens stacks above it.\n */\n create?: SelectCreate;\n /** The trusted `defaults` of that create: the registry field's static `eq` literals (P6-31). */\n createSeeds?: Readonly<Record<string, unknown>>;\n /** The form field holding the key (the create button's test id). */\n fieldName?: string;\n /** Runs before a create opens: the select returns focus to its trigger. */\n onBeforeCreate?: () => void;\n pageSize?: number;\n /**\n * A to-many select's choice (P7 3.5): a click toggles the row in `selected` through `onToggle`\n * and the dialog stays open until the user is done. Without it, a click picks one row and closes.\n */\n multiple?: boolean;\n /** The keys already chosen (`multiple` only): their rows are marked. */\n selected?: readonly (string | number)[];\n /** A row toggled in `multiple` mode: its value, and the row itself. */\n onToggle?: (value: string | number, row: Record<string, unknown>) => void;\n /**\n * `multiple` only: the Select button confirms the choice through this instead of closing the\n * dialog with `onOpenChange(false)`, so a confirm and a dismissal can be told apart.\n */\n onDone?: () => void;\n /**\n * Asked once per shown page with the keys of its rows: rows whose key is in the resolved set\n * are drawn checked and disabled (a link panel's already-linked targets). Until it answers the\n * page's rows cannot be picked and the footer says the rows are being checked; when it fails\n * none of them can, and the footer says so with a Retry.\n */\n disabledKeys?: (\n keys: readonly (string | number)[],\n signal: AbortSignal,\n ) => Promise<ReadonlySet<string>>;\n /**\n * With `disabledKeys`: the footer's note for the page's disabled rows, given how many there\n * are (the caller says what they are, \"3 already linked\"). Absent: no note.\n */\n disabledLabel?: (count: number) => React.ReactNode;\n}\n\nconst DEBOUNCE_MS = 250;\n\n/** The disabled rows of a page: the keys asked (and the attempt), and the answer. */\ninterface DisabledState {\n readonly asked: string;\n readonly attempt: number;\n readonly keys: ReadonlySet<string> | 'failed';\n}\n\ntype EnumOption = { value: string; label: string };\n\nexport function RelationPickerDialog<const N extends string = string>({\n to: toInput,\n open,\n onOpenChange,\n onSelect,\n onSelectRow,\n searchField,\n valueField: valueFieldProp,\n fixedFilters,\n scope = 'context',\n scopeField,\n create,\n createSeeds,\n fieldName,\n onBeforeCreate,\n pageSize = 8,\n multiple = false,\n selected,\n onToggle,\n onDone,\n disabledKeys,\n disabledLabel,\n}: RelationPickerDialogProps<N>): React.ReactElement {\n const to = routeResourceKey(toInput);\n const app = useApp();\n const caps = useI18nCaps();\n // The target's own caps: a column header reads `resources.<to>.fields.<f>.label` first.\n const targetCaps = useI18nCaps(to);\n const t = useOptionalT();\n const chromeT = useChromeT();\n const tr = (key: string, params?: Record<string, unknown>, fallback?: string) =>\n t?.(key, { params, defaultValue: fallback }) ?? fallback ?? key;\n const targetDef = app.registry.get(to);\n const rowTitle = useRowTitle(to, targetDef?.displayField, { recordTitle: true });\n const declaredIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof declaredIdField === 'string' && declaredIdField.length > 0 ? declaredIdField : 'id';\n const valueField = valueFieldProp ?? idField;\n\n const [search, setSearchInput] = React.useState('');\n const [debounced, setDebounced] = React.useState('');\n const [sort, setSort] = React.useState<{ field: string; dir: 'asc' | 'desc' }>({\n field: 'id',\n dir: 'desc',\n });\n\n const displayField = targetDef?.displayField ?? 'name';\n\n // Resolution order (see `RelationPickerDialogProps.searchField`'s doc):\n // the target's own `views.list.search` declaration always wins inside\n // `useDialogFilterState`; this `searchField` prop is only consulted THERE\n // when the target declares no search at all, and `displayField` is what\n // this dialog hands the hook when the caller didn't override either — so\n // a target with no declaration and no override is still searchable.\n //\n // `enabled: open` — `RelationPickerDialog` is always mounted by\n // `<RelationPicker>` (it's a controlled dialog, not conditionally\n // rendered), so without this gate the field-derivation walk inside\n // `useFilterFields` (`categorizeField` over every field of the target)\n // would run on every mount and every `fixedFilters`-identity change of\n // EVERY relation field on a form, whether its dialog ever opens or not.\n // Gating recomputes in full the moment `open` flips true — same render,\n // before the dialog shows anything, so nothing it displays changes.\n const filterState = useDialogFilterState(to, {\n fixedFilters,\n searchField: searchField ?? displayField,\n enabled: open,\n });\n\n React.useEffect(() => {\n const h = setTimeout(() => setDebounced(search), DEBOUNCE_MS);\n return () => clearTimeout(h);\n }, [search]);\n\n // The debounced text is the one thing this dialog composes itself — the\n // rest of the filter (fixed scope, the builder's group, and the actual\n // `fixed → search → group` composition) all live in `useDialogFilterState`\n // now, via the SAME `composeListFilter` core the list page uses.\n React.useEffect(() => {\n filterState.setSearch(debounced);\n // `filterState.setSearch` is stable (useCallback, empty deps) — only\n // `debounced` driving this is real; including the whole `filterState`\n // object would refire on every recompute of its OWN memo (its `filter`\n // depends on this very effect's result), which is not what \"debounced\n // changed\" means.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debounced]);\n\n // Reset volatile state each time the dialog opens.\n React.useEffect(() => {\n if (open) {\n setSearchInput('');\n setDebounced('');\n filterState.reset();\n }\n // `filterState.reset` is stable (useCallback, empty deps); `filterState`\n // itself is excluded for the same reason as the effect above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const listAccess = useListAccess(to);\n const extraColumns = React.useMemo(() => {\n const declared = (targetDef?.views?.list?.columns ?? []).map(columnName);\n // The same predicate as the list table: a pending decision shows no extra column.\n return declared\n .filter((c) => c !== 'id' && c !== displayField)\n .filter((c) => listAccess.status === 'ready' && listAccess.columnVisible(c))\n .slice(0, 2);\n }, [targetDef, displayField, listAccess]);\n const titleSortable = listAccess.status === 'ready' && listAccess.fieldVisible(displayField);\n // A sort on a field the user may not view is an ordering oracle: it is dropped, never sent.\n const effectiveSortField =\n sort.field === 'id' ||\n (sort.field === displayField ? titleSortable : extraColumns.includes(sort.field))\n ? sort.field\n : 'id';\n\n const query = useList(to, {\n ...(filterState.filter ? { filter: filterState.filter } : {}),\n sort: [\n {\n field: effectiveSortField,\n direction: effectiveSortField === sort.field ? sort.dir : 'desc',\n },\n ],\n pagination: { kind: 'offset', page: filterState.page, pageSize },\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n enabled: open && targetDef != null && !filterState.blocked,\n });\n\n const rows = (query.data?.data ?? []) as Array<{ id: unknown } & Record<string, unknown>>;\n // Unknown-total contract: a provider that cannot count omits `total` (Hasura\n // `noAggregate` targets such as `users` / `code_code`). Deriving `totalPages`\n // from a `?? 0` fallback pinned it to 1 and disabled Next forever. With an\n // unknown total we page forward while the page is full.\n const total = query.data?.total;\n const knownTotal = typeof total === 'number';\n const totalPages = knownTotal ? Math.max(1, Math.ceil(total / pageSize)) : undefined;\n const pageIsFull = rows.length >= pageSize;\n const hasNextPage = knownTotal ? filterState.page < (totalPages as number) : pageIsFull;\n const isLoading = query.isLoading || query.isFetching;\n\n // The key a row is chosen, selected and disabled by: the target's idField unless a value field\n // names another column.\n const keyOf = (row: Record<string, unknown>): unknown =>\n valueField === idField ? recordKeyOf(targetDef, row) : row[valueField];\n // The page's keys, asked of `disabledKeys` once per distinct set.\n const pageKeys = rows\n .map(keyOf)\n .filter((key): key is string | number => typeof key === 'string' || typeof key === 'number');\n const asked = JSON.stringify(pageKeys);\n const [disabled, setDisabled] = React.useState<DisabledState | undefined>(undefined);\n // A Retry after a failed answer asks the same keys again.\n const [attempt, setAttempt] = React.useState(0);\n // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by the page's keys, not the rows' identity\n React.useEffect(() => {\n if (disabledKeys === undefined || pageKeys.length === 0) return;\n const controller = new AbortController();\n disabledKeys(pageKeys, controller.signal).then(\n (keys) => {\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys });\n },\n () => {\n // Fails closed: a row that may already be linked is never offered.\n if (!controller.signal.aborted) setDisabled({ asked, attempt, keys: 'failed' });\n },\n );\n return () => controller.abort();\n }, [asked, attempt, disabledKeys]);\n // The answer for the page shown now; undefined while it is being asked.\n const answer =\n disabledKeys === undefined ||\n pageKeys.length === 0 ||\n disabled === undefined ||\n disabled.asked !== asked ||\n disabled.attempt !== attempt\n ? undefined\n : disabled.keys;\n const checking = disabledKeys !== undefined && pageKeys.length > 0 && answer === undefined;\n const checkFailed = answer === 'failed';\n const isDisabled = (row: Record<string, unknown>): boolean => {\n if (disabledKeys === undefined) return false;\n if (answer === undefined || answer === 'failed') return true;\n return answer.has(String(keyOf(row)));\n };\n const disabledCount =\n answer === undefined || answer === 'failed' ? 0 : rows.filter(isDisabled).length;\n\n const toggleSort = (field: string) => {\n setSort((prev) =>\n prev.field === field\n ? { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' }\n : { field, dir: 'asc' },\n );\n };\n\n const targetLabel = resolveResourceLabel(caps, to, 'single', targetDef);\n const colCount = 2 + extraColumns.length;\n\n const pick = (row: { id: unknown } & Record<string, unknown>) => {\n if (isDisabled(row)) return;\n const picked = keyOf(row);\n // A row without the value the field stores is never written as its id instead (spec 4.2).\n if (picked === null || picked === undefined || picked === '') return;\n if (multiple) {\n onToggle?.(picked as string | number, row as Record<string, unknown>);\n return;\n }\n onSelect(picked as string | number);\n onSelectRow?.(row as Record<string, unknown>);\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange} modal={create?.pending !== true}>\n <DialogContent\n // While a create of this select is open its sheet stacks above this dialog and stays\n // interactive: the dialog neither traps focus nor closes on a click outside, and Escape\n // closes only the sheet (P6-67).\n {...(create?.pending === true\n ? {\n onInteractOutside: (event: Event) => event.preventDefault(),\n onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),\n }\n : {})}\n className=\"flex max-h-[85vh] w-[calc(100vw-1.5rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0\"\n data-testid={`relation-picker-dialog:${to}`}\n >\n <DialogHeader className=\"border-b px-4 py-3\">\n <DialogTitle className=\"text-base\">\n {tr('relationPicker.title', { name: targetLabel }, `Select: ${targetLabel}`)}\n </DialogTitle>\n <DialogDescription className=\"sr-only\">\n {tr('relationPicker.advancedSearch', undefined, 'Advanced search')}\n </DialogDescription>\n </DialogHeader>\n\n {/* Search + the filter builder (#9 / Task 5 SP3) */}\n <div className=\"flex flex-wrap items-center gap-2 border-b px-4 py-2.5\">\n {filterState.searchEnabled && (\n <div className=\"relative min-w-[12rem] flex-1\">\n <IconSearch className=\"pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n autoFocus\n value={search}\n onChange={(e) => setSearchInput(e.target.value)}\n placeholder={tr('toolbar.search', { name: targetLabel }, `Search ${targetLabel}…`)}\n data-testid={`relation-picker-dialog:${to}:search`}\n className=\"h-9 pl-8\"\n />\n </div>\n )}\n {/*\n No `pinnedFields`/`pinnedValue`/`setPinnedValue` — this dialog has\n no pinned slot (`useDialogFilterState`'s doc: a modal has no\n spare real estate for always-visible facets), so `FilterBuilder`\n falls back to its own stable empty-array defaults.\n */}\n <FilterBuilder\n group={filterState.group}\n setGroup={filterState.setGroup}\n available={filterState.available}\n fixed={filterState.fixed}\n setPage={filterState.setPage}\n testIdBase={`relation-picker-dialog:${to}:filter`}\n />\n </div>\n\n {/* Fixed-height table area — constant height so paging never jumps. */}\n <div className=\"h-[46vh] min-h-[16rem] overflow-auto\">\n <Table>\n <TableHeader className=\"sticky top-0 z-10 bg-background shadow-[inset_0_-1px_0_var(--border)]\">\n <TableRow className=\"hover:bg-transparent\">\n <TableHead\n className=\"h-9 w-14 cursor-pointer select-none\"\n onClick={() => toggleSort('id')}\n >\n {tr('relationPicker.idColumn', undefined, 'ID')}{' '}\n <SortIcon field=\"id\" sort={sort} />\n </TableHead>\n <TableHead\n className={cn('h-9 select-none', titleSortable && 'cursor-pointer')}\n {...(titleSortable ? { onClick: () => toggleSort(displayField) } : {})}\n >\n {targetLabel} {titleSortable && <SortIcon field={displayField} sort={sort} />}\n </TableHead>\n {extraColumns.map((c) => (\n <TableHead\n key={c}\n className=\"h-9 cursor-pointer select-none\"\n onClick={() => toggleSort(c)}\n >\n {resolveFieldLabel(\n targetCaps,\n c,\n declaredText('field', targetDef?.fields?.[c], 'label'),\n )}\n <SortIcon field={c} sort={sort} />\n </TableHead>\n ))}\n <TableHead className=\"h-9 w-8\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {isLoading ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount + 1} className=\"h-64 text-center\">\n <IconLoader2 className=\"mx-auto size-4 animate-spin text-muted-foreground\" />\n </TableCell>\n </TableRow>\n ) : rows.length === 0 ? (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell\n colSpan={colCount + 1}\n className=\"h-64 text-center text-sm text-muted-foreground\"\n >\n {tr('relation.noResults', undefined, 'No results.')}\n </TableCell>\n </TableRow>\n ) : (\n rows.map((row) => {\n const idStr = String(recordKeyOf(targetDef, row));\n const rowDisabled = isDisabled(row);\n // A disabled row is drawn checked: it is already chosen elsewhere.\n const checked =\n rowDisabled ||\n (selected ?? []).some((key) => String(key) === String(keyOf(row)));\n return (\n <TableRow\n key={idStr}\n className={cn(\n rowDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',\n )}\n data-testid={`relation-picker-dialog:${to}:row:${idStr}`}\n {...(multiple ? { 'aria-selected': checked } : {})}\n {...(rowDisabled ? { 'aria-disabled': true } : {})}\n onClick={() => pick(row)}\n >\n <TableCell className=\"py-2 tabular-nums text-muted-foreground\">\n {tr('relationPicker.recordNumber', { id: idStr }, `#${idStr}`)}\n </TableCell>\n <TableCell className=\"py-2 font-medium\">{rowTitle(row)}</TableCell>\n {extraColumns.map((c) => (\n <TableCell key={c} className=\"py-2 text-muted-foreground\">\n {/* The list's display renderers: locale numbers and dates, enum labels. */}\n <ResolvedFieldDisplay\n resource={targetDef?.ref ?? { namespace: 'default', id: to }}\n name={c}\n field={\n (targetDef === undefined ? undefined : declaredField(targetDef, c)) ??\n (targetDef?.fields?.[c] as never) ?? { type: 'unknown' }\n }\n value={row[c]}\n record={row}\n fallback={formatCell(row[c], targetDef?.fields?.[c], (ns) =>\n app.registry.enums.get(ns),\n )}\n />\n </TableCell>\n ))}\n <TableCell className=\"py-2 text-right\">\n {multiple ? (\n <IconCheck\n className={cn('ml-auto size-4', checked ? 'opacity-100' : 'opacity-0')}\n aria-hidden\n />\n ) : (\n <IconChevronRight className=\"ml-auto size-4 text-muted-foreground/50\" />\n )}\n </TableCell>\n </TableRow>\n );\n })\n )}\n </TableBody>\n </Table>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-between gap-2 border-t px-4 py-2.5 sm:justify-between\">\n {/*\n A wrapper that is always there: the footer is `justify-between`, so dropping its first\n child (no create offered) would slide the pager to the left edge.\n */}\n <div className=\"flex items-center gap-3\">\n {checking && (\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-checking`}\n >\n <IconLoader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />\n {tr('relationPicker.checking', undefined, 'Checking these rows…')}\n </span>\n )}\n {checkFailed && (\n <span\n role=\"alert\"\n className=\"inline-flex items-center gap-2 text-sm text-destructive\"\n data-testid={`relation-picker-dialog:${to}:disabled-failed`}\n >\n {tr(\n 'relationPicker.checkFailed',\n undefined,\n 'These rows could not be checked, so none can be chosen.',\n )}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7\"\n onClick={() => setAttempt((n) => n + 1)}\n data-testid={`relation-picker-dialog:${to}:disabled-retry`}\n >\n {chromeT('actions.retry', { defaultValue: 'Retry' })}\n </Button>\n </span>\n )}\n {disabledCount > 0 && disabledLabel !== undefined && (\n <span\n className=\"text-sm text-muted-foreground\"\n data-testid={`relation-picker-dialog:${to}:disabled-count`}\n >\n {disabledLabel(disabledCount)}\n </span>\n )}\n {create !== undefined && (\n <RelationCreateButton\n to={to}\n fieldName={fieldName ?? to}\n create={create}\n seeds={createSeeds}\n search={search}\n beforeOpen={onBeforeCreate ?? noop}\n className=\"inline-flex items-center gap-1 whitespace-nowrap text-sm text-foreground hover:underline disabled:opacity-50\"\n />\n )}\n </div>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <span className=\"hidden tabular-nums sm:inline\">\n {knownTotal\n ? tr(\n 'pagination.pageOf',\n { page: filterState.page, total: totalPages },\n `${filterState.page} / ${totalPages}`,\n )\n : tr('pagination.pageOnly', { page: filterState.page }, `${filterState.page}`)}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={filterState.page <= 1}\n onClick={() => filterState.setPage(Math.max(1, filterState.page - 1))}\n data-testid={`relation-picker-dialog:${to}:prev`}\n >\n {tr('relationPicker.prev', undefined, 'Prev')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-8\"\n disabled={!hasNextPage}\n onClick={() => filterState.setPage(filterState.page + 1)}\n data-testid={`relation-picker-dialog:${to}:next`}\n >\n {tr('relationPicker.next', undefined, 'Next')}\n </Button>\n {multiple && (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => (onDone === undefined ? onOpenChange(false) : onDone())}\n data-testid={`relation-picker-dialog:${to}:done`}\n >\n {tr('relationPicker.select', undefined, 'Select')}\n </Button>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * A column's sort arrow. Declared here, not inside the dialog: a component made in a render is a\n * new type on every render, so React replaced these icons each time the dialog rendered — and a\n * node removed from the dialog while focus was crossing into the filter popover (which is portaled\n * outside it, so focus sits on `body` for that moment) makes the dialog's focus scope take focus\n * back, which closed the popover on any click inside it.\n */\nfunction SortIcon({\n field,\n sort,\n}: {\n readonly field: string;\n readonly sort: { readonly field: string; readonly dir: 'asc' | 'desc' };\n}): React.ReactElement {\n if (sort.field !== field) return <IconSelector className=\"ml-1 inline size-3 opacity-40\" />;\n return sort.dir === 'asc' ? (\n <IconArrowUp className=\"ml-1 inline size-3\" />\n ) : (\n <IconArrowDown className=\"ml-1 inline size-3\" />\n );\n}\n\nfunction formatCell(\n value: unknown,\n field: unknown,\n enumOf: (namespace: string) => Parameters<typeof enumValueText>[0],\n): string {\n if (value == null || value === '') return '—';\n if (typeof value === 'boolean') return value ? '✓' : '—';\n // Map an option value → its human label: the field's `texts.options`, the bound enum's\n // `texts`, else a label the field's `meta.options` carries (adopter data).\n const namespace =\n field !== null && typeof field === 'object'\n ? enumNamespaceOf(field as Parameters<typeof enumNamespaceOf>[0])\n : undefined;\n const declared =\n fieldOptionText(field, String(value)) ??\n (namespace === undefined ? undefined : enumValueText(enumOf(namespace), String(value)));\n if (declared !== undefined) return declared;\n const options = (field as { meta?: { options?: EnumOption[] } } | undefined)?.meta?.options;\n if (Array.isArray(options)) {\n const hit = options.find((o) => o.value === String(value));\n if (hit && typeof hit.label === 'string') return hit.label;\n }\n const s = String(value);\n return s.length > 40 ? `${s.slice(0, 40)}…` : s;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  const require_use_theme = require("./use-theme-DFlpXils.cjs");
2
- const require_relation_create = require("./relation-create-_SFuPQGM.cjs");
2
+ const require_relation_create = require("./relation-create-B8zngAcA.cjs");
3
3
  let _burojs_react = require("@burojs/react");
4
4
  let react = require("react");
5
5
  react = require_use_theme.__toESM(react, 1);
@@ -52,4 +52,4 @@ Object.defineProperty(exports, "useRelationSelect", {
52
52
  }
53
53
  });
54
54
 
55
- //# sourceMappingURL=relation-select-C3aV18kT.cjs.map
55
+ //# sourceMappingURL=relation-select-CguIo2M5.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"relation-select-C3aV18kT.cjs","names":["useApp","useListAccess","React","narrowSearchDecl","useSelectCreate"],"sources":["../src/fields/relation-select.ts"],"sourcesContent":["'use client';\n\nimport type { FixedFilterInput, ScopeSource, SearchDecl } from '@burojs/core';\nimport { useApp, useListAccess } from '@burojs/react';\nimport * as React from 'react';\nimport { type SelectCreate, useSelectCreate } from './relation-create.js';\nimport { narrowSearchDecl } from './search-access.js';\n\n/**\n * What a relation select needs to read its target and hand back a created record: the shared\n * wiring of `RelationPicker` and `useRelationPicker` (one place decides the value field, the\n * search declaration and the select's one pending create).\n */\nexport interface RelationSelectInput {\n readonly to: string;\n /** The target field the value holds; absent: the target's id field. */\n readonly by: string | undefined;\n /** The fallback search field when the target declares no `views.list.search`. */\n readonly searchField?: string | undefined;\n /** Every filter the option reads carry (candidate filters included). */\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n readonly scope: ScopeSource;\n /** The record field the select writes (`useList`'s `scopeField`). */\n readonly scopeField?: string | undefined;\n /** The candidates gate is not ready: no create starts and no hand-back reads. */\n readonly held: boolean;\n /** Called with the value of a created record that came back through the option query. */\n readonly onValue: (value: string | number) => void;\n}\n\nexport interface RelationSelect {\n readonly targetDef: ReturnType<ReturnType<typeof useApp>['registry']['get']>;\n readonly access: ReturnType<typeof useListAccess>;\n /** The target's id field (`data.idField`, else `id`). */\n readonly idField: string;\n /** The row field an option writes and is compared on: `by`, else the id field. */\n readonly valueField: string;\n /** The search set, without the fields the user may not view; undefined: no search. */\n readonly searchDecl: SearchDecl | undefined;\n readonly create: SelectCreate;\n /** Mount it inside the select's read scope while a created record is handed back. */\n readonly reader: React.ReactNode;\n}\n\n/** The shared wiring of a relation select (see {@link RelationSelectInput}). */\nexport function useRelationSelect(input: RelationSelectInput): RelationSelect {\n const { to, by, searchField, fixedFilters, scope, scopeField, held, onValue } = input;\n const app = useApp();\n // An unresolved target (`''`: the picker hook's field that names no relation) is unregistered.\n const targetDef = to === '' ? undefined : app.registry.get(to);\n const targetIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof targetIdField === 'string' && targetIdField.length > 0 ? targetIdField : 'id';\n const valueField = by ?? idField;\n // Search resolution: the target's declared `views.list.search` wins; else `searchField`, else\n // the target's `displayField`, else `'name'` — narrowed to the fields the user may view.\n const effectiveSearchField = searchField ?? targetDef?.displayField ?? 'name';\n const access = useListAccess(to);\n const searchDecl = React.useMemo<SearchDecl | undefined>(\n () =>\n narrowSearchDecl(\n targetDef?.views?.list?.search ?? { fields: [effectiveSearchField] },\n (name) => access.fieldVisible(name),\n ),\n [targetDef, effectiveSearchField, access],\n );\n const { create, reader } = useSelectCreate({\n to,\n by,\n valueField,\n idField,\n fixedFilters,\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n held,\n searchDecl,\n searchContext: { resource: to, def: targetDef },\n onValue,\n });\n return { targetDef, access, idField, valueField, searchDecl, create, reader };\n}\n"],"mappings":";;;;;;AA6CA,SAAgB,kBAAkB,OAA4C;CAC5E,MAAM,EAAE,IAAI,IAAI,aAAa,cAAc,OAAO,YAAY,MAAM,YAAY;CAChF,MAAM,OAAA,GAAMA,cAAAA,OAAAA,CAAO;CAEnB,MAAM,YAAY,OAAO,KAAK,KAAA,IAAY,IAAI,SAAS,IAAI,EAAE;CAC7D,MAAM,iBAAiB,WAAW,KAAA,EAAqD;CACvF,MAAM,UACJ,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAAI,gBAAgB;CAClF,MAAM,aAAa,MAAM;CAGzB,MAAM,uBAAuB,eAAe,WAAW,gBAAgB;CACvE,MAAM,UAAA,GAASC,cAAAA,cAAAA,CAAc,EAAE;CAC/B,MAAM,aAAaC,MAAM,cAErBC,wBAAAA,iBACE,WAAW,OAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,oBAAoB,EAAE,IAClE,SAAS,OAAO,aAAa,IAAI,CACpC,GACF;EAAC;EAAW;EAAsB;CAAM,CAC1C;CACA,MAAM,EAAE,QAAQ,WAAWC,wBAAAA,gBAAgB;EACzC;EACA;EACA;EACA;EACA;EACA;EACA,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW;EACjD;EACA;EACA,eAAe;GAAE,UAAU;GAAI,KAAK;EAAU;EAC9C;CACF,CAAC;CACD,OAAO;EAAE;EAAW;EAAQ;EAAS;EAAY;EAAY;EAAQ;CAAO;AAC9E"}
1
+ {"version":3,"file":"relation-select-CguIo2M5.cjs","names":["useApp","useListAccess","React","narrowSearchDecl","useSelectCreate"],"sources":["../src/fields/relation-select.ts"],"sourcesContent":["'use client';\n\nimport type { FixedFilterInput, ScopeSource, SearchDecl } from '@burojs/core';\nimport { useApp, useListAccess } from '@burojs/react';\nimport * as React from 'react';\nimport { type SelectCreate, useSelectCreate } from './relation-create.js';\nimport { narrowSearchDecl } from './search-access.js';\n\n/**\n * What a relation select needs to read its target and hand back a created record: the shared\n * wiring of `RelationPicker` and `useRelationPicker` (one place decides the value field, the\n * search declaration and the select's one pending create).\n */\nexport interface RelationSelectInput {\n readonly to: string;\n /** The target field the value holds; absent: the target's id field. */\n readonly by: string | undefined;\n /** The fallback search field when the target declares no `views.list.search`. */\n readonly searchField?: string | undefined;\n /** Every filter the option reads carry (candidate filters included). */\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n readonly scope: ScopeSource;\n /** The record field the select writes (`useList`'s `scopeField`). */\n readonly scopeField?: string | undefined;\n /** The candidates gate is not ready: no create starts and no hand-back reads. */\n readonly held: boolean;\n /** Called with the value of a created record that came back through the option query. */\n readonly onValue: (value: string | number) => void;\n}\n\nexport interface RelationSelect {\n readonly targetDef: ReturnType<ReturnType<typeof useApp>['registry']['get']>;\n readonly access: ReturnType<typeof useListAccess>;\n /** The target's id field (`data.idField`, else `id`). */\n readonly idField: string;\n /** The row field an option writes and is compared on: `by`, else the id field. */\n readonly valueField: string;\n /** The search set, without the fields the user may not view; undefined: no search. */\n readonly searchDecl: SearchDecl | undefined;\n readonly create: SelectCreate;\n /** Mount it inside the select's read scope while a created record is handed back. */\n readonly reader: React.ReactNode;\n}\n\n/** The shared wiring of a relation select (see {@link RelationSelectInput}). */\nexport function useRelationSelect(input: RelationSelectInput): RelationSelect {\n const { to, by, searchField, fixedFilters, scope, scopeField, held, onValue } = input;\n const app = useApp();\n // An unresolved target (`''`: the picker hook's field that names no relation) is unregistered.\n const targetDef = to === '' ? undefined : app.registry.get(to);\n const targetIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof targetIdField === 'string' && targetIdField.length > 0 ? targetIdField : 'id';\n const valueField = by ?? idField;\n // Search resolution: the target's declared `views.list.search` wins; else `searchField`, else\n // the target's `displayField`, else `'name'` — narrowed to the fields the user may view.\n const effectiveSearchField = searchField ?? targetDef?.displayField ?? 'name';\n const access = useListAccess(to);\n const searchDecl = React.useMemo<SearchDecl | undefined>(\n () =>\n narrowSearchDecl(\n targetDef?.views?.list?.search ?? { fields: [effectiveSearchField] },\n (name) => access.fieldVisible(name),\n ),\n [targetDef, effectiveSearchField, access],\n );\n const { create, reader } = useSelectCreate({\n to,\n by,\n valueField,\n idField,\n fixedFilters,\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n held,\n searchDecl,\n searchContext: { resource: to, def: targetDef },\n onValue,\n });\n return { targetDef, access, idField, valueField, searchDecl, create, reader };\n}\n"],"mappings":";;;;;;AA6CA,SAAgB,kBAAkB,OAA4C;CAC5E,MAAM,EAAE,IAAI,IAAI,aAAa,cAAc,OAAO,YAAY,MAAM,YAAY;CAChF,MAAM,OAAA,GAAMA,cAAAA,OAAAA,CAAO;CAEnB,MAAM,YAAY,OAAO,KAAK,KAAA,IAAY,IAAI,SAAS,IAAI,EAAE;CAC7D,MAAM,iBAAiB,WAAW,KAAA,EAAqD;CACvF,MAAM,UACJ,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAAI,gBAAgB;CAClF,MAAM,aAAa,MAAM;CAGzB,MAAM,uBAAuB,eAAe,WAAW,gBAAgB;CACvE,MAAM,UAAA,GAASC,cAAAA,cAAAA,CAAc,EAAE;CAC/B,MAAM,aAAaC,MAAM,cAErBC,wBAAAA,iBACE,WAAW,OAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,oBAAoB,EAAE,IAClE,SAAS,OAAO,aAAa,IAAI,CACpC,GACF;EAAC;EAAW;EAAsB;CAAM,CAC1C;CACA,MAAM,EAAE,QAAQ,WAAWC,wBAAAA,gBAAgB;EACzC;EACA;EACA;EACA;EACA;EACA;EACA,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW;EACjD;EACA;EACA,eAAe;GAAE,UAAU;GAAI,KAAK;EAAU;EAC9C;CACF,CAAC;CACD,OAAO;EAAE;EAAW;EAAQ;EAAS;EAAY;EAAY;EAAQ;CAAO;AAC9E"}
@@ -1,4 +1,4 @@
1
- import { n as useSelectCreate, o as narrowSearchDecl } from "./relation-create-6WicROWA.js";
1
+ import { n as useSelectCreate, o as narrowSearchDecl } from "./relation-create-CeCWyxIb.js";
2
2
  import { useApp, useListAccess } from "@burojs/react";
3
3
  import * as React from "react";
4
4
  //#region src/fields/relation-select.ts
@@ -45,4 +45,4 @@ function useRelationSelect(input) {
45
45
  //#endregion
46
46
  export { useRelationSelect as t };
47
47
 
48
- //# sourceMappingURL=relation-select-CjQT9LpS.js.map
48
+ //# sourceMappingURL=relation-select-nAYxhopN.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"relation-select-CjQT9LpS.js","names":[],"sources":["../src/fields/relation-select.ts"],"sourcesContent":["'use client';\n\nimport type { FixedFilterInput, ScopeSource, SearchDecl } from '@burojs/core';\nimport { useApp, useListAccess } from '@burojs/react';\nimport * as React from 'react';\nimport { type SelectCreate, useSelectCreate } from './relation-create.js';\nimport { narrowSearchDecl } from './search-access.js';\n\n/**\n * What a relation select needs to read its target and hand back a created record: the shared\n * wiring of `RelationPicker` and `useRelationPicker` (one place decides the value field, the\n * search declaration and the select's one pending create).\n */\nexport interface RelationSelectInput {\n readonly to: string;\n /** The target field the value holds; absent: the target's id field. */\n readonly by: string | undefined;\n /** The fallback search field when the target declares no `views.list.search`. */\n readonly searchField?: string | undefined;\n /** Every filter the option reads carry (candidate filters included). */\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n readonly scope: ScopeSource;\n /** The record field the select writes (`useList`'s `scopeField`). */\n readonly scopeField?: string | undefined;\n /** The candidates gate is not ready: no create starts and no hand-back reads. */\n readonly held: boolean;\n /** Called with the value of a created record that came back through the option query. */\n readonly onValue: (value: string | number) => void;\n}\n\nexport interface RelationSelect {\n readonly targetDef: ReturnType<ReturnType<typeof useApp>['registry']['get']>;\n readonly access: ReturnType<typeof useListAccess>;\n /** The target's id field (`data.idField`, else `id`). */\n readonly idField: string;\n /** The row field an option writes and is compared on: `by`, else the id field. */\n readonly valueField: string;\n /** The search set, without the fields the user may not view; undefined: no search. */\n readonly searchDecl: SearchDecl | undefined;\n readonly create: SelectCreate;\n /** Mount it inside the select's read scope while a created record is handed back. */\n readonly reader: React.ReactNode;\n}\n\n/** The shared wiring of a relation select (see {@link RelationSelectInput}). */\nexport function useRelationSelect(input: RelationSelectInput): RelationSelect {\n const { to, by, searchField, fixedFilters, scope, scopeField, held, onValue } = input;\n const app = useApp();\n // An unresolved target (`''`: the picker hook's field that names no relation) is unregistered.\n const targetDef = to === '' ? undefined : app.registry.get(to);\n const targetIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof targetIdField === 'string' && targetIdField.length > 0 ? targetIdField : 'id';\n const valueField = by ?? idField;\n // Search resolution: the target's declared `views.list.search` wins; else `searchField`, else\n // the target's `displayField`, else `'name'` — narrowed to the fields the user may view.\n const effectiveSearchField = searchField ?? targetDef?.displayField ?? 'name';\n const access = useListAccess(to);\n const searchDecl = React.useMemo<SearchDecl | undefined>(\n () =>\n narrowSearchDecl(\n targetDef?.views?.list?.search ?? { fields: [effectiveSearchField] },\n (name) => access.fieldVisible(name),\n ),\n [targetDef, effectiveSearchField, access],\n );\n const { create, reader } = useSelectCreate({\n to,\n by,\n valueField,\n idField,\n fixedFilters,\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n held,\n searchDecl,\n searchContext: { resource: to, def: targetDef },\n onValue,\n });\n return { targetDef, access, idField, valueField, searchDecl, create, reader };\n}\n"],"mappings":";;;;AA6CA,SAAgB,kBAAkB,OAA4C;CAC5E,MAAM,EAAE,IAAI,IAAI,aAAa,cAAc,OAAO,YAAY,MAAM,YAAY;CAChF,MAAM,MAAM,OAAO;CAEnB,MAAM,YAAY,OAAO,KAAK,KAAA,IAAY,IAAI,SAAS,IAAI,EAAE;CAC7D,MAAM,iBAAiB,WAAW,KAAA,EAAqD;CACvF,MAAM,UACJ,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAAI,gBAAgB;CAClF,MAAM,aAAa,MAAM;CAGzB,MAAM,uBAAuB,eAAe,WAAW,gBAAgB;CACvE,MAAM,SAAS,cAAc,EAAE;CAC/B,MAAM,aAAa,MAAM,cAErB,iBACE,WAAW,OAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,oBAAoB,EAAE,IAClE,SAAS,OAAO,aAAa,IAAI,CACpC,GACF;EAAC;EAAW;EAAsB;CAAM,CAC1C;CACA,MAAM,EAAE,QAAQ,WAAW,gBAAgB;EACzC;EACA;EACA;EACA;EACA;EACA;EACA,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW;EACjD;EACA;EACA,eAAe;GAAE,UAAU;GAAI,KAAK;EAAU;EAC9C;CACF,CAAC;CACD,OAAO;EAAE;EAAW;EAAQ;EAAS;EAAY;EAAY;EAAQ;CAAO;AAC9E"}
1
+ {"version":3,"file":"relation-select-nAYxhopN.js","names":[],"sources":["../src/fields/relation-select.ts"],"sourcesContent":["'use client';\n\nimport type { FixedFilterInput, ScopeSource, SearchDecl } from '@burojs/core';\nimport { useApp, useListAccess } from '@burojs/react';\nimport * as React from 'react';\nimport { type SelectCreate, useSelectCreate } from './relation-create.js';\nimport { narrowSearchDecl } from './search-access.js';\n\n/**\n * What a relation select needs to read its target and hand back a created record: the shared\n * wiring of `RelationPicker` and `useRelationPicker` (one place decides the value field, the\n * search declaration and the select's one pending create).\n */\nexport interface RelationSelectInput {\n readonly to: string;\n /** The target field the value holds; absent: the target's id field. */\n readonly by: string | undefined;\n /** The fallback search field when the target declares no `views.list.search`. */\n readonly searchField?: string | undefined;\n /** Every filter the option reads carry (candidate filters included). */\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n readonly scope: ScopeSource;\n /** The record field the select writes (`useList`'s `scopeField`). */\n readonly scopeField?: string | undefined;\n /** The candidates gate is not ready: no create starts and no hand-back reads. */\n readonly held: boolean;\n /** Called with the value of a created record that came back through the option query. */\n readonly onValue: (value: string | number) => void;\n}\n\nexport interface RelationSelect {\n readonly targetDef: ReturnType<ReturnType<typeof useApp>['registry']['get']>;\n readonly access: ReturnType<typeof useListAccess>;\n /** The target's id field (`data.idField`, else `id`). */\n readonly idField: string;\n /** The row field an option writes and is compared on: `by`, else the id field. */\n readonly valueField: string;\n /** The search set, without the fields the user may not view; undefined: no search. */\n readonly searchDecl: SearchDecl | undefined;\n readonly create: SelectCreate;\n /** Mount it inside the select's read scope while a created record is handed back. */\n readonly reader: React.ReactNode;\n}\n\n/** The shared wiring of a relation select (see {@link RelationSelectInput}). */\nexport function useRelationSelect(input: RelationSelectInput): RelationSelect {\n const { to, by, searchField, fixedFilters, scope, scopeField, held, onValue } = input;\n const app = useApp();\n // An unresolved target (`''`: the picker hook's field that names no relation) is unregistered.\n const targetDef = to === '' ? undefined : app.registry.get(to);\n const targetIdField = (targetDef?.data as { readonly idField?: unknown } | undefined)?.idField;\n const idField =\n typeof targetIdField === 'string' && targetIdField.length > 0 ? targetIdField : 'id';\n const valueField = by ?? idField;\n // Search resolution: the target's declared `views.list.search` wins; else `searchField`, else\n // the target's `displayField`, else `'name'` — narrowed to the fields the user may view.\n const effectiveSearchField = searchField ?? targetDef?.displayField ?? 'name';\n const access = useListAccess(to);\n const searchDecl = React.useMemo<SearchDecl | undefined>(\n () =>\n narrowSearchDecl(\n targetDef?.views?.list?.search ?? { fields: [effectiveSearchField] },\n (name) => access.fieldVisible(name),\n ),\n [targetDef, effectiveSearchField, access],\n );\n const { create, reader } = useSelectCreate({\n to,\n by,\n valueField,\n idField,\n fixedFilters,\n scope,\n ...(scopeField === undefined ? {} : { scopeField }),\n held,\n searchDecl,\n searchContext: { resource: to, def: targetDef },\n onValue,\n });\n return { targetDef, access, idField, valueField, searchDecl, create, reader };\n}\n"],"mappings":";;;;AA6CA,SAAgB,kBAAkB,OAA4C;CAC5E,MAAM,EAAE,IAAI,IAAI,aAAa,cAAc,OAAO,YAAY,MAAM,YAAY;CAChF,MAAM,MAAM,OAAO;CAEnB,MAAM,YAAY,OAAO,KAAK,KAAA,IAAY,IAAI,SAAS,IAAI,EAAE;CAC7D,MAAM,iBAAiB,WAAW,KAAA,EAAqD;CACvF,MAAM,UACJ,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAAI,gBAAgB;CAClF,MAAM,aAAa,MAAM;CAGzB,MAAM,uBAAuB,eAAe,WAAW,gBAAgB;CACvE,MAAM,SAAS,cAAc,EAAE;CAC/B,MAAM,aAAa,MAAM,cAErB,iBACE,WAAW,OAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,oBAAoB,EAAE,IAClE,SAAS,OAAO,aAAa,IAAI,CACpC,GACF;EAAC;EAAW;EAAsB;CAAM,CAC1C;CACA,MAAM,EAAE,QAAQ,WAAW,gBAAgB;EACzC;EACA;EACA;EACA;EACA;EACA;EACA,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW;EACjD;EACA;EACA,eAAe;GAAE,UAAU;GAAI,KAAK;EAAU;EAC9C;CACF,CAAC;CACD,OAAO;EAAE;EAAW;EAAQ;EAAS;EAAY;EAAY;EAAQ;CAAO;AAC9E"}
@@ -1,4 +1,4 @@
1
- const require_relation_create = require("./relation-create-_SFuPQGM.cjs");
1
+ const require_relation_create = require("./relation-create-B8zngAcA.cjs");
2
2
  let _burojs_core = require("@burojs/core");
3
3
  let _burojs_react = require("@burojs/react");
4
4
  let react = require("react");
@@ -90,4 +90,4 @@ Object.defineProperty(exports, "useDialogFilterState", {
90
90
  }
91
91
  });
92
92
 
93
- //# sourceMappingURL=use-dialog-filter-state-lsTJxrB4.cjs.map
93
+ //# sourceMappingURL=use-dialog-filter-state-CpWblz3C.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-dialog-filter-state-lsTJxrB4.cjs","names":["routeResourceKey","useApp","useFilterFields","useState","EMPTY_FILTER_GROUP","useCallback","useListAccess","useMemo","narrowSearchDecl","composeListFilter","namesHiddenField"],"sources":["../src/fields/use-dialog-filter-state.ts"],"sourcesContent":["import {\n EMPTY_FILTER_GROUP,\n composeListFilter,\n type ErasedResourceHandle,\n type FilterExpression,\n type FilterGroup,\n type FixedFilterInput,\n type ResolvedFixedFilter,\n type SearchDecl,\n routeResourceKey,\n type ResourceNameOrRef,\n} from '@burojs/core';\nimport { useApp, useFilterFields, useListAccess, type FilterFieldSpec } from '@burojs/react';\nimport { namesHiddenField, narrowSearchDecl } from './search-access.js';\nimport { useCallback, useMemo, useState } from 'react';\n\n/** The portable erased AST — the only shape this module deals in. */\ntype ErasedExpression = FilterExpression<ErasedResourceHandle>;\n\n/** Options for {@link useDialogFilterState}. */\nexport interface UseDialogFilterStateOptions {\n /** Call-site narrowing (e.g. scope the target list to a product group). */\n readonly fixedFilters?: readonly FixedFilterInput[];\n /**\n * Fallback search field, used ONLY when the target resource declares no\n * `views.list.search`. Wrapped into a single-field `SearchFieldsDecl`\n * (`{ fields: [searchField] }`, implicit `contains` — same degenerate form\n * `searchToFilter` already gives a bare field-name string) so the override\n * goes through the exact same `composeListFilter` → `searchToFilter` path\n * as a real declaration, rather than a second hand-built predicate at the\n * call site. The target's own declaration always wins when present — this\n * is a fallback, not an override of a declared search.\n */\n readonly searchField?: string;\n /**\n * Gates the field-derivation walk (`useFilterFields`'s `categorizeField`\n * pass over every field of the resource). Defaults to `true`. A caller\n * whose UI isn't visible yet (e.g. `RelationPickerDialog` before it's\n * opened) should pass `false` to skip that work on every mount /\n * `fixedFilters`-identity change; the derivation recomputes in full the\n * next render where this flips back to `true`.\n */\n readonly enabled?: boolean;\n}\n\n/** Return shape of {@link useDialogFilterState}. */\nexport interface DialogFilterState {\n readonly group: FilterGroup;\n readonly setGroup: (next: FilterGroup) => void;\n readonly search: string;\n readonly setSearch: (next: string) => void;\n readonly page: number;\n readonly setPage: (page: number) => void;\n readonly available: readonly FilterFieldSpec[];\n readonly fixed: readonly ResolvedFixedFilter[];\n /** The single expression to hand the data provider. */\n readonly filter: ErasedExpression | undefined;\n /** Whether the target has a search for this user (no field left means no search box). */\n readonly searchEnabled: boolean;\n /** The read must not be sent: undecided access, or a filter naming a field the user may not view. */\n readonly blocked: boolean;\n readonly reset: () => void;\n}\n\n/**\n * Local (non-URL) filter state for the relation-picker's advanced-search\n * dialog.\n *\n * `useResourceList` cannot serve this: it unconditionally calls\n * `useResourceUrlState`, which throws without a `RouterAdapterProvider` and,\n * when one IS present, writes to `?<resource>.*`. A picker dialog mounted\n * inside a form for resource A, searching TARGET resource B, would then\n * write B's list state into the URL — clobbering a real B list page open on\n * another tab of the same app, and reading back whatever that page last\n * wrote on open. So this hook keeps `group`/`search`/`page` in plain\n * `useState` and reuses the SAME composition core's list controller uses\n * (`composeListFilter`) so the two never drift on what a filter means.\n *\n * No `pinned` slot: pinned controls are a list-PAGE affordance (permanent\n * screen real estate for a couple of always-visible facets); a modal has no\n * spare real estate to spend on them.\n */\nexport function useDialogFilterState<const N extends string = string>(\n resourceInput: ResourceNameOrRef<N>,\n options: UseDialogFilterStateOptions,\n): DialogFilterState {\n const resource = routeResourceKey(resourceInput);\n const app = useApp();\n const def = app.registry.get(resource);\n const { available, fixed } = useFilterFields(resource, {\n fixedFilters: options.fixedFilters,\n enabled: options.enabled,\n });\n\n const [group, setGroupState] = useState<FilterGroup>(EMPTY_FILTER_GROUP);\n const [search, setSearchState] = useState('');\n const [page, setPage] = useState(1);\n\n // Any change to WHAT is filtered invalidates the current page — the same\n // rule `useResourceList`'s URL-state controller applies to its own\n // group/search setters.\n const setGroup = useCallback((next: FilterGroup) => {\n setGroupState(next);\n setPage(1);\n }, []);\n\n const setSearch = useCallback((next: string) => {\n setSearchState(next);\n setPage(1);\n }, []);\n\n const reset = useCallback(() => {\n setGroupState(EMPTY_FILTER_GROUP);\n setSearchState('');\n setPage(1);\n }, []);\n\n // The target's own declaration always wins; `options.searchField` only\n // fills in when the target declares no search at all. Memoized so an\n // omitted declaration doesn't hand `composeListFilter`'s `filter` memo a\n // fresh `{ fields: [...] }` literal (and therefore a spurious recompute)\n // on every render — see `UseDialogFilterStateOptions.searchField`'s doc.\n const { searchField } = options;\n const access = useListAccess(resource);\n const accessReady = access.status === 'ready';\n const searchDecl = useMemo<SearchDecl | undefined>(() => {\n const declared = def?.views?.list?.search;\n const base =\n declared !== undefined\n ? declared\n : searchField === undefined\n ? undefined\n : { fields: [searchField] };\n return narrowSearchDecl(base, (field) => access.fieldVisible(field));\n }, [def, searchField, access]);\n const searchContext = useMemo(() => ({ resource, def }), [resource, def]);\n\n const filter = useMemo<ErasedExpression | undefined>(\n () => composeListFilter({ fixed, group, search, searchDecl, searchContext }),\n [fixed, group, search, searchDecl, searchContext],\n );\n\n // A read whose composed filter names a field the user may not view is forbidden (an author\n // predicate, or a function search's output); an undecided access sends nothing yet.\n const blocked = !accessReady || namesHiddenField(filter, (field) => access.fieldVisible(field));\n\n return useMemo<DialogFilterState>(\n () => ({\n searchEnabled: searchDecl !== undefined,\n blocked,\n group,\n setGroup,\n search,\n setSearch,\n page,\n setPage,\n available,\n fixed,\n filter,\n reset,\n }),\n [\n group,\n setGroup,\n search,\n setSearch,\n page,\n available,\n fixed,\n filter,\n reset,\n searchDecl,\n blocked,\n ],\n );\n}\n"],"mappings":";;;;;AAkFA,SAAgB,qBACd,eACA,SACmB;CACnB,MAAM,YAAA,GAAWA,aAAAA,iBAAAA,CAAiB,aAAa;CAE/C,MAAM,OAAA,GADMC,cAAAA,OAAAA,CACE,CAAC,CAAC,SAAS,IAAI,QAAQ;CACrC,MAAM,EAAE,WAAW,WAAA,GAAUC,cAAAA,gBAAAA,CAAgB,UAAU;EACrD,cAAc,QAAQ;EACtB,SAAS,QAAQ;CACnB,CAAC;CAED,MAAM,CAAC,OAAO,kBAAA,GAAiBC,MAAAA,SAAAA,CAAsBC,aAAAA,kBAAkB;CACvE,MAAM,CAAC,QAAQ,mBAAA,GAAkBD,MAAAA,SAAAA,CAAS,EAAE;CAC5C,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,CAAC;CAKlC,MAAM,YAAA,GAAWE,MAAAA,YAAAA,EAAa,SAAsB;EAClD,cAAc,IAAI;EAClB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAEL,MAAM,aAAA,GAAYA,MAAAA,YAAAA,EAAa,SAAiB;EAC9C,eAAe,IAAI;EACnB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAEL,MAAM,SAAA,GAAQA,MAAAA,YAAAA,OAAkB;EAC9B,cAAcD,aAAAA,kBAAkB;EAChC,eAAe,EAAE;EACjB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAOL,MAAM,EAAE,gBAAgB;CACxB,MAAM,UAAA,GAASE,cAAAA,cAAAA,CAAc,QAAQ;CACrC,MAAM,cAAc,OAAO,WAAW;CACtC,MAAM,cAAA,GAAaC,MAAAA,QAAAA,OAAsC;EACvD,MAAM,WAAW,KAAK,OAAO,MAAM;EAOnC,OAAOC,wBAAAA,iBALL,aAAa,KAAA,IACT,WACA,gBAAgB,KAAA,IACd,KAAA,IACA,EAAE,QAAQ,CAAC,WAAW,EAAE,IACD,UAAU,OAAO,aAAa,KAAK,CAAC;CACrE,GAAG;EAAC;EAAK;EAAa;CAAM,CAAC;CAC7B,MAAM,iBAAA,GAAgBD,MAAAA,QAAAA,QAAe;EAAE;EAAU;CAAI,IAAI,CAAC,UAAU,GAAG,CAAC;CAExE,MAAM,UAAA,GAASA,MAAAA,QAAAA,QAAAA,GACPE,aAAAA,kBAAAA,CAAkB;EAAE;EAAO;EAAO;EAAQ;EAAY;CAAc,CAAC,GAC3E;EAAC;EAAO;EAAO;EAAQ;EAAY;CAAa,CAClD;CAIA,MAAM,UAAU,CAAC,eAAeC,wBAAAA,iBAAiB,SAAS,UAAU,OAAO,aAAa,KAAK,CAAC;CAE9F,QAAA,GAAOH,MAAAA,QAAAA,QACE;EACL,eAAe,eAAe,KAAA;EAC9B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;AACF"}
1
+ {"version":3,"file":"use-dialog-filter-state-CpWblz3C.cjs","names":["routeResourceKey","useApp","useFilterFields","useState","EMPTY_FILTER_GROUP","useCallback","useListAccess","useMemo","narrowSearchDecl","composeListFilter","namesHiddenField"],"sources":["../src/fields/use-dialog-filter-state.ts"],"sourcesContent":["import {\n EMPTY_FILTER_GROUP,\n composeListFilter,\n type ErasedResourceHandle,\n type FilterExpression,\n type FilterGroup,\n type FixedFilterInput,\n type ResolvedFixedFilter,\n type SearchDecl,\n routeResourceKey,\n type ResourceNameOrRef,\n} from '@burojs/core';\nimport { useApp, useFilterFields, useListAccess, type FilterFieldSpec } from '@burojs/react';\nimport { namesHiddenField, narrowSearchDecl } from './search-access.js';\nimport { useCallback, useMemo, useState } from 'react';\n\n/** The portable erased AST — the only shape this module deals in. */\ntype ErasedExpression = FilterExpression<ErasedResourceHandle>;\n\n/** Options for {@link useDialogFilterState}. */\nexport interface UseDialogFilterStateOptions {\n /** Call-site narrowing (e.g. scope the target list to a product group). */\n readonly fixedFilters?: readonly FixedFilterInput[];\n /**\n * Fallback search field, used ONLY when the target resource declares no\n * `views.list.search`. Wrapped into a single-field `SearchFieldsDecl`\n * (`{ fields: [searchField] }`, implicit `contains` — same degenerate form\n * `searchToFilter` already gives a bare field-name string) so the override\n * goes through the exact same `composeListFilter` → `searchToFilter` path\n * as a real declaration, rather than a second hand-built predicate at the\n * call site. The target's own declaration always wins when present — this\n * is a fallback, not an override of a declared search.\n */\n readonly searchField?: string;\n /**\n * Gates the field-derivation walk (`useFilterFields`'s `categorizeField`\n * pass over every field of the resource). Defaults to `true`. A caller\n * whose UI isn't visible yet (e.g. `RelationPickerDialog` before it's\n * opened) should pass `false` to skip that work on every mount /\n * `fixedFilters`-identity change; the derivation recomputes in full the\n * next render where this flips back to `true`.\n */\n readonly enabled?: boolean;\n}\n\n/** Return shape of {@link useDialogFilterState}. */\nexport interface DialogFilterState {\n readonly group: FilterGroup;\n readonly setGroup: (next: FilterGroup) => void;\n readonly search: string;\n readonly setSearch: (next: string) => void;\n readonly page: number;\n readonly setPage: (page: number) => void;\n readonly available: readonly FilterFieldSpec[];\n readonly fixed: readonly ResolvedFixedFilter[];\n /** The single expression to hand the data provider. */\n readonly filter: ErasedExpression | undefined;\n /** Whether the target has a search for this user (no field left means no search box). */\n readonly searchEnabled: boolean;\n /** The read must not be sent: undecided access, or a filter naming a field the user may not view. */\n readonly blocked: boolean;\n readonly reset: () => void;\n}\n\n/**\n * Local (non-URL) filter state for the relation-picker's advanced-search\n * dialog.\n *\n * `useResourceList` cannot serve this: it unconditionally calls\n * `useResourceUrlState`, which throws without a `RouterAdapterProvider` and,\n * when one IS present, writes to `?<resource>.*`. A picker dialog mounted\n * inside a form for resource A, searching TARGET resource B, would then\n * write B's list state into the URL — clobbering a real B list page open on\n * another tab of the same app, and reading back whatever that page last\n * wrote on open. So this hook keeps `group`/`search`/`page` in plain\n * `useState` and reuses the SAME composition core's list controller uses\n * (`composeListFilter`) so the two never drift on what a filter means.\n *\n * No `pinned` slot: pinned controls are a list-PAGE affordance (permanent\n * screen real estate for a couple of always-visible facets); a modal has no\n * spare real estate to spend on them.\n */\nexport function useDialogFilterState<const N extends string = string>(\n resourceInput: ResourceNameOrRef<N>,\n options: UseDialogFilterStateOptions,\n): DialogFilterState {\n const resource = routeResourceKey(resourceInput);\n const app = useApp();\n const def = app.registry.get(resource);\n const { available, fixed } = useFilterFields(resource, {\n fixedFilters: options.fixedFilters,\n enabled: options.enabled,\n });\n\n const [group, setGroupState] = useState<FilterGroup>(EMPTY_FILTER_GROUP);\n const [search, setSearchState] = useState('');\n const [page, setPage] = useState(1);\n\n // Any change to WHAT is filtered invalidates the current page — the same\n // rule `useResourceList`'s URL-state controller applies to its own\n // group/search setters.\n const setGroup = useCallback((next: FilterGroup) => {\n setGroupState(next);\n setPage(1);\n }, []);\n\n const setSearch = useCallback((next: string) => {\n setSearchState(next);\n setPage(1);\n }, []);\n\n const reset = useCallback(() => {\n setGroupState(EMPTY_FILTER_GROUP);\n setSearchState('');\n setPage(1);\n }, []);\n\n // The target's own declaration always wins; `options.searchField` only\n // fills in when the target declares no search at all. Memoized so an\n // omitted declaration doesn't hand `composeListFilter`'s `filter` memo a\n // fresh `{ fields: [...] }` literal (and therefore a spurious recompute)\n // on every render — see `UseDialogFilterStateOptions.searchField`'s doc.\n const { searchField } = options;\n const access = useListAccess(resource);\n const accessReady = access.status === 'ready';\n const searchDecl = useMemo<SearchDecl | undefined>(() => {\n const declared = def?.views?.list?.search;\n const base =\n declared !== undefined\n ? declared\n : searchField === undefined\n ? undefined\n : { fields: [searchField] };\n return narrowSearchDecl(base, (field) => access.fieldVisible(field));\n }, [def, searchField, access]);\n const searchContext = useMemo(() => ({ resource, def }), [resource, def]);\n\n const filter = useMemo<ErasedExpression | undefined>(\n () => composeListFilter({ fixed, group, search, searchDecl, searchContext }),\n [fixed, group, search, searchDecl, searchContext],\n );\n\n // A read whose composed filter names a field the user may not view is forbidden (an author\n // predicate, or a function search's output); an undecided access sends nothing yet.\n const blocked = !accessReady || namesHiddenField(filter, (field) => access.fieldVisible(field));\n\n return useMemo<DialogFilterState>(\n () => ({\n searchEnabled: searchDecl !== undefined,\n blocked,\n group,\n setGroup,\n search,\n setSearch,\n page,\n setPage,\n available,\n fixed,\n filter,\n reset,\n }),\n [\n group,\n setGroup,\n search,\n setSearch,\n page,\n available,\n fixed,\n filter,\n reset,\n searchDecl,\n blocked,\n ],\n );\n}\n"],"mappings":";;;;;AAkFA,SAAgB,qBACd,eACA,SACmB;CACnB,MAAM,YAAA,GAAWA,aAAAA,iBAAAA,CAAiB,aAAa;CAE/C,MAAM,OAAA,GADMC,cAAAA,OAAAA,CACE,CAAC,CAAC,SAAS,IAAI,QAAQ;CACrC,MAAM,EAAE,WAAW,WAAA,GAAUC,cAAAA,gBAAAA,CAAgB,UAAU;EACrD,cAAc,QAAQ;EACtB,SAAS,QAAQ;CACnB,CAAC;CAED,MAAM,CAAC,OAAO,kBAAA,GAAiBC,MAAAA,SAAAA,CAAsBC,aAAAA,kBAAkB;CACvE,MAAM,CAAC,QAAQ,mBAAA,GAAkBD,MAAAA,SAAAA,CAAS,EAAE;CAC5C,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,CAAC;CAKlC,MAAM,YAAA,GAAWE,MAAAA,YAAAA,EAAa,SAAsB;EAClD,cAAc,IAAI;EAClB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAEL,MAAM,aAAA,GAAYA,MAAAA,YAAAA,EAAa,SAAiB;EAC9C,eAAe,IAAI;EACnB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAEL,MAAM,SAAA,GAAQA,MAAAA,YAAAA,OAAkB;EAC9B,cAAcD,aAAAA,kBAAkB;EAChC,eAAe,EAAE;EACjB,QAAQ,CAAC;CACX,GAAG,CAAC,CAAC;CAOL,MAAM,EAAE,gBAAgB;CACxB,MAAM,UAAA,GAASE,cAAAA,cAAAA,CAAc,QAAQ;CACrC,MAAM,cAAc,OAAO,WAAW;CACtC,MAAM,cAAA,GAAaC,MAAAA,QAAAA,OAAsC;EACvD,MAAM,WAAW,KAAK,OAAO,MAAM;EAOnC,OAAOC,wBAAAA,iBALL,aAAa,KAAA,IACT,WACA,gBAAgB,KAAA,IACd,KAAA,IACA,EAAE,QAAQ,CAAC,WAAW,EAAE,IACD,UAAU,OAAO,aAAa,KAAK,CAAC;CACrE,GAAG;EAAC;EAAK;EAAa;CAAM,CAAC;CAC7B,MAAM,iBAAA,GAAgBD,MAAAA,QAAAA,QAAe;EAAE;EAAU;CAAI,IAAI,CAAC,UAAU,GAAG,CAAC;CAExE,MAAM,UAAA,GAASA,MAAAA,QAAAA,QAAAA,GACPE,aAAAA,kBAAAA,CAAkB;EAAE;EAAO;EAAO;EAAQ;EAAY;CAAc,CAAC,GAC3E;EAAC;EAAO;EAAO;EAAQ;EAAY;CAAa,CAClD;CAIA,MAAM,UAAU,CAAC,eAAeC,wBAAAA,iBAAiB,SAAS,UAAU,OAAO,aAAa,KAAK,CAAC;CAE9F,QAAA,GAAOH,MAAAA,QAAAA,QACE;EACL,eAAe,eAAe,KAAA;EAC9B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { a as namesHiddenField, o as narrowSearchDecl } from "./relation-create-6WicROWA.js";
1
+ import { a as namesHiddenField, o as narrowSearchDecl } from "./relation-create-CeCWyxIb.js";
2
2
  import { EMPTY_FILTER_GROUP, composeListFilter, routeResourceKey } from "@burojs/core";
3
3
  import { useApp, useFilterFields, useListAccess } from "@burojs/react";
4
4
  import { useCallback, useMemo, useState } from "react";
@@ -85,4 +85,4 @@ function useDialogFilterState(resourceInput, options) {
85
85
  //#endregion
86
86
  export { useDialogFilterState as t };
87
87
 
88
- //# sourceMappingURL=use-dialog-filter-state-CJoeY-1j.js.map
88
+ //# sourceMappingURL=use-dialog-filter-state-ZYxOp5WO.js.map