@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
JavaScript
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