6 changed files with 293 additions and 8 deletions
@ -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> |
||||
|
); |
||||
|
} |
||||
@ -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 }; |
||||
@ -0,0 +1,142 @@ |
|||||
|
import { useCallback, useMemo, useState } from "react"; |
||||
|
|
||||
|
export type FlagName = |
||||
|
| "UNSET" |
||||
|
| "ALTITUDE" |
||||
|
| "ALTITUDE_MSL" |
||||
|
| "GEOIDAL_SEPARATION" |
||||
|
| "DOP" |
||||
|
| "HVDOP" |
||||
|
| "SATINVIEW" |
||||
|
| "SEQ_NO" |
||||
|
| "TIMESTAMP" |
||||
|
| "HEADING" |
||||
|
| "SPEED"; |
||||
|
|
||||
|
type UsePositionFlagsProps = { |
||||
|
decode: (value: number) => FlagName[]; |
||||
|
encode: (flagNames: FlagName[]) => number; |
||||
|
hasFlag: (value: number, flagName: FlagName) => boolean; |
||||
|
getAllFlags: () => FlagName[]; |
||||
|
isValidValue: (value: number) => boolean; |
||||
|
flagsValue: number; |
||||
|
activeFlags: FlagName[]; |
||||
|
toggleFlag: (flagName: FlagName) => void; |
||||
|
setFlag: (flagName: FlagName, enabled: boolean) => void; |
||||
|
setFlags: (value: number) => void; |
||||
|
clearFlags: () => void; |
||||
|
}; |
||||
|
|
||||
|
const FLAGS_MAP: ReadonlyMap<FlagName, number> = new Map([ |
||||
|
["UNSET", 0], |
||||
|
["ALTITUDE", 1], |
||||
|
["ALTITUDE_MSL", 2], |
||||
|
["GEOIDAL_SEPARATION", 4], |
||||
|
["DOP", 8], |
||||
|
["HVDOP", 16], |
||||
|
["SATINVIEW", 32], |
||||
|
["SEQ_NO", 64], |
||||
|
["TIMESTAMP", 128], |
||||
|
["HEADING", 256], |
||||
|
["SPEED", 512], |
||||
|
]); |
||||
|
|
||||
|
export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => { |
||||
|
const [flagsValue, setFlagsValue] = useState<number>(initialValue); |
||||
|
|
||||
|
const utils = useMemo(() => { |
||||
|
const decode = (value: number): FlagName[] => { |
||||
|
if (value === 0) return ["UNSET"]; |
||||
|
const activeFlags: FlagName[] = []; |
||||
|
for (const [name, flagValue] of FLAGS_MAP) { |
||||
|
if (flagValue !== 0 && (value & flagValue) === flagValue) { |
||||
|
activeFlags.push(name); |
||||
|
} |
||||
|
} |
||||
|
return activeFlags; |
||||
|
}; |
||||
|
|
||||
|
const encode = (flagNames: FlagName[]): number => { |
||||
|
if (flagNames.includes("UNSET")) { |
||||
|
return 0; |
||||
|
} |
||||
|
return flagNames.reduce((acc, name) => { |
||||
|
const value = FLAGS_MAP.get(name); |
||||
|
if (value === undefined) { |
||||
|
throw new Error(`Invalid flag name: ${name}`); |
||||
|
} |
||||
|
return acc | value; |
||||
|
}, 0); |
||||
|
}; |
||||
|
|
||||
|
const hasFlag = (value: number, flagName: FlagName): boolean => { |
||||
|
const flagValue = FLAGS_MAP.get(flagName); |
||||
|
if (flagValue === undefined) { |
||||
|
throw new Error(`Invalid flag name: ${flagName}`); |
||||
|
} |
||||
|
return (value & flagValue) === flagValue; |
||||
|
}; |
||||
|
|
||||
|
const getAllFlags = (): FlagName[] => { |
||||
|
return Array.from(FLAGS_MAP.keys()); |
||||
|
}; |
||||
|
|
||||
|
const isValidValue = (value: number): boolean => { |
||||
|
const maxValue = Array.from(FLAGS_MAP.values()).reduce( |
||||
|
(a, b) => a + b, |
||||
|
0, |
||||
|
); |
||||
|
return Number.isInteger(value) && value >= 0 && value <= maxValue; |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
decode, |
||||
|
encode, |
||||
|
hasFlag, |
||||
|
getAllFlags, |
||||
|
isValidValue, |
||||
|
}; |
||||
|
}, []); |
||||
|
|
||||
|
const toggleFlag = useCallback((flagName: FlagName) => { |
||||
|
const flagValue = FLAGS_MAP.get(flagName); |
||||
|
if (flagValue === undefined) { |
||||
|
throw new Error(`Invalid flag name: ${flagName}`); |
||||
|
} |
||||
|
setFlagsValue((prev) => prev ^ flagValue); |
||||
|
}, []); |
||||
|
|
||||
|
const setFlag = useCallback((flagName: FlagName, enabled: boolean) => { |
||||
|
const flagValue = FLAGS_MAP.get(flagName); |
||||
|
if (flagValue === undefined) { |
||||
|
throw new Error(`Invalid flag name: ${flagName}`); |
||||
|
} |
||||
|
setFlagsValue((prev) => (enabled ? prev | flagValue : prev & ~flagValue)); |
||||
|
}, []); |
||||
|
|
||||
|
const setFlags = useCallback( |
||||
|
(value: number) => { |
||||
|
if (!utils.isValidValue(value)) { |
||||
|
throw new Error(`Invalid flags value: ${value}`); |
||||
|
} |
||||
|
setFlagsValue(value); |
||||
|
}, |
||||
|
[utils], |
||||
|
); |
||||
|
|
||||
|
const clearFlags = useCallback(() => { |
||||
|
setFlagsValue(0); |
||||
|
}, []); |
||||
|
|
||||
|
const activeFlags = utils.decode(flagsValue); |
||||
|
|
||||
|
return { |
||||
|
...utils, |
||||
|
flagsValue, |
||||
|
activeFlags, |
||||
|
toggleFlag, |
||||
|
setFlag, |
||||
|
setFlags, |
||||
|
clearFlags, |
||||
|
}; |
||||
|
}; |
||||
Loading…
Reference in new issue