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:
@@ -1,95 +0,0 @@
|
||||
import { Channel } from "@app/components/PageComponents/ChannelConfig/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 ChannelConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { channels, getWorkingChannelConfig, setDialogOpen } = useDevice();
|
||||
const { t } = useTranslation("channels");
|
||||
|
||||
const allChannels = Array.from(channels.values());
|
||||
const flags = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
allChannels.map((channel) => [
|
||||
channel.index,
|
||||
getWorkingChannelConfig(channel.index),
|
||||
]),
|
||||
),
|
||||
[allChannels, getWorkingChannelConfig],
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { Protobuf, Types } from "@meshtastic/core";
|
||||
import { getChannelName } from "@pages/Config/ChannelConfig.tsx";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import {
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getChannelName } from "../components/PageComponents/Channels/Channels.tsx";
|
||||
|
||||
type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number };
|
||||
|
||||
|
||||
+26
-25
@@ -1,11 +1,10 @@
|
||||
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
|
||||
import { Device } from "@components/PageComponents/Config/Device/index.tsx";
|
||||
import { Display } from "@components/PageComponents/Config/Display.tsx";
|
||||
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
|
||||
import { Network } from "@components/PageComponents/Config/Network/index.tsx";
|
||||
import { Position } from "@components/PageComponents/Config/Position.tsx";
|
||||
import { Power } from "@components/PageComponents/Config/Power.tsx";
|
||||
import { Security } from "@components/PageComponents/Config/Security/Security.tsx";
|
||||
import { Bluetooth } from "@components/PageComponents/Settings/Bluetooth.tsx";
|
||||
import { Device } from "@components/PageComponents/Settings/Device/index.tsx";
|
||||
import { Display } from "@components/PageComponents/Settings/Display.tsx";
|
||||
import { Network } from "@components/PageComponents/Settings/Network/index.tsx";
|
||||
import { Position } from "@components/PageComponents/Settings/Position.tsx";
|
||||
import { Power } from "@components/PageComponents/Settings/Power.tsx";
|
||||
import { User } from "@components/PageComponents/Settings/User.tsx";
|
||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
@@ -23,21 +22,25 @@ interface ConfigProps {
|
||||
}
|
||||
|
||||
type TabItem = {
|
||||
case: ValidConfigType;
|
||||
case: ValidConfigType | "user";
|
||||
label: string;
|
||||
element: ComponentType<ConfigProps>;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { getWorkingConfig } = useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
const { hasConfigChange, hasUserChange } = useDevice();
|
||||
const { t } = useTranslation("config");
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
case: "user",
|
||||
label: t("page.tabUser"),
|
||||
element: User,
|
||||
},
|
||||
{
|
||||
case: "device",
|
||||
label: t("page.tabDevice"),
|
||||
element: Device,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
case: "position",
|
||||
@@ -59,30 +62,28 @@ export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
||||
label: t("page.tabDisplay"),
|
||||
element: Display,
|
||||
},
|
||||
{
|
||||
case: "lora",
|
||||
label: t("page.tabLora"),
|
||||
element: LoRa,
|
||||
},
|
||||
{
|
||||
case: "bluetooth",
|
||||
label: t("page.tabBluetooth"),
|
||||
element: Bluetooth,
|
||||
},
|
||||
{
|
||||
case: "security",
|
||||
label: t("page.tabSecurity"),
|
||||
element: Security,
|
||||
},
|
||||
] as const;
|
||||
|
||||
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 (
|
||||
<Tabs defaultValue={t("page.tabDevice")}>
|
||||
<Tabs defaultValue={t("page.tabUser")}>
|
||||
<TabsList className="w-full dark:bg-slate-700">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
+3
-3
@@ -34,7 +34,7 @@ type TabItem = {
|
||||
};
|
||||
|
||||
export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { getWorkingModuleConfig } = useDevice();
|
||||
const { hasModuleConfigChange } = useDevice();
|
||||
const { t } = useTranslation("moduleConfig");
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
@@ -97,8 +97,8 @@ export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
||||
|
||||
const flags = useMemo(
|
||||
() =>
|
||||
new Map(tabs.map((tab) => [tab.case, getWorkingModuleConfig(tab.case)])),
|
||||
[tabs, getWorkingModuleConfig],
|
||||
new Map(tabs.map((tab) => [tab.case, hasModuleConfigChange(tab.case)])),
|
||||
[tabs, hasModuleConfigChange],
|
||||
);
|
||||
|
||||
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 { Sidebar } from "@components/Sidebar.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 { useDevice } from "@core/stores";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { ChannelConfig } from "@pages/Config/ChannelConfig.tsx";
|
||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
|
||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
|
||||
import { DeviceConfig } from "@pages/Settings/DeviceConfig.tsx";
|
||||
import { ModuleConfig } from "@pages/Settings/ModuleConfig.tsx";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import {
|
||||
BoxesIcon,
|
||||
LayersIcon,
|
||||
RadioTowerIcon,
|
||||
RefreshCwIcon,
|
||||
RouterIcon,
|
||||
SaveIcon,
|
||||
SaveOff,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RadioConfig } from "./RadioConfig.tsx";
|
||||
|
||||
const ConfigPage = () => {
|
||||
const {
|
||||
workingConfig,
|
||||
workingModuleConfig,
|
||||
workingChannelConfig,
|
||||
getAllConfigChanges,
|
||||
getAllModuleConfigChanges,
|
||||
getAllChannelChanges,
|
||||
connection,
|
||||
removeWorkingConfig,
|
||||
removeWorkingModuleConfig,
|
||||
removeWorkingChannelConfig,
|
||||
clearAllChanges,
|
||||
setConfig,
|
||||
setModuleConfig,
|
||||
addChannel,
|
||||
getConfigChangeCount,
|
||||
getModuleConfigChangeCount,
|
||||
getChannelChangeCount,
|
||||
} = useDevice();
|
||||
|
||||
const [activeConfigSection, setActiveConfigSection] = useState<
|
||||
"device" | "module" | "channel"
|
||||
>("device");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [rhfState, setRhfState] = useState({ isDirty: false, isValid: true });
|
||||
const unsubRef = useRef<(() => void) | null>(null);
|
||||
const [formMethods, setFormMethods] = useState<UseFormReturn | null>(null);
|
||||
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(
|
||||
<T extends FieldValues>(methods: UseFormReturn<T>) => {
|
||||
@@ -69,7 +110,6 @@ const ConfigPage = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
// Cleanup subscription on unmount
|
||||
useEffect(() => {
|
||||
return () => unsubRef.current?.();
|
||||
}, []);
|
||||
@@ -78,9 +118,12 @@ const ConfigPage = () => {
|
||||
setIsSaving(true);
|
||||
|
||||
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(
|
||||
workingChannelConfig.map((channel) =>
|
||||
channelChanges.map((channel) =>
|
||||
connection?.setChannel(channel).then(() => {
|
||||
toast({
|
||||
title: t("toast.savedChannel.title", {
|
||||
@@ -93,7 +136,7 @@ const ConfigPage = () => {
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
workingConfig.map((newConfig) =>
|
||||
configChanges.map((newConfig) =>
|
||||
connection?.setConfig(newConfig).then(() => {
|
||||
toast({
|
||||
title: t("toast.saveSuccess.title"),
|
||||
@@ -106,7 +149,7 @@ const ConfigPage = () => {
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
workingModuleConfig.map((newModuleConfig) =>
|
||||
moduleConfigChanges.map((newModuleConfig) =>
|
||||
connection?.setModuleConfig(newModuleConfig).then(() =>
|
||||
toast({
|
||||
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();
|
||||
}
|
||||
|
||||
workingChannelConfig.forEach((newChannel) => {
|
||||
channelChanges.forEach((newChannel) => {
|
||||
addChannel(newChannel);
|
||||
});
|
||||
workingConfig.forEach((newConfig) => {
|
||||
configChanges.forEach((newConfig) => {
|
||||
setConfig(newConfig);
|
||||
});
|
||||
workingModuleConfig.forEach((newModuleConfig) => {
|
||||
moduleConfigChanges.forEach((newModuleConfig) => {
|
||||
setModuleConfig(newModuleConfig);
|
||||
});
|
||||
|
||||
removeWorkingChannelConfig();
|
||||
removeWorkingConfig();
|
||||
removeWorkingModuleConfig();
|
||||
clearAllChanges();
|
||||
|
||||
if (formMethods) {
|
||||
formMethods.reset(formMethods.getValues(), {
|
||||
@@ -144,7 +185,6 @@ const ConfigPage = () => {
|
||||
keepValues: true,
|
||||
});
|
||||
|
||||
// Force RHF to re-validate and emit state
|
||||
formMethods.trigger();
|
||||
}
|
||||
} catch (_error) {
|
||||
@@ -162,77 +202,49 @@ const ConfigPage = () => {
|
||||
}, [
|
||||
toast,
|
||||
t,
|
||||
workingConfig,
|
||||
getAllConfigChanges,
|
||||
connection,
|
||||
workingModuleConfig,
|
||||
workingChannelConfig,
|
||||
getAllModuleConfigChanges,
|
||||
getAllChannelChanges,
|
||||
formMethods,
|
||||
addChannel,
|
||||
setConfig,
|
||||
setModuleConfig,
|
||||
removeWorkingConfig,
|
||||
removeWorkingModuleConfig,
|
||||
removeWorkingChannelConfig,
|
||||
clearAllChanges,
|
||||
]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (formMethods) {
|
||||
formMethods.reset();
|
||||
}
|
||||
removeWorkingChannelConfig();
|
||||
removeWorkingConfig();
|
||||
removeWorkingModuleConfig();
|
||||
}, [
|
||||
formMethods,
|
||||
removeWorkingConfig,
|
||||
removeWorkingModuleConfig,
|
||||
removeWorkingChannelConfig,
|
||||
]);
|
||||
clearAllChanges();
|
||||
}, [formMethods, clearAllChanges]);
|
||||
|
||||
const leftSidebar = useMemo(
|
||||
() => (
|
||||
<Sidebar>
|
||||
<SidebarSection label={t("sidebar.label")} className="py-2 px-0">
|
||||
<SidebarButton
|
||||
label={t("navigation.radioConfig")}
|
||||
active={activeConfigSection === "device"}
|
||||
onClick={() => setActiveConfigSection("device")}
|
||||
Icon={SettingsIcon}
|
||||
isDirty={workingConfig.length > 0}
|
||||
count={workingConfig.length}
|
||||
/>
|
||||
<SidebarButton
|
||||
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}
|
||||
/>
|
||||
{sections.map((section) => (
|
||||
<SidebarButton
|
||||
key={section.key}
|
||||
label={section.label}
|
||||
active={activeSection?.key === section.key}
|
||||
onClick={() => navigate({ to: section.route.to })}
|
||||
Icon={section.icon}
|
||||
isDirty={section.changeCount > 0}
|
||||
count={section.changeCount}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
</Sidebar>
|
||||
),
|
||||
[
|
||||
activeConfigSection,
|
||||
workingConfig,
|
||||
workingModuleConfig,
|
||||
workingChannelConfig,
|
||||
t,
|
||||
],
|
||||
[sections, activeSection?.key, navigate, t],
|
||||
);
|
||||
|
||||
const hasDrafts =
|
||||
workingConfig.length > 0 ||
|
||||
workingModuleConfig.length > 0 ||
|
||||
workingChannelConfig.length > 0;
|
||||
getConfigChangeCount() > 0 ||
|
||||
getModuleConfigChangeCount() > 0 ||
|
||||
getChannelChangeCount() > 0;
|
||||
const hasPending = hasDrafts || rhfState.isDirty;
|
||||
const buttonOpacity = hasPending ? "opacity-100" : "opacity-0";
|
||||
const saveDisabled = isSaving || !rhfState.isValid || !hasPending;
|
||||
@@ -291,26 +303,16 @@ const ConfigPage = () => {
|
||||
],
|
||||
);
|
||||
|
||||
const ActiveComponent = activeSection?.component;
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
contentClassName="overflow-auto"
|
||||
leftBar={leftSidebar}
|
||||
label={
|
||||
activeConfigSection === "device"
|
||||
? t("navigation.radioConfig")
|
||||
: activeConfigSection === "module"
|
||||
? t("navigation.moduleConfig")
|
||||
: t("navigation.channelConfig")
|
||||
}
|
||||
label={activeSection?.label ?? ""}
|
||||
actions={actions}
|
||||
>
|
||||
{activeConfigSection === "device" ? (
|
||||
<DeviceConfig onFormInit={onFormInit} />
|
||||
) : activeConfigSection === "module" ? (
|
||||
<ModuleConfig onFormInit={onFormInit} />
|
||||
) : (
|
||||
<ChannelConfig onFormInit={onFormInit} />
|
||||
)}
|
||||
<ActiveComponent onFormInit={onFormInit} />
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user