Merge pull request #394 from danditomaso/feat/add-multi-select

Feat: Add MultiSelect component
This commit is contained in:
Hunter Thornsberry
2025-02-07 23:42:35 -05:00
committed by GitHub
6 changed files with 278 additions and 10 deletions
+8 -1
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} />
);
}
}
+60
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>
);
}
+2 -2
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, " ")
@@ -1,25 +1,43 @@
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";
import { useCallback } from "react";
export const Position = (): JSX.Element => {
const { config, nodes, hardware, setWorkingConfig } = useDevice();
export const Position = () => {
const { config, setWorkingConfig } = useDevice();
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = 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 = useCallback(
(name: string) => {
return toggleFlag(name as FlagName);
},
[toggleFlag],
);
return (
<DynamicForm<PositionValidation>
onSubmit={onSubmit}
onSubmit={(data) => {
data.positionFlags = flagsValue;
return onSubmit(data);
}}
defaultValues={config.position}
fieldGroups={[
{
@@ -53,10 +71,16 @@ export const Position = (): JSX.Element => {
{
type: "multiSelect",
name: "positionFlags",
value: activeFlags,
isChecked: (name: string) =>
activeFlags.includes(name as FlagName),
onValueChange: onPositonFlagChange,
label: "Position Flags",
description: "Configuration options for Position messages",
placeholder: "Select position flags...",
description:
"Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.",
properties: {
enumValue: Protobuf.Config.Config_PositionConfig_PositionFlags,
enumValue: getAllFlags(),
},
},
{
+57
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 className="mr-2">
<Check className="h-4 w-4 animate-in zoom-in duration-200" />
</CheckboxPrimitive.Indicator>
{children}
</CheckboxPrimitive.Root>
);
};
export { MultiSelect, MultiSelectItem };