UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

357 lines (356 loc) 13.1 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { useReducer } from "react"; import { Button } from "../components/button.js"; import { Icon } from "../data/Icon.js"; import { isPositionalParam } from "./types.js"; function formReducer(state, action) { return { ...state, [action.name]: action.value }; } function CommandForm({ parameters, onExecute, isPending, method: _method, path, accept, initialValues }) { const [values, dispatch] = useReducer( formReducer, { parameters, path, initialValues }, ({ parameters: params, path: formPath, initialValues: overrides }) => buildInitialState(normalizeParameters(params, formPath), formPath, overrides) ); const formParameters = normalizeParameters(parameters, path); const visibleParams = formParameters.filter((p) => !(p.in === "path" && (initialValues == null ? void 0 : initialValues[p.name]))); const positionalNames = new Set(formParameters.filter(isPositionalParam).map((p) => p.name)); const inlineLayout = visibleParams.length >= INLINE_LAYOUT_THRESHOLD; function handleSubmit(event) { event.preventDefault(); const params = {}; const args = []; for (const param of formParameters) { const value = submitValue(param, values[param.name]); if (value == null) continue; if (positionalNames.has(param.name)) { args.push(...splitArgsValue(value)); } else { params[param.name] = value; } } if (args.length > 0) params.args = args.join(","); onExecute(params, { Accept: accept }); } return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [ visibleParams.length > 0 && /* @__PURE__ */ jsx("div", { className: inlineLayout ? "grid gap-2" : "grid gap-4", children: visibleParams.map((param) => /* @__PURE__ */ jsx( ParameterField, { param, value: values[param.name] || "", inline: inlineLayout, onChange: (value) => dispatch({ name: param.name, value }) }, param.name )) }), /* @__PURE__ */ jsx( "div", { className: inlineLayout ? "sticky bottom-0 z-10 -mx-2 flex justify-end border-t border-border bg-background/95 px-2 py-3 backdrop-blur-sm" : "flex justify-end", children: /* @__PURE__ */ jsx(Button, { type: "submit", disabled: isPending, children: isPending ? "Executing..." : "Execute" }) } ) ] }); } const INLINE_LAYOUT_THRESHOLD = 6; function ParameterField({ param, value, inline, onChange }) { const schema = param.schema; const fieldId = `param-${param.name}`; if (isMultiValueParam(param)) { return /* @__PURE__ */ jsx(FieldWrapper, { param, fieldId, inline, children: /* @__PURE__ */ jsx( TagInput, { id: fieldId, value, placeholder: param.description || param.name, onChange } ) }); } if (schema == null ? void 0 : schema.enum) { return /* @__PURE__ */ jsx(FieldWrapper, { param, fieldId, inline, children: /* @__PURE__ */ jsxs( "select", { id: fieldId, value, className: inputClassName, onChange: (event) => onChange(event.target.value), children: [ /* @__PURE__ */ jsxs("option", { value: "", children: [ "Select ", param.name ] }), schema.enum.map((v) => /* @__PURE__ */ jsx("option", { value: String(v), children: String(v) }, String(v))) ] } ) }); } if ((schema == null ? void 0 : schema.type) === "boolean") { const checkbox = /* @__PURE__ */ jsx( "input", { id: fieldId, type: "checkbox", className: "h-4 w-4 accent-primary", checked: value === "true", onChange: (event) => onChange(event.target.checked ? "true" : "false") } ); if (inline) { return /* @__PURE__ */ jsx(FieldWrapper, { param, fieldId, inline: true, children: /* @__PURE__ */ jsx("div", { className: "flex h-9 items-center", children: checkbox }) }); } return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [ checkbox, /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: "text-sm font-medium", children: [ param.name, param.required && /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" }) ] }), param.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: param.description }) ] }); } if (isDateParam(param)) { const dateTime = (schema == null ? void 0 : schema.format) === "date-time"; return /* @__PURE__ */ jsx(FieldWrapper, { param, fieldId, inline, children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [ /* @__PURE__ */ jsx( "input", { id: fieldId, type: dateTime ? "datetime-local" : "date", value: dateInputValue(value, dateTime), className: inputClassName, onChange: (event) => onChange(dateOutputValue(event.target.value, dateTime)) } ), value && /* @__PURE__ */ jsx( Button, { type: "button", variant: "outline", size: "sm", "aria-label": `Clear ${param.name}`, onClick: () => onChange(""), children: /* @__PURE__ */ jsx(Icon, { name: "codicon:close" }) } ) ] }) }); } const inputType = (schema == null ? void 0 : schema.type) === "integer" || (schema == null ? void 0 : schema.type) === "number" ? "number" : "text"; return /* @__PURE__ */ jsx(FieldWrapper, { param, fieldId, inline, children: /* @__PURE__ */ jsx( "input", { id: fieldId, type: inputType, value, className: inputClassName, onChange: (event) => onChange(event.target.value), placeholder: (schema == null ? void 0 : schema.default) != null ? String(schema.default) : param.description || param.name } ) }); } function TagInput({ id, value, placeholder, onChange }) { const tags = parseTags(value); function commit(raw, input) { const next = raw.split(",").map((part) => part.trim()).filter(Boolean); if (next.length === 0) return; onChange(serializeTags([...tags, ...next])); input.value = ""; } function remove(index) { onChange(serializeTags(tags.filter((_, i) => i !== index))); } function handleKeyDown(event) { const input = event.currentTarget; if (event.key === "Enter" || event.key === ",") { event.preventDefault(); commit(input.value, input); return; } if (event.key === "Backspace" && input.value === "" && tags.length > 0) { remove(tags.length - 1); } } return /* @__PURE__ */ jsxs("div", { className: "flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent px-2 py-1 shadow-sm", children: [ tags.map((tag, index) => /* @__PURE__ */ jsxs( "span", { className: "inline-flex h-6 max-w-full items-center gap-1 rounded-md bg-muted px-2 text-xs", children: [ /* @__PURE__ */ jsx("span", { className: "truncate", children: tag }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-muted-foreground hover:text-foreground", "aria-label": `Remove ${tag}`, onClick: () => remove(index), children: /* @__PURE__ */ jsx(Icon, { name: "codicon:close" }) } ) ] }, `${tag}-${index}` )), /* @__PURE__ */ jsx( "input", { id, className: "min-w-32 flex-1 bg-transparent px-1 py-1 text-sm outline-none", placeholder: tags.length === 0 ? placeholder : "", onKeyDown: handleKeyDown, onBlur: (event) => commit(event.currentTarget.value, event.currentTarget) } ) ] }); } function FieldWrapper({ param, fieldId, inline, children }) { const label = /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: "text-sm font-medium", children: [ param.name, param.required && /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" }) ] }); if (inline) { return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[10rem_1fr] items-start gap-x-3 gap-y-0.5", children: [ /* @__PURE__ */ jsx("div", { className: "flex h-9 items-center", children: label }), /* @__PURE__ */ jsx("div", { className: "min-w-0", children }), param.description && /* @__PURE__ */ jsx("p", { className: "col-start-2 text-xs text-muted-foreground", children: param.description }) ] }); } return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [ label, children, param.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: param.description }) ] }); } function normalizeParameters(parameters, path) { const pathParams = pathParamNames(path); const firstPathParam = pathParams[0]; const seen = new Set(parameters.map((param) => param.name)); const normalized = parameters.filter( (param) => !(param.name === "args" && firstPathParam != null && !seen.has(firstPathParam)) ); for (const name of pathParams) { if (seen.has(name)) continue; normalized.unshift({ name, in: "path", required: true, description: "Path parameter", schema: { type: "string" } }); } return normalized; } function pathParamNames(path) { return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).filter((name) => Boolean(name)); } function buildInitialState(parameters, path, overrides) { const state = {}; for (const p of parameters) { state[p.name] = initialParamValue(p, overrides == null ? void 0 : overrides[p.name]); } for (const name of pathParamNames(path)) { if (state[name] == null) { state[name] = (overrides == null ? void 0 : overrides[name]) ?? ""; } } return state; } function initialParamValue(param, override) { var _a; if (override != null) return sanitizeInitialValue(param, override); const value = (_a = param.schema) == null ? void 0 : _a.default; if (value == null) return ""; return sanitizeInitialValue(param, String(value)); } function sanitizeInitialValue(param, value) { const trimmed = value.trim(); if (trimmed === "[]" || trimmed === "null") return ""; if (isDateParam(param) && isZeroDate(trimmed)) return ""; return value; } function submitValue(param, value) { const trimmed = (value ?? "").trim(); if (!trimmed || trimmed === "[]" || trimmed === "null") return null; if (isDateParam(param) && isZeroDate(trimmed)) return null; return trimmed; } function splitArgsValue(value) { try { const parsed = JSON.parse(value); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch { } return value.split(",").map((part) => part.trim()).filter(Boolean); } function isMultiValueParam(param) { var _a, _b; const text = `${param.name} ${param.description ?? ""}`.toLowerCase(); return ((_a = param.schema) == null ? void 0 : _a.type) === "array" || String(((_b = param.schema) == null ? void 0 : _b.default) ?? "") === "[]" || text.includes("repeatable"); } function parseTags(value) { const trimmed = value.trim(); if (!trimmed || trimmed === "[]" || trimmed === "null") return []; try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch { } return trimmed.split(",").map((part) => part.trim()).filter(Boolean); } function serializeTags(tags) { return tags.join(","); } function isDateParam(param) { var _a, _b; const format = (_b = (_a = param.schema) == null ? void 0 : _a.format) == null ? void 0 : _b.toLowerCase(); const text = `${param.name} ${param.description ?? ""}`.toLowerCase(); return format === "date" || format === "date-time" || text.includes("date"); } function isZeroDate(value) { return value.startsWith("0001-01-01") || value === "0001-01-01"; } function dateInputValue(value, dateTime) { if (!value || value.startsWith("0001-01-01")) return ""; if (!dateTime) return value.slice(0, 10); const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value.slice(0, 16); const offset = parsed.getTimezoneOffset() * 6e4; return new Date(parsed.getTime() - offset).toISOString().slice(0, 16); } function dateOutputValue(value, dateTime) { if (!value) return ""; if (!dateTime) return value; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return parsed.toISOString(); } const inputClassName = "h-9 w-full min-w-0 rounded-md border border-input bg-background px-2 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring"; export { CommandForm, normalizeParameters, pathParamNames, submitValue }; //# sourceMappingURL=CommandForm.js.map