Browse Source

Make DynamicForm more modular

pull/101/head
Sacha Weatherstone 3 years ago
parent
commit
53a4ae290c
No known key found for this signature in database GPG Key ID: 7AB2D7E206124B31
  1. 262
      src/components/DynamicForm.tsx
  2. 97
      src/components/Form/DynamicForm.tsx
  3. 41
      src/components/Form/DynamicFormField.tsx
  4. 48
      src/components/Form/FormInput.tsx
  5. 71
      src/components/Form/FormSelect.tsx
  6. 32
      src/components/Form/FormToggle.tsx
  7. 31
      src/components/Form/FormWrapper.tsx
  8. 13
      src/components/PageComponents/Config/Bluetooth.tsx
  9. 18
      src/components/PageComponents/Config/Device.tsx
  10. 26
      src/components/PageComponents/Config/Display.tsx
  11. 39
      src/components/PageComponents/Config/LoRa.tsx
  12. 6
      src/components/PageComponents/Config/Network.tsx
  13. 6
      src/components/PageComponents/Config/Position.tsx
  14. 26
      src/components/PageComponents/Config/Power.tsx
  15. 6
      src/components/PageComponents/ModuleConfig/Audio.tsx
  16. 20
      src/components/PageComponents/ModuleConfig/CannedMessage.tsx
  17. 9
      src/components/PageComponents/ModuleConfig/ExternalNotification.tsx
  18. 2
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  19. 2
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  20. 25
      src/components/PageComponents/ModuleConfig/Serial.tsx
  21. 9
      src/components/PageComponents/ModuleConfig/StoreForward.tsx
  22. 13
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  23. 12
      src/components/PageLayout.tsx

262
src/components/DynamicForm.tsx

