Feat(config): Align settings menu to match android/ios (#906)
* feat: aligned settings menu to match android/ios * updated sidebar text size. * Update packages/web/public/i18n/locales/en/config.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/public/i18n/locales/en/config.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/src/components/PageComponents/Settings/User.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * linting/formatting fixes * fixed formatting issue --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
+32
-2
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"page": {
|
"page": {
|
||||||
"title": "Configuration",
|
"title": "Settings",
|
||||||
|
"tabUser": "User",
|
||||||
|
"tabChannels": "Channels",
|
||||||
"tabBluetooth": "Bluetooth",
|
"tabBluetooth": "Bluetooth",
|
||||||
"tabDevice": "Device",
|
"tabDevice": "Device",
|
||||||
"tabDisplay": "Display",
|
"tabDisplay": "Display",
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
"tabSecurity": "Security"
|
"tabSecurity": "Security"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"label": "Modules"
|
"label": "Configuration"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
"title": "Device Settings",
|
"title": "Device Settings",
|
||||||
@@ -424,5 +426,33 @@
|
|||||||
"description": "Settings for Logging",
|
"description": "Settings for Logging",
|
||||||
"label": "Logging Settings"
|
"label": "Logging Settings"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "User Settings",
|
||||||
|
"description": "Configure your device name and identity settings",
|
||||||
|
"longName": {
|
||||||
|
"label": "Long Name",
|
||||||
|
"description": "Your full display name (1-40 characters)",
|
||||||
|
"validation": {
|
||||||
|
"min": "Long name must be at least 1 character",
|
||||||
|
"max": "Long name must be at most 40 characters"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortName": {
|
||||||
|
"label": "Short Name",
|
||||||
|
"description": "Your abbreviated name (2-4 characters)",
|
||||||
|
"validation": {
|
||||||
|
"min": "Short name must be at least 2 characters",
|
||||||
|
"max": "Short name must be at most 4 characters"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isUnmessageable": {
|
||||||
|
"label": "Unmessageable",
|
||||||
|
"description": "Used to identify unmonitored or infrastructure nodes so that messaging is not available to nodes that will never respond."
|
||||||
|
},
|
||||||
|
"isLicensed": {
|
||||||
|
"label": "Licensed amateur radio (HAM)",
|
||||||
|
"description": "Enable if you are a licensed amateur radio operator, enabling this option disables encryption and is not compatible with the default Meshtastic network."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
"title": "Navigation",
|
"title": "Navigation",
|
||||||
"messages": "Messages",
|
"messages": "Messages",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
"config": "Config",
|
"settings": "Settings",
|
||||||
"channels": "Channels",
|
"channels": "Channels",
|
||||||
"radioConfig": "Radio Config",
|
"radioConfig": "Radio Config",
|
||||||
|
"deviceConfig": "Device Config",
|
||||||
"moduleConfig": "Module Config",
|
"moduleConfig": "Module Config",
|
||||||
"channelConfig": "Channel Config",
|
|
||||||
"nodes": "Nodes"
|
"nodes": "Nodes"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@@ -27,13 +27,7 @@
|
|||||||
"title": "Firmware",
|
"title": "Firmware",
|
||||||
"version": "v{{version}}",
|
"version": "v{{version}}",
|
||||||
"buildDate": "Build date: {{date}}"
|
"buildDate": "Build date: {{date}}"
|
||||||
},
|
}
|
||||||
"deviceName": {
|
|
||||||
"title": "Device Name",
|
|
||||||
"changeName": "Change Device Name",
|
|
||||||
"placeholder": "Enter device name"
|
|
||||||
},
|
|
||||||
"editDeviceName": "Edit device name"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batteryStatus": {
|
"batteryStatus": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Languages,
|
Languages,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Palette,
|
Palette,
|
||||||
PenLine,
|
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -53,7 +52,6 @@ export const DeviceInfoPanel = ({
|
|||||||
firmwareVersion,
|
firmwareVersion,
|
||||||
user,
|
user,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
setDialogOpen,
|
|
||||||
setCommandPaletteOpen,
|
setCommandPaletteOpen,
|
||||||
disableHover = false,
|
disableHover = false,
|
||||||
}: DeviceInfoPanelProps) => {
|
}: DeviceInfoPanelProps) => {
|
||||||
@@ -91,12 +89,6 @@ export const DeviceInfoPanel = ({
|
|||||||
icon: Palette,
|
icon: Palette,
|
||||||
render: () => <ThemeSwitcher />,
|
render: () => <ThemeSwitcher />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "changeName",
|
|
||||||
label: t("sidebar.deviceInfo.deviceName.changeName"),
|
|
||||||
icon: PenLine,
|
|
||||||
onClick: setDialogOpen,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "commandMenu",
|
id: "commandMenu",
|
||||||
label: t("page.title", { ns: "commandPalette" }),
|
label: t("page.title", { ns: "commandPalette" }),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface ImportDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||||
const { config, channels } = useDevice();
|
const { setChange, channels, config } = useDevice();
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
||||||
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
||||||
@@ -41,8 +41,6 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
const [updateConfig, setUpdateConfig] = useState<boolean>(true);
|
const [updateConfig, setUpdateConfig] = useState<boolean>(true);
|
||||||
const [importIndex, setImportIndex] = useState<number[]>([]);
|
const [importIndex, setImportIndex] = useState<number[]>([]);
|
||||||
|
|
||||||
const { setWorkingChannelConfig, setWorkingConfig } = useDevice();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// the channel information is contained in the URL's fragment, which will be present after a
|
// the channel information is contained in the URL's fragment, which will be present after a
|
||||||
// non-URL encoded `#`.
|
// non-URL encoded `#`.
|
||||||
@@ -106,7 +104,11 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setWorkingChannelConfig(payload);
|
setChange(
|
||||||
|
{ type: "channel", index: importIndex[index] ?? 0 },
|
||||||
|
payload,
|
||||||
|
channels.get(importIndex[index] ?? 0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -118,14 +120,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!deepCompareConfig(config.lora, payload, true)) {
|
if (!deepCompareConfig(config.lora, payload, true)) {
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "lora" }, payload, config.lora);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "lora",
|
|
||||||
value: payload,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset state after import
|
// Reset state after import
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function MultiSelectInput<T extends FieldValues>({
|
|||||||
isDirty,
|
isDirty,
|
||||||
invalid,
|
invalid,
|
||||||
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
|
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
|
||||||
const { t } = useTranslation("deviceConfig");
|
const { t } = useTranslation("config");
|
||||||
const { enumValue, className, ...remainingProperties } = field.properties;
|
const { enumValue, className, ...remainingProperties } = field.properties;
|
||||||
|
|
||||||
const isNewConfigStructure =
|
const isNewConfigStructure =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type LangCode, supportedLanguages } from "@app/i18n-config.ts";
|
import type { LangCode } from "@app/i18n-config.ts";
|
||||||
import useLang from "@core/hooks/useLang.ts";
|
import useLang from "@core/hooks/useLang.ts";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Check, Languages } from "lucide-react";
|
import { Check, Languages } from "lucide-react";
|
||||||
|
|||||||
+10
-11
@@ -24,12 +24,7 @@ export interface SettingsPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
||||||
const {
|
const { config, setChange, getChange, removeChange } = useDevice();
|
||||||
config,
|
|
||||||
setWorkingChannelConfig,
|
|
||||||
getWorkingChannelConfig,
|
|
||||||
removeWorkingChannelConfig,
|
|
||||||
} = useDevice();
|
|
||||||
const { t } = useTranslation(["channels", "ui", "dialog"]);
|
const { t } = useTranslation(["channels", "ui", "dialog"]);
|
||||||
|
|
||||||
const defaultConfig = channel;
|
const defaultConfig = channel;
|
||||||
@@ -51,7 +46,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const effectiveConfig = getWorkingChannelConfig(channel.index) ?? channel;
|
const workingChannel = getChange({
|
||||||
|
type: "channel",
|
||||||
|
index: channel.index,
|
||||||
|
}) as Protobuf.Channel.Channel | undefined;
|
||||||
|
const effectiveConfig = workingChannel ?? channel;
|
||||||
const formValues = {
|
const formValues = {
|
||||||
...effectiveConfig,
|
...effectiveConfig,
|
||||||
...{
|
...{
|
||||||
@@ -111,7 +110,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = create(Protobuf.Channel.ChannelSchema, {
|
||||||
...data,
|
...data,
|
||||||
settings: {
|
settings: {
|
||||||
...data.settings,
|
...data.settings,
|
||||||
@@ -121,14 +120,14 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
positionPrecision: data.settings.moduleSettings.positionPrecision,
|
positionPrecision: data.settings.moduleSettings.positionPrecision,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
if (deepCompareConfig(channel, payload, true)) {
|
if (deepCompareConfig(channel, payload, true)) {
|
||||||
removeWorkingChannelConfig(channel.index);
|
removeChange({ type: "channel", index: channel.index });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingChannelConfig(create(Protobuf.Channel.ChannelSchema, payload));
|
setChange({ type: "channel", index: channel.index }, payload, channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const preSharedKeyRegenerate = async () => {
|
const preSharedKeyRegenerate = async () => {
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
import { Channel } from "@app/components/PageComponents/ChannelConfig/Channel";
|
import { Channel } from "@app/components/PageComponents/Channels/Channel";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -30,8 +30,8 @@ export const getChannelName = (channel: Protobuf.Channel.Channel) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChannelConfig = ({ onFormInit }: ConfigProps) => {
|
export const Channels = ({ onFormInit }: ConfigProps) => {
|
||||||
const { channels, getWorkingChannelConfig, setDialogOpen } = useDevice();
|
const { channels, hasChannelChange, setDialogOpen } = useDevice();
|
||||||
const { t } = useTranslation("channels");
|
const { t } = useTranslation("channels");
|
||||||
|
|
||||||
const allChannels = Array.from(channels.values());
|
const allChannels = Array.from(channels.values());
|
||||||
@@ -40,10 +40,10 @@ export const ChannelConfig = ({ onFormInit }: ConfigProps) => {
|
|||||||
new Map(
|
new Map(
|
||||||
allChannels.map((channel) => [
|
allChannels.map((channel) => [
|
||||||
channel.index,
|
channel.index,
|
||||||
getWorkingChannelConfig(channel.index),
|
hasChannelChange(channel.index),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
[allChannels, getWorkingChannelConfig],
|
[allChannels, hasChannelChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type AmbientLightingValidation,
|
type AmbientLightingValidation,
|
||||||
AmbientLightingValidationSchema,
|
AmbientLightingValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/ambientLighting.ts";
|
} from "@app/validation/moduleConfig/ambientLighting.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface AmbientLightingModuleConfigProps {
|
interface AmbientLightingModuleConfigProps {
|
||||||
@@ -21,27 +19,20 @@ export const AmbientLighting = ({
|
|||||||
onFormInit,
|
onFormInit,
|
||||||
}: AmbientLightingModuleConfigProps) => {
|
}: AmbientLightingModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
|
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.ambientLighting, data, true)) {
|
||||||
removeWorkingModuleConfig("ambientLighting");
|
removeChange({ type: "moduleConfig", variant: "ambientLighting" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "ambientLighting" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "ambientLighting",
|
moduleConfig.ambientLighting,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type AudioValidation,
|
type AudioValidation,
|
||||||
AudioValidationSchema,
|
AudioValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/audio.ts";
|
} from "@app/validation/moduleConfig/audio.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -19,27 +18,20 @@ interface AudioModuleConfigProps {
|
|||||||
|
|
||||||
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
|
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "audio" });
|
useWaitForConfig({ moduleConfigCase: "audio" });
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.audio, data, true)) {
|
||||||
removeWorkingModuleConfig("audio");
|
removeChange({ type: "moduleConfig", variant: "audio" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "audio" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "audio",
|
moduleConfig.audio,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type CannedMessageValidation,
|
type CannedMessageValidation,
|
||||||
CannedMessageValidationSchema,
|
CannedMessageValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/cannedMessage.ts";
|
} from "@app/validation/moduleConfig/cannedMessage.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -22,27 +21,20 @@ export const CannedMessage = ({
|
|||||||
}: CannedMessageModuleConfigProps) => {
|
}: CannedMessageModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
|
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.cannedMessage, data, true)) {
|
||||||
removeWorkingModuleConfig("cannedMessage");
|
removeChange({ type: "moduleConfig", variant: "cannedMessage" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "cannedMessage" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "cannedMessage",
|
moduleConfig.cannedMessage,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type DetectionSensorValidation,
|
type DetectionSensorValidation,
|
||||||
DetectionSensorValidationSchema,
|
DetectionSensorValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/detectionSensor.ts";
|
} from "@app/validation/moduleConfig/detectionSensor.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -22,27 +21,20 @@ export const DetectionSensor = ({
|
|||||||
}: DetectionSensorModuleConfigProps) => {
|
}: DetectionSensorModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
|
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.detectionSensor, data, true)) {
|
||||||
removeWorkingModuleConfig("detectionSensor");
|
removeChange({ type: "moduleConfig", variant: "detectionSensor" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "detectionSensor" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "detectionSensor",
|
moduleConfig.detectionSensor,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type ExternalNotificationValidation,
|
type ExternalNotificationValidation,
|
||||||
ExternalNotificationValidationSchema,
|
ExternalNotificationValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/externalNotification.ts";
|
} from "@app/validation/moduleConfig/externalNotification.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface ExternalNotificationModuleConfigProps {
|
interface ExternalNotificationModuleConfigProps {
|
||||||
@@ -22,27 +20,20 @@ export const ExternalNotification = ({
|
|||||||
}: ExternalNotificationModuleConfigProps) => {
|
}: ExternalNotificationModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "externalNotification" });
|
useWaitForConfig({ moduleConfigCase: "externalNotification" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.externalNotification, data, true)) {
|
||||||
removeWorkingModuleConfig("externalNotification");
|
removeChange({ type: "moduleConfig", variant: "externalNotification" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "externalNotification" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "externalNotification",
|
moduleConfig.externalNotification,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
moduleConfig,
|
moduleConfig,
|
||||||
setWorkingModuleConfig,
|
setChange,
|
||||||
getEffectiveModuleConfig,
|
getEffectiveModuleConfig,
|
||||||
removeWorkingModuleConfig,
|
removeChange,
|
||||||
} = useDevice();
|
} = useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
@@ -39,17 +39,14 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (deepCompareConfig(moduleConfig.mqtt, payload, true)) {
|
if (deepCompareConfig(moduleConfig.mqtt, payload, true)) {
|
||||||
removeWorkingModuleConfig("mqtt");
|
removeChange({ type: "moduleConfig", variant: "mqtt" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "mqtt" },
|
||||||
payloadVariant: {
|
payload,
|
||||||
case: "mqtt",
|
moduleConfig.mqtt,
|
||||||
value: payload,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type NeighborInfoValidation,
|
type NeighborInfoValidation,
|
||||||
NeighborInfoValidationSchema,
|
NeighborInfoValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/neighborInfo.ts";
|
} from "@app/validation/moduleConfig/neighborInfo.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface NeighborInfoModuleConfigProps {
|
interface NeighborInfoModuleConfigProps {
|
||||||
@@ -20,27 +18,20 @@ interface NeighborInfoModuleConfigProps {
|
|||||||
export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
|
export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "neighborInfo" });
|
useWaitForConfig({ moduleConfigCase: "neighborInfo" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.neighborInfo, data, true)) {
|
||||||
removeWorkingModuleConfig("neighborInfo");
|
removeChange({ type: "moduleConfig", variant: "neighborInfo" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "neighborInfo" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "neighborInfo",
|
moduleConfig.neighborInfo,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type PaxcounterValidation,
|
type PaxcounterValidation,
|
||||||
PaxcounterValidationSchema,
|
PaxcounterValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/paxcounter.ts";
|
} from "@app/validation/moduleConfig/paxcounter.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface PaxcounterModuleConfigProps {
|
interface PaxcounterModuleConfigProps {
|
||||||
@@ -20,27 +18,20 @@ interface PaxcounterModuleConfigProps {
|
|||||||
export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
|
export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "paxcounter" });
|
useWaitForConfig({ moduleConfigCase: "paxcounter" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.paxcounter, data, true)) {
|
||||||
removeWorkingModuleConfig("paxcounter");
|
removeChange({ type: "moduleConfig", variant: "paxcounter" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "paxcounter" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "paxcounter",
|
moduleConfig.paxcounter,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type RangeTestValidation,
|
type RangeTestValidation,
|
||||||
RangeTestValidationSchema,
|
RangeTestValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/rangeTest.ts";
|
} from "@app/validation/moduleConfig/rangeTest.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface RangeTestModuleConfigProps {
|
interface RangeTestModuleConfigProps {
|
||||||
@@ -20,28 +18,21 @@ interface RangeTestModuleConfigProps {
|
|||||||
export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
|
export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "rangeTest" });
|
useWaitForConfig({ moduleConfigCase: "rangeTest" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.rangeTest, data, true)) {
|
||||||
removeWorkingModuleConfig("rangeTest");
|
removeChange({ type: "moduleConfig", variant: "rangeTest" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "rangeTest" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "rangeTest",
|
moduleConfig.rangeTest,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type SerialValidation,
|
type SerialValidation,
|
||||||
SerialValidationSchema,
|
SerialValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/serial.ts";
|
} from "@app/validation/moduleConfig/serial.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -20,27 +19,20 @@ interface SerialModuleConfigProps {
|
|||||||
export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
|
export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "serial" });
|
useWaitForConfig({ moduleConfigCase: "serial" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.serial, data, true)) {
|
||||||
removeWorkingModuleConfig("serial");
|
removeChange({ type: "moduleConfig", variant: "serial" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "serial" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "serial",
|
moduleConfig.serial,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type StoreForwardValidation,
|
type StoreForwardValidation,
|
||||||
StoreForwardValidationSchema,
|
StoreForwardValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/storeForward.ts";
|
} from "@app/validation/moduleConfig/storeForward.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface StoreForwardModuleConfigProps {
|
interface StoreForwardModuleConfigProps {
|
||||||
@@ -20,27 +18,20 @@ interface StoreForwardModuleConfigProps {
|
|||||||
export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
|
export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "storeForward" });
|
useWaitForConfig({ moduleConfigCase: "storeForward" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.storeForward, data, true)) {
|
||||||
removeWorkingModuleConfig("storeForward");
|
removeChange({ type: "moduleConfig", variant: "storeForward" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "storeForward" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "storeForward",
|
moduleConfig.storeForward,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import {
|
|||||||
type TelemetryValidation,
|
type TelemetryValidation,
|
||||||
TelemetryValidationSchema,
|
TelemetryValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/telemetry.ts";
|
} from "@app/validation/moduleConfig/telemetry.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface TelemetryModuleConfigProps {
|
interface TelemetryModuleConfigProps {
|
||||||
@@ -20,27 +18,20 @@ interface TelemetryModuleConfigProps {
|
|||||||
export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "telemetry" });
|
useWaitForConfig({ moduleConfigCase: "telemetry" });
|
||||||
|
|
||||||
const {
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
moduleConfig,
|
useDevice();
|
||||||
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)) {
|
if (deepCompareConfig(moduleConfig.telemetry, data, true)) {
|
||||||
removeWorkingModuleConfig("telemetry");
|
removeChange({ type: "moduleConfig", variant: "telemetry" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingModuleConfig(
|
setChange(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
{ type: "moduleConfig", variant: "telemetry" },
|
||||||
payloadVariant: {
|
data,
|
||||||
case: "telemetry",
|
moduleConfig.telemetry,
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-13
@@ -3,7 +3,6 @@ import {
|
|||||||
type BluetoothValidation,
|
type BluetoothValidation,
|
||||||
BluetoothValidationSchema,
|
BluetoothValidationSchema,
|
||||||
} from "@app/validation/config/bluetooth.ts";
|
} from "@app/validation/config/bluetooth.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -19,24 +18,16 @@ interface BluetoothConfigProps {
|
|||||||
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
|
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "bluetooth" });
|
useWaitForConfig({ configCase: "bluetooth" });
|
||||||
|
|
||||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
|
|
||||||
const onSubmit = (data: BluetoothValidation) => {
|
const onSubmit = (data: BluetoothValidation) => {
|
||||||
if (deepCompareConfig(config.bluetooth, data, true)) {
|
if (deepCompareConfig(config.bluetooth, data, true)) {
|
||||||
removeWorkingConfig("bluetooth");
|
removeChange({ type: "config", variant: "bluetooth" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "bluetooth" }, data, config.bluetooth);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "bluetooth",
|
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+4
-13
@@ -3,7 +3,6 @@ import {
|
|||||||
type DeviceValidation,
|
type DeviceValidation,
|
||||||
DeviceValidationSchema,
|
DeviceValidationSchema,
|
||||||
} from "@app/validation/config/device.ts";
|
} from "@app/validation/config/device.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
@@ -20,25 +19,17 @@ interface DeviceConfigProps {
|
|||||||
export const Device = ({ onFormInit }: DeviceConfigProps) => {
|
export const Device = ({ onFormInit }: DeviceConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "device" });
|
useWaitForConfig({ configCase: "device" });
|
||||||
|
|
||||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
const { validateRoleSelection } = useUnsafeRolesDialog();
|
const { validateRoleSelection } = useUnsafeRolesDialog();
|
||||||
|
|
||||||
const onSubmit = (data: DeviceValidation) => {
|
const onSubmit = (data: DeviceValidation) => {
|
||||||
if (deepCompareConfig(config.device, data, true)) {
|
if (deepCompareConfig(config.device, data, true)) {
|
||||||
removeWorkingConfig("device");
|
removeChange({ type: "config", variant: "device" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "device" }, data, config.device);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "device",
|
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+4
-13
@@ -3,7 +3,6 @@ import {
|
|||||||
type DisplayValidation,
|
type DisplayValidation,
|
||||||
DisplayValidationSchema,
|
DisplayValidationSchema,
|
||||||
} from "@app/validation/config/display.ts";
|
} from "@app/validation/config/display.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -18,24 +17,16 @@ interface DisplayConfigProps {
|
|||||||
}
|
}
|
||||||
export const Display = ({ onFormInit }: DisplayConfigProps) => {
|
export const Display = ({ onFormInit }: DisplayConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "display" });
|
useWaitForConfig({ configCase: "display" });
|
||||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
|
|
||||||
const onSubmit = (data: DisplayValidation) => {
|
const onSubmit = (data: DisplayValidation) => {
|
||||||
if (deepCompareConfig(config.display, data, true)) {
|
if (deepCompareConfig(config.display, data, true)) {
|
||||||
removeWorkingConfig("display");
|
removeChange({ type: "config", variant: "display" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "display" }, data, config.display);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "display",
|
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+4
-13
@@ -3,7 +3,6 @@ import {
|
|||||||
type LoRaValidation,
|
type LoRaValidation,
|
||||||
LoRaValidationSchema,
|
LoRaValidationSchema,
|
||||||
} from "@app/validation/config/lora.ts";
|
} from "@app/validation/config/lora.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -19,24 +18,16 @@ interface LoRaConfigProps {
|
|||||||
export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
|
export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "lora" });
|
useWaitForConfig({ configCase: "lora" });
|
||||||
|
|
||||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
|
|
||||||
const onSubmit = (data: LoRaValidation) => {
|
const onSubmit = (data: LoRaValidation) => {
|
||||||
if (deepCompareConfig(config.lora, data, true)) {
|
if (deepCompareConfig(config.lora, data, true)) {
|
||||||
removeWorkingConfig("lora");
|
removeChange({ type: "config", variant: "lora" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "lora" }, data, config.lora);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "lora",
|
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+4
-12
@@ -23,9 +23,8 @@ interface NetworkConfigProps {
|
|||||||
export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "network" });
|
useWaitForConfig({ configCase: "network" });
|
||||||
|
|
||||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
|
|
||||||
const networkConfig = getEffectiveConfig("network");
|
const networkConfig = getEffectiveConfig("network");
|
||||||
|
|
||||||
@@ -44,18 +43,11 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (deepCompareConfig(config.network, payload, true)) {
|
if (deepCompareConfig(config.network, payload, true)) {
|
||||||
removeWorkingConfig("network");
|
removeChange({ type: "config", variant: "network" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "network" }, payload, config.network);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "network",
|
|
||||||
value: payload,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<DynamicForm<NetworkValidation>
|
<DynamicForm<NetworkValidation>
|
||||||
+9
-13
@@ -3,7 +3,6 @@ import {
|
|||||||
type PositionValidation,
|
type PositionValidation,
|
||||||
PositionValidationSchema,
|
PositionValidationSchema,
|
||||||
} from "@app/validation/config/position.ts";
|
} from "@app/validation/config/position.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
@@ -24,26 +23,23 @@ interface PositionConfigProps {
|
|||||||
export const Position = ({ onFormInit }: PositionConfigProps) => {
|
export const Position = ({ onFormInit }: PositionConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "position" });
|
useWaitForConfig({ configCase: "position" });
|
||||||
|
|
||||||
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
|
const { setChange, config, getEffectiveConfig, removeChange } = useDevice();
|
||||||
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("config");
|
||||||
|
|
||||||
const onSubmit = (data: PositionValidation) => {
|
const onSubmit = (data: PositionValidation) => {
|
||||||
if (deepCompareConfig(config.position, data, true)) {
|
const payload = { ...data, positionFlags: flagsValue };
|
||||||
removeWorkingConfig("position");
|
if (deepCompareConfig(config.position, payload, true)) {
|
||||||
|
removeChange({ type: "config", variant: "position" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return setWorkingConfig(
|
return setChange(
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
{ type: "config", variant: "position" },
|
||||||
payloadVariant: {
|
payload,
|
||||||
case: "position",
|
config.position,
|
||||||
value: { ...data, positionFlags: flagsValue },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
+4
-14
@@ -3,14 +3,12 @@ import {
|
|||||||
type PowerValidation,
|
type PowerValidation,
|
||||||
PowerValidationSchema,
|
PowerValidationSchema,
|
||||||
} from "@app/validation/config/power.ts";
|
} from "@app/validation/config/power.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import {
|
import {
|
||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface PowerConfigProps {
|
interface PowerConfigProps {
|
||||||
@@ -19,24 +17,16 @@ interface PowerConfigProps {
|
|||||||
export const Power = ({ onFormInit }: PowerConfigProps) => {
|
export const Power = ({ onFormInit }: PowerConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "power" });
|
useWaitForConfig({ configCase: "power" });
|
||||||
|
|
||||||
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
|
const { setChange, config, getEffectiveConfig, removeChange } = useDevice();
|
||||||
useDevice();
|
const { t } = useTranslation("config");
|
||||||
const { t } = useTranslation("deviceConfig");
|
|
||||||
|
|
||||||
const onSubmit = (data: PowerValidation) => {
|
const onSubmit = (data: PowerValidation) => {
|
||||||
if (deepCompareConfig(config.power, data, true)) {
|
if (deepCompareConfig(config.power, data, true)) {
|
||||||
removeWorkingConfig("power");
|
removeChange({ type: "config", variant: "power" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange({ type: "config", variant: "power" }, data, config.power);
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "power",
|
|
||||||
value: data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+8
-18
@@ -4,7 +4,6 @@ import {
|
|||||||
type RawSecurity,
|
type RawSecurity,
|
||||||
RawSecuritySchema,
|
RawSecuritySchema,
|
||||||
} from "@app/validation/config/security.ts";
|
} from "@app/validation/config/security.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
||||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||||
@@ -15,7 +14,6 @@ import {
|
|||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { type DefaultValues, useForm } from "react-hook-form";
|
import { type DefaultValues, useForm } from "react-hook-form";
|
||||||
@@ -27,15 +25,10 @@ interface SecurityConfigProps {
|
|||||||
export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
||||||
useWaitForConfig({ configCase: "security" });
|
useWaitForConfig({ configCase: "security" });
|
||||||
|
|
||||||
const {
|
const { config, setChange, setDialogOpen, getEffectiveConfig, removeChange } =
|
||||||
config,
|
useDevice();
|
||||||
setWorkingConfig,
|
|
||||||
setDialogOpen,
|
|
||||||
getEffectiveConfig,
|
|
||||||
removeWorkingConfig,
|
|
||||||
} = useDevice();
|
|
||||||
|
|
||||||
const { t } = useTranslation("deviceConfig");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
const defaultConfig = config.security;
|
const defaultConfig = config.security;
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@@ -103,17 +96,14 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (deepCompareConfig(config.security, payload, true)) {
|
if (deepCompareConfig(config.security, payload, true)) {
|
||||||
removeWorkingConfig("security");
|
removeChange({ type: "config", variant: "security" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setChange(
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
{ type: "config", variant: "security" },
|
||||||
payloadVariant: {
|
payload,
|
||||||
case: "security",
|
config.security,
|
||||||
value: payload,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
type UserValidation,
|
||||||
|
UserValidationSchema,
|
||||||
|
} from "@app/validation/config/user.ts";
|
||||||
|
import { create } from "@bufbuild/protobuf";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
|
import { useDevice, useNodeDB } from "@core/stores";
|
||||||
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface UserConfigProps {
|
||||||
|
onFormInit: DynamicFormFormInit<UserValidation>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const User = ({ onFormInit }: UserConfigProps) => {
|
||||||
|
const { hardware, getChange, connection } = useDevice();
|
||||||
|
const { getNode } = useNodeDB();
|
||||||
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
|
const myNode = getNode(hardware.myNodeNum);
|
||||||
|
const defaultUser = myNode?.user ?? {
|
||||||
|
id: "",
|
||||||
|
longName: "",
|
||||||
|
shortName: "",
|
||||||
|
isLicensed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get working copy from change registry
|
||||||
|
const workingUser = getChange({ type: "user" }) as
|
||||||
|
| Protobuf.Mesh.User
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const effectiveUser = workingUser ?? defaultUser;
|
||||||
|
|
||||||
|
const onSubmit = (data: UserValidation) => {
|
||||||
|
connection?.setOwner(
|
||||||
|
create(Protobuf.Mesh.UserSchema, {
|
||||||
|
...data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicForm<UserValidation>
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onFormInit={onFormInit}
|
||||||
|
validationSchema={UserValidationSchema}
|
||||||
|
defaultValues={{
|
||||||
|
longName: defaultUser.longName,
|
||||||
|
shortName: defaultUser.shortName,
|
||||||
|
isLicensed: defaultUser.isLicensed,
|
||||||
|
isUnmessageable: false,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
longName: effectiveUser.longName,
|
||||||
|
shortName: effectiveUser.shortName,
|
||||||
|
isLicensed: effectiveUser.isLicensed,
|
||||||
|
isUnmessageable: false,
|
||||||
|
}}
|
||||||
|
fieldGroups={[
|
||||||
|
{
|
||||||
|
label: t("user.title"),
|
||||||
|
description: t("user.description"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "longName",
|
||||||
|
label: t("user.longName.label"),
|
||||||
|
description: t("user.longName.description"),
|
||||||
|
properties: {
|
||||||
|
fieldLength: {
|
||||||
|
min: 1,
|
||||||
|
max: 40,
|
||||||
|
showCharacterCount: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "shortName",
|
||||||
|
label: t("user.shortName.label"),
|
||||||
|
description: t("user.shortName.description"),
|
||||||
|
properties: {
|
||||||
|
fieldLength: {
|
||||||
|
min: 2,
|
||||||
|
max: 4,
|
||||||
|
showCharacterCount: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "isUnmessageable",
|
||||||
|
label: t("user.isUnmessageable.label"),
|
||||||
|
description: t("user.isUnmessageable.description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "isLicensed",
|
||||||
|
label: t("user.isLicensed.label"),
|
||||||
|
description: t("user.isLicensed.description"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -108,9 +108,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
|
|||||||
},
|
},
|
||||||
{ name: t("navigation.map"), icon: MapIcon, page: "map" },
|
{ name: t("navigation.map"), icon: MapIcon, page: "map" },
|
||||||
{
|
{
|
||||||
name: t("navigation.config"),
|
name: t("navigation.settings"),
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
page: "config",
|
page: "settings",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
|
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const SidebarSection = ({
|
|||||||
as="h3"
|
as="h3"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-2",
|
"mb-2",
|
||||||
"uppercase tracking-wider text-md",
|
"capitalize tracking-wider text-sm",
|
||||||
"transition-all duration-300 ease-in-out",
|
"transition-all duration-300 ease-in-out",
|
||||||
"whitespace-nowrap overflow-hidden",
|
"whitespace-nowrap overflow-hidden",
|
||||||
isCollapsed
|
isCollapsed
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import type { Types } from "@meshtastic/core";
|
||||||
|
|
||||||
|
// Config type discriminators
|
||||||
|
export type ValidConfigType =
|
||||||
|
| "device"
|
||||||
|
| "position"
|
||||||
|
| "power"
|
||||||
|
| "network"
|
||||||
|
| "display"
|
||||||
|
| "lora"
|
||||||
|
| "bluetooth"
|
||||||
|
| "security";
|
||||||
|
|
||||||
|
export type ValidModuleConfigType =
|
||||||
|
| "mqtt"
|
||||||
|
| "serial"
|
||||||
|
| "externalNotification"
|
||||||
|
| "storeForward"
|
||||||
|
| "rangeTest"
|
||||||
|
| "telemetry"
|
||||||
|
| "cannedMessage"
|
||||||
|
| "audio"
|
||||||
|
| "neighborInfo"
|
||||||
|
| "ambientLighting"
|
||||||
|
| "detectionSensor"
|
||||||
|
| "paxcounter";
|
||||||
|
|
||||||
|
// Unified config change key type
|
||||||
|
export type ConfigChangeKey =
|
||||||
|
| { type: "config"; variant: ValidConfigType }
|
||||||
|
| { type: "moduleConfig"; variant: ValidModuleConfigType }
|
||||||
|
| { type: "channel"; index: Types.ChannelNumber }
|
||||||
|
| { type: "user" };
|
||||||
|
|
||||||
|
// Serialized key for Map storage
|
||||||
|
export type ConfigChangeKeyString = string;
|
||||||
|
|
||||||
|
// Registry entry
|
||||||
|
export interface ChangeEntry {
|
||||||
|
key: ConfigChangeKey;
|
||||||
|
value: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
originalValue?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The unified registry
|
||||||
|
export interface ChangeRegistry {
|
||||||
|
changes: Map<ConfigChangeKeyString, ChangeEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert structured key to string for Map lookup
|
||||||
|
*/
|
||||||
|
export function serializeKey(key: ConfigChangeKey): ConfigChangeKeyString {
|
||||||
|
switch (key.type) {
|
||||||
|
case "config":
|
||||||
|
return `config:${key.variant}`;
|
||||||
|
case "moduleConfig":
|
||||||
|
return `moduleConfig:${key.variant}`;
|
||||||
|
case "channel":
|
||||||
|
return `channel:${key.index}`;
|
||||||
|
case "user":
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse operation for type-safe retrieval
|
||||||
|
*/
|
||||||
|
export function deserializeKey(keyStr: ConfigChangeKeyString): ConfigChangeKey {
|
||||||
|
const [type, variant] = keyStr.split(":");
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "config":
|
||||||
|
return { type: "config", variant: variant as ValidConfigType };
|
||||||
|
case "moduleConfig":
|
||||||
|
return {
|
||||||
|
type: "moduleConfig",
|
||||||
|
variant: variant as ValidModuleConfigType,
|
||||||
|
};
|
||||||
|
case "channel":
|
||||||
|
return {
|
||||||
|
type: "channel",
|
||||||
|
index: Number(variant) as Types.ChannelNumber,
|
||||||
|
};
|
||||||
|
case "user":
|
||||||
|
return { type: "user" };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown key type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty change registry
|
||||||
|
*/
|
||||||
|
export function createChangeRegistry(): ChangeRegistry {
|
||||||
|
return {
|
||||||
|
changes: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a config variant has changes
|
||||||
|
*/
|
||||||
|
export function hasConfigChange(
|
||||||
|
registry: ChangeRegistry,
|
||||||
|
variant: ValidConfigType,
|
||||||
|
): boolean {
|
||||||
|
return registry.changes.has(serializeKey({ type: "config", variant }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a module config variant has changes
|
||||||
|
*/
|
||||||
|
export function hasModuleConfigChange(
|
||||||
|
registry: ChangeRegistry,
|
||||||
|
variant: ValidModuleConfigType,
|
||||||
|
): boolean {
|
||||||
|
return registry.changes.has(serializeKey({ type: "moduleConfig", variant }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a channel has changes
|
||||||
|
*/
|
||||||
|
export function hasChannelChange(
|
||||||
|
registry: ChangeRegistry,
|
||||||
|
index: Types.ChannelNumber,
|
||||||
|
): boolean {
|
||||||
|
return registry.changes.has(serializeKey({ type: "channel", index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user config has changes
|
||||||
|
*/
|
||||||
|
export function hasUserChange(registry: ChangeRegistry): boolean {
|
||||||
|
return registry.changes.has(serializeKey({ type: "user" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of config changes
|
||||||
|
*/
|
||||||
|
export function getConfigChangeCount(registry: ChangeRegistry): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const keyStr of registry.changes.keys()) {
|
||||||
|
const key = deserializeKey(keyStr);
|
||||||
|
if (key.type === "config") {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of module config changes
|
||||||
|
*/
|
||||||
|
export function getModuleConfigChangeCount(registry: ChangeRegistry): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const keyStr of registry.changes.keys()) {
|
||||||
|
const key = deserializeKey(keyStr);
|
||||||
|
if (key.type === "moduleConfig") {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of channel changes
|
||||||
|
*/
|
||||||
|
export function getChannelChangeCount(registry: ChangeRegistry): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const keyStr of registry.changes.keys()) {
|
||||||
|
const key = deserializeKey(keyStr);
|
||||||
|
if (key.type === "channel") {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all config changes as an array
|
||||||
|
*/
|
||||||
|
export function getAllConfigChanges(registry: ChangeRegistry): ChangeEntry[] {
|
||||||
|
const changes: ChangeEntry[] = [];
|
||||||
|
for (const entry of registry.changes.values()) {
|
||||||
|
if (entry.key.type === "config") {
|
||||||
|
changes.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all module config changes as an array
|
||||||
|
*/
|
||||||
|
export function getAllModuleConfigChanges(
|
||||||
|
registry: ChangeRegistry,
|
||||||
|
): ChangeEntry[] {
|
||||||
|
const changes: ChangeEntry[] = [];
|
||||||
|
for (const entry of registry.changes.values()) {
|
||||||
|
if (entry.key.type === "moduleConfig") {
|
||||||
|
changes.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all channel changes as an array
|
||||||
|
*/
|
||||||
|
export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] {
|
||||||
|
const changes: ChangeEntry[] = [];
|
||||||
|
for (const entry of registry.changes.values()) {
|
||||||
|
if (entry.key.type === "channel") {
|
||||||
|
changes.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
@@ -19,9 +19,7 @@ export const mockDeviceStore: Device = {
|
|||||||
channels: new Map(),
|
channels: new Map(),
|
||||||
config: {} as Protobuf.LocalOnly.LocalConfig,
|
config: {} as Protobuf.LocalOnly.LocalConfig,
|
||||||
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig,
|
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig,
|
||||||
workingConfig: [],
|
changeRegistry: { changes: new Map() },
|
||||||
workingModuleConfig: [],
|
|
||||||
workingChannelConfig: [],
|
|
||||||
hardware: {} as Protobuf.Mesh.MyNodeInfo,
|
hardware: {} as Protobuf.Mesh.MyNodeInfo,
|
||||||
metadata: new Map(),
|
metadata: new Map(),
|
||||||
traceroutes: new Map(),
|
traceroutes: new Map(),
|
||||||
@@ -56,17 +54,8 @@ export const mockDeviceStore: Device = {
|
|||||||
setStatus: vi.fn(),
|
setStatus: vi.fn(),
|
||||||
setConfig: vi.fn(),
|
setConfig: vi.fn(),
|
||||||
setModuleConfig: vi.fn(),
|
setModuleConfig: vi.fn(),
|
||||||
setWorkingConfig: vi.fn(),
|
|
||||||
setWorkingModuleConfig: vi.fn(),
|
|
||||||
getWorkingConfig: vi.fn(),
|
|
||||||
getWorkingModuleConfig: vi.fn(),
|
|
||||||
removeWorkingConfig: vi.fn(),
|
|
||||||
removeWorkingModuleConfig: vi.fn(),
|
|
||||||
getEffectiveConfig: vi.fn(),
|
getEffectiveConfig: vi.fn(),
|
||||||
getEffectiveModuleConfig: vi.fn(),
|
getEffectiveModuleConfig: vi.fn(),
|
||||||
setWorkingChannelConfig: vi.fn(),
|
|
||||||
getWorkingChannelConfig: vi.fn(),
|
|
||||||
removeWorkingChannelConfig: vi.fn(),
|
|
||||||
setHardware: vi.fn(),
|
setHardware: vi.fn(),
|
||||||
setActiveNode: vi.fn(),
|
setActiveNode: vi.fn(),
|
||||||
setPendingSettingsChanges: vi.fn(),
|
setPendingSettingsChanges: vi.fn(),
|
||||||
@@ -90,4 +79,21 @@ export const mockDeviceStore: Device = {
|
|||||||
getUnreadCount: vi.fn().mockReturnValue(0),
|
getUnreadCount: vi.fn().mockReturnValue(0),
|
||||||
getNeighborInfo: vi.fn(),
|
getNeighborInfo: vi.fn(),
|
||||||
addNeighborInfo: vi.fn(),
|
addNeighborInfo: vi.fn(),
|
||||||
|
|
||||||
|
// New unified change tracking methods
|
||||||
|
setChange: vi.fn(),
|
||||||
|
removeChange: vi.fn(),
|
||||||
|
hasChange: vi.fn().mockReturnValue(false),
|
||||||
|
getChange: vi.fn(),
|
||||||
|
clearAllChanges: vi.fn(),
|
||||||
|
hasConfigChange: vi.fn().mockReturnValue(false),
|
||||||
|
hasModuleConfigChange: vi.fn().mockReturnValue(false),
|
||||||
|
hasChannelChange: vi.fn().mockReturnValue(false),
|
||||||
|
hasUserChange: vi.fn().mockReturnValue(false),
|
||||||
|
getConfigChangeCount: vi.fn().mockReturnValue(0),
|
||||||
|
getModuleConfigChangeCount: vi.fn().mockReturnValue(0),
|
||||||
|
getChannelChangeCount: vi.fn().mockReturnValue(0),
|
||||||
|
getAllConfigChanges: vi.fn().mockReturnValue([]),
|
||||||
|
getAllModuleConfigChanges: vi.fn().mockReturnValue([]),
|
||||||
|
getAllChannelChanges: vi.fn().mockReturnValue([]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,25 +52,7 @@ function makeChannel(index: number) {
|
|||||||
function makeWaypoint(id: number, expire?: number) {
|
function makeWaypoint(id: number, expire?: number) {
|
||||||
return create(Protobuf.Mesh.WaypointSchema, { id, expire });
|
return create(Protobuf.Mesh.WaypointSchema, { id, expire });
|
||||||
}
|
}
|
||||||
function makeConfig(fields: Record<string, any>) {
|
|
||||||
return create(Protobuf.Config.ConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "device",
|
|
||||||
value: create(Protobuf.Config.Config_DeviceConfigSchema, fields),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function makeModuleConfig(fields: Record<string, any>) {
|
|
||||||
return create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
|
||||||
payloadVariant: {
|
|
||||||
case: "mqtt",
|
|
||||||
value: create(
|
|
||||||
Protobuf.ModuleConfig.ModuleConfig_MQTTConfigSchema,
|
|
||||||
fields,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function makeAdminMessage(fields: Record<string, any>) {
|
function makeAdminMessage(fields: Record<string, any>) {
|
||||||
return create(Protobuf.Admin.AdminMessageSchema, fields);
|
return create(Protobuf.Admin.AdminMessageSchema, fields);
|
||||||
}
|
}
|
||||||
@@ -114,13 +96,13 @@ describe("DeviceStore – basic map ops & retention", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DeviceStore – working/effective config API", () => {
|
describe("DeviceStore – change registry API", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
idbMem.clear();
|
idbMem.clear();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setWorkingConfig/getWorkingConfig replaces by variant and getEffectiveConfig merges base + working", async () => {
|
it("setChange/hasChange/getChange for config and getEffectiveConfig merges base + working", async () => {
|
||||||
const { useDeviceStore } = await freshStore(false);
|
const { useDeviceStore } = await freshStore(false);
|
||||||
const state = useDeviceStore.getState();
|
const state = useDeviceStore.getState();
|
||||||
const device = state.addDevice(42);
|
const device = state.addDevice(42);
|
||||||
@@ -138,14 +120,17 @@ describe("DeviceStore – working/effective config API", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// working deviceConfig.role = ROUTER
|
// working deviceConfig.role = ROUTER
|
||||||
device.setWorkingConfig(
|
const routerConfig = create(Protobuf.Config.Config_DeviceConfigSchema, {
|
||||||
makeConfig({
|
role: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||||
role: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
});
|
||||||
}),
|
device.setChange({ type: "config", variant: "device" }, routerConfig);
|
||||||
);
|
|
||||||
|
|
||||||
// expect working deviceConfig.role = ROUTER
|
// expect change is tracked
|
||||||
const working = device.getWorkingConfig("device");
|
expect(device.hasConfigChange("device")).toBe(true);
|
||||||
|
const working = device.getChange({
|
||||||
|
type: "config",
|
||||||
|
variant: "device",
|
||||||
|
}) as Protobuf.Config.Config_DeviceConfig;
|
||||||
expect(working?.role).toBe(Protobuf.Config.Config_DeviceConfig_Role.ROUTER);
|
expect(working?.role).toBe(Protobuf.Config.Config_DeviceConfig_Role.ROUTER);
|
||||||
|
|
||||||
// expect effective deviceConfig.role = ROUTER
|
// expect effective deviceConfig.role = ROUTER
|
||||||
@@ -154,30 +139,27 @@ describe("DeviceStore – working/effective config API", () => {
|
|||||||
Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove working, effective should equal base
|
// remove change, effective should equal base
|
||||||
device.removeWorkingConfig("device");
|
device.removeChange({ type: "config", variant: "device" });
|
||||||
expect(device.getWorkingConfig("device")).toBeUndefined();
|
expect(device.hasConfigChange("device")).toBe(false);
|
||||||
expect(device.getEffectiveConfig("device")?.role).toBe(
|
expect(device.getEffectiveConfig("device")?.role).toBe(
|
||||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// add multiple, then clear all
|
// add multiple, then clear all
|
||||||
device.setWorkingConfig(makeConfig({}));
|
device.setChange({ type: "config", variant: "device" }, routerConfig);
|
||||||
device.setWorkingConfig(
|
device.setChange({ type: "config", variant: "position" }, {});
|
||||||
makeConfig({
|
device.clearAllChanges();
|
||||||
deviceRole: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
expect(device.hasConfigChange("device")).toBe(false);
|
||||||
}),
|
expect(device.hasConfigChange("position")).toBe(false);
|
||||||
);
|
|
||||||
device.removeWorkingConfig(); // clears all
|
|
||||||
expect(device.getWorkingConfig("device")).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setWorkingModuleConfig/getWorkingModuleConfig and getEffectiveModuleConfig", async () => {
|
it("setChange/hasChange for moduleConfig and getEffectiveModuleConfig", async () => {
|
||||||
const { useDeviceStore } = await freshStore(false);
|
const { useDeviceStore } = await freshStore(false);
|
||||||
const state = useDeviceStore.getState();
|
const state = useDeviceStore.getState();
|
||||||
const device = state.addDevice(7);
|
const device = state.addDevice(7);
|
||||||
|
|
||||||
// base moduleConfig.mqtt empty; add working mqtt host
|
// base moduleConfig.mqtt with base address
|
||||||
device.setModuleConfig(
|
device.setModuleConfig(
|
||||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
@@ -188,58 +170,82 @@ describe("DeviceStore – working/effective config API", () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
device.setWorkingModuleConfig(
|
|
||||||
makeModuleConfig({ address: "mqtt://working" }),
|
// working mqtt config
|
||||||
|
const workingMqtt = create(
|
||||||
|
Protobuf.ModuleConfig.ModuleConfig_MQTTConfigSchema,
|
||||||
|
{ address: "mqtt://working" },
|
||||||
|
);
|
||||||
|
device.setChange({ type: "moduleConfig", variant: "mqtt" }, workingMqtt);
|
||||||
|
|
||||||
|
expect(device.hasModuleConfigChange("mqtt")).toBe(true);
|
||||||
|
const mqtt = device.getChange({
|
||||||
|
type: "moduleConfig",
|
||||||
|
variant: "mqtt",
|
||||||
|
}) as Protobuf.ModuleConfig.ModuleConfig_MQTTConfig;
|
||||||
|
expect(mqtt?.address).toBe("mqtt://working");
|
||||||
|
|
||||||
|
// effective should return working value
|
||||||
|
expect(device.getEffectiveModuleConfig("mqtt")?.address).toBe(
|
||||||
|
"mqtt://working",
|
||||||
);
|
);
|
||||||
|
|
||||||
const mqtt = device.getWorkingModuleConfig("mqtt");
|
// remove change
|
||||||
expect(mqtt?.address).toBe("mqtt://working");
|
device.removeChange({ type: "moduleConfig", variant: "mqtt" });
|
||||||
expect(mqtt?.address).toBe("mqtt://working");
|
expect(device.hasModuleConfigChange("mqtt")).toBe(false);
|
||||||
|
|
||||||
device.removeWorkingModuleConfig("mqtt");
|
|
||||||
expect(device.getWorkingModuleConfig("mqtt")).toBeUndefined();
|
|
||||||
expect(device.getEffectiveModuleConfig("mqtt")?.address).toBe(
|
expect(device.getEffectiveModuleConfig("mqtt")?.address).toBe(
|
||||||
"mqtt://base",
|
"mqtt://base",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear all
|
// Clear all
|
||||||
device.setWorkingModuleConfig(makeModuleConfig({ address: "x" }));
|
device.setChange({ type: "moduleConfig", variant: "mqtt" }, workingMqtt);
|
||||||
device.setWorkingModuleConfig(makeModuleConfig({ address: "y" }));
|
device.clearAllChanges();
|
||||||
device.removeWorkingModuleConfig();
|
expect(device.hasModuleConfigChange("mqtt")).toBe(false);
|
||||||
expect(device.getWorkingModuleConfig("mqtt")).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("channel working config add/update/remove/get", async () => {
|
it("channel change tracking add/update/remove/get", async () => {
|
||||||
const { useDeviceStore } = await freshStore(false);
|
const { useDeviceStore } = await freshStore(false);
|
||||||
const state = useDeviceStore.getState();
|
const state = useDeviceStore.getState();
|
||||||
const device = state.addDevice(9);
|
const device = state.addDevice(9);
|
||||||
|
|
||||||
device.setWorkingChannelConfig(makeChannel(0));
|
const channel0 = makeChannel(0);
|
||||||
device.setWorkingChannelConfig(
|
const channel1 = create(Protobuf.Channel.ChannelSchema, {
|
||||||
create(Protobuf.Channel.ChannelSchema, {
|
index: 1,
|
||||||
index: 1,
|
settings: { name: "one" },
|
||||||
settings: { name: "one" },
|
});
|
||||||
}),
|
|
||||||
);
|
device.setChange({ type: "channel", index: 0 }, channel0);
|
||||||
expect(device.getWorkingChannelConfig(0)?.index).toBe(0);
|
device.setChange({ type: "channel", index: 1 }, channel1);
|
||||||
expect(device.getWorkingChannelConfig(1)?.settings?.name).toBe("one");
|
|
||||||
|
expect(device.hasChannelChange(0)).toBe(true);
|
||||||
|
expect(device.hasChannelChange(1)).toBe(true);
|
||||||
|
const ch0 = device.getChange({ type: "channel", index: 0 }) as
|
||||||
|
| Protobuf.Channel.Channel
|
||||||
|
| undefined;
|
||||||
|
expect(ch0?.index).toBe(0);
|
||||||
|
const ch1 = device.getChange({ type: "channel", index: 1 }) as
|
||||||
|
| Protobuf.Channel.Channel
|
||||||
|
| undefined;
|
||||||
|
expect(ch1?.settings?.name).toBe("one");
|
||||||
|
|
||||||
// update channel 1
|
// update channel 1
|
||||||
device.setWorkingChannelConfig(
|
const channel1Updated = create(Protobuf.Channel.ChannelSchema, {
|
||||||
create(Protobuf.Channel.ChannelSchema, {
|
index: 1,
|
||||||
index: 1,
|
settings: { name: "uno" },
|
||||||
settings: { name: "uno" },
|
});
|
||||||
}),
|
device.setChange({ type: "channel", index: 1 }, channel1Updated);
|
||||||
);
|
const ch1Updated = device.getChange({ type: "channel", index: 1 }) as
|
||||||
expect(device.getWorkingChannelConfig(1)?.settings?.name).toBe("uno");
|
| Protobuf.Channel.Channel
|
||||||
|
| undefined;
|
||||||
|
expect(ch1Updated?.settings?.name).toBe("uno");
|
||||||
|
|
||||||
// remove specific
|
// remove specific
|
||||||
device.removeWorkingChannelConfig(1);
|
device.removeChange({ type: "channel", index: 1 });
|
||||||
expect(device.getWorkingChannelConfig(1)).toBeUndefined();
|
expect(device.hasChannelChange(1)).toBe(false);
|
||||||
|
|
||||||
// remove all
|
// remove all
|
||||||
device.removeWorkingChannelConfig();
|
device.clearAllChanges();
|
||||||
expect(device.getWorkingChannelConfig(0)).toBeUndefined();
|
expect(device.hasChannelChange(0)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,7 +355,8 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
|||||||
|
|
||||||
// Old device with myNodeNum=777 and some waypoints (one expired)
|
// Old device with myNodeNum=777 and some waypoints (one expired)
|
||||||
const oldDevice = state.addDevice(1);
|
const oldDevice = state.addDevice(1);
|
||||||
oldDevice.connection = { sendWaypoint: vi.fn() } as any;
|
const mockSendWaypoint = vi.fn();
|
||||||
|
oldDevice.addConnection({ sendWaypoint: mockSendWaypoint } as any);
|
||||||
|
|
||||||
oldDevice.setHardware(makeHardware(777));
|
oldDevice.setHardware(makeHardware(777));
|
||||||
oldDevice.addWaypoint(
|
oldDevice.addWaypoint(
|
||||||
@@ -393,10 +400,10 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
|||||||
|
|
||||||
// Remove waypoint
|
// Remove waypoint
|
||||||
oldDevice.removeWaypoint(102, false);
|
oldDevice.removeWaypoint(102, false);
|
||||||
expect(oldDevice.connection?.sendWaypoint).not.toHaveBeenCalled();
|
expect(mockSendWaypoint).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await oldDevice.removeWaypoint(101, true); // toMesh=true
|
await oldDevice.removeWaypoint(101, true); // toMesh=true
|
||||||
expect(oldDevice.connection?.sendWaypoint).toHaveBeenCalled();
|
expect(mockSendWaypoint).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(98);
|
expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(98);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ import {
|
|||||||
persist,
|
persist,
|
||||||
subscribeWithSelector,
|
subscribeWithSelector,
|
||||||
} from "zustand/middleware";
|
} from "zustand/middleware";
|
||||||
|
import type { ChangeRegistry, ConfigChangeKey } from "./changeRegistry.ts";
|
||||||
|
import {
|
||||||
|
createChangeRegistry,
|
||||||
|
getAllChannelChanges,
|
||||||
|
getAllConfigChanges,
|
||||||
|
getAllModuleConfigChanges,
|
||||||
|
getChannelChangeCount,
|
||||||
|
getConfigChangeCount,
|
||||||
|
getModuleConfigChangeCount,
|
||||||
|
hasChannelChange,
|
||||||
|
hasConfigChange,
|
||||||
|
hasModuleConfigChange,
|
||||||
|
hasUserChange,
|
||||||
|
serializeKey,
|
||||||
|
} from "./changeRegistry.ts";
|
||||||
import type {
|
import type {
|
||||||
Dialogs,
|
Dialogs,
|
||||||
DialogVariant,
|
DialogVariant,
|
||||||
@@ -42,9 +57,7 @@ export interface Device extends DeviceData {
|
|||||||
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
|
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
|
||||||
config: Protobuf.LocalOnly.LocalConfig;
|
config: Protobuf.LocalOnly.LocalConfig;
|
||||||
moduleConfig: Protobuf.LocalOnly.LocalModuleConfig;
|
moduleConfig: Protobuf.LocalOnly.LocalModuleConfig;
|
||||||
workingConfig: Protobuf.Config.Config[];
|
changeRegistry: ChangeRegistry; // Unified change tracking
|
||||||
workingModuleConfig: Protobuf.ModuleConfig.ModuleConfig[];
|
|
||||||
workingChannelConfig: Protobuf.Channel.Channel[];
|
|
||||||
hardware: Protobuf.Mesh.MyNodeInfo;
|
hardware: Protobuf.Mesh.MyNodeInfo;
|
||||||
metadata: Map<number, Protobuf.Mesh.DeviceMetadata>;
|
metadata: Map<number, Protobuf.Mesh.DeviceMetadata>;
|
||||||
connection?: MeshDevice;
|
connection?: MeshDevice;
|
||||||
@@ -58,27 +71,12 @@ export interface Device extends DeviceData {
|
|||||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||||
setConfig: (config: Protobuf.Config.Config) => void;
|
setConfig: (config: Protobuf.Config.Config) => void;
|
||||||
setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
|
setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
|
||||||
setWorkingConfig: (config: Protobuf.Config.Config) => void;
|
|
||||||
setWorkingModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
|
|
||||||
getWorkingConfig<K extends ValidConfigType>(
|
|
||||||
payloadVariant: K,
|
|
||||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
|
||||||
getWorkingModuleConfig<K extends ValidModuleConfigType>(
|
|
||||||
payloadVariant: K,
|
|
||||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
|
||||||
removeWorkingConfig: (payloadVariant?: ValidConfigType) => void;
|
|
||||||
removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => void;
|
|
||||||
getEffectiveConfig<K extends ValidConfigType>(
|
getEffectiveConfig<K extends ValidConfigType>(
|
||||||
payloadVariant: K,
|
payloadVariant: K,
|
||||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
): Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
||||||
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
||||||
payloadVariant: K,
|
payloadVariant: K,
|
||||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
||||||
setWorkingChannelConfig: (channelNum: Protobuf.Channel.Channel) => void;
|
|
||||||
getWorkingChannelConfig: (
|
|
||||||
index: Types.ChannelNumber,
|
|
||||||
) => Protobuf.Channel.Channel | undefined;
|
|
||||||
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => void;
|
|
||||||
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => void;
|
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => void;
|
||||||
setActiveNode: (node: number) => void;
|
setActiveNode: (node: number) => void;
|
||||||
setPendingSettingsChanges: (state: boolean) => void;
|
setPendingSettingsChanges: (state: boolean) => void;
|
||||||
@@ -116,6 +114,27 @@ export interface Device extends DeviceData {
|
|||||||
neighborInfo: Protobuf.Mesh.NeighborInfo,
|
neighborInfo: Protobuf.Mesh.NeighborInfo,
|
||||||
) => void;
|
) => void;
|
||||||
getNeighborInfo: (nodeNum: number) => Protobuf.Mesh.NeighborInfo | undefined;
|
getNeighborInfo: (nodeNum: number) => Protobuf.Mesh.NeighborInfo | undefined;
|
||||||
|
|
||||||
|
// New unified change tracking methods
|
||||||
|
setChange: (
|
||||||
|
key: ConfigChangeKey,
|
||||||
|
value: unknown,
|
||||||
|
originalValue?: unknown,
|
||||||
|
) => void;
|
||||||
|
removeChange: (key: ConfigChangeKey) => void;
|
||||||
|
hasChange: (key: ConfigChangeKey) => boolean;
|
||||||
|
getChange: (key: ConfigChangeKey) => unknown | undefined;
|
||||||
|
clearAllChanges: () => void;
|
||||||
|
hasConfigChange: (variant: ValidConfigType) => boolean;
|
||||||
|
hasModuleConfigChange: (variant: ValidModuleConfigType) => boolean;
|
||||||
|
hasChannelChange: (index: Types.ChannelNumber) => boolean;
|
||||||
|
hasUserChange: () => boolean;
|
||||||
|
getConfigChangeCount: () => number;
|
||||||
|
getModuleConfigChangeCount: () => number;
|
||||||
|
getChannelChangeCount: () => number;
|
||||||
|
getAllConfigChanges: () => Protobuf.Config.Config[];
|
||||||
|
getAllModuleConfigChanges: () => Protobuf.ModuleConfig.ModuleConfig[];
|
||||||
|
getAllChannelChanges: () => Protobuf.Channel.Channel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface deviceState {
|
export interface deviceState {
|
||||||
@@ -157,9 +176,7 @@ function deviceFactory(
|
|||||||
channels: new Map(),
|
channels: new Map(),
|
||||||
config: create(Protobuf.LocalOnly.LocalConfigSchema),
|
config: create(Protobuf.LocalOnly.LocalConfigSchema),
|
||||||
moduleConfig: create(Protobuf.LocalOnly.LocalModuleConfigSchema),
|
moduleConfig: create(Protobuf.LocalOnly.LocalModuleConfigSchema),
|
||||||
workingConfig: [],
|
changeRegistry: createChangeRegistry(),
|
||||||
workingModuleConfig: [],
|
|
||||||
workingChannelConfig: [],
|
|
||||||
hardware: create(Protobuf.Mesh.MyNodeInfoSchema),
|
hardware: create(Protobuf.Mesh.MyNodeInfoSchema),
|
||||||
metadata: new Map(),
|
metadata: new Map(),
|
||||||
connection: undefined,
|
connection: undefined,
|
||||||
@@ -302,130 +319,6 @@ function deviceFactory(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setWorkingConfig: (config: Protobuf.Config.Config) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = device.workingConfig.findIndex(
|
|
||||||
(wc) => wc.payloadVariant.case === config.payloadVariant.case,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingConfig[index] = config;
|
|
||||||
} else {
|
|
||||||
device.workingConfig.push(config);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setWorkingModuleConfig: (
|
|
||||||
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
|
|
||||||
) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = device.workingModuleConfig.findIndex(
|
|
||||||
(wmc) =>
|
|
||||||
wmc.payloadVariant.case === moduleConfig.payloadVariant.case,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingModuleConfig[index] = moduleConfig;
|
|
||||||
} else {
|
|
||||||
device.workingModuleConfig.push(moduleConfig);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
getWorkingConfig<K extends ValidConfigType>(payloadVariant: K) {
|
|
||||||
const device = get().devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workingConfig = device.workingConfig.find(
|
|
||||||
(c) => c.payloadVariant.case === payloadVariant,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
workingConfig?.payloadVariant.case === "deviceUi" ||
|
|
||||||
workingConfig?.payloadVariant.case === "sessionkey"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return workingConfig?.payloadVariant
|
|
||||||
.value as Protobuf.LocalOnly.LocalConfig[K];
|
|
||||||
},
|
|
||||||
getWorkingModuleConfig<K extends ValidModuleConfigType>(
|
|
||||||
payloadVariant: K,
|
|
||||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined {
|
|
||||||
const device = get().devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return device.workingModuleConfig.find(
|
|
||||||
(c) => c.payloadVariant.case === payloadVariant,
|
|
||||||
)?.payloadVariant.value as Protobuf.LocalOnly.LocalModuleConfig[K];
|
|
||||||
},
|
|
||||||
|
|
||||||
removeWorkingConfig: (payloadVariant?: ValidConfigType) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payloadVariant) {
|
|
||||||
device.workingConfig = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = device.workingConfig.findIndex(
|
|
||||||
(wc: Protobuf.Config.Config) =>
|
|
||||||
wc.payloadVariant.case === payloadVariant,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingConfig.splice(index, 1);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payloadVariant) {
|
|
||||||
device.workingModuleConfig = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = device.workingModuleConfig.findIndex(
|
|
||||||
(wc: Protobuf.ModuleConfig.ModuleConfig) =>
|
|
||||||
wc.payloadVariant.case === payloadVariant,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingModuleConfig.splice(index, 1);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
getEffectiveConfig<K extends ValidConfigType>(
|
getEffectiveConfig<K extends ValidConfigType>(
|
||||||
payloadVariant: K,
|
payloadVariant: K,
|
||||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined {
|
): Protobuf.LocalOnly.LocalConfig[K] | undefined {
|
||||||
@@ -437,11 +330,13 @@ function deviceFactory(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workingValue = device.changeRegistry.changes.get(
|
||||||
|
serializeKey({ type: "config", variant: payloadVariant }),
|
||||||
|
)?.value as Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...device.config[payloadVariant],
|
...device.config[payloadVariant],
|
||||||
...device.workingConfig.find(
|
...workingValue,
|
||||||
(c) => c.payloadVariant.case === payloadVariant,
|
|
||||||
)?.payloadVariant.value,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
||||||
@@ -452,69 +347,16 @@ function deviceFactory(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workingValue = device.changeRegistry.changes.get(
|
||||||
|
serializeKey({ type: "moduleConfig", variant: payloadVariant }),
|
||||||
|
)?.value as Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...device.moduleConfig[payloadVariant],
|
...device.moduleConfig[payloadVariant],
|
||||||
...device.workingModuleConfig.find(
|
...workingValue,
|
||||||
(c) => c.payloadVariant.case === payloadVariant,
|
|
||||||
)?.payloadVariant.value,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
setWorkingChannelConfig: (config: Protobuf.Channel.Channel) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = device.workingChannelConfig.findIndex(
|
|
||||||
(wcc) => wcc.index === config.index,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingChannelConfig[index] = config;
|
|
||||||
} else {
|
|
||||||
device.workingChannelConfig.push(config);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getWorkingChannelConfig: (channelNum: Types.ChannelNumber) => {
|
|
||||||
const device = get().devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workingChannelConfig = device.workingChannelConfig.find(
|
|
||||||
(c) => c.index === channelNum,
|
|
||||||
);
|
|
||||||
|
|
||||||
return workingChannelConfig;
|
|
||||||
},
|
|
||||||
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => {
|
|
||||||
set(
|
|
||||||
produce<PrivateDeviceState>((draft) => {
|
|
||||||
const device = draft.devices.get(id);
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channelNum === undefined) {
|
|
||||||
device.workingChannelConfig = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = device.workingChannelConfig.findIndex(
|
|
||||||
(wcc: Protobuf.Channel.Channel) => wcc.index === channelNum,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
device.workingChannelConfig.splice(index, 1);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
|
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
|
||||||
set(
|
set(
|
||||||
produce<PrivateDeviceState>((draft) => {
|
produce<PrivateDeviceState>((draft) => {
|
||||||
@@ -855,6 +697,195 @@ function deviceFactory(
|
|||||||
}
|
}
|
||||||
return device.neighborInfo.get(nodeNum);
|
return device.neighborInfo.get(nodeNum);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// New unified change tracking methods
|
||||||
|
setChange: (
|
||||||
|
key: ConfigChangeKey,
|
||||||
|
value: unknown,
|
||||||
|
originalValue?: unknown,
|
||||||
|
) => {
|
||||||
|
set(
|
||||||
|
produce<PrivateDeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyStr = serializeKey(key);
|
||||||
|
device.changeRegistry.changes.set(keyStr, {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
originalValue,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChange: (key: ConfigChangeKey) => {
|
||||||
|
set(
|
||||||
|
produce<PrivateDeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.changeRegistry.changes.delete(serializeKey(key));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasChange: (key: ConfigChangeKey) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
return device?.changeRegistry.changes.has(serializeKey(key)) ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
getChange: (key: ConfigChangeKey) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.changeRegistry.changes.get(serializeKey(key))?.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAllChanges: () => {
|
||||||
|
set(
|
||||||
|
produce<PrivateDeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.changeRegistry.changes.clear();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasConfigChange: (variant: ValidConfigType) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasConfigChange(device.changeRegistry, variant);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasModuleConfigChange: (variant: ValidModuleConfigType) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasModuleConfigChange(device.changeRegistry, variant);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasChannelChange: (index: Types.ChannelNumber) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChannelChange(device.changeRegistry, index);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasUserChange: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUserChange(device.changeRegistry);
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfigChangeCount: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConfigChangeCount(device.changeRegistry);
|
||||||
|
},
|
||||||
|
|
||||||
|
getModuleConfigChangeCount: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getModuleConfigChangeCount(device.changeRegistry);
|
||||||
|
},
|
||||||
|
|
||||||
|
getChannelChangeCount: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getChannelChangeCount(device.changeRegistry);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllConfigChanges: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = getAllConfigChanges(device.changeRegistry);
|
||||||
|
return changes
|
||||||
|
.map((entry) => {
|
||||||
|
if (entry.key.type !== "config") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!entry.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return create(Protobuf.Config.ConfigSchema, {
|
||||||
|
payloadVariant: {
|
||||||
|
case: entry.key.variant,
|
||||||
|
value: entry.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter((c): c is Protobuf.Config.Config => c !== null);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllModuleConfigChanges: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = getAllModuleConfigChanges(device.changeRegistry);
|
||||||
|
return changes
|
||||||
|
.map((entry) => {
|
||||||
|
if (entry.key.type !== "moduleConfig") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!entry.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
||||||
|
payloadVariant: {
|
||||||
|
case: entry.key.variant,
|
||||||
|
value: entry.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter((c): c is Protobuf.ModuleConfig.ModuleConfig => c !== null);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllChannelChanges: () => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = getAllChannelChanges(device.changeRegistry);
|
||||||
|
return changes
|
||||||
|
.map((entry) => entry.value as Protobuf.Channel.Channel)
|
||||||
|
.filter((c): c is Protobuf.Channel.Channel => c !== undefined);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
|
import type {
|
||||||
|
ValidConfigType,
|
||||||
|
ValidModuleConfigType,
|
||||||
|
} from "./changeRegistry.ts";
|
||||||
|
|
||||||
interface Dialogs {
|
interface Dialogs {
|
||||||
import: boolean;
|
import: boolean;
|
||||||
@@ -22,16 +26,7 @@ interface Dialogs {
|
|||||||
|
|
||||||
type DialogVariant = keyof Dialogs;
|
type DialogVariant = keyof Dialogs;
|
||||||
|
|
||||||
type ValidConfigType = Exclude<
|
type Page = "messages" | "map" | "settings" | "channels" | "nodes";
|
||||||
Protobuf.Config.Config["payloadVariant"]["case"],
|
|
||||||
"deviceUi" | "sessionkey" | undefined
|
|
||||||
>;
|
|
||||||
type ValidModuleConfigType = Exclude<
|
|
||||||
Protobuf.ModuleConfig.ModuleConfig["payloadVariant"]["case"],
|
|
||||||
undefined
|
|
||||||
>;
|
|
||||||
|
|
||||||
type Page = "messages" | "map" | "config" | "channels" | "nodes";
|
|
||||||
|
|
||||||
type WaypointWithMetadata = Protobuf.Mesh.Waypoint & {
|
type WaypointWithMetadata = Protobuf.Mesh.Waypoint & {
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ i18next
|
|||||||
"channels",
|
"channels",
|
||||||
"commandPalette",
|
"commandPalette",
|
||||||
"common",
|
"common",
|
||||||
"deviceConfig",
|
"config",
|
||||||
"moduleConfig",
|
"moduleConfig",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"dialog",
|
"dialog",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { randId } from "@core/utils/randId.ts";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { Protobuf, Types } from "@meshtastic/core";
|
import { Protobuf, Types } from "@meshtastic/core";
|
||||||
import { getChannelName } from "@pages/Config/ChannelConfig.tsx";
|
|
||||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getChannelName } from "../components/PageComponents/Channels/Channels.tsx";
|
||||||
|
|
||||||
type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number };
|
type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number };
|
||||||
|
|
||||||
|
|||||||
+26
-25
@@ -1,11 +1,10 @@
|
|||||||
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
|
import { Bluetooth } from "@components/PageComponents/Settings/Bluetooth.tsx";
|
||||||
import { Device } from "@components/PageComponents/Config/Device/index.tsx";
|
import { Device } from "@components/PageComponents/Settings/Device/index.tsx";
|
||||||
import { Display } from "@components/PageComponents/Config/Display.tsx";
|
import { Display } from "@components/PageComponents/Settings/Display.tsx";
|
||||||
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
|
import { Network } from "@components/PageComponents/Settings/Network/index.tsx";
|
||||||
import { Network } from "@components/PageComponents/Config/Network/index.tsx";
|
import { Position } from "@components/PageComponents/Settings/Position.tsx";
|
||||||
import { Position } from "@components/PageComponents/Config/Position.tsx";
|
import { Power } from "@components/PageComponents/Settings/Power.tsx";
|
||||||
import { Power } from "@components/PageComponents/Config/Power.tsx";
|
import { User } from "@components/PageComponents/Settings/User.tsx";
|
||||||
import { Security } from "@components/PageComponents/Config/Security/Security.tsx";
|
|
||||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -23,21 +22,25 @@ interface ConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TabItem = {
|
type TabItem = {
|
||||||
case: ValidConfigType;
|
case: ValidConfigType | "user";
|
||||||
label: string;
|
label: string;
|
||||||
element: ComponentType<ConfigProps>;
|
element: ComponentType<ConfigProps>;
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
||||||
const { getWorkingConfig } = useDevice();
|
const { hasConfigChange, hasUserChange } = useDevice();
|
||||||
const { t } = useTranslation("deviceConfig");
|
const { t } = useTranslation("config");
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
|
{
|
||||||
|
case: "user",
|
||||||
|
label: t("page.tabUser"),
|
||||||
|
element: User,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
case: "device",
|
case: "device",
|
||||||
label: t("page.tabDevice"),
|
label: t("page.tabDevice"),
|
||||||
element: Device,
|
element: Device,
|
||||||
count: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case: "position",
|
case: "position",
|
||||||
@@ -59,30 +62,28 @@ export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
|||||||
label: t("page.tabDisplay"),
|
label: t("page.tabDisplay"),
|
||||||
element: Display,
|
element: Display,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
case: "lora",
|
|
||||||
label: t("page.tabLora"),
|
|
||||||
element: LoRa,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
case: "bluetooth",
|
case: "bluetooth",
|
||||||
label: t("page.tabBluetooth"),
|
label: t("page.tabBluetooth"),
|
||||||
element: Bluetooth,
|
element: Bluetooth,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
case: "security",
|
|
||||||
label: t("page.tabSecurity"),
|
|
||||||
element: Security,
|
|
||||||
},
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const flags = useMemo(
|
const flags = useMemo(
|
||||||
() => new Map(tabs.map((tab) => [tab.case, getWorkingConfig(tab.case)])),
|
() =>
|
||||||
[tabs, getWorkingConfig],
|
new Map(
|
||||||
|
tabs.map((tab) => [
|
||||||
|
tab.case,
|
||||||
|
tab.case === "user"
|
||||||
|
? hasUserChange()
|
||||||
|
: hasConfigChange(tab.case as ValidConfigType),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
[tabs, hasConfigChange, hasUserChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue={t("page.tabDevice")}>
|
<Tabs defaultValue={t("page.tabUser")}>
|
||||||
<TabsList className="w-full dark:bg-slate-700">
|
<TabsList className="w-full dark:bg-slate-700">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
+3
-3
@@ -34,7 +34,7 @@ type TabItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
||||||
const { getWorkingModuleConfig } = useDevice();
|
const { hasModuleConfigChange } = useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
{
|
{
|
||||||
@@ -97,8 +97,8 @@ export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
|||||||
|
|
||||||
const flags = useMemo(
|
const flags = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(tabs.map((tab) => [tab.case, getWorkingModuleConfig(tab.case)])),
|
new Map(tabs.map((tab) => [tab.case, hasModuleConfigChange(tab.case)])),
|
||||||
[tabs, getWorkingModuleConfig],
|
[tabs, hasModuleConfigChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Channels } from "@app/components/PageComponents/Channels/Channels";
|
||||||
|
import { LoRa } from "@components/PageComponents/Settings/LoRa.tsx";
|
||||||
|
import { Security } from "@components/PageComponents/Settings/Security/Security.tsx";
|
||||||
|
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@components/UI/Tabs.tsx";
|
||||||
|
import { useDevice, type ValidConfigType } from "@core/stores";
|
||||||
|
import { type ComponentType, Suspense, useMemo } from "react";
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface ConfigProps {
|
||||||
|
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabItem = {
|
||||||
|
case: ValidConfigType;
|
||||||
|
label: string;
|
||||||
|
element: ComponentType<ConfigProps>;
|
||||||
|
count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioConfig = ({ onFormInit }: ConfigProps) => {
|
||||||
|
const { hasConfigChange } = useDevice();
|
||||||
|
const { t } = useTranslation("config");
|
||||||
|
const tabs: TabItem[] = [
|
||||||
|
{
|
||||||
|
case: "lora",
|
||||||
|
label: t("page.tabLora"),
|
||||||
|
element: LoRa,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case: "channels",
|
||||||
|
label: t("page.tabChannels"),
|
||||||
|
element: Channels,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case: "security",
|
||||||
|
label: t("page.tabSecurity"),
|
||||||
|
element: Security,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const flags = useMemo(
|
||||||
|
() => new Map(tabs.map((tab) => [tab.case, hasConfigChange(tab.case)])),
|
||||||
|
[tabs, hasConfigChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue={t("page.tabLora")}>
|
||||||
|
<TabsList className="w-full dark:bg-slate-700">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.label}
|
||||||
|
value={tab.label}
|
||||||
|
className="dark:text-white relative"
|
||||||
|
>
|
||||||
|
{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 className="relative inline-flex size-3 rounded-full bg-sky-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsContent key={tab.label} value={tab.label}>
|
||||||
|
<Suspense fallback={<Spinner size="lg" className="my-5" />}>
|
||||||
|
<tab.element onFormInit={onFormInit} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
+93
-91
@@ -1,3 +1,4 @@
|
|||||||
|
import { deviceRoute, moduleRoute, radioRoute } from "@app/routes";
|
||||||
import { PageLayout } from "@components/PageLayout.tsx";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.tsx";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
|
import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
|
||||||
@@ -5,44 +6,84 @@ import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
|||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { ChannelConfig } from "@pages/Config/ChannelConfig.tsx";
|
import { DeviceConfig } from "@pages/Settings/DeviceConfig.tsx";
|
||||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
|
import { ModuleConfig } from "@pages/Settings/ModuleConfig.tsx";
|
||||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
BoxesIcon,
|
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
|
RadioTowerIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
|
RouterIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SaveOff,
|
SaveOff,
|
||||||
SettingsIcon,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { RadioConfig } from "./RadioConfig.tsx";
|
||||||
|
|
||||||
const ConfigPage = () => {
|
const ConfigPage = () => {
|
||||||
const {
|
const {
|
||||||
workingConfig,
|
getAllConfigChanges,
|
||||||
workingModuleConfig,
|
getAllModuleConfigChanges,
|
||||||
workingChannelConfig,
|
getAllChannelChanges,
|
||||||
connection,
|
connection,
|
||||||
removeWorkingConfig,
|
clearAllChanges,
|
||||||
removeWorkingModuleConfig,
|
|
||||||
removeWorkingChannelConfig,
|
|
||||||
setConfig,
|
setConfig,
|
||||||
setModuleConfig,
|
setModuleConfig,
|
||||||
addChannel,
|
addChannel,
|
||||||
|
getConfigChangeCount,
|
||||||
|
getModuleConfigChangeCount,
|
||||||
|
getChannelChangeCount,
|
||||||
} = useDevice();
|
} = useDevice();
|
||||||
|
|
||||||
const [activeConfigSection, setActiveConfigSection] = useState<
|
|
||||||
"device" | "module" | "channel"
|
|
||||||
>("device");
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [rhfState, setRhfState] = useState({ isDirty: false, isValid: true });
|
const [rhfState, setRhfState] = useState({ isDirty: false, isValid: true });
|
||||||
const unsubRef = useRef<(() => void) | null>(null);
|
const unsubRef = useRef<(() => void) | null>(null);
|
||||||
const [formMethods, setFormMethods] = useState<UseFormReturn | null>(null);
|
const [formMethods, setFormMethods] = useState<UseFormReturn | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation("deviceConfig");
|
const navigate = useNavigate();
|
||||||
|
const routerState = useRouterState();
|
||||||
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
|
const configChangeCount = getConfigChangeCount();
|
||||||
|
const moduleConfigChangeCount = getModuleConfigChangeCount();
|
||||||
|
const channelChangeCount = getChannelChangeCount();
|
||||||
|
|
||||||
|
const sections = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: "radio",
|
||||||
|
route: radioRoute,
|
||||||
|
label: t("navigation.radioConfig"),
|
||||||
|
icon: RadioTowerIcon,
|
||||||
|
changeCount: configChangeCount,
|
||||||
|
component: RadioConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "device",
|
||||||
|
route: deviceRoute,
|
||||||
|
label: t("navigation.deviceConfig"),
|
||||||
|
icon: RouterIcon,
|
||||||
|
changeCount: moduleConfigChangeCount,
|
||||||
|
component: DeviceConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "module",
|
||||||
|
route: moduleRoute,
|
||||||
|
label: t("navigation.moduleConfig"),
|
||||||
|
icon: LayersIcon,
|
||||||
|
changeCount: channelChangeCount,
|
||||||
|
component: ModuleConfig,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, configChangeCount, moduleConfigChangeCount, channelChangeCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeSection =
|
||||||
|
sections.find((section) =>
|
||||||
|
routerState.location.pathname.includes(`/settings/${section.key}`),
|
||||||
|
) ?? sections[0];
|
||||||
|
|
||||||
const onFormInit = useCallback(
|
const onFormInit = useCallback(
|
||||||
<T extends FieldValues>(methods: UseFormReturn<T>) => {
|
<T extends FieldValues>(methods: UseFormReturn<T>) => {
|
||||||
@@ -69,7 +110,6 @@ const ConfigPage = () => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cleanup subscription on unmount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => unsubRef.current?.();
|
return () => unsubRef.current?.();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -78,9 +118,12 @@ const ConfigPage = () => {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save all working channel configs first, doesn't require a commit/reboot
|
const channelChanges = getAllChannelChanges();
|
||||||
|
const configChanges = getAllConfigChanges();
|
||||||
|
const moduleConfigChanges = getAllModuleConfigChanges();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workingChannelConfig.map((channel) =>
|
channelChanges.map((channel) =>
|
||||||
connection?.setChannel(channel).then(() => {
|
connection?.setChannel(channel).then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: t("toast.savedChannel.title", {
|
title: t("toast.savedChannel.title", {
|
||||||
@@ -93,7 +136,7 @@ const ConfigPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workingConfig.map((newConfig) =>
|
configChanges.map((newConfig) =>
|
||||||
connection?.setConfig(newConfig).then(() => {
|
connection?.setConfig(newConfig).then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: t("toast.saveSuccess.title"),
|
title: t("toast.saveSuccess.title"),
|
||||||
@@ -106,7 +149,7 @@ const ConfigPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workingModuleConfig.map((newModuleConfig) =>
|
moduleConfigChanges.map((newModuleConfig) =>
|
||||||
connection?.setModuleConfig(newModuleConfig).then(() =>
|
connection?.setModuleConfig(newModuleConfig).then(() =>
|
||||||
toast({
|
toast({
|
||||||
title: t("toast.saveSuccess.title"),
|
title: t("toast.saveSuccess.title"),
|
||||||
@@ -118,23 +161,21 @@ const ConfigPage = () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workingConfig.length > 0 || workingModuleConfig.length > 0) {
|
if (configChanges.length > 0 || moduleConfigChanges.length > 0) {
|
||||||
await connection?.commitEditSettings();
|
await connection?.commitEditSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
workingChannelConfig.forEach((newChannel) => {
|
channelChanges.forEach((newChannel) => {
|
||||||
addChannel(newChannel);
|
addChannel(newChannel);
|
||||||
});
|
});
|
||||||
workingConfig.forEach((newConfig) => {
|
configChanges.forEach((newConfig) => {
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
});
|
});
|
||||||
workingModuleConfig.forEach((newModuleConfig) => {
|
moduleConfigChanges.forEach((newModuleConfig) => {
|
||||||
setModuleConfig(newModuleConfig);
|
setModuleConfig(newModuleConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
removeWorkingChannelConfig();
|
clearAllChanges();
|
||||||
removeWorkingConfig();
|
|
||||||
removeWorkingModuleConfig();
|
|
||||||
|
|
||||||
if (formMethods) {
|
if (formMethods) {
|
||||||
formMethods.reset(formMethods.getValues(), {
|
formMethods.reset(formMethods.getValues(), {
|
||||||
@@ -144,7 +185,6 @@ const ConfigPage = () => {
|
|||||||
keepValues: true,
|
keepValues: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force RHF to re-validate and emit state
|
|
||||||
formMethods.trigger();
|
formMethods.trigger();
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -162,77 +202,49 @@ const ConfigPage = () => {
|
|||||||
}, [
|
}, [
|
||||||
toast,
|
toast,
|
||||||
t,
|
t,
|
||||||
workingConfig,
|
getAllConfigChanges,
|
||||||
connection,
|
connection,
|
||||||
workingModuleConfig,
|
getAllModuleConfigChanges,
|
||||||
workingChannelConfig,
|
getAllChannelChanges,
|
||||||
formMethods,
|
formMethods,
|
||||||
addChannel,
|
addChannel,
|
||||||
setConfig,
|
setConfig,
|
||||||
setModuleConfig,
|
setModuleConfig,
|
||||||
removeWorkingConfig,
|
clearAllChanges,
|
||||||
removeWorkingModuleConfig,
|
|
||||||
removeWorkingChannelConfig,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
if (formMethods) {
|
if (formMethods) {
|
||||||
formMethods.reset();
|
formMethods.reset();
|
||||||
}
|
}
|
||||||
removeWorkingChannelConfig();
|
clearAllChanges();
|
||||||
removeWorkingConfig();
|
}, [formMethods, clearAllChanges]);
|
||||||
removeWorkingModuleConfig();
|
|
||||||
}, [
|
|
||||||
formMethods,
|
|
||||||
removeWorkingConfig,
|
|
||||||
removeWorkingModuleConfig,
|
|
||||||
removeWorkingChannelConfig,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const leftSidebar = useMemo(
|
const leftSidebar = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarSection label={t("sidebar.label")} className="py-2 px-0">
|
<SidebarSection label={t("sidebar.label")} className="py-2 px-0">
|
||||||
<SidebarButton
|
{sections.map((section) => (
|
||||||
label={t("navigation.radioConfig")}
|
<SidebarButton
|
||||||
active={activeConfigSection === "device"}
|
key={section.key}
|
||||||
onClick={() => setActiveConfigSection("device")}
|
label={section.label}
|
||||||
Icon={SettingsIcon}
|
active={activeSection?.key === section.key}
|
||||||
isDirty={workingConfig.length > 0}
|
onClick={() => navigate({ to: section.route.to })}
|
||||||
count={workingConfig.length}
|
Icon={section.icon}
|
||||||
/>
|
isDirty={section.changeCount > 0}
|
||||||
<SidebarButton
|
count={section.changeCount}
|
||||||
label={t("navigation.moduleConfig")}
|
/>
|
||||||
active={activeConfigSection === "module"}
|
))}
|
||||||
onClick={() => setActiveConfigSection("module")}
|
|
||||||
Icon={BoxesIcon}
|
|
||||||
isDirty={workingModuleConfig.length > 0}
|
|
||||||
count={workingModuleConfig.length}
|
|
||||||
/>
|
|
||||||
<SidebarButton
|
|
||||||
label={t("navigation.channelConfig")}
|
|
||||||
active={activeConfigSection === "channel"}
|
|
||||||
onClick={() => setActiveConfigSection("channel")}
|
|
||||||
Icon={LayersIcon}
|
|
||||||
isDirty={workingChannelConfig.length > 0}
|
|
||||||
count={workingChannelConfig.length}
|
|
||||||
/>
|
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
),
|
),
|
||||||
[
|
[sections, activeSection?.key, navigate, t],
|
||||||
activeConfigSection,
|
|
||||||
workingConfig,
|
|
||||||
workingModuleConfig,
|
|
||||||
workingChannelConfig,
|
|
||||||
t,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasDrafts =
|
const hasDrafts =
|
||||||
workingConfig.length > 0 ||
|
getConfigChangeCount() > 0 ||
|
||||||
workingModuleConfig.length > 0 ||
|
getModuleConfigChangeCount() > 0 ||
|
||||||
workingChannelConfig.length > 0;
|
getChannelChangeCount() > 0;
|
||||||
const hasPending = hasDrafts || rhfState.isDirty;
|
const hasPending = hasDrafts || rhfState.isDirty;
|
||||||
const buttonOpacity = hasPending ? "opacity-100" : "opacity-0";
|
const buttonOpacity = hasPending ? "opacity-100" : "opacity-0";
|
||||||
const saveDisabled = isSaving || !rhfState.isValid || !hasPending;
|
const saveDisabled = isSaving || !rhfState.isValid || !hasPending;
|
||||||
@@ -291,26 +303,16 @@ const ConfigPage = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ActiveComponent = activeSection?.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
contentClassName="overflow-auto"
|
contentClassName="overflow-auto"
|
||||||
leftBar={leftSidebar}
|
leftBar={leftSidebar}
|
||||||
label={
|
label={activeSection?.label ?? ""}
|
||||||
activeConfigSection === "device"
|
|
||||||
? t("navigation.radioConfig")
|
|
||||||
: activeConfigSection === "module"
|
|
||||||
? t("navigation.moduleConfig")
|
|
||||||
: t("navigation.channelConfig")
|
|
||||||
}
|
|
||||||
actions={actions}
|
actions={actions}
|
||||||
>
|
>
|
||||||
{activeConfigSection === "device" ? (
|
<ActiveComponent onFormInit={onFormInit} />
|
||||||
<DeviceConfig onFormInit={onFormInit} />
|
|
||||||
) : activeConfigSection === "module" ? (
|
|
||||||
<ModuleConfig onFormInit={onFormInit} />
|
|
||||||
) : (
|
|
||||||
<ChannelConfig onFormInit={onFormInit} />
|
|
||||||
)}
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||||
import type { useAppStore, useMessageStore } from "@core/stores";
|
import type { useAppStore, useMessageStore } from "@core/stores";
|
||||||
import ConfigPage from "@pages/Config/index.tsx";
|
|
||||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||||
import MapPage from "@pages/Map/index.tsx";
|
import MapPage from "@pages/Map/index.tsx";
|
||||||
import MessagesPage from "@pages/Messages.tsx";
|
import MessagesPage from "@pages/Messages.tsx";
|
||||||
import NodesPage from "@pages/Nodes/index.tsx";
|
import NodesPage from "@pages/Nodes/index.tsx";
|
||||||
|
import ConfigPage from "@pages/Settings/index.tsx";
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
createRoute,
|
createRoute,
|
||||||
@@ -109,9 +109,33 @@ export const mapWithParamsRoute = createRoute({
|
|||||||
// }),
|
// }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const configRoute = createRoute({
|
export const settingsRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/config",
|
path: "/settings",
|
||||||
|
component: ConfigPage,
|
||||||
|
// beforeLoad: () => {
|
||||||
|
// throw redirect({
|
||||||
|
// to: "/settings/radio",
|
||||||
|
// replace: true,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const radioRoute = createRoute({
|
||||||
|
getParentRoute: () => settingsRoute,
|
||||||
|
path: "radio",
|
||||||
|
component: ConfigPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deviceRoute = createRoute({
|
||||||
|
getParentRoute: () => settingsRoute,
|
||||||
|
path: "device",
|
||||||
|
component: ConfigPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const moduleRoute = createRoute({
|
||||||
|
getParentRoute: () => settingsRoute,
|
||||||
|
path: "module",
|
||||||
component: ConfigPage,
|
component: ConfigPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +157,7 @@ const routeTree = rootRoute.addChildren([
|
|||||||
messagesWithParamsRoute,
|
messagesWithParamsRoute,
|
||||||
mapRoute,
|
mapRoute,
|
||||||
mapWithParamsRoute,
|
mapWithParamsRoute,
|
||||||
configRoute,
|
settingsRoute.addChildren([radioRoute, deviceRoute, moduleRoute]),
|
||||||
nodesRoute,
|
nodesRoute,
|
||||||
dialogWithParamsRoute,
|
dialogWithParamsRoute,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import commandPaletteEN from "@public/i18n/locales/en/commandPalette.json" with
|
|||||||
import commonEN from "@public/i18n/locales/en/common.json" with {
|
import commonEN from "@public/i18n/locales/en/common.json" with {
|
||||||
type: "json",
|
type: "json",
|
||||||
};
|
};
|
||||||
import dashboardEN from "@public/i18n/locales/en/dashboard.json" with {
|
import configEN from "@public/i18n/locales/en/config.json" with {
|
||||||
type: "json",
|
type: "json",
|
||||||
};
|
};
|
||||||
import deviceConfigEN from "@public/i18n/locales/en/deviceConfig.json" with {
|
import dashboardEN from "@public/i18n/locales/en/dashboard.json" with {
|
||||||
type: "json",
|
type: "json",
|
||||||
};
|
};
|
||||||
import dialogEN from "@public/i18n/locales/en/dialog.json" with {
|
import dialogEN from "@public/i18n/locales/en/dialog.json" with {
|
||||||
@@ -53,7 +53,7 @@ const appNamespaces = [
|
|||||||
"channels",
|
"channels",
|
||||||
"commandPalette",
|
"commandPalette",
|
||||||
"common",
|
"common",
|
||||||
"deviceConfig",
|
"config",
|
||||||
"moduleConfig",
|
"moduleConfig",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"dialog",
|
"dialog",
|
||||||
@@ -76,7 +76,7 @@ i18n.use(initReactI18next).init({
|
|||||||
channels: channelsEN,
|
channels: channelsEN,
|
||||||
commandPalette: commandPaletteEN,
|
commandPalette: commandPaletteEN,
|
||||||
common: commonEN,
|
common: commonEN,
|
||||||
deviceConfig: deviceConfigEN,
|
config: configEN,
|
||||||
moduleConfig: moduleConfigEN,
|
moduleConfig: moduleConfigEN,
|
||||||
dashboard: dashboardEN,
|
dashboard: dashboardEN,
|
||||||
dialog: dialogEN,
|
dialog: dialogEN,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { t } from "i18next";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const UserValidationSchema = z.object({
|
||||||
|
longName: z
|
||||||
|
.string()
|
||||||
|
.min(1, t("deviceName.validation.longNameMin"))
|
||||||
|
.max(40, t("deviceName.validation.longNameMax")),
|
||||||
|
shortName: z
|
||||||
|
.string()
|
||||||
|
.min(2, t("deviceName.validation.shortNameMin"))
|
||||||
|
.max(4, t("deviceName.validation.shortNameMax")),
|
||||||
|
isUnmessageable: z.boolean().default(false),
|
||||||
|
isLicensed: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserValidation = z.infer<typeof UserValidationSchema>;
|
||||||
Reference in New Issue
Block a user