mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-24 17:18:23 +03:00
default column layout and add field sizing
This commit is contained in:
parent
3042c36168
commit
d3924e2cff
@ -147,6 +147,35 @@ const applyUiSchemaPathOverrides = (
|
|||||||
return updated;
|
return updated;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyLayoutGridFieldDefaults = (uiSchema: UiSchema): UiSchema => {
|
||||||
|
const applyDefaults = (node: unknown): unknown => {
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
return node.map((item) => applyDefaults(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof node !== "object" || node === null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNode: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
Object.entries(node).forEach(([key, value]) => {
|
||||||
|
nextNode[key] = applyDefaults(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(nextNode["ui:layoutGrid"]) &&
|
||||||
|
nextNode["ui:field"] === undefined
|
||||||
|
) {
|
||||||
|
nextNode["ui:field"] = "LayoutGridField";
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
return applyDefaults(uiSchema) as UiSchema;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ConfigFormProps {
|
export interface ConfigFormProps {
|
||||||
/** JSON Schema for the form */
|
/** JSON Schema for the form */
|
||||||
schema: RJSFSchema;
|
schema: RJSFSchema;
|
||||||
@ -245,7 +274,9 @@ export function ConfigForm({
|
|||||||
transformedSchema,
|
transformedSchema,
|
||||||
pathOverrides,
|
pathOverrides,
|
||||||
);
|
);
|
||||||
const merged = mergeUiSchema(expandedUiSchema, baseUiSchema);
|
const merged = applyLayoutGridFieldDefaults(
|
||||||
|
mergeUiSchema(expandedUiSchema, baseUiSchema),
|
||||||
|
);
|
||||||
|
|
||||||
// Add field groups
|
// Add field groups
|
||||||
if (fieldGroups) {
|
if (fieldGroups) {
|
||||||
@ -311,7 +342,7 @@ export function ConfigForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("config-form", className)}>
|
<div className={cn("config-form w-full max-w-5xl", className)}>
|
||||||
<Form
|
<Form
|
||||||
schema={transformedSchema}
|
schema={transformedSchema}
|
||||||
uiSchema={finalUiSchema}
|
uiSchema={finalUiSchema}
|
||||||
|
|||||||
@ -32,6 +32,12 @@ const auth: SectionConfigOverrides = {
|
|||||||
reset_admin_password: {
|
reset_admin_password: {
|
||||||
"ui:widget": "switch",
|
"ui:widget": "switch",
|
||||||
},
|
},
|
||||||
|
native_oauth_url: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
failed_login_rate_limit: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,11 @@ const database: SectionConfigOverrides = {
|
|||||||
restartRequired: [],
|
restartRequired: [],
|
||||||
fieldOrder: ["path"],
|
fieldOrder: ["path"],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
|
uiSchema: {
|
||||||
|
path: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
|
|||||||
const detect: SectionConfigOverrides = {
|
const detect: SectionConfigOverrides = {
|
||||||
base: {
|
base: {
|
||||||
sectionDocs: "/configuration/camera_specific",
|
sectionDocs: "/configuration/camera_specific",
|
||||||
|
fieldOrder: [
|
||||||
|
"enabled",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"fps",
|
||||||
|
"min_initialized",
|
||||||
|
"max_disappeared",
|
||||||
|
"annotation_offset",
|
||||||
|
"stationary",
|
||||||
|
"interval",
|
||||||
|
"threshold",
|
||||||
|
"max_frames",
|
||||||
|
],
|
||||||
restartRequired: [
|
restartRequired: [
|
||||||
"width",
|
"width",
|
||||||
"height",
|
"height",
|
||||||
@ -12,7 +25,7 @@ const detect: SectionConfigOverrides = {
|
|||||||
"stationary",
|
"stationary",
|
||||||
],
|
],
|
||||||
fieldGroups: {
|
fieldGroups: {
|
||||||
resolution: ["width", "height", "fps"],
|
resolution: ["enabled", "width", "height", "fps"],
|
||||||
tracking: ["min_initialized", "max_disappeared"],
|
tracking: ["min_initialized", "max_disappeared"],
|
||||||
},
|
},
|
||||||
hiddenFields: ["enabled_in_config"],
|
hiddenFields: ["enabled_in_config"],
|
||||||
@ -22,46 +35,6 @@ const detect: SectionConfigOverrides = {
|
|||||||
"annotation_offset",
|
"annotation_offset",
|
||||||
"stationary",
|
"stationary",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
|
||||||
"ui:field": "LayoutGridField",
|
|
||||||
"ui:layoutGrid": [
|
|
||||||
{
|
|
||||||
"ui:row": [{ enabled: { "ui:col": "col-span-12 lg:col-span-4" } }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ui:row": [
|
|
||||||
{ width: { "ui:col": "col-span-12 md:col-span-4" } },
|
|
||||||
{ height: { "ui:col": "col-span-12 md:col-span-4" } },
|
|
||||||
{ fps: { "ui:col": "col-span-12 md:col-span-4" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ui:row": [
|
|
||||||
{ min_initialized: { "ui:col": "col-span-12 md:col-span-3" } },
|
|
||||||
{ max_disappeared: { "ui:col": "col-span-12 md:col-span-3" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ui:row": [
|
|
||||||
{ annotation_offset: { "ui:col": "col-span-12 md:col-span-3" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ui:row": [{ stationary: { "ui:col": "col-span-12" } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stationary: {
|
|
||||||
"ui:field": "LayoutGridField",
|
|
||||||
"ui:layoutGrid": [
|
|
||||||
{
|
|
||||||
"ui:row": [
|
|
||||||
{ interval: { "ui:col": "col-span-12 md:col-span-3" } },
|
|
||||||
{ threshold: { "ui:col": "col-span-12 md:col-span-3" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,11 @@ const environmentVars: SectionConfigOverrides = {
|
|||||||
restartRequired: [],
|
restartRequired: [],
|
||||||
fieldOrder: [],
|
fieldOrder: [],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
|
uiSchema: {
|
||||||
|
additionalProperties: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,9 @@ const ffmpeg: SectionConfigOverrides = {
|
|||||||
"gpu",
|
"gpu",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
|
path: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
global_args: arrayAsTextWidget,
|
global_args: arrayAsTextWidget,
|
||||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
input_args: ffmpegArgsWidget("input_args"),
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
@ -71,6 +74,9 @@ const ffmpeg: SectionConfigOverrides = {
|
|||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
items: {
|
items: {
|
||||||
|
path: {
|
||||||
|
"ui:options": { size: "full" },
|
||||||
|
},
|
||||||
global_args: arrayAsTextWidget,
|
global_args: arrayAsTextWidget,
|
||||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
input_args: ffmpegArgsWidget("input_args"),
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
@ -104,6 +110,9 @@ const ffmpeg: SectionConfigOverrides = {
|
|||||||
"gpu",
|
"gpu",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
|
path: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
global_args: arrayAsTextWidget,
|
global_args: arrayAsTextWidget,
|
||||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||||
input_args: ffmpegArgsWidget("input_args"),
|
input_args: ffmpegArgsWidget("input_args"),
|
||||||
|
|||||||
@ -14,6 +14,27 @@ const genai: SectionConfigOverrides = {
|
|||||||
],
|
],
|
||||||
advancedFields: ["base_url", "provider_options", "runtime_options"],
|
advancedFields: ["base_url", "provider_options", "runtime_options"],
|
||||||
hiddenFields: ["genai.enabled_in_config"],
|
hiddenFields: ["genai.enabled_in_config"],
|
||||||
|
uiSchema: {
|
||||||
|
api_key: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
base_url: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
provider_options: {
|
||||||
|
additionalProperties: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime_options: {
|
||||||
|
additionalProperties: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,9 @@ const lpr: SectionConfigOverrides = {
|
|||||||
"replace_rules",
|
"replace_rules",
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
|
format: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
replace_rules: {
|
replace_rules: {
|
||||||
"ui:field": "ReplaceRulesField",
|
"ui:field": "ReplaceRulesField",
|
||||||
"ui:options": {
|
"ui:options": {
|
||||||
|
|||||||
@ -28,6 +28,14 @@ const model: SectionConfigOverrides = {
|
|||||||
"non_logo_attributes",
|
"non_logo_attributes",
|
||||||
"plus",
|
"plus",
|
||||||
],
|
],
|
||||||
|
uiSchema: {
|
||||||
|
path: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
labelmap_path: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,26 @@ const mqtt: SectionConfigOverrides = {
|
|||||||
"tls_insecure",
|
"tls_insecure",
|
||||||
],
|
],
|
||||||
liveValidate: true,
|
liveValidate: true,
|
||||||
uiSchema: {},
|
uiSchema: {
|
||||||
|
host: {
|
||||||
|
"ui:options": { size: "sm" },
|
||||||
|
},
|
||||||
|
topic_prefix: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
client_id: {
|
||||||
|
"ui:options": { size: "sm" },
|
||||||
|
},
|
||||||
|
tls_ca_certs: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
tls_client_cert: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
tls_client_key: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,13 @@ const networking: SectionConfigOverrides = {
|
|||||||
"listen.internal": {
|
"listen.internal": {
|
||||||
"ui:options": {
|
"ui:options": {
|
||||||
suppressMultiSchema: true,
|
suppressMultiSchema: true,
|
||||||
|
size: "sm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"listen.external": {
|
"listen.external": {
|
||||||
"ui:options": {
|
"ui:options": {
|
||||||
suppressMultiSchema: true,
|
suppressMultiSchema: true,
|
||||||
|
size: "sm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -59,6 +59,16 @@ const objects: SectionConfigOverrides = {
|
|||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
"ui:widget": "textarea",
|
"ui:widget": "textarea",
|
||||||
|
"ui:options": {
|
||||||
|
size: "full",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object_prompts: {
|
||||||
|
additionalProperties: {
|
||||||
|
"ui:options": {
|
||||||
|
size: "full",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required_zones: {
|
required_zones: {
|
||||||
"ui:widget": "zoneNames",
|
"ui:widget": "zoneNames",
|
||||||
|
|||||||
@ -20,6 +20,9 @@ const onvif: SectionConfigOverrides = {
|
|||||||
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
|
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
|
||||||
overrideFields: [],
|
overrideFields: [],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
|
host: {
|
||||||
|
"ui:options": { size: "sm" },
|
||||||
|
},
|
||||||
autotracking: {
|
autotracking: {
|
||||||
required_zones: {
|
required_zones: {
|
||||||
"ui:widget": "zoneNames",
|
"ui:widget": "zoneNames",
|
||||||
|
|||||||
@ -14,6 +14,12 @@ const proxy: SectionConfigOverrides = {
|
|||||||
advancedFields: ["header_map", "auth_secret", "separator"],
|
advancedFields: ["header_map", "auth_secret", "separator"],
|
||||||
liveValidate: true,
|
liveValidate: true,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
|
logout_url: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
auth_secret: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
header_map: {
|
header_map: {
|
||||||
"ui:after": { render: "ProxyRoleMap" },
|
"ui:after": { render: "ProxyRoleMap" },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,6 +20,13 @@ const record: SectionConfigOverrides = {
|
|||||||
},
|
},
|
||||||
hiddenFields: ["enabled_in_config", "sync_recordings"],
|
hiddenFields: ["enabled_in_config", "sync_recordings"],
|
||||||
advancedFields: ["expire_interval", "preview", "export"],
|
advancedFields: ["expire_interval", "preview", "export"],
|
||||||
|
uiSchema: {
|
||||||
|
export: {
|
||||||
|
hwaccel_args: {
|
||||||
|
"ui:options": { size: "lg" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,9 +29,15 @@ const review: SectionConfigOverrides = {
|
|||||||
genai: {
|
genai: {
|
||||||
additional_concerns: {
|
additional_concerns: {
|
||||||
"ui:widget": "textarea",
|
"ui:widget": "textarea",
|
||||||
|
"ui:options": {
|
||||||
|
size: "full",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
activity_context_prompt: {
|
activity_context_prompt: {
|
||||||
"ui:widget": "textarea",
|
"ui:widget": "textarea",
|
||||||
|
"ui:options": {
|
||||||
|
size: "full",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,14 @@ const tls: SectionConfigOverrides = {
|
|||||||
restartRequired: [],
|
restartRequired: [],
|
||||||
fieldOrder: ["enabled", "cert", "key"],
|
fieldOrder: ["enabled", "cert", "key"],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
|
uiSchema: {
|
||||||
|
cert: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
"ui:options": { size: "md" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export function AdvancedCollapsible({
|
|||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-4 pt-2">
|
<CollapsibleContent className="mt-2 space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
|
||||||
{children}
|
{children}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@ -114,6 +114,13 @@ interface PropertyElement {
|
|||||||
content: React.ReactElement;
|
content: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isObjectLikeElement(item: PropertyElement) {
|
||||||
|
const fieldSchema = item.content.props?.schema as
|
||||||
|
| { type?: string | string[] }
|
||||||
|
| undefined;
|
||||||
|
return fieldSchema?.type === "object";
|
||||||
|
}
|
||||||
|
|
||||||
// Custom ObjectFieldTemplate wrapper that applies grid layout
|
// Custom ObjectFieldTemplate wrapper that applies grid layout
|
||||||
function GridLayoutObjectFieldTemplate(
|
function GridLayoutObjectFieldTemplate(
|
||||||
props: ObjectFieldTemplateProps,
|
props: ObjectFieldTemplateProps,
|
||||||
@ -361,9 +368,16 @@ function GridLayoutObjectFieldTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={rowIndex} className="space-y-4">
|
<div
|
||||||
|
key={rowIndex}
|
||||||
|
className={cn(
|
||||||
|
"space-y-4",
|
||||||
|
rowGroupKey &&
|
||||||
|
"rounded-lg border border-border/70 bg-card/30 p-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{showGroupLabel && (
|
{showGroupLabel && (
|
||||||
<div className="text-md font-medium text-primary">
|
<div className="border-b border-border/60 pb-2 text-sm font-semibold text-primary-variant">
|
||||||
{getGroupLabel(rowGroupKey)}
|
{getGroupLabel(rowGroupKey)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -404,9 +418,12 @@ function GridLayoutObjectFieldTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
leftoverSections.push(
|
leftoverSections.push(
|
||||||
<div key={groupKey} className="space-y-6">
|
<div
|
||||||
|
key={groupKey}
|
||||||
|
className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4"
|
||||||
|
>
|
||||||
{showGroupLabel && (
|
{showGroupLabel && (
|
||||||
<div className="text-md font-medium text-primary">
|
<div className="border-b border-border/60 pb-2 text-sm font-semibold text-primary-variant">
|
||||||
{getGroupLabel(groupKey)}
|
{getGroupLabel(groupKey)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -429,7 +446,16 @@ function GridLayoutObjectFieldTemplate(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ungroupedLeftovers.map((item) => (
|
{ungroupedLeftovers.map((item) => (
|
||||||
<div key={item.name}>{item.content}</div>
|
<div
|
||||||
|
key={item.name}
|
||||||
|
className={cn(
|
||||||
|
groupedLeftovers.size > 0 &&
|
||||||
|
!isObjectLikeElement(item) &&
|
||||||
|
"px-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// Base Input Template - default input wrapper
|
// Base Input Template - default input wrapper
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
|
||||||
export function BaseInputTemplate(props: WidgetProps) {
|
export function BaseInputTemplate(props: WidgetProps) {
|
||||||
const {
|
const {
|
||||||
@ -14,9 +15,11 @@ export function BaseInputTemplate(props: WidgetProps) {
|
|||||||
onFocus,
|
onFocus,
|
||||||
placeholder,
|
placeholder,
|
||||||
schema,
|
schema,
|
||||||
|
options,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const inputType = type || "text";
|
const inputType = type || "text";
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "xs");
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
@ -32,6 +35,7 @@ export function BaseInputTemplate(props: WidgetProps) {
|
|||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
|
className={fieldClassName}
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
|
|||||||
@ -101,7 +101,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
const suppressDescription = uiOptionsFromSchema.suppressDescription === true;
|
const suppressDescription = uiOptionsFromSchema.suppressDescription === true;
|
||||||
|
|
||||||
// Determine field characteristics
|
// Determine field characteristics
|
||||||
const isAdvanced = uiOptionsFromSchema.advanced === true;
|
|
||||||
const isBoolean =
|
const isBoolean =
|
||||||
schema.type === "boolean" ||
|
schema.type === "boolean" ||
|
||||||
(Array.isArray(schema.type) && schema.type.includes("boolean"));
|
(Array.isArray(schema.type) && schema.type.includes("boolean"));
|
||||||
@ -111,11 +110,33 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
const suppressMultiSchema =
|
const suppressMultiSchema =
|
||||||
(uiSchema?.["ui:options"] as UiSchema["ui:options"] | undefined)
|
(uiSchema?.["ui:options"] as UiSchema["ui:options"] | undefined)
|
||||||
?.suppressMultiSchema === true;
|
?.suppressMultiSchema === true;
|
||||||
|
const schemaTypes = Array.isArray(schema.type)
|
||||||
|
? schema.type
|
||||||
|
: schema.type
|
||||||
|
? [schema.type]
|
||||||
|
: [];
|
||||||
|
const nonNullSchemaTypes = schemaTypes.filter((type) => type !== "null");
|
||||||
|
const isScalarValueField =
|
||||||
|
nonNullSchemaTypes.length === 1 &&
|
||||||
|
["string", "number", "integer"].includes(nonNullSchemaTypes[0]);
|
||||||
|
|
||||||
// Only suppress labels/descriptions if this is a multi-schema field (anyOf/oneOf) with suppressMultiSchema flag
|
// Only suppress labels/descriptions if this is a multi-schema field (anyOf/oneOf) with suppressMultiSchema flag
|
||||||
// This prevents duplicate labels while still showing the inner field's label
|
// This prevents duplicate labels while still showing the inner field's label
|
||||||
const isMultiSchemaWrapper =
|
const isMultiSchemaWrapper =
|
||||||
(schema.anyOf || schema.oneOf) && (suppressMultiSchema || isNullableUnion);
|
(schema.anyOf || schema.oneOf) && (suppressMultiSchema || isNullableUnion);
|
||||||
|
const useSplitBooleanLayout =
|
||||||
|
uiOptionsFromSchema.splitLayout !== false &&
|
||||||
|
isBoolean &&
|
||||||
|
!isMultiSchemaWrapper &&
|
||||||
|
!isObjectField &&
|
||||||
|
!isAdditionalProperty;
|
||||||
|
const useSplitLayout =
|
||||||
|
uiOptionsFromSchema.splitLayout !== false &&
|
||||||
|
isScalarValueField &&
|
||||||
|
!isBoolean &&
|
||||||
|
!isMultiSchemaWrapper &&
|
||||||
|
!isObjectField &&
|
||||||
|
!isAdditionalProperty;
|
||||||
|
|
||||||
// Get translation path for this field
|
// Get translation path for this field
|
||||||
const pathSegments = fieldPathId.path.filter(
|
const pathSegments = fieldPathId.path.filter(
|
||||||
@ -379,17 +400,11 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
{beforeContent}
|
{beforeContent}
|
||||||
<div
|
<div className={cn("space-y-1")} data-field-id={translationPath}>
|
||||||
className={cn(
|
|
||||||
"space-y-1",
|
|
||||||
isAdvanced && "border-l-2 border-muted pl-4",
|
|
||||||
isBoolean && "flex items-center justify-between gap-4",
|
|
||||||
)}
|
|
||||||
data-field-id={translationPath}
|
|
||||||
>
|
|
||||||
{displayLabel &&
|
{displayLabel &&
|
||||||
finalLabel &&
|
finalLabel &&
|
||||||
!isBoolean &&
|
!isBoolean &&
|
||||||
|
!useSplitLayout &&
|
||||||
!isMultiSchemaWrapper &&
|
!isMultiSchemaWrapper &&
|
||||||
!isObjectField &&
|
!isObjectField &&
|
||||||
!isAdditionalProperty && (
|
!isAdditionalProperty && (
|
||||||
@ -409,29 +424,180 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isBoolean ? (
|
{isBoolean ? (
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
useSplitBooleanLayout ? (
|
||||||
<div className="space-y-0.5">
|
<>
|
||||||
{displayLabel && finalLabel && (
|
<div className="space-y-1.5 md:hidden">
|
||||||
<Label
|
<div className="flex items-center justify-between gap-4">
|
||||||
htmlFor={id}
|
{displayLabel && finalLabel && (
|
||||||
className={cn(
|
<Label
|
||||||
"text-sm font-medium",
|
htmlFor={id}
|
||||||
isModified && "text-danger",
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
isModified && "text-danger",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{finalLabel}
|
||||||
|
{required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
)}
|
)}
|
||||||
>
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
{finalLabel}
|
</div>
|
||||||
{required && (
|
{finalDescription && shouldShowDescription && (
|
||||||
<span className="ml-1 text-destructive">*</span>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{finalDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{fieldDocsUrl && shouldShowDescription && (
|
||||||
|
<div className="flex items-center text-xs text-primary-variant">
|
||||||
|
<Link
|
||||||
|
to={fieldDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:grid md:grid-cols-[minmax(14rem,22rem)_minmax(0,1fr)] md:items-start md:gap-x-6">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{displayLabel && finalLabel && (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
isModified && "text-danger",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{finalLabel}
|
||||||
|
{required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
)}
|
)}
|
||||||
</Label>
|
{finalDescription && shouldShowDescription && (
|
||||||
)}
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{finalDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{fieldDocsUrl && shouldShowDescription && (
|
||||||
|
<div className="flex items-center text-xs text-primary-variant">
|
||||||
|
<Link
|
||||||
|
to={fieldDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{displayLabel && finalLabel && (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
isModified && "text-danger",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{finalLabel}
|
||||||
|
{required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
{finalDescription && shouldShowDescription && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{finalDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{fieldDocsUrl && shouldShowDescription && (
|
||||||
|
<div className="flex items-center text-xs text-primary-variant">
|
||||||
|
<Link
|
||||||
|
to={fieldDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : useSplitLayout ? (
|
||||||
|
<div className="space-y-3 md:grid md:grid-cols-[minmax(14rem,22rem)_minmax(0,1fr)] md:items-start md:gap-x-6 md:space-y-0">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{displayLabel &&
|
||||||
|
finalLabel &&
|
||||||
|
!isMultiSchemaWrapper &&
|
||||||
|
!isObjectField &&
|
||||||
|
!isAdditionalProperty && (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
isModified && "text-danger",
|
||||||
|
errors &&
|
||||||
|
errors.props?.errors?.length > 0 &&
|
||||||
|
"text-destructive",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{finalLabel}
|
||||||
|
{required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
{finalDescription && shouldShowDescription && (
|
{finalDescription && shouldShowDescription && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="hidden text-xs text-muted-foreground md:block">
|
||||||
{finalDescription}
|
{finalDescription}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{fieldDocsUrl && shouldShowDescription && (
|
{fieldDocsUrl && shouldShowDescription && (
|
||||||
<div className="flex items-center text-xs text-primary-variant">
|
<div className="hidden items-center text-xs text-primary-variant md:flex">
|
||||||
|
<Link
|
||||||
|
to={fieldDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-2xl space-y-1">
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{finalDescription && shouldShowDescription && (
|
||||||
|
<p className="text-xs text-muted-foreground md:hidden">
|
||||||
|
{finalDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{fieldDocsUrl && shouldShowDescription && (
|
||||||
|
<div className="flex items-center text-xs text-primary-variant md:hidden">
|
||||||
<Link
|
<Link
|
||||||
to={fieldDocsUrl}
|
to={fieldDocsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -444,7 +610,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -323,12 +323,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
||||||
|
const isObjectLikeField = (item: (typeof properties)[number]) => {
|
||||||
|
const fieldSchema = item.content.props.schema as
|
||||||
|
| { type?: string | string[] }
|
||||||
|
| undefined;
|
||||||
|
return fieldSchema?.type === "object";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<div key={group.key} className="space-y-6">
|
<div
|
||||||
<div className="text-md font-medium text-primary">
|
key={group.key}
|
||||||
|
className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4"
|
||||||
|
>
|
||||||
|
<div className="text-md border-b border-border/60 pb-4 font-semibold text-primary-variant">
|
||||||
{group.label}
|
{group.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -342,7 +351,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
{ungrouped.length > 0 && (
|
{ungrouped.length > 0 && (
|
||||||
<div className={cn("space-y-6", groups.length > 0 && "pt-2")}>
|
<div className={cn("space-y-6", groups.length > 0 && "pt-2")}>
|
||||||
{ungrouped.map((element) => (
|
{ungrouped.map((element) => (
|
||||||
<div key={element.name}>{element.content}</div>
|
<div
|
||||||
|
key={element.name}
|
||||||
|
className={cn(
|
||||||
|
groups.length > 0 && !isObjectLikeField(element) && "px-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{element.content}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
37
web/src/components/config-form/theme/utils/fieldSizing.ts
Normal file
37
web/src/components/config-form/theme/utils/fieldSizing.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const FIELD_SIZE_CLASS_MAP = {
|
||||||
|
xs: "max-w-xs",
|
||||||
|
sm: "max-w-sm",
|
||||||
|
md: "max-w-md",
|
||||||
|
lg: "max-w-2xl",
|
||||||
|
full: "max-w-full",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FieldSizeOption = keyof typeof FIELD_SIZE_CLASS_MAP;
|
||||||
|
|
||||||
|
type FieldSizingOptions = {
|
||||||
|
size?: FieldSizeOption;
|
||||||
|
maxWidthClassName?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSizedFieldClassName(
|
||||||
|
options: unknown,
|
||||||
|
defaultSize: FieldSizeOption = "lg",
|
||||||
|
) {
|
||||||
|
const sizingOptions =
|
||||||
|
typeof options === "object" && options !== null
|
||||||
|
? (options as FieldSizingOptions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const sizeClass =
|
||||||
|
FIELD_SIZE_CLASS_MAP[sizingOptions?.size ?? defaultSize] ??
|
||||||
|
FIELD_SIZE_CLASS_MAP[defaultSize];
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
"w-full",
|
||||||
|
sizingOptions?.maxWidthClassName ?? sizeClass,
|
||||||
|
sizingOptions?.className,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,3 +15,4 @@ export {
|
|||||||
normalizeFieldValue,
|
normalizeFieldValue,
|
||||||
isSubtreeModified,
|
isSubtreeModified,
|
||||||
} from "./overrides";
|
} from "./overrides";
|
||||||
|
export { getSizedFieldClassName } from "./fieldSizing";
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
|
||||||
export function PasswordWidget(props: WidgetProps) {
|
export function PasswordWidget(props: WidgetProps) {
|
||||||
const {
|
const {
|
||||||
@ -16,12 +18,14 @@ export function PasswordWidget(props: WidgetProps) {
|
|||||||
onFocus,
|
onFocus,
|
||||||
placeholder,
|
placeholder,
|
||||||
schema,
|
schema,
|
||||||
|
options,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "sm");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className={cn("relative", fieldClassName)}>
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
@ -34,7 +38,7 @@ export function PasswordWidget(props: WidgetProps) {
|
|||||||
onBlur={(e) => onBlur(id, e.target.value)}
|
onBlur={(e) => onBlur(id, e.target.value)}
|
||||||
onFocus={(e) => onFocus(id, e.target.value)}
|
onFocus={(e) => onFocus(id, e.target.value)}
|
||||||
aria-label={schema.title}
|
aria-label={schema.title}
|
||||||
className="pr-10"
|
className="w-full pr-10"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
|
||||||
export function SelectWidget(props: WidgetProps) {
|
export function SelectWidget(props: WidgetProps) {
|
||||||
const {
|
const {
|
||||||
@ -21,6 +22,7 @@ export function SelectWidget(props: WidgetProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { enumOptions = [] } = options;
|
const { enumOptions = [] } = options;
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "sm");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -34,7 +36,7 @@ export function SelectWidget(props: WidgetProps) {
|
|||||||
}}
|
}}
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
>
|
>
|
||||||
<SelectTrigger id={id} className="w-full">
|
<SelectTrigger id={id} className={fieldClassName}>
|
||||||
<SelectValue placeholder={placeholder || schema.title || "Select..."} />
|
<SelectValue placeholder={placeholder || schema.title || "Select..."} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
// Text Widget - maps to shadcn/ui Input
|
// Text Widget - maps to shadcn/ui Input
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
|
||||||
export function TextWidget(props: WidgetProps) {
|
export function TextWidget(props: WidgetProps) {
|
||||||
const {
|
const {
|
||||||
@ -19,11 +21,12 @@ export function TextWidget(props: WidgetProps) {
|
|||||||
const isNullable = Array.isArray(schema.type)
|
const isNullable = Array.isArray(schema.type)
|
||||||
? schema.type.includes("null")
|
? schema.type.includes("null")
|
||||||
: false;
|
: false;
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "xs");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
className="text-md"
|
className={cn("text-md", fieldClassName)}
|
||||||
type="text"
|
type="text"
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
// Textarea Widget - maps to shadcn/ui Textarea
|
// Textarea Widget - maps to shadcn/ui Textarea
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
|
||||||
export function TextareaWidget(props: WidgetProps) {
|
export function TextareaWidget(props: WidgetProps) {
|
||||||
const {
|
const {
|
||||||
@ -19,11 +21,12 @@ export function TextareaWidget(props: WidgetProps) {
|
|||||||
const isNullable = Array.isArray(schema.type)
|
const isNullable = Array.isArray(schema.type)
|
||||||
? schema.type.includes("null")
|
? schema.type.includes("null")
|
||||||
: false;
|
: false;
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "md");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
id={id}
|
id={id}
|
||||||
className="text-md"
|
className={cn("text-md", fieldClassName)}
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
placeholder={placeholder || (options.placeholder as string) || ""}
|
placeholder={placeholder || (options.placeholder as string) || ""}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user