@ -1,262 +0,0 @@
import {
Controller,
DeepPartial,
FieldValues,
Path,
SubmitHandler,
useForm
} from "react-hook-form";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
import { ErrorMessage } from "@hookform/error-message";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@components/UI/Select.js";
import { Switch } from "@components/UI/Switch.js";
import { H4 } from "@components/UI/Typography/H4.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
interface DisabledBy<T> {
fieldName: Path<T>;
selector?: number;
invert?: boolean;
}
interface BasicFieldProps<T> {
name: Path<T>;
label: string;
description?: string;
active?: boolean;
required?: boolean;
disabledBy?: DisabledBy<T>[];
}
interface InputFieldProps<T> extends BasicFieldProps<T> {
type: "text" | "number" | "password";
suffix?: string;
}
interface SelectFieldProps<T> extends BasicFieldProps<T> {
type: "select" | "multiSelect";
enumValue: {
[s: string]: string | number;
};
formatEnumName?: boolean;
}
interface ToggleFieldProps<T> extends BasicFieldProps<T> {
type: "toggle";
}
export interface FormProps<T extends FieldValues> {
onSubmit: SubmitHandler<T>;
defaultValues?: DeepPartial<T>;
fieldGroups: {
label: string;
description: string;
fields: (InputFieldProps<T> | SelectFieldProps<T> | ToggleFieldProps<T>)[];
}[];
}
export function DynamicForm<T extends FieldValues>({
fieldGroups,
onSubmit,
defaultValues
}: FormProps<T>) {
const { register, handleSubmit, control, getValues } = useForm<T>({
mode: "onChange",
defaultValues: defaultValues
});
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => {
if (!disabledBy) return false;
return disabledBy.some((field) => {
const value = getValues(field.fieldName);
if (typeof value === "boolean") return field.invert ? value : !value;
if (typeof value === "number")
return field.invert
? field.selector !== value
: field.selector === value;
return false;
});
};
return (
<form
className="space-y-8 divide-y divide-gray-200"
onChange={handleSubmit(onSubmit)}
>
{fieldGroups.map((fieldGroup, index) => (
<div
key={index}
className="space-y-8 divide-y divide-gray-200 sm:space-y-5"
>
<div>
<H4 className="font-medium">{fieldGroup.label}</H4>
<Subtle>{fieldGroup.description}</Subtle>
</div>
{fieldGroup.fields.map((field, index) => {
const fieldWrapperData: FieldWrapperProps = {
label: field.label,
description: field.description,
disabled: isDisabled(field.disabledBy)
};
switch (field.type) {
case "text":
return (
<FieldWrapper key={index} {...fieldWrapperData}>
<Input
type="text"
suffix={field.suffix}
disabled={fieldWrapperData.disabled}
{...register(field.name)}
/>
</FieldWrapper>
);
case "number":
return (
<FieldWrapper key={index} {...fieldWrapperData}>
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Input
type="number"
value={parseInt(value)}
suffix={field.suffix}
onChange={(e) => onChange(parseInt(e.target.value))}
disabled={fieldWrapperData.disabled}
{...rest}
/>
)}
/>
</FieldWrapper>
);
case "password":
return (
<FieldWrapper key={index} {...fieldWrapperData}>
<Input
type="password"
suffix={field.suffix}
disabled={fieldWrapperData.disabled}
// action={{
// icon: hidden ? EyeIcon : EyeOffIcon,
// onClick: () => {
// }
// }}
{...register(field.name)}
/>
</FieldWrapper>
);
case "toggle":
return (
<FieldWrapper key={index} {...fieldWrapperData}>
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Switch
checked={value}
onCheckedChange={onChange}
disabled={fieldWrapperData.disabled}
{...rest}
/>
)}
/>
</FieldWrapper>
);
case "select":
const optionsEnumValues = field.enumValue
? Object.entries(field.enumValue).filter(
(value) => typeof value[1] === "number"
)
: [];
return (
<FieldWrapper key={index} {...fieldWrapperData}>
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Select
onValueChange={(e) => onChange(parseInt(e))}
disabled={fieldWrapperData.disabled}
value={value?.toString()}
{...rest}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{optionsEnumValues.map(([name, value], index) => (
<SelectItem key={index} value={value.toString()}>
{field.formatEnumName
? name
.replace(/_/g, " ")
.toLowerCase()
.split(" ")
.map(
(s) =>
s.charAt(0).toUpperCase() +
s.substring(1)
)
.join(" ")
: name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</FieldWrapper>
);
case "multiSelect":
return (
<FieldWrapper key={index} {...fieldWrapperData}>
tmp
</FieldWrapper>
);
}
})}
</div>
))}
</form>
);
}
interface FieldWrapperProps {
label: string;
description?: string;
disabled?: boolean;
children?: React.ReactNode;
}
const FieldWrapper = ({
label,
description,
disabled,
children
}: FieldWrapperProps): JSX.Element => (
<div className="pt-6 sm:pt-5">
<div role="group" aria-labelledby="label-notifications">
<div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4">
<Label>{label}</Label>
<div className="sm:col-span-2">
<div className="max-w-lg">
<p className="text-sm text-gray-500">{description}</p>
<div className="mt-4 space-y-4">
<div className="flex items-center">{children}</div>
</div>
</div>
</div>
</div>
</div>
</div>
);

97
src/components/Form/DynamicForm.tsx

