Browse Source

Config reset WIP

pull/652/head
philon- 1 year ago
committed by philon-
parent
commit
0444357713
  1. 12
      src/components/Form/DynamicForm.tsx
  2. 26
      src/components/Form/DynamicFormField.tsx
  3. 5
      src/components/Form/FormInput.tsx
  4. 15
      src/components/Form/FormMultiSelect.tsx
  5. 3
      src/components/Form/FormPasswordGenerator.tsx
  6. 14
      src/components/Form/FormSelect.tsx
  7. 16
      src/components/Form/FormToggle.tsx
  8. 10
      src/components/PageComponents/Config/Bluetooth.tsx
  9. 9
      src/components/PageComponents/Config/Device/index.tsx
  10. 9
      src/components/PageComponents/Config/Display.tsx
  11. 9
      src/components/PageComponents/Config/LoRa.tsx
  12. 23
      src/components/PageComponents/Config/Network/index.tsx
  13. 9
      src/components/PageComponents/Config/Position.tsx
  14. 9
      src/components/PageComponents/Config/Power.tsx
  15. 9
      src/components/PageComponents/Config/Security/Security.tsx
  16. 28
      src/components/PageComponents/ModuleConfig/AmbientLighting.tsx
  17. 26
      src/components/PageComponents/ModuleConfig/Audio.tsx
  18. 28
      src/components/PageComponents/ModuleConfig/CannedMessage.tsx
  19. 28
      src/components/PageComponents/ModuleConfig/DetectionSensor.tsx
  20. 28
      src/components/PageComponents/ModuleConfig/ExternalNotification.tsx
  21. 46
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  22. 26
      src/components/PageComponents/ModuleConfig/NeighborInfo.tsx
  23. 26
      src/components/PageComponents/ModuleConfig/Paxcounter.tsx
  24. 26
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  25. 26
      src/components/PageComponents/ModuleConfig/Serial.tsx
  26. 26
      src/components/PageComponents/ModuleConfig/StoreForward.tsx
  27. 26
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  28. 16
      src/components/UI/Input.tsx
  29. 7
      src/components/UI/Sidebar/SidebarButton.tsx
  30. 22
      src/core/stores/deviceStore.ts
  31. 60
      src/core/utils/deepCompareConfig.test.ts
  32. 68
      src/core/utils/deepCompareConfig.ts
  33. 60
      src/pages/Config/ModuleConfig.tsx
  34. 27
      src/pages/Config/index.tsx

12
src/components/Form/DynamicForm.tsx

