@pipe0/react 0.0.2 → 0.0.4
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 +14 -0
- package/dist/components/defaults/adapters/key-value-list-input.mjs +3 -1
- package/dist/components/defaults/adapters/key-value-list-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/prompt-input.mjs +1 -0
- package/dist/components/defaults/adapters/prompt-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/tagged-text-input.mjs +1 -0
- package/dist/components/defaults/adapters/tagged-text-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/template-input.mjs +1 -0
- package/dist/components/defaults/adapters/template-input.mjs.map +1 -1
- package/dist/components/internal/LiquidEditor/LiquidEditor.mjs +61 -37
- package/dist/components/internal/LiquidEditor/LiquidEditor.mjs.map +1 -1
- package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs +45 -31
- package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs.map +1 -1
- package/dist/components/internal/suggestion-menu/suggestion-menu.mjs +17 -9
- package/dist/components/internal/suggestion-menu/suggestion-menu.mjs.map +1 -1
- package/dist/components/ui/button.d.mts +2 -2
- package/dist/hooks/use-async-remote-source.mjs +93 -0
- package/dist/hooks/use-async-remote-source.mjs.map +1 -0
- package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
- package/dist/styles/pipe0-form.css +0 -48
- package/dist/types/field-props.d.mts +26 -2
- package/dist/types/field-props.d.mts.map +1 -1
- package/dist/utils/build-section-handlers.mjs +8 -3
- package/dist/utils/build-section-handlers.mjs.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -19,7 +19,7 @@ import { Plus, X } from "lucide-react";
|
|
|
19
19
|
* single legend renders once below the entire list.
|
|
20
20
|
*/
|
|
21
21
|
function KeyValueListInputAdapter(field) {
|
|
22
|
-
const { rows, addRow, removeRow, setKey, setValue, searchSecrets, meta } = field;
|
|
22
|
+
const { rows, addRow, removeRow, setKey, setValue, searchSecrets, searchConstants, meta } = field;
|
|
23
23
|
const maxItems = meta.maxItems ?? 50;
|
|
24
24
|
const canAdd = rows.length < maxItems;
|
|
25
25
|
const [ids, setIds] = useState(() => rows.map(() => crypto.randomUUID()));
|
|
@@ -50,6 +50,7 @@ function KeyValueListInputAdapter(field) {
|
|
|
50
50
|
onChange: (v) => setKey(index, v),
|
|
51
51
|
inputFields: meta.inputFields ?? [],
|
|
52
52
|
searchSecrets,
|
|
53
|
+
searchConstants,
|
|
53
54
|
placeholder: meta.keyPlaceholder ?? meta.keyLabel ?? "Key",
|
|
54
55
|
hideLegend: true
|
|
55
56
|
})
|
|
@@ -61,6 +62,7 @@ function KeyValueListInputAdapter(field) {
|
|
|
61
62
|
onChange: (v) => setValue(index, v),
|
|
62
63
|
inputFields: meta.inputFields ?? [],
|
|
63
64
|
searchSecrets,
|
|
65
|
+
searchConstants,
|
|
64
66
|
placeholder: meta.valuePlaceholder ?? meta.valueLabel ?? "Value",
|
|
65
67
|
hideLegend: true
|
|
66
68
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-value-list-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/key-value-list-input.tsx"],"sourcesContent":["import { Plus, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldLegend } from \"../../internal/field-legend.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { Button } from \"../../ui/button.js\";\n\n/**\n * Adapter for `pipesKeyValueListInput`. Each row's key and value cells use\n * `LiquidEditor` so they support `/` references (input fields + secrets +\n * constants in one picker). The kvl is used for things like HTTP headers\n * and query params where either column may legitimately reference a field\n * (e.g. a per-record `pageToken`) or a secret (e.g. an Authorization\n * value).\n *\n * The unified legend would otherwise render twice per row (once per\n * cell) — too noisy. The per-cell editors run with `hideLegend`; a\n * single legend renders once below the entire list.\n */\nexport function KeyValueListInputAdapter(field: FieldHandle<\"key_value_list_input\">) {\n const { rows, addRow, removeRow, setKey, setValue, searchSecrets, meta } = field;\n const maxItems = meta.maxItems ?? 50;\n const canAdd = rows.length < maxItems;\n\n // Stable per-row identifiers, kept in lockstep with `rows`. The form value\n // is positional — `[{key, value}, ...]` — so reordering or removing the\n // row at index N would alias N's editor onto another row's content if we\n // keyed by index. We mirror mutations through `handleAdd`/`handleRemove`,\n // and lazily pad if `rows` was extended externally (e.g. form reset).\n const [ids, setIds] = useState<string[]>(() => rows.map(() => crypto.randomUUID()));\n if (ids.length < rows.length) {\n setIds((prev) => [\n ...prev,\n ...Array.from({ length: rows.length - prev.length }, () => crypto.randomUUID()),\n ]);\n }\n\n const handleAdd = () => {\n setIds((prev) => [...prev, crypto.randomUUID()]);\n addRow();\n };\n const handleRemove = (index: number) => {\n setIds((prev) => prev.filter((_, i) => i !== index));\n removeRow(index);\n };\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-2\">\n {rows.length === 0 && (\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n {meta.keyLabel ? `No ${meta.keyLabel.toLowerCase()}s yet.` : \"No entries yet.\"}\n </p>\n )}\n {rows.map((row, index) => (\n <div key={ids[index]} className=\"pz:flex pz:items-start pz:gap-2\">\n <div className=\"pz:basis-1/3 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.key}\n onChange={(v) => setKey(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n placeholder={meta.keyPlaceholder ?? meta.keyLabel ?? \"Key\"}\n hideLegend\n />\n </div>\n <div className=\"pz:flex-1 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.value}\n onChange={(v) => setValue(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n placeholder={meta.valuePlaceholder ?? meta.valueLabel ?? \"Value\"}\n hideLegend\n />\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Remove row\"\n onClick={() => handleRemove(index)}\n >\n <X className=\"pz:size-4\" />\n </Button>\n </div>\n ))}\n {rows.length > 0 && <FieldLegend entries={[{ key: \"/\", label: \"to insert a reference\" }]} />}\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={!canAdd}>\n <Plus className=\"pz:size-4 pz:mr-1\" />\n Add {meta.keyLabel ?? \"row\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,yBAAyB,OAA4C;CACnF,MAAM,EAAE,MAAM,QAAQ,WAAW,QAAQ,UAAU,eAAe,SAAS;
|
|
1
|
+
{"version":3,"file":"key-value-list-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/key-value-list-input.tsx"],"sourcesContent":["import { Plus, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldLegend } from \"../../internal/field-legend.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { Button } from \"../../ui/button.js\";\n\n/**\n * Adapter for `pipesKeyValueListInput`. Each row's key and value cells use\n * `LiquidEditor` so they support `/` references (input fields + secrets +\n * constants in one picker). The kvl is used for things like HTTP headers\n * and query params where either column may legitimately reference a field\n * (e.g. a per-record `pageToken`) or a secret (e.g. an Authorization\n * value).\n *\n * The unified legend would otherwise render twice per row (once per\n * cell) — too noisy. The per-cell editors run with `hideLegend`; a\n * single legend renders once below the entire list.\n */\nexport function KeyValueListInputAdapter(field: FieldHandle<\"key_value_list_input\">) {\n const { rows, addRow, removeRow, setKey, setValue, searchSecrets, searchConstants, meta } = field;\n const maxItems = meta.maxItems ?? 50;\n const canAdd = rows.length < maxItems;\n\n // Stable per-row identifiers, kept in lockstep with `rows`. The form value\n // is positional — `[{key, value}, ...]` — so reordering or removing the\n // row at index N would alias N's editor onto another row's content if we\n // keyed by index. We mirror mutations through `handleAdd`/`handleRemove`,\n // and lazily pad if `rows` was extended externally (e.g. form reset).\n const [ids, setIds] = useState<string[]>(() => rows.map(() => crypto.randomUUID()));\n if (ids.length < rows.length) {\n setIds((prev) => [\n ...prev,\n ...Array.from({ length: rows.length - prev.length }, () => crypto.randomUUID()),\n ]);\n }\n\n const handleAdd = () => {\n setIds((prev) => [...prev, crypto.randomUUID()]);\n addRow();\n };\n const handleRemove = (index: number) => {\n setIds((prev) => prev.filter((_, i) => i !== index));\n removeRow(index);\n };\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-2\">\n {rows.length === 0 && (\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n {meta.keyLabel ? `No ${meta.keyLabel.toLowerCase()}s yet.` : \"No entries yet.\"}\n </p>\n )}\n {rows.map((row, index) => (\n <div key={ids[index]} className=\"pz:flex pz:items-start pz:gap-2\">\n <div className=\"pz:basis-1/3 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.key}\n onChange={(v) => setKey(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n searchConstants={searchConstants}\n placeholder={meta.keyPlaceholder ?? meta.keyLabel ?? \"Key\"}\n hideLegend\n />\n </div>\n <div className=\"pz:flex-1 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.value}\n onChange={(v) => setValue(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n searchConstants={searchConstants}\n placeholder={meta.valuePlaceholder ?? meta.valueLabel ?? \"Value\"}\n hideLegend\n />\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Remove row\"\n onClick={() => handleRemove(index)}\n >\n <X className=\"pz:size-4\" />\n </Button>\n </div>\n ))}\n {rows.length > 0 && <FieldLegend entries={[{ key: \"/\", label: \"to insert a reference\" }]} />}\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={!canAdd}>\n <Plus className=\"pz:size-4 pz:mr-1\" />\n Add {meta.keyLabel ?? \"row\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,yBAAyB,OAA4C;CACnF,MAAM,EAAE,MAAM,QAAQ,WAAW,QAAQ,UAAU,eAAe,iBAAiB,SAAS;CAC5F,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,SAAS,KAAK,SAAS;CAO7B,MAAM,CAAC,KAAK,UAAU,eAAyB,KAAK,UAAU,OAAO,YAAY,CAAC,CAAC;AACnF,KAAI,IAAI,SAAS,KAAK,OACpB,SAAQ,SAAS,CACf,GAAG,MACH,GAAG,MAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,KAAK,QAAQ,QAAQ,OAAO,YAAY,CAAC,CAChF,CAAC;CAGJ,MAAM,kBAAkB;AACtB,UAAQ,SAAS,CAAC,GAAG,MAAM,OAAO,YAAY,CAAC,CAAC;AAChD,UAAQ;;CAEV,MAAM,gBAAgB,UAAkB;AACtC,UAAQ,SAAS,KAAK,QAAQ,GAAG,MAAM,MAAM,MAAM,CAAC;AACpD,YAAU,MAAM;;AAGlB,QACE,qBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAA/B;GACG,KAAK,WAAW,KACf,oBAAC,KAAD;IAAG,WAAU;cACV,KAAK,WAAW,MAAM,KAAK,SAAS,aAAa,CAAC,UAAU;IAC3D;GAEL,KAAK,KAAK,KAAK,UACd,qBAAC,OAAD;IAAsB,WAAU;cAAhC;KACE,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,OAAO,OAAO,EAAE;OACjC,aAAa,KAAK,eAAe,EAAE;OACpB;OACE;OACjB,aAAa,KAAK,kBAAkB,KAAK,YAAY;OACrD;OACA;MACE;KACN,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,SAAS,OAAO,EAAE;OACnC,aAAa,KAAK,eAAe,EAAE;OACpB;OACE;OACjB,aAAa,KAAK,oBAAoB,KAAK,cAAc;OACzD;OACA;MACE;KACN,oBAAC,QAAD;MACE,MAAK;MACL,SAAQ;MACR,MAAK;MACL,cAAW;MACX,eAAe,aAAa,MAAM;gBAElC,oBAAC,GAAD,EAAG,WAAU,aAAc;MACpB;KACL;MAhCI,IAAI,OAgCR,CACN;GACD,KAAK,SAAS,KAAK,oBAAC,aAAD,EAAa,SAAS,CAAC;IAAE,KAAK;IAAK,OAAO;IAAyB,CAAC,EAAI;GAC5F,oBAAC,OAAD,YACE,qBAAC,QAAD;IAAQ,MAAK;IAAS,SAAQ;IAAU,MAAK;IAAK,SAAS;IAAW,UAAU,CAAC;cAAjF;KACE,oBAAC,MAAD,EAAM,WAAU,qBAAsB;;KACjC,KAAK,YAAY;KACf;OACL;GACF"}
|
|
@@ -124,6 +124,7 @@ function PromptInputAdapter(field) {
|
|
|
124
124
|
}),
|
|
125
125
|
inputFields: meta.inputFields ?? [],
|
|
126
126
|
searchSecrets: field.searchSecrets,
|
|
127
|
+
searchConstants: field.searchConstants,
|
|
127
128
|
multiline: true,
|
|
128
129
|
autoGrow: true,
|
|
129
130
|
directives: (meta.supportedTags ?? ["input", "output"]).includes("output") ? ["output"] : []
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/prompt-input.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport { useFieldError } from \"../../../hooks/use-field-error.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport type { BaseFieldProps } from \"../../../types/field-props.js\";\nimport { generateRandomString } from \"../../../utils/generate-random-string.js\";\nimport { IconCheck, IconPencil, IconPlus, IconTrash } from \"../../internal/icons.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { SchemaEditor } from \"../../internal/schema-editor/SchemaEditor.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover.js\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../ui/table.js\";\n\nfunction ChangeSchemaNameForm({\n defaultName,\n existingNames,\n onSubmit,\n onClose,\n}: {\n defaultName: string;\n existingNames: string[];\n onSubmit: (newName: string) => void;\n onClose: () => void;\n}) {\n const [inputValue, setInputValue] = useState(defaultName);\n const trimmed = inputValue.trim();\n const isDuplicate = trimmed !== defaultName && existingNames.includes(trimmed);\n const isInvalid = !trimmed || isDuplicate;\n\n const handleSubmit = () => {\n if (isInvalid) return;\n if (defaultName !== trimmed) onSubmit(trimmed);\n onClose();\n };\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-1\">\n <div className=\"pz:flex pz:items-center pz:gap-2\">\n <Input\n placeholder=\"Change name…\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n minLength={1}\n aria-invalid={isDuplicate}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleSubmit}\n disabled={isInvalid}\n >\n <IconCheck width={14} height={14} />\n </Button>\n </div>\n {isDuplicate && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n A schema with that name already exists.\n </span>\n )}\n </div>\n );\n}\n\ntype PromptValue = BaseFieldProps<\"prompt_input\">[\"value\"];\n\n/**\n * Prompt editor with Tiptap rich text, @/ autocomplete, and JSON schema management.\n * For a more customized editor, provide a custom adapter via FormProvider.\n */\nconst EMPTY_PROMPT: PromptValue = { template: \"\", json_schemas: {} };\n\nexport function PromptInputAdapter(field: FieldHandle<\"prompt_input\">) {\n const meta = field.meta;\n const value = field.value ?? EMPTY_PROMPT;\n\n const templateError = useFieldError(field.form, `${field.path}.template`);\n\n const tableData = useMemo(() => Object.entries(value.json_schemas || {}), [value.json_schemas]);\n\n // Mutation callbacks read the freshest value from RHF at call time instead\n // of closing over `value` — so they depend only on stable identifiers\n // (`field.form` and `field.path`) and don't re-create on every keystroke\n // into the prompt template.\n const formRef = field.form;\n const path = field.path;\n\n const readCurrent = useCallback(\n (): PromptValue => (formRef.getValues(path as any) as PromptValue | undefined) ?? EMPTY_PROMPT,\n [formRef, path],\n );\n\n const handleChange = useCallback((v: PromptValue) => field.setValue(v), [field]);\n\n const handleAddSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (current.json_schemas?.[schemaName]) return;\n handleChange({\n ...current,\n json_schemas: {\n ...current.json_schemas,\n [schemaName]: { type: \"object\", properties: {} },\n },\n });\n },\n [handleChange, readCurrent],\n );\n\n const handleChangeSchemaName = useCallback(\n (oldName: string, newName: string) => {\n const current = readCurrent();\n const schemas = current.json_schemas ?? {};\n if (!schemas[oldName] || oldName === newName || schemas[newName]) return;\n const { [oldName]: oldSchema, ...rest } = schemas;\n handleChange({ template: current.template, json_schemas: { ...rest, [newName]: oldSchema } });\n },\n [handleChange, readCurrent],\n );\n\n const handleDeleteSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (!current.json_schemas?.[schemaName]) return;\n const { [schemaName]: _removed, ...rest } = current.json_schemas;\n handleChange({ template: current.template, json_schemas: rest });\n },\n [handleChange, readCurrent],\n );\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-3\">\n <div\n aria-invalid={!!templateError}\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n templateError && \"pz:border-destructive\",\n )}\n >\n <LiquidEditor\n value={value.template}\n onChange={(v) => handleChange({ ...value, template: v })}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n multiline\n autoGrow\n directives={\n (meta.supportedTags ?? [\"input\", \"output\"]).includes(\"output\") ? [\"output\"] : []\n }\n />\n </div>\n {templateError && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n {templateError}\n </span>\n )}\n\n {/* JSON Schemas section */}\n <div className=\"pz:flex pz:flex-col pz:gap-2\">\n <div className=\"pz:flex pz:items-center pz:justify-between\">\n <span className=\"pz:text-sm pz:font-medium\">JSON Schemas</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleAddSchema(generateRandomString(5))}\n >\n <IconPlus width={14} height={14} /> Add\n </Button>\n </div>\n {tableData.length > 0 && (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead>Schema</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tableData.map(([schemaName, schema]) => (\n <TableRow key={schemaName}>\n <TableCell className=\"pz:flex pz:items-center pz:gap-1\">\n <span>{schemaName}</span>\n <Popover>\n <PopoverTrigger\n render={\n <Button type=\"button\" variant=\"ghost\" size=\"icon\" title=\"Rename\">\n <IconPencil width={12} height={12} />\n </Button>\n }\n />\n <PopoverContent sideOffset={4} className=\"pz:w-auto pz:p-2\">\n <ChangeSchemaNameForm\n defaultName={schemaName}\n existingNames={Object.keys(value.json_schemas || {})}\n onSubmit={(newName) => handleChangeSchemaName(schemaName, newName)}\n onClose={() => {}}\n />\n </PopoverContent>\n </Popover>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleDeleteSchema(schemaName)}\n title=\"Delete schema\"\n >\n <IconTrash width={12} height={12} />\n </Button>\n </TableCell>\n <TableCell>\n <SchemaEditor\n value={schema}\n onChange={(newSchema) => {\n handleChange({\n ...value,\n json_schemas: {\n ...value.json_schemas,\n [schemaName]: newSchema,\n },\n });\n }}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prompt-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/prompt-input.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport { useFieldError } from \"../../../hooks/use-field-error.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport type { BaseFieldProps } from \"../../../types/field-props.js\";\nimport { generateRandomString } from \"../../../utils/generate-random-string.js\";\nimport { IconCheck, IconPencil, IconPlus, IconTrash } from \"../../internal/icons.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { SchemaEditor } from \"../../internal/schema-editor/SchemaEditor.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover.js\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../ui/table.js\";\n\nfunction ChangeSchemaNameForm({\n defaultName,\n existingNames,\n onSubmit,\n onClose,\n}: {\n defaultName: string;\n existingNames: string[];\n onSubmit: (newName: string) => void;\n onClose: () => void;\n}) {\n const [inputValue, setInputValue] = useState(defaultName);\n const trimmed = inputValue.trim();\n const isDuplicate = trimmed !== defaultName && existingNames.includes(trimmed);\n const isInvalid = !trimmed || isDuplicate;\n\n const handleSubmit = () => {\n if (isInvalid) return;\n if (defaultName !== trimmed) onSubmit(trimmed);\n onClose();\n };\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-1\">\n <div className=\"pz:flex pz:items-center pz:gap-2\">\n <Input\n placeholder=\"Change name…\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n minLength={1}\n aria-invalid={isDuplicate}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleSubmit}\n disabled={isInvalid}\n >\n <IconCheck width={14} height={14} />\n </Button>\n </div>\n {isDuplicate && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n A schema with that name already exists.\n </span>\n )}\n </div>\n );\n}\n\ntype PromptValue = BaseFieldProps<\"prompt_input\">[\"value\"];\n\n/**\n * Prompt editor with Tiptap rich text, @/ autocomplete, and JSON schema management.\n * For a more customized editor, provide a custom adapter via FormProvider.\n */\nconst EMPTY_PROMPT: PromptValue = { template: \"\", json_schemas: {} };\n\nexport function PromptInputAdapter(field: FieldHandle<\"prompt_input\">) {\n const meta = field.meta;\n const value = field.value ?? EMPTY_PROMPT;\n\n const templateError = useFieldError(field.form, `${field.path}.template`);\n\n const tableData = useMemo(() => Object.entries(value.json_schemas || {}), [value.json_schemas]);\n\n // Mutation callbacks read the freshest value from RHF at call time instead\n // of closing over `value` — so they depend only on stable identifiers\n // (`field.form` and `field.path`) and don't re-create on every keystroke\n // into the prompt template.\n const formRef = field.form;\n const path = field.path;\n\n const readCurrent = useCallback(\n (): PromptValue => (formRef.getValues(path as any) as PromptValue | undefined) ?? EMPTY_PROMPT,\n [formRef, path],\n );\n\n const handleChange = useCallback((v: PromptValue) => field.setValue(v), [field]);\n\n const handleAddSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (current.json_schemas?.[schemaName]) return;\n handleChange({\n ...current,\n json_schemas: {\n ...current.json_schemas,\n [schemaName]: { type: \"object\", properties: {} },\n },\n });\n },\n [handleChange, readCurrent],\n );\n\n const handleChangeSchemaName = useCallback(\n (oldName: string, newName: string) => {\n const current = readCurrent();\n const schemas = current.json_schemas ?? {};\n if (!schemas[oldName] || oldName === newName || schemas[newName]) return;\n const { [oldName]: oldSchema, ...rest } = schemas;\n handleChange({ template: current.template, json_schemas: { ...rest, [newName]: oldSchema } });\n },\n [handleChange, readCurrent],\n );\n\n const handleDeleteSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (!current.json_schemas?.[schemaName]) return;\n const { [schemaName]: _removed, ...rest } = current.json_schemas;\n handleChange({ template: current.template, json_schemas: rest });\n },\n [handleChange, readCurrent],\n );\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-3\">\n <div\n aria-invalid={!!templateError}\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n templateError && \"pz:border-destructive\",\n )}\n >\n <LiquidEditor\n value={value.template}\n onChange={(v) => handleChange({ ...value, template: v })}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n multiline\n autoGrow\n directives={\n (meta.supportedTags ?? [\"input\", \"output\"]).includes(\"output\") ? [\"output\"] : []\n }\n />\n </div>\n {templateError && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n {templateError}\n </span>\n )}\n\n {/* JSON Schemas section */}\n <div className=\"pz:flex pz:flex-col pz:gap-2\">\n <div className=\"pz:flex pz:items-center pz:justify-between\">\n <span className=\"pz:text-sm pz:font-medium\">JSON Schemas</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleAddSchema(generateRandomString(5))}\n >\n <IconPlus width={14} height={14} /> Add\n </Button>\n </div>\n {tableData.length > 0 && (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead>Schema</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tableData.map(([schemaName, schema]) => (\n <TableRow key={schemaName}>\n <TableCell className=\"pz:flex pz:items-center pz:gap-1\">\n <span>{schemaName}</span>\n <Popover>\n <PopoverTrigger\n render={\n <Button type=\"button\" variant=\"ghost\" size=\"icon\" title=\"Rename\">\n <IconPencil width={12} height={12} />\n </Button>\n }\n />\n <PopoverContent sideOffset={4} className=\"pz:w-auto pz:p-2\">\n <ChangeSchemaNameForm\n defaultName={schemaName}\n existingNames={Object.keys(value.json_schemas || {})}\n onSubmit={(newName) => handleChangeSchemaName(schemaName, newName)}\n onClose={() => {}}\n />\n </PopoverContent>\n </Popover>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleDeleteSchema(schemaName)}\n title=\"Delete schema\"\n >\n <IconTrash width={12} height={12} />\n </Button>\n </TableCell>\n <TableCell>\n <SchemaEditor\n value={schema}\n onChange={(newSchema) => {\n handleChange({\n ...value,\n json_schemas: {\n ...value.json_schemas,\n [schemaName]: newSchema,\n },\n });\n }}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -79,6 +79,7 @@ function TaggedTextInputAdapter(field) {
|
|
|
79
79
|
onChange: (v) => field.setValue(v),
|
|
80
80
|
inputFields: meta.inputFields ?? [],
|
|
81
81
|
searchSecrets: field.searchSecrets,
|
|
82
|
+
searchConstants: field.searchConstants,
|
|
82
83
|
placeholder: meta.placeholder,
|
|
83
84
|
multiline: meta.multiline,
|
|
84
85
|
expectedFieldType: meta.expectedTagType,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tagged-text-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-text-input.tsx"],"sourcesContent":["import {\n autoUpdate,\n flip,\n offset,\n shift,\n size,\n useDismiss,\n useFloating,\n useInteractions,\n useTransitionStyles,\n} from \"@floating-ui/react\";\nimport type { StoreOption, TaggedTextMeta } from \"@pipe0/base\";\nimport { ChevronDown } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Adapter for `pipesTaggedTextInput`. Renders a tiptap-backed editor that\n * supports `/` to open a unified reference picker (input fields filtered\n * by `expectedTagType`, secrets, constants).\n *\n * When `meta.optionsDef` is present, the input also gains a value-suggestions\n * popover (e.g. listing the user's existing pipe0 sheets). The popover opens\n * on focus, hides automatically when the user is composing a `{{ tag }}`,\n * and is gated by `meta.optionsDef.enabledIf` (sub-feature gate — the input\n * itself stays editable in every state). Picking an option REPLACES the\n * input value with the option's value.\n */\nexport function TaggedTextInputAdapter(field: FieldHandle<\"tagged_text_input\">) {\n const meta = field.meta as TaggedTextMeta;\n const hasError = !!field.error;\n const hasSuggestions = !!meta.optionsDef;\n\n const [open, setOpen] = useState(false);\n\n // Hide the popover whenever the user is composing a `{{ tag }}` — the\n // LiquidEditor SuggestionMenu owns that interaction.\n const valueIsTag = useMemo(() => /\\{\\{[^}]*\\}?\\}?/.test(field.value ?? \"\"), [field.value]);\n\n const hasQuery = (field.value ?? \"\").trim().length > 0;\n\n const popoverShow = hasSuggestions && open && !valueIsTag;\n\n // Floating UI reads positioning straight from the wrapper element via\n // `refs.setReference`; `whileElementsMounted: autoUpdate` subscribes to the\n // ancestor scroll/resize that actually matters (no global window listener,\n // no React state for the rect).\n const { refs, floatingStyles, context } = useFloating({\n open: popoverShow,\n onOpenChange: (next) => {\n if (!next) setOpen(false);\n },\n placement: \"bottom-start\",\n whileElementsMounted: autoUpdate,\n middleware: [\n offset(4),\n flip({ mainAxis: true, crossAxis: false }),\n shift(),\n size({\n apply({ elements, rects }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n elements.floating.style.maxWidth = \"32rem\";\n },\n }),\n ],\n });\n\n const { isMounted, styles: transitionStyles } = useTransitionStyles(context);\n const dismiss = useDismiss(context, {\n outsidePress: true,\n outsidePressEvent: \"mousedown\",\n });\n const { getFloatingProps } = useInteractions([dismiss]);\n\n // Filter options client-side by current input text. Empty input → all options.\n const visibleOptions = useMemo(() => {\n const q = (field.value ?? \"\").trim().toLowerCase();\n if (!q) return field.options;\n return field.options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [field.options, field.value]);\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n ref={refs.setReference}\n className={cn(\n \"pz:flex pz:rounded-lg pz:border pz:border-input pz:bg-transparent\",\n meta.multiline ? \"pz:items-start\" : \"pz:items-center\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n onFocusCapture={() => {\n if (hasSuggestions) setOpen(true);\n }}\n onBlurCapture={(e) => {\n // Only close when focus leaves both the wrapper AND the popover.\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) return;\n if (next && document.querySelector(\"[data-p0='tagged-suggestions']\")?.contains(next)) {\n return;\n }\n setOpen(false);\n }}\n >\n <div className=\"pz:flex-1 pz:min-w-0\">\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n placeholder={meta.placeholder}\n multiline={meta.multiline}\n expectedFieldType={meta.expectedTagType}\n directives={[]}\n />\n </div>\n {hasSuggestions && (\n <div\n className={cn(\n \"pz:flex pz:items-center pz:gap-1 pz:pr-2 pz:text-muted-foreground\",\n meta.multiline && \"pz:pt-1\",\n )}\n >\n <ChevronDown className=\"pz:size-4 pz:pointer-events-none\" aria-hidden=\"true\" />\n </div>\n )}\n </div>\n\n {hasSuggestions && isMounted && (\n <div\n ref={refs.setFloating}\n data-p0=\"tagged-suggestions\"\n style={{ ...floatingStyles, ...transitionStyles, zIndex: 50 }}\n {...getFloatingProps({\n // Prevent stealing focus from the editor when the user clicks an option.\n onMouseDown: (e) => e.preventDefault(),\n })}\n className=\"pz:relative pz:rounded-lg pz:border pz:border-input pz:bg-popover pz:text-popover-foreground pz:shadow-md pz:overflow-hidden\"\n >\n {field.pending && visibleOptions.length > 0 && (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: not relevant\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 <SuggestionsBody\n suggestionsDisabled={field.suggestionsDisabled}\n reason={field.suggestionsDisabledReason}\n pending={field.pending}\n options={visibleOptions}\n hasQuery={hasQuery}\n onPick={(value) => {\n field.setValue(value);\n setOpen(false);\n }}\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction SuggestionsBody({\n suggestionsDisabled,\n reason,\n pending,\n options,\n hasQuery,\n onPick,\n}: {\n suggestionsDisabled: boolean;\n reason?: string;\n pending: boolean;\n options: StoreOption[];\n hasQuery: boolean;\n onPick: (value: string) => void;\n}) {\n if (suggestionsDisabled) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {reason ?? \"Suggestions unavailable.\"}\n </div>\n );\n }\n if (pending && options.length === 0) {\n // Initial load — render a visible spinner so the popover has feedback.\n return (\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 }\n if (options.length === 0) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {hasQuery ? \"No matches\" : \"No options available\"}\n </div>\n );\n }\n return (\n <ul\n className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:transition-opacity\"\n style={pending ? { opacity: 0.5, pointerEvents: \"none\" } : undefined}\n >\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n role=\"option\"\n className=\"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer pz:hover:bg-accent pz:hover:text-accent-foreground\"\n onClick={() => onPick(opt.value)}\n >\n <WidgetStrip widgets={opt.widgets} />\n <span>{opt.label}</span>\n </button>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tagged-text-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-text-input.tsx"],"sourcesContent":["import {\n autoUpdate,\n flip,\n offset,\n shift,\n size,\n useDismiss,\n useFloating,\n useInteractions,\n useTransitionStyles,\n} from \"@floating-ui/react\";\nimport type { StoreOption, TaggedTextMeta } from \"@pipe0/base\";\nimport { ChevronDown } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Adapter for `pipesTaggedTextInput`. Renders a tiptap-backed editor that\n * supports `/` to open a unified reference picker (input fields filtered\n * by `expectedTagType`, secrets, constants).\n *\n * When `meta.optionsDef` is present, the input also gains a value-suggestions\n * popover (e.g. listing the user's existing pipe0 sheets). The popover opens\n * on focus, hides automatically when the user is composing a `{{ tag }}`,\n * and is gated by `meta.optionsDef.enabledIf` (sub-feature gate — the input\n * itself stays editable in every state). Picking an option REPLACES the\n * input value with the option's value.\n */\nexport function TaggedTextInputAdapter(field: FieldHandle<\"tagged_text_input\">) {\n const meta = field.meta as TaggedTextMeta;\n const hasError = !!field.error;\n const hasSuggestions = !!meta.optionsDef;\n\n const [open, setOpen] = useState(false);\n\n // Hide the popover whenever the user is composing a `{{ tag }}` — the\n // LiquidEditor SuggestionMenu owns that interaction.\n const valueIsTag = useMemo(() => /\\{\\{[^}]*\\}?\\}?/.test(field.value ?? \"\"), [field.value]);\n\n const hasQuery = (field.value ?? \"\").trim().length > 0;\n\n const popoverShow = hasSuggestions && open && !valueIsTag;\n\n // Floating UI reads positioning straight from the wrapper element via\n // `refs.setReference`; `whileElementsMounted: autoUpdate` subscribes to the\n // ancestor scroll/resize that actually matters (no global window listener,\n // no React state for the rect).\n const { refs, floatingStyles, context } = useFloating({\n open: popoverShow,\n onOpenChange: (next) => {\n if (!next) setOpen(false);\n },\n placement: \"bottom-start\",\n whileElementsMounted: autoUpdate,\n middleware: [\n offset(4),\n flip({ mainAxis: true, crossAxis: false }),\n shift(),\n size({\n apply({ elements, rects }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n elements.floating.style.maxWidth = \"32rem\";\n },\n }),\n ],\n });\n\n const { isMounted, styles: transitionStyles } = useTransitionStyles(context);\n const dismiss = useDismiss(context, {\n outsidePress: true,\n outsidePressEvent: \"mousedown\",\n });\n const { getFloatingProps } = useInteractions([dismiss]);\n\n // Filter options client-side by current input text. Empty input → all options.\n const visibleOptions = useMemo(() => {\n const q = (field.value ?? \"\").trim().toLowerCase();\n if (!q) return field.options;\n return field.options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [field.options, field.value]);\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n ref={refs.setReference}\n className={cn(\n \"pz:flex pz:rounded-lg pz:border pz:border-input pz:bg-transparent\",\n meta.multiline ? \"pz:items-start\" : \"pz:items-center\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n onFocusCapture={() => {\n if (hasSuggestions) setOpen(true);\n }}\n onBlurCapture={(e) => {\n // Only close when focus leaves both the wrapper AND the popover.\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) return;\n if (next && document.querySelector(\"[data-p0='tagged-suggestions']\")?.contains(next)) {\n return;\n }\n setOpen(false);\n }}\n >\n <div className=\"pz:flex-1 pz:min-w-0\">\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n placeholder={meta.placeholder}\n multiline={meta.multiline}\n expectedFieldType={meta.expectedTagType}\n directives={[]}\n />\n </div>\n {hasSuggestions && (\n <div\n className={cn(\n \"pz:flex pz:items-center pz:gap-1 pz:pr-2 pz:text-muted-foreground\",\n meta.multiline && \"pz:pt-1\",\n )}\n >\n <ChevronDown className=\"pz:size-4 pz:pointer-events-none\" aria-hidden=\"true\" />\n </div>\n )}\n </div>\n\n {hasSuggestions && isMounted && (\n <div\n ref={refs.setFloating}\n data-p0=\"tagged-suggestions\"\n style={{ ...floatingStyles, ...transitionStyles, zIndex: 50 }}\n {...getFloatingProps({\n // Prevent stealing focus from the editor when the user clicks an option.\n onMouseDown: (e) => e.preventDefault(),\n })}\n className=\"pz:relative pz:rounded-lg pz:border pz:border-input pz:bg-popover pz:text-popover-foreground pz:shadow-md pz:overflow-hidden\"\n >\n {field.pending && visibleOptions.length > 0 && (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: not relevant\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 <SuggestionsBody\n suggestionsDisabled={field.suggestionsDisabled}\n reason={field.suggestionsDisabledReason}\n pending={field.pending}\n options={visibleOptions}\n hasQuery={hasQuery}\n onPick={(value) => {\n field.setValue(value);\n setOpen(false);\n }}\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction SuggestionsBody({\n suggestionsDisabled,\n reason,\n pending,\n options,\n hasQuery,\n onPick,\n}: {\n suggestionsDisabled: boolean;\n reason?: string;\n pending: boolean;\n options: StoreOption[];\n hasQuery: boolean;\n onPick: (value: string) => void;\n}) {\n if (suggestionsDisabled) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {reason ?? \"Suggestions unavailable.\"}\n </div>\n );\n }\n if (pending && options.length === 0) {\n // Initial load — render a visible spinner so the popover has feedback.\n return (\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 }\n if (options.length === 0) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {hasQuery ? \"No matches\" : \"No options available\"}\n </div>\n );\n }\n return (\n <ul\n className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:transition-opacity\"\n style={pending ? { opacity: 0.5, pointerEvents: \"none\" } : undefined}\n >\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n role=\"option\"\n className=\"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer pz:hover:bg-accent pz:hover:text-accent-foreground\"\n onClick={() => onPick(opt.value)}\n >\n <WidgetStrip widgets={opt.widgets} />\n <span>{opt.label}</span>\n </button>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,uBAAuB,OAAyC;CAC9E,MAAM,OAAO,MAAM;CACnB,MAAM,WAAW,CAAC,CAAC,MAAM;CACzB,MAAM,iBAAiB,CAAC,CAAC,KAAK;CAE9B,MAAM,CAAC,MAAM,WAAW,SAAS,MAAM;CAIvC,MAAM,aAAa,cAAc,kBAAkB,KAAK,MAAM,SAAS,GAAG,EAAE,CAAC,MAAM,MAAM,CAAC;CAE1F,MAAM,YAAY,MAAM,SAAS,IAAI,MAAM,CAAC,SAAS;CAQrD,MAAM,EAAE,MAAM,gBAAgB,YAAY,YAAY;EACpD,MAPkB,kBAAkB,QAAQ,CAAC;EAQ7C,eAAe,SAAS;AACtB,OAAI,CAAC,KAAM,SAAQ,MAAM;;EAE3B,WAAW;EACX,sBAAsB;EACtB,YAAY;GACV,OAAO,EAAE;GACT,KAAK;IAAE,UAAU;IAAM,WAAW;IAAO,CAAC;GAC1C,OAAO;GACP,KAAK,EACH,MAAM,EAAE,UAAU,SAAS;AACzB,aAAS,SAAS,MAAM,WAAW,GAAG,MAAM,UAAU,MAAM;AAC5D,aAAS,SAAS,MAAM,WAAW;MAEtC,CAAC;GACH;EACF,CAAC;CAEF,MAAM,EAAE,WAAW,QAAQ,qBAAqB,oBAAoB,QAAQ;CAK5E,MAAM,EAAE,qBAAqB,gBAAgB,CAJ7B,WAAW,SAAS;EAClC,cAAc;EACd,mBAAmB;EACpB,CAAC,CACoD,CAAC;CAGvD,MAAM,iBAAiB,cAAc;EACnC,MAAM,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,aAAa;AAClD,MAAI,CAAC,EAAG,QAAO,MAAM;AACrB,SAAO,MAAM,QAAQ,QAClB,MAAM,EAAE,MAAM,aAAa,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC,SAAS,EAAE,CAC9E;IACA,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC;AAEhC,QACE,qBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAA/B,CACE,qBAAC,OAAD;GACE,KAAK,KAAK;GACV,WAAW,GACT,qEACA,KAAK,YAAY,mBAAmB,mBACpC,YAAY,2BACb;GACD,gBAAc,YAAY;GAC1B,sBAAsB;AACpB,QAAI,eAAgB,SAAQ,KAAK;;GAEnC,gBAAgB,MAAM;IAEpB,MAAM,OAAO,EAAE;AACf,QAAI,QAAQ,EAAE,cAAc,SAAS,KAAK,CAAE;AAC5C,QAAI,QAAQ,SAAS,cAAc,iCAAiC,EAAE,SAAS,KAAK,CAClF;AAEF,YAAQ,MAAM;;aAlBlB,CAqBE,oBAAC,OAAD;IAAK,WAAU;cACb,oBAAC,cAAD;KACE,OAAO,MAAM,SAAS;KACtB,WAAW,MAAM,MAAM,SAAS,EAAE;KAClC,aAAa,KAAK,eAAe,EAAE;KACnC,eAAe,MAAM;KACrB,iBAAiB,MAAM;KACvB,aAAa,KAAK;KAClB,WAAW,KAAK;KAChB,mBAAmB,KAAK;KACxB,YAAY,EAAE;KACd;IACE,GACL,kBACC,oBAAC,OAAD;IACE,WAAW,GACT,qEACA,KAAK,aAAa,UACnB;cAED,oBAAC,aAAD;KAAa,WAAU;KAAmC,eAAY;KAAS;IAC3E,EAEJ;MAEL,kBAAkB,aACjB,qBAAC,OAAD;GACE,KAAK,KAAK;GACV,WAAQ;GACR,OAAO;IAAE,GAAG;IAAgB,GAAG;IAAkB,QAAQ;IAAI;GAC7D,GAAI,iBAAiB,EAEnB,cAAc,MAAM,EAAE,gBAAgB,EACvC,CAAC;GACF,WAAU;aARZ,CAUG,MAAM,WAAW,eAAe,SAAS,KAExC,oBAAC,QAAD;IACE,cAAW;IACX,WAAU;IACV,GAEJ,oBAAC,iBAAD;IACE,qBAAqB,MAAM;IAC3B,QAAQ,MAAM;IACd,SAAS,MAAM;IACf,SAAS;IACC;IACV,SAAS,UAAU;AACjB,WAAM,SAAS,MAAM;AACrB,aAAQ,MAAM;;IAEhB,EACE;KAEJ;;;AAIV,SAAS,gBAAgB,EACvB,qBACA,QACA,SACA,SACA,UACA,UAQC;AACD,KAAI,oBACF,QACE,oBAAC,OAAD;EAAK,WAAU;YACZ,UAAU;EACP;AAGV,KAAI,WAAW,QAAQ,WAAW,EAEhC,QACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAW;EACX,WAAU;YAEV,oBAAC,QAAD,EAAM,WAAU,wIAAyI;EACrJ;AAGV,KAAI,QAAQ,WAAW,EACrB,QACE,oBAAC,OAAD;EAAK,WAAU;YACZ,WAAW,eAAe;EACvB;AAGV,QACE,oBAAC,MAAD;EACE,WAAU;EACV,OAAO,UAAU;GAAE,SAAS;GAAK,eAAe;GAAQ,GAAG;YAE1D,QAAQ,KAAK,QACZ,oBAAC,MAAD,YACE,qBAAC,UAAD;GACE,MAAK;GACL,MAAK;GACL,WAAU;GACV,eAAe,OAAO,IAAI,MAAM;aAJlC,CAME,oBAAC,aAAD,EAAa,SAAS,IAAI,SAAW,GACrC,oBAAC,QAAD,YAAO,IAAI,OAAa,EACjB;MACN,EAVI,IAAI,MAUR,CACL;EACC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/template-input.tsx"],"sourcesContent":["import type { TemplateInputMeta } from \"@pipe0/base\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Template editor with Liquid syntax support — `/` opens a unified\n * reference picker (input fields, secrets, constants). Templates cannot\n * declare outputs, so the directive picker is disabled.\n */\nexport function TemplateInputAdapter(field: FieldHandle<\"template_input\">) {\n const meta = field.meta as TemplateInputMeta;\n const hasError = !!field.error;\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n >\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n multiline\n directives={[]}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,qBAAqB,OAAsC;CACzE,MAAM,OAAO,MAAM;CACnB,MAAM,WAAW,CAAC,CAAC,MAAM;AAEzB,QACE,oBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAC7B,oBAAC,OAAD;GACE,WAAW,GACT,0EACA,YAAY,2BACb;GACD,gBAAc,YAAY;aAE1B,oBAAC,cAAD;IACE,OAAO,MAAM,SAAS;IACtB,WAAW,MAAM,MAAM,SAAS,EAAE;IAClC,aAAa,KAAK,eAAe,EAAE;IACnC,eAAe,MAAM;IACrB;IACA,YAAY,EAAE;IACd;GACE;EACF"}
|
|
1
|
+
{"version":3,"file":"template-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/template-input.tsx"],"sourcesContent":["import type { TemplateInputMeta } from \"@pipe0/base\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Template editor with Liquid syntax support — `/` opens a unified\n * reference picker (input fields, secrets, constants). Templates cannot\n * declare outputs, so the directive picker is disabled.\n */\nexport function TemplateInputAdapter(field: FieldHandle<\"template_input\">) {\n const meta = field.meta as TemplateInputMeta;\n const hasError = !!field.error;\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n >\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n multiline\n directives={[]}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,qBAAqB,OAAsC;CACzE,MAAM,OAAO,MAAM;CACnB,MAAM,WAAW,CAAC,CAAC,MAAM;AAEzB,QACE,oBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAC7B,oBAAC,OAAD;GACE,WAAW,GACT,0EACA,YAAY,2BACb;GACD,gBAAc,YAAY;aAE1B,oBAAC,cAAD;IACE,OAAO,MAAM,SAAS;IACtB,WAAW,MAAM,MAAM,SAAS,EAAE;IAClC,aAAa,KAAK,eAAe,EAAE;IACnC,eAAe,MAAM;IACrB,iBAAiB,MAAM;IACvB;IACA,YAAY,EAAE;IACd;GACE;EACF"}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.mjs";
|
|
2
2
|
import { FieldLegend } from "../field-legend.mjs";
|
|
3
|
+
import { useAsyncRemoteSource } from "../../../hooks/use-async-remote-source.mjs";
|
|
3
4
|
import { SuggestionMenu } from "../suggestion-menu/suggestion-menu.mjs";
|
|
4
5
|
import { TagChipDecoration } from "../tag-chip-decoration.mjs";
|
|
5
6
|
import { ChipEditPopover } from "./ChipEditPopover.mjs";
|
|
6
|
-
import { UnifiedReferencePicker, buildReferenceItems } from "./UnifiedReferencePicker.mjs";
|
|
7
|
-
import { useCallback, useEffect, useMemo,
|
|
7
|
+
import { UnifiedReferencePicker, buildReferenceItems, parseQuery } from "./UnifiedReferencePicker.mjs";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
8
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
10
|
import { RECORD_FIELD_TYPES } from "@pipe0/base";
|
|
10
11
|
import { EditorContent, EditorContext, useEditor } from "@tiptap/react";
|
|
11
12
|
import StarterKit from "@tiptap/starter-kit";
|
|
12
13
|
|
|
13
14
|
//#region src/components/internal/LiquidEditor/LiquidEditor.tsx
|
|
14
|
-
const SECRET_SEARCH_DEBOUNCE_MS = 150;
|
|
15
15
|
function escapeHtml(s) {
|
|
16
16
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
17
17
|
}
|
|
@@ -53,11 +53,19 @@ const OUTPUT_TEMPLATE = (fieldType) => {
|
|
|
53
53
|
* byte-identical to what the legacy `TagEditor` / `TextPromptEditor`
|
|
54
54
|
* produced. No engine, analyzer, or metadata changes.
|
|
55
55
|
*/
|
|
56
|
-
function LiquidEditor({ value, onChange, inputFields, searchSecrets,
|
|
56
|
+
function LiquidEditor({ value, onChange, inputFields, searchSecrets, searchConstants, multiline = false, autoGrow = true, directives = [], expectedFieldType, placeholder, className, editorProps, hideLegend = false }) {
|
|
57
57
|
const blockSeparator = multiline ? "\n" : "";
|
|
58
|
-
const visibleConstants = constantSuggestions ?? [];
|
|
59
58
|
const supportsOutput = directives.includes("output");
|
|
60
59
|
const hasSecretsResolver = !!searchSecrets;
|
|
60
|
+
const hasConstantsResolver = !!searchConstants;
|
|
61
|
+
const secretSource = useAsyncRemoteSource({ search: searchSecrets });
|
|
62
|
+
const constantSource = useAsyncRemoteSource({ search: searchConstants });
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
secretSource.prefetch();
|
|
65
|
+
}, [secretSource.prefetch]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
constantSource.prefetch();
|
|
68
|
+
}, [constantSource.prefetch]);
|
|
61
69
|
const [chipTarget, setChipTarget] = useState(null);
|
|
62
70
|
const tagChipExtension = useMemo(() => TagChipDecoration.configure({ onChipClick: (hit, el) => {
|
|
63
71
|
setChipTarget({
|
|
@@ -100,43 +108,55 @@ function LiquidEditor({ value, onChange, inputFields, searchSecrets, constantSug
|
|
|
100
108
|
blockSeparator
|
|
101
109
|
]);
|
|
102
110
|
const insertField = useCallback((name) => `{{ ${name} }}`, []);
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
latestQueryRef.current = query;
|
|
107
|
-
const myQuery = query;
|
|
108
|
-
const buildWith = (secrets) => buildReferenceItems(myQuery, {
|
|
111
|
+
const [referenceQuery, setReferenceQuery] = useState("");
|
|
112
|
+
const buildVisibleItems = useCallback((query) => {
|
|
113
|
+
const all = buildReferenceItems(query, {
|
|
109
114
|
inputFields,
|
|
110
|
-
secretSuggestions:
|
|
111
|
-
constantSuggestions:
|
|
115
|
+
secretSuggestions: secretSource.items,
|
|
116
|
+
constantSuggestions: constantSource.items
|
|
112
117
|
}, expectedFieldType, insertField);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
const { prefix, residual } = parseQuery(query);
|
|
119
|
+
if (!!residual.trim() || prefix !== null) return {
|
|
120
|
+
items: all,
|
|
121
|
+
overflow: {
|
|
122
|
+
field: 0,
|
|
123
|
+
secret: 0,
|
|
124
|
+
constant: 0
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const fields = all.filter((i) => i.context?.kind === "field");
|
|
128
|
+
const secrets = all.filter((i) => i.context?.kind === "secret");
|
|
129
|
+
const constants = all.filter((i) => i.context?.kind === "constant");
|
|
130
|
+
return {
|
|
131
|
+
items: [
|
|
132
|
+
...fields.slice(0, 5),
|
|
133
|
+
...secrets.slice(0, 5),
|
|
134
|
+
...constants.slice(0, 5)
|
|
135
|
+
],
|
|
136
|
+
overflow: {
|
|
137
|
+
field: Math.max(0, fields.length - 5),
|
|
138
|
+
secret: Math.max(0, secrets.length - 5),
|
|
139
|
+
constant: Math.max(0, constants.length - 5)
|
|
140
|
+
}
|
|
141
|
+
};
|
|
133
142
|
}, [
|
|
134
|
-
searchSecrets,
|
|
135
143
|
inputFields,
|
|
136
|
-
visibleConstants,
|
|
137
144
|
expectedFieldType,
|
|
138
|
-
insertField
|
|
145
|
+
insertField,
|
|
146
|
+
secretSource.items,
|
|
147
|
+
constantSource.items
|
|
148
|
+
]);
|
|
149
|
+
const referenceItems = useCallback(({ query }) => {
|
|
150
|
+
setReferenceQuery(query);
|
|
151
|
+
secretSource.ensure(query);
|
|
152
|
+
constantSource.ensure(query);
|
|
153
|
+
return buildVisibleItems(query).items;
|
|
154
|
+
}, [
|
|
155
|
+
secretSource.ensure,
|
|
156
|
+
constantSource.ensure,
|
|
157
|
+
buildVisibleItems
|
|
139
158
|
]);
|
|
159
|
+
const { items: liveReferenceItems, overflow: liveOverflow } = useMemo(() => buildVisibleItems(referenceQuery), [referenceQuery, buildVisibleItems]);
|
|
140
160
|
const directiveItems = useCallback(() => RECORD_FIELD_TYPES.map((fieldType) => ({
|
|
141
161
|
title: `output: ${fieldType}`,
|
|
142
162
|
keywords: [fieldType, "output"],
|
|
@@ -163,14 +183,18 @@ function LiquidEditor({ value, onChange, inputFields, searchSecrets, constantSug
|
|
|
163
183
|
char: "/",
|
|
164
184
|
pluginKey: "liquidEditorReferencePicker",
|
|
165
185
|
items: referenceItems,
|
|
186
|
+
liveItems: liveReferenceItems,
|
|
166
187
|
children: (props) => /* @__PURE__ */ jsx(UnifiedReferencePicker, {
|
|
167
188
|
items: props.items,
|
|
168
189
|
selectedIndex: props.selectedIndex,
|
|
169
190
|
onSelect: props.onSelect,
|
|
170
191
|
query: props.query,
|
|
171
192
|
hasSecretsResolver,
|
|
193
|
+
hasConstantsResolver,
|
|
172
194
|
inputFieldsCount: inputFields.length,
|
|
173
|
-
|
|
195
|
+
secretsPending: secretSource.pending,
|
|
196
|
+
constantsPending: constantSource.pending,
|
|
197
|
+
overflow: liveOverflow
|
|
174
198
|
})
|
|
175
199
|
}),
|
|
176
200
|
supportsOutput && /* @__PURE__ */ jsx(SuggestionMenu, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LiquidEditor.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/LiquidEditor.tsx"],"sourcesContent":["import {\n type PipesFieldDefinitionWithName,\n RECORD_FIELD_TYPES,\n type RecordFieldType,\n} from \"@pipe0/base\";\nimport type { EditorProps } from \"@tiptap/pm/view\";\nimport { EditorContent, EditorContext, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport { FieldLegend, type LegendEntry } from \"../field-legend.js\";\nimport { SuggestionMenu } from \"../suggestion-menu/suggestion-menu.js\";\nimport type { SuggestionItem } from \"../suggestion-menu/suggestion-menu-types.js\";\nimport { type ChipHit, TagChipDecoration } from \"../tag-chip-decoration.js\";\nimport { ChipEditPopover, type ChipEditTarget } from \"./ChipEditPopover.js\";\nimport {\n buildReferenceItems,\n type ReferenceContext,\n UnifiedReferencePicker,\n} from \"./UnifiedReferencePicker.js\";\n\nexport type DirectiveKind = \"output\";\n\nconst SECRET_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface LiquidEditorProps {\n value: string;\n onChange: (v: string) => void;\n inputFields: PipesFieldDefinitionWithName[];\n /**\n * Per-keystroke async searcher for secrets. Called from the `/` reference\n * picker as the user types. Calls are debounced and race-guarded — only\n * the response for the latest query is rendered.\n */\n searchSecrets?: (query: string) => Promise<SecretSuggestion[]>;\n constantSuggestions?: ConstantSuggestion[];\n multiline?: boolean;\n /**\n * When true, the editor renders within `min-h`/`max-h` bounds. When\n * false, callers control the height via `className`. Default: true.\n */\n autoGrow?: boolean;\n directives?: DirectiveKind[];\n expectedFieldType?: RecordFieldType;\n placeholder?: string;\n className?: string;\n editorProps?: EditorProps;\n /**\n * Suppress the per-editor legend. Used by `key_value_list_input` which\n * renders a single legend below the entire list rather than per-cell.\n */\n hideLegend?: boolean;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/**\n * Reconciles single- and multi-line text into Tiptap-friendly HTML.\n * - Single-line: collapses whitespace runs containing newlines into a\n * single space and renders as one paragraph.\n * - Multi-line: paragraphs separated by blank lines (`\\n\\n+`); soft\n * breaks within a paragraph render as `<br>`. Leading spaces on each\n * line are preserved with ` ` so JSON-shaped payloads round-trip\n * through the editor.\n */\nfunction textToTiptapHTML(text: string, multiline: boolean): string {\n if (!multiline) {\n return `<p>${escapeHtml(text.replace(/\\s*\\n+\\s*/g, \" \"))}</p>`;\n }\n return escapeHtml(text)\n .split(/\\n\\n+/)\n .map((block) => {\n const lines = block.split(/\\n/);\n const htmlLines = lines\n .map((line) => line.replace(/^ +/, (spaces) => \" \".repeat(spaces.length)))\n .join(\"<br>\");\n return `<p>${htmlLines}</p>`;\n })\n .join(\"\");\n}\n\nconst OUTPUT_TEMPLATE = (fieldType: RecordFieldType): string => {\n if (fieldType === \"json\") {\n return `{% output FIELD_NAME, type: \"json\", schema: \"SCHEMA_NAME\", description: \"\" %}`;\n }\n return `{% output FIELD_NAME, type: \"${fieldType}\", description: \"\" %}`;\n};\n\n/**\n * Single editor surface for every form field that accepts Liquid tags.\n * Two triggers, two cognitive categories:\n *\n * - `/` opens a unified reference picker (Fields, Secrets, Constants).\n * Prefix syntax `/f/`, `/s/`, `/c/` filters to one source. Picking\n * emits the corresponding `{{ … }}` Liquid Output.\n *\n * - `@` opens a directives picker (only when `directives.length > 0`).\n * Today the only directive is `output`, which inserts a templated\n * `{% output FIELD_NAME, type: \"...\", description: \"\" %}` block.\n *\n * Chips render via the `TagChipDecoration` extension. Click or\n * Enter/Space on a chip mounts the contextual `ChipEditPopover`.\n *\n * Backwards compat: the persisted value is plain Liquid text,\n * byte-identical to what the legacy `TagEditor` / `TextPromptEditor`\n * produced. No engine, analyzer, or metadata changes.\n */\nexport function LiquidEditor({\n value,\n onChange,\n inputFields,\n searchSecrets,\n constantSuggestions,\n multiline = false,\n autoGrow = true,\n directives = [],\n expectedFieldType,\n placeholder,\n className,\n editorProps,\n hideLegend = false,\n}: LiquidEditorProps) {\n const blockSeparator = multiline ? \"\\n\" : \"\";\n const visibleConstants = constantSuggestions ?? [];\n const supportsOutput = directives.includes(\"output\");\n const hasSecretsResolver = !!searchSecrets;\n\n const [chipTarget, setChipTarget] = useState<ChipEditTarget | null>(null);\n\n const tagChipExtension = useMemo(\n () =>\n TagChipDecoration.configure({\n onChipClick: (hit: ChipHit, el: HTMLElement) => {\n setChipTarget({\n kind: hit.kind,\n raw: hit.raw,\n from: hit.from,\n to: hit.to,\n rect: el.getBoundingClientRect(),\n });\n },\n }),\n [],\n );\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({ hardBreak: !multiline ? false : undefined }),\n tagChipExtension,\n ],\n parseOptions: { preserveWhitespace: \"full\" },\n editorProps: {\n ...editorProps,\n attributes: {\n class: cn(\n \"pz:px-2.5 pz:py-1 pz:text-sm pz:outline-none pz:break-words\",\n multiline\n ? autoGrow\n ? \"pz:min-h-24 pz:max-h-96 pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:min-h-8\",\n className,\n ),\n ...(placeholder ? { \"data-placeholder\": placeholder } : {}),\n ...editorProps?.attributes,\n },\n handleKeyDown(view, event) {\n if (editorProps?.handleKeyDown?.(view, event)) return true;\n // Single-line: Enter must not split the document. Multi-line: let\n // StarterKit handle Enter so paragraphs and blocks behave normally.\n if (!multiline && event.key === \"Enter\") return true;\n return false;\n },\n },\n content: textToTiptapHTML(value || \"\", multiline),\n onUpdate({ editor }) {\n onChange(editor.getText({ blockSeparator }));\n },\n });\n\n // Sync the editor's document when `value` changes from outside (e.g.\n // form reset, programmatic setValue). `useEditor` only reads `content`\n // at mount; without this hook the visible UI drifts from form state.\n // The early-return short-circuits the loop that the editor's own\n // `onUpdate` would otherwise create.\n useEffect(() => {\n if (!editor) return;\n const current = editor.getText({ blockSeparator });\n if (current === (value ?? \"\")) return;\n editor.commands.setContent(textToTiptapHTML(value ?? \"\", multiline), false);\n }, [editor, value, multiline, blockSeparator]);\n\n const insertField = useCallback((name: string) => `{{ ${name} }}`, []);\n\n // Live-query state for secrets. The picker calls `referenceItems(query)` on\n // every keystroke; we debounce the underlying fetch and race-guard via\n // `latestQueryRef` so only the response for the most recent query lands.\n // `lastSecretsRef` carries the previous fetch's results forward so stale\n // calls render something sensible instead of an empty list.\n const lastSecretsRef = useRef<SecretSuggestion[]>([]);\n const latestQueryRef = useRef(\"\");\n\n const referenceItems = useCallback(\n ({ query }: { query: string }): Promise<SuggestionItem<ReferenceContext>[]> => {\n latestQueryRef.current = query;\n const myQuery = query;\n\n const buildWith = (secrets: SecretSuggestion[]) =>\n buildReferenceItems(\n myQuery,\n {\n inputFields,\n secretSuggestions: secrets,\n constantSuggestions: visibleConstants,\n },\n expectedFieldType,\n insertField,\n );\n\n if (!searchSecrets) {\n return Promise.resolve(buildWith([]));\n }\n\n return new Promise<SuggestionItem<ReferenceContext>[]>((resolve) => {\n setTimeout(async () => {\n if (latestQueryRef.current !== myQuery) {\n resolve(buildWith(lastSecretsRef.current));\n return;\n }\n try {\n const secrets = await searchSecrets(myQuery);\n if (latestQueryRef.current !== myQuery) {\n resolve(buildWith(lastSecretsRef.current));\n return;\n }\n lastSecretsRef.current = secrets;\n resolve(buildWith(secrets));\n } catch {\n resolve(buildWith(lastSecretsRef.current));\n }\n }, SECRET_SEARCH_DEBOUNCE_MS);\n });\n },\n [searchSecrets, inputFields, visibleConstants, expectedFieldType, insertField],\n );\n\n const directiveItems = useCallback(\n () =>\n RECORD_FIELD_TYPES.map<SuggestionItem>((fieldType) => ({\n title: `output: ${fieldType}`,\n keywords: [fieldType, \"output\"],\n onSelect: ({ editor: ed, range }) => {\n ed.chain().focus().insertContentAt(range, OUTPUT_TEMPLATE(fieldType)).run();\n },\n })),\n [],\n );\n\n const legendEntries: LegendEntry[] = [];\n legendEntries.push({ key: \"/\", label: \"to insert a reference\" });\n if (supportsOutput) legendEntries.push({ key: \"@\", label: \"to insert a directive\" });\n\n return (\n <EditorContext.Provider value={{ editor }}>\n <EditorContent editor={editor} />\n\n {!hideLegend && <FieldLegend entries={legendEntries} />}\n\n <SuggestionMenu\n editor={editor}\n char=\"/\"\n pluginKey=\"liquidEditorReferencePicker\"\n items={referenceItems}\n >\n {(props) => (\n <UnifiedReferencePicker\n items={props.items as SuggestionItem<ReferenceContext>[]}\n selectedIndex={props.selectedIndex}\n onSelect={props.onSelect as (item: SuggestionItem<ReferenceContext>) => void}\n query={props.query}\n hasSecretsResolver={hasSecretsResolver}\n inputFieldsCount={inputFields.length}\n constantsCount={visibleConstants.length}\n />\n )}\n </SuggestionMenu>\n\n {supportsOutput && (\n <SuggestionMenu\n editor={editor}\n char=\"@\"\n pluginKey=\"liquidEditorDirectivePicker\"\n items={directiveItems}\n >\n {({ items, selectedIndex, onSelect }) => (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-56\" role=\"listbox\">\n <div className=\"pz:px-2 pz:pt-1 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n Add output declaration\n </div>\n {items.map((item, index) => (\n <button\n key={item.title}\n type=\"button\"\n role=\"option\"\n aria-selected={selectedIndex === index}\n onClick={() => onSelect(item)}\n className={cn(\n \"pz:flex pz:w-full pz:items-center pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer\",\n selectedIndex === index\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n )}\n >\n {item.title}\n </button>\n ))}\n </div>\n )}\n </SuggestionMenu>\n )}\n\n {chipTarget && editor && (\n <ChipEditPopover\n editor={editor}\n target={chipTarget}\n onClose={() => setChipTarget(null)}\n />\n )}\n </EditorContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,MAAM,4BAA4B;AA+BlC,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;;;;;;;;;;AAY7E,SAAS,iBAAiB,MAAc,WAA4B;AAClE,KAAI,CAAC,UACH,QAAO,MAAM,WAAW,KAAK,QAAQ,cAAc,IAAI,CAAC,CAAC;AAE3D,QAAO,WAAW,KAAK,CACpB,MAAM,QAAQ,CACd,KAAK,UAAU;AAKd,SAAO,MAJO,MAAM,MAAM,KAAK,CAE5B,KAAK,SAAS,KAAK,QAAQ,QAAQ,WAAW,SAAS,OAAO,OAAO,OAAO,CAAC,CAAC,CAC9E,KAAK,OAAO,CACQ;GACvB,CACD,KAAK,GAAG;;AAGb,MAAM,mBAAmB,cAAuC;AAC9D,KAAI,cAAc,OAChB,QAAO;AAET,QAAO,gCAAgC,UAAU;;;;;;;;;;;;;;;;;;;;;AAsBnD,SAAgB,aAAa,EAC3B,OACA,UACA,aACA,eACA,qBACA,YAAY,OACZ,WAAW,MACX,aAAa,EAAE,EACf,mBACA,aACA,WACA,aACA,aAAa,SACO;CACpB,MAAM,iBAAiB,YAAY,OAAO;CAC1C,MAAM,mBAAmB,uBAAuB,EAAE;CAClD,MAAM,iBAAiB,WAAW,SAAS,SAAS;CACpD,MAAM,qBAAqB,CAAC,CAAC;CAE7B,MAAM,CAAC,YAAY,iBAAiB,SAAgC,KAAK;CAEzE,MAAM,mBAAmB,cAErB,kBAAkB,UAAU,EAC1B,cAAc,KAAc,OAAoB;AAC9C,gBAAc;GACZ,MAAM,IAAI;GACV,KAAK,IAAI;GACT,MAAM,IAAI;GACV,IAAI,IAAI;GACR,MAAM,GAAG,uBAAuB;GACjC,CAAC;IAEL,CAAC,EACJ,EAAE,CACH;CAED,MAAM,SAAS,UAAU;EACvB,YAAY,CACV,WAAW,UAAU,EAAE,WAAW,CAAC,YAAY,QAAQ,QAAW,CAAC,EACnE,iBACD;EACD,cAAc,EAAE,oBAAoB,QAAQ;EAC5C,aAAa;GACX,GAAG;GACH,YAAY;IACV,OAAO,GACL,+DACA,YACI,WACE,oEACA,4CACF,cACJ,UACD;IACD,GAAI,cAAc,EAAE,oBAAoB,aAAa,GAAG,EAAE;IAC1D,GAAG,aAAa;IACjB;GACD,cAAc,MAAM,OAAO;AACzB,QAAI,aAAa,gBAAgB,MAAM,MAAM,CAAE,QAAO;AAGtD,QAAI,CAAC,aAAa,MAAM,QAAQ,QAAS,QAAO;AAChD,WAAO;;GAEV;EACD,SAAS,iBAAiB,SAAS,IAAI,UAAU;EACjD,SAAS,EAAE,UAAU;AACnB,YAAS,OAAO,QAAQ,EAAE,gBAAgB,CAAC,CAAC;;EAE/C,CAAC;AAOF,iBAAgB;AACd,MAAI,CAAC,OAAQ;AAEb,MADgB,OAAO,QAAQ,EAAE,gBAAgB,CAAC,MACjC,SAAS,IAAK;AAC/B,SAAO,SAAS,WAAW,iBAAiB,SAAS,IAAI,UAAU,EAAE,MAAM;IAC1E;EAAC;EAAQ;EAAO;EAAW;EAAe,CAAC;CAE9C,MAAM,cAAc,aAAa,SAAiB,MAAM,KAAK,MAAM,EAAE,CAAC;CAOtE,MAAM,iBAAiB,OAA2B,EAAE,CAAC;CACrD,MAAM,iBAAiB,OAAO,GAAG;CAEjC,MAAM,iBAAiB,aACpB,EAAE,YAA4E;AAC7E,iBAAe,UAAU;EACzB,MAAM,UAAU;EAEhB,MAAM,aAAa,YACjB,oBACE,SACA;GACE;GACA,mBAAmB;GACnB,qBAAqB;GACtB,EACD,mBACA,YACD;AAEH,MAAI,CAAC,cACH,QAAO,QAAQ,QAAQ,UAAU,EAAE,CAAC,CAAC;AAGvC,SAAO,IAAI,SAA6C,YAAY;AAClE,cAAW,YAAY;AACrB,QAAI,eAAe,YAAY,SAAS;AACtC,aAAQ,UAAU,eAAe,QAAQ,CAAC;AAC1C;;AAEF,QAAI;KACF,MAAM,UAAU,MAAM,cAAc,QAAQ;AAC5C,SAAI,eAAe,YAAY,SAAS;AACtC,cAAQ,UAAU,eAAe,QAAQ,CAAC;AAC1C;;AAEF,oBAAe,UAAU;AACzB,aAAQ,UAAU,QAAQ,CAAC;YACrB;AACN,aAAQ,UAAU,eAAe,QAAQ,CAAC;;MAE3C,0BAA0B;IAC7B;IAEJ;EAAC;EAAe;EAAa;EAAkB;EAAmB;EAAY,CAC/E;CAED,MAAM,iBAAiB,kBAEnB,mBAAmB,KAAqB,eAAe;EACrD,OAAO,WAAW;EAClB,UAAU,CAAC,WAAW,SAAS;EAC/B,WAAW,EAAE,QAAQ,IAAI,YAAY;AACnC,MAAG,OAAO,CAAC,OAAO,CAAC,gBAAgB,OAAO,gBAAgB,UAAU,CAAC,CAAC,KAAK;;EAE9E,EAAE,EACL,EAAE,CACH;CAED,MAAM,gBAA+B,EAAE;AACvC,eAAc,KAAK;EAAE,KAAK;EAAK,OAAO;EAAyB,CAAC;AAChE,KAAI,eAAgB,eAAc,KAAK;EAAE,KAAK;EAAK,OAAO;EAAyB,CAAC;AAEpF,QACE,qBAAC,cAAc,UAAf;EAAwB,OAAO,EAAE,QAAQ;YAAzC;GACE,oBAAC,eAAD,EAAuB,QAAU;GAEhC,CAAC,cAAc,oBAAC,aAAD,EAAa,SAAS,eAAiB;GAEvD,oBAAC,gBAAD;IACU;IACR,MAAK;IACL,WAAU;IACV,OAAO;eAEL,UACA,oBAAC,wBAAD;KACE,OAAO,MAAM;KACb,eAAe,MAAM;KACrB,UAAU,MAAM;KAChB,OAAO,MAAM;KACO;KACpB,kBAAkB,YAAY;KAC9B,gBAAgB,iBAAiB;KACjC;IAEW;GAEhB,kBACC,oBAAC,gBAAD;IACU;IACR,MAAK;IACL,WAAU;IACV,OAAO;eAEL,EAAE,OAAO,eAAe,eACxB,qBAAC,OAAD;KAAK,WAAU;KAAoD,MAAK;eAAxE,CACE,oBAAC,OAAD;MAAK,WAAU;gBAAiH;MAE1H,GACL,MAAM,KAAK,MAAM,UAChB,oBAAC,UAAD;MAEE,MAAK;MACL,MAAK;MACL,iBAAe,kBAAkB;MACjC,eAAe,SAAS,KAAK;MAC7B,WAAW,GACT,+GACA,kBAAkB,QACd,2CACA,qDACL;gBAEA,KAAK;MACC,EAbF,KAAK,MAaH,CACT,CACE;;IAEO;GAGlB,cAAc,UACb,oBAAC,iBAAD;IACU;IACR,QAAQ;IACR,eAAe,cAAc,KAAK;IAClC;GAEmB"}
|
|
1
|
+
{"version":3,"file":"LiquidEditor.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/LiquidEditor.tsx"],"sourcesContent":["import {\n type PipesFieldDefinitionWithName,\n RECORD_FIELD_TYPES,\n type RecordFieldType,\n} from \"@pipe0/base\";\nimport type { EditorProps } from \"@tiptap/pm/view\";\nimport { EditorContent, EditorContext, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useAsyncRemoteSource } from \"../../../hooks/use-async-remote-source.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport { FieldLegend, type LegendEntry } from \"../field-legend.js\";\nimport { SuggestionMenu } from \"../suggestion-menu/suggestion-menu.js\";\nimport type { SuggestionItem } from \"../suggestion-menu/suggestion-menu-types.js\";\nimport { type ChipHit, TagChipDecoration } from \"../tag-chip-decoration.js\";\nimport { ChipEditPopover, type ChipEditTarget } from \"./ChipEditPopover.js\";\nimport {\n buildReferenceItems,\n parseQuery,\n type ReferenceContext,\n SECTION_CAP,\n UnifiedReferencePicker,\n} from \"./UnifiedReferencePicker.js\";\n\nexport type DirectiveKind = \"output\";\n\nexport interface LiquidEditorProps {\n value: string;\n onChange: (v: string) => void;\n inputFields: PipesFieldDefinitionWithName[];\n /**\n * Per-keystroke async searcher for secrets. Called from the `/` reference\n * picker as the user types. Calls are debounced and race-guarded — only\n * the response for the latest query is rendered.\n */\n searchSecrets?: (query: string) => Promise<SecretSuggestion[]>;\n /**\n * Per-keystroke async searcher for constants. Mirrors `searchSecrets`.\n */\n searchConstants?: (query: string) => Promise<ConstantSuggestion[]>;\n multiline?: boolean;\n /**\n * When true, the editor renders within `min-h`/`max-h` bounds. When\n * false, callers control the height via `className`. Default: true.\n */\n autoGrow?: boolean;\n directives?: DirectiveKind[];\n expectedFieldType?: RecordFieldType;\n placeholder?: string;\n className?: string;\n editorProps?: EditorProps;\n /**\n * Suppress the per-editor legend. Used by `key_value_list_input` which\n * renders a single legend below the entire list rather than per-cell.\n */\n hideLegend?: boolean;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/**\n * Reconciles single- and multi-line text into Tiptap-friendly HTML.\n * - Single-line: collapses whitespace runs containing newlines into a\n * single space and renders as one paragraph.\n * - Multi-line: paragraphs separated by blank lines (`\\n\\n+`); soft\n * breaks within a paragraph render as `<br>`. Leading spaces on each\n * line are preserved with ` ` so JSON-shaped payloads round-trip\n * through the editor.\n */\nfunction textToTiptapHTML(text: string, multiline: boolean): string {\n if (!multiline) {\n return `<p>${escapeHtml(text.replace(/\\s*\\n+\\s*/g, \" \"))}</p>`;\n }\n return escapeHtml(text)\n .split(/\\n\\n+/)\n .map((block) => {\n const lines = block.split(/\\n/);\n const htmlLines = lines\n .map((line) => line.replace(/^ +/, (spaces) => \" \".repeat(spaces.length)))\n .join(\"<br>\");\n return `<p>${htmlLines}</p>`;\n })\n .join(\"\");\n}\n\nconst OUTPUT_TEMPLATE = (fieldType: RecordFieldType): string => {\n if (fieldType === \"json\") {\n return `{% output FIELD_NAME, type: \"json\", schema: \"SCHEMA_NAME\", description: \"\" %}`;\n }\n return `{% output FIELD_NAME, type: \"${fieldType}\", description: \"\" %}`;\n};\n\n/**\n * Single editor surface for every form field that accepts Liquid tags.\n * Two triggers, two cognitive categories:\n *\n * - `/` opens a unified reference picker (Fields, Secrets, Constants).\n * Prefix syntax `/f/`, `/s/`, `/c/` filters to one source. Picking\n * emits the corresponding `{{ … }}` Liquid Output.\n *\n * - `@` opens a directives picker (only when `directives.length > 0`).\n * Today the only directive is `output`, which inserts a templated\n * `{% output FIELD_NAME, type: \"...\", description: \"\" %}` block.\n *\n * Chips render via the `TagChipDecoration` extension. Click or\n * Enter/Space on a chip mounts the contextual `ChipEditPopover`.\n *\n * Backwards compat: the persisted value is plain Liquid text,\n * byte-identical to what the legacy `TagEditor` / `TextPromptEditor`\n * produced. No engine, analyzer, or metadata changes.\n */\nexport function LiquidEditor({\n value,\n onChange,\n inputFields,\n searchSecrets,\n searchConstants,\n multiline = false,\n autoGrow = true,\n directives = [],\n expectedFieldType,\n placeholder,\n className,\n editorProps,\n hideLegend = false,\n}: LiquidEditorProps) {\n const blockSeparator = multiline ? \"\\n\" : \"\";\n const supportsOutput = directives.includes(\"output\");\n const hasSecretsResolver = !!searchSecrets;\n const hasConstantsResolver = !!searchConstants;\n\n const secretSource = useAsyncRemoteSource<SecretSuggestion>({ search: searchSecrets });\n const constantSource = useAsyncRemoteSource<ConstantSuggestion>({ search: searchConstants });\n\n // Warm caches once per resolver identity so the empty-query open is hot.\n useEffect(() => {\n secretSource.prefetch();\n }, [secretSource.prefetch]);\n\n useEffect(() => {\n constantSource.prefetch();\n }, [constantSource.prefetch]);\n\n const [chipTarget, setChipTarget] = useState<ChipEditTarget | null>(null);\n\n const tagChipExtension = useMemo(\n () =>\n TagChipDecoration.configure({\n onChipClick: (hit: ChipHit, el: HTMLElement) => {\n setChipTarget({\n kind: hit.kind,\n raw: hit.raw,\n from: hit.from,\n to: hit.to,\n rect: el.getBoundingClientRect(),\n });\n },\n }),\n [],\n );\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({ hardBreak: !multiline ? false : undefined }),\n tagChipExtension,\n ],\n parseOptions: { preserveWhitespace: \"full\" },\n editorProps: {\n ...editorProps,\n attributes: {\n class: cn(\n \"pz:px-2.5 pz:py-1 pz:text-sm pz:outline-none pz:break-words\",\n multiline\n ? autoGrow\n ? \"pz:min-h-24 pz:max-h-96 pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:min-h-8\",\n className,\n ),\n ...(placeholder ? { \"data-placeholder\": placeholder } : {}),\n ...editorProps?.attributes,\n },\n handleKeyDown(view, event) {\n if (editorProps?.handleKeyDown?.(view, event)) return true;\n // Single-line: Enter must not split the document. Multi-line: let\n // StarterKit handle Enter so paragraphs and blocks behave normally.\n if (!multiline && event.key === \"Enter\") return true;\n return false;\n },\n },\n content: textToTiptapHTML(value || \"\", multiline),\n onUpdate({ editor }) {\n onChange(editor.getText({ blockSeparator }));\n },\n });\n\n // Sync the editor's document when `value` changes from outside (e.g.\n // form reset, programmatic setValue). `useEditor` only reads `content`\n // at mount; without this hook the visible UI drifts from form state.\n // The early-return short-circuits the loop that the editor's own\n // `onUpdate` would otherwise create.\n useEffect(() => {\n if (!editor) return;\n const current = editor.getText({ blockSeparator });\n if (current === (value ?? \"\")) return;\n editor.commands.setContent(textToTiptapHTML(value ?? \"\", multiline), false);\n }, [editor, value, multiline, blockSeparator]);\n\n const insertField = useCallback((name: string) => `{{ ${name} }}`, []);\n\n // Tracks the current query in the open reference picker. Drives the\n // `liveItems` recomputation below — when the async sources resolve, we\n // re-render the picker against this query without waiting for Tiptap to\n // re-call items().\n const [referenceQuery, setReferenceQuery] = useState(\"\");\n\n // In sectioned (empty-query) view, cap each kind at SECTION_CAP visible\n // rows. Users see a compact summary and type to filter; the act of\n // filtering switches to flat mode where all matches are shown.\n // Caps are applied at this layer (not inside the picker) so the array\n // passed to SuggestionMenu matches what's rendered — keyboard nav\n // operates on the visible set and never lands on a hidden row.\n const buildVisibleItems = useCallback(\n (\n query: string,\n ): {\n items: SuggestionItem<ReferenceContext>[];\n overflow: Record<ReferenceContext[\"kind\"], number>;\n } => {\n const all = buildReferenceItems(\n query,\n {\n inputFields,\n secretSuggestions: secretSource.items,\n constantSuggestions: constantSource.items,\n },\n expectedFieldType,\n insertField,\n );\n const { prefix, residual } = parseQuery(query);\n const isFlat = !!residual.trim() || prefix !== null;\n if (isFlat) {\n return { items: all, overflow: { field: 0, secret: 0, constant: 0 } };\n }\n const fields = all.filter((i) => i.context?.kind === \"field\");\n const secrets = all.filter((i) => i.context?.kind === \"secret\");\n const constants = all.filter((i) => i.context?.kind === \"constant\");\n return {\n items: [\n ...fields.slice(0, SECTION_CAP),\n ...secrets.slice(0, SECTION_CAP),\n ...constants.slice(0, SECTION_CAP),\n ],\n overflow: {\n field: Math.max(0, fields.length - SECTION_CAP),\n secret: Math.max(0, secrets.length - SECTION_CAP),\n constant: Math.max(0, constants.length - SECTION_CAP),\n },\n };\n },\n [inputFields, expectedFieldType, insertField, secretSource.items, constantSource.items],\n );\n\n const referenceItems = useCallback(\n ({ query }: { query: string }): SuggestionItem<ReferenceContext>[] => {\n setReferenceQuery(query);\n // Fire-and-forget: results land via React state in `secretSource.items`\n // / `constantSource.items` and trigger a re-render. The popup reads\n // them through `liveItems` (see below).\n secretSource.ensure(query);\n constantSource.ensure(query);\n return buildVisibleItems(query).items;\n },\n [secretSource.ensure, constantSource.ensure, buildVisibleItems],\n );\n\n // Live items derived from the current query + cached source items. This is\n // recomputed whenever an async source resolves (which updates\n // `secretSource.items` / `constantSource.items` via setState). Passed to\n // SuggestionMenu via `liveItems` so the open popup re-renders without\n // needing the user to type — Tiptap's items() callback alone cannot\n // deliver async results that arrive between keystrokes.\n const { items: liveReferenceItems, overflow: liveOverflow } = useMemo(\n () => buildVisibleItems(referenceQuery),\n [referenceQuery, buildVisibleItems],\n );\n\n const directiveItems = useCallback(\n () =>\n RECORD_FIELD_TYPES.map<SuggestionItem>((fieldType) => ({\n title: `output: ${fieldType}`,\n keywords: [fieldType, \"output\"],\n onSelect: ({ editor: ed, range }) => {\n ed.chain().focus().insertContentAt(range, OUTPUT_TEMPLATE(fieldType)).run();\n },\n })),\n [],\n );\n\n const legendEntries: LegendEntry[] = [];\n legendEntries.push({ key: \"/\", label: \"to insert a reference\" });\n if (supportsOutput) legendEntries.push({ key: \"@\", label: \"to insert a directive\" });\n\n return (\n <EditorContext.Provider value={{ editor }}>\n <EditorContent editor={editor} />\n\n {!hideLegend && <FieldLegend entries={legendEntries} />}\n\n <SuggestionMenu\n editor={editor}\n char=\"/\"\n pluginKey=\"liquidEditorReferencePicker\"\n items={referenceItems}\n liveItems={liveReferenceItems}\n >\n {(props) => (\n <UnifiedReferencePicker\n items={props.items as SuggestionItem<ReferenceContext>[]}\n selectedIndex={props.selectedIndex}\n onSelect={props.onSelect as (item: SuggestionItem<ReferenceContext>) => void}\n query={props.query}\n hasSecretsResolver={hasSecretsResolver}\n hasConstantsResolver={hasConstantsResolver}\n inputFieldsCount={inputFields.length}\n secretsPending={secretSource.pending}\n constantsPending={constantSource.pending}\n overflow={liveOverflow}\n />\n )}\n </SuggestionMenu>\n\n {supportsOutput && (\n <SuggestionMenu\n editor={editor}\n char=\"@\"\n pluginKey=\"liquidEditorDirectivePicker\"\n items={directiveItems}\n >\n {({ items, selectedIndex, onSelect }) => (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-56\" role=\"listbox\">\n <div className=\"pz:px-2 pz:pt-1 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n Add output declaration\n </div>\n {items.map((item, index) => (\n <button\n key={item.title}\n type=\"button\"\n role=\"option\"\n aria-selected={selectedIndex === index}\n onClick={() => onSelect(item)}\n className={cn(\n \"pz:flex pz:w-full pz:items-center pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer\",\n selectedIndex === index\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n )}\n >\n {item.title}\n </button>\n ))}\n </div>\n )}\n </SuggestionMenu>\n )}\n\n {chipTarget && editor && (\n <ChipEditPopover editor={editor} target={chipTarget} onClose={() => setChipTarget(null)} />\n )}\n </EditorContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA2DA,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;;;;;;;;;;AAY7E,SAAS,iBAAiB,MAAc,WAA4B;AAClE,KAAI,CAAC,UACH,QAAO,MAAM,WAAW,KAAK,QAAQ,cAAc,IAAI,CAAC,CAAC;AAE3D,QAAO,WAAW,KAAK,CACpB,MAAM,QAAQ,CACd,KAAK,UAAU;AAKd,SAAO,MAJO,MAAM,MAAM,KAAK,CAE5B,KAAK,SAAS,KAAK,QAAQ,QAAQ,WAAW,SAAS,OAAO,OAAO,OAAO,CAAC,CAAC,CAC9E,KAAK,OAAO,CACQ;GACvB,CACD,KAAK,GAAG;;AAGb,MAAM,mBAAmB,cAAuC;AAC9D,KAAI,cAAc,OAChB,QAAO;AAET,QAAO,gCAAgC,UAAU;;;;;;;;;;;;;;;;;;;;;AAsBnD,SAAgB,aAAa,EAC3B,OACA,UACA,aACA,eACA,iBACA,YAAY,OACZ,WAAW,MACX,aAAa,EAAE,EACf,mBACA,aACA,WACA,aACA,aAAa,SACO;CACpB,MAAM,iBAAiB,YAAY,OAAO;CAC1C,MAAM,iBAAiB,WAAW,SAAS,SAAS;CACpD,MAAM,qBAAqB,CAAC,CAAC;CAC7B,MAAM,uBAAuB,CAAC,CAAC;CAE/B,MAAM,eAAe,qBAAuC,EAAE,QAAQ,eAAe,CAAC;CACtF,MAAM,iBAAiB,qBAAyC,EAAE,QAAQ,iBAAiB,CAAC;AAG5F,iBAAgB;AACd,eAAa,UAAU;IACtB,CAAC,aAAa,SAAS,CAAC;AAE3B,iBAAgB;AACd,iBAAe,UAAU;IACxB,CAAC,eAAe,SAAS,CAAC;CAE7B,MAAM,CAAC,YAAY,iBAAiB,SAAgC,KAAK;CAEzE,MAAM,mBAAmB,cAErB,kBAAkB,UAAU,EAC1B,cAAc,KAAc,OAAoB;AAC9C,gBAAc;GACZ,MAAM,IAAI;GACV,KAAK,IAAI;GACT,MAAM,IAAI;GACV,IAAI,IAAI;GACR,MAAM,GAAG,uBAAuB;GACjC,CAAC;IAEL,CAAC,EACJ,EAAE,CACH;CAED,MAAM,SAAS,UAAU;EACvB,YAAY,CACV,WAAW,UAAU,EAAE,WAAW,CAAC,YAAY,QAAQ,QAAW,CAAC,EACnE,iBACD;EACD,cAAc,EAAE,oBAAoB,QAAQ;EAC5C,aAAa;GACX,GAAG;GACH,YAAY;IACV,OAAO,GACL,+DACA,YACI,WACE,oEACA,4CACF,cACJ,UACD;IACD,GAAI,cAAc,EAAE,oBAAoB,aAAa,GAAG,EAAE;IAC1D,GAAG,aAAa;IACjB;GACD,cAAc,MAAM,OAAO;AACzB,QAAI,aAAa,gBAAgB,MAAM,MAAM,CAAE,QAAO;AAGtD,QAAI,CAAC,aAAa,MAAM,QAAQ,QAAS,QAAO;AAChD,WAAO;;GAEV;EACD,SAAS,iBAAiB,SAAS,IAAI,UAAU;EACjD,SAAS,EAAE,UAAU;AACnB,YAAS,OAAO,QAAQ,EAAE,gBAAgB,CAAC,CAAC;;EAE/C,CAAC;AAOF,iBAAgB;AACd,MAAI,CAAC,OAAQ;AAEb,MADgB,OAAO,QAAQ,EAAE,gBAAgB,CAAC,MACjC,SAAS,IAAK;AAC/B,SAAO,SAAS,WAAW,iBAAiB,SAAS,IAAI,UAAU,EAAE,MAAM;IAC1E;EAAC;EAAQ;EAAO;EAAW;EAAe,CAAC;CAE9C,MAAM,cAAc,aAAa,SAAiB,MAAM,KAAK,MAAM,EAAE,CAAC;CAMtE,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,GAAG;CAQxD,MAAM,oBAAoB,aAEtB,UAIG;EACH,MAAM,MAAM,oBACV,OACA;GACE;GACA,mBAAmB,aAAa;GAChC,qBAAqB,eAAe;GACrC,EACD,mBACA,YACD;EACD,MAAM,EAAE,QAAQ,aAAa,WAAW,MAAM;AAE9C,MADe,CAAC,CAAC,SAAS,MAAM,IAAI,WAAW,KAE7C,QAAO;GAAE,OAAO;GAAK,UAAU;IAAE,OAAO;IAAG,QAAQ;IAAG,UAAU;IAAG;GAAE;EAEvE,MAAM,SAAS,IAAI,QAAQ,MAAM,EAAE,SAAS,SAAS,QAAQ;EAC7D,MAAM,UAAU,IAAI,QAAQ,MAAM,EAAE,SAAS,SAAS,SAAS;EAC/D,MAAM,YAAY,IAAI,QAAQ,MAAM,EAAE,SAAS,SAAS,WAAW;AACnE,SAAO;GACL,OAAO;IACL,GAAG,OAAO,MAAM,KAAe;IAC/B,GAAG,QAAQ,MAAM,KAAe;IAChC,GAAG,UAAU,MAAM,KAAe;IACnC;GACD,UAAU;IACR,OAAO,KAAK,IAAI,GAAG,OAAO,WAAqB;IAC/C,QAAQ,KAAK,IAAI,GAAG,QAAQ,WAAqB;IACjD,UAAU,KAAK,IAAI,GAAG,UAAU,WAAqB;IACtD;GACF;IAEH;EAAC;EAAa;EAAmB;EAAa,aAAa;EAAO,eAAe;EAAM,CACxF;CAED,MAAM,iBAAiB,aACpB,EAAE,YAAmE;AACpE,oBAAkB,MAAM;AAIxB,eAAa,OAAO,MAAM;AAC1B,iBAAe,OAAO,MAAM;AAC5B,SAAO,kBAAkB,MAAM,CAAC;IAElC;EAAC,aAAa;EAAQ,eAAe;EAAQ;EAAkB,CAChE;CAQD,MAAM,EAAE,OAAO,oBAAoB,UAAU,iBAAiB,cACtD,kBAAkB,eAAe,EACvC,CAAC,gBAAgB,kBAAkB,CACpC;CAED,MAAM,iBAAiB,kBAEnB,mBAAmB,KAAqB,eAAe;EACrD,OAAO,WAAW;EAClB,UAAU,CAAC,WAAW,SAAS;EAC/B,WAAW,EAAE,QAAQ,IAAI,YAAY;AACnC,MAAG,OAAO,CAAC,OAAO,CAAC,gBAAgB,OAAO,gBAAgB,UAAU,CAAC,CAAC,KAAK;;EAE9E,EAAE,EACL,EAAE,CACH;CAED,MAAM,gBAA+B,EAAE;AACvC,eAAc,KAAK;EAAE,KAAK;EAAK,OAAO;EAAyB,CAAC;AAChE,KAAI,eAAgB,eAAc,KAAK;EAAE,KAAK;EAAK,OAAO;EAAyB,CAAC;AAEpF,QACE,qBAAC,cAAc,UAAf;EAAwB,OAAO,EAAE,QAAQ;YAAzC;GACE,oBAAC,eAAD,EAAuB,QAAU;GAEhC,CAAC,cAAc,oBAAC,aAAD,EAAa,SAAS,eAAiB;GAEvD,oBAAC,gBAAD;IACU;IACR,MAAK;IACL,WAAU;IACV,OAAO;IACP,WAAW;eAET,UACA,oBAAC,wBAAD;KACE,OAAO,MAAM;KACb,eAAe,MAAM;KACrB,UAAU,MAAM;KAChB,OAAO,MAAM;KACO;KACE;KACtB,kBAAkB,YAAY;KAC9B,gBAAgB,aAAa;KAC7B,kBAAkB,eAAe;KACjC,UAAU;KACV;IAEW;GAEhB,kBACC,oBAAC,gBAAD;IACU;IACR,MAAK;IACL,WAAU;IACV,OAAO;eAEL,EAAE,OAAO,eAAe,eACxB,qBAAC,OAAD;KAAK,WAAU;KAAoD,MAAK;eAAxE,CACE,oBAAC,OAAD;MAAK,WAAU;gBAAiH;MAE1H,GACL,MAAM,KAAK,MAAM,UAChB,oBAAC,UAAD;MAEE,MAAK;MACL,MAAK;MACL,iBAAe,kBAAkB;MACjC,eAAe,SAAS,KAAK;MAC7B,WAAW,GACT,+GACA,kBAAkB,QACd,2CACA,qDACL;gBAEA,KAAK;MACC,EAbF,KAAK,MAaH,CACT,CACE;;IAEO;GAGlB,cAAc,UACb,oBAAC,iBAAD;IAAyB;IAAQ,QAAQ;IAAY,eAAe,cAAc,KAAK;IAAI;GAEtE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Hash, Key } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
//#region src/components/internal/LiquidEditor/UnifiedReferencePicker.tsx
|
|
@@ -135,18 +135,7 @@ function ItemRow({ item, active, onSelect }) {
|
|
|
135
135
|
]
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
* Picker UI rendered inside the SuggestionMenu's children render-prop.
|
|
140
|
-
*
|
|
141
|
-
* - Empty query → sectioned view (Fields, Secrets, Constants).
|
|
142
|
-
* - Non-empty query → flat ranked list.
|
|
143
|
-
*
|
|
144
|
-
* The selected-index pointer from `SuggestionMenuRenderProps` always maps
|
|
145
|
-
* to the flat `items` order. Sectioned mode uses the same flat order but
|
|
146
|
-
* inserts headers between the boundaries, so highlight tracking still
|
|
147
|
-
* works without bespoke navigation.
|
|
148
|
-
*/
|
|
149
|
-
function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecretsResolver, inputFieldsCount, constantsCount }) {
|
|
138
|
+
function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecretsResolver, hasConstantsResolver, inputFieldsCount, secretsPending, constantsPending, overflow }) {
|
|
150
139
|
const { prefix, residual } = parseQuery(query);
|
|
151
140
|
if (!!residual.trim() || prefix !== null) {
|
|
152
141
|
if (items.length === 0) return /* @__PURE__ */ jsxs("div", {
|
|
@@ -154,7 +143,7 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
|
|
|
154
143
|
role: "listbox",
|
|
155
144
|
children: [prefix && /* @__PURE__ */ jsx(SectionHeader, { children: prefix === "field" ? "Fields" : prefix === "secret" ? "Secrets" : "Constants" }), /* @__PURE__ */ jsx("div", {
|
|
156
145
|
className: "pz:px-2 pz:py-1.5 pz:text-sm pz:text-muted-foreground",
|
|
157
|
-
children: describeEmpty(prefix)
|
|
146
|
+
children: prefix === "secret" && secretsPending || prefix === "constant" && constantsPending || prefix === null && (secretsPending || constantsPending) ? "Searching…" : describeEmpty(prefix)
|
|
158
147
|
})]
|
|
159
148
|
});
|
|
160
149
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -181,7 +170,9 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
|
|
|
181
170
|
startIndex: 0,
|
|
182
171
|
selectedIndex,
|
|
183
172
|
onSelect,
|
|
184
|
-
emptyHint: inputFieldsCount === 0 ? "No input fields available." : "No matching input fields."
|
|
173
|
+
emptyHint: inputFieldsCount === 0 ? "No input fields available." : "No matching input fields.",
|
|
174
|
+
overflow: overflow.field,
|
|
175
|
+
overflowHelper: "Type to filter."
|
|
185
176
|
}),
|
|
186
177
|
hasSecretsResolver && /* @__PURE__ */ jsx(Section, {
|
|
187
178
|
label: "Secrets",
|
|
@@ -190,42 +181,65 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
|
|
|
190
181
|
startIndex: fieldItems.length,
|
|
191
182
|
selectedIndex,
|
|
192
183
|
onSelect,
|
|
193
|
-
emptyHint: "No matching secrets.",
|
|
194
|
-
helperHint: "Type /s/ to filter to secrets only."
|
|
184
|
+
emptyHint: secretsPending ? "Searching…" : "No matching secrets.",
|
|
185
|
+
helperHint: "Type /s/ to filter to secrets only.",
|
|
186
|
+
pending: secretsPending,
|
|
187
|
+
overflow: overflow.secret,
|
|
188
|
+
overflowHelper: "Type to filter, or /s/ for secrets only."
|
|
195
189
|
}),
|
|
196
|
-
|
|
190
|
+
hasConstantsResolver && /* @__PURE__ */ jsx(Section, {
|
|
197
191
|
label: "Constants",
|
|
198
192
|
kind: "constant",
|
|
199
193
|
items: constantItems,
|
|
200
194
|
startIndex: fieldItems.length + secretItems.length,
|
|
201
195
|
selectedIndex,
|
|
202
196
|
onSelect,
|
|
203
|
-
emptyHint: "No matching constants."
|
|
197
|
+
emptyHint: constantsPending ? "Searching…" : "No matching constants.",
|
|
198
|
+
helperHint: "Type /c/ to filter to constants only.",
|
|
199
|
+
pending: constantsPending,
|
|
200
|
+
overflow: overflow.constant,
|
|
201
|
+
overflowHelper: "Type to filter, or /c/ for constants only."
|
|
204
202
|
})
|
|
205
203
|
]
|
|
206
204
|
});
|
|
207
205
|
}
|
|
208
|
-
function Section({ label, kind, items, startIndex, selectedIndex, onSelect, emptyHint, helperHint }) {
|
|
206
|
+
function Section({ label, kind, items, startIndex, selectedIndex, onSelect, emptyHint, helperHint, pending, overflow = 0, overflowHelper }) {
|
|
209
207
|
return /* @__PURE__ */ jsxs("div", {
|
|
210
208
|
className: "pz:flex pz:flex-col",
|
|
211
|
-
children: [/* @__PURE__ */ jsxs(SectionHeader, { children: [
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
children: [/* @__PURE__ */ jsxs(SectionHeader, { children: [
|
|
210
|
+
label,
|
|
211
|
+
items.length > 0 && /* @__PURE__ */ jsxs("span", {
|
|
212
|
+
className: "pz:ml-1 pz:normal-case pz:tracking-normal",
|
|
213
|
+
children: [
|
|
214
|
+
"(",
|
|
215
|
+
items.length,
|
|
216
|
+
overflow > 0 && ` of ${items.length + overflow}`,
|
|
217
|
+
")"
|
|
218
|
+
]
|
|
219
|
+
}),
|
|
220
|
+
pending && /* @__PURE__ */ jsx("span", {
|
|
221
|
+
className: "pz:ml-1 pz:normal-case pz:tracking-normal pz:opacity-70",
|
|
222
|
+
children: "· Searching…"
|
|
223
|
+
})
|
|
224
|
+
] }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
219
225
|
className: "pz:px-2 pz:py-1 pz:text-xs pz:text-muted-foreground",
|
|
220
226
|
children: [emptyHint, helperHint && /* @__PURE__ */ jsx("span", {
|
|
221
227
|
className: "pz:block pz:opacity-70",
|
|
222
228
|
children: helperHint
|
|
223
229
|
})]
|
|
224
|
-
}) : items.map((item, i) => /* @__PURE__ */ jsx(ItemRow, {
|
|
230
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [items.map((item, i) => /* @__PURE__ */ jsx(ItemRow, {
|
|
225
231
|
item,
|
|
226
232
|
active: selectedIndex === startIndex + i,
|
|
227
233
|
onSelect
|
|
228
|
-
}, `${kind}-${item.title}`))
|
|
234
|
+
}, `${kind}-${item.title}`)), overflow > 0 && overflowHelper && /* @__PURE__ */ jsxs("div", {
|
|
235
|
+
className: "pz:px-2 pz:py-0.5 pz:text-[11px] pz:text-muted-foreground pz:opacity-70",
|
|
236
|
+
children: [
|
|
237
|
+
"+",
|
|
238
|
+
overflow,
|
|
239
|
+
" more — ",
|
|
240
|
+
overflowHelper
|
|
241
|
+
]
|
|
242
|
+
})] })]
|
|
229
243
|
});
|
|
230
244
|
}
|
|
231
245
|
function describeEmpty(prefix) {
|
|
@@ -236,5 +250,5 @@ function describeEmpty(prefix) {
|
|
|
236
250
|
}
|
|
237
251
|
|
|
238
252
|
//#endregion
|
|
239
|
-
export { UnifiedReferencePicker, buildReferenceItems };
|
|
253
|
+
export { UnifiedReferencePicker, buildReferenceItems, parseQuery };
|
|
240
254
|
//# sourceMappingURL=UnifiedReferencePicker.mjs.map
|