@ -0,0 +1,97 @@
import {
Control,
DeepPartial,
FieldValues,
Path,
SubmitHandler,
useForm
} from "react-hook-form";
import { H4 } from "@components/UI/Typography/H4.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
import { DynamicFormField, FieldProps } from "./DynamicFormField.js";
import { FieldWrapper } from "./FormWrapper.js";
interface DisabledBy<T> {
fieldName: Path<T>;
selector?: number;
invert?: boolean;
}
export interface BaseFormBuilderProps<T> {
name: Path<T>;
disabledBy?: DisabledBy<T>[];
label: string;
description?: string;
properties?: {};
}
export interface GenericFormElementProps<T extends FieldValues, Y> {
control: Control<T>;
disabled?: boolean;
field: Y;
}
export interface DynamicFormProps<T extends FieldValues> {
onSubmit: SubmitHandler<T>;
defaultValues?: DeepPartial<T>;
fieldGroups: {
label: string;
description: string;
fields: FieldProps<T>[];
}[];
}
export function DynamicForm<T extends FieldValues>({
fieldGroups,
onSubmit,
defaultValues
}: DynamicFormProps<T>) {
const { handleSubmit, control, getValues } = useForm<T>({
mode: "onChange",
defaultValues: defaultValues
});
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => {
if (!disabledBy) return false;
return disabledBy.some((field) => {
const value = getValues(field.fieldName);
if (typeof value === "boolean") return field.invert ? value : !value;
if (typeof value === "number")
return field.invert
? field.selector !== value
: field.selector === value;
return false;
});
};
return (
<form
className="space-y-8 divide-y divide-gray-200"
onChange={handleSubmit(onSubmit)}
>
{fieldGroups.map((fieldGroup, index) => (
<div
key={index}
className="space-y-8 divide-y divide-gray-200 sm:space-y-5"
>
<div>
<H4 className="font-medium">{fieldGroup.label}</H4>
<Subtle>{fieldGroup.description}</Subtle>
</div>
{fieldGroup.fields.map((field, index) => (
<FieldWrapper label={field.label} description={field.description}>
<DynamicFormField
key={index}
field={field}
control={control}
disabled={isDisabled(field.disabledBy)}
/>
</FieldWrapper>
))}
</div>
))}
</form>
);
}

41
src/components/Form/DynamicFormField.tsx

@ -0,0 +1,41 @@
import type { Control, FieldValues } from "react-hook-form";
import { GenericInput, InputFieldProps } from "./FormInput.js";
import { ToggleFieldProps, ToggleInput } from "./FormToggle.js";
import { SelectFieldProps, SelectInput } from "./FormSelect.js";
export type FieldProps<T> =
| InputFieldProps<T>
| SelectFieldProps<T>
| ToggleFieldProps<T>;
export interface DynamicFormFieldProps<T extends FieldValues> {
field: FieldProps<T>;
control: Control<T>;
disabled?: boolean;
}
export function DynamicFormField<T extends FieldValues>({
field,
control,
disabled
}: DynamicFormFieldProps<T>) {
switch (field.type) {
case "text":
case "password":
case "number":
return (
<GenericInput field={field} control={control} disabled={disabled} />
);
case "toggle":
return (
<ToggleInput field={field} control={control} disabled={disabled} />
);
case "select":
return (
<SelectInput field={field} control={control} disabled={disabled} />
);
case "multiSelect":
return <div>tmp</div>;
}
}

48
src/components/Form/FormInput.tsx

@ -0,0 +1,48 @@
import type { LucideIcon } from "lucide-react";
import type {
BaseFormBuilderProps,
GenericFormElementProps
} from "./DynamicForm.js";
import { Input } from "../UI/Input.js";
import { Controller, FieldValues } from "react-hook-form";
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
type: "text" | "number" | "password";
properties?: {
prefix?: string;
suffix?: string;
action?: {
icon: LucideIcon;
onClick: () => void;
};
};
}
export function GenericInput<T extends FieldValues>({
control,
disabled,
field
}: GenericFormElementProps<T, InputFieldProps<T>>) {
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Input
type={field.type}
value={field.type === "number" ? parseInt(value) : value}
onChange={(e) =>
onChange(
field.type === "number"
? parseInt(e.target.value)
: e.target.value
)
}
disabled={disabled}
{...field.properties}
{...rest}
/>
)}
/>
);
}

71
src/components/Form/FormSelect.tsx

