mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +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;
|
||||
};
|
||||
|
||||
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 {
|
||||
/** JSON Schema for the form */
|
||||
schema: RJSFSchema;
|
||||
@ -245,7 +274,9 @@ export function ConfigForm({
|
||||
transformedSchema,
|
||||
pathOverrides,
|
||||
);
|
||||
const merged = mergeUiSchema(expandedUiSchema, baseUiSchema);
|
||||
const merged = applyLayoutGridFieldDefaults(
|
||||
mergeUiSchema(expandedUiSchema, baseUiSchema),
|
||||
);
|
||||
|
||||
// Add field groups
|
||||
if (fieldGroups) {
|
||||
@ -311,7 +342,7 @@ export function ConfigForm({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("config-form", className)}>
|
||||
<div className={cn("config-form w-full max-w-5xl", className)}>
|
||||
<Form
|
||||
schema={transformedSchema}
|
||||
uiSchema={finalUiSchema}
|
||||
|
||||
@ -32,6 +32,12 @@ const auth: SectionConfigOverrides = {
|
||||
reset_admin_password: {
|
||||
"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: [],
|
||||
fieldOrder: ["path"],
|
||||
advancedFields: [],
|
||||
uiSchema: {
|
||||
path: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
|
||||
const detect: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/camera_specific",
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"width",
|
||||
"height",
|
||||
"fps",
|
||||
"min_initialized",
|
||||
"max_disappeared",
|
||||
"annotation_offset",
|
||||
"stationary",
|
||||
"interval",
|
||||
"threshold",
|
||||
"max_frames",
|
||||
],
|
||||
restartRequired: [
|
||||
"width",
|
||||
"height",
|
||||
@ -12,7 +25,7 @@ const detect: SectionConfigOverrides = {
|
||||
"stationary",
|
||||
],
|
||||
fieldGroups: {
|
||||
resolution: ["width", "height", "fps"],
|
||||
resolution: ["enabled", "width", "height", "fps"],
|
||||
tracking: ["min_initialized", "max_disappeared"],
|
||||
},
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
@ -22,46 +35,6 @@ const detect: SectionConfigOverrides = {
|
||||
"annotation_offset",
|
||||
"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: [],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
uiSchema: {
|
||||
additionalProperties: {
|
||||
"ui:options": { size: "lg" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -58,6 +58,9 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
"gpu",
|
||||
],
|
||||
uiSchema: {
|
||||
path: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
global_args: arrayAsTextWidget,
|
||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||
input_args: ffmpegArgsWidget("input_args"),
|
||||
@ -71,6 +74,9 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
path: {
|
||||
"ui:options": { size: "full" },
|
||||
},
|
||||
global_args: arrayAsTextWidget,
|
||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||
input_args: ffmpegArgsWidget("input_args"),
|
||||
@ -104,6 +110,9 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
"gpu",
|
||||
],
|
||||
uiSchema: {
|
||||
path: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
global_args: arrayAsTextWidget,
|
||||
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
|
||||
input_args: ffmpegArgsWidget("input_args"),
|
||||
|
||||
@ -14,6 +14,27 @@ const genai: SectionConfigOverrides = {
|
||||
],
|
||||
advancedFields: ["base_url", "provider_options", "runtime_options"],
|
||||
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",
|
||||
],
|
||||
uiSchema: {
|
||||
format: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
replace_rules: {
|
||||
"ui:field": "ReplaceRulesField",
|
||||
"ui:options": {
|
||||
|
||||
@ -28,6 +28,14 @@ const model: SectionConfigOverrides = {
|
||||
"non_logo_attributes",
|
||||
"plus",
|
||||
],
|
||||
uiSchema: {
|
||||
path: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
labelmap_path: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -47,7 +47,26 @@ const mqtt: SectionConfigOverrides = {
|
||||
"tls_insecure",
|
||||
],
|
||||
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": {
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
size: "sm",
|
||||
},
|
||||
},
|
||||
"listen.external": {
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
size: "sm",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -59,6 +59,16 @@ const objects: SectionConfigOverrides = {
|
||||
},
|
||||
prompt: {
|
||||
"ui:widget": "textarea",
|
||||
"ui:options": {
|
||||
size: "full",
|
||||
},
|
||||
},
|
||||
object_prompts: {
|
||||
additionalProperties: {
|
||||
"ui:options": {
|
||||
size: "full",
|
||||
},
|
||||
},
|
||||
},
|
||||
required_zones: {
|
||||
"ui:widget": "zoneNames",
|
||||
|
||||
@ -20,6 +20,9 @@ const onvif: SectionConfigOverrides = {
|
||||
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
|
||||
overrideFields: [],
|
||||
uiSchema: {
|
||||
host: {
|
||||
"ui:options": { size: "sm" },
|
||||
},
|
||||
autotracking: {
|
||||
required_zones: {
|
||||
"ui:widget": "zoneNames",
|
||||
|
||||
@ -14,6 +14,12 @@ const proxy: SectionConfigOverrides = {
|
||||
advancedFields: ["header_map", "auth_secret", "separator"],
|
||||
liveValidate: true,
|
||||
uiSchema: {
|
||||
logout_url: {
|
||||
"ui:options": { size: "lg" },
|
||||
},
|
||||
auth_secret: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
header_map: {
|
||||
"ui:after": { render: "ProxyRoleMap" },
|
||||
},
|
||||
|
||||
@ -20,6 +20,13 @@ const record: SectionConfigOverrides = {
|
||||
},
|
||||
hiddenFields: ["enabled_in_config", "sync_recordings"],
|
||||
advancedFields: ["expire_interval", "preview", "export"],
|
||||
uiSchema: {
|
||||
export: {
|
||||
hwaccel_args: {
|
||||
"ui:options": { size: "lg" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -29,9 +29,15 @@ const review: SectionConfigOverrides = {
|
||||
genai: {
|
||||
additional_concerns: {
|
||||
"ui:widget": "textarea",
|
||||
"ui:options": {
|
||||
size: "full",
|
||||
},
|
||||
},
|
||||
activity_context_prompt: {
|
||||
"ui:widget": "textarea",
|
||||
"ui:options": {
|
||||
size: "full",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -6,6 +6,14 @@ const tls: SectionConfigOverrides = {
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "cert", "key"],
|
||||
advancedFields: [],
|
||||
uiSchema: {
|
||||
cert: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
key: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ export function AdvancedCollapsible({
|
||||
{label}
|
||||
</Button>
|
||||
</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}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
@ -114,6 +114,13 @@ interface PropertyElement {
|
||||
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
|
||||
function GridLayoutObjectFieldTemplate(
|
||||
props: ObjectFieldTemplateProps,
|
||||
@ -361,9 +368,16 @@ function GridLayoutObjectFieldTemplate(
|
||||
}
|
||||
|
||||
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 && (
|
||||
<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)}
|
||||
</div>
|
||||
)}
|
||||
@ -404,9 +418,12 @@ function GridLayoutObjectFieldTemplate(
|
||||
}
|
||||
|
||||
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 && (
|
||||
<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)}
|
||||
</div>
|
||||
)}
|
||||
@ -429,7 +446,16 @@ function GridLayoutObjectFieldTemplate(
|
||||
)}
|
||||
>
|
||||
{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>,
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Base Input Template - default input wrapper
|
||||
import type { WidgetProps } from "@rjsf/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getSizedFieldClassName } from "../utils";
|
||||
|
||||
export function BaseInputTemplate(props: WidgetProps) {
|
||||
const {
|
||||
@ -14,9 +15,11 @@ export function BaseInputTemplate(props: WidgetProps) {
|
||||
onFocus,
|
||||
placeholder,
|
||||
schema,
|
||||
options,
|
||||
} = props;
|
||||
|
||||
const inputType = type || "text";
|
||||
const fieldClassName = getSizedFieldClassName(options, "xs");
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
@ -32,6 +35,7 @@ export function BaseInputTemplate(props: WidgetProps) {
|
||||
<Input
|
||||
id={id}
|
||||
type={inputType}
|
||||
className={fieldClassName}
|
||||
value={value ?? ""}
|
||||
disabled={disabled || readonly}
|
||||
placeholder={placeholder || ""}
|
||||
|
||||
@ -101,7 +101,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
const suppressDescription = uiOptionsFromSchema.suppressDescription === true;
|
||||
|
||||
// Determine field characteristics
|
||||
const isAdvanced = uiOptionsFromSchema.advanced === true;
|
||||
const isBoolean =
|
||||
schema.type === "boolean" ||
|
||||
(Array.isArray(schema.type) && schema.type.includes("boolean"));
|
||||
@ -111,11 +110,33 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
const suppressMultiSchema =
|
||||
(uiSchema?.["ui:options"] as UiSchema["ui:options"] | undefined)
|
||||
?.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
|
||||
// This prevents duplicate labels while still showing the inner field's label
|
||||
const isMultiSchemaWrapper =
|
||||
(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
|
||||
const pathSegments = fieldPathId.path.filter(
|
||||
@ -379,17 +400,11 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
>
|
||||
<div className="flex flex-col space-y-6">
|
||||
{beforeContent}
|
||||
<div
|
||||
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}
|
||||
>
|
||||
<div className={cn("space-y-1")} data-field-id={translationPath}>
|
||||
{displayLabel &&
|
||||
finalLabel &&
|
||||
!isBoolean &&
|
||||
!useSplitLayout &&
|
||||
!isMultiSchemaWrapper &&
|
||||
!isObjectField &&
|
||||
!isAdditionalProperty && (
|
||||
@ -409,29 +424,180 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
)}
|
||||
|
||||
{isBoolean ? (
|
||||
<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",
|
||||
useSplitBooleanLayout ? (
|
||||
<>
|
||||
<div className="space-y-1.5 md:hidden">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{displayLabel && finalLabel && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
isModified && "text-danger",
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
{required && (
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
)}
|
||||
</Label>
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
{required && (
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
{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="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 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="hidden text-xs text-muted-foreground md:block">
|
||||
{finalDescription}
|
||||
</p>
|
||||
)}
|
||||
{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
|
||||
to={fieldDocsUrl}
|
||||
target="_blank"
|
||||
@ -444,7 +610,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -323,12 +323,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
}>;
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{groups.map((group) => (
|
||||
<div key={group.key} className="space-y-6">
|
||||
<div className="text-md font-medium text-primary">
|
||||
<div
|
||||
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}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
@ -342,7 +351,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
{ungrouped.length > 0 && (
|
||||
<div className={cn("space-y-6", groups.length > 0 && "pt-2")}>
|
||||
{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>
|
||||
)}
|
||||
|
||||
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,
|
||||
isSubtreeModified,
|
||||
} from "./overrides";
|
||||
export { getSizedFieldClassName } from "./fieldSizing";
|
||||
|
||||
@ -4,6 +4,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSizedFieldClassName } from "../utils";
|
||||
|
||||
export function PasswordWidget(props: WidgetProps) {
|
||||
const {
|
||||
@ -16,12 +18,14 @@ export function PasswordWidget(props: WidgetProps) {
|
||||
onFocus,
|
||||
placeholder,
|
||||
schema,
|
||||
options,
|
||||
} = props;
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const fieldClassName = getSizedFieldClassName(options, "sm");
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={cn("relative", fieldClassName)}>
|
||||
<Input
|
||||
id={id}
|
||||
type={showPassword ? "text" : "password"}
|
||||
@ -34,7 +38,7 @@ export function PasswordWidget(props: WidgetProps) {
|
||||
onBlur={(e) => onBlur(id, e.target.value)}
|
||||
onFocus={(e) => onFocus(id, e.target.value)}
|
||||
aria-label={schema.title}
|
||||
className="pr-10"
|
||||
className="w-full pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { getSizedFieldClassName } from "../utils";
|
||||
|
||||
export function SelectWidget(props: WidgetProps) {
|
||||
const {
|
||||
@ -21,6 +22,7 @@ export function SelectWidget(props: WidgetProps) {
|
||||
} = props;
|
||||
|
||||
const { enumOptions = [] } = options;
|
||||
const fieldClassName = getSizedFieldClassName(options, "sm");
|
||||
|
||||
return (
|
||||
<Select
|
||||
@ -34,7 +36,7 @@ export function SelectWidget(props: WidgetProps) {
|
||||
}}
|
||||
disabled={disabled || readonly}
|
||||
>
|
||||
<SelectTrigger id={id} className="w-full">
|
||||
<SelectTrigger id={id} className={fieldClassName}>
|
||||
<SelectValue placeholder={placeholder || schema.title || "Select..."} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// Text Widget - maps to shadcn/ui Input
|
||||
import type { WidgetProps } from "@rjsf/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSizedFieldClassName } from "../utils";
|
||||
|
||||
export function TextWidget(props: WidgetProps) {
|
||||
const {
|
||||
@ -19,11 +21,12 @@ export function TextWidget(props: WidgetProps) {
|
||||
const isNullable = Array.isArray(schema.type)
|
||||
? schema.type.includes("null")
|
||||
: false;
|
||||
const fieldClassName = getSizedFieldClassName(options, "xs");
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
className="text-md"
|
||||
className={cn("text-md", fieldClassName)}
|
||||
type="text"
|
||||
value={value ?? ""}
|
||||
disabled={disabled || readonly}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// Textarea Widget - maps to shadcn/ui Textarea
|
||||
import type { WidgetProps } from "@rjsf/utils";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSizedFieldClassName } from "../utils";
|
||||
|
||||
export function TextareaWidget(props: WidgetProps) {
|
||||
const {
|
||||
@ -19,11 +21,12 @@ export function TextareaWidget(props: WidgetProps) {
|
||||
const isNullable = Array.isArray(schema.type)
|
||||
? schema.type.includes("null")
|
||||
: false;
|
||||
const fieldClassName = getSizedFieldClassName(options, "md");
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
id={id}
|
||||
className="text-md"
|
||||
className={cn("text-md", fieldClassName)}
|
||||
value={value ?? ""}
|
||||
disabled={disabled || readonly}
|
||||
placeholder={placeholder || (options.placeholder as string) || ""}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user