Edit json, record and list values in the JSON dialog
An inject's payload and a flow input's starting value of those types use JsonField instead of a one-line text field. The field can now hold no value (shown as the placeholder; an emptied editor saves null) and checks the shape: a record must be an object, a list an array.
This commit is contained in:
@@ -60,7 +60,10 @@ priority. A field marked as a credential renders as a picker over your stored
|
||||
[secrets](operations.md) instead of a text box. A setting holding JSON, such as
|
||||
a Switch node's rules, shows one line of it; click it to edit the whole value
|
||||
in a larger editor, which saves only once the text parses. Escape or Cancel
|
||||
leaves the setting as it was.
|
||||
leaves the setting as it was. An inject's payload and a flow input's starting
|
||||
value of type `json`, `record` or `list` open the same editor, which also holds
|
||||
the shape (a `record` is an object, a `list` an array); emptying it leaves no
|
||||
value.
|
||||
|
||||
A Function node gets a second settings section where you add your own: a
|
||||
setting is a constant of this node's code and arrives as an argument beside its
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { qualify } from "./deriveEdges"
|
||||
import { JsonField } from "./JsonField"
|
||||
import { useLiveValue } from "./liveStore"
|
||||
import { asText, DTYPES } from "./NodePanel"
|
||||
import { InfoTip, PANEL_SECTION } from "./SidePanel"
|
||||
@@ -51,10 +52,25 @@ export function parseByDtype(dtype: DType | undefined, raw: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
/** Why a parsed value is not the shape its type declares, or null. */
|
||||
function shapeError(dtype: DType | undefined, value: unknown): string | null {
|
||||
if (
|
||||
dtype === "record" &&
|
||||
(value === null || typeof value !== "object" || Array.isArray(value))
|
||||
) {
|
||||
return "A record is an object: { … }"
|
||||
}
|
||||
if (dtype === "list" && !Array.isArray(value)) {
|
||||
return "A list is an array: [ … ]"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* One literal, entered the way its type is entered.
|
||||
*
|
||||
* A flag has two values and gets a choice of them; everything else is typed
|
||||
* A flag has two values and gets a choice of them; JSON, a record and a list
|
||||
* get the JSON editor, which also holds their shape; everything else is typed
|
||||
* and read back with {@link parseByDtype}. Deliberately not `type="number"`
|
||||
* for the numbers — see that function on why half-typed input must survive.
|
||||
*
|
||||
@@ -99,6 +115,21 @@ export function DtypeValue({
|
||||
)
|
||||
}
|
||||
|
||||
if (dtype === "json" || dtype === "record" || dtype === "list") {
|
||||
return (
|
||||
<JsonField
|
||||
id={id}
|
||||
value={value}
|
||||
label={label}
|
||||
// Empty is no value here, as it is in the text field below.
|
||||
placeholder={placeholder ?? ""}
|
||||
check={(next) => shapeError(dtype, next)}
|
||||
className={className}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
|
||||
@@ -14,14 +14,18 @@ import { cn } from "@/lib/utils"
|
||||
|
||||
const NodeEditor = lazy(() => import("./NodeEditor"))
|
||||
|
||||
/** Why `text` is not JSON, or null when it is. */
|
||||
function parseError(text: string): string | null {
|
||||
/** Why `text` is not JSON of the right shape, or null when it is. */
|
||||
function parseError(
|
||||
text: string,
|
||||
check?: (value: unknown) => string | null,
|
||||
): string | null {
|
||||
let value: unknown
|
||||
try {
|
||||
JSON.parse(text)
|
||||
return null
|
||||
value = JSON.parse(text)
|
||||
} catch (error) {
|
||||
return (error as Error).message
|
||||
}
|
||||
return check?.(value) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,6 +39,8 @@ export function JsonField({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
placeholder,
|
||||
check,
|
||||
className,
|
||||
onChange,
|
||||
}: {
|
||||
@@ -42,12 +48,22 @@ export function JsonField({
|
||||
value: unknown
|
||||
/** The setting's name, which titles the dialog. */
|
||||
label: string
|
||||
/**
|
||||
* What having no value means, shown while there is none. Given, emptying
|
||||
* the editor saves no value (null) instead of refusing to parse.
|
||||
*/
|
||||
placeholder?: string
|
||||
/** Why a parsed value is the wrong shape, or null when it fits. */
|
||||
check?: (value: unknown) => string | null
|
||||
className?: string
|
||||
onChange: (next: unknown) => void
|
||||
}) {
|
||||
// The text being edited; null while the dialog is shut.
|
||||
const [draft, setDraft] = useState<string | null>(null)
|
||||
const error = draft === null ? null : parseError(draft)
|
||||
const nullable = placeholder !== undefined
|
||||
const blank = nullable && (value === null || value === undefined)
|
||||
const emptied = nullable && draft?.trim() === ""
|
||||
const error = draft === null || emptied ? null : parseError(draft, check)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -60,10 +76,19 @@ export function JsonField({
|
||||
"flex h-8 min-w-0 items-center gap-2 rounded-full border border-input bg-transparent px-3 text-left font-mono text-sm outline-none transition-[color,box-shadow] hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30",
|
||||
className,
|
||||
)}
|
||||
onClick={() => setDraft(JSON.stringify(value, null, 2) ?? "")}
|
||||
onClick={() =>
|
||||
setDraft(blank ? "" : (JSON.stringify(value, null, 2) ?? ""))
|
||||
}
|
||||
>
|
||||
{/* No width of its own, so a long value never widens the panel. */}
|
||||
<span className="w-0 flex-1 truncate">{JSON.stringify(value)}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"w-0 flex-1 truncate",
|
||||
blank && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{blank ? placeholder : JSON.stringify(value)}
|
||||
</span>
|
||||
<Maximize2 className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
@@ -111,7 +136,7 @@ export function JsonField({
|
||||
data-testid="json-save"
|
||||
onClick={() => {
|
||||
if (draft === null) return
|
||||
onChange(JSON.parse(draft))
|
||||
onChange(emptied ? null : JSON.parse(draft))
|
||||
setDraft(null)
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user