@ -45,6 +45,8 @@ export interface GenericFormElementProps<T extends FieldValues, Y> {
control: Control<T>; control: Control<T>;
disabled?: boolean; disabled?: boolean;
field: Y; field: Y;
isDirty?: boolean;
invalid?: boolean;
} }
export interface DynamicFormProps<T extends FieldValues> { export interface DynamicFormProps<T extends FieldValues> {
@ -104,7 +106,8 @@ export function DynamicForm<T extends FieldValues>({
values, values,
}); });
} }
const { handleSubmit, control, getValues, formState } = methods; const { handleSubmit, control, getValues, formState, getFieldState } =
methods;
useEffect(() => { useEffect(() => {
if (!propMethods) { if (!propMethods) {
@ -130,10 +133,7 @@ export function DynamicForm<T extends FieldValues>({
} }
} }
}, [formState.errors]); }, [formState.errors]);
console.debug(
`DynamicForm: ${formId} - Form state:`,
formState.isDirty,
);
const isDisabled = ( const isDisabled = (
disabledBy?: DisabledBy<T>[], disabledBy?: DisabledBy<T>[],
disabled?: boolean, disabled?: boolean,
@ -194,6 +194,8 @@ export function DynamicForm<T extends FieldValues>({
field={field} field={field}
control={control} control={control}
disabled={isDisabled(field.disabledBy, field.disabled)} disabled={isDisabled(field.disabledBy, field.disabled)}
isDirty={getFieldState(field.name).isDirty}
invalid={getFieldState(field.name).invalid}
/> />
</FieldWrapper> </FieldWrapper>
); );

26
src/components/Form/DynamicFormField.tsx

@ -31,19 +31,29 @@ export interface DynamicFormFieldProps<T extends FieldValues> {
field: FieldProps<T>; field: FieldProps<T>;
control: Control<T>; control: Control<T>;
disabled?: boolean; disabled?: boolean;
isDirty?: boolean;
invalid?: boolean;
} }
export function DynamicFormField<T extends FieldValues>({ export function DynamicFormField<T extends FieldValues>({
field, field,
control, control,
disabled, disabled,
isDirty,
invalid,
}: DynamicFormFieldProps<T>) { }: DynamicFormFieldProps<T>) {
switch (field.type) { switch (field.type) {
case "text": case "text":
case "password": case "password":
case "number": case "number":
return ( return (
<GenericInput field={field} control={control} disabled={disabled} /> <GenericInput
field={field}
control={control}
disabled={disabled}
isDirty={isDirty}
invalid={invalid}
/>
); );
case "toggle": case "toggle":
@ -52,6 +62,8 @@ export function DynamicFormField<T extends FieldValues>({
field={field} field={field}
control={control} control={control}
disabled={disabled} disabled={disabled}
isDirty={isDirty}
invalid={invalid}
/> />
); );
case "select": case "select":
@ -60,6 +72,8 @@ export function DynamicFormField<T extends FieldValues>({
field={field} field={field}
control={control} control={control}
disabled={disabled} disabled={disabled}
isDirty={isDirty}
invalid={invalid}
/> />
); );
case "passwordGenerator": case "passwordGenerator":
@ -68,11 +82,19 @@ export function DynamicFormField<T extends FieldValues>({
field={field} field={field}
control={control} control={control}
disabled={disabled} disabled={disabled}
isDirty={isDirty}
invalid={invalid}
/> />
); );
case "multiSelect": case "multiSelect":
return ( return (
<MultiSelectInput field={field} control={control} disabled={disabled} /> <MultiSelectInput
field={field}
control={control}
disabled={disabled}
isDirty={isDirty}
invalid={invalid}
/>
); );
} }
} }

5
src/components/Form/FormInput.tsx

@ -31,14 +31,15 @@ export function GenericInput<T extends FieldValues>({
control, control,
disabled, disabled,
field, field,
isDirty,
invalid,
}: GenericFormElementProps<T, InputFieldProps<T>>) { }: GenericFormElementProps<T, InputFieldProps<T>>) {
const { fieldLength, ...restProperties } = field.properties || {}; const { fieldLength, ...restProperties } = field.properties || {};
const [currentLength, setCurrentLength] = useState<number>( const [currentLength, setCurrentLength] = useState<number>(
fieldLength?.currentValueLength || 0, fieldLength?.currentValueLength || 0,
); );
const { field: controllerField, fieldState: { invalid, isDirty } } = const { field: controllerField } = useController({
useController({
name: field.name, name: field.name,
control, control,
}); });

15
src/components/Form/FormMultiSelect.tsx

@ -6,6 +6,7 @@ import type { FieldValues } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { FLAGS_CONFIG } from "@core/hooks/usePositionFlags.ts"; import type { FLAGS_CONFIG } from "@core/hooks/usePositionFlags.ts";
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx"; import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx";
import { cn } from "@core/utils/cn.ts";
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> { export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "multiSelect"; type: "multiSelect";
@ -23,9 +24,11 @@ export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
export function MultiSelectInput<T extends FieldValues>({ export function MultiSelectInput<T extends FieldValues>({
field, field,
isDirty,
invalid,
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) { }: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const { enumValue, ...remainingProperties } = field.properties; const { enumValue, className, ...remainingProperties } = field.properties;
const isNewConfigStructure = const isNewConfigStructure =
typeof Object.values(enumValue)[0] === "object" && typeof Object.values(enumValue)[0] === "object" &&
@ -48,7 +51,15 @@ export function MultiSelectInput<T extends FieldValues>({
); );
return ( return (
<MultiSelect {...remainingProperties}> <MultiSelect
className={cn([
className,
"rounded-md",
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-5" : "",
invalid ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-5" : "",
])}
{...remainingProperties}
>
{optionsToRender.map((option) => { {optionsToRender.map((option) => {
return ( return (
<MultiSelectItem <MultiSelectItem

3
src/components/Form/FormPasswordGenerator.tsx

@ -30,6 +30,8 @@ export function PasswordGenerator<T extends FieldValues>({
control, control,
field, field,
disabled, disabled,
isDirty,
invalid,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) { }: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
const { isVisible } = usePasswordVisibilityToggle(); const { isVisible } = usePasswordVisibilityToggle();
const { trigger } = useFormContext(); const { trigger } = useFormContext();
@ -45,7 +47,6 @@ export function PasswordGenerator<T extends FieldValues>({
render={( render={(
{ {
field: { value, onChange, ...rest }, field: { value, onChange, ...rest },
fieldState: { invalid, isDirty },
}, },
) => ( ) => (
<Generator <Generator

14
src/components/Form/FormSelect.tsx

@ -10,6 +10,7 @@ import {
SelectValue, SelectValue,
} from "@components/UI/Select.tsx"; } from "@components/UI/Select.tsx";
import { type FieldValues, useController } from "react-hook-form"; import { type FieldValues, useController } from "react-hook-form";
import { cn } from "@core/utils/cn.ts";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> { export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select"; type: "select";
@ -38,6 +39,8 @@ export function SelectInput<T extends FieldValues>({
control, control,
disabled, disabled,
field, field,
isDirty,
invalid,
}: GenericFormElementProps<T, SelectFieldProps<T>>) { }: GenericFormElementProps<T, SelectFieldProps<T>>) {
const { const {
field: { value, onChange, ...rest }, field: { value, onChange, ...rest },
@ -80,7 +83,16 @@ export function SelectInput<T extends FieldValues>({
{...remainingProperties} {...remainingProperties}
{...rest} {...rest}
> >
<SelectTrigger id={field.name}> <SelectTrigger
id={field.name}
className={cn([
field.properties?.className,
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
invalid
? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2"
: "",
])}
>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

16
src/components/Form/FormToggle.tsx

@ -4,6 +4,7 @@ import type {
} from "@components/Form/DynamicForm.tsx"; } from "@components/Form/DynamicForm.tsx";
import { Switch } from "@components/UI/Switch.tsx"; import { Switch } from "@components/UI/Switch.tsx";
import { Controller, type FieldValues } from "react-hook-form"; import { Controller, type FieldValues } from "react-hook-form";
import { cn } from "@core/utils/cn.ts";
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> { export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
type: "toggle"; type: "toggle";
@ -14,12 +15,16 @@ export function ToggleInput<T extends FieldValues>({
control, control,
disabled, disabled,
field, field,
isDirty,
invalid,
}: GenericFormElementProps<T, ToggleFieldProps<T>>) { }: GenericFormElementProps<T, ToggleFieldProps<T>>) {
return ( return (
<Controller <Controller
name={field.name} name={field.name}
control={control} control={control}
render={({ field: { value, onChange, ...rest } }) => ( render={(
{ field: { value, onChange, ...rest } },
) => (
<Switch <Switch
checked={value} checked={value}
onCheckedChange={(v) => { onCheckedChange={(v) => {
@ -29,6 +34,15 @@ export function ToggleInput<T extends FieldValues>({
id={field.name} id={field.name}
disabled={disabled} disabled={disabled}
{...field.properties} {...field.properties}
className={cn([
field.properties?.className,
isDirty
? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2"
: "",
invalid
? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2"
: "",
])}
{...rest} {...rest}
/> />
)} )}

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

@ -10,14 +10,22 @@ import {
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface BluetoothConfigProps { interface BluetoothConfigProps {
onFormInit: DynamicFormFormInit<BluetoothValidation>; onFormInit: DynamicFormFormInit<BluetoothValidation>;
} }
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => { export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
const { config, setWorkingConfig, getEffectiveConfig } = useDevice(); const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const onSubmit = (data: BluetoothValidation) => { const onSubmit = (data: BluetoothValidation) => {
if (deepCompareConfig(config.bluetooth, data, true)) {
removeWorkingConfig("bluetooth");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

9
src/components/PageComponents/Config/Device/index.tsx

@ -11,16 +11,23 @@ import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts"; import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface DeviceConfigProps { interface DeviceConfigProps {
onFormInit: DynamicFormFormInit<DeviceValidation>; onFormInit: DynamicFormFormInit<DeviceValidation>;
} }
export const Device = ({ onFormInit }: DeviceConfigProps) => { export const Device = ({ onFormInit }: DeviceConfigProps) => {
const { config, setWorkingConfig, getEffectiveConfig } = useDevice(); const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const { validateRoleSelection } = useUnsafeRolesDialog(); const { validateRoleSelection } = useUnsafeRolesDialog();
const onSubmit = (data: DeviceValidation) => { const onSubmit = (data: DeviceValidation) => {
if (deepCompareConfig(config.device, data, true)) {
removeWorkingConfig("device");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

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

@ -10,15 +10,22 @@ import {
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface DisplayConfigProps { interface DisplayConfigProps {
onFormInit: DynamicFormFormInit<DisplayValidation>; onFormInit: DynamicFormFormInit<DisplayValidation>;
} }
export const Display = ({ onFormInit }: DisplayConfigProps) => { export const Display = ({ onFormInit }: DisplayConfigProps) => {
const { config, setWorkingConfig, getEffectiveConfig } = useDevice(); const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const onSubmit = (data: DisplayValidation) => { const onSubmit = (data: DisplayValidation) => {
if (deepCompareConfig(config.display, data, true)) {
removeWorkingConfig("display");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

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

@ -10,15 +10,22 @@ import {
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface LoRaConfigProps { interface LoRaConfigProps {
onFormInit: DynamicFormFormInit<LoRaValidation>; onFormInit: DynamicFormFormInit<LoRaValidation>;
} }
export const LoRa = ({ onFormInit }: LoRaConfigProps) => { export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
const { config, setWorkingConfig, getEffectiveConfig } = useDevice(); const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const onSubmit = (data: LoRaValidation) => { const onSubmit = (data: LoRaValidation) => {
if (deepCompareConfig(config.lora, data, true)) {
removeWorkingConfig("lora");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

23
src/components/PageComponents/Config/Network/index.tsx

@ -14,22 +14,20 @@ import {
} from "@core/utils/ip.ts"; } from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface NetworkConfigProps { interface NetworkConfigProps {
onFormInit: DynamicFormFormInit<NetworkValidation>; onFormInit: DynamicFormFormInit<NetworkValidation>;
} }
export const Network = ({ onFormInit }: NetworkConfigProps) => { export const Network = ({ onFormInit }: NetworkConfigProps) => {
const { config, setWorkingConfig, getEffectiveConfig } = useDevice(); const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const networkConfig = getEffectiveConfig("network"); const networkConfig = getEffectiveConfig("network");
const onSubmit = (data: NetworkValidation) => { const onSubmit = (data: NetworkValidation) => {
setWorkingConfig( const payload = {
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "network",
value: {
...data, ...data,
ipv4Config: create( ipv4Config: create(
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
@ -40,7 +38,18 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""), dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
}, },
), ),
}, };
if (deepCompareConfig(config.network, payload, true)) {
removeWorkingConfig("network");
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "network",
value: payload,
}, },
}), }),
); );

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

@ -15,18 +15,25 @@ import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PositionConfigProps { interface PositionConfigProps {
onFormInit: DynamicFormFormInit<PositionValidation>; onFormInit: DynamicFormFormInit<PositionValidation>;
} }
export const Position = ({ onFormInit }: PositionConfigProps) => { export const Position = ({ onFormInit }: PositionConfigProps) => {
const { setWorkingConfig, config, getEffectiveConfig } = useDevice(); const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags( const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
getEffectiveConfig("position")?.positionFlags ?? 0, getEffectiveConfig("position")?.positionFlags ?? 0,
); );
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const onSubmit = (data: PositionValidation) => { const onSubmit = (data: PositionValidation) => {
if (deepCompareConfig(config.position, data, true)) {
removeWorkingConfig("position");
return;
}
return setWorkingConfig( return setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

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

@ -10,15 +10,22 @@ import {
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PowerConfigProps { interface PowerConfigProps {
onFormInit: DynamicFormFormInit<PowerValidation>; onFormInit: DynamicFormFormInit<PowerValidation>;
} }
export const Power = ({ onFormInit }: PowerConfigProps) => { export const Power = ({ onFormInit }: PowerConfigProps) => {
const { setWorkingConfig, config, getEffectiveConfig } = useDevice(); const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig"); const { t } = useTranslation("deviceConfig");
const onSubmit = (data: PowerValidation) => { const onSubmit = (data: PowerValidation) => {
if (deepCompareConfig(config.power, data, true)) {
removeWorkingConfig("power");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

9
src/components/PageComponents/Config/Security/Security.tsx

@ -18,14 +18,18 @@ import { fromByteArray, toByteArray } from "base64-js";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { type DefaultValues, useForm } from "react-hook-form"; import { type DefaultValues, useForm } from "react-hook-form";
import { createZodResolver } from "@components/Form/createZodResolver.ts"; import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface SecurityConfigProps { interface SecurityConfigProps {
onFormInit: DynamicFormFormInit<RawSecurity>; onFormInit: DynamicFormFormInit<RawSecurity>;
} }
export const Security = ({ onFormInit }: SecurityConfigProps) => { export const Security = ({ onFormInit }: SecurityConfigProps) => {
const { const {
config,
setWorkingConfig, setWorkingConfig,
setDialogOpen, setDialogOpen,
getEffectiveConfig, getEffectiveConfig,
removeWorkingConfig,
} = useDevice(); } = useDevice();
const { removeError } = useAppStore(); const { removeError } = useAppStore();
@ -86,6 +90,11 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
], ],
}; };
if (deepCompareConfig(config.security, payload, true)) {
removeWorkingConfig("security");
return;
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {

28
src/components/PageComponents/ModuleConfig/AmbientLighting.tsx

@ -4,15 +4,35 @@ import {
AmbientLightingValidationSchema, AmbientLightingValidationSchema,
} from "@app/validation/moduleConfig/ambientLighting.ts"; } from "@app/validation/moduleConfig/ambientLighting.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface AmbientLightingModuleConfigProps {
onFormInit: DynamicFormFormInit<AmbientLightingValidation>;
}
export const AmbientLighting = () => { export const AmbientLighting = (
const { moduleConfig, setWorkingModuleConfig } = useDevice(); { onFormInit }: AmbientLightingModuleConfigProps,
) => {
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AmbientLightingValidation) => { const onSubmit = (data: AmbientLightingValidation) => {
if (deepCompareConfig(moduleConfig.ambientLighting, data, true)) {
removeWorkingModuleConfig("ambientLighting");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +46,11 @@ export const AmbientLighting = () => {
return ( return (
<DynamicForm<AmbientLightingValidation> <DynamicForm<AmbientLightingValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={AmbientLightingValidationSchema} validationSchema={AmbientLightingValidationSchema}
formId="ModuleConfig_AmbientLightingConfig" formId="ModuleConfig_AmbientLightingConfig"
defaultValues={moduleConfig.ambientLighting} defaultValues={moduleConfig.ambientLighting}
values={getEffectiveModuleConfig("ambientLighting")}
fieldGroups={[ fieldGroups={[
{ {
label: t("ambientLighting.title"), label: t("ambientLighting.title"),

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

@ -3,16 +3,34 @@ import {
AudioValidationSchema, AudioValidationSchema,
} from "@app/validation/moduleConfig/audio.ts"; } from "@app/validation/moduleConfig/audio.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface AudioModuleConfigProps {
onFormInit: DynamicFormFormInit<AudioValidation>;
}
export const Audio = () => { export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AudioValidation) => { const onSubmit = (data: AudioValidation) => {
if (deepCompareConfig(moduleConfig.audio, data, true)) {
removeWorkingModuleConfig("audio");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const Audio = () => {
return ( return (
<DynamicForm<AudioValidation> <DynamicForm<AudioValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={AudioValidationSchema} validationSchema={AudioValidationSchema}
formId="ModuleConfig_AudioConfig" formId="ModuleConfig_AudioConfig"
defaultValues={moduleConfig.audio} defaultValues={moduleConfig.audio}
values={getEffectiveModuleConfig("audio")}
fieldGroups={[ fieldGroups={[
{ {
label: t("audio.title"), label: t("audio.title"),

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

@ -3,16 +3,36 @@ import {
CannedMessageValidationSchema, CannedMessageValidationSchema,
} from "@app/validation/moduleConfig/cannedMessage.ts"; } from "@app/validation/moduleConfig/cannedMessage.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface CannedMessageModuleConfigProps {
onFormInit: DynamicFormFormInit<CannedMessageValidation>;
}
export const CannedMessage = () => { export const CannedMessage = (
const { moduleConfig, setWorkingModuleConfig } = useDevice(); { onFormInit }: CannedMessageModuleConfigProps,
) => {
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: CannedMessageValidation) => { const onSubmit = (data: CannedMessageValidation) => {
if (deepCompareConfig(moduleConfig.cannedMessage, data, true)) {
removeWorkingModuleConfig("cannedMessage");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +46,11 @@ export const CannedMessage = () => {
return ( return (
<DynamicForm<CannedMessageValidation> <DynamicForm<CannedMessageValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={CannedMessageValidationSchema} validationSchema={CannedMessageValidationSchema}
formId="ModuleConfig_CannedMessageConfig" formId="ModuleConfig_CannedMessageConfig"
defaultValues={moduleConfig.cannedMessage} defaultValues={moduleConfig.cannedMessage}
values={getEffectiveModuleConfig("cannedMessage")}
fieldGroups={[ fieldGroups={[
{ {
label: t("cannedMessage.title"), label: t("cannedMessage.title"),

28
src/components/PageComponents/ModuleConfig/DetectionSensor.tsx

@ -4,15 +4,35 @@ import {
DetectionSensorValidationSchema, DetectionSensorValidationSchema,
} from "@app/validation/moduleConfig/detectionSensor.ts"; } from "@app/validation/moduleConfig/detectionSensor.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface DetectionSensorModuleConfigProps {
onFormInit: DynamicFormFormInit<DetectionSensorValidation>;
}
export const DetectionSensor = () => { export const DetectionSensor = (
const { moduleConfig, setWorkingModuleConfig } = useDevice(); { onFormInit }: DetectionSensorModuleConfigProps,
) => {
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: DetectionSensorValidation) => { const onSubmit = (data: DetectionSensorValidation) => {
if (deepCompareConfig(moduleConfig.detectionSensor, data, true)) {
removeWorkingModuleConfig("detectionSensor");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +46,11 @@ export const DetectionSensor = () => {
return ( return (
<DynamicForm<DetectionSensorValidation> <DynamicForm<DetectionSensorValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={DetectionSensorValidationSchema} validationSchema={DetectionSensorValidationSchema}
formId="ModuleConfig_DetectionSensorConfig" formId="ModuleConfig_DetectionSensorConfig"
defaultValues={moduleConfig.detectionSensor} defaultValues={moduleConfig.detectionSensor}
values={getEffectiveModuleConfig("detectionSensor")}
fieldGroups={[ fieldGroups={[
{ {
label: t("detectionSensor.title"), label: t("detectionSensor.title"),

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

@ -3,16 +3,36 @@ import {
ExternalNotificationValidationSchema, ExternalNotificationValidationSchema,
} from "@app/validation/moduleConfig/externalNotification.ts"; } from "@app/validation/moduleConfig/externalNotification.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface ExternalNotificationModuleConfigProps {
onFormInit: DynamicFormFormInit<ExternalNotificationValidation>;
}
export const ExternalNotification = () => { export const ExternalNotification = (
const { moduleConfig, setWorkingModuleConfig } = useDevice(); { onFormInit }: ExternalNotificationModuleConfigProps,
) => {
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: ExternalNotificationValidation) => { const onSubmit = (data: ExternalNotificationValidation) => {
if (deepCompareConfig(moduleConfig.externalNotification, data, true)) {
removeWorkingModuleConfig("externalNotification");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +46,11 @@ export const ExternalNotification = () => {
return ( return (
<DynamicForm<ExternalNotificationValidation> <DynamicForm<ExternalNotificationValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={ExternalNotificationValidationSchema} validationSchema={ExternalNotificationValidationSchema}
formId="ModuleConfig_ExternalNotificationConfig" formId="ModuleConfig_ExternalNotificationConfig"
defaultValues={moduleConfig.externalNotification} defaultValues={moduleConfig.externalNotification}
values={getEffectiveModuleConfig("externalNotification")}
fieldGroups={[ fieldGroups={[
{ {
label: t("externalNotification.title"), label: t("externalNotification.title"),

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

@ -4,26 +4,47 @@ import {
MqttValidationSchema, MqttValidationSchema,
} from "@app/validation/moduleConfig/mqtt.ts"; } from "@app/validation/moduleConfig/mqtt.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface MqttModuleConfigProps {
onFormInit: DynamicFormFormInit<MqttValidation>;
}
export const MQTT = () => { export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
const { config, moduleConfig, setWorkingModuleConfig } = useDevice(); const {
config,
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: MqttValidation) => { const onSubmit = (data: MqttValidation) => {
setWorkingModuleConfig( const payload = {
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "mqtt",
value: {
...data, ...data,
mapReportSettings: create( mapReportSettings: create(
Protobuf.ModuleConfig.ModuleConfig_MapReportSettingsSchema, Protobuf.ModuleConfig.ModuleConfig_MapReportSettingsSchema,
data.mapReportSettings, data.mapReportSettings,
), ),
}, };
if (deepCompareConfig(moduleConfig.mqtt, payload, true)) {
removeWorkingModuleConfig("mqtt");
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "mqtt",
value: payload,
}, },
}), }),
); );
@ -32,9 +53,16 @@ export const MQTT = () => {
return ( return (
<DynamicForm<MqttValidation> <DynamicForm<MqttValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={MqttValidationSchema} validationSchema={MqttValidationSchema}
formId="ModuleConfig_MqttConfig" formId="ModuleConfig_MqttConfig"
defaultValues={moduleConfig.mqtt} defaultValues={moduleConfig.mqtt}
values={(() => {
const cfg = getEffectiveModuleConfig("mqtt");
return cfg
? { ...cfg, mapReportSettings: cfg.mapReportSettings ?? {} }
: undefined;
})()}
fieldGroups={[ fieldGroups={[
{ {
label: t("mqtt.title"), label: t("mqtt.title"),

26
src/components/PageComponents/ModuleConfig/NeighborInfo.tsx

@ -4,15 +4,33 @@ import {
NeighborInfoValidationSchema, NeighborInfoValidationSchema,
} from "@app/validation/moduleConfig/neighborInfo.ts"; } from "@app/validation/moduleConfig/neighborInfo.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface NeighborInfoModuleConfigProps {
onFormInit: DynamicFormFormInit<NeighborInfoValidation>;
}
export const NeighborInfo = () => { export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: NeighborInfoValidation) => { const onSubmit = (data: NeighborInfoValidation) => {
if (deepCompareConfig(moduleConfig.neighborInfo, data, true)) {
removeWorkingModuleConfig("neighborInfo");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const NeighborInfo = () => {
return ( return (
<DynamicForm<NeighborInfoValidation> <DynamicForm<NeighborInfoValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={NeighborInfoValidationSchema} validationSchema={NeighborInfoValidationSchema}
formId="ModuleConfig_NeighborInfoConfig" formId="ModuleConfig_NeighborInfoConfig"
defaultValues={moduleConfig.neighborInfo} defaultValues={moduleConfig.neighborInfo}
values={getEffectiveModuleConfig("neighborInfo")}
fieldGroups={[ fieldGroups={[
{ {
label: t("neighborInfo.title"), label: t("neighborInfo.title"),

26
src/components/PageComponents/ModuleConfig/Paxcounter.tsx

@ -3,16 +3,34 @@ import {
PaxcounterValidationSchema, PaxcounterValidationSchema,
} from "@app/validation/moduleConfig/paxcounter.ts"; } from "@app/validation/moduleConfig/paxcounter.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PaxcounterModuleConfigProps {
onFormInit: DynamicFormFormInit<PaxcounterValidation>;
}
export const Paxcounter = () => { export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: PaxcounterValidation) => { const onSubmit = (data: PaxcounterValidation) => {
if (deepCompareConfig(moduleConfig.paxcounter, data, true)) {
removeWorkingModuleConfig("paxcounter");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const Paxcounter = () => {
return ( return (
<DynamicForm<PaxcounterValidation> <DynamicForm<PaxcounterValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={PaxcounterValidationSchema} validationSchema={PaxcounterValidationSchema}
formId="ModuleConfig_PaxcounterConfig" formId="ModuleConfig_PaxcounterConfig"
defaultValues={moduleConfig.paxcounter} defaultValues={moduleConfig.paxcounter}
values={getEffectiveModuleConfig("paxcounter")}
fieldGroups={[ fieldGroups={[
{ {
label: t("paxcounter.title"), label: t("paxcounter.title"),

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

@ -3,16 +3,34 @@ import {
RangeTestValidationSchema, RangeTestValidationSchema,
} from "@app/validation/moduleConfig/rangeTest.ts"; } from "@app/validation/moduleConfig/rangeTest.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface RangeTestModuleConfigProps {
onFormInit: DynamicFormFormInit<RangeTestValidation>;
}
export const RangeTest = () => { export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: RangeTestValidation) => { const onSubmit = (data: RangeTestValidation) => {
if (deepCompareConfig(moduleConfig.rangeTest, data, true)) {
removeWorkingModuleConfig("rangeTest");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const RangeTest = () => {
return ( return (
<DynamicForm<RangeTestValidation> <DynamicForm<RangeTestValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={RangeTestValidationSchema} validationSchema={RangeTestValidationSchema}
formId="ModuleConfig_RangeTestConfig" formId="ModuleConfig_RangeTestConfig"
defaultValues={moduleConfig.rangeTest} defaultValues={moduleConfig.rangeTest}
values={getEffectiveModuleConfig("rangeTest")}
fieldGroups={[ fieldGroups={[
{ {
label: t("rangeTest.title"), label: t("rangeTest.title"),

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

@ -3,16 +3,34 @@ import {
SerialValidationSchema, SerialValidationSchema,
} from "@app/validation/moduleConfig/serial.ts"; } from "@app/validation/moduleConfig/serial.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface SerialModuleConfigProps {
onFormInit: DynamicFormFormInit<SerialValidation>;
}
export const Serial = () => { export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: SerialValidation) => { const onSubmit = (data: SerialValidation) => {
if (deepCompareConfig(moduleConfig.serial, data, true)) {
removeWorkingModuleConfig("serial");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const Serial = () => {
return ( return (
<DynamicForm<SerialValidation> <DynamicForm<SerialValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={SerialValidationSchema} validationSchema={SerialValidationSchema}
formId="ModuleConfig_SerialConfig" formId="ModuleConfig_SerialConfig"
defaultValues={moduleConfig.serial} defaultValues={moduleConfig.serial}
values={getEffectiveModuleConfig("serial")}
fieldGroups={[ fieldGroups={[
{ {
label: t("serial.title"), label: t("serial.title"),

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

@ -3,16 +3,34 @@ import {
StoreForwardValidationSchema, StoreForwardValidationSchema,
} from "@app/validation/moduleConfig/storeForward.ts"; } from "@app/validation/moduleConfig/storeForward.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface StoreForwardModuleConfigProps {
onFormInit: DynamicFormFormInit<StoreForwardValidation>;
}
export const StoreForward = () => { export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: StoreForwardValidation) => { const onSubmit = (data: StoreForwardValidation) => {
if (deepCompareConfig(moduleConfig.storeForward, data, true)) {
removeWorkingModuleConfig("storeForward");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const StoreForward = () => {
return ( return (
<DynamicForm<StoreForwardValidation> <DynamicForm<StoreForwardValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={StoreForwardValidationSchema} validationSchema={StoreForwardValidationSchema}
formId="ModuleConfig_StoreForwardConfig" formId="ModuleConfig_StoreForwardConfig"
defaultValues={moduleConfig.storeForward} defaultValues={moduleConfig.storeForward}
values={getEffectiveModuleConfig("storeForward")}
fieldGroups={[ fieldGroups={[
{ {
label: t("storeForward.title"), label: t("storeForward.title"),

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

@ -3,16 +3,34 @@ import {
TelemetryValidationSchema, TelemetryValidationSchema,
} from "@app/validation/moduleConfig/telemetry.ts"; } from "@app/validation/moduleConfig/telemetry.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface TelemetryModuleConfigProps {
onFormInit: DynamicFormFormInit<TelemetryValidation>;
}
export const Telemetry = () => { export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
const { moduleConfig, setWorkingModuleConfig } = useDevice(); const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TelemetryValidation) => { const onSubmit = (data: TelemetryValidation) => {
if (deepCompareConfig(moduleConfig.telemetry, data, true)) {
removeWorkingModuleConfig("telemetry");
return;
}
setWorkingModuleConfig( setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, { create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: { payloadVariant: {
@ -26,9 +44,11 @@ export const Telemetry = () => {
return ( return (
<DynamicForm<TelemetryValidation> <DynamicForm<TelemetryValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={TelemetryValidationSchema} validationSchema={TelemetryValidationSchema}
formId="ModuleConfig_TelemetryConfig" formId="ModuleConfig_TelemetryConfig"
defaultValues={moduleConfig.telemetry} defaultValues={moduleConfig.telemetry}
values={getEffectiveModuleConfig("telemetry")}
fieldGroups={[ fieldGroups={[
{ {
label: t("telemetry.title"), label: t("telemetry.title"),

16
src/components/UI/Input.tsx

@ -13,9 +13,9 @@ const inputVariants = cva(
variant: { variant: {
default: "border-slate-300 dark:border-slate-500", default: "border-slate-300 dark:border-slate-500",
invalid: invalid:
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500", "border-2 border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
dirty: dirty:
"border-sky-500 dark:border-sky-500 focus:ring-sky-500 dark:focus:ring-sky-500", "border-2 border-sky-500 dark:border-sky-500 focus:ring-sky-500 dark:focus:ring-sky-500",
}, },
}, },
defaultVariants: { defaultVariants: {
@ -165,8 +165,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<span <span
className={cn( className={cn(
"inline-flex items-center border border-l-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300", "inline-flex items-center border border-l-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300",
variant === "invalid" && "border-red-500 dark:border-red-500", variant === "invalid" &&
variant === "dirty" && "border-sky-500 dark:border-sky-500", "border-2 border-l-0 border-red-500 dark:border-red-500",
variant === "dirty" &&
"border-2 border-l-0 border-sky-500 dark:border-sky-500",
!hasActions && "rounded-r-md", !hasActions && "rounded-r-md",
)} )}
> >
@ -178,8 +180,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<div <div
className={cn( className={cn(
"flex items-center divide-x divide-slate-300 border border-l-0 border-slate-300 dark:divide-slate-700 dark:border-slate-500", "flex items-center divide-x divide-slate-300 border border-l-0 border-slate-300 dark:divide-slate-700 dark:border-slate-500",
variant === "invalid" && "border-red-500 dark:border-red-500", variant === "invalid" &&
variant === "dirty" && "border-sky-500 dark:border-sky-500", "border-2 border-l-0 border-red-500 dark:border-red-500",
variant === "dirty" &&
"border-2 border-l-0 border-sky-500 dark:border-sky-500",
disabled && disabled &&
"border-slate-200 dark:border-slate-700 divide-slate-200", "border-slate-200 dark:border-slate-700 divide-slate-200",
!hasSuffix && "rounded-r-md", !hasSuffix && "rounded-r-md",

7
src/components/UI/Sidebar/SidebarButton.tsx

@ -13,6 +13,7 @@ export interface SidebarButtonProps {
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
preventCollapse?: boolean; preventCollapse?: boolean;
isDirty?: boolean;
} }
export const SidebarButton = ({ export const SidebarButton = ({
@ -24,6 +25,7 @@ export const SidebarButton = ({
onClick, onClick,
disabled = false, disabled = false,
preventCollapse = false, preventCollapse = false,
isDirty,
}: SidebarButtonProps) => { }: SidebarButtonProps) => {
const { isCollapsed: isSidebarCollapsed } = useSidebar(); const { isCollapsed: isSidebarCollapsed } = useSidebar();
const isButtonCollapsed = isSidebarCollapsed && !preventCollapse; const isButtonCollapsed = isSidebarCollapsed && !preventCollapse;
@ -64,13 +66,14 @@ export const SidebarButton = ({
{label} {label}
</span> </span>
{!isButtonCollapsed && !active && count && count > 0 && ( {!isButtonCollapsed && ((!active && count && count > 0) || isDirty) && (
<div <div
className={cn( className={cn(
"ml-auto flex-shrink-0 justify-end text-white text-xs rounded-full px-1.5 py-0.5 bg-red-600", "ml-auto flex-shrink-0 justify-end text-white text-xs rounded-full px-1.5 py-0.5",
"flex-shrink-0", "flex-shrink-0",
"transition-opacity duration-300 ease-in-out", "transition-opacity duration-300 ease-in-out",
isButtonCollapsed ? "opacity-0 invisible" : "opacity-100 visible", isButtonCollapsed ? "opacity-0 invisible" : "opacity-100 visible",
isDirty ? "bg-sky-500" : "bg-red-600",
)} )}
> >
{count} {count}

22
src/core/stores/deviceStore.ts

@ -308,15 +308,7 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
(wc) => wc.payloadVariant.case === config.payloadVariant.case, (wc) => wc.payloadVariant.case === config.payloadVariant.case,
); );
if ( if (index !== -1) {
// This doesn't quite work as expected, but it is a workaround for now.
JSON.stringify(
current(device.config[config.payloadVariant.case]),
) ===
JSON.stringify(config.payloadVariant.value)
) {
device.workingConfig.splice(index, 1);
} else if (index !== -1) {
device.workingConfig[index] = config; device.workingConfig[index] = config;
} else { } else {
device.workingConfig.push(config); device.workingConfig.push(config);
@ -336,16 +328,8 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
wmc.payloadVariant.case === wmc.payloadVariant.case ===
moduleConfig.payloadVariant.case, moduleConfig.payloadVariant.case,
); );
if (
JSON.stringify( if (index !== -1) {
current(
device.moduleConfig[moduleConfig.payloadVariant.case],
),
) ===
JSON.stringify(moduleConfig.payloadVariant.value)
) {
device.workingModuleConfig.splice(index, 1);
} else if (index !== -1) {
device.workingModuleConfig[index] = moduleConfig; device.workingModuleConfig[index] = moduleConfig;
} else { } else {
device.workingModuleConfig.push(moduleConfig); device.workingModuleConfig.push(moduleConfig);

60
src/core/utils/deepCompareConfig.test.ts

@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { deepCompareConfig } from "./deepCompareConfig";
describe("deepCompareConfig", () => {
it("returns true for identical primitives", () => {
expect(deepCompareConfig(5, 5)).toBe(true);
expect(deepCompareConfig("foo", "foo")).toBe(true);
expect(deepCompareConfig(true, true)).toBe(true);
});
it("returns false for different primitives", () => {
expect(deepCompareConfig(5, 6)).toBe(false);
expect(deepCompareConfig("foo", "bar")).toBe(false);
expect(deepCompareConfig(true, false)).toBe(false);
});
it("handles nulls correctly", () => {
expect(deepCompareConfig(null, null)).toBe(true);
expect(deepCompareConfig(null, undefined)).toBe(false);
expect(deepCompareConfig(null, {})).toBe(false);
});
it("allows undefined in working when allowUndefined is true", () => {
expect(deepCompareConfig({ a: 1 }, { a: undefined }, true)).toBe(true);
expect(deepCompareConfig([1, 2, 3], [1, undefined, 3], true)).toBe(true);
});
it("rejects undefined in working when allowUndefined is false", () => {
expect(deepCompareConfig({ a: 1 }, { a: undefined }, false)).toBe(false);
});
it("compares arrays deeply", () => {
expect(deepCompareConfig([1, [2, 3]], [1, [2, 3]])).toBe(true);
expect(deepCompareConfig([1, [2, 3]], [1, [2, 4]])).toBe(false);
});
it("compares objects deeply", () => {
const existing = { x: 10, y: { z: 20 } };
const workingEqual = { x: 10, y: { z: 20 } };
const workingDiff = { x: 10, y: { z: 21 } };
expect(deepCompareConfig(existing, workingEqual)).toBe(true);
expect(deepCompareConfig(existing, workingDiff)).toBe(false);
});
it("ignores $typeName key in existing", () => {
const existing = { $typeName: "Test", a: 1 };
const working = { a: 1 };
expect(deepCompareConfig(existing, working)).toBe(true);
});
it("fails when working has extra keys", () => {
expect(deepCompareConfig({ a: 1 }, { a: 1, b: 2 })).toBe(false);
});
it("allows working arrays to be shorter if allowUndefined is true", () => {
expect(deepCompareConfig([1, 2, 3, 4], [1, 2], true)).toBe(true);
expect(deepCompareConfig([1, 2, 3, 4], [1, 2], false)).toBe(false);
});
});

68
src/core/utils/deepCompareConfig.ts

@ -0,0 +1,68 @@
export function deepCompareConfig(
existing: unknown,
working: unknown,
allowUndefined = false,
): boolean {
if (existing === working) return true;
if (
allowUndefined &&
(typeof existing === "undefined" || typeof working === "undefined")
) return true;
if (typeof existing !== typeof working) {
return false;
}
if (existing === null || working === null) {
if (existing !== working) {
return false;
}
return true;
}
if (Array.isArray(existing) && Array.isArray(working)) {
if (existing.length !== working.length && !allowUndefined) {
return false;
}
for (let i = 0; i < existing.length; i++) {
if (
!deepCompareConfig(existing[i], working[i], allowUndefined)
) {
return false;
}
}
return true;
}
if (typeof existing === "object" && typeof working === "object") {
const exObj = existing as Record<string, unknown>;
const woObj = working as Record<string, unknown>;
const keys = new Set<string>([
...Object.keys(exObj),
...Object.keys(woObj),
]);
for (const key of keys) {
if (key === "$typeName") continue;
const hasExisting = Object.prototype.hasOwnProperty.call(exObj, key);
const hasWorking = Object.prototype.hasOwnProperty.call(woObj, key);
const valExisting = exObj[key];
const valWorking = woObj[key];
if (!hasWorking && allowUndefined && hasExisting) {
continue;
}
if (
!deepCompareConfig(valExisting, valWorking, allowUndefined)
) {
return false;
}
}
return true;
}
return false;
}

60
src/pages/Config/ModuleConfig.tsx

@ -17,59 +17,93 @@ import {
TabsTrigger, TabsTrigger,
} from "@components/UI/Tabs.tsx"; } from "@components/UI/Tabs.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {
useDevice,
type ValidModuleConfigType,
} from "@core/stores/deviceStore.ts";
import { useMemo } from "react";
import type { ComponentType } from "react";
import type { UseFormReturn } from "react-hook-form";
interface ConfigProps {
// We can get rid of this exception if we import every config schema and pass the union type
// deno-lint-ignore no-explicit-any
onFormInit: (methods: UseFormReturn<any>) => void;
}
type TabItem = {
case: ValidModuleConfigType;
label: string;
element: ComponentType<ConfigProps>;
count?: number;
};
export const ModuleConfig = () => { export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
const { getWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig"); const { t } = useTranslation("moduleConfig");
const tabs = [ const tabs: TabItem[] = [
{ {
case: "mqtt",
label: t("page.tabMqtt"), label: t("page.tabMqtt"),
element: MQTT, element: MQTT,
}, },
{ {
case: "serial",
label: t("page.tabSerial"), label: t("page.tabSerial"),
element: Serial, element: Serial,
}, },
{ {
case: "externalNotification",
label: t("page.tabExternalNotification"), label: t("page.tabExternalNotification"),
element: ExternalNotification, element: ExternalNotification,
}, },
{ {
case: "storeForward",
label: t("page.tabStoreAndForward"), label: t("page.tabStoreAndForward"),
element: StoreForward, element: StoreForward,
}, },
{ {
case: "rangeTest",
label: t("page.tabRangeTest"), label: t("page.tabRangeTest"),
element: RangeTest, element: RangeTest,
}, },
{ {
case: "telemetry",
label: t("page.tabTelemetry"), label: t("page.tabTelemetry"),
element: Telemetry, element: Telemetry,
}, },
{ {
case: "cannedMessage",
label: t("page.tabCannedMessage"), label: t("page.tabCannedMessage"),
element: CannedMessage, element: CannedMessage,
}, },
{ {
case: "audio",
label: t("page.tabAudio"), label: t("page.tabAudio"),
element: Audio, element: Audio,
}, },
{ {
case: "neighborInfo",
label: t("page.tabNeighborInfo"), label: t("page.tabNeighborInfo"),
element: NeighborInfo, element: NeighborInfo,
}, },
{ {
case: "ambientLighting",
label: t("page.tabAmbientLighting"), label: t("page.tabAmbientLighting"),
element: AmbientLighting, element: AmbientLighting,
}, },
{ {
case: "detectionSensor",
label: t("page.tabDetectionSensor"), label: t("page.tabDetectionSensor"),
element: DetectionSensor, element: DetectionSensor,
}, },
{ { case: "paxcounter", label: t("page.tabPaxcounter"), element: Paxcounter },
label: t("page.tabPaxcounter"), ] as const;
element: Paxcounter,
}, const flags = useMemo(
]; () =>
new Map(tabs.map((tab) => [tab.case, getWorkingModuleConfig(tab.case)])),
[tabs, getWorkingModuleConfig],
);
return ( return (
<Tabs defaultValue={t("page.tabMqtt")}> <Tabs defaultValue={t("page.tabMqtt")}>
@ -78,15 +112,23 @@ export const ModuleConfig = () => {
<TabsTrigger <TabsTrigger
key={tab.label} key={tab.label}
value={tab.label} value={tab.label}
className="dark:text-white" className="dark:text-white relative"
> >
{tab.label} {tab.label}
{flags.get(tab.case) && (
<span className="absolute -top-0.5 -right-0.5 z-50 flex size-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-500 opacity-25">
</span>
<span className="relative inline-flex size-3 rounded-full bg-sky-500">
</span>
</span>
)}
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabsContent key={tab.label} value={tab.label}> <TabsContent key={tab.label} value={tab.label}>
<tab.element /> <tab.element onFormInit={onFormInit} />
</TabsContent> </TabsContent>
))} ))}
</Tabs> </Tabs>

27
src/pages/Config/index.tsx

@ -58,7 +58,6 @@ const ConfigPage = () => {
setIsSaving(true); setIsSaving(true);
try { try {
if (activeConfigSection === "device") {
await Promise.all( await Promise.all(
workingConfig.map((config) => workingConfig.map((config) =>
connection?.setConfig(config).then(() => connection?.setConfig(config).then(() =>
@ -71,7 +70,7 @@ const ConfigPage = () => {
) )
), ),
); );
} else {
await Promise.all( await Promise.all(
workingModuleConfig.map((moduleConfig) => workingModuleConfig.map((moduleConfig) =>
connection?.setModuleConfig(moduleConfig).then(() => connection?.setModuleConfig(moduleConfig).then(() =>
@ -84,14 +83,11 @@ const ConfigPage = () => {
) )
), ),
); );
setIsSaving(false);
}
await connection?.commitEditSettings().then(() => { await connection?.commitEditSettings().then(() => {
if (activeConfigSection === "device") {
removeWorkingConfig(); removeWorkingConfig();
} else {
removeWorkingModuleConfig(); removeWorkingModuleConfig();
}
if (formMethods) { if (formMethods) {
formMethods.reset({ formMethods.reset({
keepDefaultValues: true, keepDefaultValues: true,
@ -115,11 +111,8 @@ const ConfigPage = () => {
formMethods.reset(); formMethods.reset();
} }
if (activeConfigSection === "device") {
removeWorkingConfig(); removeWorkingConfig();
} else {
removeWorkingModuleConfig(); removeWorkingModuleConfig();
}
}; };
const leftSidebar = useMemo( const leftSidebar = useMemo(
@ -134,23 +127,26 @@ const ConfigPage = () => {
active={activeConfigSection === "device"} active={activeConfigSection === "device"}
onClick={() => setActiveConfigSection("device")} onClick={() => setActiveConfigSection("device")}
Icon={SettingsIcon} Icon={SettingsIcon}
isDirty={workingConfig.length > 0}
count={workingConfig.length}
/> />
<SidebarButton <SidebarButton
label={t("navigation.moduleConfig")} label={t("navigation.moduleConfig")}
active={activeConfigSection === "module"} active={activeConfigSection === "module"}
onClick={() => setActiveConfigSection("module")} onClick={() => setActiveConfigSection("module")}
Icon={BoxesIcon} Icon={BoxesIcon}
isDirty={workingModuleConfig.length > 0}
count={workingModuleConfig.length}
/> />
</SidebarSection> </SidebarSection>
</Sidebar> </Sidebar>
), ),
[activeConfigSection], [activeConfigSection, workingConfig, workingModuleConfig],
); );
const buttonOpacity = useMemo( const buttonOpacity = useMemo(
() => (formMethods?.formState.isDirty || () => (formMethods?.formState.isDirty ||
(activeConfigSection === "device" && workingConfig.length > 0) || workingConfig.length > 0 || workingModuleConfig.length > 0
(activeConfigSection === "module" && workingModuleConfig.length > 0)
? "opacity-100" ? "opacity-100"
: "opacity-0"), : "opacity-0"),
[formMethods?.formState.isDirty, workingConfig, workingModuleConfig], [formMethods?.formState.isDirty, workingConfig, workingModuleConfig],
@ -179,8 +175,7 @@ const ConfigPage = () => {
icon: isError ? SaveOff : SaveIcon, icon: isError ? SaveOff : SaveIcon,
isLoading: isSaving, isLoading: isSaving,
disabled: isSaving || disabled: isSaving ||
(activeConfigSection === "device" && workingConfig.length === 0) || (workingConfig.length === 0 && workingModuleConfig.length === 0),
(activeConfigSection === "module" && workingModuleConfig.length === 0),
iconClasses: isError ? "text-red-400 cursor-not-allowed" : "", iconClasses: isError ? "text-red-400 cursor-not-allowed" : "",
onClick: handleSave, onClick: handleSave,
label: t("common:button.save"), label: t("common:button.save"),
@ -204,7 +199,7 @@ const ConfigPage = () => {
> >
{activeConfigSection === "device" {activeConfigSection === "device"
? <DeviceConfig onFormInit={onFormInit} /> ? <DeviceConfig onFormInit={onFormInit} />
: <ModuleConfig />} : <ModuleConfig onFormInit={onFormInit} />}
</PageLayout> </PageLayout>
</> </>
); );

Loading…
Cancel
Save