@pipe0/react 0.2.5 → 0.2.7

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 (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/defaults/adapters/context-select-input.mjs +20 -1
  3. package/dist/components/defaults/adapters/context-select-input.mjs.map +1 -1
  4. package/dist/components/defaults/adapters/field-select-input.mjs +33 -0
  5. package/dist/components/defaults/adapters/field-select-input.mjs.map +1 -0
  6. package/dist/components/defaults/adapters/index.d.mts.map +1 -1
  7. package/dist/components/defaults/adapters/index.mjs +4 -0
  8. package/dist/components/defaults/adapters/index.mjs.map +1 -1
  9. package/dist/components/defaults/adapters/pipes-run-if-input.mjs +53 -5
  10. package/dist/components/defaults/adapters/pipes-run-if-input.mjs.map +1 -1
  11. package/dist/components/defaults/adapters/typed-fields-select-input.mjs +48 -0
  12. package/dist/components/defaults/adapters/typed-fields-select-input.mjs.map +1 -0
  13. package/dist/components/internal/combobox/suggest-combobox.mjs +35 -6
  14. package/dist/components/internal/combobox/suggest-combobox.mjs.map +1 -1
  15. package/dist/hooks/use-effect-catalog-table.d.mts.map +1 -1
  16. package/dist/hooks/use-effect-catalog-table.mjs +18 -11
  17. package/dist/hooks/use-effect-catalog-table.mjs.map +1 -1
  18. package/dist/hooks/use-form-core.d.mts.map +1 -1
  19. package/dist/hooks/use-form-core.mjs +26 -8
  20. package/dist/hooks/use-form-core.mjs.map +1 -1
  21. package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
  22. package/dist/hooks/use-pipe-catalog-table.d.mts.map +1 -1
  23. package/dist/hooks/use-pipe-catalog-table.mjs +20 -16
  24. package/dist/hooks/use-pipe-catalog-table.mjs.map +1 -1
  25. package/dist/hooks/use-search-catalog-table.d.mts.map +1 -1
  26. package/dist/hooks/use-search-catalog-table.mjs +20 -13
  27. package/dist/hooks/use-search-catalog-table.mjs.map +1 -1
  28. package/dist/hooks/use-searches-catalog-table.d.mts.map +1 -1
  29. package/dist/hooks/use-searches-catalog-table.mjs +20 -13
  30. package/dist/hooks/use-searches-catalog-table.mjs.map +1 -1
  31. package/dist/types/field-props.d.mts +35 -2
  32. package/dist/types/field-props.d.mts.map +1 -1
  33. package/dist/utils/build-section-handlers.mjs +21 -2
  34. package/dist/utils/build-section-handlers.mjs.map +1 -1
  35. package/dist/utils/catalog-helpers.mjs +10 -1
  36. package/dist/utils/catalog-helpers.mjs.map +1 -1
  37. package/package.json +2 -2
@@ -71,7 +71,7 @@ function SortableChip({ id, onRemove, children }) {
71
71
  * - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.
72
72
  * - Debounced async loader with an in-memory cache keyed by query string.
73
73
  */
74
- function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate = false, disableSearch = false, maxItems, excludeValues = [], ariaInvalid, disabled, debounceMs = 300, labelFor, iconFor, emptyLabel, placeholder, reorderable = false }) {
74
+ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate = false, disableSearch = false, maxItems, excludeValues = [], ariaInvalid, disabled, debounceMs = 300, labelFor, iconFor, emptyLabel, placeholder, reorderable = false, single = false }) {
75
75
  const [input, setInput] = useState("");
76
76
  const [debouncedQuery, setDebouncedQuery] = useState("");
77
77
  const [open, setOpen] = useState(false);
@@ -156,6 +156,14 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
156
156
  value: valueObjects,
157
157
  onValueChange: (next) => {
158
158
  if (!Array.isArray(next)) return;
159
+ if (single) {
160
+ const last = [...next].reverse().map((item) => typeof item === "string" ? item : item.value).find((v) => v && !excludeSet.has(v));
161
+ onChange(last ? [last] : []);
162
+ setInput("");
163
+ if (isAsync) debouncedSetQuery("");
164
+ setOpen(false);
165
+ return;
166
+ }
159
167
  const seen = /* @__PURE__ */ new Set();
160
168
  const deduped = [];
161
169
  for (const item of next) {
@@ -183,7 +191,20 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
183
191
  "aria-invalid": ariaInvalid || void 0,
184
192
  ref: anchor,
185
193
  children: [
186
- reorderable ? /* @__PURE__ */ jsx(DndContext, {
194
+ single ? valueObjects[0] && input === "" && /* @__PURE__ */ jsxs("span", {
195
+ className: "pz:flex pz:min-w-0 pz:items-center pz:gap-1.5 pz:text-sm",
196
+ children: [
197
+ iconFor?.(valueObjects[0].value),
198
+ valueObjects[0].widgets && /* @__PURE__ */ jsx(WidgetStrip, {
199
+ widgets: valueObjects[0].widgets,
200
+ size: 14
201
+ }),
202
+ /* @__PURE__ */ jsx("span", {
203
+ className: "pz:truncate",
204
+ children: valueObjects[0].label
205
+ })
206
+ ]
207
+ }) : reorderable ? /* @__PURE__ */ jsx(DndContext, {
187
208
  sensors: sortSensors,
188
209
  collisionDetection: closestCenter,
189
210
  onDragEnd: handleDragEnd,
@@ -220,10 +241,18 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
220
241
  if (trimmedInput.length === 0) return;
221
242
  e.preventDefault();
222
243
  e.stopPropagation();
223
- if (atMax) return;
244
+ if (!single && atMax) return;
224
245
  const toAdd = filteredSuggestions.find((o) => o.value.toLowerCase() === trimmedInput.toLowerCase())?.value ?? (allowCreate ? trimmedInput : null);
225
246
  if (!toAdd) return;
226
- if (selectedSet.has(toAdd) || excludeSet.has(toAdd)) return;
247
+ if (excludeSet.has(toAdd)) return;
248
+ if (single) {
249
+ onChange([toAdd]);
250
+ setInput("");
251
+ if (isAsync) debouncedSetQuery("");
252
+ setOpen(false);
253
+ return;
254
+ }
255
+ if (selectedSet.has(toAdd)) return;
227
256
  onChange([...value, toAdd]);
228
257
  setInput("");
229
258
  if (isAsync) debouncedSetQuery("");
@@ -247,7 +276,7 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
247
276
  /* @__PURE__ */ jsxs(ComboboxList, {
248
277
  className: cn(loading && items.length > 0 && "pz:opacity-50 pz:transition-opacity pz:pointer-events-none"),
249
278
  children: [
250
- atMax && /* @__PURE__ */ jsxs("div", {
279
+ !single && atMax && /* @__PURE__ */ jsxs("div", {
251
280
  className: "pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground",
252
281
  children: [
253
282
  "Maximum ",
@@ -263,7 +292,7 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
263
292
  return /* @__PURE__ */ jsxs(ComboboxItem, {
264
293
  value: item,
265
294
  className: item.__create ? "pz:italic" : void 0,
266
- disabled: !selectedSet.has(item.value) && atMax && !item.__create,
295
+ disabled: !single && !selectedSet.has(item.value) && atMax && !item.__create,
267
296
  children: [
268
297
  !item.__create && iconFor?.(item.value),
269
298
  leadingWidgets && /* @__PURE__ */ jsx(WidgetStrip, {
@@ -1 +1 @@
1
- {"version":3,"file":"suggest-combobox.mjs","names":[],"sources":["../../../../src/components/internal/combobox/suggest-combobox.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconGripVertical } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\n/** Internal item type — the base-ui Combobox handles `{value, label}` natively. */\ntype ComboboxOption = PayloadOption & { __create?: boolean };\n\n/** Split widgets so `field_type` can render on the trailing edge of dropdown\n * rows while every other widget stays in the leading strip. */\nfunction splitWidgets(widgets: WidgetsByKind): {\n leading: WidgetsByKind | undefined;\n} {\n const { field_type: _ignored, ...rest } = widgets;\n const hasLeading = Object.values(rest).some((v) => v != null);\n return { leading: hasLeading ? rest : undefined };\n}\n\n/** Reorderable chip matching `ComboboxChip` visually, with a grip handle. */\nfunction SortableChip({\n id,\n onRemove,\n children,\n}: {\n id: string;\n onRemove: () => void;\n children: ReactNode;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: React.CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\",\n )}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n\nexport interface SuggestComboboxProps {\n value: string[];\n onChange: (value: string[]) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n excludeValues?: string[];\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n labelFor?: (value: string) => string;\n /** Optional leading icon (e.g. provider logo) rendered inside chips and list items. */\n iconFor?: (value: string) => ReactNode;\n emptyLabel?: string;\n placeholder?: string;\n /** When true, chips become drag-sortable and reorder the `value` array. */\n reorderable?: boolean;\n}\n\n/**\n * Unified combobox built on `@base-ui/react`'s Combobox primitive.\n *\n * - Chips + inline search input via `ComboboxChips` / `ComboboxChipsInput`.\n * - Static (`options`) or async (`loadOptions`) item sources. External\n * filtering in both cases so match-sorter ranking / server relevance is\n * honored and base-ui's built-in filter doesn't double-filter.\n * - Free-text creation (`allowCreate`) renders an inline \"Add \\\"X\\\"\" item.\n * - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.\n * - Debounced async loader with an in-memory cache keyed by query string.\n */\nexport function SuggestCombobox({\n value,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n excludeValues = [],\n ariaInvalid,\n disabled,\n debounceMs = 300,\n labelFor,\n iconFor,\n emptyLabel,\n placeholder,\n reorderable = false,\n}: SuggestComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n // Per-instance cache key prefix so two comboboxes with the same query but\n // different `loadOptions` functions don't share results.\n const instanceId = useId();\n\n // SWR handles dedupe + caching by-query + race-safety. We defer the\n // empty-query fetch until the user opens the dropdown — some backends\n // (e.g. Crustdata persondb) don't accept empty queries, and on-mount\n // fetches are wasted work if the field is never opened. `keepPreviousData`\n // preserves the prior result set during a refetch so the UI dims rather\n // than empties.\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n // `swrKey` was `null` when `loadOptions` is undefined, so the fetcher\n // never runs without one — the runtime check is just to satisfy TS.\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const excludeSet = useMemo(() => new Set(excludeValues), [excludeValues]);\n const selectedSet = useMemo(() => new Set(value), [value]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked = isAsync ? source : filterOptions(source, input);\n return ranked.filter((o) => !excludeSet.has(o.value));\n }, [isAsync, asyncResults, options, input, excludeSet]);\n\n const trimmedInput = input.trim();\n const showCreateRow =\n allowCreate &&\n trimmedInput.length > 0 &&\n !selectedSet.has(trimmedInput) &&\n !excludeSet.has(trimmedInput) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n if (labelFor) return labelFor(v);\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n // Persist widgets per value across loadOptions cycles. Async results are\n // replaced with each query, so without this cache an icon shown when\n // picking a value would disappear on the next refetch. Updates run in an\n // effect (not during render) so the cache stays consistent under strict\n // mode and concurrent rendering.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n useEffect(() => {\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n }, [options, asyncResults]);\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets ??\n widgetCacheRef.current.get(v);\n\n const valueObjects = useMemo<ComboboxOption[]>(\n () =>\n value.map((v) => ({\n value: v,\n label: resolveLabel(v),\n widgets: resolveWidgets(v),\n })),\n // resolveLabel/resolveWidgets close over options/asyncResults/labelFor\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [value, options, asyncResults, labelFor],\n );\n\n const items = useMemo<ComboboxOption[]>(() => {\n const list: ComboboxOption[] = [];\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, trimmedInput, filteredSuggestions]);\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = value.indexOf(String(active.id));\n const to = value.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const next = [...value];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n onChange(next);\n };\n\n return (\n <Combobox<ComboboxOption, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n value={valueObjects}\n onValueChange={(next) => {\n if (!Array.isArray(next)) return;\n // Unique by value, rewrite __create items to clean objects.\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const item of next) {\n const v = typeof item === \"string\" ? item : (item as ComboboxOption).value;\n if (!v || seen.has(v)) continue;\n if (excludeSet.has(v)) continue;\n if (maxItems !== undefined && deduped.length >= maxItems) break;\n seen.add(v);\n deduped.push(v);\n }\n onChange(deduped);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n isItemEqualToValue={(a, b) => (a as ComboboxOption).value === (b as ComboboxOption).value}\n itemToStringLabel={(item) => (item as ComboboxOption).label}\n itemToStringValue={(item) => (item as ComboboxOption).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {reorderable ? (\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={value} strategy={horizontalListSortingStrategy}>\n {valueObjects.map((v) => (\n <SortableChip\n key={v.value}\n id={v.value}\n onRemove={() => onChange(value.filter((x) => x !== v.value))}\n >\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </SortableChip>\n ))}\n </SortableContext>\n </DndContext>\n ) : (\n valueObjects.map((v) => (\n <ComboboxChip key={v.value}>\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </ComboboxChip>\n ))\n )}\n <ComboboxChipsInput\n placeholder={value.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n // Prefer an exact match against the visible suggestions, otherwise\n // fall back to creating a new value when allowed.\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? trimmedInput : null);\n if (!toAdd) return;\n if (selectedSet.has(toAdd) || excludeSet.has(toAdd)) return;\n onChange([...value, toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n // Field-type widget renders on the trailing edge of the row so\n // the field name reads first; everything else stays in the\n // leading widget strip.\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const leadingWidgets =\n !item.__create && item.widgets ? splitWidgets(item.widgets).leading : undefined;\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={item}\n className={item.__create ? \"pz:italic\" : undefined}\n disabled={!selectedSet.has(item.value) && atMax && !item.__create}\n >\n {!item.__create && iconFor?.(item.value)}\n {leadingWidgets && <WidgetStrip widgets={leadingWidgets} size={14} />}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"suggest-combobox.mjs","names":[],"sources":["../../../../src/components/internal/combobox/suggest-combobox.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconGripVertical } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\n/** Internal item type — the base-ui Combobox handles `{value, label}` natively. */\ntype ComboboxOption = PayloadOption & { __create?: boolean };\n\n/** Split widgets so `field_type` can render on the trailing edge of dropdown\n * rows while every other widget stays in the leading strip. */\nfunction splitWidgets(widgets: WidgetsByKind): {\n leading: WidgetsByKind | undefined;\n} {\n const { field_type: _ignored, ...rest } = widgets;\n const hasLeading = Object.values(rest).some((v) => v != null);\n return { leading: hasLeading ? rest : undefined };\n}\n\n/** Reorderable chip matching `ComboboxChip` visually, with a grip handle. */\nfunction SortableChip({\n id,\n onRemove,\n children,\n}: {\n id: string;\n onRemove: () => void;\n children: ReactNode;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: React.CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\",\n )}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n\nexport interface SuggestComboboxProps {\n value: string[];\n onChange: (value: string[]) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n excludeValues?: string[];\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n labelFor?: (value: string) => string;\n /** Optional leading icon (e.g. provider logo) rendered inside chips and list items. */\n iconFor?: (value: string) => ReactNode;\n emptyLabel?: string;\n placeholder?: string;\n /** When true, chips become drag-sortable and reorder the `value` array. */\n reorderable?: boolean;\n /**\n * Single-select mode: the selection renders as a plain value (not a\n * removable chip), picking an option REPLACES it, and the popover closes on\n * pick — a searchable analog of a native select. Clicking the selected\n * option again clears it. `value` stays an array (length ≤ 1) so callers\n * keep one contract across both modes; `maxItems`/`reorderable` are ignored.\n */\n single?: boolean;\n}\n\n/**\n * Unified combobox built on `@base-ui/react`'s Combobox primitive.\n *\n * - Chips + inline search input via `ComboboxChips` / `ComboboxChipsInput`.\n * - Static (`options`) or async (`loadOptions`) item sources. External\n * filtering in both cases so match-sorter ranking / server relevance is\n * honored and base-ui's built-in filter doesn't double-filter.\n * - Free-text creation (`allowCreate`) renders an inline \"Add \\\"X\\\"\" item.\n * - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.\n * - Debounced async loader with an in-memory cache keyed by query string.\n */\nexport function SuggestCombobox({\n value,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n excludeValues = [],\n ariaInvalid,\n disabled,\n debounceMs = 300,\n labelFor,\n iconFor,\n emptyLabel,\n placeholder,\n reorderable = false,\n single = false,\n}: SuggestComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n // Per-instance cache key prefix so two comboboxes with the same query but\n // different `loadOptions` functions don't share results.\n const instanceId = useId();\n\n // SWR handles dedupe + caching by-query + race-safety. We defer the\n // empty-query fetch until the user opens the dropdown — some backends\n // (e.g. Crustdata persondb) don't accept empty queries, and on-mount\n // fetches are wasted work if the field is never opened. `keepPreviousData`\n // preserves the prior result set during a refetch so the UI dims rather\n // than empties.\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n // `swrKey` was `null` when `loadOptions` is undefined, so the fetcher\n // never runs without one — the runtime check is just to satisfy TS.\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const excludeSet = useMemo(() => new Set(excludeValues), [excludeValues]);\n const selectedSet = useMemo(() => new Set(value), [value]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked = isAsync ? source : filterOptions(source, input);\n return ranked.filter((o) => !excludeSet.has(o.value));\n }, [isAsync, asyncResults, options, input, excludeSet]);\n\n const trimmedInput = input.trim();\n const showCreateRow =\n allowCreate &&\n trimmedInput.length > 0 &&\n !selectedSet.has(trimmedInput) &&\n !excludeSet.has(trimmedInput) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n if (labelFor) return labelFor(v);\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n // Persist widgets per value across loadOptions cycles. Async results are\n // replaced with each query, so without this cache an icon shown when\n // picking a value would disappear on the next refetch. Updates run in an\n // effect (not during render) so the cache stays consistent under strict\n // mode and concurrent rendering.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n useEffect(() => {\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n }, [options, asyncResults]);\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets ??\n widgetCacheRef.current.get(v);\n\n const valueObjects = useMemo<ComboboxOption[]>(\n () =>\n value.map((v) => ({\n value: v,\n label: resolveLabel(v),\n widgets: resolveWidgets(v),\n })),\n // resolveLabel/resolveWidgets close over options/asyncResults/labelFor\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [value, options, asyncResults, labelFor],\n );\n\n const items = useMemo<ComboboxOption[]>(() => {\n const list: ComboboxOption[] = [];\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, trimmedInput, filteredSuggestions]);\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = value.indexOf(String(active.id));\n const to = value.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const next = [...value];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n onChange(next);\n };\n\n return (\n <Combobox<ComboboxOption, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n value={valueObjects}\n onValueChange={(next) => {\n if (!Array.isArray(next)) return;\n if (single) {\n // Replace-on-pick: the base-ui multiple model APPENDS the picked\n // item, so the last valid entry is the user's choice; toggling the\n // selected item off yields an empty array (= clear).\n const last = [...next]\n .reverse()\n .map((item) => (typeof item === \"string\" ? item : (item as ComboboxOption).value))\n .find((v) => v && !excludeSet.has(v));\n onChange(last ? [last] : []);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n setOpen(false);\n return;\n }\n // Unique by value, rewrite __create items to clean objects.\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const item of next) {\n const v = typeof item === \"string\" ? item : (item as ComboboxOption).value;\n if (!v || seen.has(v)) continue;\n if (excludeSet.has(v)) continue;\n if (maxItems !== undefined && deduped.length >= maxItems) break;\n seen.add(v);\n deduped.push(v);\n }\n onChange(deduped);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n isItemEqualToValue={(a, b) => (a as ComboboxOption).value === (b as ComboboxOption).value}\n itemToStringLabel={(item) => (item as ComboboxOption).label}\n itemToStringValue={(item) => (item as ComboboxOption).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {single ? (\n // Select-like value display: a plain label, hidden while the user\n // types so the input reads like a search box over the selection.\n valueObjects[0] &&\n input === \"\" && (\n <span className=\"pz:flex pz:min-w-0 pz:items-center pz:gap-1.5 pz:text-sm\">\n {iconFor?.(valueObjects[0].value)}\n {valueObjects[0].widgets && (\n <WidgetStrip widgets={valueObjects[0].widgets} size={14} />\n )}\n <span className=\"pz:truncate\">{valueObjects[0].label}</span>\n </span>\n )\n ) : reorderable ? (\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={value} strategy={horizontalListSortingStrategy}>\n {valueObjects.map((v) => (\n <SortableChip\n key={v.value}\n id={v.value}\n onRemove={() => onChange(value.filter((x) => x !== v.value))}\n >\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </SortableChip>\n ))}\n </SortableContext>\n </DndContext>\n ) : (\n valueObjects.map((v) => (\n <ComboboxChip key={v.value}>\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </ComboboxChip>\n ))\n )}\n <ComboboxChipsInput\n placeholder={value.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (!single && atMax) return;\n // Prefer an exact match against the visible suggestions, otherwise\n // fall back to creating a new value when allowed.\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? trimmedInput : null);\n if (!toAdd) return;\n if (excludeSet.has(toAdd)) return;\n if (single) {\n onChange([toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n setOpen(false);\n return;\n }\n if (selectedSet.has(toAdd)) return;\n onChange([...value, toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {!single && atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n // Field-type widget renders on the trailing edge of the row so\n // the field name reads first; everything else stays in the\n // leading widget strip.\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const leadingWidgets =\n !item.__create && item.widgets ? splitWidgets(item.widgets).leading : undefined;\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={item}\n className={item.__create ? \"pz:italic\" : undefined}\n disabled={!single && !selectedSet.has(item.value) && atMax && !item.__create}\n >\n {!item.__create && iconFor?.(item.value)}\n {leadingWidgets && <WidgetStrip widgets={leadingWidgets} size={14} />}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-effect-catalog-table.d.mts","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"mappings":";;;;;;;iBAqCgB,qBAAA,CACd,MAAA;EACE,oBAAA,GAAuB,YAAA;;;AAF3B;;EAOI,MAAA,IAAU,KAAA,EAAO,2BAAA;AAAA;;;;cA+EL,mBAAA;WAA4B,cAAA;EAAA;;;;;sBAhCjC,mBAAA;;yCAAmB,OAAA,CAAA,cAAA;AAAA;AAAA,KAmFlB,2BAAA,GAA8B,UAAA,QAAkB,qBAAA"}
1
+ {"version":3,"file":"use-effect-catalog-table.d.mts","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"mappings":";;;;;;;iBA+CgB,qBAAA,CACd,MAAA;EACE,oBAAA,GAAuB,YAAA;;;AAF3B;;EAOI,MAAA,IAAU,KAAA,EAAO,2BAAA;AAAA;;;;cAgFL,mBAAA;WAA4B,cAAA;EAAA;;;;;sBAhCjC,mBAAA;;yCAAmB,OAAA,CAAA,cAAA;AAAA;AAAA,KAmFlB,2BAAA,GAA8B,UAAA,QAAkB,qBAAA"}
@@ -1,23 +1,26 @@
1
- import { fuzzyFilter } from "../utils/catalog-helpers.mjs";
1
+ import { catalogSearchFilter, catalogSearchSort } from "../utils/catalog-helpers.mjs";
2
2
  import { useDebounce } from "./use-debounce.mjs";
3
3
  import { useCallback, useMemo, useState } from "react";
4
- import { getInitialSheetEffectTableData, getLowestSheetEffectCreditAmount, isTokenBilledOperations } from "@pipe0/base";
5
- import { createColumnHelper, getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table";
4
+ import { getInitialSheetEffectTableData, getLowestSheetEffectCreditAmount, getSheetEffectSearchTarget, isTokenBilledOperations } from "@pipe0/base";
5
+ import { createColumnHelper, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
6
6
 
7
7
  //#region src/hooks/use-effect-catalog-table.ts
8
8
  const columnHelper = createColumnHelper();
9
9
  const columns = [
10
10
  columnHelper.accessor("effectId", {
11
11
  filterFn: "includesString",
12
- enableGlobalFilter: true
13
- }),
14
- columnHelper.accessor("description", {
15
- filterFn: "fuzzy",
16
- enableGlobalFilter: true
12
+ enableGlobalFilter: false
17
13
  }),
14
+ columnHelper.accessor("description", { enableGlobalFilter: false }),
18
15
  columnHelper.accessor("label", {
19
16
  filterFn: "includesString",
20
- enableGlobalFilter: true
17
+ enableGlobalFilter: false
18
+ }),
19
+ columnHelper.accessor((row) => getSheetEffectSearchTarget(row), {
20
+ id: "search",
21
+ enableGlobalFilter: true,
22
+ enableSorting: true,
23
+ sortingFn: catalogSearchSort
21
24
  })
22
25
  ];
23
26
  function useEffectCatalogTable(config = {}) {
@@ -29,8 +32,8 @@ function useEffectCatalogTable(config = {}) {
29
32
  data: useMemo(() => category ? baselineTableData.filter((e) => (e.categories ?? []).includes(category)) : baselineTableData, [baselineTableData, category]),
30
33
  getCoreRowModel: getCoreRowModel(),
31
34
  getFilteredRowModel: getFilteredRowModel(),
32
- globalFilterFn: fuzzyFilter,
33
- filterFns: { fuzzy: fuzzyFilter },
35
+ getSortedRowModel: getSortedRowModel(),
36
+ globalFilterFn: catalogSearchFilter,
34
37
  getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter !== false,
35
38
  initialState: {
36
39
  columnFilters: initialColumnFilters,
@@ -40,6 +43,10 @@ function useEffectCatalogTable(config = {}) {
40
43
  const [globalFilterInput, setGlobalFilterInput, setGlobalFilterImmediately] = useDebounce((v) => {
41
44
  if (v === table.getState().globalFilter) return;
42
45
  table.setGlobalFilter(v);
46
+ table.setSorting(v ? [{
47
+ id: "search",
48
+ desc: true
49
+ }] : []);
43
50
  if (v) setCategory(null);
44
51
  });
45
52
  const handleCategoryChange = useCallback((next) => {
@@ -1 +1 @@
1
- {"version":3,"file":"use-effect-catalog-table.mjs","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"sourcesContent":["import {\n getInitialSheetEffectTableData,\n getLowestSheetEffectCreditAmount,\n isTokenBilledOperations,\n type ProviderName,\n type SheetEffectCatalogTableData,\n type SheetEffectCategory,\n} from \"@pipe0/base\";\nimport {\n type ColumnFilter,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { EffectCardData } from \"../types/catalog-adapters.js\";\nimport { fuzzyFilter } from \"../utils/catalog-helpers.js\";\nimport { useDebounce } from \"./use-debounce.js\";\n\nconst columnHelper = createColumnHelper<SheetEffectCatalogTableData>();\n\nconst columns = [\n columnHelper.accessor(\"effectId\", {\n filterFn: \"includesString\",\n enableGlobalFilter: true,\n }),\n columnHelper.accessor(\"description\", {\n filterFn: \"fuzzy\" as any,\n enableGlobalFilter: true,\n }),\n columnHelper.accessor(\"label\", {\n filterFn: \"includesString\",\n enableGlobalFilter: true,\n }),\n];\n\nexport function useEffectCatalogTable(\n config: {\n initialColumnFilters?: ColumnFilter[];\n /**\n * Optional predicate to pre-filter the catalog (e.g. by `selectionMode`\n * for the current row selection). Memoize it to keep the table stable.\n */\n filter?: (entry: SheetEffectCatalogTableData) => boolean;\n } = {},\n) {\n const [category, setCategory] = useState<SheetEffectCategory | null>(null);\n const { initialColumnFilters = [] as ColumnFilter[], filter } = config;\n\n // Baseline (config.filter applied, but no category / global filter). Drives\n // the stable category-button counts so the row doesn't jump.\n const baselineTableData = useMemo(\n () =>\n filter ? getInitialSheetEffectTableData().filter(filter) : getInitialSheetEffectTableData(),\n [filter],\n );\n\n const initialEffectTableData = useMemo(\n () =>\n category\n ? baselineTableData.filter((e) => (e.categories ?? []).includes(category))\n : baselineTableData,\n [baselineTableData, category],\n );\n\n const table = useReactTable({\n columns,\n data: initialEffectTableData,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n globalFilterFn: fuzzyFilter,\n filterFns: {\n fuzzy: fuzzyFilter,\n },\n getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter !== false,\n initialState: {\n columnFilters: initialColumnFilters,\n pagination: {\n pageSize: 10,\n },\n },\n });\n\n const [globalFilterInput, setGlobalFilterInput, setGlobalFilterImmediately] = useDebounce((v) => {\n if (v === table.getState().globalFilter) return;\n table.setGlobalFilter(v);\n if (v) setCategory(null);\n });\n\n const handleCategoryChange = useCallback(\n (next: SheetEffectCategory | null) => {\n setCategory(next);\n setGlobalFilterImmediately(\"\");\n },\n [setGlobalFilterImmediately],\n );\n\n const rows = table.getRowModel().rows;\n const cards = useMemo<EffectCardData[]>(\n () =>\n rows.map((row) => {\n const entry = row.original;\n const provider = (entry.managedProviders[0] ?? \"pipe0\") as ProviderName;\n return {\n effectId: entry.effectId,\n baseEffect: entry.baseEffect,\n label: entry.label,\n description: entry.description,\n provider,\n providers: entry.managedProviders.length ? entry.managedProviders : [provider],\n startingCreditAmount: getLowestSheetEffectCreditAmount(entry),\n costMode: isTokenBilledOperations(entry.billableOperations)\n ? (\"usage\" as const)\n : (\"per_result\" as const),\n selectionMode: entry.selectionMode,\n entry,\n };\n }),\n [rows],\n );\n\n const cardsByCategory = useMemo<\n { category: SheetEffectCategory; cards: EffectCardData[] }[]\n >(() => {\n const groups = new Map<SheetEffectCategory, EffectCardData[]>();\n for (const card of cards) {\n const cats = (card.entry.categories ?? []) as SheetEffectCategory[];\n for (const cat of cats) {\n const arr = groups.get(cat);\n if (arr) arr.push(card);\n else groups.set(cat, [card]);\n }\n }\n return Array.from(groups, ([category, group]) => ({ category, cards: group }));\n }, [cards]);\n\n const categoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const card of cards) {\n for (const cat of (card.entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [cards]);\n\n // Stable counts based on the baseline (no category / global filter applied).\n const baselineCardCount = baselineTableData.length;\n const baselineCategoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const entry of baselineTableData) {\n for (const cat of (entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [baselineTableData]);\n\n return {\n table,\n cards,\n cardsByCategory,\n categoryCounts,\n baselineCardCount,\n baselineCategoryCounts,\n\n category,\n setCategory: handleCategoryChange,\n globalFilterInput,\n setGlobalFilterInput,\n };\n}\n\nexport type UseEffectCatalogTableReturn = ReturnType<typeof useEffectCatalogTable>;\n"],"mappings":";;;;;;;AAoBA,MAAM,eAAe,oBAAiD;AAEtE,MAAM,UAAU;CACd,aAAa,SAAS,YAAY;EAChC,UAAU;EACV,oBAAoB;EACrB,CAAC;CACF,aAAa,SAAS,eAAe;EACnC,UAAU;EACV,oBAAoB;EACrB,CAAC;CACF,aAAa,SAAS,SAAS;EAC7B,UAAU;EACV,oBAAoB;EACrB,CAAC;CACH;AAED,SAAgB,sBACd,SAOI,EAAE,EACN;CACA,MAAM,CAAC,UAAU,eAAe,SAAqC,KAAK;CAC1E,MAAM,EAAE,uBAAuB,EAAE,EAAoB,WAAW;CAIhE,MAAM,oBAAoB,cAEtB,SAAS,gCAAgC,CAAC,OAAO,OAAO,GAAG,gCAAgC,EAC7F,CAAC,OAAO,CACT;CAUD,MAAM,QAAQ,cAAc;EAC1B;EACA,MAV6B,cAE3B,WACI,kBAAkB,QAAQ,OAAO,EAAE,cAAc,EAAE,EAAE,SAAS,SAAS,CAAC,GACxE,mBACN,CAAC,mBAAmB,SAAS,CAC9B;EAKC,iBAAiB,iBAAiB;EAClC,qBAAqB,qBAAqB;EAC1C,gBAAgB;EAChB,WAAW,EACT,OAAO,aACR;EACD,2BAA2B,WAAW,OAAO,UAAU,uBAAuB;EAC9E,cAAc;GACZ,eAAe;GACf,YAAY,EACV,UAAU,IACX;GACF;EACF,CAAC;CAEF,MAAM,CAAC,mBAAmB,sBAAsB,8BAA8B,aAAa,MAAM;AAC/F,MAAI,MAAM,MAAM,UAAU,CAAC,aAAc;AACzC,QAAM,gBAAgB,EAAE;AACxB,MAAI,EAAG,aAAY,KAAK;GACxB;CAEF,MAAM,uBAAuB,aAC1B,SAAqC;AACpC,cAAY,KAAK;AACjB,6BAA2B,GAAG;IAEhC,CAAC,2BAA2B,CAC7B;CAED,MAAM,OAAO,MAAM,aAAa,CAAC;CACjC,MAAM,QAAQ,cAEV,KAAK,KAAK,QAAQ;EAChB,MAAM,QAAQ,IAAI;EAClB,MAAM,WAAY,MAAM,iBAAiB,MAAM;AAC/C,SAAO;GACL,UAAU,MAAM;GAChB,YAAY,MAAM;GAClB,OAAO,MAAM;GACb,aAAa,MAAM;GACnB;GACA,WAAW,MAAM,iBAAiB,SAAS,MAAM,mBAAmB,CAAC,SAAS;GAC9E,sBAAsB,iCAAiC,MAAM;GAC7D,UAAU,wBAAwB,MAAM,mBAAmB,GACtD,UACA;GACL,eAAe,MAAM;GACrB;GACD;GACD,EACJ,CAAC,KAAK,CACP;AAuCD,QAAO;EACL;EACA;EACA,iBAxCsB,cAEhB;GACN,MAAM,yBAAS,IAAI,KAA4C;AAC/D,QAAK,MAAM,QAAQ,OAAO;IACxB,MAAM,OAAQ,KAAK,MAAM,cAAc,EAAE;AACzC,SAAK,MAAM,OAAO,MAAM;KACtB,MAAM,MAAM,OAAO,IAAI,IAAI;AAC3B,SAAI,IAAK,KAAI,KAAK,KAAK;SAClB,QAAO,IAAI,KAAK,CAAC,KAAK,CAAC;;;AAGhC,UAAO,MAAM,KAAK,SAAS,CAAC,UAAU,YAAY;IAAE;IAAU,OAAO;IAAO,EAAE;KAC7E,CAAC,MAAM,CAAC;EA4BT,gBA1BqB,cAA4D;GACjF,MAAM,SAAuD,EAAE;AAC/D,QAAK,MAAM,QAAQ,MACjB,MAAK,MAAM,OAAQ,KAAK,MAAM,cAAc,EAAE,CAC5C,QAAO,QAAQ,OAAO,QAAQ,KAAK;AAGvC,UAAO;KACN,CAAC,MAAM,CAAC;EAmBT,mBAhBwB,kBAAkB;EAiB1C,wBAhB6B,cAA4D;GACzF,MAAM,SAAuD,EAAE;AAC/D,QAAK,MAAM,SAAS,kBAClB,MAAK,MAAM,OAAQ,MAAM,cAAc,EAAE,CACvC,QAAO,QAAQ,OAAO,QAAQ,KAAK;AAGvC,UAAO;KACN,CAAC,kBAAkB,CAAC;EAUrB;EACA,aAAa;EACb;EACA;EACD"}
1
+ {"version":3,"file":"use-effect-catalog-table.mjs","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"sourcesContent":["import {\n getInitialSheetEffectTableData,\n getLowestSheetEffectCreditAmount,\n getSheetEffectSearchTarget,\n isTokenBilledOperations,\n type ProviderName,\n type SheetEffectCatalogTableData,\n type SheetEffectCategory,\n} from \"@pipe0/base\";\nimport {\n type ColumnFilter,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { EffectCardData } from \"../types/catalog-adapters.js\";\nimport { catalogSearchFilter, catalogSearchSort } from \"../utils/catalog-helpers.js\";\nimport { useDebounce } from \"./use-debounce.js\";\n\nconst columnHelper = createColumnHelper<SheetEffectCatalogTableData>();\n\nconst columns = [\n columnHelper.accessor(\"effectId\", {\n filterFn: \"includesString\",\n enableGlobalFilter: false,\n }),\n columnHelper.accessor(\"description\", {\n enableGlobalFilter: false,\n }),\n columnHelper.accessor(\"label\", {\n filterFn: \"includesString\",\n enableGlobalFilter: false,\n }),\n // Hidden relevance column — the ONLY globally-filterable one, so the global\n // filter scores each row exactly once and catalogSearchSort can read the\n // score back from this column's filter meta.\n columnHelper.accessor((row) => getSheetEffectSearchTarget(row), {\n id: \"search\",\n enableGlobalFilter: true,\n enableSorting: true,\n sortingFn: catalogSearchSort,\n }),\n];\n\nexport function useEffectCatalogTable(\n config: {\n initialColumnFilters?: ColumnFilter[];\n /**\n * Optional predicate to pre-filter the catalog (e.g. by `selectionMode`\n * for the current row selection). Memoize it to keep the table stable.\n */\n filter?: (entry: SheetEffectCatalogTableData) => boolean;\n } = {},\n) {\n const [category, setCategory] = useState<SheetEffectCategory | null>(null);\n const { initialColumnFilters = [] as ColumnFilter[], filter } = config;\n\n // Baseline (config.filter applied, but no category / global filter). Drives\n // the stable category-button counts so the row doesn't jump.\n const baselineTableData = useMemo(\n () =>\n filter ? getInitialSheetEffectTableData().filter(filter) : getInitialSheetEffectTableData(),\n [filter],\n );\n\n const initialEffectTableData = useMemo(\n () =>\n category\n ? baselineTableData.filter((e) => (e.categories ?? []).includes(category))\n : baselineTableData,\n [baselineTableData, category],\n );\n\n const table = useReactTable({\n columns,\n data: initialEffectTableData,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getSortedRowModel: getSortedRowModel(),\n globalFilterFn: catalogSearchFilter,\n getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter !== false,\n initialState: {\n columnFilters: initialColumnFilters,\n pagination: {\n pageSize: 10,\n },\n },\n });\n\n const [globalFilterInput, setGlobalFilterInput, setGlobalFilterImmediately] = useDebounce((v) => {\n if (v === table.getState().globalFilter) return;\n table.setGlobalFilter(v);\n // Relevance order only while a query is active; empty query restores\n // catalog order.\n table.setSorting(v ? [{ id: \"search\", desc: true }] : []);\n if (v) setCategory(null);\n });\n\n const handleCategoryChange = useCallback(\n (next: SheetEffectCategory | null) => {\n setCategory(next);\n setGlobalFilterImmediately(\"\");\n },\n [setGlobalFilterImmediately],\n );\n\n const rows = table.getRowModel().rows;\n const cards = useMemo<EffectCardData[]>(\n () =>\n rows.map((row) => {\n const entry = row.original;\n const provider = (entry.managedProviders[0] ?? \"pipe0\") as ProviderName;\n return {\n effectId: entry.effectId,\n baseEffect: entry.baseEffect,\n label: entry.label,\n description: entry.description,\n provider,\n providers: entry.managedProviders.length ? entry.managedProviders : [provider],\n startingCreditAmount: getLowestSheetEffectCreditAmount(entry),\n costMode: isTokenBilledOperations(entry.billableOperations)\n ? (\"usage\" as const)\n : (\"per_result\" as const),\n selectionMode: entry.selectionMode,\n entry,\n };\n }),\n [rows],\n );\n\n const cardsByCategory = useMemo<\n { category: SheetEffectCategory; cards: EffectCardData[] }[]\n >(() => {\n const groups = new Map<SheetEffectCategory, EffectCardData[]>();\n for (const card of cards) {\n const cats = (card.entry.categories ?? []) as SheetEffectCategory[];\n for (const cat of cats) {\n const arr = groups.get(cat);\n if (arr) arr.push(card);\n else groups.set(cat, [card]);\n }\n }\n return Array.from(groups, ([category, group]) => ({ category, cards: group }));\n }, [cards]);\n\n const categoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const card of cards) {\n for (const cat of (card.entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [cards]);\n\n // Stable counts based on the baseline (no category / global filter applied).\n const baselineCardCount = baselineTableData.length;\n const baselineCategoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const entry of baselineTableData) {\n for (const cat of (entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [baselineTableData]);\n\n return {\n table,\n cards,\n cardsByCategory,\n categoryCounts,\n baselineCardCount,\n baselineCategoryCounts,\n\n category,\n setCategory: handleCategoryChange,\n globalFilterInput,\n setGlobalFilterInput,\n };\n}\n\nexport type UseEffectCatalogTableReturn = ReturnType<typeof useEffectCatalogTable>;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-form-core.d.mts","names":[],"sources":["../../src/hooks/use-form-core.ts"],"mappings":";;KA+BY,cAAA;AAAA,KASA,cAAA;EACV,MAAA,EAAQ,cAAA,EADgB;EAGxB,QAAA,WAFsB;EAItB,cAAA,WAJQ;EAMR,mBAAA,WAFA;EAIA,yBAAA;AAAA"}
1
+ {"version":3,"file":"use-form-core.d.mts","names":[],"sources":["../../src/hooks/use-form-core.ts"],"mappings":";;KAgCY,cAAA;AAAA,KASA,cAAA;EACV,MAAA,EAAQ,cAAA,EADgB;EAGxB,QAAA,WAFsB;EAItB,cAAA,WAJQ;EAMR,mBAAA,WAFA;EAIA,yBAAA;AAAA"}
@@ -14,7 +14,7 @@ function collectEnabledSlots(formConfig) {
14
14
  slotId: "field",
15
15
  enabledIf: field.enabledIf
16
16
  });
17
- if ((field.type === "context_select_input" || field.type === "tagged_text_input") && "optionsDef" in field && field.optionsDef) {
17
+ if ((field.type === "context_select_input" || field.type === "tagged_text_input" || field.type === "condition_block_input" || field.type === "field_select_input" || field.type === "fields_select_input" || field.type === "typed_fields_select_input") && "optionsDef" in field && field.optionsDef) {
18
18
  const optionsDef = field.optionsDef;
19
19
  if (optionsDef?.enabledIf) out.push({
20
20
  fieldPath: field.path,
@@ -135,6 +135,24 @@ function useFormCore(options) {
135
135
  scopesKey,
136
136
  teamId
137
137
  ]);
138
+ const getFieldContextResolver = resolvers?.getFieldContext;
139
+ const searchFieldContext = useCallback(async (fieldPath, query) => {
140
+ if (!getFieldContextResolver) return [];
141
+ const idKey = kind === "search" ? "search_id" : kind === "effect" ? "effect_id" : "pipe_id";
142
+ return (await Promise.resolve(getFieldContextResolver({
143
+ fieldPath,
144
+ query,
145
+ payload: {
146
+ ...form.getValues(),
147
+ [idKey]: pipeOrSearchId
148
+ }
149
+ })))?.field_options?.[fieldPath]?.options ?? [];
150
+ }, [
151
+ getFieldContextResolver,
152
+ kind,
153
+ pipeOrSearchId,
154
+ form
155
+ ]);
138
156
  const slots = useMemo(() => collectEnabledSlots(formConfig), [formConfig]);
139
157
  const watchedValues = form.watch();
140
158
  const slotResults = useMemo(() => {
@@ -147,7 +165,6 @@ function useFormCore(options) {
147
165
  const lastFiredSigRef = useRef({});
148
166
  useEffect(() => {
149
167
  if (slots.length === 0) return;
150
- let cancelled = false;
151
168
  const nextLoadStates = { ...fieldLoadStates };
152
169
  const byField = {};
153
170
  for (const slot of slots) {
@@ -186,6 +203,7 @@ function useFormCore(options) {
186
203
  });
187
204
  if (lastFiredSigRef.current[slot.fieldPath] === perFieldSig) continue;
188
205
  lastFiredSigRef.current[slot.fieldPath] = perFieldSig;
206
+ const isCurrent = () => lastFiredSigRef.current[slot.fieldPath] === perFieldSig;
189
207
  const existing = nextLoadStates[slot.fieldPath];
190
208
  nextLoadStates[slot.fieldPath] = {
191
209
  status: "loading",
@@ -203,7 +221,8 @@ function useFormCore(options) {
203
221
  [idKey]: pipeOrSearchId
204
222
  }
205
223
  })).then((incoming) => {
206
- if (cancelled) return;
224
+ if (!isCurrent()) return;
225
+ if (!incoming?.field_options) throw new Error("Empty field-context response");
207
226
  mergeStore(incoming);
208
227
  setFieldLoadStates((s) => ({
209
228
  ...s,
@@ -216,7 +235,7 @@ function useFormCore(options) {
216
235
  }
217
236
  }));
218
237
  }).catch(() => {
219
- if (cancelled) return;
238
+ if (!isCurrent()) return;
220
239
  setFieldLoadStates((s) => ({
221
240
  ...s,
222
241
  [slot.fieldPath]: {
@@ -243,9 +262,6 @@ function useFormCore(options) {
243
262
  }
244
263
  setFieldLoadStates(nextLoadStates);
245
264
  prevResultsRef.current = slotResults;
246
- return () => {
247
- cancelled = true;
248
- };
249
265
  }, [
250
266
  slotResultsSig,
251
267
  slots,
@@ -256,7 +272,8 @@ function useFormCore(options) {
256
272
  const sections = useMemo(() => buildSectionHandles(formConfig, form, publicKey, {
257
273
  fieldLoadStates,
258
274
  searchSecrets,
259
- searchConstants
275
+ searchConstants,
276
+ searchFieldContext
260
277
  }), [
261
278
  formConfig,
262
279
  form,
@@ -265,6 +282,7 @@ function useFormCore(options) {
265
282
  isSubmitted,
266
283
  searchSecrets,
267
284
  searchConstants,
285
+ searchFieldContext,
268
286
  watchedValues,
269
287
  formErrors
270
288
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"use-form-core.mjs","names":[],"sources":["../../src/hooks/use-form-core.ts"],"sourcesContent":["import { standardSchemaResolver } from \"@hookform/resolvers/standard-schema\";\nimport type {\n EnabledIf,\n EnabledResult,\n FormResolvers,\n FormSection,\n FormStore,\n GeneratedInputMeta,\n PipesEnvironment,\n ProviderName,\n} from \"@pipe0/base\";\nimport { joinConnectionString } from \"@pipe0/base\";\nimport {\n type Dispatch,\n type SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { type FieldValues, type UseFormReturn, useForm } from \"react-hook-form\";\nimport type { AnyFieldProps, ConstantSuggestion, SecretSuggestion } from \"../types/field-props.js\";\nimport type { FormSectionHandle } from \"../types/form-handle.js\";\nimport { buildSectionHandles } from \"../utils/build-section-handlers.js\";\nimport { mergeFormStores } from \"../utils/merge-form-stores.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type ResourceStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\nexport type SlotId = \"field\" | \"suggestions\";\n\nexport type FieldEnabledState = {\n disabled: boolean;\n reason?: string;\n};\n\nexport type FieldLoadState = {\n status: ResourceStatus;\n /** Field-level disabled (from `meta.enabledIf`). */\n disabled: boolean;\n /** Field-level disabled reason. */\n disabledReason?: string;\n /** Suggestions sub-feature gate (from `optionsDef.enabledIf`). */\n suggestionsDisabled: boolean;\n /** Suggestions sub-feature reason. */\n suggestionsDisabledReason?: string;\n};\n\nexport interface UseFormCoreOptions<T extends FieldValues> {\n pipeOrSearchId: string;\n kind: \"pipe\" | \"search\" | \"effect\";\n schema: any;\n publicKey: string;\n defaultValues?: T;\n formConfig: FormSection[];\n resolvers?: FormResolvers;\n store: FormStore;\n setStore: Dispatch<SetStateAction<FormStore>>;\n environment?: PipesEnvironment;\n /**\n * Form-level scope tags. Bundled into every `resolvers.getSecrets` call so\n * the backend can return only the secrets allowed in the declared scopes\n * (intersection on top of cascade visibility).\n */\n scopes?: string[];\n /**\n * Current team context. Bundled into every `resolvers.getSecrets` call so\n * the backend can restrict team-level secrets to exactly this team.\n */\n teamId?: string;\n}\n\nexport interface UseFormCoreReturn<T extends FieldValues> {\n connectionsStatus: ResourceStatus;\n /** Map of field path → load state for dynamic context_select_input fields. */\n fieldLoadStates: Record<string, FieldLoadState>;\n /** Subset of `fieldLoadStates` where `status === \"error\"`. RHF-style. */\n fieldLoaderErrors: Record<string, FieldLoadState>;\n /** Subset of `fieldLoadStates` where `status === \"loading\"`. RHF-style. */\n loadingFieldLoaders: Record<string, FieldLoadState>;\n /** True when any field loader has errored. */\n hasFieldLoaderError: boolean;\n /** True when any field loader is currently loading. */\n isFieldLoaderLoading: boolean;\n form: UseFormReturn<T, any, any>;\n sections: FormSectionHandle[];\n fields: AnyFieldProps[];\n reset: (values?: T) => void;\n}\n\ntype EnabledSlot = {\n fieldPath: string;\n slotId: SlotId;\n enabledIf: EnabledIf;\n /** Only present for \"suggestions\" slots — used to drive option fetching. */\n optionsDef?: {\n requires: { connection?: ProviderName; fields?: readonly string[] };\n };\n};\n\nfunction collectEnabledSlots(formConfig: FormSection[]): EnabledSlot[] {\n const out: EnabledSlot[] = [];\n for (const section of formConfig) {\n for (const group of section.groups) {\n for (const field of group.fields as GeneratedInputMeta[]) {\n if (field.enabledIf) {\n out.push({\n fieldPath: field.path,\n slotId: \"field\",\n enabledIf: field.enabledIf,\n });\n }\n const supportsOptionsDef =\n field.type === \"context_select_input\" || field.type === \"tagged_text_input\";\n if (supportsOptionsDef && \"optionsDef\" in field && field.optionsDef) {\n const optionsDef = field.optionsDef as EnabledSlot[\"optionsDef\"] & {\n enabledIf?: EnabledIf;\n };\n if (optionsDef?.enabledIf) {\n out.push({\n fieldPath: field.path,\n slotId: \"suggestions\",\n enabledIf: optionsDef.enabledIf,\n optionsDef,\n });\n } else {\n // Even without a sub-feature enabledIf, we still need to track\n // this field as having a fetchable suggestions slot — it's\n // always enabled and the fetch fires on every value change.\n out.push({\n fieldPath: field.path,\n slotId: \"suggestions\",\n enabledIf: () => ({ disabled: false }),\n optionsDef,\n });\n }\n }\n }\n }\n }\n return out;\n}\n\nfunction getPathValue(obj: unknown, path: string): unknown {\n const parts = path.split(\".\");\n let cur: any = obj;\n for (const part of parts) {\n if (cur == null) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\nfunction evalSlot(slot: EnabledSlot, payload: unknown): EnabledResult {\n return slot.enabledIf(payload);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useFormCore<T extends FieldValues>(\n options: UseFormCoreOptions<T>,\n): UseFormCoreReturn<T> {\n const {\n schema,\n publicKey,\n defaultValues,\n formConfig,\n resolvers,\n pipeOrSearchId,\n kind,\n setStore,\n environment = \"production\",\n scopes,\n teamId,\n } = options;\n\n // Stable signature for `scopes` so its identity doesn't churn the effect\n // dep across renders when callers pass a fresh array literal each time.\n const scopesKey = useMemo(() => (scopes ? JSON.stringify(scopes) : \"\"), [scopes]);\n\n // --- Per-resource status ---\n const [connectionsStatus, setConnectionsStatus] = useState<ResourceStatus>(\n resolvers?.getConnections ? \"loading\" : \"idle\",\n );\n const [fieldLoadStates, setFieldLoadStates] = useState<Record<string, FieldLoadState>>({});\n\n // --- Form ---\n const form = useForm<T>({\n resolver: standardSchemaResolver(schema),\n defaultValues: defaultValues as any,\n });\n\n // --- Helpers ---\n const mergeStore = useCallback(\n (incoming: FormStore) => setStore((old) => mergeFormStores(old, incoming)),\n [setStore],\n );\n\n // Latest-ref for formConfig: Effect 1 reads it for the connector prefill but\n // must not depend on it (formConfig rebuilds on every store merge, which\n // would re-fire the connections fetch in a loop).\n const formConfigRef = useRef(formConfig);\n formConfigRef.current = formConfig;\n\n // --- Effect 1: Load connections ---\n useEffect(() => {\n if (!resolvers?.getConnections) {\n setConnectionsStatus(\"idle\");\n return;\n }\n\n let cancelled = false;\n setConnectionsStatus(\"loading\");\n\n Promise.resolve(resolvers.getConnections({ id: pipeOrSearchId, environment }))\n .then((conns) => {\n if (cancelled) return;\n\n mergeStore({\n field_options: {\n connections: {\n options: conns.map((c) => ({\n label: c.public_id,\n value: c.public_id,\n widgets: {\n provider_logo: { provider: c.provider as ProviderName },\n },\n })),\n },\n },\n });\n\n // Prefill default connections into an EMPTY required connector. A\n // stored payload with a required connector always carries >=1\n // connection (min-1 validation), so an empty required connector\n // implies a fresh create form — prefilling never overwrites a\n // user's or stored choice. Runs once per connections load, so a\n // user clearing the field afterwards is not fought.\n const connectorField = formConfigRef.current\n .flatMap((section) => section.groups.flatMap((group) => group.fields))\n .map((field) => field as GeneratedInputMeta)\n .find((field) => field.type === \"connector_input\");\n if (\n connectorField?.type === \"connector_input\" &&\n connectorField.connectorMode === \"required\"\n ) {\n const current = form.getValues(connectorField.path as any) as\n | { connections?: { connection: string }[] }\n | null\n | undefined;\n const isEmpty = current == null || (current.connections?.length ?? 0) === 0;\n const defaults = conns.filter((c) => c.is_default);\n if (isEmpty && defaults.length > 0) {\n form.setValue(\n connectorField.path as any,\n {\n strategy: \"first\",\n connections: defaults.map((c) => ({\n type: \"vault\",\n connection: joinConnectionString({ provider: c.provider, id: c.public_id }),\n })),\n } as any,\n // Not dirtying: a freshly opened form with a prefilled default\n // should still count as pristine.\n { shouldDirty: false, shouldValidate: false },\n );\n }\n }\n\n setConnectionsStatus(\"ready\");\n })\n .catch(() => {\n if (!cancelled) setConnectionsStatus(\"error\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [pipeOrSearchId, environment, resolvers?.getConnections, mergeStore]);\n\n // --- Curried secrets search ---\n // Each keystroke in the reference picker fires this with the latest query.\n // The picker handles debounce + race-correctness; here we just bundle in\n // form-level args (environment / scopes / teamId) and call the resolver.\n // No caching: every call hits the resolver. Returns [] if no resolver.\n const getSecretsResolver = resolvers?.getSecrets;\n const searchSecrets = useCallback(\n async (query: string): Promise<SecretSuggestion[]> => {\n if (!getSecretsResolver) return [];\n return Promise.resolve(getSecretsResolver({ query, environment, scopes, teamId }));\n },\n // `scopesKey` is the stable serialization of `scopes`; depending on the\n // raw array would churn the callback identity on every parent render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [getSecretsResolver, environment, scopesKey, teamId],\n );\n\n // --- Curried constants search ---\n // Mirrors `searchSecrets` exactly. Same per-keystroke contract, same\n // form-level arg bundling. Returns [] when no resolver is wired.\n const getConstantsResolver = resolvers?.getConstants;\n const searchConstants = useCallback(\n async (query: string): Promise<ConstantSuggestion[]> => {\n if (!getConstantsResolver) return [];\n return Promise.resolve(getConstantsResolver({ query, environment, scopes, teamId }));\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [getConstantsResolver, environment, scopesKey, teamId],\n );\n\n // --- Effect 2: Evaluate enabledIf slots + drive context_select fetching ---\n const slots = useMemo(() => collectEnabledSlots(formConfig), [formConfig]);\n\n // Subscribe to all form value changes so resolvers re-evaluate.\n const watchedValues = form.watch();\n\n // Evaluate every slot against the current payload. Keep the result\n // serialized so the effect's deps stay stable across reference-identical\n // re-renders.\n const slotResults = useMemo(() => {\n const out: Record<string, EnabledResult> = {};\n for (const slot of slots) {\n out[`${slot.fieldPath}::${slot.slotId}`] = evalSlot(slot, watchedValues);\n }\n return out;\n }, [slots, watchedValues]);\n const slotResultsSig = useMemo(() => JSON.stringify(slotResults), [slotResults]);\n\n // Track previous results so we can detect enabled→disabled transitions\n // (drives field-value clearing) and last-fired fetch signatures.\n const prevResultsRef = useRef<Record<string, EnabledResult>>({});\n const lastFiredSigRef = useRef<Record<string, string>>({});\n\n useEffect(() => {\n if (slots.length === 0) return;\n\n let cancelled = false;\n const nextLoadStates: Record<string, FieldLoadState> = {\n ...fieldLoadStates,\n };\n\n // Group slot results by field path for the load-state map.\n const byField: Record<string, { field?: EnabledResult; suggestions?: EnabledResult }> = {};\n for (const slot of slots) {\n const r = slotResults[`${slot.fieldPath}::${slot.slotId}`];\n if (r == null) continue;\n byField[slot.fieldPath] ??= {};\n byField[slot.fieldPath][slot.slotId] = r;\n }\n\n for (const slot of slots) {\n const key = `${slot.fieldPath}::${slot.slotId}`;\n const r = slotResults[key];\n if (r == null) continue;\n const prev = prevResultsRef.current[key];\n\n // Field-slot only: on enabled→disabled, reset the value to the field's\n // configured default (from the seeded defaultValues) rather than `\"\"`.\n // `\"\"` is invalid for non-string fields — e.g. a gated boolean fails\n // `z.boolean()` and a nullable int's `null` default would coerce to `0`.\n // Fall back to `\"\"` only when there's NO default at the path, so explicit\n // `null`/`false` defaults survive (this resets an optional int back to\n // empty when its gate turns off).\n if (slot.slotId === \"field\" && r.disabled && prev != null && !prev.disabled) {\n const def = getPathValue(defaultValues, slot.fieldPath);\n form.setValue(slot.fieldPath as any, (def === undefined ? \"\" : def) as any, {\n shouldDirty: true,\n shouldTouch: true,\n });\n }\n\n // Suggestions-slot: drive the existing context_select_input fetch.\n if (slot.slotId === \"suggestions\" && resolvers?.getFieldContext) {\n // If suggestions are gated, skip the fetch and reset dedupe so\n // re-enabling re-fires.\n if (r.disabled) {\n delete lastFiredSigRef.current[slot.fieldPath];\n continue;\n }\n\n // Resolve the connection for the legacy `requires.connection`\n // (still needed to pick the right secret for the fetch).\n const watchedConnections = (\n watchedValues as {\n connector?: { connections?: { connection?: string }[] };\n }\n )?.connector?.connections;\n const required = slot.optionsDef?.requires;\n\n let connectionId: string | undefined;\n if (required?.connection) {\n const match = watchedConnections?.find((c) =>\n c?.connection?.startsWith(`${required.connection}_`),\n );\n connectionId = match?.connection;\n // No matching connection — can't fetch, even if enabledIf passed.\n if (!connectionId) continue;\n }\n\n // Per-field signature for dedupe.\n const perFieldSig = JSON.stringify({\n connectionId,\n deps: required?.fields?.map((d) => getPathValue(watchedValues, d)) ?? [],\n });\n if (lastFiredSigRef.current[slot.fieldPath] === perFieldSig) continue;\n lastFiredSigRef.current[slot.fieldPath] = perFieldSig;\n\n // Mark loading for this field's fetch.\n const existing = nextLoadStates[slot.fieldPath];\n nextLoadStates[slot.fieldPath] = {\n status: \"loading\",\n disabled: existing?.disabled ?? false,\n disabledReason: existing?.disabledReason,\n suggestionsDisabled: false,\n suggestionsDisabledReason: undefined,\n };\n\n const idKey = kind === \"search\" ? \"search_id\" : kind === \"effect\" ? \"effect_id\" : \"pipe_id\";\n\n Promise.resolve(\n resolvers.getFieldContext({\n fieldPath: slot.fieldPath,\n query: \"\",\n payload: {\n ...(watchedValues as Record<string, unknown>),\n [idKey]: pipeOrSearchId,\n },\n }),\n )\n .then((incoming: FormStore) => {\n if (cancelled) return;\n mergeStore(incoming);\n setFieldLoadStates((s) => ({\n ...s,\n [slot.fieldPath]: {\n ...(s[slot.fieldPath] ?? {\n disabled: false,\n suggestionsDisabled: false,\n }),\n status: \"ready\",\n },\n }));\n })\n .catch(() => {\n if (cancelled) return;\n setFieldLoadStates((s) => ({\n ...s,\n [slot.fieldPath]: {\n ...(s[slot.fieldPath] ?? {\n disabled: false,\n suggestionsDisabled: false,\n }),\n status: \"error\",\n },\n }));\n });\n }\n }\n\n // Reflect the latest enabledIf evaluation into fieldLoadStates so the\n // adapters and field-wrapper can render the disabled state.\n for (const fieldPath of Object.keys(byField)) {\n const { field: fieldR, suggestions: sugR } = byField[fieldPath]!;\n const prevState = nextLoadStates[fieldPath];\n nextLoadStates[fieldPath] = {\n status: prevState?.status ?? \"idle\",\n disabled: fieldR == null ? (prevState?.disabled ?? false) : fieldR.disabled,\n disabledReason: fieldR == null ? prevState?.disabledReason : fieldR.message,\n suggestionsDisabled:\n sugR == null ? (prevState?.suggestionsDisabled ?? false) : sugR.disabled,\n suggestionsDisabledReason:\n sugR == null ? prevState?.suggestionsDisabledReason : sugR.message,\n };\n }\n\n setFieldLoadStates(nextLoadStates);\n prevResultsRef.current = slotResults;\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slotResultsSig, slots, pipeOrSearchId]);\n\n // --- Sections & fields ---\n const isSubmitted = form.formState.isSubmitted;\n const formErrors = form.formState.errors;\n\n // `watchedValues` and `formErrors` must be in the dep array: field handles\n // bake `form.getValues(path)` snapshots into `field.value` / textareaProps /\n // selectedValue, and adapters render those as controlled inputs. Without\n // these deps, the memo never refreshes on keystrokes and the controlled\n // inputs snap back to their stale snapshot — typing/selecting appears to\n // do nothing. See FieldRenderer's memo comment for the intended contract.\n const sections = useMemo(\n () =>\n buildSectionHandles(formConfig, form as any, publicKey, {\n fieldLoadStates,\n searchSecrets,\n searchConstants,\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n formConfig,\n form,\n publicKey,\n fieldLoadStates,\n isSubmitted,\n searchSecrets,\n searchConstants,\n watchedValues,\n formErrors,\n ],\n );\n\n const fields = useMemo(\n () => sections.flatMap((s) => s.groups.flatMap((g) => g.fields)),\n [sections],\n );\n\n const { fieldLoaderErrors, loadingFieldLoaders, hasFieldLoaderError, isFieldLoaderLoading } =\n useMemo(() => {\n const errors: Record<string, FieldLoadState> = {};\n const loading: Record<string, FieldLoadState> = {};\n for (const [path, state] of Object.entries(fieldLoadStates)) {\n if (state.status === \"error\") errors[path] = state;\n else if (state.status === \"loading\") loading[path] = state;\n }\n return {\n fieldLoaderErrors: errors,\n loadingFieldLoaders: loading,\n hasFieldLoaderError: Object.keys(errors).length > 0,\n isFieldLoaderLoading: Object.keys(loading).length > 0,\n };\n }, [fieldLoadStates]);\n\n return {\n connectionsStatus,\n fieldLoadStates,\n fieldLoaderErrors,\n loadingFieldLoaders,\n hasFieldLoaderError,\n isFieldLoaderLoading,\n form,\n sections,\n fields,\n reset: form.reset,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-form-core.mjs","names":[],"sources":["../../src/hooks/use-form-core.ts"],"sourcesContent":["import { standardSchemaResolver } from \"@hookform/resolvers/standard-schema\";\nimport type {\n EnabledIf,\n EnabledResult,\n FormResolvers,\n FormSection,\n FormStore,\n GeneratedInputMeta,\n PipesEnvironment,\n ProviderName,\n StoreOption,\n} from \"@pipe0/base\";\nimport { joinConnectionString } from \"@pipe0/base\";\nimport {\n type Dispatch,\n type SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { type FieldValues, type UseFormReturn, useForm } from \"react-hook-form\";\nimport type { AnyFieldProps, ConstantSuggestion, SecretSuggestion } from \"../types/field-props.js\";\nimport type { FormSectionHandle } from \"../types/form-handle.js\";\nimport { buildSectionHandles } from \"../utils/build-section-handlers.js\";\nimport { mergeFormStores } from \"../utils/merge-form-stores.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type ResourceStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\nexport type SlotId = \"field\" | \"suggestions\";\n\nexport type FieldEnabledState = {\n disabled: boolean;\n reason?: string;\n};\n\nexport type FieldLoadState = {\n status: ResourceStatus;\n /** Field-level disabled (from `meta.enabledIf`). */\n disabled: boolean;\n /** Field-level disabled reason. */\n disabledReason?: string;\n /** Suggestions sub-feature gate (from `optionsDef.enabledIf`). */\n suggestionsDisabled: boolean;\n /** Suggestions sub-feature reason. */\n suggestionsDisabledReason?: string;\n};\n\nexport interface UseFormCoreOptions<T extends FieldValues> {\n pipeOrSearchId: string;\n kind: \"pipe\" | \"search\" | \"effect\";\n schema: any;\n publicKey: string;\n defaultValues?: T;\n formConfig: FormSection[];\n resolvers?: FormResolvers;\n store: FormStore;\n setStore: Dispatch<SetStateAction<FormStore>>;\n environment?: PipesEnvironment;\n /**\n * Form-level scope tags. Bundled into every `resolvers.getSecrets` call so\n * the backend can return only the secrets allowed in the declared scopes\n * (intersection on top of cascade visibility).\n */\n scopes?: string[];\n /**\n * Current team context. Bundled into every `resolvers.getSecrets` call so\n * the backend can restrict team-level secrets to exactly this team.\n */\n teamId?: string;\n}\n\nexport interface UseFormCoreReturn<T extends FieldValues> {\n connectionsStatus: ResourceStatus;\n /** Map of field path → load state for dynamic context_select_input fields. */\n fieldLoadStates: Record<string, FieldLoadState>;\n /** Subset of `fieldLoadStates` where `status === \"error\"`. RHF-style. */\n fieldLoaderErrors: Record<string, FieldLoadState>;\n /** Subset of `fieldLoadStates` where `status === \"loading\"`. RHF-style. */\n loadingFieldLoaders: Record<string, FieldLoadState>;\n /** True when any field loader has errored. */\n hasFieldLoaderError: boolean;\n /** True when any field loader is currently loading. */\n isFieldLoaderLoading: boolean;\n form: UseFormReturn<T, any, any>;\n sections: FormSectionHandle[];\n fields: AnyFieldProps[];\n reset: (values?: T) => void;\n}\n\ntype EnabledSlot = {\n fieldPath: string;\n slotId: SlotId;\n enabledIf: EnabledIf;\n /** Only present for \"suggestions\" slots — used to drive option fetching. */\n optionsDef?: {\n requires: { connection?: ProviderName; fields?: readonly string[] };\n };\n};\n\nfunction collectEnabledSlots(formConfig: FormSection[]): EnabledSlot[] {\n const out: EnabledSlot[] = [];\n for (const section of formConfig) {\n for (const group of section.groups) {\n for (const field of group.fields as GeneratedInputMeta[]) {\n if (field.enabledIf) {\n out.push({\n fieldPath: field.path,\n slotId: \"field\",\n enabledIf: field.enabledIf,\n });\n }\n // Keep in sync with `FIELD_CONTEXT_CAPABLE_TYPES` in @pipe0/base\n // (get-pipes-form-config.ts) and `resolveField` (form-inputs.ts).\n const supportsOptionsDef =\n field.type === \"context_select_input\" ||\n field.type === \"tagged_text_input\" ||\n field.type === \"condition_block_input\" ||\n field.type === \"field_select_input\" ||\n field.type === \"fields_select_input\" ||\n field.type === \"typed_fields_select_input\";\n if (supportsOptionsDef && \"optionsDef\" in field && field.optionsDef) {\n const optionsDef = field.optionsDef as EnabledSlot[\"optionsDef\"] & {\n enabledIf?: EnabledIf;\n };\n if (optionsDef?.enabledIf) {\n out.push({\n fieldPath: field.path,\n slotId: \"suggestions\",\n enabledIf: optionsDef.enabledIf,\n optionsDef,\n });\n } else {\n // Even without a sub-feature enabledIf, we still need to track\n // this field as having a fetchable suggestions slot — it's\n // always enabled and the fetch fires on every value change.\n out.push({\n fieldPath: field.path,\n slotId: \"suggestions\",\n enabledIf: () => ({ disabled: false }),\n optionsDef,\n });\n }\n }\n }\n }\n }\n return out;\n}\n\nfunction getPathValue(obj: unknown, path: string): unknown {\n const parts = path.split(\".\");\n let cur: any = obj;\n for (const part of parts) {\n if (cur == null) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\nfunction evalSlot(slot: EnabledSlot, payload: unknown): EnabledResult {\n return slot.enabledIf(payload);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useFormCore<T extends FieldValues>(\n options: UseFormCoreOptions<T>,\n): UseFormCoreReturn<T> {\n const {\n schema,\n publicKey,\n defaultValues,\n formConfig,\n resolvers,\n pipeOrSearchId,\n kind,\n setStore,\n environment = \"production\",\n scopes,\n teamId,\n } = options;\n\n // Stable signature for `scopes` so its identity doesn't churn the effect\n // dep across renders when callers pass a fresh array literal each time.\n const scopesKey = useMemo(() => (scopes ? JSON.stringify(scopes) : \"\"), [scopes]);\n\n // --- Per-resource status ---\n const [connectionsStatus, setConnectionsStatus] = useState<ResourceStatus>(\n resolvers?.getConnections ? \"loading\" : \"idle\",\n );\n const [fieldLoadStates, setFieldLoadStates] = useState<Record<string, FieldLoadState>>({});\n\n // --- Form ---\n const form = useForm<T>({\n resolver: standardSchemaResolver(schema),\n defaultValues: defaultValues as any,\n });\n\n // --- Helpers ---\n const mergeStore = useCallback(\n (incoming: FormStore) => setStore((old) => mergeFormStores(old, incoming)),\n [setStore],\n );\n\n // Latest-ref for formConfig: Effect 1 reads it for the connector prefill but\n // must not depend on it (formConfig rebuilds on every store merge, which\n // would re-fire the connections fetch in a loop).\n const formConfigRef = useRef(formConfig);\n formConfigRef.current = formConfig;\n\n // --- Effect 1: Load connections ---\n useEffect(() => {\n if (!resolvers?.getConnections) {\n setConnectionsStatus(\"idle\");\n return;\n }\n\n let cancelled = false;\n setConnectionsStatus(\"loading\");\n\n Promise.resolve(resolvers.getConnections({ id: pipeOrSearchId, environment }))\n .then((conns) => {\n if (cancelled) return;\n\n mergeStore({\n field_options: {\n connections: {\n options: conns.map((c) => ({\n label: c.public_id,\n value: c.public_id,\n widgets: {\n provider_logo: { provider: c.provider as ProviderName },\n },\n })),\n },\n },\n });\n\n // Prefill default connections into an EMPTY required connector. A\n // stored payload with a required connector always carries >=1\n // connection (min-1 validation), so an empty required connector\n // implies a fresh create form — prefilling never overwrites a\n // user's or stored choice. Runs once per connections load, so a\n // user clearing the field afterwards is not fought.\n const connectorField = formConfigRef.current\n .flatMap((section) => section.groups.flatMap((group) => group.fields))\n .map((field) => field as GeneratedInputMeta)\n .find((field) => field.type === \"connector_input\");\n if (\n connectorField?.type === \"connector_input\" &&\n connectorField.connectorMode === \"required\"\n ) {\n const current = form.getValues(connectorField.path as any) as\n | { connections?: { connection: string }[] }\n | null\n | undefined;\n const isEmpty = current == null || (current.connections?.length ?? 0) === 0;\n const defaults = conns.filter((c) => c.is_default);\n if (isEmpty && defaults.length > 0) {\n form.setValue(\n connectorField.path as any,\n {\n strategy: \"first\",\n connections: defaults.map((c) => ({\n type: \"vault\",\n connection: joinConnectionString({ provider: c.provider, id: c.public_id }),\n })),\n } as any,\n // Not dirtying: a freshly opened form with a prefilled default\n // should still count as pristine.\n { shouldDirty: false, shouldValidate: false },\n );\n }\n }\n\n setConnectionsStatus(\"ready\");\n })\n .catch(() => {\n if (!cancelled) setConnectionsStatus(\"error\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [pipeOrSearchId, environment, resolvers?.getConnections, mergeStore]);\n\n // --- Curried secrets search ---\n // Each keystroke in the reference picker fires this with the latest query.\n // The picker handles debounce + race-correctness; here we just bundle in\n // form-level args (environment / scopes / teamId) and call the resolver.\n // No caching: every call hits the resolver. Returns [] if no resolver.\n const getSecretsResolver = resolvers?.getSecrets;\n const searchSecrets = useCallback(\n async (query: string): Promise<SecretSuggestion[]> => {\n if (!getSecretsResolver) return [];\n return Promise.resolve(getSecretsResolver({ query, environment, scopes, teamId }));\n },\n // `scopesKey` is the stable serialization of `scopes`; depending on the\n // raw array would churn the callback identity on every parent render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [getSecretsResolver, environment, scopesKey, teamId],\n );\n\n // --- Curried constants search ---\n // Mirrors `searchSecrets` exactly. Same per-keystroke contract, same\n // form-level arg bundling. Returns [] when no resolver is wired.\n const getConstantsResolver = resolvers?.getConstants;\n const searchConstants = useCallback(\n async (query: string): Promise<ConstantSuggestion[]> => {\n if (!getConstantsResolver) return [];\n return Promise.resolve(getConstantsResolver({ query, environment, scopes, teamId }));\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [getConstantsResolver, environment, scopesKey, teamId],\n );\n\n // --- Curried field-context search ---\n // Per-keystroke server-side option search for context selects. Mirrors\n // `searchSecrets`: bundles the LIVE payload (`form.getValues()`, never a\n // stale watched snapshot) plus the pipe/search id, and returns ONE field's\n // options directly — never merged into the shared store, since a keystroke\n // must not mutate global state.\n const getFieldContextResolver = resolvers?.getFieldContext;\n const searchFieldContext = useCallback(\n async (fieldPath: string, query: string): Promise<StoreOption[]> => {\n if (!getFieldContextResolver) return [];\n const idKey = kind === \"search\" ? \"search_id\" : kind === \"effect\" ? \"effect_id\" : \"pipe_id\";\n const incoming = await Promise.resolve(\n getFieldContextResolver({\n fieldPath,\n query,\n payload: {\n ...(form.getValues() as Record<string, unknown>),\n [idKey]: pipeOrSearchId,\n },\n }),\n );\n return incoming?.field_options?.[fieldPath]?.options ?? [];\n },\n [getFieldContextResolver, kind, pipeOrSearchId, form],\n );\n\n // --- Effect 2: Evaluate enabledIf slots + drive context_select fetching ---\n const slots = useMemo(() => collectEnabledSlots(formConfig), [formConfig]);\n\n // Subscribe to all form value changes so resolvers re-evaluate.\n const watchedValues = form.watch();\n\n // Evaluate every slot against the current payload. Keep the result\n // serialized so the effect's deps stay stable across reference-identical\n // re-renders.\n const slotResults = useMemo(() => {\n const out: Record<string, EnabledResult> = {};\n for (const slot of slots) {\n out[`${slot.fieldPath}::${slot.slotId}`] = evalSlot(slot, watchedValues);\n }\n return out;\n }, [slots, watchedValues]);\n const slotResultsSig = useMemo(() => JSON.stringify(slotResults), [slotResults]);\n\n // Track previous results so we can detect enabled→disabled transitions\n // (drives field-value clearing) and last-fired fetch signatures.\n const prevResultsRef = useRef<Record<string, EnabledResult>>({});\n const lastFiredSigRef = useRef<Record<string, string>>({});\n\n useEffect(() => {\n if (slots.length === 0) return;\n\n const nextLoadStates: Record<string, FieldLoadState> = {\n ...fieldLoadStates,\n };\n\n // Group slot results by field path for the load-state map.\n const byField: Record<string, { field?: EnabledResult; suggestions?: EnabledResult }> = {};\n for (const slot of slots) {\n const r = slotResults[`${slot.fieldPath}::${slot.slotId}`];\n if (r == null) continue;\n byField[slot.fieldPath] ??= {};\n byField[slot.fieldPath][slot.slotId] = r;\n }\n\n for (const slot of slots) {\n const key = `${slot.fieldPath}::${slot.slotId}`;\n const r = slotResults[key];\n if (r == null) continue;\n const prev = prevResultsRef.current[key];\n\n // Field-slot only: on enabled→disabled, reset the value to the field's\n // configured default (from the seeded defaultValues) rather than `\"\"`.\n // `\"\"` is invalid for non-string fields — e.g. a gated boolean fails\n // `z.boolean()` and a nullable int's `null` default would coerce to `0`.\n // Fall back to `\"\"` only when there's NO default at the path, so explicit\n // `null`/`false` defaults survive (this resets an optional int back to\n // empty when its gate turns off).\n if (slot.slotId === \"field\" && r.disabled && prev != null && !prev.disabled) {\n const def = getPathValue(defaultValues, slot.fieldPath);\n form.setValue(slot.fieldPath as any, (def === undefined ? \"\" : def) as any, {\n shouldDirty: true,\n shouldTouch: true,\n });\n }\n\n // Suggestions-slot: drive the existing context_select_input fetch.\n if (slot.slotId === \"suggestions\" && resolvers?.getFieldContext) {\n // If suggestions are gated, skip the fetch and reset dedupe so\n // re-enabling re-fires.\n if (r.disabled) {\n delete lastFiredSigRef.current[slot.fieldPath];\n continue;\n }\n\n // Resolve the connection for the legacy `requires.connection`\n // (still needed to pick the right secret for the fetch).\n const watchedConnections = (\n watchedValues as {\n connector?: { connections?: { connection?: string }[] };\n }\n )?.connector?.connections;\n const required = slot.optionsDef?.requires;\n\n let connectionId: string | undefined;\n if (required?.connection) {\n const match = watchedConnections?.find((c) =>\n c?.connection?.startsWith(`${required.connection}_`),\n );\n connectionId = match?.connection;\n // No matching connection — can't fetch, even if enabledIf passed.\n if (!connectionId) continue;\n }\n\n // Per-field signature for dedupe. It also scopes the handlers below:\n // a settled fetch only applies while it is still the LAST one fired\n // for its field. Effect re-runs (slots identity churn, sig changes)\n // must NOT invalidate an in-flight fetch — the dedupe ref survives\n // re-runs, so a lifetime-cancelled fetch would never be refired and\n // the field would spin forever.\n const perFieldSig = JSON.stringify({\n connectionId,\n deps: required?.fields?.map((d) => getPathValue(watchedValues, d)) ?? [],\n });\n if (lastFiredSigRef.current[slot.fieldPath] === perFieldSig) continue;\n lastFiredSigRef.current[slot.fieldPath] = perFieldSig;\n const isCurrent = () => lastFiredSigRef.current[slot.fieldPath] === perFieldSig;\n\n // Mark loading for this field's fetch.\n const existing = nextLoadStates[slot.fieldPath];\n nextLoadStates[slot.fieldPath] = {\n status: \"loading\",\n disabled: existing?.disabled ?? false,\n disabledReason: existing?.disabledReason,\n suggestionsDisabled: false,\n suggestionsDisabledReason: undefined,\n };\n\n const idKey = kind === \"search\" ? \"search_id\" : kind === \"effect\" ? \"effect_id\" : \"pipe_id\";\n\n Promise.resolve(\n resolvers.getFieldContext({\n fieldPath: slot.fieldPath,\n query: \"\",\n payload: {\n ...(watchedValues as Record<string, unknown>),\n [idKey]: pipeOrSearchId,\n },\n }),\n )\n .then((incoming: FormStore | undefined) => {\n if (!isCurrent()) return;\n // A resolver may resolve with `undefined` on transport errors\n // (openapi-fetch RESOLVES with `{ error }` instead of rejecting).\n // Merging undefined would throw inside the setStore updater — i.e.\n // during render, past this chain's .catch — so route it there.\n if (!incoming?.field_options) {\n throw new Error(\"Empty field-context response\");\n }\n mergeStore(incoming);\n setFieldLoadStates((s) => ({\n ...s,\n [slot.fieldPath]: {\n ...(s[slot.fieldPath] ?? {\n disabled: false,\n suggestionsDisabled: false,\n }),\n status: \"ready\",\n },\n }));\n })\n .catch(() => {\n if (!isCurrent()) return;\n setFieldLoadStates((s) => ({\n ...s,\n [slot.fieldPath]: {\n ...(s[slot.fieldPath] ?? {\n disabled: false,\n suggestionsDisabled: false,\n }),\n status: \"error\",\n },\n }));\n });\n }\n }\n\n // Reflect the latest enabledIf evaluation into fieldLoadStates so the\n // adapters and field-wrapper can render the disabled state.\n for (const fieldPath of Object.keys(byField)) {\n const { field: fieldR, suggestions: sugR } = byField[fieldPath]!;\n const prevState = nextLoadStates[fieldPath];\n nextLoadStates[fieldPath] = {\n status: prevState?.status ?? \"idle\",\n disabled: fieldR == null ? (prevState?.disabled ?? false) : fieldR.disabled,\n disabledReason: fieldR == null ? prevState?.disabledReason : fieldR.message,\n suggestionsDisabled:\n sugR == null ? (prevState?.suggestionsDisabled ?? false) : sugR.disabled,\n suggestionsDisabledReason:\n sugR == null ? prevState?.suggestionsDisabledReason : sugR.message,\n };\n }\n\n setFieldLoadStates(nextLoadStates);\n prevResultsRef.current = slotResults;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slotResultsSig, slots, pipeOrSearchId]);\n\n // --- Sections & fields ---\n const isSubmitted = form.formState.isSubmitted;\n const formErrors = form.formState.errors;\n\n // `watchedValues` and `formErrors` must be in the dep array: field handles\n // bake `form.getValues(path)` snapshots into `field.value` / textareaProps /\n // selectedValue, and adapters render those as controlled inputs. Without\n // these deps, the memo never refreshes on keystrokes and the controlled\n // inputs snap back to their stale snapshot — typing/selecting appears to\n // do nothing. See FieldRenderer's memo comment for the intended contract.\n const sections = useMemo(\n () =>\n buildSectionHandles(formConfig, form as any, publicKey, {\n fieldLoadStates,\n searchSecrets,\n searchConstants,\n searchFieldContext,\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n formConfig,\n form,\n publicKey,\n fieldLoadStates,\n isSubmitted,\n searchSecrets,\n searchConstants,\n searchFieldContext,\n watchedValues,\n formErrors,\n ],\n );\n\n const fields = useMemo(\n () => sections.flatMap((s) => s.groups.flatMap((g) => g.fields)),\n [sections],\n );\n\n const { fieldLoaderErrors, loadingFieldLoaders, hasFieldLoaderError, isFieldLoaderLoading } =\n useMemo(() => {\n const errors: Record<string, FieldLoadState> = {};\n const loading: Record<string, FieldLoadState> = {};\n for (const [path, state] of Object.entries(fieldLoadStates)) {\n if (state.status === \"error\") errors[path] = state;\n else if (state.status === \"loading\") loading[path] = state;\n }\n return {\n fieldLoaderErrors: errors,\n loadingFieldLoaders: loading,\n hasFieldLoaderError: Object.keys(errors).length > 0,\n isFieldLoaderLoading: Object.keys(loading).length > 0,\n };\n }, [fieldLoadStates]);\n\n return {\n connectionsStatus,\n fieldLoadStates,\n fieldLoaderErrors,\n loadingFieldLoaders,\n hasFieldLoaderError,\n isFieldLoaderLoading,\n form,\n sections,\n fields,\n reset: form.reset,\n };\n}\n"],"mappings":"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"}