fumadocs-openapi
Version:
Generate MDX docs for your OpenAPI spec
409 lines (406 loc) • 13.4 kB
JavaScript
'use client';
import { cn } from "../../utils/cn.js";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/components/select.js";
import { Input, labelVariants } from "../../ui/components/input.js";
import { getDefaultValue } from "../get-default-values.js";
import { FormatFlags, schemaToString } from "../../utils/schema-to-string.js";
import { anyFields, useFieldInfo, useResolvedSchema, useSchemaScope } from "../schema.js";
import { useState } from "react";
import { useController, useFieldArray, useFormContext } from "react-hook-form";
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
import { ChevronDown, Plus, Trash2, X } from "lucide-react";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
//#region src/playground/components/inputs.tsx
function FieldLabel(props) {
return /* @__PURE__ */ jsx("label", {
...props,
className: cn("w-full inline-flex items-center gap-0.5", props.className),
children: props.children
});
}
function FieldLabelName({ required = false, ...props }) {
return /* @__PURE__ */ jsxs("span", {
...props,
className: cn(labelVariants(), "font-mono me-auto", props.className),
children: [props.children, required && /* @__PURE__ */ jsx("span", {
className: "text-red-400/80 mx-1",
children: "*"
})]
});
}
function FieldLabelType(props) {
return /* @__PURE__ */ jsx("code", {
...props,
className: cn("text-xs text-fd-muted-foreground", props.className),
children: props.children
});
}
function ObjectInput({ field: _field, fieldName, ...props }) {
const field = useResolvedSchema(_field);
return /* @__PURE__ */ jsxs("div", {
...props,
className: cn("grid grid-cols-1 gap-4 @md:grid-cols-2", props.className),
children: [Object.entries(field.properties ?? {}).map(([key, child]) => /* @__PURE__ */ jsx(FieldSet, {
name: key,
field: child,
fieldName: `${fieldName}.${key}`,
isRequired: field.required?.includes(key)
}, key)), (field.additionalProperties || field.patternProperties) && /* @__PURE__ */ jsx(DynamicProperties, {
fieldName,
filterKey: (v) => !field.properties || !Object.keys(field.properties).includes(v),
getType: (key) => {
for (const pattern in field.patternProperties) if (key.match(RegExp(pattern))) return field.patternProperties[pattern];
if (field.additionalProperties) return field.additionalProperties;
return anyFields;
}
})]
});
}
function JsonInput({ fieldName }) {
const controller = useController({ name: fieldName });
const [error, setError] = useState(null);
const [value, setValue] = useState(() => JSON.stringify(controller.field.value, null, 2));
return /* @__PURE__ */ jsxs("div", {
className: "flex flex-col bg-fd-secondary text-fd-secondary-foreground overflow-hidden border rounded-lg",
children: [/* @__PURE__ */ jsx("textarea", {
...controller.field,
value,
className: "p-2 h-[240px] text-sm font-mono resize-none focus-visible:outline-none",
onChange: (v) => {
setValue(v.target.value);
try {
controller.field.onChange(JSON.parse(v.target.value));
setError(null);
} catch (e) {
if (e instanceof Error) setError(e.message);
}
}
}), /* @__PURE__ */ jsx("p", {
className: "p-2 text-xs font-mono border-t text-red-400 empty:hidden",
children: error
})]
});
}
function DynamicProperties({ fieldName, filterKey = () => true, getType = () => anyFields }) {
const { control, setValue, getValues } = useFormContext();
const [nextName, setNextName] = useState("");
const [properties, setProperties] = useState(() => {
const value = getValues(fieldName);
if (value) return Object.keys(value).filter(filterKey);
return [];
});
const onAppend = () => {
const name = nextName.trim();
if (name.length === 0) return;
setProperties((p) => {
if (p.includes(name) || !filterKey(name)) return p;
const type = getType(name);
setValue(`${fieldName}.${name}`, getDefaultValue(type));
setNextName("");
return [...p, name];
});
};
return /* @__PURE__ */ jsxs(Fragment$1, { children: [properties.map((item) => {
return /* @__PURE__ */ jsx(FieldSet, {
name: item,
field: getType(item),
fieldName: `${fieldName}.${item}`,
toolbar: /* @__PURE__ */ jsx("button", {
type: "button",
"aria-label": "Remove Item",
className: cn(buttonVariants({
color: "outline",
size: "icon-xs"
})),
onClick: () => {
setProperties((p) => p.filter((prop) => prop !== item));
control.unregister(`${fieldName}.${item}`);
},
children: /* @__PURE__ */ jsx(Trash2, {})
})
}, item);
}), /* @__PURE__ */ jsxs("div", {
className: "flex gap-2 col-span-full",
children: [/* @__PURE__ */ jsx(Input, {
value: nextName,
placeholder: "Enter Property Name",
onChange: (e) => setNextName(e.target.value),
onKeyDown: (e) => {
if (e.key === "Enter") {
onAppend();
e.preventDefault();
}
}
}), /* @__PURE__ */ jsx("button", {
type: "button",
className: cn(buttonVariants({
color: "secondary",
size: "sm"
}), "px-4"),
onClick: onAppend,
children: "New"
})]
})] });
}
function FieldInput({ field, fieldName, isRequired, ...props }) {
const form = useFormContext();
const { field: { value, onChange, ...restField }, fieldState } = useController({
control: form.control,
name: fieldName
});
if (field.type === "null") return;
if (field.type === "string" && field.format === "binary") return /* @__PURE__ */ jsxs("div", {
...props,
children: [/* @__PURE__ */ jsx("label", {
htmlFor: fieldName,
className: cn(buttonVariants({
color: "secondary",
className: "w-full h-9 gap-2 truncate"
})),
children: value instanceof File ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
className: "text-fd-muted-foreground text-xs",
children: "Selected"
}), /* @__PURE__ */ jsx("span", {
className: "truncate w-0 flex-1 text-end",
children: value.name
})] }) : /* @__PURE__ */ jsx("span", {
className: "text-fd-muted-foreground",
children: "Upload"
})
}), /* @__PURE__ */ jsx("input", {
id: fieldName,
type: "file",
multiple: false,
onChange: (e) => {
if (!e.target.files) return;
onChange(e.target.files.item(0));
},
hidden: true,
...restField
})]
});
if (field.type === "boolean") return /* @__PURE__ */ jsxs(Select, {
value: String(value),
onValueChange: (value$1) => onChange(value$1 === "undefined" ? void 0 : value$1 === "true"),
disabled: restField.disabled,
children: [/* @__PURE__ */ jsx(SelectTrigger, {
id: fieldName,
className: props.className,
...restField,
children: /* @__PURE__ */ jsx(SelectValue, {})
}), /* @__PURE__ */ jsxs(SelectContent, { children: [
/* @__PURE__ */ jsx(SelectItem, {
value: "true",
children: "True"
}),
/* @__PURE__ */ jsx(SelectItem, {
value: "false",
children: "False"
}),
!isRequired && /* @__PURE__ */ jsx(SelectItem, {
value: "undefined",
children: "Unset"
})
] })]
});
const isNumber = field.type === "integer" || field.type === "number";
return /* @__PURE__ */ jsxs("div", {
...props,
className: cn("flex flex-row gap-2", props.className),
children: [/* @__PURE__ */ jsx(Input, {
id: fieldName,
placeholder: "Enter value",
type: isNumber ? "number" : "text",
step: field.type === "integer" ? 1 : void 0,
value: value ?? "",
onChange: (e) => {
if (isNumber && !Number.isNaN(e.target.valueAsNumber)) onChange(e.target.valueAsNumber);
else if (!isNumber) onChange(e.target.value);
},
...restField
}), fieldState.isDirty && /* @__PURE__ */ jsx("button", {
type: "button",
onClick: () => form.resetField(fieldName),
className: "text-fd-muted-foreground",
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
})]
});
}
function FieldSet({ field: _field, fieldName, toolbar, name, isRequired, depth = 0, slotType, collapsible = true, ...props }) {
const { readOnly, writeOnly } = useSchemaScope();
const field = useResolvedSchema(_field);
const [show, setShow] = useState(!collapsible);
const { info, updateInfo } = useFieldInfo(fieldName, field, depth);
if (_field === false) return;
if (field.readOnly && !readOnly) return;
if (field.writeOnly && !writeOnly) return;
if (info.unionField) {
const union = field[info.unionField];
const showSelect = union.length > 1;
return /* @__PURE__ */ jsx(FieldSet, {
...props,
name,
fieldName,
isRequired,
field: union[info.oneOf],
depth: depth + 1,
slotType: showSelect ? false : slotType,
toolbar: /* @__PURE__ */ jsxs(Fragment$1, { children: [showSelect && /* @__PURE__ */ jsx("select", {
className: "text-xs font-mono",
value: info.oneOf,
onChange: (e) => {
updateInfo({ oneOf: Number(e.target.value) });
},
children: union.map((item, i) => /* @__PURE__ */ jsx("option", {
value: i,
className: "bg-fd-popover text-fd-popover-foreground",
children: schemaToString(item, void 0, FormatFlags.UseAlias)
}, i))
}), toolbar] })
});
}
if (Array.isArray(field.type)) {
const showSelect = field.type.length > 1;
return /* @__PURE__ */ jsx(FieldSet, {
...props,
name,
fieldName,
isRequired,
field: {
...field,
type: info.selectedType
},
depth: depth + 1,
slotType: showSelect ? false : slotType,
toolbar: /* @__PURE__ */ jsxs(Fragment$1, { children: [showSelect && /* @__PURE__ */ jsx("select", {
className: "text-xs font-mono",
value: info.selectedType,
onChange: (e) => {
updateInfo({ selectedType: e.target.value });
},
children: field.type.map((item) => /* @__PURE__ */ jsx("option", {
value: item,
className: "bg-fd-popover text-fd-popover-foreground",
children: item
}, item))
}), toolbar] })
});
}
const showBn = collapsible && /* @__PURE__ */ jsx("button", {
type: "button",
onClick: () => setShow((prev) => !prev),
className: cn(buttonVariants({
size: "icon-xs",
color: "ghost",
className: "text-fd-muted-foreground -ms-1"
})),
children: /* @__PURE__ */ jsx(ChevronDown, { className: cn(show && "rotate-180") })
});
if (field.type === "object" || info.intersection) return /* @__PURE__ */ jsxs("fieldset", {
...props,
className: cn("flex flex-col gap-1.5 col-span-full @container", props.className),
children: [/* @__PURE__ */ jsxs(FieldLabel, {
htmlFor: fieldName,
children: [
showBn,
/* @__PURE__ */ jsx(FieldLabelName, {
required: isRequired,
children: name
}),
slotType ?? /* @__PURE__ */ jsx(FieldLabelType, { children: schemaToString(field) }),
toolbar
]
}), show && /* @__PURE__ */ jsx(ObjectInput, {
field: info.intersection?.merged ?? field,
fieldName,
...props,
className: cn("rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm", props.className)
})]
});
if (field.type === "array") return /* @__PURE__ */ jsxs("fieldset", {
...props,
className: cn("flex flex-col gap-1.5 col-span-full", props.className),
children: [/* @__PURE__ */ jsxs(FieldLabel, {
htmlFor: fieldName,
children: [
showBn,
/* @__PURE__ */ jsx(FieldLabelName, {
required: isRequired,
children: name
}),
slotType ?? /* @__PURE__ */ jsx(FieldLabelType, { children: schemaToString(field) }),
toolbar
]
}), show && /* @__PURE__ */ jsx(ArrayInput, {
fieldName,
items: field.items ?? anyFields,
...props,
className: cn("rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm", props.className)
})]
});
return /* @__PURE__ */ jsxs("fieldset", {
...props,
className: cn("flex flex-col gap-1.5", props.className),
children: [/* @__PURE__ */ jsxs(FieldLabel, {
htmlFor: fieldName,
children: [
/* @__PURE__ */ jsx(FieldLabelName, {
required: isRequired,
children: name
}),
slotType ?? /* @__PURE__ */ jsx(FieldLabelType, { children: schemaToString(field) }),
toolbar
]
}), /* @__PURE__ */ jsx(FieldInput, {
field,
fieldName,
isRequired
})]
});
}
function ArrayInput({ fieldName, items, ...props }) {
const name = fieldName.split(".").at(-1) ?? "";
const { fields, append, remove } = useFieldArray({ name: fieldName });
return /* @__PURE__ */ jsxs("div", {
...props,
className: cn("flex flex-col gap-2", props.className),
children: [fields.map((item, index) => /* @__PURE__ */ jsx(FieldSet, {
name: /* @__PURE__ */ jsxs("span", {
className: "text-fd-muted-foreground",
children: [
name,
"[",
index,
"]"
]
}),
field: items,
isRequired: true,
fieldName: `${fieldName}.${index}`,
toolbar: /* @__PURE__ */ jsx("button", {
type: "button",
"aria-label": "Remove Item",
className: cn(buttonVariants({
color: "outline",
size: "icon-xs"
})),
onClick: () => remove(index),
children: /* @__PURE__ */ jsx(Trash2, {})
})
}, item.id)), /* @__PURE__ */ jsxs("button", {
type: "button",
className: cn(buttonVariants({
color: "secondary",
className: "gap-1.5 py-2",
size: "sm"
})),
onClick: () => {
append(getDefaultValue(items));
},
children: [/* @__PURE__ */ jsx(Plus, { className: "size-4" }), "New Item"]
})]
});
}
//#endregion
export { FieldInput, FieldSet, JsonInput, ObjectInput };
//# sourceMappingURL=inputs.js.map