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:
Dan Ditomaso
2025-10-24 13:31:22 -04:00
committed by GitHub
co-authored by Copilot
parent ff02b1455d
commit 0cf677c8d5
45 changed files with 1116 additions and 777 deletions
@@ -4,7 +4,6 @@ import {
Languages,
type LucideIcon,
Palette,
PenLine,
Search as SearchIcon,
ZapIcon,
} from "lucide-react";
@@ -53,7 +52,6 @@ export const DeviceInfoPanel = ({
firmwareVersion,
user,
isCollapsed,
setDialogOpen,
setCommandPaletteOpen,
disableHover = false,
}: DeviceInfoPanelProps) => {
@@ -91,12 +89,6 @@ export const DeviceInfoPanel = ({
icon: Palette,
render: () => <ThemeSwitcher />,
},
{
id: "changeName",
label: t("sidebar.deviceInfo.deviceName.changeName"),
icon: PenLine,
onClick: setDialogOpen,
},
{
id: "commandMenu",
label: t("page.title", { ns: "commandPalette" }),
@@ -33,7 +33,7 @@ export interface ImportDialogProps {
}
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const { config, channels } = useDevice();
const { setChange, channels, config } = useDevice();
const { t } = useTranslation("dialog");
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
@@ -41,8 +41,6 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const [updateConfig, setUpdateConfig] = useState<boolean>(true);
const [importIndex, setImportIndex] = useState<number[]>([]);
const { setWorkingChannelConfig, setWorkingConfig } = useDevice();
useEffect(() => {
// the channel information is contained in the URL's fragment, which will be present after a
// non-URL encoded `#`.
@@ -106,7 +104,11 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
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)) {
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "lora",
value: payload,
},
}),
);
setChange({ type: "config", variant: "lora" }, payload, config.lora);
}
}
// Reset state after import
@@ -25,7 +25,7 @@ export function MultiSelectInput<T extends FieldValues>({
isDirty,
invalid,
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
const { t } = useTranslation("deviceConfig");
const { t } = useTranslation("config");
const { enumValue, className, ...remainingProperties } = field.properties;
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 { cn } from "@core/utils/cn.ts";
import { Check, Languages } from "lucide-react";
@@ -24,12 +24,7 @@ export interface SettingsPanelProps {
}
export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
const {
config,
setWorkingChannelConfig,
getWorkingChannelConfig,
removeWorkingChannelConfig,
} = useDevice();
const { config, setChange, getChange, removeChange } = useDevice();
const { t } = useTranslation(["channels", "ui", "dialog"]);
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 = {
...effectiveConfig,
...{
@@ -111,7 +110,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
return;
}
const payload = {
const payload = create(Protobuf.Channel.ChannelSchema, {
...data,
settings: {
...data.settings,
@@ -121,14 +120,14 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
positionPrecision: data.settings.moduleSettings.positionPrecision,
}),
},
};
});
if (deepCompareConfig(channel, payload, true)) {
removeWorkingChannelConfig(channel.index);
removeChange({ type: "channel", index: channel.index });
return;
}
setWorkingChannelConfig(create(Protobuf.Channel.ChannelSchema, payload));
setChange({ type: "channel", index: channel.index }, payload, channel);
};
const preSharedKeyRegenerate = async () => {
@@ -0,0 +1,95 @@
import { Channel } from "@app/components/PageComponents/Channels/Channel";
import { Button } from "@components/UI/Button.tsx";
import { Spinner } from "@components/UI/Spinner.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { useDevice } from "@core/stores";
import type { Protobuf } from "@meshtastic/core";
import i18next from "i18next";
import { QrCodeIcon, UploadIcon } from "lucide-react";
import { 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;
}
export const getChannelName = (channel: Protobuf.Channel.Channel) => {
return channel.settings?.name.length
? channel.settings?.name
: channel.index === 0
? i18next.t("page.broadcastLabel")
: i18next.t("page.channelIndex", {
ns: "channels",
index: channel.index,
});
};
export const Channels = ({ onFormInit }: ConfigProps) => {
const { channels, hasChannelChange, setDialogOpen } = useDevice();
const { t } = useTranslation("channels");
const allChannels = Array.from(channels.values());
const flags = useMemo(
() =>
new Map(
allChannels.map((channel) => [
channel.index,
hasChannelChange(channel.index),
]),
),
[allChannels, hasChannelChange],
);
return (
<Tabs defaultValue="channel_0">
<TabsList className="w-full dark:bg-slate-700">
{allChannels.map((channel) => (
<TabsTrigger
key={`channel_${channel.index}`}
value={`channel_${channel.index}`}
className="dark:text-white relative"
>
{getChannelName(channel)}
{flags.get(channel.index) && (
<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>
))}
<Button
className="ml-auto mr-1 h-8"
onClick={() => setDialogOpen("import", true)}
>
<UploadIcon className="mr-2" size={14} />
{t("page.import")}
</Button>
<Button className=" h-8" onClick={() => setDialogOpen("QR", true)}>
<QrCodeIcon className="mr-2" size={14} />
{t("page.export")}
</Button>
</TabsList>
{allChannels.map((channel) => (
<TabsContent
key={`channel_${channel.index}`}
value={`channel_${channel.index}`}
>
<Suspense fallback={<Spinner size="lg" className="my-5" />}>
<Channel
key={channel.index}
onFormInit={onFormInit}
channel={channel}
/>
</Suspense>
</TabsContent>
))}
</Tabs>
);
};
@@ -3,14 +3,12 @@ import {
type AmbientLightingValidation,
AmbientLightingValidationSchema,
} from "@app/validation/moduleConfig/ambientLighting.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface AmbientLightingModuleConfigProps {
@@ -21,27 +19,20 @@ export const AmbientLighting = ({
onFormInit,
}: AmbientLightingModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AmbientLightingValidation) => {
if (deepCompareConfig(moduleConfig.ambientLighting, data, true)) {
removeWorkingModuleConfig("ambientLighting");
removeChange({ type: "moduleConfig", variant: "ambientLighting" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "ambientLighting",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "ambientLighting" },
data,
moduleConfig.ambientLighting,
);
};
@@ -3,7 +3,6 @@ import {
type AudioValidation,
AudioValidationSchema,
} from "@app/validation/moduleConfig/audio.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -19,27 +18,20 @@ interface AudioModuleConfigProps {
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "audio" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AudioValidation) => {
if (deepCompareConfig(moduleConfig.audio, data, true)) {
removeWorkingModuleConfig("audio");
removeChange({ type: "moduleConfig", variant: "audio" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "audio",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "audio" },
data,
moduleConfig.audio,
);
};
@@ -3,7 +3,6 @@ import {
type CannedMessageValidation,
CannedMessageValidationSchema,
} from "@app/validation/moduleConfig/cannedMessage.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -22,27 +21,20 @@ export const CannedMessage = ({
}: CannedMessageModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: CannedMessageValidation) => {
if (deepCompareConfig(moduleConfig.cannedMessage, data, true)) {
removeWorkingModuleConfig("cannedMessage");
removeChange({ type: "moduleConfig", variant: "cannedMessage" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "cannedMessage",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "cannedMessage" },
data,
moduleConfig.cannedMessage,
);
};
@@ -3,7 +3,6 @@ import {
type DetectionSensorValidation,
DetectionSensorValidationSchema,
} from "@app/validation/moduleConfig/detectionSensor.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -22,27 +21,20 @@ export const DetectionSensor = ({
}: DetectionSensorModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: DetectionSensorValidation) => {
if (deepCompareConfig(moduleConfig.detectionSensor, data, true)) {
removeWorkingModuleConfig("detectionSensor");
removeChange({ type: "moduleConfig", variant: "detectionSensor" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "detectionSensor",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "detectionSensor" },
data,
moduleConfig.detectionSensor,
);
};
@@ -3,14 +3,12 @@ import {
type ExternalNotificationValidation,
ExternalNotificationValidationSchema,
} from "@app/validation/moduleConfig/externalNotification.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface ExternalNotificationModuleConfigProps {
@@ -22,27 +20,20 @@ export const ExternalNotification = ({
}: ExternalNotificationModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "externalNotification" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: ExternalNotificationValidation) => {
if (deepCompareConfig(moduleConfig.externalNotification, data, true)) {
removeWorkingModuleConfig("externalNotification");
removeChange({ type: "moduleConfig", variant: "externalNotification" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "externalNotification",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "externalNotification" },
data,
moduleConfig.externalNotification,
);
};
@@ -23,9 +23,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
const {
config,
moduleConfig,
setWorkingModuleConfig,
setChange,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
removeChange,
} = useDevice();
const { t } = useTranslation("moduleConfig");
@@ -39,17 +39,14 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
};
if (deepCompareConfig(moduleConfig.mqtt, payload, true)) {
removeWorkingModuleConfig("mqtt");
removeChange({ type: "moduleConfig", variant: "mqtt" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "mqtt",
value: payload,
},
}),
setChange(
{ type: "moduleConfig", variant: "mqtt" },
payload,
moduleConfig.mqtt,
);
};
@@ -3,14 +3,12 @@ import {
type NeighborInfoValidation,
NeighborInfoValidationSchema,
} from "@app/validation/moduleConfig/neighborInfo.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface NeighborInfoModuleConfigProps {
@@ -20,27 +18,20 @@ interface NeighborInfoModuleConfigProps {
export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "neighborInfo" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: NeighborInfoValidation) => {
if (deepCompareConfig(moduleConfig.neighborInfo, data, true)) {
removeWorkingModuleConfig("neighborInfo");
removeChange({ type: "moduleConfig", variant: "neighborInfo" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "neighborInfo",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "neighborInfo" },
data,
moduleConfig.neighborInfo,
);
};
@@ -3,14 +3,12 @@ import {
type PaxcounterValidation,
PaxcounterValidationSchema,
} from "@app/validation/moduleConfig/paxcounter.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface PaxcounterModuleConfigProps {
@@ -20,27 +18,20 @@ interface PaxcounterModuleConfigProps {
export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "paxcounter" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: PaxcounterValidation) => {
if (deepCompareConfig(moduleConfig.paxcounter, data, true)) {
removeWorkingModuleConfig("paxcounter");
removeChange({ type: "moduleConfig", variant: "paxcounter" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "paxcounter",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "paxcounter" },
data,
moduleConfig.paxcounter,
);
};
@@ -3,14 +3,12 @@ import {
type RangeTestValidation,
RangeTestValidationSchema,
} from "@app/validation/moduleConfig/rangeTest.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface RangeTestModuleConfigProps {
@@ -20,28 +18,21 @@ interface RangeTestModuleConfigProps {
export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "rangeTest" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: RangeTestValidation) => {
if (deepCompareConfig(moduleConfig.rangeTest, data, true)) {
removeWorkingModuleConfig("rangeTest");
removeChange({ type: "moduleConfig", variant: "rangeTest" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "rangeTest",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "rangeTest" },
data,
moduleConfig.rangeTest,
);
};
@@ -3,7 +3,6 @@ import {
type SerialValidation,
SerialValidationSchema,
} from "@app/validation/moduleConfig/serial.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -20,27 +19,20 @@ interface SerialModuleConfigProps {
export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "serial" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: SerialValidation) => {
if (deepCompareConfig(moduleConfig.serial, data, true)) {
removeWorkingModuleConfig("serial");
removeChange({ type: "moduleConfig", variant: "serial" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "serial",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "serial" },
data,
moduleConfig.serial,
);
};
@@ -3,14 +3,12 @@ import {
type StoreForwardValidation,
StoreForwardValidationSchema,
} from "@app/validation/moduleConfig/storeForward.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface StoreForwardModuleConfigProps {
@@ -20,27 +18,20 @@ interface StoreForwardModuleConfigProps {
export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "storeForward" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: StoreForwardValidation) => {
if (deepCompareConfig(moduleConfig.storeForward, data, true)) {
removeWorkingModuleConfig("storeForward");
removeChange({ type: "moduleConfig", variant: "storeForward" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "storeForward",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "storeForward" },
data,
moduleConfig.storeForward,
);
};
@@ -3,14 +3,12 @@ import {
type TelemetryValidation,
TelemetryValidationSchema,
} from "@app/validation/moduleConfig/telemetry.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface TelemetryModuleConfigProps {
@@ -20,27 +18,20 @@ interface TelemetryModuleConfigProps {
export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "telemetry" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TelemetryValidation) => {
if (deepCompareConfig(moduleConfig.telemetry, data, true)) {
removeWorkingModuleConfig("telemetry");
removeChange({ type: "moduleConfig", variant: "telemetry" });
return;
}
setWorkingModuleConfig(
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
payloadVariant: {
case: "telemetry",
value: data,
},
}),
setChange(
{ type: "moduleConfig", variant: "telemetry" },
data,
moduleConfig.telemetry,
);
};
@@ -3,7 +3,6 @@ import {
type BluetoothValidation,
BluetoothValidationSchema,
} from "@app/validation/config/bluetooth.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -19,24 +18,16 @@ interface BluetoothConfigProps {
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
useWaitForConfig({ configCase: "bluetooth" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const onSubmit = (data: BluetoothValidation) => {
if (deepCompareConfig(config.bluetooth, data, true)) {
removeWorkingConfig("bluetooth");
removeChange({ type: "config", variant: "bluetooth" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "bluetooth",
value: data,
},
}),
);
setChange({ type: "config", variant: "bluetooth" }, data, config.bluetooth);
};
return (
@@ -3,7 +3,6 @@ import {
type DeviceValidation,
DeviceValidationSchema,
} from "@app/validation/config/device.ts";
import { create } from "@bufbuild/protobuf";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import {
DynamicForm,
@@ -20,25 +19,17 @@ interface DeviceConfigProps {
export const Device = ({ onFormInit }: DeviceConfigProps) => {
useWaitForConfig({ configCase: "device" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const { validateRoleSelection } = useUnsafeRolesDialog();
const onSubmit = (data: DeviceValidation) => {
if (deepCompareConfig(config.device, data, true)) {
removeWorkingConfig("device");
removeChange({ type: "config", variant: "device" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "device",
value: data,
},
}),
);
setChange({ type: "config", variant: "device" }, data, config.device);
};
return (
@@ -3,7 +3,6 @@ import {
type DisplayValidation,
DisplayValidationSchema,
} from "@app/validation/config/display.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -18,24 +17,16 @@ interface DisplayConfigProps {
}
export const Display = ({ onFormInit }: DisplayConfigProps) => {
useWaitForConfig({ configCase: "display" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const onSubmit = (data: DisplayValidation) => {
if (deepCompareConfig(config.display, data, true)) {
removeWorkingConfig("display");
removeChange({ type: "config", variant: "display" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "display",
value: data,
},
}),
);
setChange({ type: "config", variant: "display" }, data, config.display);
};
return (
@@ -3,7 +3,6 @@ import {
type LoRaValidation,
LoRaValidationSchema,
} from "@app/validation/config/lora.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -19,24 +18,16 @@ interface LoRaConfigProps {
export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
useWaitForConfig({ configCase: "lora" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const onSubmit = (data: LoRaValidation) => {
if (deepCompareConfig(config.lora, data, true)) {
removeWorkingConfig("lora");
removeChange({ type: "config", variant: "lora" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "lora",
value: data,
},
}),
);
setChange({ type: "config", variant: "lora" }, data, config.lora);
};
return (
@@ -23,9 +23,8 @@ interface NetworkConfigProps {
export const Network = ({ onFormInit }: NetworkConfigProps) => {
useWaitForConfig({ configCase: "network" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { config, setChange, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const networkConfig = getEffectiveConfig("network");
@@ -44,18 +43,11 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
};
if (deepCompareConfig(config.network, payload, true)) {
removeWorkingConfig("network");
removeChange({ type: "config", variant: "network" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "network",
value: payload,
},
}),
);
setChange({ type: "config", variant: "network" }, payload, config.network);
};
return (
<DynamicForm<NetworkValidation>
@@ -3,7 +3,6 @@ import {
type PositionValidation,
PositionValidationSchema,
} from "@app/validation/config/position.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
@@ -24,26 +23,23 @@ interface PositionConfigProps {
export const Position = ({ onFormInit }: PositionConfigProps) => {
useWaitForConfig({ configCase: "position" });
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { setChange, config, getEffectiveConfig, removeChange } = useDevice();
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
getEffectiveConfig("position")?.positionFlags ?? 0,
);
const { t } = useTranslation("deviceConfig");
const { t } = useTranslation("config");
const onSubmit = (data: PositionValidation) => {
if (deepCompareConfig(config.position, data, true)) {
removeWorkingConfig("position");
const payload = { ...data, positionFlags: flagsValue };
if (deepCompareConfig(config.position, payload, true)) {
removeChange({ type: "config", variant: "position" });
return;
}
return setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "position",
value: { ...data, positionFlags: flagsValue },
},
}),
return setChange(
{ type: "config", variant: "position" },
payload,
config.position,
);
};
@@ -3,14 +3,12 @@ import {
type PowerValidation,
PowerValidationSchema,
} from "@app/validation/config/power.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface PowerConfigProps {
@@ -19,24 +17,16 @@ interface PowerConfigProps {
export const Power = ({ onFormInit }: PowerConfigProps) => {
useWaitForConfig({ configCase: "power" });
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { setChange, config, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
const onSubmit = (data: PowerValidation) => {
if (deepCompareConfig(config.power, data, true)) {
removeWorkingConfig("power");
removeChange({ type: "config", variant: "power" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "power",
value: data,
},
}),
);
setChange({ type: "config", variant: "power" }, data, config.power);
};
return (
@@ -4,7 +4,6 @@ import {
type RawSecurity,
RawSecuritySchema,
} from "@app/validation/config/security.ts";
import { create } from "@bufbuild/protobuf";
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
@@ -15,7 +14,6 @@ import {
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { Protobuf } from "@meshtastic/core";
import { fromByteArray, toByteArray } from "base64-js";
import { useEffect, useState } from "react";
import { type DefaultValues, useForm } from "react-hook-form";
@@ -27,15 +25,10 @@ interface SecurityConfigProps {
export const Security = ({ onFormInit }: SecurityConfigProps) => {
useWaitForConfig({ configCase: "security" });
const {
config,
setWorkingConfig,
setDialogOpen,
getEffectiveConfig,
removeWorkingConfig,
} = useDevice();
const { config, setChange, setDialogOpen, getEffectiveConfig, removeChange } =
useDevice();
const { t } = useTranslation("deviceConfig");
const { t } = useTranslation("config");
const defaultConfig = config.security;
const defaultValues = {
@@ -103,17 +96,14 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
};
if (deepCompareConfig(config.security, payload, true)) {
removeWorkingConfig("security");
removeChange({ type: "config", variant: "security" });
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "security",
value: payload,
},
}),
setChange(
{ type: "config", variant: "security" },
payload,
config.security,
);
};
@@ -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"),
},
],
},
]}
/>
);
};
+2 -2
View File
@@ -108,9 +108,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
},
{ name: t("navigation.map"), icon: MapIcon, page: "map" },
{
name: t("navigation.config"),
name: t("navigation.settings"),
icon: SettingsIcon,
page: "config",
page: "settings",
},
{
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
@@ -21,7 +21,7 @@ export const SidebarSection = ({
as="h3"
className={cn(
"mb-2",
"uppercase tracking-wider text-md",
"capitalize tracking-wider text-sm",
"transition-all duration-300 ease-in-out",
"whitespace-nowrap overflow-hidden",
isCollapsed