@ -0,0 +1,71 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps
} from "./DynamicForm.js";
import { Controller, FieldValues } from "react-hook-form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "../UI/Select.js";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select" | "multiSelect";
properties: BaseFormBuilderProps<T>["properties"] & {
enumValue: {
[s: string]: string | number;
};
formatEnumName?: boolean;
};
}
export function SelectInput<T extends FieldValues>({
control,
disabled,
field
}: GenericFormElementProps<T, SelectFieldProps<T>>) {
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => {
const { enumValue, formatEnumName, ...remainingProperties } =
field.properties;
const optionsEnumValues = enumValue
? Object.entries(enumValue).filter(
(value) => typeof value[1] === "number"
)
: [];
return (
<Select
onValueChange={(e) => onChange(parseInt(e))}
disabled={disabled}
value={value?.toString()}
{...remainingProperties}
{...rest}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{optionsEnumValues.map(([name, value], index) => (
<SelectItem key={index} value={value.toString()}>
{formatEnumName
? name
.replace(/_/g, " ")
.toLowerCase()
.split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
.join(" ")
: name}
</SelectItem>
))}
</SelectContent>
</Select>
);
}}
/>
);
}

32
src/components/Form/FormToggle.tsx

@ -0,0 +1,32 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps
} from "./DynamicForm.js";
import { Controller, FieldValues } from "react-hook-form";
import { Switch } from "../UI/Switch.js";
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
type: "toggle";
}
export function ToggleInput<T extends FieldValues>({
control,
disabled,
field
}: GenericFormElementProps<T, ToggleFieldProps<T>>) {
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Switch
checked={value}
onCheckedChange={onChange}
disabled={disabled}
{...field.properties}
{...rest}
/>
)}
/>
);
}

31
src/components/Form/FormWrapper.tsx

@ -0,0 +1,31 @@
import { Label } from "../UI/Label.js";
import { ErrorMessage } from "@hookform/error-message";
export interface FieldWrapperProps {
label: string;
description?: string;
disabled?: boolean;
children?: React.ReactNode;
}
export const FieldWrapper = ({
label,
description,
children
}: FieldWrapperProps): JSX.Element => (
<div className="pt-6 sm:pt-5">
<div role="group" aria-labelledby="label-notifications">
<div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4">
<Label>{label}</Label>
<div className="sm:col-span-2">
<div className="max-w-lg">
<p className="text-sm text-gray-500">{description}</p>
<div className="mt-4 space-y-4">
<div className="flex items-center">{children}</div>
</div>
</div>
</div>
</div>
</div>
</div>
);

13
src/components/PageComponents/Config/Bluetooth.tsx

@ -1,7 +1,7 @@
import type { BluetoothValidation } from "@app/validation/config/bluetooth.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Bluetooth = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -37,13 +37,15 @@ export const Bluetooth = (): JSX.Element => {
name: "mode",
label: "Pairing mode",
description: "Pin selection behaviour.",
enumValue: Protobuf.Config_BluetoothConfig_PairingMode,
formatEnumName: true,
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
enumValue: Protobuf.Config_BluetoothConfig_PairingMode,
formatEnumName: true
}
},
{
type: "number",
@ -60,7 +62,8 @@ export const Bluetooth = (): JSX.Element => {
{
fieldName: "enabled"
}
]
],
properties: {}
}
]
}

18
src/components/PageComponents/Config/Device.tsx

@ -1,7 +1,7 @@
import type { DeviceValidation } from "@app/validation/config/device.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Device = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -31,8 +31,10 @@ export const Device = (): JSX.Element => {
name: "role",
label: "Role",
description: "What role the device performs on the mesh",
enumValue: Protobuf.Config_DeviceConfig_Role,
formatEnumName: true
properties: {
enumValue: Protobuf.Config_DeviceConfig_Role,
formatEnumName: true
}
},
{
type: "toggle",
@ -64,15 +66,19 @@ export const Device = (): JSX.Element => {
name: "rebroadcastMode",
label: "Rebroadcast Mode",
description: "How to handle rebroadcasting",
enumValue: Protobuf.Config_DeviceConfig_RebroadcastMode,
formatEnumName: true
properties: {
enumValue: Protobuf.Config_DeviceConfig_RebroadcastMode,
formatEnumName: true
}
},
{
type: "number",
name: "nodeInfoBroadcastSecs",
label: "Node Info Broadcast Interval",
description: "How often to broadcast node info",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
}
]
}

26
src/components/PageComponents/Config/Display.tsx

