Merge pull request #439 from danditomaso/fix/validate-ble-pin
fix: Add 6-digit BLE PIN validation and error management
This commit is contained in:
@@ -13,6 +13,7 @@ import { Controller, type FieldValues } from "react-hook-form";
|
|||||||
|
|
||||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "select";
|
type: "select";
|
||||||
|
selectChange?: (e: string) => void;
|
||||||
properties: BaseFormBuilderProps<T>["properties"] & {
|
properties: BaseFormBuilderProps<T>["properties"] & {
|
||||||
enumValue: {
|
enumValue: {
|
||||||
[s: string]: string | number;
|
[s: string]: string | number;
|
||||||
@@ -40,7 +41,10 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(e) => onChange(Number.parseInt(e))}
|
onValueChange={(e) => {
|
||||||
|
if (field.selectChange) field.selectChange(e);
|
||||||
|
onChange(Number.parseInt(e));
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={value?.toString()}
|
value={value?.toString()}
|
||||||
{...remainingProperties}
|
{...remainingProperties}
|
||||||
|
|||||||
@@ -1,23 +1,57 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Bluetooth = (): JSX.Element => {
|
export const Bluetooth = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
const [bluetoothValidationText, setBluetoothValidationText] =
|
const {
|
||||||
useState<string>();
|
hasErrors,
|
||||||
|
getErrorMessage,
|
||||||
|
hasFieldError,
|
||||||
|
addError,
|
||||||
|
removeError,
|
||||||
|
clearErrors,
|
||||||
|
} = useAppStore();
|
||||||
|
|
||||||
|
const [bluetoothPin, setBluetoothPin] = useState(
|
||||||
|
config?.bluetooth?.fixedPin.toString() ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateBluetoothPin = (pin: string) => {
|
||||||
|
// if empty show error they need a pin set
|
||||||
|
if (pin === "") {
|
||||||
|
return addError("fixedPin", "Bluetooth Pin is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear any existing errors
|
||||||
|
clearErrors();
|
||||||
|
|
||||||
|
// if it starts with 0 show error
|
||||||
|
if (pin[0] === "0") {
|
||||||
|
return addError("fixedPin", "Bluetooth Pin cannot start with 0");
|
||||||
|
}
|
||||||
|
// if it's not 6 digits show error
|
||||||
|
if (pin.length < 6) {
|
||||||
|
return addError("fixedPin", "Pin must be 6 digits");
|
||||||
|
}
|
||||||
|
|
||||||
|
removeError("fixedPin");
|
||||||
|
};
|
||||||
|
|
||||||
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.value[0] === "0") {
|
const numericValue = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||||||
setBluetoothValidationText("Bluetooth Pin cannot start with 0.");
|
setBluetoothPin(numericValue);
|
||||||
} else {
|
validateBluetoothPin(numericValue);
|
||||||
setBluetoothValidationText("");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: BluetoothValidation) => {
|
const onSubmit = (data: BluetoothValidation) => {
|
||||||
|
if (hasErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setWorkingConfig(
|
||||||
new Protobuf.Config.Config({
|
new Protobuf.Config.Config({
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
@@ -48,6 +82,12 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
name: "mode",
|
name: "mode",
|
||||||
label: "Pairing mode",
|
label: "Pairing mode",
|
||||||
description: "Pin selection behaviour.",
|
description: "Pin selection behaviour.",
|
||||||
|
selectChange: (e) => {
|
||||||
|
if (e !== "1") {
|
||||||
|
setBluetoothPin("");
|
||||||
|
removeError("fixedPin");
|
||||||
|
}
|
||||||
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
@@ -63,7 +103,9 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
name: "fixedPin",
|
name: "fixedPin",
|
||||||
label: "Pin",
|
label: "Pin",
|
||||||
description: "Pin to use when pairing",
|
description: "Pin to use when pairing",
|
||||||
validationText: bluetoothValidationText,
|
validationText: hasFieldError("fixedPin")
|
||||||
|
? getErrorMessage("fixedPin")
|
||||||
|
: "",
|
||||||
inputChange: bluetoothPinChangeEvent,
|
inputChange: bluetoothPinChangeEvent,
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
@@ -77,7 +119,9 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
properties: {},
|
properties: {
|
||||||
|
value: bluetoothPin,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
checked ? setHTTPS(true) : setHTTPS(false);
|
checked ? setHTTPS(true) : setHTTPS(false);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// label="Use TLS"
|
// label="Use TLS"
|
||||||
// description="Description"
|
// description="Description"
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from "@app/core/utils/cn.ts";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
import Footer from "./UI/Footer";
|
import Footer from "./UI/Footer";
|
||||||
|
import { Spinner } from "./UI/Spinner";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,6 +11,8 @@ export interface PageLayoutProps {
|
|||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
iconClasses?: string;
|
iconClasses?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ export const PageLayout = ({
|
|||||||
noPadding,
|
noPadding,
|
||||||
actions,
|
actions,
|
||||||
children,
|
children,
|
||||||
}: PageLayoutProps): JSX.Element => {
|
}: PageLayoutProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-full w-full flex-col">
|
<div className="relative flex h-full w-full flex-col">
|
||||||
@@ -33,14 +36,22 @@ export const PageLayout = ({
|
|||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center">
|
||||||
<span className="w-full text-lg font-medium">{label}</span>
|
<span className="w-full text-lg font-medium">{label}</span>
|
||||||
<div className="flex justify-end space-x-4">
|
<div className="flex justify-end space-x-4">
|
||||||
{actions?.map((action, index) => (
|
{actions?.map((action) => (
|
||||||
<button
|
<button
|
||||||
key={action.icon.displayName}
|
key={action.icon.displayName}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={action?.disabled}
|
||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon className={action.iconClasses} />
|
{action?.isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<action.icon
|
||||||
|
className={action.iconClasses}
|
||||||
|
aria-disabled={action.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { cn } from "@app/core/utils/cn";
|
||||||
|
|
||||||
|
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: "h-4 w-4",
|
||||||
|
md: "h-8 w-8",
|
||||||
|
lg: "h-12 w-12",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Spinner({ className, size = "md", ...props }: SpinnerProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center fade-in-50 fade-out-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={cn("animate-spin-slow stroke-current", sizeClasses[size])}
|
||||||
|
role="img"
|
||||||
|
aria-label="Loading spinner"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="128"
|
||||||
|
y1="32"
|
||||||
|
x2="128"
|
||||||
|
y2="64"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="195.9"
|
||||||
|
y1="60.1"
|
||||||
|
x2="173.3"
|
||||||
|
y2="82.7"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="224"
|
||||||
|
y1="128"
|
||||||
|
x2="192"
|
||||||
|
y2="128"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="195.9"
|
||||||
|
y1="195.9"
|
||||||
|
x2="173.3"
|
||||||
|
y2="173.3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="128"
|
||||||
|
y1="224"
|
||||||
|
x2="128"
|
||||||
|
y2="192"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="60.1"
|
||||||
|
y1="195.9"
|
||||||
|
x2="82.7"
|
||||||
|
y2="173.3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="32"
|
||||||
|
y1="128"
|
||||||
|
x2="64"
|
||||||
|
y2="128"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="60.1"
|
||||||
|
y1="60.1"
|
||||||
|
x2="82.7"
|
||||||
|
y2="82.7"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="24"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ export type AccentColor =
|
|||||||
| "purple"
|
| "purple"
|
||||||
| "pink";
|
| "pink";
|
||||||
|
|
||||||
|
interface ErrorState {
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
selectedDevice: number;
|
selectedDevice: number;
|
||||||
devices: {
|
devices: {
|
||||||
@@ -33,11 +38,11 @@ interface AppState {
|
|||||||
nodeNumDetails: number;
|
nodeNumDetails: number;
|
||||||
activeChat: number;
|
activeChat: number;
|
||||||
chatType: "broadcast" | "direct";
|
chatType: "broadcast" | "direct";
|
||||||
|
errors: ErrorState[];
|
||||||
|
|
||||||
setRasterSources: (sources: RasterSource[]) => void;
|
setRasterSources: (sources: RasterSource[]) => void;
|
||||||
addRasterSource: (source: RasterSource) => void;
|
addRasterSource: (source: RasterSource) => void;
|
||||||
removeRasterSource: (index: number) => void;
|
removeRasterSource: (index: number) => void;
|
||||||
|
|
||||||
setSelectedDevice: (deviceId: number) => void;
|
setSelectedDevice: (deviceId: number) => void;
|
||||||
addDevice: (device: { id: number; num: number }) => void;
|
addDevice: (device: { id: number; num: number }) => void;
|
||||||
removeDevice: (deviceId: number) => void;
|
removeDevice: (deviceId: number) => void;
|
||||||
@@ -49,9 +54,18 @@ interface AppState {
|
|||||||
setNodeNumDetails: (nodeNum: number) => void;
|
setNodeNumDetails: (nodeNum: number) => void;
|
||||||
setActiveChat: (chat: number) => void;
|
setActiveChat: (chat: number) => void;
|
||||||
setChatType: (type: "broadcast" | "direct") => void;
|
setChatType: (type: "broadcast" | "direct") => void;
|
||||||
|
|
||||||
|
// Error management
|
||||||
|
hasErrors: () => boolean;
|
||||||
|
getErrorMessage: (field: string) => string | undefined;
|
||||||
|
hasFieldError: (field: string) => boolean;
|
||||||
|
addError: (field: string, message: string) => void;
|
||||||
|
removeError: (field: string) => void;
|
||||||
|
clearErrors: () => void;
|
||||||
|
setNewErrors: (newErrors: ErrorState[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = create<AppState>()((set) => ({
|
export const useAppStore = create<AppState>()((set, get) => ({
|
||||||
selectedDevice: 0,
|
selectedDevice: 0,
|
||||||
devices: [],
|
devices: [],
|
||||||
currentPage: "messages",
|
currentPage: "messages",
|
||||||
@@ -67,6 +81,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
nodeNumDetails: 0,
|
nodeNumDetails: 0,
|
||||||
activeChat: Types.ChannelNumber.Primary,
|
activeChat: Types.ChannelNumber.Primary,
|
||||||
chatType: "broadcast",
|
chatType: "broadcast",
|
||||||
|
errors: [],
|
||||||
|
|
||||||
setRasterSources: (sources: RasterSource[]) => {
|
setRasterSources: (sources: RasterSource[]) => {
|
||||||
set(
|
set(
|
||||||
@@ -146,4 +161,47 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
set(() => ({
|
set(() => ({
|
||||||
chatType: type,
|
chatType: type,
|
||||||
})),
|
})),
|
||||||
|
hasErrors: () => {
|
||||||
|
const state = get();
|
||||||
|
return state.errors.length > 0;
|
||||||
|
},
|
||||||
|
getErrorMessage: (field: string) => {
|
||||||
|
const state = get();
|
||||||
|
return state.errors.find((err) => err.field === field)?.message;
|
||||||
|
},
|
||||||
|
hasFieldError: (field: string) => {
|
||||||
|
const state = get();
|
||||||
|
return state.errors.some((err) => err.field === field);
|
||||||
|
},
|
||||||
|
addError: (field: string, message: string) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.errors = [
|
||||||
|
...draft.errors.filter((e) => e.field !== field),
|
||||||
|
{ field, message },
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeError: (field: string) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.errors = draft.errors.filter((e) => e.field !== field);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
clearErrors: () => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.errors = [];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setNewErrors: (newErrors: ErrorState[]) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.errors = newErrors;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -99,3 +99,13 @@
|
|||||||
img {
|
img {
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin-slower {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin-slow {
|
||||||
|
animation: spin-slower 2s linear infinite;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@components/UI/Tabs.tsx";
|
} from "@components/UI/Tabs.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
export const DeviceConfig = (): JSX.Element => {
|
export const DeviceConfig = () => {
|
||||||
const { metadata } = useDevice();
|
const { metadata } = useDevice();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
|||||||
+55
-26
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import { PageLayout } from "@components/PageLayout.tsx";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.tsx";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
@@ -6,15 +7,62 @@ import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
|||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
|
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
|
||||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
|
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
|
||||||
import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react";
|
import { BoxesIcon, SaveIcon, SaveOff, SettingsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const ConfigPage = (): JSX.Element => {
|
const ConfigPage = () => {
|
||||||
const { workingConfig, workingModuleConfig, connection } = useDevice();
|
const { workingConfig, workingModuleConfig, connection } = useDevice();
|
||||||
|
const { hasErrors } = useAppStore();
|
||||||
const [activeConfigSection, setActiveConfigSection] = useState<
|
const [activeConfigSection, setActiveConfigSection] = useState<
|
||||||
"device" | "module"
|
"device" | "module"
|
||||||
>("device");
|
>("device");
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const isError = hasErrors();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (hasErrors()) {
|
||||||
|
return toast({
|
||||||
|
title: "Config Errors Exist",
|
||||||
|
description: "Please fix the configuration errors before saving.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
if (activeConfigSection === "device") {
|
||||||
|
await Promise.all(
|
||||||
|
workingConfig.map((config) =>
|
||||||
|
connection?.setConfig(config).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Saving Config",
|
||||||
|
description: `The configuration change ${config.payloadVariant.case} has been saved.`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await Promise.all(
|
||||||
|
workingModuleConfig.map((moduleConfig) =>
|
||||||
|
connection?.setModuleConfig(moduleConfig).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Saving Config",
|
||||||
|
description: `The configuration change ${moduleConfig.payloadVariant.case} has been saved.`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await connection?.commitEditSettings();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error Saving Config",
|
||||||
|
description: "An error occurred while saving the configuration.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -40,30 +88,11 @@ const ConfigPage = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
icon: SaveIcon,
|
icon: isError ? SaveOff : SaveIcon,
|
||||||
async onClick() {
|
isLoading: isSaving,
|
||||||
if (activeConfigSection === "device") {
|
disabled: isSaving,
|
||||||
workingConfig.map(
|
iconClasses: isError ? "text-red-400 cursor-not-allowed" : "",
|
||||||
async (config) =>
|
onClick: handleSave,
|
||||||
await connection?.setConfig(config).then(() =>
|
|
||||||
toast({
|
|
||||||
title: `Config ${config.payloadVariant.case} saved`,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
workingModuleConfig.map(
|
|
||||||
async (moduleConfig) =>
|
|
||||||
await connection?.setModuleConfig(moduleConfig).then(() =>
|
|
||||||
toast({
|
|
||||||
title: `Config ${moduleConfig.payloadVariant.case} saved`,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection?.commitEditSettings();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user