@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.
- package/CHANGELOG.md +18 -0
- package/dist/components/defaults/adapters/context-select-input.mjs +20 -1
- package/dist/components/defaults/adapters/context-select-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/field-select-input.mjs +33 -0
- package/dist/components/defaults/adapters/field-select-input.mjs.map +1 -0
- package/dist/components/defaults/adapters/index.d.mts.map +1 -1
- package/dist/components/defaults/adapters/index.mjs +4 -0
- package/dist/components/defaults/adapters/index.mjs.map +1 -1
- package/dist/components/defaults/adapters/pipes-run-if-input.mjs +53 -5
- package/dist/components/defaults/adapters/pipes-run-if-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/typed-fields-select-input.mjs +48 -0
- package/dist/components/defaults/adapters/typed-fields-select-input.mjs.map +1 -0
- package/dist/components/internal/combobox/suggest-combobox.mjs +35 -6
- package/dist/components/internal/combobox/suggest-combobox.mjs.map +1 -1
- package/dist/hooks/use-effect-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-effect-catalog-table.mjs +18 -11
- package/dist/hooks/use-effect-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-form-core.d.mts.map +1 -1
- package/dist/hooks/use-form-core.mjs +26 -8
- package/dist/hooks/use-form-core.mjs.map +1 -1
- package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
- package/dist/hooks/use-pipe-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-pipe-catalog-table.mjs +20 -16
- package/dist/hooks/use-pipe-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-search-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-search-catalog-table.mjs +20 -13
- package/dist/hooks/use-search-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-searches-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-searches-catalog-table.mjs +20 -13
- package/dist/hooks/use-searches-catalog-table.mjs.map +1 -1
- package/dist/types/field-props.d.mts +35 -2
- package/dist/types/field-props.d.mts.map +1 -1
- package/dist/utils/build-section-handlers.mjs +21 -2
- package/dist/utils/build-section-handlers.mjs.map +1 -1
- package/dist/utils/catalog-helpers.mjs +10 -1
- package/dist/utils/catalog-helpers.mjs.map +1 -1
- 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
|
-
|
|
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 (
|
|
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":";;;;;;;
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
33
|
-
|
|
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 {
|
|
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":";;
|
|
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 (
|
|
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 (
|
|
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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"}
|