@ -1,7 +1,7 @@
import type { DisplayValidation } from "@app/validation/config/display.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Display = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -31,14 +31,18 @@ export const Display = (): JSX.Element => {
name: "screenOnSecs",
label: "Screen Timeout",
description: "Turn off the display after this long",
suffix: "seconds"
properties: {
suffix: "seconds"
}
},
{
type: "select",
name: "gpsFormat",
label: "GPS Display Units",
description: "Coordinate display format",
enumValue: Protobuf.Config_DisplayConfig_GpsCoordinateFormat
properties: {
enumValue: Protobuf.Config_DisplayConfig_GpsCoordinateFormat
}
},
{
type: "number",
@ -63,23 +67,29 @@ export const Display = (): JSX.Element => {
name: "units",
label: "Display Units",
description: "Display metric or imperial units",
enumValue: Protobuf.Config_DisplayConfig_DisplayUnits,
formatEnumName: true
properties: {
enumValue: Protobuf.Config_DisplayConfig_DisplayUnits,
formatEnumName: true
}
},
{
type: "select",
name: "oled",
label: "OLED Type",
description: "Type of OLED screen attached to the device",
enumValue: Protobuf.Config_DisplayConfig_OledType
properties: {
enumValue: Protobuf.Config_DisplayConfig_OledType
}
},
{
type: "select",
name: "displaymode",
label: "Display Mode",
description: "Screen layout variant",
enumValue: Protobuf.Config_DisplayConfig_DisplayMode,
formatEnumName: true
properties: {
enumValue: Protobuf.Config_DisplayConfig_DisplayMode,
formatEnumName: true
}
},
{
type: "toggle",

39
src/components/PageComponents/Config/LoRa.tsx

@ -1,7 +1,7 @@
import type { LoRaValidation } from "@app/validation/config/lora.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const LoRa = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -31,7 +31,9 @@ export const LoRa = (): JSX.Element => {
name: "region",
label: "Region",
description: "Sets the region for your node",
enumValue: Protobuf.Config_LoRaConfig_RegionCode
properties: {
enumValue: Protobuf.Config_LoRaConfig_RegionCode
}
},
{
type: "number",
@ -62,39 +64,46 @@ export const LoRa = (): JSX.Element => {
name: "modemPreset",
label: "Modem Preset",
description: "Modem preset to use",
enumValue: Protobuf.Config_LoRaConfig_ModemPreset,
formatEnumName: true,
disabledBy: [
{
fieldName: "usePreset"
}
]
],
properties: {
enumValue: Protobuf.Config_LoRaConfig_ModemPreset,
formatEnumName: true
}
},
{
type: "number",
name: "bandwidth",
label: "Bandwidth",
description: "Channel bandwidth in MHz",
suffix: "MHz",
disabledBy: [
{
fieldName: "usePreset",
invert: true
}
]
],
properties: {
suffix: "MHz"
}
},
{
type: "number",
name: "spreadFactor",
label: "Spreading Factor",
description: "Indicates the number of chirps per symbol",
suffix: "CPS",
disabledBy: [
{
fieldName: "usePreset",
invert: true
}
]
],
properties: {
suffix: "CPS"
}
},
{
type: "number",
@ -125,7 +134,9 @@ export const LoRa = (): JSX.Element => {
name: "txPower",
label: "Transmit Power",
description: "Max transmit power",
suffix: "dBm"
properties: {
suffix: "dBm"
}
},
{
type: "toggle",
@ -139,7 +150,9 @@ export const LoRa = (): JSX.Element => {
label: "Frequency Offset",
description:
"Frequency offset to correct for crystal calibration errors",
suffix: "Hz"
properties: {
suffix: "Hz"
}
},
{
type: "toggle",
@ -152,7 +165,9 @@ export const LoRa = (): JSX.Element => {
name: "overrideFrequency",
label: "Override Frequency",
description: "Override frequency",
suffix: "Hz"
properties: {
suffix: "Hz"
}
}
]
}

6
src/components/PageComponents/Config/Network.tsx

@ -1,7 +1,7 @@
import type { NetworkValidation } from "@app/validation/config/network.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Network = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -82,7 +82,9 @@ export const Network = (): JSX.Element => {
name: "addressMode",
label: "Address Mode",
description: "Address assignment selection",
enumValue: Protobuf.Config_NetworkConfig_AddressMode
properties: {
enumValue: Protobuf.Config_NetworkConfig_AddressMode
}
},
{
type: "text",

6
src/components/PageComponents/Config/Position.tsx

@ -1,7 +1,7 @@
import type { PositionValidation } from "@app/validation/config/position.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Position = (): JSX.Element => {
const { config, nodes, hardware, setWorkingConfig } = useDevice();
@ -51,7 +51,9 @@ export const Position = (): JSX.Element => {
name: "positionFlags",
label: "Position Flags",
description: "Configuration options for Position messages",
enumValue: Protobuf.Config_PositionConfig_PositionFlags
properties: {
enumValue: Protobuf.Config_PositionConfig_PositionFlags
}
},
{
type: "number",

26
src/components/PageComponents/Config/Power.tsx

@ -1,7 +1,7 @@
import type { PowerValidation } from "@app/validation/config/power.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Power = (): JSX.Element => {
const { config, setWorkingConfig } = useDevice();
@ -39,7 +39,9 @@ export const Power = (): JSX.Element => {
label: "Shutdown on battery delay",
description:
"Automatically shutdown node after this long when on battery, 0 for indefinite",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
},
{
type: "number",
@ -53,7 +55,9 @@ export const Power = (): JSX.Element => {
label: "No Connection Bluetooth Disabled",
description:
"If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
}
]
},
@ -67,7 +71,9 @@ export const Power = (): JSX.Element => {
label: "Mesh SDS Timeout",
description:
"The device will enter super deep sleep after this time",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
},
{
type: "number",
@ -75,14 +81,18 @@ export const Power = (): JSX.Element => {
label: "Super Deep Sleep Duration",
description:
"How long the device will be in super deep sleep for",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
},
{
type: "number",
name: "lsSecs",
label: "Light Sleep Duration",
description: "How long the device will be in light sleep for",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
},
{
type: "number",
@ -90,7 +100,9 @@ export const Power = (): JSX.Element => {
label: "Minimum Wake Time",
description:
"Minimum amount of time the device will stay awake for after receiving a packet",
suffix: "Seconds"
properties: {
suffix: "Seconds"
}
}
]
}

6
src/components/PageComponents/ModuleConfig/Audio.tsx

@ -1,7 +1,7 @@
import type { AudioValidation } from "@app/validation/moduleConfig/audio.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Audio = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -43,7 +43,9 @@ export const Audio = (): JSX.Element => {
name: "bitrate",
label: "Bitrate",
description: "Bitrate to use for audio encoding",
enumValue: Protobuf.ModuleConfig_AudioConfig_Audio_Baud
properties: {
enumValue: Protobuf.ModuleConfig_AudioConfig_Audio_Baud
}
},
{
type: "number",

20
src/components/PageComponents/ModuleConfig/CannedMessage.tsx

@ -1,7 +1,7 @@
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const CannedMessage = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -55,24 +55,30 @@ export const CannedMessage = (): JSX.Element => {
name: "inputbrokerEventCw",
label: "Clockwise event",
description: "Select input event.",
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
properties: {
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
}
},
{
type: "select",
name: "inputbrokerEventCcw",
label: "Counter Clockwise event",
description: "Select input event.",
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
properties: {
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
}
},
{
type: "select",
name: "inputbrokerEventPress",
label: "Press event",
description: "Select input event",
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
properties: {
enumValue:
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
}
},
{
type: "toggle",

9
src/components/PageComponents/ModuleConfig/ExternalNotification.tsx

@ -1,7 +1,7 @@
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const ExternalNotification = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -37,12 +37,15 @@ export const ExternalNotification = (): JSX.Element => {
name: "outputMs",
label: "Output MS",
description: "Output MS",
suffix: "ms",
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
suffix: "ms"
}
},
{
type: "number",

2
src/components/PageComponents/ModuleConfig/MQTT.tsx

@ -1,6 +1,6 @@
import type { MQTTValidation } from "@app/validation/moduleConfig/mqtt.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@app/core/stores/deviceStore.js";
export const MQTT = (): JSX.Element => {

2
src/components/PageComponents/ModuleConfig/RangeTest.tsx

@ -1,7 +1,7 @@
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const RangeTest = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();

25
src/components/PageComponents/ModuleConfig/Serial.tsx

@ -1,7 +1,7 @@
import type { SerialValidation } from "@app/validation/moduleConfig/serial.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Serial = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -71,38 +71,47 @@ export const Serial = (): JSX.Element => {
name: "baud",
label: "Baud Rate",
description: "The serial baud rate",
enumValue: Protobuf.ModuleConfig_SerialConfig_Serial_Baud,
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
enumValue: Protobuf.ModuleConfig_SerialConfig_Serial_Baud
}
},
{
type: "number",
name: "timeout",
label: "Timeout",
suffix: "Seconds",
description:
"Seconds to wait before we consider your packet as 'done'",
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
suffix: "Seconds"
}
},
{
type: "select",
name: "mode",
label: "Mode",
description: "Select Mode",
enumValue: Protobuf.ModuleConfig_SerialConfig_Serial_Mode,
formatEnumName: true,
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
enumValue: Protobuf.ModuleConfig_SerialConfig_Serial_Mode,
formatEnumName: true
}
}
]
}

