feat: add multi select component. feat: add multi select to position flags section

This commit is contained in:
Dan Ditomaso
2025-01-31 15:57:23 -05:00
parent c7e2baea1b
commit 3d3b59686c
6 changed files with 293 additions and 8 deletions

View File

@@ -1,3 +1,7 @@
import {
type MultiSelectFieldProps,
MultiSelectInput,
} from "@app/components/Form/FormMultiSelect";
import {
GenericInput,
type InputFieldProps,
@@ -19,6 +23,7 @@ import type { Control, FieldValues } from "react-hook-form";
export type FieldProps<T> =
| InputFieldProps<T>
| SelectFieldProps<T>
| MultiSelectFieldProps<T>
| ToggleFieldProps<T>
| PasswordGeneratorProps<T>;
@@ -58,6 +63,8 @@ export function DynamicFormField<T extends FieldValues>({
/>
);
case "multiSelect":
return <div>tmp</div>;
return (
<MultiSelectInput field={field} control={control} disabled={disabled} />
);
}
}

View File

@@ -0,0 +1,60 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.tsx";
import type { FieldValues } from "react-hook-form";
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect";
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "multiSelect";
placeholder?: string;
onValueChange: (name: string) => void;
isChecked: (name: string) => boolean;
value: string[];
properties: BaseFormBuilderProps<T>["properties"] & {
enumValue: {
[s: string]: string | number;
};
formatEnumName?: boolean;
};
}
export function MultiSelectInput<T extends FieldValues>({
field,
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
const { enumValue, formatEnumName, ...remainingProperties } =
field.properties;
// Make sure to filter out the UNSET value, as it shouldn't be shown in the UI
const optionsEnumValues = enumValue
? Object.entries(enumValue)
.filter((value) => typeof value[1] === "number")
.filter((value) => value[0] !== "UNSET")
: [];
const formatName = (name: string) => {
if (!formatEnumName) return name;
return name
.replace(/_/g, " ")
.toLowerCase()
.split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
.join(" ");
};
return (
<MultiSelect {...remainingProperties}>
{optionsEnumValues.map(([name, value]) => (
<MultiSelectItem
key={name}
name={name}
value={value.toString()}
checked={field.isChecked(name)}
onCheckedChange={() => field.onValueChange(name)}
>
{formatEnumName ? formatName(name) : name}
</MultiSelectItem>
))}
</MultiSelect>
);
}

View File

@@ -12,7 +12,7 @@ import {
import { Controller, type FieldValues } from "react-hook-form";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select" | "multiSelect";
type: "select";
properties: BaseFormBuilderProps<T>["properties"] & {
enumValue: {
[s: string]: string | number;
@@ -51,7 +51,7 @@ export function SelectInput<T extends FieldValues>({
</SelectTrigger>
<SelectContent>
{optionsEnumValues.map(([name, value]) => (
<SelectItem key={name + value} value={value.toString()}>
<SelectItem key={name} value={value.toString()}>
{formatEnumName
? name
.replace(/_/g, " ")

View File

@@ -1,25 +1,39 @@
import {
type FlagName,
usePositionFlags,
} from "@app/core/hooks/usePositionFlags";
import type { PositionValidation } from "@app/validation/config/position.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Position = (): JSX.Element => {
const { config, nodes, hardware, setWorkingConfig } = useDevice();
export const Position = () => {
const { config, setWorkingConfig } = useDevice();
const { flagsValue, activeFlags, toggleFlag } = usePositionFlags(
config.position.positionFlags ?? 0,
);
const onSubmit = (data: PositionValidation) => {
setWorkingConfig(
return setWorkingConfig(
new Protobuf.Config.Config({
payloadVariant: {
case: "position",
value: data,
value: { ...data, positionFlags: flagsValue },
},
}),
);
};
const onPositonFlagChange = (name: string) => {
return toggleFlag(name as FlagName);
};
return (
<DynamicForm<PositionValidation>
onSubmit={onSubmit}
onSubmit={(data) => {
data.positionFlags = flagsValue;
return onSubmit(data);
}}
defaultValues={config.position}
fieldGroups={[
{
@@ -53,7 +67,12 @@ export const Position = (): JSX.Element => {
{
type: "multiSelect",
name: "positionFlags",
value: activeFlags,
isChecked: (name: string) =>
activeFlags.includes(name as FlagName),
onValueChange: onPositonFlagChange,
label: "Position Flags",
placeholder: "Select position flags...",
description: "Configuration options for Position messages",
properties: {
enumValue: Protobuf.Config.Config_PositionConfig_PositionFlags,

View File

@@ -0,0 +1,57 @@
import { cn } from "@app/core/utils/cn";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
interface MultiSelectProps {
children: React.ReactNode;
className?: string;
}
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
return (
<div className={cn("flex flex-wrap gap-2", className)}>{children}</div>
);
};
interface MultiSelectItemProps {
name: string;
value: string;
checked: boolean;
onCheckedChange: (name: string, value: boolean) => void;
children: React.ReactNode;
className?: string;
}
const MultiSelectItem = ({
name,
value,
checked,
onCheckedChange,
children,
className = "",
}: MultiSelectItemProps) => {
return (
<CheckboxPrimitive.Root
name={name}
id={value}
checked={checked}
onCheckedChange={(val) => onCheckedChange(name, !!val)}
className={cn(
`
inline-flex items-center rounded-md px-3 py-2 text-sm transition-colors
border border-slate-300
hover:bg-slate-100 dark:hover:bg-slate-800
focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2
data-[state=checked]:bg-slate-100 dark:data-[state=checked]:bg-slate-700`,
className,
)}
>
<CheckboxPrimitive.Indicator>
<Check className="h-4 w-4 animate-in zoom-in duration-200" />
</CheckboxPrimitive.Indicator>
<span className="ml-2">{children}</span>
</CheckboxPrimitive.Root>
);
};
export { MultiSelect, MultiSelectItem };