fix: resolve lint warnings/errors and apply formatting (#1024)

* fix: resolve lint warnings/errors and apply formatting

Fix 10 oxlint issues (2 errors, 8 warnings):
- Remove unused catch parameters in Security.tsx and ImportDialog.tsx
- Remove stray expression in Generator.tsx
- Add eslint-disable for debounced useCallback in FilterControl.tsx
- Remove unnecessary deps (resolveDB, store) in bindStoreToDevice.ts
- Prefix unused variant param in AppSidebar.tsx
- Memoize tabs arrays in DeviceConfig, RadioConfig, ModuleConfig
- Fix channels type in RadioConfig TabItem

Also applies oxfmt formatting across all files.

* updating lock file

* update protobuf package

* fix: regenerate pnpm-lock.yaml and exclude jsr protobufs from minimumReleaseAge

* prevented http card from always being polled

* updated pnpm config fiile

* updated actions

* removed biome config and lint system leftovers

* updating protobuf package
This commit is contained in:
Dan Ditomaso
2026-03-11 21:54:25 -04:00
committed by GitHub
parent a90df07ebe
commit c729d3b25e
256 changed files with 2164 additions and 4362 deletions
+1 -2
View File
@@ -59,6 +59,5 @@ export function App() {
</div>
</DeviceWrapper>
</ErrorBoundary>
// </ThemeProvider>
);
); // </ThemeProvider>
}
+1 -3
View File
@@ -8,8 +8,6 @@ export interface DeviceWrapperProps {
export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
return (
<CurrentDeviceContext.Provider value={{ deviceId }}>
{children}
</CurrentDeviceContext.Provider>
<CurrentDeviceContext.Provider value={{ deviceId }}>{children}</CurrentDeviceContext.Provider>
);
};
@@ -1,21 +1,7 @@
import { vi } from "vitest";
vi.mock("@components/UI/Checkbox.tsx", () => ({
Checkbox: ({
id,
checked,
onChange,
}: {
id: string;
checked: boolean;
onChange: () => void;
}) => (
<input
data-testid="checkbox"
type="checkbox"
id={id}
checked={checked}
onChange={onChange}
/>
Checkbox: ({ id, checked, onChange }: { id: string; checked: boolean; onChange: () => void }) => (
<input data-testid="checkbox" type="checkbox" id={id} checked={checked} onChange={onChange} />
),
}));
+2 -10
View File
@@ -1,9 +1,4 @@
import {
BatteryFullIcon,
BatteryLowIcon,
BatteryMediumIcon,
PlugZapIcon,
} from "lucide-react";
import { BatteryFullIcon, BatteryLowIcon, BatteryMediumIcon, PlugZapIcon } from "lucide-react";
import type React from "react";
import { useTranslation } from "react-i18next";
import type { DeviceMetrics } from "./types.ts";
@@ -43,10 +38,7 @@ export const getBatteryStatus = (level: number): BatteryStatusKey => {
const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
const { t } = useTranslation();
if (
deviceMetrics?.batteryLevel === undefined ||
deviceMetrics?.batteryLevel === null
) {
if (deviceMetrics?.batteryLevel === undefined || deviceMetrics?.batteryLevel === null) {
return null;
}
@@ -8,12 +8,7 @@ import {
CommandList,
} from "@components/UI/Command.tsx";
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
import {
useAppStore,
useDevice,
useDeviceStore,
useNodeDB,
} from "@core/stores";
import { useAppStore, useDevice, useDeviceStore, useNodeDB } from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { useNavigate } from "@tanstack/react-router";
import { useCommandState } from "cmdk";
@@ -62,12 +57,8 @@ export interface SubItem {
}
export const CommandPalette = () => {
const {
commandPaletteOpen,
setCommandPaletteOpen,
setConnectDialogOpen,
setSelectedDevice,
} = useAppStore();
const { commandPaletteOpen, setCommandPaletteOpen, setConnectDialogOpen, setSelectedDevice } =
useAppStore();
const { getDevices } = useDeviceStore();
const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
@@ -123,9 +114,7 @@ export const CommandPalette = () => {
label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({
label:
getNode(device.hardware.myNodeNum)?.user?.longName ??
t("unknown.shortName"),
label: getNode(device.hardware.myNodeNum)?.user?.longName ?? t("unknown.shortName"),
icon: <Avatar nodeNum={device.hardware.myNodeNum} />,
action() {
setSelectedDevice(device.id);
@@ -268,10 +257,7 @@ export const CommandPalette = () => {
}, [setCommandPaletteOpen]);
return (
<CommandDialog
open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen}
>
<CommandDialog open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
<CommandInput placeholder={t("search.commandPalette")} />
<CommandList>
<CommandEmpty>{t("emptyState")}</CommandEmpty>
@@ -107,9 +107,7 @@ export const DeviceInfoPanel = ({
label: t("batteryVoltage.title"),
icon: ZapIcon,
value:
voltage !== undefined
? `${voltage?.toPrecision(3)} V`
: t("unknown.notAvailable", "N/A"),
voltage !== undefined ? `${voltage?.toPrecision(3)} V` : t("unknown.notAvailable", "N/A"),
},
{
id: "firmware",
@@ -207,9 +205,7 @@ export const DeviceInfoPanel = ({
</button>
)}
{!isCollapsed && (
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
)}
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />}
<div
className={cn(
@@ -241,9 +237,7 @@ export const DeviceInfoPanel = ({
})}
</div>
{!isCollapsed && (
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
)}
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />}
<div
className={cn(
@@ -257,9 +251,7 @@ export const DeviceInfoPanel = ({
{actionButtons.map((buttonItem) => {
const Icon = buttonItem.icon;
if (buttonItem.render) {
return (
<Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>
);
return <Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>;
}
return (
<Button
@@ -281,8 +273,7 @@ export const DeviceInfoPanel = ({
"flex-shrink-0 w-4",
"text-gray-500 dark:text-gray-400",
"transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200",
)}
/>
<Subtle
@@ -290,8 +281,7 @@ export const DeviceInfoPanel = ({
"text-sm",
"text-gray-600 dark:text-gray-300",
"transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100",
)}
>
{buttonItem.label}
@@ -1,19 +1,11 @@
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
import { Switch } from "@app/components/UI/Switch.tsx";
import type {
ConnectionType,
NewConnection,
} from "@app/core/stores/deviceStore/types.ts";
import type { ConnectionType, NewConnection } from "@app/core/stores/deviceStore/types.ts";
import { testHttpReachable } from "@app/pages/Connections/utils";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import {
type BrowserFeature,
@@ -45,9 +37,7 @@ type DialogState = {
protocol: "http" | "https";
url: string;
testStatus: TestingStatus;
btSelected:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined;
serialSelected: { vendorId?: number; productId?: number } | undefined;
};
@@ -60,9 +50,7 @@ type DialogAction =
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
| {
type: "SET_BT_SELECTED";
payload:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined;
}
| {
type: "SET_SERIAL_SELECTED";
@@ -82,30 +70,27 @@ type Pane = {
build: () => NewConnection | null;
};
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> =
{
"Web Bluetooth": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
i18nKey: "addConnection.validation.requiresWebBluetooth",
},
"Web Serial": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
i18nKey: "addConnection.validation.requiresWebSerial",
},
"Secure Context": {
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
i18nKey: "addConnection.validation.requiresSecureContext",
},
};
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
"Web Bluetooth": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
i18nKey: "addConnection.validation.requiresWebBluetooth",
},
"Web Serial": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
i18nKey: "addConnection.validation.requiresWebSerial",
},
"Secure Context": {
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
i18nKey: "addConnection.validation.requiresSecureContext",
},
};
const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
if (missingFeatures.length === 0) {
return null;
}
const browserFeatures = missingFeatures.filter(
(feature) => feature !== "Secure Context",
);
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature =
@@ -167,22 +152,17 @@ const initialState: DialogState = {
btSelected: undefined,
serialSelected: undefined,
};
export const createInitialDialogState = (
overrides?: Partial<DialogState>,
): DialogState => {
export const createInitialDialogState = (overrides?: Partial<DialogState>): DialogState => {
return { ...initialState, ...(overrides ?? {}) };
};
export const dialogStateInitializer = (
overrides?: Partial<DialogState>,
): DialogState => createInitialDialogState(overrides);
export const dialogStateInitializer = (overrides?: Partial<DialogState>): DialogState =>
createInitialDialogState(overrides);
function dialogReducer(state: DialogState, action: DialogAction): DialogState {
switch (action.type) {
case "RESET":
return createInitialDialogState(
action.payload?.isHTTPS ? { protocol: "https" } : {},
);
return createInitialDialogState(action.payload?.isHTTPS ? { protocol: "https" } : {});
case "SET_TAB":
return { ...state, tab: action.payload };
case "SET_NAME":
@@ -223,22 +203,13 @@ function PickerRow({
<div className="grid gap-2">
<Label>{label}</Label>
<div className="flex items-center gap-2">
<Button
variant="subtle"
className="gap-2"
onClick={onPick}
disabled={disabled}
>
<Button variant="subtle" className="gap-2" onClick={onPick} disabled={disabled}>
<MousePointerClick className="h-4 w-4" />
{buttonText}
</Button>
<div className="text-sm text-slate-500 dark:text-slate-400 truncate">
{display}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400 truncate">{display}</div>
</div>
{helper ? (
<p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p>
) : null}
{helper ? <p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p> : null}
</div>
);
}
@@ -267,10 +238,8 @@ export default function AddConnectionDialog({
const { unsupported } = useBrowserFeatureDetection();
const { t } = useTranslation();
const bluetoothSupported =
typeof navigator !== "undefined" && "bluetooth" in navigator;
const serialSupported =
typeof navigator !== "undefined" && "serial" in navigator;
const bluetoothSupported = typeof navigator !== "undefined" && "bluetooth" in navigator;
const serialSupported = typeof navigator !== "undefined" && "serial" in navigator;
const isURLHTTPS = isHTTPS;
const reset = useCallback(() => {
@@ -299,9 +268,7 @@ export default function AddConnectionDialog({
if (!bluetoothSupported) {
toast({
title: t("addConnection.bluetoothConnection.notSupported.title"),
description: t(
"addConnection.bluetoothConnection.notSupported.description",
),
description: t("addConnection.bluetoothConnection.notSupported.description"),
});
return;
}
@@ -336,9 +303,7 @@ export default function AddConnectionDialog({
if (!serialSupported) {
toast({
title: t("addConnection.serialConnection.notSupported.title"),
description: t(
"addConnection.serialConnection.notSupported.description",
),
description: t("addConnection.serialConnection.notSupported.description"),
});
return;
}
@@ -370,9 +335,7 @@ export default function AddConnectionDialog({
}
toast({
title: t("addConnection.serialConnection.portSelected.title"),
description: t(
"addConnection.serialConnection.portSelected.description",
),
description: t("addConnection.serialConnection.portSelected.description"),
});
} catch (err) {
makeToastErrorHandler("Serial")(err);
@@ -397,9 +360,7 @@ export default function AddConnectionDialog({
dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
toast({
title: t("addConnection.httpConnection.connectionTest.failure.title"),
description: t(
"addConnection.httpConnection.connectionTest.failure.description",
),
description: t("addConnection.httpConnection.connectionTest.failure.description"),
});
}
}, [state.protocol, state.url, toast, t]);
@@ -410,9 +371,7 @@ export default function AddConnectionDialog({
placeholder: t("addConnection.httpConnection.namePlaceholder"),
children: () => (
<div className="flex flex-col gap-4">
<Label htmlFor="url">
{t("addConnection.httpConnection.heading")}
</Label>
<Label htmlFor="url">{t("addConnection.httpConnection.heading")}</Label>
<Input
id={"url"}
@@ -448,23 +407,19 @@ export default function AddConnectionDialog({
className="gap-2"
onClick={handleTestHttp}
disabled={
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
.success === false || state.testStatus === "testing"
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === false ||
state.testStatus === "testing"
}
>
{state.testStatus === "testing" ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{t(
"addConnection.httpConnection.connectionTest.button.loading",
)}
{t("addConnection.httpConnection.connectionTest.button.loading")}
</>
) : (
<>
<MousePointerClick className="h-4 w-4" />
{t(
"addConnection.httpConnection.connectionTest.button.label",
)}
{t("addConnection.httpConnection.connectionTest.button.label")}
</>
)}
</Button>
@@ -477,9 +432,7 @@ export default function AddConnectionDialog({
{state.testStatus === "failure" && (
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
<XCircle className="h-4 w-4" />
{t(
"addConnection.httpConnection.connectionTest.notReachable",
)}
{t("addConnection.httpConnection.connectionTest.notReachable")}
</div>
)}
</div>
@@ -489,8 +442,8 @@ export default function AddConnectionDialog({
</div>
),
validate: () =>
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
.success === true && state.testStatus === "success",
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === true &&
state.testStatus === "success",
build: () => ({
type: "http",
name: state.name.trim(),
@@ -503,12 +456,8 @@ export default function AddConnectionDialog({
<>
<SupportBadge
supported={bluetoothSupported}
labelSupported={t(
"addConnection.bluetoothConnection.supported.title",
)}
labelUnsupported={t(
"addConnection.bluetoothConnection.notSupported.title",
)}
labelSupported={t("addConnection.bluetoothConnection.supported.title")}
labelUnsupported={t("addConnection.bluetoothConnection.notSupported.title")}
/>
<PickerRow
label={t("addConnection.bluetoothConnection.device")}
@@ -522,10 +471,7 @@ export default function AddConnectionDialog({
}
helper={t("addConnection.bluetoothConnection.helperText")}
/>
<FeatureErrorMessage
missingFeatures={unsupported}
tabId="bluetooth"
/>
<FeatureErrorMessage missingFeatures={unsupported} tabId="bluetooth" />
</>
),
validate: () => state.name.trim().length > 0 && !!state.btSelected,
@@ -543,12 +489,8 @@ export default function AddConnectionDialog({
<>
<SupportBadge
supported={serialSupported}
labelSupported={t(
"addConnection.serialConnection.supported.title",
)}
labelUnsupported={t(
"addConnection.serialConnection.notSupported.title",
)}
labelSupported={t("addConnection.serialConnection.supported.title")}
labelUnsupported={t("addConnection.serialConnection.notSupported.title")}
/>
<PickerRow
label={t("addConnection.serialConnection.port")}
@@ -558,10 +500,8 @@ export default function AddConnectionDialog({
display={
state.serialSelected
? t("addConnection.serialConnection.deviceName", {
vendorId:
state.serialSelected.vendorId?.toString(16) ?? "?",
productId:
state.serialSelected.productId?.toString(16) ?? "?",
vendorId: state.serialSelected.vendorId?.toString(16) ?? "?",
productId: state.serialSelected.productId?.toString(16) ?? "?",
})
: t("addConnection.serialConnection.notSelected")
}
@@ -571,8 +511,7 @@ export default function AddConnectionDialog({
</>
),
validate: () =>
state.name.trim().length > 0 &&
(!!state.serialSelected || !serialSupported),
state.name.trim().length > 0 && (!!state.serialSelected || !serialSupported),
build: () => ({
type: "serial",
name: state.name.trim(),
@@ -598,8 +537,7 @@ export default function AddConnectionDialog({
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
const submit =
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
async () => {
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => {
if (!canCreate) {
return;
}
@@ -608,8 +546,7 @@ export default function AddConnectionDialog({
if (!payload) {
return;
}
const btDevice =
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined;
await fn(payload, btDevice);
};
@@ -625,9 +562,7 @@ export default function AddConnectionDialog({
>
<Tabs
value={state.tab}
onValueChange={(v) =>
dispatch({ type: "SET_TAB", payload: v as TabKey })
}
onValueChange={(v) => dispatch({ type: "SET_TAB", payload: v as TabKey })}
>
<TabsList className="grid grid-cols-3">
{TAB_META.map(({ key, label, Icon }) => (
@@ -647,20 +582,14 @@ export default function AddConnectionDialog({
<Input
id={`name-${state.tab}`}
value={state.name}
onChange={(evt) =>
dispatch({ type: "SET_NAME", payload: evt.target.value })
}
onChange={(evt) => dispatch({ type: "SET_NAME", payload: evt.target.value })}
placeholder={currentPane.placeholder}
/>
</div>
{PANES[key].children()}
<div className="flex justify-end">
<div className="inline-flex rounded-md shadow-sm overflow-hidden border">
<Button
onClick={submit(onSave)}
disabled={!canCreate}
className="rounded-none"
>
<Button onClick={submit(onSave)} disabled={!canCreate} className="rounded-none">
{t("button.saveConnection")}
</Button>
</div>
@@ -28,18 +28,13 @@ export const urlOrIpv4Schema = z
}
// IPv4 pattern
const ipv4Regex =
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
// Domain pattern (e.g. example.com, meshtastic.local)
const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/;
// Local domain (e.g. meshtastic.local)
const localDomainRegex = /^(?!-)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.local$/;
return (
ipv4Regex.test(host) ||
domainRegex.test(host) ||
localDomainRegex.test(host)
);
return ipv4Regex.test(host) || domainRegex.test(host) || localDomainRegex.test(host);
}, "Must be a valid IPv4 address or domain name with optional port (10-65535)")
.transform((val) => {
return /^https?:\/\//i.test(val) ? val : `http://${val}`;
@@ -47,9 +47,7 @@ describe("ClearAllStoresDialog", () => {
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Clear all local storage" }),
);
fireEvent.click(screen.getByRole("button", { name: "Clear all local storage" }));
expect(mockClearAllStores).toHaveBeenCalledTimes(1);
expect(assignedHref).toBe("/"); // forced reload target
@@ -7,10 +7,7 @@ export interface ClearAllStoresDialogProps {
onOpenChange: (open: boolean) => void;
}
export const ClearAllStoresDialog = ({
open,
onOpenChange,
}: ClearAllStoresDialogProps) => {
export const ClearAllStoresDialog = ({ open, onOpenChange }: ClearAllStoresDialogProps) => {
const { t } = useTranslation("dialog");
const handleClearAllStores = () => {
@@ -14,10 +14,7 @@ export interface ClientNotificationDialogProps {
onOpenChange: (open: boolean) => void;
}
export const ClientNotificationDialog = ({
open,
onOpenChange,
}: ClientNotificationDialogProps) => {
export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificationDialogProps) => {
const { t } = useTranslation("dialog");
const { getClientNotification, removeClientNotification } = useDevice();
@@ -46,19 +46,13 @@ describe("DeleteMessagesDialog", () => {
it("renders the dialog when open is true", () => {
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
expect(screen.getByText("Clear All Messages")).toBeInTheDocument();
expect(
screen.getByText(/This action will clear all message history./),
).toBeInTheDocument();
expect(screen.getByText(/This action will clear all message history./)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Clear Messages" }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Clear Messages" })).toBeInTheDocument();
});
it("does not render the dialog when open is false", () => {
render(
<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />,
);
render(<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />);
expect(screen.queryByText("Clear All Messages")).toBeNull();
});
@@ -8,10 +8,7 @@ export interface DeleteMessagesDialogProps {
onOpenChange: (open: boolean) => void;
}
export const DeleteMessagesDialog = ({
open,
onOpenChange,
}: DeleteMessagesDialogProps) => {
export const DeleteMessagesDialog = ({ open, onOpenChange }: DeleteMessagesDialogProps) => {
const { t } = useTranslation("dialog");
const messageStore = useMessages();
@@ -33,9 +33,7 @@ describe("FactoryResetConfigDialog", () => {
it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Config" }),
);
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Config" }));
expect(mockFactoryReset).toHaveBeenCalledTimes(1);
@@ -8,10 +8,7 @@ export interface FactoryResetConfigDialogProps {
onOpenChange: (open: boolean) => void;
}
export const FactoryResetConfigDialog = ({
open,
onOpenChange,
}: FactoryResetConfigDialogProps) => {
export const FactoryResetConfigDialog = ({ open, onOpenChange }: FactoryResetConfigDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
@@ -61,9 +61,7 @@ describe("FactoryResetDeviceDialog", () => {
);
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Device" }),
);
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Device" }));
// Called immediately
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
@@ -1,10 +1,5 @@
import { toast } from "@core/hooks/useToast.ts";
import {
useDevice,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { useDevice, useDeviceStore, useMessageStore, useNodeDBStore } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
@@ -13,10 +8,7 @@ export interface FactoryResetDeviceDialogProps {
onOpenChange: (open: boolean) => void;
}
export const FactoryResetDeviceDialog = ({
open,
onOpenChange,
}: FactoryResetDeviceDialogProps) => {
export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDeviceDialogProps) => {
const { t } = useTranslation("dialog");
const { connection, id } = useDevice();
@@ -47,8 +47,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
try {
const channelsUrl = new URL(importDialogInput);
if (
(channelsUrl.hostname !== "meshtastic.org" &&
channelsUrl.pathname !== "/e/") ||
(channelsUrl.hostname !== "meshtastic.org" && channelsUrl.pathname !== "/e/") ||
!channelsUrl.hash
) {
throw t("import.error.invalidUrl");
@@ -56,11 +55,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const encodedChannelConfig = channelsUrl.hash.substring(1);
const paddedString = encodedChannelConfig
.padEnd(
encodedChannelConfig.length +
((4 - (encodedChannelConfig.length % 4)) % 4),
"=",
)
.padEnd(encodedChannelConfig.length + ((4 - (encodedChannelConfig.length % 4)) % 4), "=")
.replace(/-/g, "+")
.replace(/_/g, "/");
@@ -75,43 +70,35 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
setImportIndex(newImportChannelArray);
setUpdateConfig(newChannelSet?.loraConfig !== undefined);
setValidUrl(true);
} catch (_error) {
} catch {
setValidUrl(false);
setChannelSet(undefined);
}
}, [importDialogInput, t]);
const apply = () => {
channelSet?.settings.forEach(
(ch: Protobuf.Channel.ChannelSettings, index: number) => {
if (importIndex[index] === -1) {
return;
}
channelSet?.settings.forEach((ch: Protobuf.Channel.ChannelSettings, index: number) => {
if (importIndex[index] === -1) {
return;
}
const payload = create(Protobuf.Channel.ChannelSchema, {
index: importIndex[index],
role:
importIndex[index] === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
});
const payload = create(Protobuf.Channel.ChannelSchema, {
index: importIndex[index],
role:
importIndex[index] === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
});
if (
!deepCompareConfig(
channels.get(importIndex[index] ?? 0),
payload,
true,
)
) {
setChange(
{ type: "channel", index: importIndex[index] ?? 0 },
payload,
channels.get(importIndex[index] ?? 0),
);
}
},
);
if (!deepCompareConfig(channels.get(importIndex[index] ?? 0), payload, true)) {
setChange(
{ type: "channel", index: importIndex[index] ?? 0 },
payload,
channels.get(importIndex[index] ?? 0),
);
}
});
if (channelSet?.loraConfig && updateConfig) {
const payload = {
@@ -146,23 +133,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
<DialogHeader>
<DialogTitle>{t("import.title")}</DialogTitle>
<DialogDescription>
<Trans
i18nKey={"import.description"}
components={{ italic: <i />, br: <br /> }}
/>
<Trans i18nKey={"import.description"} components={{ italic: <i />, br: <br /> }} />
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<Label>{t("import.channelSetUrl")}</Label>
<Input
value={importDialogInput}
variant={
importDialogInput === ""
? "default"
: validUrl
? "dirty"
: "invalid"
}
variant={importDialogInput === "" ? "default" : validUrl ? "dirty" : "invalid"}
onChange={(e) => {
setImportDialogInput(e.target.value);
}}
@@ -191,10 +169,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
<span className="flex-1">{t("import.channelSlot")}</span>
</div>
{channelSet?.settings.map((channel, index) => (
<div
className="flex items-center"
key={`channel_${channel.id}_${index}`}
>
<div className="flex items-center" key={`channel_${channel.id}_${index}`}>
<Label className="flex-1">
{channel.name.length
? channel.name
@@ -214,14 +189,10 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
disabled={importIndex.includes(i) && index !== i}
value={i.toString()}
>
{i === 0
? t("import.primary")
: `${t("import.channelPrefix")}${i}`}
{i === 0 ? t("import.primary") : `${t("import.channelPrefix")}${i}`}
</SelectItem>
))}
<SelectItem value="-1">
{t("import.doNotImport")}
</SelectItem>
<SelectItem value="-1">{t("import.doNotImport")}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -27,13 +27,10 @@ export const LocationResponseDialog = ({
const from = getNode(location?.from ?? 0);
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const shortName =
from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName"));
const position = location?.data;
@@ -70,23 +67,18 @@ export const LocationResponseDialog = ({
rel="noreferrer"
>
{" "}
{position.latitudeI ?? 0 / 1e7},{" "}
{position.longitudeI ?? 0 / 1e7}
{position.latitudeI ?? 0 / 1e7}, {position.longitudeI ?? 0 / 1e7}
</a>
</p>
<p>
{t("locationResponse.altitude")} {position.altitude}
{(position.altitude ?? 0) < 1
? t("unit.meter.one")
: t("unit.meter.plural")}
{(position.altitude ?? 0) < 1 ? t("unit.meter.one") : t("unit.meter.plural")}
</p>
</span>
</div>
) : (
// Optional: Show a message if coordinates are not available
<p className="text-textPrimary">
{t("locationResponse.noCoordinates")}
</p>
<p className="text-textPrimary">{t("locationResponse.noCoordinates")}</p>
)}
</DialogDescription>
</DialogContent>
@@ -18,11 +18,7 @@ export interface ManagedModeDialogProps {
onSubmit: () => void;
}
export const ManagedModeDialog = ({
open,
onOpenChange,
onSubmit,
}: ManagedModeDialogProps) => {
export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeDialogProps) => {
const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false);
@@ -47,9 +43,7 @@ export const ManagedModeDialog = ({
onChange={() => setConfirmState(!confirmState)}
name="confirmUnderstanding"
>
<p className="dark:text-white pt-1">
{t("managedMode.confirmUnderstanding")}
</p>
<p className="dark:text-white pt-1">{t("managedMode.confirmUnderstanding")}</p>
</Checkbox>
</div>
<DialogFooter>
@@ -8,12 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
import {
type BrowserFeature,
useBrowserFeatureDetection,
@@ -63,9 +58,7 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
return null;
}
const browserFeatures = missingFeatures.filter(
(feature) => feature !== "Secure Context",
);
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature =
@@ -133,17 +126,13 @@ export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
id: "BLE",
label: t("newDeviceDialog.tabBluetooth"),
element: BLE,
isDisabled:
unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"),
},
{
id: "Serial",
label: t("newDeviceDialog.tabSerial"),
element: Serial,
isDisabled:
unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
isDisabled: unsupported.includes("Web Serial") || unsupported.includes("Secure Context"),
},
];
@@ -50,10 +50,7 @@ export interface NodeDetailsDialogProps {
onOpenChange: (open: boolean) => void;
}
export const NodeDetailsDialog = ({
open,
onOpenChange,
}: NodeDetailsDialogProps) => {
export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps) => {
const { t } = useTranslation("dialog");
const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
@@ -64,12 +61,8 @@ export const NodeDetailsDialog = ({
const node = getNode(nodeNumDetails);
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(
node?.isFavorite ?? false,
);
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(
node?.isIgnored ?? false,
);
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(node?.isFavorite ?? false);
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(node?.isIgnored ?? false);
useEffect(() => {
if (!node) {
@@ -168,8 +161,7 @@ export const NodeDetailsDialog = ({
key: "batteryLevel",
label: t("nodeDetails.batteryLevel"),
value: node.deviceMetrics?.batteryLevel,
format: (val: number) =>
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
format: (val: number) => (val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`),
},
{
key: "voltage",
@@ -202,27 +194,17 @@ export const NodeDetailsDialog = ({
<DialogFooter>
<div className="w-full ">
<div className="flex flex-row flex-wrap space-y-1">
<Button
className="mr-1"
name="message"
onClick={handleDirectMessage}
>
<Button className="mr-1" name="message" onClick={handleDirectMessage}>
<MessageSquareIcon className="mr-2" />
{t("nodeDetails.message")}
</Button>
<Button
className="mr-1"
name="traceRoute"
onClick={handleTraceroute}
>
<Button className="mr-1" name="traceRoute" onClick={handleTraceroute}>
<WaypointsIcon className="mr-2" />
{t("nodeDetails.traceRoute")}
</Button>
<Button className="mr-1" onClick={handleToggleFavorite}>
<StarIcon
className={cn(
isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "",
)}
className={cn(isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "")}
/>
</Button>
<div className="flex flex-1 justify-start" />
@@ -243,9 +225,7 @@ export const NodeDetailsDialog = ({
</Button>
</TooltipTrigger>
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
{isIgnoredState
? t("nodeDetails.unignoreNode")
: t("nodeDetails.ignoreNode")}
{isIgnoredState ? t("nodeDetails.unignoreNode") : t("nodeDetails.ignoreNode")}
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
</TooltipContent>
</Tooltip>
@@ -275,9 +255,7 @@ export const NodeDetailsDialog = ({
<div className="flex flex-col flex-wrap space-x-1 space-y-1">
<div className="flex flex-row space-x-2">
<div className="w-full bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg">
<p className="text-lg font-semibold">
{t("nodeDetails.details")}
</p>
<p className="text-lg font-semibold">{t("nodeDetails.details")}</p>
<table className="table-fixed w-full">
<tbody>
<tr>
@@ -291,9 +269,10 @@ export const NodeDetailsDialog = ({
<tr>
<td>{t("nodeDetails.role")}</td>
<td>
{Protobuf.Config.Config_DeviceConfig_Role[
node.user?.role ?? 0
]?.replace(/_/g, " ")}
{Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0]?.replace(
/_/g,
" ",
)}
</td>
</tr>
<tr>
@@ -312,44 +291,35 @@ export const NodeDetailsDialog = ({
<td>{t("nodeDetails.hardware")}</td>
<td>
{(
Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0
] ?? t("unknown.shortName")
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
t("unknown.shortName")
).replace(/_/g, " ")}
</td>
</tr>
<tr>
<td>{t("nodeDetails.messageable")}</td>
<td>
{node.user?.isUnmessagable ? t("no") : t("yes")}
</td>
<td>{node.user?.isUnmessagable ? t("no") : t("yes")}</td>
</tr>
</tbody>
</table>
</div>
<DeviceImage
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
deviceType={
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
"UNKNOWN"
}
deviceType={Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNKNOWN"}
/>
</div>
</div>
<div>
<div className={sectionClassName}>
<p className="text-lg font-semibold">
{t("nodeDetails.security")}
</p>
<p className="text-lg font-semibold">{t("nodeDetails.security")}</p>
<table className="table-auto w-full">
<tbody>
<tr>
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
<td>
<pre className="text-xs pt-0.5">
{node.user?.publicKey &&
node.user?.publicKey.length > 0
{node.user?.publicKey && node.user?.publicKey.length > 0
? fromByteArray(node.user.publicKey)
: t("unknown.longName")}
</pre>
@@ -368,9 +338,7 @@ export const NodeDetailsDialog = ({
</div>
<div className={sectionClassName}>
<p className="text-lg font-semibold">
{t("nodeDetails.position")}
</p>
<p className="text-lg font-semibold">{t("nodeDetails.position")}</p>
{node.position ? (
<table className="table-auto w-full">
@@ -387,8 +355,7 @@ export const NodeDetailsDialog = ({
target="_blank"
rel="noreferrer"
>
{node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
{node.position.latitudeI / 1e7}, {node.position.longitudeI / 1e7}
</a>
</td>
</tr>
@@ -407,11 +374,7 @@ export const NodeDetailsDialog = ({
) : (
<p>{t("unknown.longName")}</p>
)}
<Button
onClick={handleRequestPosition}
name="requestPosition"
className="mt-2"
>
<Button onClick={handleRequestPosition} name="requestPosition" className="mt-2">
<MapPinnedIcon className="mr-2" />
{t("nodeDetails.requestPosition")}
</Button>
@@ -436,9 +399,7 @@ export const NodeDetailsDialog = ({
<tr>
<td>{t("nodeDetails.uptime")}</td>
<td>
<Uptime
seconds={node.deviceMetrics.uptimeSeconds}
/>
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
</td>
</tr>
)}
@@ -457,9 +418,7 @@ export const NodeDetailsDialog = ({
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">
{JSON.stringify(node, null, 2)}
</pre>
<pre className="text-xs w-full">{JSON.stringify(node, null, 2)}</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
@@ -19,25 +19,19 @@ export interface PkiBackupDialogProps {
onOpenChange: (open: boolean) => void;
}
export const PkiBackupDialog = ({
open,
onOpenChange,
}: PkiBackupDialogProps) => {
export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) => {
const { t } = useTranslation("dialog");
const { config, setDialogOpen } = useDevice();
const { getMyNode } = useNodeDB();
const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey;
const decodeKeyData = React.useCallback(
(key: Uint8Array<ArrayBufferLike>) => {
if (!key) {
return "";
}
return fromByteArray(key ?? new Uint8Array(0));
},
[],
);
const decodeKeyData = React.useCallback((key: Uint8Array<ArrayBufferLike>) => {
if (!key) {
return "";
}
return fromByteArray(key ?? new Uint8Array(0));
}, []);
const closeDialog = React.useCallback(() => {
setDialogOpen("pkiBackup", false);
@@ -133,9 +127,7 @@ export const PkiBackupDialog = ({
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
<DialogDescription>
<span className="font-bold break-before-auto">
{t("pkiBackup.loseKeysWarning")}
</span>
<span className="font-bold break-before-auto">{t("pkiBackup.loseKeysWarning")}</span>
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-6">
@@ -46,11 +46,7 @@ export const PkiRegenerateDialog = ({
<DialogDescription>{dialogText.description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="destructive"
name="regenerate"
onClick={() => onSubmit()}
>
<Button variant="destructive" name="regenerate" onClick={() => onSubmit()}>
{dialogText.button}
</Button>
</DialogFooter>
@@ -24,12 +24,7 @@ export interface QRDialogProps {
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
}
export const QRDialog = ({
open,
onOpenChange,
loraConfig,
channels,
}: QRDialogProps) => {
export const QRDialog = ({ open, onOpenChange, loraConfig, channels }: QRDialogProps) => {
const { t } = useTranslation("dialog");
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
@@ -49,16 +44,12 @@ export const QRDialog = ({
settings: channelsToEncode,
}),
);
const base64 = fromByteArray(
toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded),
)
const base64 = fromByteArray(toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
setQrCodeUrl(
`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`,
);
setQrCodeUrl(`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`);
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
return (
@@ -89,14 +80,9 @@ export const QRDialog = ({
checked={selectedChannels.includes(channel.index)}
onChange={() => {
if (selectedChannels.includes(channel.index)) {
setSelectedChannels(
selectedChannels.filter((c) => c !== channel.index),
);
setSelectedChannels(selectedChannels.filter((c) => c !== channel.index));
} else {
setSelectedChannels([
...selectedChannels,
channel.index,
]);
setSelectedChannels([...selectedChannels, channel.index]);
}
}}
/>
@@ -1,10 +1,5 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import type {
ButtonHTMLAttributes,
ClassAttributes,
InputHTMLAttributes,
ReactNode,
} from "react";
import type { ButtonHTMLAttributes, ClassAttributes, InputHTMLAttributes, ReactNode } from "react";
import type { JSX } from "react/jsx-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RebootDialog } from "./RebootDialog.tsx";
@@ -54,16 +49,10 @@ vi.mock("@components/UI/Input.tsx", async () => {
vi.mock("@components/UI/Dialog.tsx", () => {
return {
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogHeader: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
DialogDescription: ({ children }: { children: ReactNode }) => (
<p>{children}</p>
),
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
DialogClose: () => null,
};
});
@@ -81,12 +70,8 @@ describe("RebootDialog", () => {
it("renders dialog with default input value", () => {
render(<RebootDialog open onOpenChange={() => {}} />);
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
expect(
screen.getByRole("heading", { name: /reboot device/i, level: 1 }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /reboot now/i }),
).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /reboot device/i, level: 1 })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument();
});
it("calls correct reboot function based on OTA checkbox state", () => {
@@ -199,8 +184,6 @@ describe("RebootDialog", () => {
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
});
expect(rebootMock).toHaveBeenCalledWith(-1);
expect(
screen.queryByText(/reboot has been scheduled/i),
).not.toBeInTheDocument();
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
});
});
@@ -96,11 +96,7 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
<Separator />
{!isScheduled ? (
<>
<Checkbox
checked={isOTA}
onChange={(checked) => setIsOTA(checked)}
className="px-2"
>
<Checkbox checked={isOTA} onChange={(checked) => setIsOTA(checked)} className="px-2">
{t("reboot.ota")}
</Checkbox>
<div className="flex gap-2 px-2 items-center relative">
@@ -137,9 +133,7 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
) : (
<div className="px-2">
<div className="pb-6 pt-2 text-center">
<Label className=" text-gray-700 dark:text-gray-300 ">
{t("reboot.scheduled")}
</Label>
<Label className=" text-gray-700 dark:text-gray-300 ">{t("reboot.scheduled")}</Label>
</div>
<Button
variant="destructive"
@@ -1,17 +1,11 @@
import {
CurrentDeviceContext,
useDeviceStore,
useMessageStore,
} from "@core/stores";
import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
import { render } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
vi.mock("@core/stores", async () => {
const actual = (await vi.importActual(
"@core/stores",
)) as typeof import("@core/stores");
const actual = (await vi.importActual("@core/stores")) as typeof import("@core/stores");
return {
...actual,
useMessageStore: vi.fn(),
@@ -16,10 +16,7 @@ export interface RefreshKeysDialogProps {
onOpenChange: (open: boolean) => void;
}
export const RefreshKeysDialog = ({
open,
onOpenChange,
}: RefreshKeysDialogProps) => {
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog");
const { activeChat } = useMessages();
const { nodeErrors, getNode } = useNodeDB();
@@ -48,10 +45,7 @@ export const RefreshKeysDialog = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-8 flex flex-col gap-2"
aria-describedby={undefined}
>
<DialogContent className="max-w-8 flex flex-col gap-2" aria-describedby={undefined}>
<DialogClose onClick={handleCloseDialog} />
<DialogHeader>
<DialogTitle>{text.title}</DialogTitle>
@@ -60,30 +54,17 @@ export const RefreshKeysDialog = ({
<ul className="mt-2">
<li className="flex place-items-center gap-2 items-start">
<div className="p-2 bg-slate-500 rounded-lg mt-1">
<LockKeyholeOpenIcon
size={30}
className="text-white justify-center"
/>
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
</div>
<div className="flex flex-col gap-2">
<div>
<p className="font-bold mb-0.5">
{t("refreshKeys.label.acceptNewKeys")}
</p>
<p className="font-bold mb-0.5">{t("refreshKeys.label.acceptNewKeys")}</p>
<p>{t("refreshKeys.description.acceptNewKeys")}</p>
</div>
<Button
variant="default"
name="requestNewKeys"
onClick={handleNodeRemove}
>
<Button variant="default" name="requestNewKeys" onClick={handleNodeRemove}>
{t("button.requestNewKeys")}
</Button>
<Button
variant="outline"
name="dismiss"
onClick={handleCloseDialog}
>
<Button variant="outline" name="dismiss" onClick={handleCloseDialog}>
{t("button.dismiss")}
</Button>
</div>
@@ -8,10 +8,7 @@ export interface RemoveNodeDialogProps {
onOpenChange: (open: boolean) => void;
}
export const RemoveNodeDialog = ({
open,
onOpenChange,
}: RemoveNodeDialogProps) => {
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const { getNode, removeNode } = useNodeDB();
@@ -48,9 +48,7 @@ describe("ResetNodeDbDialog", () => {
);
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Reset Node Database" }),
);
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" }));
// Called immediately
expect(mockResetNodes).toHaveBeenCalledTimes(1);
@@ -8,10 +8,7 @@ export interface ResetNodeDbDialogProps {
onOpenChange: (open: boolean) => void;
}
export const ResetNodeDbDialog = ({
open,
onOpenChange,
}: ResetNodeDbDialogProps) => {
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
@@ -32,13 +32,10 @@ export const TracerouteResponseDialog = ({
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
const fromLongName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const fromShortName =
from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName"));
const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
@@ -20,16 +20,12 @@ export interface RouterRoleDialogProps {
onOpenChange: (open: boolean) => void;
}
export const UnsafeRolesDialog = ({
open,
onOpenChange,
}: RouterRoleDialogProps) => {
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false);
const { setDialogOpen } = useDevice();
const deviceRoleLink =
"https://meshtastic.org/docs/configuration/radio/device/";
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
const choosingTheRightDeviceRoleLink =
"https://meshtastic.org/blog/choosing-the-right-device-role/";
@@ -63,17 +59,11 @@ export const UnsafeRolesDialog = ({
onChange={() => setConfirmState(!confirmState)}
name="confirmUnderstanding"
>
<span className="dark:text-white">
{t("unsafeRoles.confirmUnderstanding")}
</span>
<span className="dark:text-white">{t("unsafeRoles.confirmUnderstanding")}</span>
</Checkbox>
</div>
<DialogFooter className="mt-6">
<Button
variant="default"
name="dismiss"
onClick={() => handleCloseDialog("dismiss")}
>
<Button variant="default" name="dismiss" onClick={() => handleCloseDialog("dismiss")}>
{t("button.dismiss")}
</Button>
<Button
@@ -4,20 +4,11 @@ import {
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { eventBus } from "@core/utils/eventBus.ts";
import { renderHook } from "@testing-library/react";
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from "vitest";
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest";
const mockNavigate = vi.fn();
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@tanstack/react-router")>();
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
return {
...actual,
useNavigate: () => mockNavigate,
@@ -65,10 +56,7 @@ describe("useUnsafeRolesDialog", () => {
result.current.handleCloseDialog();
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
false,
);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", false);
});
});
@@ -77,8 +65,7 @@ describe("useUnsafeRolesDialog", () => {
const { result } = renderUnsafeRolesHook();
const safeRole = "SAFE_ROLE";
const validationResult =
await result.current.validateRoleSelection(safeRole);
const validationResult = await result.current.validateRoleSelection(safeRole);
expect(validationResult).toBe(true);
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
@@ -87,61 +74,40 @@ describe("useUnsafeRolesDialog", () => {
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0]!,
);
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
true,
);
expect(eventBus.on).toHaveBeenCalledWith(
"dialog:unsafeRoles",
expect.any(Function),
);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true);
expect(eventBus.on).toHaveBeenCalledWith("dialog:unsafeRoles", expect.any(Function));
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
const validationResult = await validationPromise;
expect(validationResult).toBe(true);
expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
});
it("should resolve with false when user dismisses the dialog", async () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0]!,
);
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "dismiss" });
const validationResult = await validationPromise;
expect(validationResult).toBe(false);
expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
});
it("should clean up event listener after response", async () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[1]!,
);
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[1]!);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
await validationPromise;
expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
});
});
@@ -152,13 +118,9 @@ describe("useUnsafeRolesDialog", () => {
mockDevice.setDialogOpen.mockClear();
(eventBus.on as Mock).mockClear();
const validationPromise =
result.current.validateRoleSelection(unsafeRole);
const validationPromise = result.current.validateRoleSelection(unsafeRole);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
true,
);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
@@ -21,11 +21,7 @@ export const useUnsafeRolesDialog = () => {
setDialogOpen("unsafeRoles", true);
return new Promise((resolve) => {
const handleResponse = ({
action,
}: {
action: "confirm" | "dismiss";
}) => {
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => {
eventBus.off("dialog:unsafeRoles", handleResponse);
resolve(action === "confirm");
};
@@ -62,9 +62,7 @@ export interface UseDialogReturn {
}
export const useDialog = (initialState?: DialogState): UseDialogReturn => {
const [state, setState] = useState<DialogState>(
initialState ?? { isOpen: false },
);
const [state, setState] = useState<DialogState>(initialState ?? { isOpen: false });
const openDialog = useCallback((config: DialogConfig) => {
setState({ isOpen: true, config });
@@ -97,10 +95,7 @@ export const useDialog = (initialState?: DialogState): UseDialogReturn => {
};
};
export const getDefaultTexts = (
type: DialogType,
t: ReturnType<typeof useTranslation>["t"],
) => {
export const getDefaultTexts = (type: DialogType, t: ReturnType<typeof useTranslation>["t"]) => {
switch (type) {
case "confirm":
return {
@@ -1,8 +1,5 @@
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import {
DynamicFormField,
type FieldProps,
} from "@components/Form/DynamicFormField.tsx";
import { DynamicFormField, type FieldProps } from "@components/Form/DynamicFormField.tsx";
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Heading } from "@components/UI/Typography/Heading.tsx";
@@ -66,9 +63,7 @@ export interface DynamicFormProps<T extends FieldValues> {
validationSchema?: ZodType<T>;
}
export type DynamicFormFormInit<T extends FieldValues> = (
methods: UseFormReturn<T, T, T>,
) => void;
export type DynamicFormFormInit<T extends FieldValues> = (methods: UseFormReturn<T, T, T>) => void;
export function DynamicForm<T extends FieldValues>({
propMethods,
@@ -86,9 +81,7 @@ export function DynamicForm<T extends FieldValues>({
const internalMethods = useForm<T>({
mode: "onChange",
defaultValues: defaultValues,
resolver: validationSchema
? createZodResolver(validationSchema)
: undefined,
resolver: validationSchema ? createZodResolver(validationSchema) : undefined,
shouldFocusError: false,
resetOptions: { keepDefaultValues: true },
values,
@@ -96,8 +89,7 @@ export function DynamicForm<T extends FieldValues>({
const methods = propMethods ?? internalMethods;
const { handleSubmit, control, getValues, formState, getFieldState } =
methods;
const { handleSubmit, control, getValues, formState, getFieldState } = methods;
useEffect(() => {
if (!propMethods) {
@@ -105,10 +97,7 @@ export function DynamicForm<T extends FieldValues>({
}
}, [onFormInit, propMethods, internalMethods]);
const isDisabled = (
disabledBy?: DisabledBy<T>[],
disabled?: boolean,
): boolean => {
const isDisabled = (disabledBy?: DisabledBy<T>[], disabled?: boolean): boolean => {
if (disabled) {
return true;
}
@@ -125,9 +114,7 @@ export function DynamicForm<T extends FieldValues>({
return field.invert ? value : !value;
}
if (typeof value === "number") {
return field.invert
? field.selector !== value
: field.selector === value;
return field.invert ? field.selector !== value : field.selector === value;
}
return false;
});
@@ -1,24 +1,12 @@
import {
GenericInput,
type InputFieldProps,
} from "@components/Form/FormInput.tsx";
import { GenericInput, type InputFieldProps } from "@components/Form/FormInput.tsx";
import {
PasswordGenerator,
type PasswordGeneratorProps,
} from "@components/Form/FormPasswordGenerator.tsx";
import {
type SelectFieldProps,
SelectInput,
} from "@components/Form/FormSelect.tsx";
import {
type ToggleFieldProps,
ToggleInput,
} from "@components/Form/FormToggle.tsx";
import { type SelectFieldProps, SelectInput } from "@components/Form/FormSelect.tsx";
import { type ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.tsx";
import type { Control, FieldValues } from "react-hook-form";
import {
type MultiSelectFieldProps,
MultiSelectInput,
} from "./FormMultiSelect.tsx";
import { type MultiSelectFieldProps, MultiSelectInput } from "./FormMultiSelect.tsx";
export type FieldProps<T> =
| InputFieldProps<T>
@@ -46,14 +34,7 @@ export function DynamicFormField<T extends FieldValues>({
case "text":
case "password":
case "number":
return (
<GenericInput
field={field}
control={control}
disabled={disabled}
isDirty={isDirty}
/>
);
return <GenericInput field={field} control={control} disabled={disabled} isDirty={isDirty} />;
case "toggle":
return (
+5 -18
View File
@@ -48,10 +48,7 @@ export function GenericInput<T extends FieldValues>({
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
if (
field.properties?.fieldLength?.max &&
newValue.length > field.properties.fieldLength.max
) {
if (field.properties?.fieldLength?.max && newValue.length > field.properties.fieldLength.max) {
return;
}
@@ -60,32 +57,22 @@ export function GenericInput<T extends FieldValues>({
}
controllerField.onChange(
field.type === "number"
? Number.parseFloat(newValue).toString()
: newValue,
field.type === "number" ? Number.parseFloat(newValue).toString() : newValue,
);
};
const currentLength = controllerField.value
? String(controllerField.value).length
: 0;
const currentLength = controllerField.value ? String(controllerField.value).length : 0;
return (
<div className="relative w-full">
<Input
type={field.type}
step={field.properties?.step}
value={
field.type === "number"
? String(controllerField.value)
: controllerField.value
}
value={field.type === "number" ? String(controllerField.value) : controllerField.value}
id={field.name}
onChange={handleInputChange}
showCopyButton={field.properties?.showCopyButton}
showPasswordToggle={
field.properties?.showPasswordToggle || field.type === "password"
}
showPasswordToggle={field.properties?.showPasswordToggle || field.type === "password"}
className={field.properties?.className}
{...restProperties}
disabled={disabled}
@@ -33,20 +33,17 @@ export function MultiSelectInput<T extends FieldValues>({
Object.values(enumValue)[0] !== null &&
"i18nKey" in Object.values(enumValue)[0];
const optionsToRender = Object.entries(enumValue).map(
([key, configOrValue]) => {
if (isNewConfigStructure) {
const config =
configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
return {
key,
display: t(config.i18nKey),
value: config.value,
};
}
return { key, display: key, value: configOrValue as number };
},
);
const optionsToRender = Object.entries(enumValue).map(([key, configOrValue]) => {
if (isNewConfigStructure) {
const config = configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
return {
key,
display: t(config.i18nKey),
value: config.value,
};
}
return { key, display: key, value: configOrValue as number };
});
return (
<MultiSelect
@@ -49,13 +49,8 @@ export function SelectInput<T extends FieldValues>({
control,
});
const {
enumValue,
formatEnumName,
defaultValue,
className,
...remainingProperties
} = field.properties;
const { enumValue, formatEnumName, defaultValue, className, ...remainingProperties } =
field.properties;
const valueToKeyMap: Record<string, string> = {};
const optionsEnumValues: [string, number][] = [];
@@ -100,9 +95,7 @@ export function SelectInput<T extends FieldValues>({
className={cn([
className,
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
invalid
? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2"
: "",
invalid ? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2" : "",
])}
ref={ref}
onBlur={onBlur}
@@ -34,12 +34,8 @@ export function ToggleInput<T extends FieldValues>({
{...field.properties}
className={cn([
field.properties?.className,
isDirty
? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2"
: "",
invalid
? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2"
: "",
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
invalid ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2" : "",
])}
{...rest}
/>
@@ -24,9 +24,7 @@ export const FieldWrapper = ({
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
<Label htmlFor={fieldName}>{label}</Label>
<div className="max-w-3xl">
<p className="text-sm text-slate-500 dark:text-slate-400">
{description}
</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{description}</p>
<p hidden={valid ?? true} className="text-sm text-red-500">
{validationText}
</p>
@@ -10,11 +10,7 @@ import type { ZodType } from "zod/v4";
export function createZodResolver<T extends FieldValues>(
schema: ZodType<T, unknown>,
): Resolver<T, unknown> {
return (
values: T,
_context: unknown,
_options?: ResolverOptions<T>,
): ResolverResult<T> => {
return (values: T, _context: unknown, _options?: ResolverOptions<T>): ResolverResult<T> => {
const result = schema.safeParse(values);
if (result.success) {
return {
@@ -23,10 +19,7 @@ export function createZodResolver<T extends FieldValues>(
};
}
const errors: Record<
string,
FieldError & { params?: Record<string, unknown> }
> = {};
const errors: Record<string, FieldError & { params?: Record<string, unknown> }> = {};
for (const issue of result.error.issues) {
const { path, code, message, ...params } = issue;
@@ -42,8 +35,7 @@ export function createZodResolver<T extends FieldValues>(
: "";
const newCode =
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) +
(suffix ? `.${suffix}` : "");
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) + (suffix ? `.${suffix}` : "");
const fieldError: FieldError & { params?: Record<string, unknown> } = {
type: newCode,
@@ -18,9 +18,7 @@ interface LanguageSwitcherProps {
disableHover?: boolean;
}
export default function LanguageSwitcher({
disableHover = false,
}: LanguageSwitcherProps) {
export default function LanguageSwitcher({ disableHover = false }: LanguageSwitcherProps) {
const { i18n } = useTranslation("ui");
const { set: setLanguage, current, getSupportedLangs } = useLang();
@@ -46,15 +44,13 @@ export default function LanguageSwitcher({
size={16}
className={cn(
"text-gray-500 dark:text-gray-400 w-4 flex-shrink-0 transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200",
)}
/>
<Subtle
className={cn(
"text-sm text-gray-600 dark:text-gray-100 transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100",
)}
>
{`${t("languagePicker.label")}:`}
@@ -62,8 +58,7 @@ export default function LanguageSwitcher({
<Subtle
className={cn(
"text-sm font-medium text-gray-700 dark:text-gray-200 transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-900 dark:group-hover:text-white",
!disableHover && "group-hover:text-gray-900 dark:group-hover:text-white",
)}
>
{current?.name}
@@ -81,9 +76,7 @@ export default function LanguageSwitcher({
<span>{language.flag}</span>
<span>{language.name}</span>
</div>
{i18n.language === language.code && (
<Check className="h-4 w-4 text-primary" />
)}
{i18n.language === language.code && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
+1 -3
View File
@@ -45,9 +45,7 @@ export const BaseMap = ({
const locale = useMemo(() => {
return {
"GeolocateControl.FindMyLocation": t(
"maplibre.GeolocateControl.FindMyLocation",
),
"GeolocateControl.FindMyLocation": t("maplibre.GeolocateControl.FindMyLocation"),
"NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"),
"NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"),
"ScaleControl.Meters": t("unit.meter.suffix"),
@@ -1,14 +1,8 @@
import {
type ChannelValidation,
makeChannelSchema,
} from "@app/validation/channel.ts";
import { type ChannelValidation, makeChannelSchema } from "@app/validation/channel.ts";
import { create } from "@bufbuild/protobuf";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -37,8 +31,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: {
...defaultConfig?.settings?.moduleSettings,
positionPrecision:
defaultConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
defaultConfig?.settings?.moduleSettings?.positionPrecision === undefined
? 10
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
},
@@ -60,8 +53,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: {
...effectiveConfig?.settings?.moduleSettings,
positionPrecision:
effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
effectiveConfig?.settings?.moduleSettings?.positionPrecision === undefined
? 10
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
},
@@ -69,11 +61,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
},
};
const [preSharedDialogOpen, setPreSharedDialogOpen] =
useState<boolean>(false);
const [byteCount, setBytes] = useState<number>(
effectiveConfig?.settings?.psk.length ?? 16,
);
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false);
const [byteCount, setBytes] = useState<number>(effectiveConfig?.settings?.psk.length ?? 16);
const ChannelValidationSchema = useMemo(() => {
return makeChannelSchema(byteCount);
}, [byteCount]);
@@ -1,12 +1,7 @@
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
import { useDevice } from "@core/stores";
import type { Protobuf } from "@meshtastic/core";
import i18next from "i18next";
@@ -36,13 +31,7 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
const allChannels = Array.from(channels.values());
const flags = useMemo(
() =>
new Map(
allChannels.map((channel) => [
channel.index,
hasChannelChange(channel.index),
]),
),
() => new Map(allChannels.map((channel) => [channel.index, hasChannelChange(channel.index)])),
[allChannels, hasChannelChange],
);
@@ -64,10 +53,7 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
)}
</TabsTrigger>
))}
<Button
className="ml-auto mr-1 h-8"
onClick={() => setDialogOpen("import", true)}
>
<Button className="ml-auto mr-1 h-8" onClick={() => setDialogOpen("import", true)}>
<UploadIcon className="mr-2" size={14} />
{t("page.import")}
</Button>
@@ -77,16 +63,9 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
</Button>
</TabsList>
{allChannels.map((channel) => (
<TabsContent
key={`channel_${channel.index}`}
value={`channel_${channel.index}`}
>
<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}
/>
<Channel key={channel.index} onFormInit={onFormInit} channel={channel} />
</Suspense>
</TabsContent>
))}
@@ -1,11 +1,7 @@
import { Button } from "@app/components/UI/Button";
import type { Connection } from "@app/core/stores/deviceStore/types";
export function ConnectionStatusBadge({
status,
}: {
status: Connection["status"];
}) {
export function ConnectionStatusBadge({ status }: { status: Connection["status"] }) {
let color = "";
let displayStatus = status;
@@ -30,13 +26,8 @@ export function ConnectionStatusBadge({
}
return (
<Button variant="subtle" className="inline-flex items-center gap-2">
<span
className={`h-2.5 w-2.5 rounded-full ${color}`}
aria-hidden="true"
/>
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">
{displayStatus}
</span>
<span className={`h-2.5 w-2.5 rounded-full ${color}`} aria-hidden="true" />
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">{displayStatus}</span>
</Button>
);
}
@@ -13,11 +13,7 @@ export interface HeatmapLayerProps {
mode: HeatmapMode;
}
export const HeatmapLayer = ({
id,
filteredNodes,
mode,
}: HeatmapLayerProps) => {
export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) => {
const data: FeatureCollection = useMemo(() => {
const features: Feature[] = filteredNodes
.filter((node) => hasPos(node.position))
@@ -45,9 +41,7 @@ export const HeatmapLayer = ({
const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
() => ({
"heatmap-weight":
mode === "density"
? 1
: ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
mode === "density" ? 1 : ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
@@ -69,17 +63,7 @@ export const HeatmapLayer = ({
1,
"rgb(178,24,43)",
],
"heatmap-radius": [
"interpolate",
["linear"],
["zoom"],
0,
2,
9,
20,
15,
30,
],
"heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20, 15, 30],
// Opacity 0.7 to be visible but not blocking
"heatmap-opacity": 0.7,
}),
@@ -50,8 +50,7 @@ export const NodesLayer = ({
() =>
popupState?.type !== "node"
? undefined
: (filteredNodes.find((node) => node.num === popupState.num) ??
undefined),
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
[popupState, filteredNodes],
);
@@ -78,9 +77,7 @@ export const NodesLayer = ({
const isExpanded = expandedCluster === key;
// Precompute pixel offsets for expanded state
const expandedOffsets = isExpanded
? fanOutOffsetsPx(nodes.length, key)
: undefined;
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined;
// Always render all node markers in the cluster
for (const [i, node] of nodes.entries()) {
@@ -36,10 +36,7 @@ export function generatePrecisionCircles(
>();
for (const node of filteredNodes) {
if (
node.position?.precisionBits === undefined ||
node.position.precisionBits === 0
) {
if (node.position?.precisionBits === undefined || node.position.precisionBits === 0) {
continue;
}
const [lng, lat] = toLngLat(node.position);
@@ -63,9 +60,7 @@ export function generatePrecisionCircles(
}
}
const items = Array.from(unique.values()).sort(
(a, b) => a.radiusM - b.radiusM,
);
const items = Array.from(unique.values()).sort((a, b) => a.radiusM - b.radiusM);
const features: Feature<Polygon, CircleProps>[] = items.map(
({ lng, lat, radiusM, r, g, b, a }) => {
@@ -96,13 +91,7 @@ export const SourcePrecisionCircles = ({
type="fill"
layout={{ visibility: isVisible ? "visible" : "none" }}
paint={{
"fill-color": [
"rgba",
["get", "r"],
["get", "g"],
["get", "b"],
["get", "a"],
],
"fill-color": ["rgba", ["get", "r"], ["get", "g"], ["get", "b"], ["get", "a"]],
}}
/>
<Layer
@@ -64,11 +64,7 @@ type Pair = {
ba?: number; // SNR b->a
};
function arcSegment(
aLngLat: LngLat,
bLngLat: LngLat,
curved: boolean,
): LngLat[] | undefined {
function arcSegment(aLngLat: LngLat, bLngLat: LngLat, curved: boolean): LngLat[] | undefined {
if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) {
// Straight line
return [aLngLat, bLngLat];
@@ -180,9 +176,7 @@ function pushIfFeature(
}
}
function generateNeighborLines(
neighborInfos: NeighborInfos[],
): FeatureCollection {
function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollection {
// Collect positions for all referenced nodes, discard pairs with missing positions
const idToLngLat = new Map<number, LngLat>();
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
@@ -246,12 +240,7 @@ function generateNeighborLines(
return { type: "FeatureCollection", features };
}
export const SNRTooltip = ({
pos,
snr,
from,
to,
}: Partial<SNRTooltipProps> = {}) => {
export const SNRTooltip = ({ pos, snr, from, to }: Partial<SNRTooltipProps> = {}) => {
const { t } = useTranslation();
if (!pos) {
@@ -301,9 +290,7 @@ export const SNRLayer = ({
neighborInfo: {
...neighborInfo,
neighbors: neighborInfo.neighbors.map((n) => {
const node = filteredNodes.find(
(node) => node.num === n.nodeId,
);
const node = filteredNodes.find((node) => node.num === n.nodeId);
return { ...n, num: node?.num, position: node?.position };
}),
},
@@ -325,10 +312,7 @@ export const SNRLayer = ({
}))
: [];
const featureCollection = generateNeighborLines([
...remotePairs,
...directPairs,
]);
const featureCollection = generateNeighborLines([...remotePairs, ...directPairs]);
return (
<Source type="geojson" data={featureCollection}>
@@ -82,11 +82,7 @@ export const WaypointLayer = ({
offset={[0, 25]}
onClose={() => setPopupState(undefined)}
>
<WaypointDetail
waypoint={popupState.waypoint}
myNode={myNode}
onEdit={() => {}}
/>
<WaypointDetail waypoint={popupState.waypoint} myNode={myNode} onEdit={() => {}} />
</PopupWrapper>,
);
}
@@ -8,13 +8,7 @@ type StackBadgeProps = {
onClick: (e: { originalEvent: MouseEvent }) => void;
};
export const StackBadge = ({
lng,
lat,
count,
isVisible = true,
onClick,
}: StackBadgeProps) => {
export const StackBadge = ({ lng, lat, count, isVisible = true, onClick }: StackBadgeProps) => {
return (
<Marker
longitude={lng}
@@ -16,14 +16,7 @@ import {
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useNavigate } from "@tanstack/react-router";
import {
Dot,
LockIcon,
LockOpenIcon,
MessageSquareIcon,
MountainSnow,
Star,
} from "lucide-react";
import { Dot, LockIcon, LockOpenIcon, MessageSquareIcon, MountainSnow, Star } from "lucide-react";
import { useTranslation } from "react-i18next";
export interface NodeDetailProps {
@@ -104,9 +97,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
fill={node.isFavorite ? "black" : "none"}
size={15}
aria-label={
node.isFavorite
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")
node.isFavorite ? t("nodeDetail.favorite.label") : t("nodeDetail.notFavorite.label")
}
/>
</div>
@@ -127,15 +118,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div
className="flex gap-1"
title={new Date(node.lastHeard * 1000).toLocaleString(
navigator.language,
)}
title={new Date(node.lastHeard * 1000).toLocaleString(navigator.language)}
>
<div>
{node.lastHeard > 0 && (
<div>
{t("nodeDetail.status.heard")}{" "}
<TimeAgo timestamp={node.lastHeard * 1000} />
{t("nodeDetail.status.heard")} <TimeAgo timestamp={node.lastHeard * 1000} />
</div>
)}
</div>
@@ -153,13 +141,9 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div className="flex mt-2 text-sm">
<div className="flex items-center grow">
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
{Number.isNaN(node.hopsAway)
? t("unit.hopsAway.unknown")
: node.hopsAway}
</div>
<div>
{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}
{Number.isNaN(node.hopsAway) ? t("unit.hopsAway.unknown") : node.hopsAway}
</div>
<div>{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}</div>
</div>
{node.position?.altitude && (
<div className="flex items-center grow">
@@ -182,17 +166,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
{!!node.deviceMetrics?.channelUtilization && (
<div className="grow">
<div>{t("channelUtilization.short")}</div>
<Mono>
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
</Mono>
<Mono>{node.deviceMetrics?.channelUtilization.toPrecision(3)}%</Mono>
</div>
)}
{!!node.deviceMetrics?.airUtilTx && (
<div className="grow">
<div>{t("airtimeUtilization.short")}</div>
<Mono className="text-gray-500">
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
</Mono>
<Mono className="text-gray-500">{node.deviceMetrics?.airUtilTx.toPrecision(3)}%</Mono>
</div>
)}
</div>
@@ -2,12 +2,7 @@ import { TimeAgo } from "@components/generic/TimeAgo";
import { Separator } from "@components/UI/Separator.tsx";
import type { WaypointWithMetadata } from "@core/stores";
import { useNodeDB } from "@core/stores";
import {
bearingDegrees,
distanceMeters,
hasPos,
toLngLat,
} from "@core/utils/geo";
import { bearingDegrees, distanceMeters, hasPos, toLngLat } from "@core/utils/geo";
import type { Protobuf } from "@meshtastic/core";
import {
ClockFadingIcon,
@@ -88,14 +83,10 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<div className="flex flex-wrap items-start gap-x-3">
<dt className="inline-flex items-center gap-2 text-slate-500 min-w-0">
<ClockPlusIcon size={14} aria-hidden />
<span className="truncate">
{t("waypointDetail.createdDate")}
</span>
<span className="truncate">{t("waypointDetail.createdDate")}</span>
</dt>
<dd className="ms-auto text-right">
<time
dateTime={new Date(waypoint.metadata.created).toISOString()}
>
<time dateTime={new Date(waypoint.metadata.created).toISOString()}>
<TimeAgo timestamp={waypoint.metadata.created} />
</time>
</dd>
@@ -109,9 +100,7 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<span className="truncate">{t("waypointDetail.updated")}</span>
</dt>
<dd className="ms-auto text-right">
<time
dateTime={new Date(waypoint.metadata.updated).toISOString()}
>
<time dateTime={new Date(waypoint.metadata.updated).toISOString()}>
<TimeAgo timestamp={waypoint.metadata.updated} />
</time>
</dd>
@@ -143,9 +132,7 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<dd className="ms-auto text-right">
<data value={Math.round(distance)}>
{Math.round(distance)}{" "}
{distance === 1
? t("unit.meter.one")
: t("unit.meter.plural")}
{distance === 1 ? t("unit.meter.one") : t("unit.meter.plural")}
</data>
</dd>
</div>
@@ -179,8 +166,7 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<span className="truncate">{t("waypointDetail.lockedTo")}</span>
</dt>
<dd className="ms-auto text-right">
{getNode(waypoint.lockedTo)?.user?.longName ??
t("unknown.longName")}
{getNode(waypoint.lockedTo)?.user?.longName ?? t("unknown.longName")}
</dd>
</div>
)}
@@ -1,10 +1,6 @@
import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/UI/Popover.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "@components/UI/Popover.tsx";
import { cn } from "@core/utils/cn.ts";
import { LayersIcon } from "lucide-react";
import { type ReactNode, useMemo } from "react";
@@ -44,12 +40,7 @@ interface CheckboxProps {
className?: string;
}
const CheckboxItem = ({
label,
checked,
onChange,
className,
}: CheckboxProps) => {
const CheckboxItem = ({ label, checked, onChange, className }: CheckboxProps) => {
return (
<Checkbox
checked={checked}
@@ -149,9 +140,7 @@ export function MapLayerTool({
<CheckboxItem
label={label}
checked={visibilityState[key as keyof VisibilityState]}
onChange={() =>
handleCheckboxChange(key as keyof VisibilityState)
}
onChange={() => handleCheckboxChange(key as keyof VisibilityState)}
/>
{key === "heatmap" && visibilityState.heatmap && (
<div className="pl-6 pt-2 flex flex-col gap-1">
@@ -162,9 +151,7 @@ export function MapLayerTool({
checked={heatmapMode === "density"}
onChange={() => setHeatmapMode("density")}
/>
<span className="text-sm dark:text-slate-300">
{t("layerTool.density")}
</span>
<span className="text-sm dark:text-slate-300">{t("layerTool.density")}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
@@ -68,9 +68,7 @@ const DateDelimiter = ({ label }: { label: string }) => (
<li aria-label={label}>
<div className="my-2 flex h-3 items-center justify-center">
<Separator className="bg-slate-100 dark:bg-slate-800" />
<div className="mx-5 whitespace-nowrap text-center text-xs text-slate-400">
{label}
</div>
<div className="mx-5 whitespace-nowrap text-center text-xs text-slate-400">{label}</div>
<Separator className="bg-slate-100 dark:bg-slate-800" />
</div>
</li>
@@ -108,9 +106,7 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
const { i18n, t } = useTranslation();
const locale = useMemo(
() =>
i18n.language ||
(typeof navigator !== "undefined" ? navigator.language : "en-US"),
() => i18n.language || (typeof navigator !== "undefined" ? navigator.language : "en-US"),
[i18n.language],
);
@@ -14,10 +14,7 @@ interface MessageActionsMenuProps {
onReply?: () => void;
}
export const MessageActionsMenu = ({
onAddReaction,
onReply,
}: MessageActionsMenuProps) => {
export const MessageActionsMenu = ({ onAddReaction, onReply }: MessageActionsMenuProps) => {
const { t } = useTranslation();
const hoverIconBarClass = cn(
"absolute top-2 right-2",
@@ -1,23 +1,11 @@
import type { Types } from "@meshtastic/core";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MessageInput, type MessageInputProps } from "./MessageInput.tsx";
vi.mock("@components/UI/Button.tsx", () => ({
Button: vi.fn(({ type, className, children, onClick, onSubmit, ...rest }) => (
<button
type={type}
className={className}
onClick={onClick}
onSubmit={onSubmit}
{...rest}
>
<button type={type} className={className} onClick={onClick} onSubmit={onSubmit} {...rest}>
{children}
</button>
)),
@@ -155,9 +143,7 @@ describe("MessageInput", () => {
expect(mockOnSend).toHaveBeenCalledTimes(1);
expect(mockOnSend).toHaveBeenCalledWith(testMessage);
expect((inputElement as HTMLInputElement).value).toBe("");
expect(screen.getByTestId("byte-counter")).toHaveTextContent(
`0/${defaultProps.maxBytes}`,
);
expect(screen.getByTestId("byte-counter")).toHaveTextContent(`0/${defaultProps.maxBytes}`);
expect(mockClearDraft).toHaveBeenCalledTimes(1);
expect(mockClearDraft).toHaveBeenCalledWith(defaultProps.to);
});
@@ -222,20 +208,16 @@ describe("MessageInput", () => {
it("should work with broadcast destination for drafts", () => {
const broadcastDest: Types.Destination = "broadcast";
mockGetDraft.mockImplementation((key) =>
key === broadcastDest ? "Broadcast draft" : "",
);
mockGetDraft.mockImplementation((key) => (key === broadcastDest ? "Broadcast draft" : ""));
renderComponent({ to: broadcastDest });
expect(mockGetDraft).toHaveBeenCalledWith(broadcastDest);
expect(
(screen.getByTestId("message-input-field") as HTMLInputElement).value,
).toBe("Broadcast draft");
expect((screen.getByTestId("message-input-field") as HTMLInputElement).value).toBe(
"Broadcast draft",
);
const inputElement = screen.getByTestId(
"message-input-field",
) as HTMLInputElement;
const inputElement = screen.getByTestId("message-input-field") as HTMLInputElement;
const formElement = screen.getByRole("form");
const newMessage = "New broadcast msg";
@@ -20,9 +20,7 @@ export const MessageInput = ({ onSend, to, maxBytes }: MessageInputProps) => {
const initialDraft = getDraft(to);
const [localDraft, setLocalDraft] = useState(initialDraft);
const [messageBytes, setMessageBytes] = useState(() =>
calculateBytes(initialDraft),
);
const [messageBytes, setMessageBytes] = useState(() => calculateBytes(initialDraft));
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
@@ -34,9 +34,7 @@ function useSuspendingMyNode() {
const checkInterval = setInterval(() => {
const node = getMyNode();
if (node) {
console.log(
"[MessageItem] myNode now available, resolving promise",
);
console.log("[MessageItem] myNode now available, resolving promise");
clearInterval(checkInterval);
myNodePromises.delete(deviceKey);
resolve(node);
@@ -151,8 +149,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const longName = messageUser?.user?.longName;
const derivedShortName = messageUser?.user?.shortName || fallbackName;
const derivedDisplayName = longName || derivedShortName;
const isFavorite =
messageUser?.num !== myNodeNum && messageUser?.isFavorite;
const isFavorite = messageUser?.num !== myNodeNum && messageUser?.isFavorite;
return {
displayName: derivedDisplayName,
shortName: derivedShortName,
@@ -164,10 +161,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const messageStatusInfo = getMessageStatusInfo(message.state);
const StatusIconComponent = messageStatusInfo.icon;
const messageDate = useMemo(
() => (message.date ? new Date(message.date) : null),
[message.date],
);
const messageDate = useMemo(() => (message.date ? new Date(message.date) : null), [message.date]);
const locale = i18n.language;
const formattedTime = useMemo(
@@ -204,12 +198,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
return (
<li className={messageItemWrapperClass}>
<div className="grid grid-cols-[auto_1fr] gap-x-2">
<Avatar
size="sm"
nodeNum={nodeNum}
className="pt-0.5"
showFavorite={isFavorite}
/>
<Avatar size="sm" nodeNum={nodeNum} className="pt-0.5" showFavorite={isFavorite} />
<div className="flex flex-col gap-0.5 min-w-0">
<div className="flex items-center gap-1.5">
@@ -217,10 +206,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
{displayName}
</span>
{messageDate && (
<time
dateTime={messageDate.toISOString()}
className={dateTextStyle}
>
<time dateTime={messageDate.toISOString()} className={dateTextStyle}>
<span aria-hidden="true">{formattedTime}</span>
<span className="sr-only">{fullDateTime}</span>
</time>
@@ -229,10 +215,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
<StatusTooltip statusInfo={messageStatusInfo}>
<span aria-label={messageStatusInfo.ariaLabel} role="img">
<StatusIconComponent
className={cn(
"size-4 shrink-0",
messageStatusInfo.iconClassName,
)}
className={cn("size-4 shrink-0", messageStatusInfo.iconClassName)}
aria-hidden="true"
/>
</span>
@@ -74,14 +74,7 @@ describe("TraceRoute", () => {
});
it("renders the route to destination with SNR values", () => {
render(
<TraceRoute
from={fromUser}
to={toUser}
route={[1, 2]}
snrTowards={[10, 20, 30]}
/>,
);
render(<TraceRoute from={fromUser} to={toUser} route={[1, 2]} snrTowards={[10, 20, 30]} />);
expect(screen.getByText("Source Node")).toBeInTheDocument();
expect(screen.getByText("Destination Node")).toBeInTheDocument();
@@ -3,10 +3,7 @@ import {
type AmbientLightingValidation,
AmbientLightingValidationSchema,
} from "@app/validation/moduleConfig/ambientLighting.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -15,12 +12,9 @@ interface AmbientLightingModuleConfigProps {
onFormInit: DynamicFormFormInit<AmbientLightingValidation>;
}
export const AmbientLighting = ({
onFormInit,
}: AmbientLightingModuleConfigProps) => {
export const AmbientLighting = ({ onFormInit }: AmbientLightingModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AmbientLightingValidation) => {
@@ -1,12 +1,6 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type AudioValidation,
AudioValidationSchema,
} from "@app/validation/moduleConfig/audio.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { type AudioValidation, AudioValidationSchema } from "@app/validation/moduleConfig/audio.ts";
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";
@@ -18,8 +12,7 @@ interface AudioModuleConfigProps {
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "audio" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AudioValidation) => {
@@ -28,11 +21,7 @@ export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "audio" },
data,
moduleConfig.audio,
);
setChange({ type: "moduleConfig", variant: "audio" }, data, moduleConfig.audio);
};
return (
@@ -65,8 +54,7 @@ export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
label: t("audio.bitrate.label"),
description: t("audio.bitrate.description"),
properties: {
enumValue:
Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
enumValue: Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
},
},
{
@@ -3,10 +3,7 @@ import {
type CannedMessageValidation,
CannedMessageValidationSchema,
} from "@app/validation/moduleConfig/cannedMessage.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -16,13 +13,10 @@ interface CannedMessageModuleConfigProps {
onFormInit: DynamicFormFormInit<CannedMessageValidation>;
}
export const CannedMessage = ({
onFormInit,
}: CannedMessageModuleConfigProps) => {
export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: CannedMessageValidation) => {
@@ -31,11 +25,7 @@ export const CannedMessage = ({
return;
}
setChange(
{ type: "moduleConfig", variant: "cannedMessage" },
data,
moduleConfig.cannedMessage,
);
setChange({ type: "moduleConfig", variant: "cannedMessage" }, data, moduleConfig.cannedMessage);
};
return (
@@ -86,9 +76,7 @@ export const CannedMessage = ({
label: t("cannedMessage.inputbrokerEventCw.label"),
description: t("cannedMessage.inputbrokerEventCw.description"),
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -97,9 +85,7 @@ export const CannedMessage = ({
label: t("cannedMessage.inputbrokerEventCcw.label"),
description: t("cannedMessage.inputbrokerEventCcw.description"),
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -108,9 +94,7 @@ export const CannedMessage = ({
label: t("cannedMessage.inputbrokerEventPress.label"),
description: t("cannedMessage.inputbrokerEventPress.description"),
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -3,10 +3,7 @@ import {
type DetectionSensorValidation,
DetectionSensorValidationSchema,
} from "@app/validation/moduleConfig/detectionSensor.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -16,13 +13,10 @@ interface DetectionSensorModuleConfigProps {
onFormInit: DynamicFormFormInit<DetectionSensorValidation>;
}
export const DetectionSensor = ({
onFormInit,
}: DetectionSensorModuleConfigProps) => {
export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: DetectionSensorValidation) => {
@@ -60,9 +54,7 @@ export const DetectionSensor = ({
type: "number",
name: "minimumBroadcastSecs",
label: t("detectionSensor.minimumBroadcastSecs.label"),
description: t(
"detectionSensor.minimumBroadcastSecs.description",
),
description: t("detectionSensor.minimumBroadcastSecs.description"),
properties: {
suffix: t("unit.second.plural"),
},
@@ -120,18 +112,14 @@ export const DetectionSensor = ({
type: "select",
name: "detectionTriggerType",
label: t("detectionSensor.detectionTriggerType.label"),
description: t(
"detectionSensor.detectionTriggerType.description",
),
description: t("detectionSensor.detectionTriggerType.description"),
disabledBy: [
{
fieldName: "enabled",
},
],
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_DetectionSensorConfig_TriggerType,
enumValue: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig_TriggerType,
},
},
{
@@ -3,10 +3,7 @@ import {
type ExternalNotificationValidation,
ExternalNotificationValidationSchema,
} from "@app/validation/moduleConfig/externalNotification.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -15,13 +12,10 @@ interface ExternalNotificationModuleConfigProps {
onFormInit: DynamicFormFormInit<ExternalNotificationValidation>;
}
export const ExternalNotification = ({
onFormInit,
}: ExternalNotificationModuleConfigProps) => {
export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "externalNotification" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: ExternalNotificationValidation) => {
@@ -129,9 +123,7 @@ export const ExternalNotification = ({
type: "toggle",
name: "alertMessageVibra",
label: t("externalNotification.alertMessageVibra.label"),
description: t(
"externalNotification.alertMessageVibra.description",
),
description: t("externalNotification.alertMessageVibra.description"),
disabledBy: [
{
fieldName: "enabled",
@@ -142,9 +134,7 @@ export const ExternalNotification = ({
type: "toggle",
name: "alertMessageBuzzer",
label: t("externalNotification.alertMessageBuzzer.label"),
description: t(
"externalNotification.alertMessageBuzzer.description",
),
description: t("externalNotification.alertMessageBuzzer.description"),
disabledBy: [
{
fieldName: "enabled",
@@ -177,9 +167,7 @@ export const ExternalNotification = ({
type: "toggle",
name: "alertBellBuzzer",
label: t("externalNotification.alertBellBuzzer.label"),
description: t(
"externalNotification.alertBellBuzzer.description",
),
description: t("externalNotification.alertBellBuzzer.description"),
disabledBy: [
{
fieldName: "enabled",
@@ -1,13 +1,7 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type MqttValidation,
MqttValidationSchema,
} from "@app/validation/moduleConfig/mqtt.ts";
import { type MqttValidation, MqttValidationSchema } from "@app/validation/moduleConfig/mqtt.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -20,13 +14,7 @@ interface MqttModuleConfigProps {
export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "mqtt" });
const {
config,
moduleConfig,
setChange,
getEffectiveModuleConfig,
removeChange,
} = useDevice();
const { config, moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: MqttValidation) => {
@@ -43,11 +31,7 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "mqtt" },
payload,
moduleConfig.mqtt,
);
setChange({ type: "moduleConfig", variant: "mqtt" }, payload, moduleConfig.mqtt);
};
const populateDefaultValues = (
@@ -185,9 +169,7 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
type: "number",
name: "mapReportSettings.publishIntervalSecs",
label: t("mqtt.mapReportSettings.publishIntervalSecs.label"),
description: t(
"mqtt.mapReportSettings.publishIntervalSecs.description",
),
description: t("mqtt.mapReportSettings.publishIntervalSecs.description"),
properties: {
suffix: t("unit.second.plural"),
},
@@ -204,75 +186,33 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
type: "select",
name: "mapReportSettings.positionPrecision",
label: t("mqtt.mapReportSettings.positionPrecision.label"),
description: t(
"mqtt.mapReportSettings.positionPrecision.description",
),
description: t("mqtt.mapReportSettings.positionPrecision.description"),
properties: {
enumValue:
config.display?.units === 0
? {
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_km23",
)]: 10,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_km12",
)]: 11,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_km5_8",
)]: 12,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_km2_9",
)]: 13,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_km1_5",
)]: 14,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m700",
)]: 15,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m350",
)]: 16,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m200",
)]: 17,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m90",
)]: 18,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m50",
)]: 19,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")]: 10,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")]: 11,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")]: 12,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km2_9")]: 13,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km1_5")]: 14,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m700")]: 15,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m350")]: 16,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m200")]: 17,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m90")]: 18,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m50")]: 19,
}
: {
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi15",
)]: 10,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3",
)]: 11,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6",
)]: 12,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8",
)]: 13,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9",
)]: 14,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5",
)]: 15,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_2",
)]: 16,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft600",
)]: 17,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft300",
)]: 18,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft150",
)]: 19,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi15")]: 10,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3")]: 11,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6")]: 12,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8")]: 13,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9")]: 14,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5")]: 15,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_2")]: 16,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft600")]: 17,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft300")]: 18,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft150")]: 19,
},
},
disabledBy: [
@@ -3,10 +3,7 @@ import {
type NeighborInfoValidation,
NeighborInfoValidationSchema,
} from "@app/validation/moduleConfig/neighborInfo.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -18,8 +15,7 @@ interface NeighborInfoModuleConfigProps {
export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "neighborInfo" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: NeighborInfoValidation) => {
@@ -28,11 +24,7 @@ export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "neighborInfo" },
data,
moduleConfig.neighborInfo,
);
setChange({ type: "moduleConfig", variant: "neighborInfo" }, data, moduleConfig.neighborInfo);
};
return (
@@ -3,10 +3,7 @@ import {
type PaxcounterValidation,
PaxcounterValidationSchema,
} from "@app/validation/moduleConfig/paxcounter.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -18,8 +15,7 @@ interface PaxcounterModuleConfigProps {
export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "paxcounter" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: PaxcounterValidation) => {
@@ -28,11 +24,7 @@ export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "paxcounter" },
data,
moduleConfig.paxcounter,
);
setChange({ type: "moduleConfig", variant: "paxcounter" }, data, moduleConfig.paxcounter);
};
return (
@@ -3,10 +3,7 @@ import {
type RangeTestValidation,
RangeTestValidationSchema,
} from "@app/validation/moduleConfig/rangeTest.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -18,8 +15,7 @@ interface RangeTestModuleConfigProps {
export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "rangeTest" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
@@ -29,11 +25,7 @@ export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "rangeTest" },
data,
moduleConfig.rangeTest,
);
setChange({ type: "moduleConfig", variant: "rangeTest" }, data, moduleConfig.rangeTest);
};
return (
@@ -3,10 +3,7 @@ import {
type SerialValidation,
SerialValidationSchema,
} from "@app/validation/moduleConfig/serial.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -19,8 +16,7 @@ interface SerialModuleConfigProps {
export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "serial" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: SerialValidation) => {
@@ -29,11 +25,7 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "serial" },
data,
moduleConfig.serial,
);
setChange({ type: "moduleConfig", variant: "serial" }, data, moduleConfig.serial);
};
return (
@@ -99,8 +91,7 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
},
],
properties: {
enumValue:
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
},
},
{
@@ -129,8 +120,7 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
},
],
properties: {
enumValue:
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
formatEnumName: true,
},
},
@@ -3,10 +3,7 @@ import {
type StoreForwardValidation,
StoreForwardValidationSchema,
} from "@app/validation/moduleConfig/storeForward.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -18,8 +15,7 @@ interface StoreForwardModuleConfigProps {
export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "storeForward" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: StoreForwardValidation) => {
@@ -28,11 +24,7 @@ export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "storeForward" },
data,
moduleConfig.storeForward,
);
setChange({ type: "moduleConfig", variant: "storeForward" }, data, moduleConfig.storeForward);
};
return (
@@ -3,10 +3,7 @@ import {
type TelemetryValidation,
TelemetryValidationSchema,
} from "@app/validation/moduleConfig/telemetry.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -18,8 +15,7 @@ interface TelemetryModuleConfigProps {
export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "telemetry" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TelemetryValidation) => {
@@ -28,11 +24,7 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
return;
}
setChange(
{ type: "moduleConfig", variant: "telemetry" },
data,
moduleConfig.telemetry,
);
setChange({ type: "moduleConfig", variant: "telemetry" }, data, moduleConfig.telemetry);
};
return (
@@ -69,9 +61,7 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
type: "toggle",
name: "environmentMeasurementEnabled",
label: t("telemetry.environmentMeasurementEnabled.label"),
description: t(
"telemetry.environmentMeasurementEnabled.description",
),
description: t("telemetry.environmentMeasurementEnabled.description"),
},
{
type: "toggle",
@@ -83,9 +73,7 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
type: "toggle",
name: "environmentDisplayFahrenheit",
label: t("telemetry.environmentDisplayFahrenheit.label"),
description: t(
"telemetry.environmentDisplayFahrenheit.description",
),
description: t("telemetry.environmentDisplayFahrenheit.description"),
},
{
type: "toggle",
@@ -3,10 +3,7 @@ import {
type BluetoothValidation,
BluetoothValidationSchema,
} from "@app/validation/config/bluetooth.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -1,13 +1,7 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type DeviceValidation,
DeviceValidationSchema,
} from "@app/validation/config/device.ts";
import { type DeviceValidation, DeviceValidationSchema } from "@app/validation/config/device.ts";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
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";
@@ -106,8 +100,7 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
properties: {
fieldLength: {
max: 64,
currentValueLength:
getEffectiveConfig("device")?.tzdef?.length,
currentValueLength: getEffectiveConfig("device")?.tzdef?.length,
showCharacterCount: true,
},
},
@@ -1,12 +1,6 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type DisplayValidation,
DisplayValidationSchema,
} from "@app/validation/config/display.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { type DisplayValidation, DisplayValidationSchema } from "@app/validation/config/display.ts";
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";
@@ -60,9 +54,7 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
label: t("display.gpsDisplayUnits.label"),
description: t("display.gpsDisplayUnits.description"),
properties: {
enumValue:
Protobuf.Config
.Config_DisplayConfig_DeprecatedGpsCoordinateFormat,
enumValue: Protobuf.Config.Config_DisplayConfig_DeprecatedGpsCoordinateFormat,
},
},
{
@@ -1,12 +1,6 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type LoRaValidation,
LoRaValidationSchema,
} from "@app/validation/config/lora.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { type LoRaValidation, LoRaValidationSchema } from "@app/validation/config/lora.ts";
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";
@@ -1,19 +1,10 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type NetworkValidation,
NetworkValidationSchema,
} from "@app/validation/config/network.ts";
import { type NetworkValidation, NetworkValidationSchema } from "@app/validation/config/network.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import {
convertIntToIpAddress,
convertIpAddressToInt,
} from "@core/utils/ip.ts";
import { convertIntToIpAddress, convertIpAddressToInt } from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
@@ -31,15 +22,12 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
const onSubmit = (data: NetworkValidation) => {
const payload = {
...data,
ipv4Config: create(
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
{
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
},
),
ipv4Config: create(Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, {
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
}),
};
if (deepCompareConfig(config.network, payload, true)) {
@@ -58,12 +46,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
...config.network,
ipv4Config: {
ip: convertIntToIpAddress(config.network?.ipv4Config?.ip ?? 0),
gateway: convertIntToIpAddress(
config.network?.ipv4Config?.gateway ?? 0,
),
subnet: convertIntToIpAddress(
config.network?.ipv4Config?.subnet ?? 0,
),
gateway: convertIntToIpAddress(config.network?.ipv4Config?.gateway ?? 0),
subnet: convertIntToIpAddress(config.network?.ipv4Config?.subnet ?? 0),
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
},
enabledProtocols:
@@ -75,12 +59,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
...networkConfig,
ipv4Config: {
ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0),
gateway: convertIntToIpAddress(
networkConfig?.ipv4Config?.gateway ?? 0,
),
subnet: convertIntToIpAddress(
networkConfig?.ipv4Config?.subnet ?? 0,
),
gateway: convertIntToIpAddress(networkConfig?.ipv4Config?.gateway ?? 0),
subnet: convertIntToIpAddress(networkConfig?.ipv4Config?.subnet ?? 0),
dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0),
},
enabledProtocols:
@@ -157,8 +137,7 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -170,8 +149,7 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -183,8 +161,7 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -196,8 +173,7 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -4,14 +4,8 @@ import {
PositionValidationSchema,
} from "@app/validation/config/position.ts";
import { create } from "@bufbuild/protobuf";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import {
type FlagName,
usePositionFlags,
} from "@core/hooks/usePositionFlags.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { type FlagName, usePositionFlags } from "@core/hooks/usePositionFlags.ts";
import { useDevice, useNodeDB } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
@@ -24,13 +18,7 @@ interface PositionConfigProps {
export const Position = ({ onFormInit }: PositionConfigProps) => {
useWaitForConfig({ configCase: "position" });
const {
setChange,
config,
getEffectiveConfig,
removeChange,
queueAdminMessage,
} = useDevice();
const { setChange, config, getEffectiveConfig, removeChange, queueAdminMessage } = useDevice();
const { getMyNode } = useNodeDB();
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
getEffectiveConfig("position")?.positionFlags ?? 0,
@@ -48,24 +36,15 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
...config.position,
...effectiveConfig,
// Include current position coordinates if available
latitude: currentPosition?.latitudeI
? currentPosition.latitudeI / 1e7
: undefined,
longitude: currentPosition?.longitudeI
? currentPosition.longitudeI / 1e7
: undefined,
latitude: currentPosition?.latitudeI ? currentPosition.latitudeI / 1e7 : undefined,
longitude: currentPosition?.longitudeI ? currentPosition.longitudeI / 1e7 : undefined,
altitude: currentPosition?.altitude ?? 0,
} as PositionValidation;
}, [config.position, effectiveConfig, currentPosition]);
const onSubmit = (data: PositionValidation) => {
// Exclude position coordinates from config payload (they're handled via admin message)
const {
latitude: _latitude,
longitude: _longitude,
altitude: _altitude,
...configData
} = data;
const { latitude: _latitude, longitude: _longitude, altitude: _altitude, ...configData } = data;
const payload = { ...configData, positionFlags: flagsValue };
// Save config first
@@ -74,19 +53,11 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
removeChange({ type: "config", variant: "position" });
configResult = undefined;
} else {
configResult = setChange(
{ type: "config", variant: "position" },
payload,
config.position,
);
configResult = setChange({ type: "config", variant: "position" }, payload, config.position);
}
// Then handle position coordinates via admin message if fixedPosition is enabled
if (
data.fixedPosition &&
data.latitude !== undefined &&
data.longitude !== undefined
) {
if (data.fixedPosition && data.latitude !== undefined && data.longitude !== undefined) {
const message = create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: "setFixedPosition",
@@ -150,8 +121,7 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
disabledBy: [
{
fieldName: "gpsMode",
selector:
Protobuf.Config.Config_PositionConfig_GpsMode.ENABLED,
selector: Protobuf.Config.Config_PositionConfig_GpsMode.ENABLED,
},
],
},
@@ -198,16 +168,14 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
label: t("position.fixedPosition.altitude.label"),
description: t("position.fixedPosition.altitude.description", {
unit:
displayUnits ===
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
? "Feet"
: "Meters",
}),
properties: {
step: 0.0000001,
suffix:
displayUnits ===
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
? "Feet"
: "Meters",
},
@@ -221,8 +189,7 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
type: "multiSelect",
name: "positionFlags",
value: activeFlags,
isChecked: (name: string) =>
activeFlags?.includes(name as FlagName) ?? false,
isChecked: (name: string) => activeFlags?.includes(name as FlagName) ?? false,
onValueChange: onPositonFlagChange,
label: t("position.positionFlags.label"),
placeholder: t("position.flags.placeholder"),
@@ -1,12 +1,6 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type PowerValidation,
PowerValidationSchema,
} from "@app/validation/config/power.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { type PowerValidation, PowerValidationSchema } from "@app/validation/config/power.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -7,10 +7,7 @@ import {
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
@@ -25,8 +22,7 @@ interface SecurityConfigProps {
export const Security = ({ onFormInit }: SecurityConfigProps) => {
useWaitForConfig({ configCase: "security" });
const { config, setChange, setDialogOpen, getEffectiveConfig, removeChange } =
useDevice();
const { config, setChange, setDialogOpen, getEffectiveConfig, removeChange } = useDevice();
const { t } = useTranslation("config");
@@ -48,9 +44,7 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
const formValues = {
...effectiveConfig,
...{
privateKey: fromByteArray(
effectiveConfig?.privateKey ?? new Uint8Array(0),
),
privateKey: fromByteArray(effectiveConfig?.privateKey ?? new Uint8Array(0)),
publicKey: fromByteArray(effectiveConfig?.publicKey ?? new Uint8Array(0)),
adminKey: [
fromByteArray(effectiveConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
@@ -74,10 +68,8 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
onFormInit?.(formMethods);
}, [onFormInit, formMethods]);
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] =
useState<boolean>(false);
const [managedModeDialogOpen, setManagedModeDialogOpen] =
useState<boolean>(false);
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState<boolean>(false);
const [managedModeDialogOpen, setManagedModeDialogOpen] = useState<boolean>(false);
const onSubmit = (data: RawSecurity) => {
if (!formState.isReady) {
@@ -100,11 +92,7 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
return;
}
setChange(
{ type: "config", variant: "security" },
payload,
config.security,
);
setChange({ type: "config", variant: "security" }, payload, config.security);
};
const pkiRegenerate = () => {
@@ -114,14 +102,12 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
const updatePublicKey = async (privateKey: string) => {
try {
const publicKey = fromByteArray(
getX25519PublicKey(toByteArray(privateKey)),
);
const publicKey = fromByteArray(getX25519PublicKey(toByteArray(privateKey)));
setValue("privateKey", privateKey, { shouldDirty: true });
setValue("publicKey", publicKey, { shouldDirty: true });
setPrivateKeyDialogOpen(false);
} catch (_e) {
} catch {
setValue("privateKey", privateKey, { shouldDirty: true });
}
const valid = await trigger(["privateKey", "publicKey"]);
@@ -202,9 +188,7 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
properties: {
showCopyButton: true,
},
@@ -218,9 +202,7 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
properties: {
showCopyButton: true,
},
@@ -234,9 +216,7 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
properties: {
showCopyButton: true,
},
@@ -1,12 +1,6 @@
import {
type UserValidation,
UserValidationSchema,
} from "@app/validation/config/user.ts";
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 { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice, useNodeDB } from "@core/stores";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
@@ -29,9 +23,7 @@ export const User = ({ onFormInit }: UserConfigProps) => {
};
// Get working copy from change registry
const workingUser = getChange({ type: "user" }) as
| Protobuf.Mesh.User
| undefined;
const workingUser = getChange({ type: "user" }) as Protobuf.Mesh.User | undefined;
const effectiveUser = workingUser ?? defaultUser;
+3 -11
View File
@@ -68,9 +68,7 @@ export const PageLayout = ({
>
{/* Header Content */}
<div className="flex flex-1 items-center justify-between min-w-0">
<span className="text-lg font-medium text-foreground truncate px-2">
{label}
</span>
<span className="text-lg font-medium text-foreground truncate px-2">{label}</span>
<div className="flex items-center space-x-1 md:space-x-2 shrink-0 pr-6">
{actions?.map((action) => {
return (
@@ -93,15 +91,9 @@ export const PageLayout = ({
(action.isLoading ? (
<Spinner size="md" />
) : (
<action.icon
className={cn("h-5 w-5", action.iconClasses)}
/>
<action.icon className={cn("h-5 w-5", action.iconClasses)} />
))}
{action.label && (
<span className="text-sm px-1 pt-0.5">
{action.label}
</span>
)}
{action.label && <span className="text-sm px-1 pt-0.5">{action.label}</span>}
</button>
);
})}
+5 -18
View File
@@ -60,10 +60,7 @@ const CollapseToggleButton = () => {
>
<CircleChevronLeft
size={24}
className={cn(
"transition-transform duration-300 ease-in-out",
isCollapsed && "rotate-180",
)}
className={cn("transition-transform duration-300 ease-in-out", isCollapsed && "rotate-180")}
/>
</button>
);
@@ -147,18 +144,12 @@ export const Sidebar = ({ children }: SidebarProps) => {
isCollapsed && "justify-center px-0",
)}
>
<img
src="/logo.svg"
alt={t("app.logo")}
className="size-10 flex-shrink-0 rounded-xl"
/>
<img src="/logo.svg" alt={t("app.logo")} className="size-10 flex-shrink-0 rounded-xl" />
<h2
className={cn(
"text-xl font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap",
"transition-all duration-300 ease-in-out",
isCollapsed
? "opacity-0 max-w-0 invisible ml-0"
: "opacity-100 max-w-xs visible ml-2",
isCollapsed ? "opacity-0 max-w-0 invisible ml-0" : "opacity-100 max-w-xs visible ml-2",
)}
>
{t("app.title")}
@@ -185,9 +176,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
})}
</SidebarSection>
<div className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")}>
{children}
</div>
<div className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")}>{children}</div>
<div className=" pt-4 border-t-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700 h-full flex-1">
{myNode === undefined ? (
@@ -208,9 +197,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
setCommandPaletteOpen={() => setCommandPaletteOpen(true)}
setDialogOpen={() => setDialogOpen("deviceName", true)}
user={myNode.user}
firmwareVersion={
myMetadata?.firmwareVersion ?? t("unknown.notAvailable")
}
firmwareVersion={myMetadata?.firmwareVersion ?? t("unknown.notAvailable")}
deviceMetrics={{
batteryLevel: myNode.deviceMetrics?.batteryLevel,
voltage:
@@ -42,8 +42,7 @@ export default function ThemeSwitcher({
const toggleTheme = () => {
const preferences: ThemePreference[] = ["light", "dark", "system"];
const currentIndex = preferences.indexOf(preference);
const nextPreference =
preferences[(currentIndex + 1) % preferences.length] ?? "system";
const nextPreference = preferences[(currentIndex + 1) % preferences.length] ?? "system";
setPreference(nextPreference);
toggleShowTooltip();
};
@@ -90,8 +89,7 @@ export default function ThemeSwitcher({
"text-sm",
"text-gray-600 dark:text-gray-300",
"transition-colors duration-150",
!disableHover &&
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100",
)}
>
{t("theme.changeTheme")}
+1 -6
View File
@@ -14,12 +14,7 @@ export function Toaster() {
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, duration, ...props }) => (
<Toast
key={id}
{...props}
duration={duration}
className="flex flex-col gap-4"
>
<Toast key={id} {...props} duration={duration} className="flex flex-col gap-4">
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
+1 -4
View File
@@ -35,10 +35,7 @@ export const AccordionContent = forwardRef<
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"p-4 border-b border-slate-200 dark:border-slate-800",
className,
)}
className={cn("p-4 border-b border-slate-200 dark:border-slate-800", className)}
{...props}
/>
));
+9 -39
View File
@@ -7,10 +7,7 @@ const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = ({
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
const AlertDialogPortal = ({ children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
@@ -52,29 +49,14 @@ const AlertDialogContent = React.forwardRef<
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
@@ -86,10 +68,7 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900 dark:text-slate-100",
className,
)}
className={cn("text-lg font-semibold text-slate-900 dark:text-slate-100", className)}
{...props}
/>
));
@@ -105,18 +84,13 @@ const AlertDialogDescription = React.forwardRef<
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
@@ -126,11 +100,7 @@ const AlertDialogCancel = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className,
)}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
+2 -5
View File
@@ -23,13 +23,10 @@ const badgeVariants = cva(
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+3 -6
View File
@@ -9,10 +9,8 @@ const buttonVariants = cva(
variant: {
default:
"bg-slate-900 text-white dark:bg-slate-50 hover:dark:bg-slate-200 dark:text-slate-900 hover:bg-slate-500",
destructive:
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
success:
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
destructive: "bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
success: "bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
outline:
"bg-transparent border border-slate-400 hover:text-slate-400 dark:hover:text-slate-300 dark:border-slate-400 dark:text-slate-100 ",
subtle:
@@ -38,8 +36,7 @@ const buttonVariants = cva(
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
icon?: React.ReactNode;
iconAlignment?: "left" | "right";
}
+40 -65
View File
@@ -1,85 +1,60 @@
import { cn } from "@core/utils/cn.ts";
import * as React from "react";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-slate-200 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100",
className,
)}
{...props}
/>
));
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-slate-200 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100",
className,
)}
{...props}
/>
),
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, children, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
>
{children}
</h3>
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-slate-500 dark:text-slate-400", className)}
{...props}
/>
<p ref={ref} className={cn("text-sm text-slate-500 dark:text-slate-400", className)} {...props} />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
),
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
};
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -58,9 +58,7 @@ describe("Checkbox", () => {
it("controlled: reflects external prop changes after onChange", () => {
const onChange = vi.fn();
const { rerender } = render(
<Checkbox checked={false} onChange={onChange} />,
);
const { rerender } = render(<Checkbox checked={false} onChange={onChange} />);
const checkbox = screen.getByRole("checkbox");
const presentation = screen.getByRole("presentation");
@@ -82,9 +80,7 @@ describe("Checkbox", () => {
it("renders children inside the label", () => {
render(<Checkbox>Test Label</Checkbox>);
expect(screen.getByTestId("label-component")).toHaveTextContent(
"Test Label",
);
expect(screen.getByTestId("label-component")).toHaveTextContent("Test Label");
});
it("applies custom className to wrapper label", () => {
@@ -117,10 +113,7 @@ describe("Checkbox", () => {
it("passes through additional props to the input", () => {
render(<Checkbox data-testid="extra-prop" />);
expect(screen.getByRole("checkbox")).toHaveAttribute(
"data-testid",
"extra-prop",
);
expect(screen.getByRole("checkbox")).toHaveAttribute("data-testid", "extra-prop");
});
it("uncontrolled: toggles checked state when clicking the visual box", () => {
@@ -82,11 +82,7 @@ export function Checkbox({
</div>
)}
</div>
{children && (
<span className="text-sm text-gray-900 dark:text-gray-100">
{children}
</span>
)}
{children && <span className="text-sm text-gray-900 dark:text-gray-100">{children}</span>}
</label>
);
}
+3 -16
View File
@@ -75,11 +75,7 @@ const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
@@ -128,18 +124,9 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
className,
)}
{...props}
/>
<span className={cn("ml-auto text-xs tracking-widest text-slate-500", className)} {...props} />
);
};
CommandShortcut.displayName = "CommandShortcut";
+6 -27
View File
@@ -7,10 +7,7 @@ const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
const DialogPortal = ({ children, ...props }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
@@ -72,29 +69,14 @@ const DialogClose = ({
</DialogPrimitive.Close>
);
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left ",
className,
)}
{...props}
/>
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left ", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
@@ -106,10 +88,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900 dark:text-slate-100",
className,
)}
className={cn("text-lg font-semibold text-slate-900 dark:text-slate-100", className)}
{...props}
/>
));
@@ -34,8 +34,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -50,8 +49,7 @@ const DropdownMenuSubContent = React.forwardRef<
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -110,8 +108,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -165,18 +162,9 @@ const DropdownMenuSeparator = React.forwardRef<
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
className,
)}
{...props}
/>
<span className={cn("ml-auto text-xs tracking-widest text-slate-500", className)} {...props} />
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";

Some files were not shown because too many files have changed in this diff Show More