9
src/components/PageComponents/ModuleConfig/StoreForward.tsx

@ -1,7 +1,7 @@
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const StoreForward = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -48,12 +48,15 @@ export const StoreForward = (): JSX.Element => {
name: "records",
label: "Number of records",
description: "Number of records to store",
suffix: "Records",
disabledBy: [
{
fieldName: "enabled"
}
]
],
properties: {
suffix: "Records"
}
},
{
type: "number",

13
src/components/PageComponents/ModuleConfig/Telemetry.tsx

@ -1,7 +1,7 @@
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "@components/DynamicForm.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
export const Telemetry = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
@ -29,15 +29,20 @@ export const Telemetry = (): JSX.Element => {
{
type: "number",
name: "deviceUpdateInterval",
label: "Interval to get telemetry data",
suffix: "seconds"
label: "Query Interval",
description: "Interval to get telemetry data",
properties: {
suffix: "seconds"
}
},
{
type: "number",
name: "environmentUpdateInterval",
label: "Update Interval",
description: "How often to send Metrics over the mesh",
suffix: "seconds"
properties: {
suffix: "seconds"
}
},
{
type: "toggle",

12
src/components/PageLayout.tsx

@ -1,7 +1,9 @@
import { cn } from "@app/core/utils/cn.js";
import { LucideIcon, AlignLeftIcon } from "lucide-react";
export interface PageLayoutProps {
label: string;
noPadding?: boolean;
children: React.ReactNode;
actions?: {
icon: LucideIcon;
@ -10,7 +12,8 @@ export interface PageLayoutProps {
}
export const PageLayout = ({
label: title,
label,
noPadding,
actions,
children
}: PageLayoutProps): JSX.Element => {
@ -22,7 +25,7 @@ export const PageLayout = ({
</button>
<div className="flex flex-1 items-center justify-between px-4 md:px-0">
<div className="flex w-full items-center">
<span className="w-full text-lg font-medium">{title}</span>
<span className="w-full text-lg font-medium">{label}</span>
<div className="flex justify-end space-x-4">
{actions?.map((action, index) => (
<button
@ -37,7 +40,10 @@ export const PageLayout = ({
</div>
</div>
</div>
{children}
{/* relative flex h-full w-full flex-col */}
<div className={cn("flex h-full w-full flex-col", !noPadding && "p-3")}>
{children}
</div>
</div>
);
};

Loading…
Cancel
Save