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:
@@ -4,10 +4,7 @@ import LanguageSwitcher from "@app/components/LanguageSwitcher";
|
||||
import { ConnectionStatusBadge } from "@app/components/PageComponents/Connections/ConnectionStatusBadge";
|
||||
import type { Connection } from "@app/core/stores/deviceStore/types";
|
||||
import { useConnections } from "@app/pages/Connections/useConnections";
|
||||
import {
|
||||
connectionTypeIcon,
|
||||
formatConnectionSubtext,
|
||||
} from "@app/pages/Connections/utils";
|
||||
import { connectionTypeIcon, formatConnectionSubtext } from "@app/pages/Connections/utils";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -21,13 +18,7 @@ import {
|
||||
} from "@components/UI/AlertDialog.tsx";
|
||||
import { Badge } from "@components/UI/Badge.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@components/UI/Card.tsx";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@components/UI/Card.tsx";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -68,11 +59,12 @@ export const Connections = () => {
|
||||
const { t } = useTranslation("connections");
|
||||
|
||||
// On first mount, sync statuses and refresh
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: This can cause the icon to refresh too often
|
||||
useEffect(() => {
|
||||
syncConnectionStatuses();
|
||||
refreshStatuses();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const copy = [...connections];
|
||||
return copy.sort((a, b) => {
|
||||
@@ -104,9 +96,7 @@ export const Connections = () => {
|
||||
<ArrowLeft className="size-5" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
|
||||
{t("page.title")}
|
||||
</h1>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">{t("page.title")}</h1>
|
||||
<p className="lg:w-4/6 md:w-5/6 text-slate-500 dark:text-slate-400 mt-1">
|
||||
{t("page.description")}
|
||||
</p>
|
||||
@@ -126,9 +116,7 @@ export const Connections = () => {
|
||||
{sorted.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
{t("noConnections.title")}{" "}
|
||||
</CardTitle>
|
||||
<CardTitle className="text-lg">{t("noConnections.title")} </CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-slate-500 dark:text-slate-400">
|
||||
{t("noConnections.description")}
|
||||
@@ -227,10 +215,7 @@ export const Connections = () => {
|
||||
interpolation: { escapeValue: false },
|
||||
}),
|
||||
});
|
||||
if (
|
||||
created.status === "connected" ||
|
||||
created.status === "configured"
|
||||
) {
|
||||
if (created.status === "connected" || created.status === "configured") {
|
||||
navigate({ to: "/" });
|
||||
}
|
||||
} else {
|
||||
@@ -247,8 +232,7 @@ export const Connections = () => {
|
||||
|
||||
function TypeBadge({ type }: { type: Connection["type"] }) {
|
||||
const Icon = connectionTypeIcon(type);
|
||||
const label =
|
||||
type === "http" ? "HTTP" : type === "bluetooth" ? "Bluetooth" : "Serial";
|
||||
const label = type === "http" ? "HTTP" : type === "bluetooth" ? "Bluetooth" : "Serial";
|
||||
return (
|
||||
<Badge variant="default" className="gap-1.5">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
@@ -275,10 +259,8 @@ function ConnectionCard({
|
||||
const { t } = useTranslation("connections");
|
||||
|
||||
const Icon = connectionTypeIcon(connection.type);
|
||||
const isBusy =
|
||||
connection.status === "connecting" || connection.status === "configuring";
|
||||
const isConnected =
|
||||
connection.status === "connected" || connection.status === "configured";
|
||||
const isBusy = connection.status === "connecting" || connection.status === "configuring";
|
||||
const isConnected = connection.status === "connected" || connection.status === "configured";
|
||||
const isError = connection.status === "error";
|
||||
|
||||
return (
|
||||
@@ -315,19 +297,13 @@ function ConnectionCard({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{connection.type === "http" && connection.isDefault && (
|
||||
<DropdownMenuItem
|
||||
className="gap-2"
|
||||
onClick={() => onSetDefault()}
|
||||
>
|
||||
<DropdownMenuItem className="gap-2" onClick={() => onSetDefault()}>
|
||||
<StarOff className="size-4" />
|
||||
{t("button.unsetDefault")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{connection.type === "http" && !connection.isDefault && (
|
||||
<DropdownMenuItem
|
||||
className="gap-2"
|
||||
onClick={() => onSetDefault()}
|
||||
>
|
||||
<DropdownMenuItem className="gap-2" onClick={() => onSetDefault()}>
|
||||
<Star className="size-4" />
|
||||
{t("button.setDefault")}
|
||||
</DropdownMenuItem>
|
||||
@@ -344,17 +320,13 @@ function ConnectionCard({
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("deleteConnection")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("deleteConnection")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("areYouSure", { name: connection.name })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
onClick={() => onDelete()}
|
||||
@@ -371,9 +343,7 @@ function ConnectionCard({
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{connection.error ? (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
{connection.error}
|
||||
</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{connection.error}</p>
|
||||
) : connection.lastConnectedAt ? (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t("lastConnectedAt", { date: "" })}{" "}
|
||||
@@ -383,9 +353,7 @@ function ConnectionCard({
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t("neverConnected")}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{t("neverConnected")}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center gap-2 mt-auto">
|
||||
|
||||
@@ -4,16 +4,8 @@ import type {
|
||||
ConnectionStatus,
|
||||
NewConnection,
|
||||
} from "@app/core/stores/deviceStore/types";
|
||||
import {
|
||||
createConnectionFromInput,
|
||||
testHttpReachable,
|
||||
} from "@app/pages/Connections/utils";
|
||||
import {
|
||||
useAppStore,
|
||||
useDeviceStore,
|
||||
useMessageStore,
|
||||
useNodeDBStore,
|
||||
} from "@core/stores";
|
||||
import { createConnectionFromInput, testHttpReachable } from "@app/pages/Connections/utils";
|
||||
import { useAppStore, useDeviceStore, useMessageStore, useNodeDBStore } from "@core/stores";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
@@ -35,9 +27,7 @@ export function useConnections() {
|
||||
|
||||
const addSavedConnection = useDeviceStore((s) => s.addSavedConnection);
|
||||
const updateSavedConnection = useDeviceStore((s) => s.updateSavedConnection);
|
||||
const removeSavedConnectionFromStore = useDeviceStore(
|
||||
(s) => s.removeSavedConnection,
|
||||
);
|
||||
const removeSavedConnectionFromStore = useDeviceStore((s) => s.removeSavedConnection);
|
||||
|
||||
// DeviceStore methods
|
||||
const setActiveConnectionId = useDeviceStore((s) => s.setActiveConnectionId);
|
||||
@@ -77,9 +67,7 @@ export function useConnections() {
|
||||
if (unsubConfigComplete) {
|
||||
unsubConfigComplete();
|
||||
configSubscriptions.delete(id);
|
||||
console.log(
|
||||
`[useConnections] Config subscription cleaned up for connection ${id}`,
|
||||
);
|
||||
console.log(`[useConnections] Config subscription cleaned up for connection ${id}`);
|
||||
}
|
||||
|
||||
// Get device and MeshDevice from Device.connection
|
||||
@@ -178,9 +166,7 @@ export function useConnections() {
|
||||
// Listen for config complete event (with nonce/ID)
|
||||
const unsubConfigComplete = meshDevice.events.onConfigComplete.subscribe(
|
||||
(configCompleteId) => {
|
||||
console.log(
|
||||
`[useConnections] Configuration complete with ID: ${configCompleteId}`,
|
||||
);
|
||||
console.log(`[useConnections] Configuration complete with ID: ${configCompleteId}`);
|
||||
device.setConnectionPhase("configured");
|
||||
updateStatus(id, "configured");
|
||||
|
||||
@@ -188,9 +174,7 @@ export function useConnections() {
|
||||
const oldHeartbeat = heartbeats.get(id);
|
||||
if (oldHeartbeat) {
|
||||
clearInterval(oldHeartbeat);
|
||||
console.log(
|
||||
`[useConnections] Switching to maintenance heartbeat (5 min interval)`,
|
||||
);
|
||||
console.log(`[useConnections] Switching to maintenance heartbeat (5 min interval)`);
|
||||
}
|
||||
|
||||
const maintenanceHeartbeat = setInterval(() => {
|
||||
@@ -211,9 +195,7 @@ export function useConnections() {
|
||||
meshDevice
|
||||
.configure()
|
||||
.then(() => {
|
||||
console.log(
|
||||
"[useConnections] Configuration complete, starting heartbeat",
|
||||
);
|
||||
console.log("[useConnections] Configuration complete, starting heartbeat");
|
||||
// Send initial heartbeat after configure completes
|
||||
meshDevice
|
||||
.heartbeat()
|
||||
@@ -221,10 +203,7 @@ export function useConnections() {
|
||||
// Start fast heartbeat after first successful heartbeat
|
||||
const configHeartbeatId = setInterval(() => {
|
||||
meshDevice.heartbeat().catch((error) => {
|
||||
console.warn(
|
||||
"[useConnections] Config heartbeat failed:",
|
||||
error,
|
||||
);
|
||||
console.warn("[useConnections] Config heartbeat failed:", error);
|
||||
});
|
||||
}, CONFIG_HEARTBEAT_INTERVAL_MS);
|
||||
heartbeats.set(id, configHeartbeatId);
|
||||
@@ -303,9 +282,7 @@ export function useConnections() {
|
||||
if (getDevices) {
|
||||
const known = await getDevices();
|
||||
if (known && known.length > 0 && conn.deviceId) {
|
||||
bleDevice = known.find(
|
||||
(d: BluetoothDevice) => d.id === conn.deviceId,
|
||||
);
|
||||
bleDevice = known.find((d: BluetoothDevice) => d.id === conn.deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,22 +290,15 @@ export function useConnections() {
|
||||
// Prompt user to reselect (filter by optional service if provided)
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: !conn.gattServiceUUID,
|
||||
optionalServices: conn.gattServiceUUID
|
||||
? [conn.gattServiceUUID]
|
||||
: undefined,
|
||||
filters: conn.gattServiceUUID
|
||||
? [{ services: [conn.gattServiceUUID] }]
|
||||
: undefined,
|
||||
optionalServices: conn.gattServiceUUID ? [conn.gattServiceUUID] : undefined,
|
||||
filters: conn.gattServiceUUID ? [{ services: [conn.gattServiceUUID] }] : undefined,
|
||||
});
|
||||
}
|
||||
if (!bleDevice) {
|
||||
throw new Error(
|
||||
"Bluetooth device not available. Re-select the device.",
|
||||
);
|
||||
throw new Error("Bluetooth device not available. Re-select the device.");
|
||||
}
|
||||
|
||||
const transport =
|
||||
await TransportWebBluetooth.createFromDevice(bleDevice);
|
||||
const transport = await TransportWebBluetooth.createFromDevice(bleDevice);
|
||||
setupMeshDevice(id, transport, bleDevice);
|
||||
|
||||
bleDevice.addEventListener("gattserverdisconnected", () => {
|
||||
@@ -363,8 +333,7 @@ export function useConnections() {
|
||||
}
|
||||
).getInfo?.() ?? {};
|
||||
return (
|
||||
info.usbVendorId === conn.usbVendorId &&
|
||||
info.usbProductId === conn.usbProductId
|
||||
info.usbVendorId === conn.usbVendorId && info.usbProductId === conn.usbProductId
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -373,9 +342,7 @@ export function useConnections() {
|
||||
port = await (
|
||||
navigator as Navigator & {
|
||||
serial: {
|
||||
requestPort: (
|
||||
options: Record<string, unknown>,
|
||||
) => Promise<SerialPort>;
|
||||
requestPort: (options: Record<string, unknown>) => Promise<SerialPort>;
|
||||
};
|
||||
}
|
||||
).serial.requestPort({});
|
||||
@@ -426,9 +393,7 @@ export function useConnections() {
|
||||
if (heartbeatId) {
|
||||
clearInterval(heartbeatId);
|
||||
heartbeats.delete(id);
|
||||
console.log(
|
||||
`[useConnections] Heartbeat stopped for connection ${id}`,
|
||||
);
|
||||
console.log(`[useConnections] Heartbeat stopped for connection ${id}`);
|
||||
}
|
||||
|
||||
// Unsubscribe from config complete event
|
||||
@@ -436,9 +401,7 @@ export function useConnections() {
|
||||
if (unsubConfigComplete) {
|
||||
unsubConfigComplete();
|
||||
configSubscriptions.delete(id);
|
||||
console.log(
|
||||
`[useConnections] Config subscription cleaned up for connection ${id}`,
|
||||
);
|
||||
console.log(`[useConnections] Config subscription cleaned up for connection ${id}`);
|
||||
}
|
||||
|
||||
// Get device and meshDevice from Device.connection
|
||||
@@ -560,9 +523,7 @@ export function useConnections() {
|
||||
getDevices?: () => Promise<BluetoothDevice[]>;
|
||||
}
|
||||
).getDevices?.();
|
||||
const hasPermission = known?.some(
|
||||
(d: BluetoothDevice) => d.id === c.deviceId,
|
||||
);
|
||||
const hasPermission = known?.some((d: BluetoothDevice) => d.id === c.deviceId);
|
||||
updateSavedConnection(c.id, {
|
||||
status: hasPermission ? "configured" : "disconnected",
|
||||
});
|
||||
@@ -607,10 +568,7 @@ export function useConnections() {
|
||||
};
|
||||
}
|
||||
).getInfo?.() ?? {};
|
||||
return (
|
||||
info.usbVendorId === c.usbVendorId &&
|
||||
info.usbProductId === c.usbProductId
|
||||
);
|
||||
return info.usbVendorId === c.usbVendorId && info.usbProductId === c.usbProductId;
|
||||
});
|
||||
updateSavedConnection(c.id, {
|
||||
status: hasPermission ? "configured" : "disconnected",
|
||||
@@ -626,9 +584,7 @@ export function useConnections() {
|
||||
|
||||
const syncConnectionStatuses = useCallback(() => {
|
||||
// Find which connection corresponds to the currently selected device
|
||||
const activeConnection = connections.find(
|
||||
(c) => c.meshDeviceId === selectedDeviceId,
|
||||
);
|
||||
const activeConnection = connections.find((c) => c.meshDeviceId === selectedDeviceId);
|
||||
|
||||
// Update all connection statuses
|
||||
connections.forEach((conn) => {
|
||||
|
||||
@@ -40,10 +40,7 @@ export function createConnectionFromInput(input: NewConnection): Connection {
|
||||
};
|
||||
}
|
||||
|
||||
export async function testHttpReachable(
|
||||
url: string,
|
||||
timeoutMs = 2500,
|
||||
): Promise<boolean> {
|
||||
export async function testHttpReachable(url: string, timeoutMs = 2500): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
type VisibilityState,
|
||||
} from "@app/components/PageComponents/Map/Tools/MapLayerTool.tsx";
|
||||
import { FilterControl } from "@components/generic/Filter/FilterControl.tsx";
|
||||
import {
|
||||
type FilterState,
|
||||
useFilterNode,
|
||||
} from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { type FilterState, useFilterNode } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { BaseMap } from "@components/Map.tsx";
|
||||
import {
|
||||
HeatmapLayer,
|
||||
@@ -31,14 +28,7 @@ import { hasPos, toLngLat } from "@core/utils/geo.ts";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { FunnelIcon, LocateFixedIcon } from "lucide-react";
|
||||
import {
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useDeferredValue, useId, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type MapLayerMouseEvent, useMap } from "react-map-gl/maplibre";
|
||||
|
||||
@@ -50,9 +40,7 @@ const MapPage = () => {
|
||||
const { nodes: validNodes, myNode } = useNodeDB(
|
||||
(db) => ({
|
||||
// only nodes with a position
|
||||
nodes: db.getNodes((n): n is Protobuf.Mesh.NodeInfo =>
|
||||
Boolean(n.position?.latitudeI),
|
||||
),
|
||||
nodes: db.getNodes((n): n is Protobuf.Mesh.NodeInfo => Boolean(n.position?.latitudeI)),
|
||||
myNode: db.getMyNode(),
|
||||
|
||||
// References to cause re-render on change
|
||||
@@ -76,9 +64,7 @@ const MapPage = () => {
|
||||
const [heatmapMode, setHeatmapMode] = useState<HeatmapMode>("density");
|
||||
|
||||
// Filters
|
||||
const [filterState, setFilterState] = useState<FilterState>(
|
||||
() => defaultFilterValues,
|
||||
);
|
||||
const [filterState, setFilterState] = useState<FilterState>(() => defaultFilterValues);
|
||||
const deferredFilterState = useDeferredValue(filterState);
|
||||
|
||||
const filteredNodes = useMemo(
|
||||
@@ -119,12 +105,7 @@ const MapPage = () => {
|
||||
mode={heatmapMode}
|
||||
/>
|
||||
),
|
||||
[
|
||||
filteredNodes,
|
||||
visibilityState.heatmap,
|
||||
heatmapMode,
|
||||
heatmapLayerElementId,
|
||||
],
|
||||
[filteredNodes, visibilityState.heatmap, heatmapMode, heatmapLayerElementId],
|
||||
);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
@@ -136,8 +117,7 @@ const MapPage = () => {
|
||||
const hoveredFeature = features?.[0];
|
||||
|
||||
if (hoveredFeature) {
|
||||
const { from, to, snr, name, shortName, num } =
|
||||
hoveredFeature.properties;
|
||||
const { from, to, snr, name, shortName, num } = hoveredFeature.properties;
|
||||
|
||||
// Handle Heatmap Hover
|
||||
if (
|
||||
@@ -197,14 +177,7 @@ const MapPage = () => {
|
||||
isVisible={visibilityState.nodeMarkers}
|
||||
/>
|
||||
),
|
||||
[
|
||||
filteredNodes,
|
||||
expandedCluster,
|
||||
mapRef,
|
||||
myNode,
|
||||
popupState,
|
||||
visibilityState.nodeMarkers,
|
||||
],
|
||||
[filteredNodes, expandedCluster, mapRef, myNode, popupState, visibilityState.nodeMarkers],
|
||||
);
|
||||
|
||||
// Precision circles
|
||||
@@ -217,11 +190,7 @@ const MapPage = () => {
|
||||
isVisible={visibilityState.positionPrecision}
|
||||
/>
|
||||
),
|
||||
[
|
||||
filteredNodes,
|
||||
visibilityState.positionPrecision,
|
||||
precisionCirclesElementId,
|
||||
],
|
||||
[filteredNodes, visibilityState.positionPrecision, precisionCirclesElementId],
|
||||
);
|
||||
|
||||
// Waypoints
|
||||
@@ -244,10 +213,7 @@ const MapPage = () => {
|
||||
onLoad={getMapBounds}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onMapBackgroundClick}
|
||||
interactiveLayerIds={[
|
||||
snrLayerElementId,
|
||||
`${heatmapLayerElementId}-interaction`,
|
||||
]}
|
||||
interactiveLayerIds={[snrLayerElementId, `${heatmapLayerElementId}-interaction`]}
|
||||
>
|
||||
{heatmapLayerElement}
|
||||
{markerElements}
|
||||
@@ -256,12 +222,7 @@ const MapPage = () => {
|
||||
{waypointLayerElement}
|
||||
|
||||
{snrHover && (
|
||||
<SNRTooltip
|
||||
pos={snrHover.pos}
|
||||
snr={snrHover.snr}
|
||||
from={snrHover.from}
|
||||
to={snrHover.to}
|
||||
/>
|
||||
<SNRTooltip pos={snrHover.pos} snr={snrHover.snr} from={snrHover.from} to={snrHover.to} />
|
||||
)}
|
||||
</BaseMap>
|
||||
<div className="flex flex-col space-y-1 fixed top-35 right-2.5">
|
||||
|
||||
@@ -21,13 +21,7 @@ import { randId } from "@core/utils/randId.ts";
|
||||
import { Protobuf, Types } from "@meshtastic/core";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import {
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getChannelName } from "../components/PageComponents/Channels/Channels.tsx";
|
||||
|
||||
@@ -65,8 +59,7 @@ export const MessagesPage = () => {
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const chatType =
|
||||
type === "direct" ? MessageType.Direct : MessageType.Broadcast;
|
||||
const chatType = type === "direct" ? MessageType.Direct : MessageType.Broadcast;
|
||||
const numericChatId = Number(chatId);
|
||||
|
||||
const allChannels = Array.from(channels.values());
|
||||
@@ -77,10 +70,7 @@ export const MessagesPage = () => {
|
||||
useEffect(() => {
|
||||
if (!type && !chatId && filteredChannels.length > 0) {
|
||||
const defaultChannel = filteredChannels[0];
|
||||
navigateToChat(
|
||||
MessageType.Broadcast,
|
||||
defaultChannel?.index.toString() ?? "0",
|
||||
);
|
||||
navigateToChat(MessageType.Broadcast, defaultChannel?.index.toString() ?? "0");
|
||||
}
|
||||
}, [type, chatId, filteredChannels, navigateToChat]);
|
||||
|
||||
@@ -96,10 +86,7 @@ export const MessagesPage = () => {
|
||||
return getNodes((node: Protobuf.Mesh.NodeInfo) => {
|
||||
const longName = node.user?.longName?.toLowerCase() ?? "";
|
||||
const shortName = node.user?.shortName?.toLowerCase() ?? "";
|
||||
return (
|
||||
longName.includes(lowerCaseSearchTerm) ||
|
||||
shortName.includes(lowerCaseSearchTerm)
|
||||
);
|
||||
return longName.includes(lowerCaseSearchTerm) || shortName.includes(lowerCaseSearchTerm);
|
||||
}, true)
|
||||
.map((node: Protobuf.Mesh.NodeInfo) => ({
|
||||
...node,
|
||||
@@ -117,19 +104,12 @@ export const MessagesPage = () => {
|
||||
const sendText = useCallback(
|
||||
async (message: string) => {
|
||||
const toValue = isDirect ? numericChatId : MessageType.Broadcast;
|
||||
const channelValue = isDirect
|
||||
? Types.ChannelNumber.Primary
|
||||
: numericChatId;
|
||||
const channelValue = isDirect ? Types.ChannelNumber.Primary : numericChatId;
|
||||
|
||||
let messageId: number | undefined;
|
||||
|
||||
try {
|
||||
messageId = await connection?.sendText(
|
||||
message,
|
||||
toValue,
|
||||
true,
|
||||
channelValue,
|
||||
);
|
||||
messageId = await connection?.sendText(message, toValue, true, channelValue);
|
||||
if (messageId !== undefined) {
|
||||
if (chatType === MessageType.Broadcast) {
|
||||
setMessageState({
|
||||
@@ -217,19 +197,13 @@ export const MessagesPage = () => {
|
||||
ns: "channels",
|
||||
}))
|
||||
}
|
||||
active={
|
||||
numericChatId === channel.index &&
|
||||
chatType === MessageType.Broadcast
|
||||
}
|
||||
active={numericChatId === channel.index && chatType === MessageType.Broadcast}
|
||||
onClick={() => {
|
||||
navigateToChat(MessageType.Broadcast, channel.index.toString());
|
||||
resetUnread(channel.index);
|
||||
}}
|
||||
>
|
||||
<HashIcon
|
||||
size={16}
|
||||
className={cn(isCollapsed ? "mr-0 mt-2" : "mr-2")}
|
||||
/>
|
||||
<HashIcon size={16} className={cn(isCollapsed ? "mr-0 mt-2" : "mr-2")} />
|
||||
</SidebarButton>
|
||||
))}
|
||||
</SidebarSection>
|
||||
@@ -248,10 +222,7 @@ export const MessagesPage = () => {
|
||||
);
|
||||
|
||||
const rightSidebar = (
|
||||
<SidebarSection
|
||||
label=""
|
||||
className="px-0 flex flex-col h-full overflow-y-auto"
|
||||
>
|
||||
<SidebarSection label="" className="px-0 flex flex-col h-full overflow-y-auto">
|
||||
<label className="p-2 block" htmlFor="nodeSearch">
|
||||
<Input
|
||||
type="text"
|
||||
@@ -263,9 +234,7 @@ export const MessagesPage = () => {
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-full flex-1 overflow-y-auto gap-2.5 pt-1 ",
|
||||
)}
|
||||
className={cn("flex flex-col h-full flex-1 overflow-y-auto gap-2.5 pt-1 ")}
|
||||
style={{ contentVisibility: "auto", containIntrinsicSize: "100px" }}
|
||||
>
|
||||
{filteredNodes()?.map((node) => (
|
||||
@@ -274,9 +243,7 @@ export const MessagesPage = () => {
|
||||
preventCollapse
|
||||
label={node.user?.longName ?? t("unknown.shortName")}
|
||||
count={node.unreadCount > 0 ? node.unreadCount : undefined}
|
||||
active={
|
||||
numericChatId === node.num && chatType === MessageType.Direct
|
||||
}
|
||||
active={numericChatId === node.num && chatType === MessageType.Direct}
|
||||
onClick={() => {
|
||||
navigateToChat(MessageType.Direct, node.num.toString());
|
||||
resetUnread(node.num);
|
||||
@@ -314,9 +281,7 @@ export const MessagesPage = () => {
|
||||
? [
|
||||
{
|
||||
key: "encryption",
|
||||
icon: otherNode.user?.publicKey?.length
|
||||
? LockIcon
|
||||
: LockOpenIcon,
|
||||
icon: otherNode.user?.publicKey?.length ? LockIcon : LockOpenIcon,
|
||||
iconClasses: otherNode.user?.publicKey?.length
|
||||
? "text-green-600"
|
||||
: "text-yellow-300",
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog.tsx";
|
||||
import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog.tsx";
|
||||
import { FilterControl } from "@components/generic/Filter/FilterControl.tsx";
|
||||
import {
|
||||
type FilterState,
|
||||
useFilterNode,
|
||||
} from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { type FilterState, useFilterNode } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import {
|
||||
type DataRow,
|
||||
type Heading,
|
||||
Table,
|
||||
} from "@components/generic/Table/index.tsx";
|
||||
import { type DataRow, type Heading, Table } from "@components/generic/Table/index.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { PageLayout } from "@components/PageLayout.tsx";
|
||||
import { Sidebar } from "@components/Sidebar.tsx";
|
||||
@@ -21,13 +14,7 @@ import { useAppStore, useDevice, useNodeDB } from "@core/stores";
|
||||
import { Protobuf, type Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import {
|
||||
type JSX,
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type JSX, useCallback, useDeferredValue, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { base16 } from "rfc4648";
|
||||
|
||||
@@ -53,9 +40,7 @@ const NodesPage = (): JSX.Element => {
|
||||
Types.PacketMetadata<Protobuf.Mesh.Position> | undefined
|
||||
>();
|
||||
|
||||
const [filterState, setFilterState] = useState<FilterState>(
|
||||
() => defaultFilterValues,
|
||||
);
|
||||
const [filterState, setFilterState] = useState<FilterState>(() => defaultFilterValues);
|
||||
const deferredFilterState = useDeferredValue(filterState);
|
||||
|
||||
// stable predicate so the selector identity doesn’t thrash
|
||||
@@ -136,9 +121,7 @@ const NodesPage = (): JSX.Element => {
|
||||
.match(/.{1,2}/g)
|
||||
?.join(":") ?? t("unknown.shortName");
|
||||
|
||||
const shortName =
|
||||
node.user?.shortName ??
|
||||
numberToHexUnpadded(node.num).slice(-4).toUpperCase();
|
||||
const shortName = node.user?.shortName ?? numberToHexUnpadded(node.num).slice(-4).toUpperCase();
|
||||
const longName =
|
||||
node.user?.longName ??
|
||||
t("fallbackName", {
|
||||
@@ -164,7 +147,9 @@ const NodesPage = (): JSX.Element => {
|
||||
<h1
|
||||
onMouseDown={() => handleNodeInfoDialog(node.num)}
|
||||
onKeyUp={(evt) => {
|
||||
evt.key === "Enter" && handleNodeInfoDialog(node.num);
|
||||
if (evt.key === "Enter") {
|
||||
handleNodeInfoDialog(node.num);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer underline ml-2 whitespace-break-spaces"
|
||||
>
|
||||
@@ -180,14 +165,10 @@ const NodesPage = (): JSX.Element => {
|
||||
? node?.viaMqtt === false && node.hopsAway === 0
|
||||
? t("nodesTable.connectionStatus.direct")
|
||||
: `${node.hopsAway?.toString()} ${
|
||||
(node.hopsAway ?? 0 > 1)
|
||||
? t("unit.hop.plural")
|
||||
: t("unit.hops_one")
|
||||
(node.hopsAway ?? 0 > 1) ? t("unit.hop.plural") : t("unit.hops_one")
|
||||
} ${t("nodesTable.connectionStatus.away")}`
|
||||
: t("unknown.longName")}
|
||||
{node?.viaMqtt === true
|
||||
? t("nodesTable.connectionStatus.viaMqtt")
|
||||
: ""}
|
||||
{node?.viaMqtt === true ? t("nodesTable.connectionStatus.viaMqtt") : ""}
|
||||
</Mono>
|
||||
),
|
||||
sortValue: node.hopsAway ?? Number.MAX_SAFE_INTEGER,
|
||||
@@ -198,10 +179,7 @@ const NodesPage = (): JSX.Element => {
|
||||
{node.lastHeard === 0 ? (
|
||||
t("unknown.longName")
|
||||
) : (
|
||||
<TimeAgo
|
||||
timestamp={node.lastHeard * 1000}
|
||||
locale={current?.code}
|
||||
/>
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} locale={current?.code} />
|
||||
)}
|
||||
</Mono>
|
||||
),
|
||||
@@ -232,11 +210,8 @@ const NodesPage = (): JSX.Element => {
|
||||
sortValue: node.snr,
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>
|
||||
),
|
||||
sortValue:
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNSET",
|
||||
content: <Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>,
|
||||
sortValue: Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNSET",
|
||||
},
|
||||
{
|
||||
content: <Mono>{macAddress}</Mono>,
|
||||
|
||||
@@ -6,12 +6,7 @@ import { Position } from "@components/PageComponents/Settings/Position.tsx";
|
||||
import { Power } from "@components/PageComponents/Settings/Power.tsx";
|
||||
import { User } from "@components/PageComponents/Settings/User.tsx";
|
||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import { useDevice, type ValidConfigType } from "@core/stores";
|
||||
import { type ComponentType, Suspense, useMemo } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
@@ -31,52 +26,53 @@ type TabItem = {
|
||||
export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { hasConfigChange, hasUserChange } = useDevice();
|
||||
const { t } = useTranslation("config");
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
case: "user",
|
||||
label: t("page.tabUser"),
|
||||
element: User,
|
||||
},
|
||||
{
|
||||
case: "device",
|
||||
label: t("page.tabDevice"),
|
||||
element: Device,
|
||||
},
|
||||
{
|
||||
case: "position",
|
||||
label: t("page.tabPosition"),
|
||||
element: Position,
|
||||
},
|
||||
{
|
||||
case: "power",
|
||||
label: t("page.tabPower"),
|
||||
element: Power,
|
||||
},
|
||||
{
|
||||
case: "network",
|
||||
label: t("page.tabNetwork"),
|
||||
element: Network,
|
||||
},
|
||||
{
|
||||
case: "display",
|
||||
label: t("page.tabDisplay"),
|
||||
element: Display,
|
||||
},
|
||||
{
|
||||
case: "bluetooth",
|
||||
label: t("page.tabBluetooth"),
|
||||
element: Bluetooth,
|
||||
},
|
||||
] as const;
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
case: "user",
|
||||
label: t("page.tabUser"),
|
||||
element: User,
|
||||
},
|
||||
{
|
||||
case: "device",
|
||||
label: t("page.tabDevice"),
|
||||
element: Device,
|
||||
},
|
||||
{
|
||||
case: "position",
|
||||
label: t("page.tabPosition"),
|
||||
element: Position,
|
||||
},
|
||||
{
|
||||
case: "power",
|
||||
label: t("page.tabPower"),
|
||||
element: Power,
|
||||
},
|
||||
{
|
||||
case: "network",
|
||||
label: t("page.tabNetwork"),
|
||||
element: Network,
|
||||
},
|
||||
{
|
||||
case: "display",
|
||||
label: t("page.tabDisplay"),
|
||||
element: Display,
|
||||
},
|
||||
{
|
||||
case: "bluetooth",
|
||||
label: t("page.tabBluetooth"),
|
||||
element: Bluetooth,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const flags = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
tabs.map((tab) => [
|
||||
tab.case,
|
||||
tab.case === "user"
|
||||
? hasUserChange()
|
||||
: hasConfigChange(tab.case as ValidConfigType),
|
||||
tab.case === "user" ? hasUserChange() : hasConfigChange(tab.case as ValidConfigType),
|
||||
]),
|
||||
),
|
||||
[tabs, hasConfigChange, hasUserChange],
|
||||
@@ -86,11 +82,7 @@ export const DeviceConfig = ({ onFormInit }: ConfigProps) => {
|
||||
<Tabs defaultValue={t("page.tabUser")}>
|
||||
<TabsList className="w-full dark:bg-slate-700">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.label}
|
||||
value={tab.label}
|
||||
className="dark:text-white relative"
|
||||
>
|
||||
<TabsTrigger key={tab.label} value={tab.label} className="dark:text-white relative">
|
||||
{tab.label}
|
||||
{flags.get(tab.case) && (
|
||||
<span className="absolute -top-0.5 -right-0.5 z-50 flex size-3">
|
||||
|
||||
@@ -11,12 +11,7 @@ import { Serial } from "@components/PageComponents/ModuleConfig/Serial.tsx";
|
||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.tsx";
|
||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.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, type ValidModuleConfigType } from "@core/stores";
|
||||
import { type ComponentType, Suspense, useMemo } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
@@ -36,68 +31,70 @@ type TabItem = {
|
||||
export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { hasModuleConfigChange } = useDevice();
|
||||
const { t } = useTranslation("moduleConfig");
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
case: "mqtt",
|
||||
label: t("page.tabMqtt"),
|
||||
element: MQTT,
|
||||
},
|
||||
{
|
||||
case: "serial",
|
||||
label: t("page.tabSerial"),
|
||||
element: Serial,
|
||||
},
|
||||
{
|
||||
case: "externalNotification",
|
||||
label: t("page.tabExternalNotification"),
|
||||
element: ExternalNotification,
|
||||
},
|
||||
{
|
||||
case: "storeForward",
|
||||
label: t("page.tabStoreAndForward"),
|
||||
element: StoreForward,
|
||||
},
|
||||
{
|
||||
case: "rangeTest",
|
||||
label: t("page.tabRangeTest"),
|
||||
element: RangeTest,
|
||||
},
|
||||
{
|
||||
case: "telemetry",
|
||||
label: t("page.tabTelemetry"),
|
||||
element: Telemetry,
|
||||
},
|
||||
{
|
||||
case: "cannedMessage",
|
||||
label: t("page.tabCannedMessage"),
|
||||
element: CannedMessage,
|
||||
},
|
||||
{
|
||||
case: "audio",
|
||||
label: t("page.tabAudio"),
|
||||
element: Audio,
|
||||
},
|
||||
{
|
||||
case: "neighborInfo",
|
||||
label: t("page.tabNeighborInfo"),
|
||||
element: NeighborInfo,
|
||||
},
|
||||
{
|
||||
case: "ambientLighting",
|
||||
label: t("page.tabAmbientLighting"),
|
||||
element: AmbientLighting,
|
||||
},
|
||||
{
|
||||
case: "detectionSensor",
|
||||
label: t("page.tabDetectionSensor"),
|
||||
element: DetectionSensor,
|
||||
},
|
||||
{ case: "paxcounter", label: t("page.tabPaxcounter"), element: Paxcounter },
|
||||
] as const;
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
case: "mqtt",
|
||||
label: t("page.tabMqtt"),
|
||||
element: MQTT,
|
||||
},
|
||||
{
|
||||
case: "serial",
|
||||
label: t("page.tabSerial"),
|
||||
element: Serial,
|
||||
},
|
||||
{
|
||||
case: "externalNotification",
|
||||
label: t("page.tabExternalNotification"),
|
||||
element: ExternalNotification,
|
||||
},
|
||||
{
|
||||
case: "storeForward",
|
||||
label: t("page.tabStoreAndForward"),
|
||||
element: StoreForward,
|
||||
},
|
||||
{
|
||||
case: "rangeTest",
|
||||
label: t("page.tabRangeTest"),
|
||||
element: RangeTest,
|
||||
},
|
||||
{
|
||||
case: "telemetry",
|
||||
label: t("page.tabTelemetry"),
|
||||
element: Telemetry,
|
||||
},
|
||||
{
|
||||
case: "cannedMessage",
|
||||
label: t("page.tabCannedMessage"),
|
||||
element: CannedMessage,
|
||||
},
|
||||
{
|
||||
case: "audio",
|
||||
label: t("page.tabAudio"),
|
||||
element: Audio,
|
||||
},
|
||||
{
|
||||
case: "neighborInfo",
|
||||
label: t("page.tabNeighborInfo"),
|
||||
element: NeighborInfo,
|
||||
},
|
||||
{
|
||||
case: "ambientLighting",
|
||||
label: t("page.tabAmbientLighting"),
|
||||
element: AmbientLighting,
|
||||
},
|
||||
{
|
||||
case: "detectionSensor",
|
||||
label: t("page.tabDetectionSensor"),
|
||||
element: DetectionSensor,
|
||||
},
|
||||
{ case: "paxcounter", label: t("page.tabPaxcounter"), element: Paxcounter },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const flags = useMemo(
|
||||
() =>
|
||||
new Map(tabs.map((tab) => [tab.case, hasModuleConfigChange(tab.case)])),
|
||||
() => new Map(tabs.map((tab) => [tab.case, hasModuleConfigChange(tab.case)])),
|
||||
[tabs, hasModuleConfigChange],
|
||||
);
|
||||
|
||||
@@ -105,11 +102,7 @@ export const ModuleConfig = ({ onFormInit }: ConfigProps) => {
|
||||
<Tabs defaultValue={t("page.tabMqtt")}>
|
||||
<TabsList className="w-full dark:bg-slate-800">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.label}
|
||||
value={tab.label}
|
||||
className="dark:text-white relative"
|
||||
>
|
||||
<TabsTrigger key={tab.label} value={tab.label} className="dark:text-white relative">
|
||||
{tab.label}
|
||||
{flags.get(tab.case) && (
|
||||
<span className="absolute -top-0.5 -right-0.5 z-50 flex size-3">
|
||||
|
||||
@@ -2,12 +2,7 @@ import { Channels } from "@app/components/PageComponents/Channels/Channels";
|
||||
import { LoRa } from "@components/PageComponents/Settings/LoRa.tsx";
|
||||
import { Security } from "@components/PageComponents/Settings/Security/Security.tsx";
|
||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import { useDevice, type ValidConfigType } from "@core/stores";
|
||||
import { type ComponentType, Suspense, useMemo } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
@@ -18,7 +13,7 @@ interface ConfigProps {
|
||||
}
|
||||
|
||||
type TabItem = {
|
||||
case: ValidConfigType;
|
||||
case: ValidConfigType | "channels";
|
||||
label: string;
|
||||
element: ComponentType<ConfigProps>;
|
||||
count?: number;
|
||||
@@ -27,26 +22,30 @@ type TabItem = {
|
||||
export const RadioConfig = ({ onFormInit }: ConfigProps) => {
|
||||
const { hasConfigChange } = useDevice();
|
||||
const { t } = useTranslation("config");
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
case: "lora",
|
||||
label: t("page.tabLora"),
|
||||
element: LoRa,
|
||||
},
|
||||
{
|
||||
case: "channels",
|
||||
label: t("page.tabChannels"),
|
||||
element: Channels,
|
||||
},
|
||||
{
|
||||
case: "security",
|
||||
label: t("page.tabSecurity"),
|
||||
element: Security,
|
||||
},
|
||||
] as const;
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
case: "lora",
|
||||
label: t("page.tabLora"),
|
||||
element: LoRa,
|
||||
},
|
||||
{
|
||||
case: "channels",
|
||||
label: t("page.tabChannels"),
|
||||
element: Channels,
|
||||
},
|
||||
{
|
||||
case: "security",
|
||||
label: t("page.tabSecurity"),
|
||||
element: Security,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const flags = useMemo(
|
||||
() => new Map(tabs.map((tab) => [tab.case, hasConfigChange(tab.case)])),
|
||||
() =>
|
||||
new Map(tabs.map((tab) => [tab.case, tab.case !== "channels" && hasConfigChange(tab.case)])),
|
||||
[tabs, hasConfigChange],
|
||||
);
|
||||
|
||||
@@ -54,11 +53,7 @@ export const RadioConfig = ({ onFormInit }: ConfigProps) => {
|
||||
<Tabs defaultValue={t("page.tabLora")}>
|
||||
<TabsList className="w-full dark:bg-slate-700">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.label}
|
||||
value={tab.label}
|
||||
className="dark:text-white relative"
|
||||
>
|
||||
<TabsTrigger key={tab.label} value={tab.label} className="dark:text-white relative">
|
||||
{tab.label}
|
||||
{flags.get(tab.case) && (
|
||||
<span className="absolute -top-0.5 -right-0.5 z-50 flex size-3">
|
||||
|
||||
@@ -90,30 +90,27 @@ const ConfigPage = () => {
|
||||
routerState.location.pathname.includes(`/settings/${section.key}`),
|
||||
) ?? sections[0];
|
||||
|
||||
const onFormInit = useCallback(
|
||||
<T extends FieldValues>(methods: UseFormReturn<T>) => {
|
||||
setFormMethods(methods as UseFormReturn);
|
||||
const onFormInit = useCallback(<T extends FieldValues>(methods: UseFormReturn<T>) => {
|
||||
setFormMethods(methods as UseFormReturn);
|
||||
|
||||
setRhfState({
|
||||
// Assume defailt on init, changes will be caught by subscription
|
||||
isDirty: false,
|
||||
isValid: true,
|
||||
});
|
||||
setRhfState({
|
||||
// Assume defailt on init, changes will be caught by subscription
|
||||
isDirty: false,
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
// Unsubscribe from previous subscriptions & subscribe to form changes
|
||||
unsubRef.current?.();
|
||||
unsubRef.current = methods.subscribe({
|
||||
formState: { isDirty: true, isValid: true },
|
||||
callback: ({ isValid, isDirty }) => {
|
||||
setRhfState({
|
||||
isDirty: isDirty ?? false,
|
||||
isValid: isValid ?? true,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Unsubscribe from previous subscriptions & subscribe to form changes
|
||||
unsubRef.current?.();
|
||||
unsubRef.current = methods.subscribe({
|
||||
formState: { isDirty: true, isValid: true },
|
||||
callback: ({ isValid, isDirty }) => {
|
||||
setRhfState({
|
||||
isDirty: isDirty ?? false,
|
||||
isValid: isValid ?? true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => unsubRef.current?.();
|
||||
@@ -206,7 +203,7 @@ const ConfigPage = () => {
|
||||
|
||||
formMethods.trigger();
|
||||
}
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
toast({
|
||||
title: t("toast.configSaveError.title"),
|
||||
description: t("toast.configSaveError.description"),
|
||||
@@ -300,9 +297,7 @@ const ConfigPage = () => {
|
||||
isLoading: isSaving,
|
||||
disabled: saveDisabled,
|
||||
iconClasses:
|
||||
!rhfState.isValid && hasPending
|
||||
? "text-red-400 cursor-not-allowed"
|
||||
: "cursor-pointer",
|
||||
!rhfState.isValid && hasPending ? "text-red-400 cursor-not-allowed" : "cursor-pointer",
|
||||
className: cn([
|
||||
"transition-opacity hover:bg-slate-200 disabled:hover:bg-white",
|
||||
"hover:dark:bg-slate-300 hover:dark:text-black",
|
||||
|
||||
Reference in New Issue
Block a user