Execute oxfmt (#1166)
Ran pnpm oxfmt . This should get the web repo aligned so that we can better enforce oxfmt going forward.
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meshtastic-web",
|
"name": "meshtastic-web",
|
||||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/r3dpoint/devcontainer-features/tailwindcss-standalone-cli:1": {
|
"ghcr.io/r3dpoint/devcontainer-features/tailwindcss-standalone-cli:1": {
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {
|
"ghcr.io/devcontainers-extra/features/pnpm:2": {
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ on:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PROTOBUF_DIR: ./packages/protobufs
|
PROTOBUF_DIR: ./packages/protobufs
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
codegen:
|
codegen:
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Install workspace dependencies
|
- name: Install workspace dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
fetch-tags: true # IMPORTANT: we need tags to resolve the release commit
|
fetch-tags: true # IMPORTANT: we need tags to resolve the release commit
|
||||||
|
|
||||||
- name: Configure Git author
|
- name: Configure Git author
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json",
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
|
|||||||
Vendored
+2
-2
@@ -2,5 +2,5 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/i18n/locales/*-*/**": true
|
"**/i18n/locales/*-*/**": true
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-6
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="system">
|
<html lang="en" data-theme="system">
|
||||||
<head>
|
<head>
|
||||||
<!-- CookieYes banner -->
|
<!-- CookieYes banner -->
|
||||||
@@ -11,11 +11,7 @@
|
|||||||
|
|
||||||
<link rel="manifest" href="site.webmanifest" />
|
<link rel="manifest" href="site.webmanifest" />
|
||||||
<link rel="mask-icon" href="logo_black.svg" color="#67ea94" />
|
<link rel="mask-icon" href="logo_black.svg" color="#67ea94" />
|
||||||
<link
|
<link rel="stylesheet" href="src/index.css" crossorigin="anonymous" />
|
||||||
rel="stylesheet"
|
|
||||||
href="src/index.css"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<meta name="theme-color" content="#67ea94" />
|
<meta name="theme-color" content="#67ea94" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="Meshtastic Web Client" />
|
<meta name="description" content="Meshtastic Web Client" />
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "meshtastic-web",
|
"name": "meshtastic-web",
|
||||||
"version": "2.6.6-0",
|
"version": "2.6.6-0",
|
||||||
"type": "module",
|
|
||||||
"description": "Meshtastic web client",
|
"description": "Meshtastic web client",
|
||||||
|
"homepage": "https://meshtastic.org",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/meshtastic/web/issues"
|
||||||
|
},
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/meshtastic/web.git"
|
"url": "git+https://github.com/meshtastic/web.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"type": "module",
|
||||||
"url": "https://github.com/meshtastic/web/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://meshtastic.org",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"setup:certs": "mkcert localhost 127.0.0.1 ::1",
|
"setup:certs": "mkcert localhost 127.0.0.1 ::1",
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"@meshtastic/transport-web-serial": "workspace:*",
|
"@meshtastic/transport-web-serial": "workspace:*",
|
||||||
"@noble/curves": "^1.9.2",
|
"@noble/curves": "^1.9.2",
|
||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"better-result": "^2.9.2",
|
|
||||||
"@radix-ui/react-accordion": "^1.2.13",
|
"@radix-ui/react-accordion": "^1.2.13",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.16",
|
"@radix-ui/react-alert-dialog": "^1.1.16",
|
||||||
"@radix-ui/react-checkbox": "^1.3.4",
|
"@radix-ui/react-checkbox": "^1.3.4",
|
||||||
@@ -69,6 +68,7 @@
|
|||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
"better-result": "^2.9.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "Meshtastic",
|
"name": "Meshtastic",
|
||||||
"short_name": "Web Client",
|
"short_name": "Web Client",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"description": "Meshtastic Web App",
|
"description": "Meshtastic Web App",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/logo.svg",
|
"src": "/logo.svg",
|
||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"type": "image/svg+xml"
|
"type": "image/svg+xml"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#67ea94",
|
"theme_color": "#67ea94",
|
||||||
"background_color": "#67ea94",
|
"background_color": "#67ea94",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface DeviceWrapperProps {
|
|||||||
|
|
||||||
export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
|
export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
|
||||||
return (
|
return (
|
||||||
<CurrentDeviceContext.Provider value={{ deviceId }}>{children}</CurrentDeviceContext.Provider>
|
<CurrentDeviceContext.Provider value={{ deviceId }}>
|
||||||
|
{children}
|
||||||
|
</CurrentDeviceContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("@components/UI/Checkbox.tsx", () => ({
|
vi.mock("@components/UI/Checkbox.tsx", () => ({
|
||||||
Checkbox: ({ id, checked, onChange }: { id: string; checked: boolean; onChange: () => void }) => (
|
Checkbox: ({
|
||||||
<input data-testid="checkbox" type="checkbox" id={id} checked={checked} onChange={onChange} />
|
id,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
}) => (
|
||||||
|
<input
|
||||||
|
data-testid="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { BatteryFullIcon, BatteryLowIcon, BatteryMediumIcon, PlugZapIcon } from "lucide-react";
|
import {
|
||||||
|
BatteryFullIcon,
|
||||||
|
BatteryLowIcon,
|
||||||
|
BatteryMediumIcon,
|
||||||
|
PlugZapIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { DeviceMetrics } from "./types.ts";
|
import type { DeviceMetrics } from "./types.ts";
|
||||||
@@ -38,7 +43,10 @@ export const getBatteryStatus = (level: number): BatteryStatusKey => {
|
|||||||
const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
|
const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (deviceMetrics?.batteryLevel === undefined || deviceMetrics?.batteryLevel === null) {
|
if (
|
||||||
|
deviceMetrics?.batteryLevel === undefined ||
|
||||||
|
deviceMetrics?.batteryLevel === null
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ export interface SubItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CommandPalette = () => {
|
export const CommandPalette = () => {
|
||||||
const { commandPaletteOpen, setCommandPaletteOpen, setConnectDialogOpen, setSelectedDevice } =
|
const {
|
||||||
useAppStore();
|
commandPaletteOpen,
|
||||||
|
setCommandPaletteOpen,
|
||||||
|
setConnectDialogOpen,
|
||||||
|
setSelectedDevice,
|
||||||
|
} = useAppStore();
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
const { setDialogOpen, connection } = useDevice();
|
const { setDialogOpen, connection } = useDevice();
|
||||||
const allNodes = useNodesAsProto();
|
const allNodes = useNodesAsProto();
|
||||||
@@ -116,7 +120,9 @@ export const CommandPalette = () => {
|
|||||||
label: t("manage.command.switchNode"),
|
label: t("manage.command.switchNode"),
|
||||||
icon: ArrowLeftRightIcon,
|
icon: ArrowLeftRightIcon,
|
||||||
subItems: getDevices().map((device) => ({
|
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} />,
|
icon: <Avatar nodeNum={device.hardware.myNodeNum} />,
|
||||||
action() {
|
action() {
|
||||||
setSelectedDevice(device.id);
|
setSelectedDevice(device.id);
|
||||||
@@ -259,7 +265,10 @@ export const CommandPalette = () => {
|
|||||||
}, [setCommandPaletteOpen]);
|
}, [setCommandPaletteOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
|
<CommandDialog
|
||||||
|
open={commandPaletteOpen}
|
||||||
|
onOpenChange={setCommandPaletteOpen}
|
||||||
|
>
|
||||||
<CommandInput placeholder={t("search.commandPalette")} />
|
<CommandInput placeholder={t("search.commandPalette")} />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{t("emptyState")}</CommandEmpty>
|
<CommandEmpty>{t("emptyState")}</CommandEmpty>
|
||||||
|
|||||||
@@ -64,12 +64,21 @@ export const ConnectingOverlay = (): ReactElement | null => {
|
|||||||
const counters =
|
const counters =
|
||||||
progress.phase === "configuring" || progress.phase === "configured"
|
progress.phase === "configuring" || progress.phase === "configured"
|
||||||
? progress.received
|
? progress.received
|
||||||
: { config: 0, modules: 0, channels: 0, nodes: 0, myInfo: false, metadata: false };
|
: {
|
||||||
|
config: 0,
|
||||||
|
modules: 0,
|
||||||
|
channels: 0,
|
||||||
|
nodes: 0,
|
||||||
|
myInfo: false,
|
||||||
|
metadata: false,
|
||||||
|
};
|
||||||
|
|
||||||
const isConnecting = active.status === "connecting";
|
const isConnecting = active.status === "connecting";
|
||||||
const phaseLabel = isConnecting
|
const phaseLabel = isConnecting
|
||||||
? t("overlay.phase.connecting", { defaultValue: "Opening transport…" })
|
? t("overlay.phase.connecting", { defaultValue: "Opening transport…" })
|
||||||
: t("overlay.phase.configuring", { defaultValue: "Streaming configuration…" });
|
: t("overlay.phase.configuring", {
|
||||||
|
defaultValue: "Streaming configuration…",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open>
|
<Dialog open>
|
||||||
@@ -133,7 +142,11 @@ export const ConnectingOverlay = (): ReactElement | null => {
|
|||||||
"Taking longer than usual. The device may be in CLI / bootloader mode, or the firmware isn't responding to config requests.",
|
"Taking longer than usual. The device may be in CLI / bootloader mode, or the firmware isn't responding to config requests.",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<Button type="button" variant="outline" onClick={() => void disconnect(active.id)}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void disconnect(active.id)}
|
||||||
|
>
|
||||||
{t("overlay.cancel", { defaultValue: "Cancel" })}
|
{t("overlay.cancel", { defaultValue: "Cancel" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ export const DeviceInfoPanel = ({
|
|||||||
label: t("batteryVoltage.title"),
|
label: t("batteryVoltage.title"),
|
||||||
icon: ZapIcon,
|
icon: ZapIcon,
|
||||||
value:
|
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",
|
id: "firmware",
|
||||||
@@ -205,7 +207,9 @@ export const DeviceInfoPanel = ({
|
|||||||
</button>
|
</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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -237,7 +241,9 @@ export const DeviceInfoPanel = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -251,7 +257,9 @@ export const DeviceInfoPanel = ({
|
|||||||
{actionButtons.map((buttonItem) => {
|
{actionButtons.map((buttonItem) => {
|
||||||
const Icon = buttonItem.icon;
|
const Icon = buttonItem.icon;
|
||||||
if (buttonItem.render) {
|
if (buttonItem.render) {
|
||||||
return <Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>;
|
return (
|
||||||
|
<Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -273,7 +281,8 @@ export const DeviceInfoPanel = ({
|
|||||||
"flex-shrink-0 w-4",
|
"flex-shrink-0 w-4",
|
||||||
"text-gray-500 dark:text-gray-400",
|
"text-gray-500 dark:text-gray-400",
|
||||||
"transition-colors duration-150",
|
"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
|
<Subtle
|
||||||
@@ -281,7 +290,8 @@ export const DeviceInfoPanel = ({
|
|||||||
"text-sm",
|
"text-sm",
|
||||||
"text-gray-600 dark:text-gray-300",
|
"text-gray-600 dark:text-gray-300",
|
||||||
"transition-colors duration-150",
|
"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}
|
{buttonItem.label}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
|
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
|
||||||
import { Switch } from "@app/components/UI/Switch.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 { testHttpReachable } from "@app/pages/Connections/utils";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.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 { Link } from "@components/UI/Typography/Link.tsx";
|
||||||
import {
|
import {
|
||||||
type BrowserFeature,
|
type BrowserFeature,
|
||||||
@@ -37,7 +45,9 @@ type DialogState = {
|
|||||||
protocol: "http" | "https";
|
protocol: "http" | "https";
|
||||||
url: string;
|
url: string;
|
||||||
testStatus: TestingStatus;
|
testStatus: TestingStatus;
|
||||||
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined;
|
btSelected:
|
||||||
|
| { id: string; name?: string; device?: BluetoothDevice }
|
||||||
|
| undefined;
|
||||||
serialSelected: { vendorId?: number; productId?: number } | undefined;
|
serialSelected: { vendorId?: number; productId?: number } | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +60,9 @@ type DialogAction =
|
|||||||
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
|
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
|
||||||
| {
|
| {
|
||||||
type: "SET_BT_SELECTED";
|
type: "SET_BT_SELECTED";
|
||||||
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined;
|
payload:
|
||||||
|
| { id: string; name?: string; device?: BluetoothDevice }
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "SET_SERIAL_SELECTED";
|
type: "SET_SERIAL_SELECTED";
|
||||||
@@ -70,27 +82,30 @@ type Pane = {
|
|||||||
build: () => NewConnection | null;
|
build: () => NewConnection | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
|
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",
|
"Web Bluetooth": {
|
||||||
i18nKey: "addConnection.validation.requiresWebBluetooth",
|
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",
|
"Web Serial": {
|
||||||
i18nKey: "addConnection.validation.requiresWebSerial",
|
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",
|
"Secure Context": {
|
||||||
i18nKey: "addConnection.validation.requiresSecureContext",
|
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||||
},
|
i18nKey: "addConnection.validation.requiresSecureContext",
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||||
if (missingFeatures.length === 0) {
|
if (missingFeatures.length === 0) {
|
||||||
return null;
|
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 needsSecureContext = missingFeatures.includes("Secure Context");
|
||||||
|
|
||||||
const needsFeature =
|
const needsFeature =
|
||||||
@@ -152,17 +167,22 @@ const initialState: DialogState = {
|
|||||||
btSelected: undefined,
|
btSelected: undefined,
|
||||||
serialSelected: undefined,
|
serialSelected: undefined,
|
||||||
};
|
};
|
||||||
export const createInitialDialogState = (overrides?: Partial<DialogState>): DialogState => {
|
export const createInitialDialogState = (
|
||||||
|
overrides?: Partial<DialogState>,
|
||||||
|
): DialogState => {
|
||||||
return { ...initialState, ...(overrides ?? {}) };
|
return { ...initialState, ...(overrides ?? {}) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dialogStateInitializer = (overrides?: Partial<DialogState>): DialogState =>
|
export const dialogStateInitializer = (
|
||||||
createInitialDialogState(overrides);
|
overrides?: Partial<DialogState>,
|
||||||
|
): DialogState => createInitialDialogState(overrides);
|
||||||
|
|
||||||
function dialogReducer(state: DialogState, action: DialogAction): DialogState {
|
function dialogReducer(state: DialogState, action: DialogAction): DialogState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "RESET":
|
case "RESET":
|
||||||
return createInitialDialogState(action.payload?.isHTTPS ? { protocol: "https" } : {});
|
return createInitialDialogState(
|
||||||
|
action.payload?.isHTTPS ? { protocol: "https" } : {},
|
||||||
|
);
|
||||||
case "SET_TAB":
|
case "SET_TAB":
|
||||||
return { ...state, tab: action.payload };
|
return { ...state, tab: action.payload };
|
||||||
case "SET_NAME":
|
case "SET_NAME":
|
||||||
@@ -203,13 +223,22 @@ function PickerRow({
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<MousePointerClick className="h-4 w-4" />
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -238,8 +267,10 @@ export default function AddConnectionDialog({
|
|||||||
const { unsupported } = useBrowserFeatureDetection();
|
const { unsupported } = useBrowserFeatureDetection();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const bluetoothSupported = typeof navigator !== "undefined" && "bluetooth" in navigator;
|
const bluetoothSupported =
|
||||||
const serialSupported = typeof navigator !== "undefined" && "serial" in navigator;
|
typeof navigator !== "undefined" && "bluetooth" in navigator;
|
||||||
|
const serialSupported =
|
||||||
|
typeof navigator !== "undefined" && "serial" in navigator;
|
||||||
const isURLHTTPS = isHTTPS;
|
const isURLHTTPS = isHTTPS;
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
@@ -268,7 +299,9 @@ export default function AddConnectionDialog({
|
|||||||
if (!bluetoothSupported) {
|
if (!bluetoothSupported) {
|
||||||
toast({
|
toast({
|
||||||
title: t("addConnection.bluetoothConnection.notSupported.title"),
|
title: t("addConnection.bluetoothConnection.notSupported.title"),
|
||||||
description: t("addConnection.bluetoothConnection.notSupported.description"),
|
description: t(
|
||||||
|
"addConnection.bluetoothConnection.notSupported.description",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -303,7 +336,9 @@ export default function AddConnectionDialog({
|
|||||||
if (!serialSupported) {
|
if (!serialSupported) {
|
||||||
toast({
|
toast({
|
||||||
title: t("addConnection.serialConnection.notSupported.title"),
|
title: t("addConnection.serialConnection.notSupported.title"),
|
||||||
description: t("addConnection.serialConnection.notSupported.description"),
|
description: t(
|
||||||
|
"addConnection.serialConnection.notSupported.description",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -335,7 +370,9 @@ export default function AddConnectionDialog({
|
|||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: t("addConnection.serialConnection.portSelected.title"),
|
title: t("addConnection.serialConnection.portSelected.title"),
|
||||||
description: t("addConnection.serialConnection.portSelected.description"),
|
description: t(
|
||||||
|
"addConnection.serialConnection.portSelected.description",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
makeToastErrorHandler("Serial")(err);
|
makeToastErrorHandler("Serial")(err);
|
||||||
@@ -360,7 +397,9 @@ export default function AddConnectionDialog({
|
|||||||
dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
|
dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
|
||||||
toast({
|
toast({
|
||||||
title: t("addConnection.httpConnection.connectionTest.failure.title"),
|
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]);
|
}, [state.protocol, state.url, toast, t]);
|
||||||
@@ -371,7 +410,9 @@ export default function AddConnectionDialog({
|
|||||||
placeholder: t("addConnection.httpConnection.namePlaceholder"),
|
placeholder: t("addConnection.httpConnection.namePlaceholder"),
|
||||||
children: () => (
|
children: () => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Label htmlFor="url">{t("addConnection.httpConnection.heading")}</Label>
|
<Label htmlFor="url">
|
||||||
|
{t("addConnection.httpConnection.heading")}
|
||||||
|
</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id={"url"}
|
id={"url"}
|
||||||
@@ -407,19 +448,23 @@ export default function AddConnectionDialog({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
onClick={handleTestHttp}
|
onClick={handleTestHttp}
|
||||||
disabled={
|
disabled={
|
||||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === false ||
|
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
|
||||||
state.testStatus === "testing"
|
.success === false || state.testStatus === "testing"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{state.testStatus === "testing" ? (
|
{state.testStatus === "testing" ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<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" />
|
<MousePointerClick className="h-4 w-4" />
|
||||||
{t("addConnection.httpConnection.connectionTest.button.label")}
|
{t(
|
||||||
|
"addConnection.httpConnection.connectionTest.button.label",
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -432,7 +477,9 @@ export default function AddConnectionDialog({
|
|||||||
{state.testStatus === "failure" && (
|
{state.testStatus === "failure" && (
|
||||||
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
|
||||||
<XCircle className="h-4 w-4" />
|
<XCircle className="h-4 w-4" />
|
||||||
{t("addConnection.httpConnection.connectionTest.notReachable")}
|
{t(
|
||||||
|
"addConnection.httpConnection.connectionTest.notReachable",
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -442,8 +489,8 @@ export default function AddConnectionDialog({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
validate: () =>
|
validate: () =>
|
||||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === true &&
|
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
|
||||||
state.testStatus === "success",
|
.success === true && state.testStatus === "success",
|
||||||
build: () => ({
|
build: () => ({
|
||||||
type: "http",
|
type: "http",
|
||||||
name: state.name.trim(),
|
name: state.name.trim(),
|
||||||
@@ -456,8 +503,12 @@ export default function AddConnectionDialog({
|
|||||||
<>
|
<>
|
||||||
<SupportBadge
|
<SupportBadge
|
||||||
supported={bluetoothSupported}
|
supported={bluetoothSupported}
|
||||||
labelSupported={t("addConnection.bluetoothConnection.supported.title")}
|
labelSupported={t(
|
||||||
labelUnsupported={t("addConnection.bluetoothConnection.notSupported.title")}
|
"addConnection.bluetoothConnection.supported.title",
|
||||||
|
)}
|
||||||
|
labelUnsupported={t(
|
||||||
|
"addConnection.bluetoothConnection.notSupported.title",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<PickerRow
|
<PickerRow
|
||||||
label={t("addConnection.bluetoothConnection.device")}
|
label={t("addConnection.bluetoothConnection.device")}
|
||||||
@@ -471,7 +522,10 @@ export default function AddConnectionDialog({
|
|||||||
}
|
}
|
||||||
helper={t("addConnection.bluetoothConnection.helperText")}
|
helper={t("addConnection.bluetoothConnection.helperText")}
|
||||||
/>
|
/>
|
||||||
<FeatureErrorMessage missingFeatures={unsupported} tabId="bluetooth" />
|
<FeatureErrorMessage
|
||||||
|
missingFeatures={unsupported}
|
||||||
|
tabId="bluetooth"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
validate: () => state.name.trim().length > 0 && !!state.btSelected,
|
validate: () => state.name.trim().length > 0 && !!state.btSelected,
|
||||||
@@ -489,8 +543,12 @@ export default function AddConnectionDialog({
|
|||||||
<>
|
<>
|
||||||
<SupportBadge
|
<SupportBadge
|
||||||
supported={serialSupported}
|
supported={serialSupported}
|
||||||
labelSupported={t("addConnection.serialConnection.supported.title")}
|
labelSupported={t(
|
||||||
labelUnsupported={t("addConnection.serialConnection.notSupported.title")}
|
"addConnection.serialConnection.supported.title",
|
||||||
|
)}
|
||||||
|
labelUnsupported={t(
|
||||||
|
"addConnection.serialConnection.notSupported.title",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<PickerRow
|
<PickerRow
|
||||||
label={t("addConnection.serialConnection.port")}
|
label={t("addConnection.serialConnection.port")}
|
||||||
@@ -500,8 +558,10 @@ export default function AddConnectionDialog({
|
|||||||
display={
|
display={
|
||||||
state.serialSelected
|
state.serialSelected
|
||||||
? t("addConnection.serialConnection.deviceName", {
|
? t("addConnection.serialConnection.deviceName", {
|
||||||
vendorId: state.serialSelected.vendorId?.toString(16) ?? "?",
|
vendorId:
|
||||||
productId: state.serialSelected.productId?.toString(16) ?? "?",
|
state.serialSelected.vendorId?.toString(16) ?? "?",
|
||||||
|
productId:
|
||||||
|
state.serialSelected.productId?.toString(16) ?? "?",
|
||||||
})
|
})
|
||||||
: t("addConnection.serialConnection.notSelected")
|
: t("addConnection.serialConnection.notSelected")
|
||||||
}
|
}
|
||||||
@@ -511,7 +571,8 @@ export default function AddConnectionDialog({
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
validate: () =>
|
validate: () =>
|
||||||
state.name.trim().length > 0 && (!!state.serialSelected || !serialSupported),
|
state.name.trim().length > 0 &&
|
||||||
|
(!!state.serialSelected || !serialSupported),
|
||||||
build: () => ({
|
build: () => ({
|
||||||
type: "serial",
|
type: "serial",
|
||||||
name: state.name.trim(),
|
name: state.name.trim(),
|
||||||
@@ -537,7 +598,8 @@ export default function AddConnectionDialog({
|
|||||||
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
|
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
|
||||||
|
|
||||||
const submit =
|
const submit =
|
||||||
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => {
|
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
|
||||||
|
async () => {
|
||||||
if (!canCreate) {
|
if (!canCreate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -546,7 +608,8 @@ export default function AddConnectionDialog({
|
|||||||
if (!payload) {
|
if (!payload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined;
|
const btDevice =
|
||||||
|
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
|
||||||
await fn(payload, btDevice);
|
await fn(payload, btDevice);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -562,7 +625,9 @@ export default function AddConnectionDialog({
|
|||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={state.tab}
|
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">
|
<TabsList className="grid grid-cols-3">
|
||||||
{TAB_META.map(({ key, label, Icon }) => (
|
{TAB_META.map(({ key, label, Icon }) => (
|
||||||
@@ -582,14 +647,20 @@ export default function AddConnectionDialog({
|
|||||||
<Input
|
<Input
|
||||||
id={`name-${state.tab}`}
|
id={`name-${state.tab}`}
|
||||||
value={state.name}
|
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}
|
placeholder={currentPane.placeholder}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{PANES[key].children()}
|
{PANES[key].children()}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="inline-flex rounded-md shadow-sm overflow-hidden border">
|
<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")}
|
{t("button.saveConnection")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,13 +28,18 @@ export const urlOrIpv4Schema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IPv4 pattern
|
// 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)
|
// Domain pattern (e.g. example.com, meshtastic.local)
|
||||||
const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/;
|
const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/;
|
||||||
// Local domain (e.g. meshtastic.local)
|
// Local domain (e.g. meshtastic.local)
|
||||||
const localDomainRegex = /^(?!-)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.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)")
|
}, "Must be a valid IPv4 address or domain name with optional port (10-65535)")
|
||||||
.transform((val) => {
|
.transform((val) => {
|
||||||
return /^https?:\/\//i.test(val) ? val : `http://${val}`;
|
return /^https?:\/\//i.test(val) ? val : `http://${val}`;
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ describe("ClearAllStoresDialog", () => {
|
|||||||
|
|
||||||
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
|
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
|
||||||
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
|
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(mockClearAllStores).toHaveBeenCalledTimes(1);
|
||||||
expect(assignedHref).toBe("/"); // forced reload target
|
expect(assignedHref).toBe("/"); // forced reload target
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ export interface ClearAllStoresDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClearAllStoresDialog = ({ open, onOpenChange }: ClearAllStoresDialogProps) => {
|
export const ClearAllStoresDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: ClearAllStoresDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
|
|
||||||
const handleClearAllStores = () => {
|
const handleClearAllStores = () => {
|
||||||
|
|||||||
+33
-11
@@ -21,9 +21,13 @@ export interface ClientNotificationDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificationDialogProps) => {
|
export const ClientNotificationDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: ClientNotificationDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection, getClientNotification, removeClientNotification } = useDevice();
|
const { connection, getClientNotification, removeClientNotification } =
|
||||||
|
useDevice();
|
||||||
const [securityNumber, setSecurityNumber] = useState("");
|
const [securityNumber, setSecurityNumber] = useState("");
|
||||||
|
|
||||||
const notification = getClientNotification(0);
|
const notification = getClientNotification(0);
|
||||||
@@ -42,9 +46,11 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
nonce: bigint,
|
nonce: bigint,
|
||||||
secNum?: number,
|
secNum?: number,
|
||||||
) => {
|
) => {
|
||||||
connection?.sendKeyVerification(messageType, 0, nonce, secNum).catch((error) => {
|
connection
|
||||||
console.error("Failed to send key verification message:", error);
|
?.sendKeyVerification(messageType, 0, nonce, secNum)
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.error("Failed to send key verification message:", error);
|
||||||
|
});
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +68,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("keyVerification.inform.title")}</DialogTitle>
|
<DialogTitle>{t("keyVerification.inform.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("keyVerification.inform.description", { name: value.remoteLongname })}
|
{t("keyVerification.inform.description", {
|
||||||
|
name: value.remoteLongname,
|
||||||
|
})}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<p className="py-4 text-center font-mono text-4xl tracking-widest">
|
<p className="py-4 text-center font-mono text-4xl tracking-widest">
|
||||||
@@ -84,7 +92,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("keyVerification.request.title")}</DialogTitle>
|
<DialogTitle>{t("keyVerification.request.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("keyVerification.request.description", { name: value.remoteLongname })}
|
{t("keyVerification.request.description", {
|
||||||
|
name: value.remoteLongname,
|
||||||
|
})}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Input
|
<Input
|
||||||
@@ -102,7 +112,11 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
variant="default"
|
variant="default"
|
||||||
disabled={securityNumber === ""}
|
disabled={securityNumber === ""}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
respond(MessageType.PROVIDE_SECURITY_NUMBER, value.nonce, Number(securityNumber))
|
respond(
|
||||||
|
MessageType.PROVIDE_SECURITY_NUMBER,
|
||||||
|
value.nonce,
|
||||||
|
Number(securityNumber),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t("keyVerification.request.submit")}
|
{t("keyVerification.request.submit")}
|
||||||
@@ -119,7 +133,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("keyVerification.final.title")}</DialogTitle>
|
<DialogTitle>{t("keyVerification.final.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("keyVerification.final.description", { name: value.remoteLongname })}
|
{t("keyVerification.final.description", {
|
||||||
|
name: value.remoteLongname,
|
||||||
|
})}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<p className="py-4 text-center font-mono text-4xl tracking-widest">
|
<p className="py-4 text-center font-mono text-4xl tracking-widest">
|
||||||
@@ -132,7 +148,10 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
>
|
>
|
||||||
{t("keyVerification.final.reject")}
|
{t("keyVerification.final.reject")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" onClick={() => respond(MessageType.DO_VERIFY, value.nonce)}>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => respond(MessageType.DO_VERIFY, value.nonce)}
|
||||||
|
>
|
||||||
{t("keyVerification.final.verify")}
|
{t("keyVerification.final.verify")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -145,7 +164,10 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("clientNotification.title")}</DialogTitle>
|
<DialogTitle>{t("clientNotification.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t([`clientNotification.${notification?.message}`, notification?.message ?? ""])}
|
{t([
|
||||||
|
`clientNotification.${notification?.message}`,
|
||||||
|
notification?.message ?? "",
|
||||||
|
])}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,13 +38,19 @@ describe("DeleteMessagesDialog", () => {
|
|||||||
it("renders the dialog when open is true", () => {
|
it("renders the dialog when open is true", () => {
|
||||||
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
||||||
expect(screen.getByText("Clear All Messages")).toBeInTheDocument();
|
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: "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", () => {
|
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();
|
expect(screen.queryByText("Clear All Messages")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export interface DeleteMessagesDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMessagesDialog = ({ open, onOpenChange }: DeleteMessagesDialogProps) => {
|
export const DeleteMessagesDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DeleteMessagesDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -33,7 +33,9 @@ describe("FactoryResetConfigDialog", () => {
|
|||||||
it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
|
it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
|
||||||
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
|
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);
|
expect(mockFactoryReset).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,10 @@ export interface FactoryResetConfigDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FactoryResetConfigDialog = ({ open, onOpenChange }: FactoryResetConfigDialogProps) => {
|
export const FactoryResetConfigDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: FactoryResetConfigDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -47,7 +47,9 @@ describe("FactoryResetDeviceDialog", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
|
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Device" }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Factory Reset Device" }),
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
|
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,10 @@ export interface FactoryResetDeviceDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDeviceDialogProps) => {
|
export const FactoryResetDeviceDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: FactoryResetDeviceDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection, id } = useDevice();
|
const { connection, id } = useDevice();
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
try {
|
try {
|
||||||
const channelsUrl = new URL(importDialogInput);
|
const channelsUrl = new URL(importDialogInput);
|
||||||
if (
|
if (
|
||||||
(channelsUrl.hostname !== "meshtastic.org" && channelsUrl.pathname !== "/e/") ||
|
(channelsUrl.hostname !== "meshtastic.org" &&
|
||||||
|
channelsUrl.pathname !== "/e/") ||
|
||||||
!channelsUrl.hash
|
!channelsUrl.hash
|
||||||
) {
|
) {
|
||||||
throw t("import.error.invalidUrl");
|
throw t("import.error.invalidUrl");
|
||||||
@@ -56,7 +57,11 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
|
|
||||||
const encodedChannelConfig = channelsUrl.hash.substring(1);
|
const encodedChannelConfig = channelsUrl.hash.substring(1);
|
||||||
const paddedString = encodedChannelConfig
|
const paddedString = encodedChannelConfig
|
||||||
.padEnd(encodedChannelConfig.length + ((4 - (encodedChannelConfig.length % 4)) % 4), "=")
|
.padEnd(
|
||||||
|
encodedChannelConfig.length +
|
||||||
|
((4 - (encodedChannelConfig.length % 4)) % 4),
|
||||||
|
"=",
|
||||||
|
)
|
||||||
.replace(/-/g, "+")
|
.replace(/-/g, "+")
|
||||||
.replace(/_/g, "/");
|
.replace(/_/g, "/");
|
||||||
|
|
||||||
@@ -79,22 +84,24 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
|
|
||||||
const apply = () => {
|
const apply = () => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
channelSet?.settings.forEach((ch: Protobuf.Channel.ChannelSettings, index: number) => {
|
channelSet?.settings.forEach(
|
||||||
if (importIndex[index] === -1) {
|
(ch: Protobuf.Channel.ChannelSettings, index: number) => {
|
||||||
return;
|
if (importIndex[index] === -1) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = create(Protobuf.Channel.ChannelSchema, {
|
const payload = create(Protobuf.Channel.ChannelSchema, {
|
||||||
index: importIndex[index],
|
index: importIndex[index],
|
||||||
role:
|
role:
|
||||||
importIndex[index] === 0
|
importIndex[index] === 0
|
||||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||||
settings: ch,
|
settings: ch,
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.setChannel(payload);
|
editor.setChannel(payload);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (channelSet?.loraConfig && updateConfig) {
|
if (channelSet?.loraConfig && updateConfig) {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -126,14 +133,23 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("import.title")}</DialogTitle>
|
<DialogTitle>{t("import.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans i18nKey={"import.description"} components={{ italic: <i />, br: <br /> }} />
|
<Trans
|
||||||
|
i18nKey={"import.description"}
|
||||||
|
components={{ italic: <i />, br: <br /> }}
|
||||||
|
/>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>{t("import.channelSetUrl")}</Label>
|
<Label>{t("import.channelSetUrl")}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={importDialogInput}
|
value={importDialogInput}
|
||||||
variant={importDialogInput === "" ? "default" : validUrl ? "dirty" : "invalid"}
|
variant={
|
||||||
|
importDialogInput === ""
|
||||||
|
? "default"
|
||||||
|
: validUrl
|
||||||
|
? "dirty"
|
||||||
|
: "invalid"
|
||||||
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setImportDialogInput(e.target.value);
|
setImportDialogInput(e.target.value);
|
||||||
}}
|
}}
|
||||||
@@ -162,7 +178,10 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
<span className="flex-1">{t("import.channelSlot")}</span>
|
<span className="flex-1">{t("import.channelSlot")}</span>
|
||||||
</div>
|
</div>
|
||||||
{channelSet?.settings.map((channel, index) => (
|
{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">
|
<Label className="flex-1">
|
||||||
{channel.name.length
|
{channel.name.length
|
||||||
? channel.name
|
? channel.name
|
||||||
@@ -182,10 +201,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
|||||||
disabled={importIndex.includes(i) && index !== i}
|
disabled={importIndex.includes(i) && index !== i}
|
||||||
value={i.toString()}
|
value={i.toString()}
|
||||||
>
|
>
|
||||||
{i === 0 ? t("import.primary") : `${t("import.channelPrefix")}${i}`}
|
{i === 0
|
||||||
|
? t("import.primary")
|
||||||
|
: `${t("import.channelPrefix")}${i}`}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectItem value="-1">{t("import.doNotImport")}</SelectItem>
|
<SelectItem value="-1">
|
||||||
|
{t("import.doNotImport")}
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ export const LocationResponseDialog = ({
|
|||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const from = useNodeAsProto(location?.from ?? 0);
|
const from = useNodeAsProto(location?.from ?? 0);
|
||||||
const longName =
|
const longName =
|
||||||
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
from?.user?.longName ??
|
||||||
|
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||||
const shortName =
|
const shortName =
|
||||||
from?.user?.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;
|
const position = location?.data;
|
||||||
|
|
||||||
@@ -65,18 +68,23 @@ export const LocationResponseDialog = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
{position.latitudeI ?? 0 / 1e7}, {position.longitudeI ?? 0 / 1e7}
|
{position.latitudeI ?? 0 / 1e7},{" "}
|
||||||
|
{position.longitudeI ?? 0 / 1e7}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t("locationResponse.altitude")} {position.altitude}
|
{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>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Optional: Show a message if coordinates are not available
|
// 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>
|
</DialogDescription>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ export interface ManagedModeDialogProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeDialogProps) => {
|
export const ManagedModeDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
}: ManagedModeDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const [confirmState, setConfirmState] = useState(false);
|
const [confirmState, setConfirmState] = useState(false);
|
||||||
|
|
||||||
@@ -43,7 +47,9 @@ export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeD
|
|||||||
onChange={() => setConfirmState(!confirmState)}
|
onChange={() => setConfirmState(!confirmState)}
|
||||||
name="confirmUnderstanding"
|
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>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export interface NodeDetailsDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps) => {
|
export const NodeDetailsDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: NodeDetailsDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { setDialogOpen, connection } = useDevice();
|
const { setDialogOpen, connection } = useDevice();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -62,8 +65,12 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
|
|
||||||
const node = useNodeAsProto(nodeNumDetails);
|
const node = useNodeAsProto(nodeNumDetails);
|
||||||
|
|
||||||
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(node?.isFavorite ?? false);
|
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(
|
||||||
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(node?.isIgnored ?? false);
|
node?.isFavorite ?? false,
|
||||||
|
);
|
||||||
|
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(
|
||||||
|
node?.isIgnored ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
@@ -178,7 +185,8 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
key: "batteryLevel",
|
key: "batteryLevel",
|
||||||
label: t("nodeDetails.batteryLevel"),
|
label: t("nodeDetails.batteryLevel"),
|
||||||
value: node.deviceMetrics?.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",
|
key: "voltage",
|
||||||
@@ -211,21 +219,35 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="w-full ">
|
<div className="w-full ">
|
||||||
<div className="flex flex-row flex-wrap space-y-1">
|
<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" />
|
<MessageSquareIcon className="mr-2" />
|
||||||
{t("nodeDetails.message")}
|
{t("nodeDetails.message")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="mr-1" name="traceRoute" onClick={handleTraceroute}>
|
<Button
|
||||||
|
className="mr-1"
|
||||||
|
name="traceRoute"
|
||||||
|
onClick={handleTraceroute}
|
||||||
|
>
|
||||||
<WaypointsIcon className="mr-2" />
|
<WaypointsIcon className="mr-2" />
|
||||||
{t("nodeDetails.traceRoute")}
|
{t("nodeDetails.traceRoute")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="mr-1" onClick={handleToggleFavorite}>
|
<Button className="mr-1" onClick={handleToggleFavorite}>
|
||||||
<StarIcon
|
<StarIcon
|
||||||
className={cn(isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "")}
|
className={cn(
|
||||||
|
isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
{node.user?.publicKey && node.user.publicKey.length > 0 && (
|
{node.user?.publicKey && node.user.publicKey.length > 0 && (
|
||||||
<Button className="mr-1" name="verifyKey" onClick={handleInitiateKeyVerification}>
|
<Button
|
||||||
|
className="mr-1"
|
||||||
|
name="verifyKey"
|
||||||
|
onClick={handleInitiateKeyVerification}
|
||||||
|
>
|
||||||
<ShieldCheckIcon className="mr-2" />
|
<ShieldCheckIcon className="mr-2" />
|
||||||
{t("nodeDetails.verifyKey")}
|
{t("nodeDetails.verifyKey")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -248,7 +270,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
|
<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" />
|
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -278,7 +302,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
<div className="flex flex-col flex-wrap space-x-1 space-y-1">
|
<div className="flex flex-col flex-wrap space-x-1 space-y-1">
|
||||||
<div className="flex flex-row space-x-2">
|
<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">
|
<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">
|
<table className="table-fixed w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -292,10 +318,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("nodeDetails.role")}</td>
|
<td>{t("nodeDetails.role")}</td>
|
||||||
<td>
|
<td>
|
||||||
{Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0]?.replace(
|
{Protobuf.Config.Config_DeviceConfig_Role[
|
||||||
/_/g,
|
node.user?.role ?? 0
|
||||||
" ",
|
]?.replace(/_/g, " ")}
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -314,35 +339,44 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
<td>{t("nodeDetails.hardware")}</td>
|
<td>{t("nodeDetails.hardware")}</td>
|
||||||
<td>
|
<td>
|
||||||
{(
|
{(
|
||||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
Protobuf.Mesh.HardwareModel[
|
||||||
t("unknown.shortName")
|
node.user?.hwModel ?? 0
|
||||||
|
] ?? t("unknown.shortName")
|
||||||
).replace(/_/g, " ")}
|
).replace(/_/g, " ")}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("nodeDetails.messageable")}</td>
|
<td>{t("nodeDetails.messageable")}</td>
|
||||||
<td>{node.user?.isUnmessagable ? t("no") : t("yes")}</td>
|
<td>
|
||||||
|
{node.user?.isUnmessagable ? t("no") : t("yes")}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<DeviceImage
|
<DeviceImage
|
||||||
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className={sectionClassName}>
|
<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">
|
<table className="table-auto w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
|
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
|
||||||
<td>
|
<td>
|
||||||
<pre className="text-xs pt-0.5">
|
<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)
|
? fromByteArray(node.user.publicKey)
|
||||||
: t("unknown.longName")}
|
: t("unknown.longName")}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -361,7 +395,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={sectionClassName}>
|
<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 ? (
|
{node.position ? (
|
||||||
<table className="table-auto w-full">
|
<table className="table-auto w-full">
|
||||||
@@ -378,7 +414,8 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{node.position.latitudeI / 1e7}, {node.position.longitudeI / 1e7}
|
{node.position.latitudeI / 1e7},{" "}
|
||||||
|
{node.position.longitudeI / 1e7}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -397,7 +434,11 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
) : (
|
) : (
|
||||||
<p>{t("unknown.longName")}</p>
|
<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" />
|
<MapPinnedIcon className="mr-2" />
|
||||||
{t("nodeDetails.requestPosition")}
|
{t("nodeDetails.requestPosition")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -422,7 +463,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("nodeDetails.uptime")}</td>
|
<td>{t("nodeDetails.uptime")}</td>
|
||||||
<td>
|
<td>
|
||||||
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
|
<Uptime
|
||||||
|
seconds={node.deviceMetrics.uptimeSeconds}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
@@ -441,7 +484,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
|
|||||||
</p>
|
</p>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="overflow-x-scroll">
|
<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>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -20,19 +20,25 @@ export interface PkiBackupDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) => {
|
export const PkiBackupDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: PkiBackupDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { config, setDialogOpen } = useDevice();
|
const { config, setDialogOpen } = useDevice();
|
||||||
const myNode = useMyNodeAsProto();
|
const myNode = useMyNodeAsProto();
|
||||||
const privateKey = config.security?.privateKey;
|
const privateKey = config.security?.privateKey;
|
||||||
const publicKey = config.security?.publicKey;
|
const publicKey = config.security?.publicKey;
|
||||||
|
|
||||||
const decodeKeyData = React.useCallback((key: Uint8Array<ArrayBufferLike>) => {
|
const decodeKeyData = React.useCallback(
|
||||||
if (!key) {
|
(key: Uint8Array<ArrayBufferLike>) => {
|
||||||
return "";
|
if (!key) {
|
||||||
}
|
return "";
|
||||||
return fromByteArray(key ?? new Uint8Array(0));
|
}
|
||||||
}, []);
|
return fromByteArray(key ?? new Uint8Array(0));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const closeDialog = React.useCallback(() => {
|
const closeDialog = React.useCallback(() => {
|
||||||
setDialogOpen("pkiBackup", false);
|
setDialogOpen("pkiBackup", false);
|
||||||
@@ -128,7 +134,9 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
|
|||||||
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
|
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
|
||||||
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
|
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
|
||||||
<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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="mt-6">
|
<DialogFooter className="mt-6">
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export const PkiRegenerateDialog = ({
|
|||||||
<DialogDescription>{dialogText.description}</DialogDescription>
|
<DialogDescription>{dialogText.description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="destructive" name="regenerate" onClick={() => onSubmit()}>
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
name="regenerate"
|
||||||
|
onClick={() => onSubmit()}
|
||||||
|
>
|
||||||
{dialogText.button}
|
{dialogText.button}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -44,12 +44,16 @@ export const QRDialog = ({ open, onOpenChange, loraConfig }: QRDialogProps) => {
|
|||||||
settings: channelsToEncode,
|
settings: channelsToEncode,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const base64 = fromByteArray(toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded))
|
const base64 = fromByteArray(
|
||||||
|
toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded),
|
||||||
|
)
|
||||||
.replace(/=/g, "")
|
.replace(/=/g, "")
|
||||||
.replace(/\+/g, "-")
|
.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]);
|
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,9 +84,14 @@ export const QRDialog = ({ open, onOpenChange, loraConfig }: QRDialogProps) => {
|
|||||||
checked={selectedChannels.includes(channel.index)}
|
checked={selectedChannels.includes(channel.index)}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
if (selectedChannels.includes(channel.index)) {
|
if (selectedChannels.includes(channel.index)) {
|
||||||
setSelectedChannels(selectedChannels.filter((c) => c !== channel.index));
|
setSelectedChannels(
|
||||||
|
selectedChannels.filter((c) => c !== channel.index),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedChannels([...selectedChannels, channel.index]);
|
setSelectedChannels([
|
||||||
|
...selectedChannels,
|
||||||
|
channel.index,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
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 type { JSX } from "react/jsx-runtime";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { RebootDialog } from "./RebootDialog.tsx";
|
import { RebootDialog } from "./RebootDialog.tsx";
|
||||||
@@ -49,10 +54,16 @@ vi.mock("@components/UI/Input.tsx", async () => {
|
|||||||
vi.mock("@components/UI/Dialog.tsx", () => {
|
vi.mock("@components/UI/Dialog.tsx", () => {
|
||||||
return {
|
return {
|
||||||
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
DialogContent: ({ children }: { children: ReactNode }) => (
|
||||||
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
DialogHeader: ({ children }: { children: ReactNode }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
|
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
|
||||||
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
|
DialogDescription: ({ children }: { children: ReactNode }) => (
|
||||||
|
<p>{children}</p>
|
||||||
|
),
|
||||||
DialogClose: () => null,
|
DialogClose: () => null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -70,8 +81,12 @@ describe("RebootDialog", () => {
|
|||||||
it("renders dialog with default input value", () => {
|
it("renders dialog with default input value", () => {
|
||||||
render(<RebootDialog open onOpenChange={() => {}} />);
|
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||||
expect(screen.getByRole("heading", { name: /reboot device/i, level: 1 })).toBeInTheDocument();
|
expect(
|
||||||
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument();
|
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", () => {
|
it("calls correct reboot function based on OTA checkbox state", () => {
|
||||||
@@ -184,6 +199,8 @@ describe("RebootDialog", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||||
});
|
});
|
||||||
expect(rebootMock).toHaveBeenCalledWith(-1);
|
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,7 +96,11 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
|||||||
<Separator />
|
<Separator />
|
||||||
{!isScheduled ? (
|
{!isScheduled ? (
|
||||||
<>
|
<>
|
||||||
<Checkbox checked={isOTA} onChange={(checked) => setIsOTA(checked)} className="px-2">
|
<Checkbox
|
||||||
|
checked={isOTA}
|
||||||
|
onChange={(checked) => setIsOTA(checked)}
|
||||||
|
className="px-2"
|
||||||
|
>
|
||||||
{t("reboot.ota")}
|
{t("reboot.ota")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<div className="flex gap-2 px-2 items-center relative">
|
<div className="flex gap-2 px-2 items-center relative">
|
||||||
@@ -133,7 +137,9 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<div className="pb-6 pt-2 text-center">
|
<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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export interface RefreshKeysDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
export const RefreshKeysDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: RefreshKeysDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const nodeError = useNodeErrors()[0];
|
const nodeError = useNodeErrors()[0];
|
||||||
const nodeWithError = useNodeAsProto(nodeError?.node ?? 0);
|
const nodeWithError = useNodeAsProto(nodeError?.node ?? 0);
|
||||||
@@ -42,7 +45,10 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<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} />
|
<DialogClose onClick={handleCloseDialog} />
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{text.title}</DialogTitle>
|
<DialogTitle>{text.title}</DialogTitle>
|
||||||
@@ -51,17 +57,30 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
|
|||||||
<ul className="mt-2">
|
<ul className="mt-2">
|
||||||
<li className="flex place-items-center gap-2 items-start">
|
<li className="flex place-items-center gap-2 items-start">
|
||||||
<div className="p-2 bg-slate-500 rounded-lg mt-1">
|
<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>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div>
|
<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>
|
<p>{t("refreshKeys.description.acceptNewKeys")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="default" name="requestNewKeys" onClick={handleNodeRemove}>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
name="requestNewKeys"
|
||||||
|
onClick={handleNodeRemove}
|
||||||
|
>
|
||||||
{t("button.requestNewKeys")}
|
{t("button.requestNewKeys")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" name="dismiss" onClick={handleCloseDialog}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
name="dismiss"
|
||||||
|
onClick={handleCloseDialog}
|
||||||
|
>
|
||||||
{t("button.dismiss")}
|
{t("button.dismiss")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export interface RemoveNodeDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => {
|
export const RemoveNodeDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: RemoveNodeDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
const { nodeNumToBeRemoved } = useAppStore();
|
const { nodeNumToBeRemoved } = useAppStore();
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ describe("ResetNodeDbDialog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("calls reset({ keepMyNode: true }) then clears chat after resolve", async () => {
|
it("calls reset({ keepMyNode: true }) then clears chat after resolve", async () => {
|
||||||
let resolveReset: ((value: { status: "ok"; value: number }) => void) | undefined;
|
let resolveReset:
|
||||||
|
| ((value: { status: "ok"; value: number }) => void)
|
||||||
|
| undefined;
|
||||||
mockResetNodes.mockImplementation(
|
mockResetNodes.mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
@@ -35,7 +37,9 @@ describe("ResetNodeDbDialog", () => {
|
|||||||
mockClearAll.mockResolvedValue(undefined);
|
mockClearAll.mockResolvedValue(undefined);
|
||||||
|
|
||||||
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
|
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Reset Node Database" }),
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockResetNodes).toHaveBeenCalledWith({ keepMyNode: true });
|
expect(mockResetNodes).toHaveBeenCalledWith({ keepMyNode: true });
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export interface ResetNodeDbDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
|
export const ResetNodeDbDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: ResetNodeDbDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ export const TracerouteResponseDialog = ({
|
|||||||
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
|
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
|
||||||
const from = useNodeAsProto(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
|
const from = useNodeAsProto(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
|
||||||
const fromLongName =
|
const fromLongName =
|
||||||
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
from?.user?.longName ??
|
||||||
|
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||||
const fromShortName =
|
const fromShortName =
|
||||||
from?.user?.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 toUser = useNodeAsProto(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
|
const toUser = useNodeAsProto(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ export interface RouterRoleDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
|
export const UnsafeRolesDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: RouterRoleDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const [confirmState, setConfirmState] = useState(false);
|
const [confirmState, setConfirmState] = useState(false);
|
||||||
const { setDialogOpen } = useDevice();
|
const { setDialogOpen } = useDevice();
|
||||||
|
|
||||||
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
|
const deviceRoleLink =
|
||||||
|
"https://meshtastic.org/docs/configuration/radio/device/";
|
||||||
const choosingTheRightDeviceRoleLink =
|
const choosingTheRightDeviceRoleLink =
|
||||||
"https://meshtastic.org/blog/choosing-the-right-device-role/";
|
"https://meshtastic.org/blog/choosing-the-right-device-role/";
|
||||||
|
|
||||||
@@ -59,11 +63,17 @@ export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps)
|
|||||||
onChange={() => setConfirmState(!confirmState)}
|
onChange={() => setConfirmState(!confirmState)}
|
||||||
name="confirmUnderstanding"
|
name="confirmUnderstanding"
|
||||||
>
|
>
|
||||||
<span className="dark:text-white">{t("unsafeRoles.confirmUnderstanding")}</span>
|
<span className="dark:text-white">
|
||||||
|
{t("unsafeRoles.confirmUnderstanding")}
|
||||||
|
</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="mt-6">
|
<DialogFooter className="mt-6">
|
||||||
<Button variant="default" name="dismiss" onClick={() => handleCloseDialog("dismiss")}>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
name="dismiss"
|
||||||
|
onClick={() => handleCloseDialog("dismiss")}
|
||||||
|
>
|
||||||
{t("button.dismiss")}
|
{t("button.dismiss")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import {
|
|||||||
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||||
import { eventBus } from "@core/utils/eventBus.ts";
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
import { renderHook } from "@testing-library/react";
|
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();
|
const mockNavigate = vi.fn();
|
||||||
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
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 {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useNavigate: () => mockNavigate,
|
useNavigate: () => mockNavigate,
|
||||||
@@ -56,7 +65,10 @@ describe("useUnsafeRolesDialog", () => {
|
|||||||
|
|
||||||
result.current.handleCloseDialog();
|
result.current.handleCloseDialog();
|
||||||
|
|
||||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", false);
|
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||||
|
"unsafeRoles",
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,7 +77,8 @@ describe("useUnsafeRolesDialog", () => {
|
|||||||
const { result } = renderUnsafeRolesHook();
|
const { result } = renderUnsafeRolesHook();
|
||||||
const safeRole = "SAFE_ROLE";
|
const safeRole = "SAFE_ROLE";
|
||||||
|
|
||||||
const validationResult = await result.current.validateRoleSelection(safeRole);
|
const validationResult =
|
||||||
|
await result.current.validateRoleSelection(safeRole);
|
||||||
|
|
||||||
expect(validationResult).toBe(true);
|
expect(validationResult).toBe(true);
|
||||||
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
|
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
|
||||||
@@ -74,40 +87,61 @@ describe("useUnsafeRolesDialog", () => {
|
|||||||
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
|
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
|
||||||
const { result } = renderUnsafeRolesHook();
|
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(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||||
expect(eventBus.on).toHaveBeenCalledWith("dialog:unsafeRoles", expect.any(Function));
|
"unsafeRoles",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(eventBus.on).toHaveBeenCalledWith(
|
||||||
|
"dialog:unsafeRoles",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
|
||||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||||
onHandler({ action: "confirm" });
|
onHandler({ action: "confirm" });
|
||||||
const validationResult = await validationPromise;
|
const validationResult = await validationPromise;
|
||||||
|
|
||||||
expect(validationResult).toBe(true);
|
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 () => {
|
it("should resolve with false when user dismisses the dialog", async () => {
|
||||||
const { result } = renderUnsafeRolesHook();
|
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];
|
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||||
onHandler({ action: "dismiss" });
|
onHandler({ action: "dismiss" });
|
||||||
|
|
||||||
const validationResult = await validationPromise;
|
const validationResult = await validationPromise;
|
||||||
expect(validationResult).toBe(false);
|
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 () => {
|
it("should clean up event listener after response", async () => {
|
||||||
const { result } = renderUnsafeRolesHook();
|
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];
|
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||||
|
|
||||||
onHandler({ action: "confirm" });
|
onHandler({ action: "confirm" });
|
||||||
await validationPromise;
|
await validationPromise;
|
||||||
|
|
||||||
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
|
expect(eventBus.off).toHaveBeenCalledWith(
|
||||||
|
"dialog:unsafeRoles",
|
||||||
|
onHandler,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,9 +152,13 @@ describe("useUnsafeRolesDialog", () => {
|
|||||||
mockDevice.setDialogOpen.mockClear();
|
mockDevice.setDialogOpen.mockClear();
|
||||||
(eventBus.on as Mock).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];
|
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||||
onHandler({ action: "confirm" });
|
onHandler({ action: "confirm" });
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export const useUnsafeRolesDialog = () => {
|
|||||||
setDialogOpen("unsafeRoles", true);
|
setDialogOpen("unsafeRoles", true);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => {
|
const handleResponse = ({
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
action: "confirm" | "dismiss";
|
||||||
|
}) => {
|
||||||
eventBus.off("dialog:unsafeRoles", handleResponse);
|
eventBus.off("dialog:unsafeRoles", handleResponse);
|
||||||
resolve(action === "confirm");
|
resolve(action === "confirm");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export interface UseDialogReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useDialog = (initialState?: DialogState): 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) => {
|
const openDialog = useCallback((config: DialogConfig) => {
|
||||||
setState({ isOpen: true, config });
|
setState({ isOpen: true, config });
|
||||||
@@ -95,7 +97,10 @@ 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) {
|
switch (type) {
|
||||||
case "confirm":
|
case "confirm":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
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 { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||||
@@ -66,7 +69,9 @@ export interface DynamicFormProps<T extends FieldValues> {
|
|||||||
validationSchema?: ZodType<T>;
|
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>({
|
export function DynamicForm<T extends FieldValues>({
|
||||||
propMethods,
|
propMethods,
|
||||||
@@ -84,7 +89,9 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
const internalMethods = useForm<T>({
|
const internalMethods = useForm<T>({
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
defaultValues: defaultValues,
|
defaultValues: defaultValues,
|
||||||
resolver: validationSchema ? createZodResolver(validationSchema) : undefined,
|
resolver: validationSchema
|
||||||
|
? createZodResolver(validationSchema)
|
||||||
|
: undefined,
|
||||||
shouldFocusError: false,
|
shouldFocusError: false,
|
||||||
resetOptions: { keepDefaultValues: true },
|
resetOptions: { keepDefaultValues: true },
|
||||||
values,
|
values,
|
||||||
@@ -92,7 +99,8 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
|
|
||||||
const methods = propMethods ?? internalMethods;
|
const methods = propMethods ?? internalMethods;
|
||||||
|
|
||||||
const { handleSubmit, control, getValues, formState, getFieldState } = methods;
|
const { handleSubmit, control, getValues, formState, getFieldState } =
|
||||||
|
methods;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!propMethods) {
|
if (!propMethods) {
|
||||||
@@ -100,7 +108,10 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
}
|
}
|
||||||
}, [onFormInit, propMethods, internalMethods]);
|
}, [onFormInit, propMethods, internalMethods]);
|
||||||
|
|
||||||
const isDisabled = (disabledBy?: DisabledBy<T>[], disabled?: boolean): boolean => {
|
const isDisabled = (
|
||||||
|
disabledBy?: DisabledBy<T>[],
|
||||||
|
disabled?: boolean,
|
||||||
|
): boolean => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -117,7 +128,9 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
return field.invert ? value : !value;
|
return field.invert ? value : !value;
|
||||||
}
|
}
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return field.invert ? field.selector !== value : field.selector === value;
|
return field.invert
|
||||||
|
? field.selector !== value
|
||||||
|
: field.selector === value;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { GenericInput, type InputFieldProps } from "@components/Form/FormInput.tsx";
|
import {
|
||||||
|
GenericInput,
|
||||||
|
type InputFieldProps,
|
||||||
|
} from "@components/Form/FormInput.tsx";
|
||||||
import {
|
import {
|
||||||
PasswordGenerator,
|
PasswordGenerator,
|
||||||
type PasswordGeneratorProps,
|
type PasswordGeneratorProps,
|
||||||
} from "@components/Form/FormPasswordGenerator.tsx";
|
} from "@components/Form/FormPasswordGenerator.tsx";
|
||||||
import { type SelectFieldProps, SelectInput } from "@components/Form/FormSelect.tsx";
|
import {
|
||||||
import { type ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.tsx";
|
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 { Control, FieldValues } from "react-hook-form";
|
||||||
import { type MultiSelectFieldProps, MultiSelectInput } from "./FormMultiSelect.tsx";
|
import {
|
||||||
|
type MultiSelectFieldProps,
|
||||||
|
MultiSelectInput,
|
||||||
|
} from "./FormMultiSelect.tsx";
|
||||||
|
|
||||||
export type FieldProps<T> =
|
export type FieldProps<T> =
|
||||||
| InputFieldProps<T>
|
| InputFieldProps<T>
|
||||||
@@ -34,7 +46,14 @@ export function DynamicFormField<T extends FieldValues>({
|
|||||||
case "text":
|
case "text":
|
||||||
case "password":
|
case "password":
|
||||||
case "number":
|
case "number":
|
||||||
return <GenericInput field={field} control={control} disabled={disabled} isDirty={isDirty} />;
|
return (
|
||||||
|
<GenericInput
|
||||||
|
field={field}
|
||||||
|
control={control}
|
||||||
|
disabled={disabled}
|
||||||
|
isDirty={isDirty}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,22 +60,32 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
controllerField.onChange(
|
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 (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Input
|
<Input
|
||||||
type={field.type}
|
type={field.type}
|
||||||
step={field.properties?.step}
|
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}
|
id={field.name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
showCopyButton={field.properties?.showCopyButton}
|
showCopyButton={field.properties?.showCopyButton}
|
||||||
showPasswordToggle={field.properties?.showPasswordToggle || field.type === "password"}
|
showPasswordToggle={
|
||||||
|
field.properties?.showPasswordToggle || field.type === "password"
|
||||||
|
}
|
||||||
className={field.properties?.className}
|
className={field.properties?.className}
|
||||||
{...restProperties}
|
{...restProperties}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -33,17 +33,20 @@ export function MultiSelectInput<T extends FieldValues>({
|
|||||||
Object.values(enumValue)[0] !== null &&
|
Object.values(enumValue)[0] !== null &&
|
||||||
"i18nKey" in Object.values(enumValue)[0];
|
"i18nKey" in Object.values(enumValue)[0];
|
||||||
|
|
||||||
const optionsToRender = Object.entries(enumValue).map(([key, configOrValue]) => {
|
const optionsToRender = Object.entries(enumValue).map(
|
||||||
if (isNewConfigStructure) {
|
([key, configOrValue]) => {
|
||||||
const config = configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
|
if (isNewConfigStructure) {
|
||||||
return {
|
const config =
|
||||||
key,
|
configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
|
||||||
display: t(config.i18nKey),
|
return {
|
||||||
value: config.value,
|
key,
|
||||||
};
|
display: t(config.i18nKey),
|
||||||
}
|
value: config.value,
|
||||||
return { key, display: key, value: configOrValue as number };
|
};
|
||||||
});
|
}
|
||||||
|
return { key, display: key, value: configOrValue as number };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
|||||||
@@ -49,8 +49,13 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
control,
|
control,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { enumValue, formatEnumName, defaultValue, className, ...remainingProperties } =
|
const {
|
||||||
field.properties;
|
enumValue,
|
||||||
|
formatEnumName,
|
||||||
|
defaultValue,
|
||||||
|
className,
|
||||||
|
...remainingProperties
|
||||||
|
} = field.properties;
|
||||||
const valueToKeyMap: Record<string, string> = {};
|
const valueToKeyMap: Record<string, string> = {};
|
||||||
const optionsEnumValues: [string, number][] = [];
|
const optionsEnumValues: [string, number][] = [];
|
||||||
|
|
||||||
@@ -95,7 +100,9 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
className={cn([
|
className={cn([
|
||||||
className,
|
className,
|
||||||
isDirty ? "focus:ring-sky-500 ring-sky-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 outline-offset-2" : "",
|
invalid
|
||||||
|
? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2"
|
||||||
|
: "",
|
||||||
])}
|
])}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ export function ToggleInput<T extends FieldValues>({
|
|||||||
{...field.properties}
|
{...field.properties}
|
||||||
className={cn([
|
className={cn([
|
||||||
field.properties?.className,
|
field.properties?.className,
|
||||||
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
|
isDirty
|
||||||
invalid ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2" : "",
|
? "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}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export const FieldWrapper = ({
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
|
||||||
<Label htmlFor={fieldName}>{label}</Label>
|
<Label htmlFor={fieldName}>{label}</Label>
|
||||||
<div className="max-w-3xl">
|
<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">
|
<p hidden={valid ?? true} className="text-sm text-red-500">
|
||||||
{validationText}
|
{validationText}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import type { ZodType } from "zod/v4";
|
|||||||
export function createZodResolver<T extends FieldValues>(
|
export function createZodResolver<T extends FieldValues>(
|
||||||
schema: ZodType<T, unknown>,
|
schema: ZodType<T, unknown>,
|
||||||
): Resolver<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);
|
const result = schema.safeParse(values);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
@@ -20,7 +24,10 @@ 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) {
|
for (const issue of result.error.issues) {
|
||||||
const { path, code, message, ...params } = issue;
|
const { path, code, message, ...params } = issue;
|
||||||
@@ -36,7 +43,8 @@ export function createZodResolver<T extends FieldValues>(
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const newCode =
|
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> } = {
|
const fieldError: FieldError & { params?: Record<string, unknown> } = {
|
||||||
type: newCode,
|
type: newCode,
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ interface LanguageSwitcherProps {
|
|||||||
disableHover?: boolean;
|
disableHover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LanguageSwitcher({ disableHover = false }: LanguageSwitcherProps) {
|
export default function LanguageSwitcher({
|
||||||
|
disableHover = false,
|
||||||
|
}: LanguageSwitcherProps) {
|
||||||
const { i18n } = useTranslation("ui");
|
const { i18n } = useTranslation("ui");
|
||||||
const { set: setLanguage, current, getSupportedLangs } = useLang();
|
const { set: setLanguage, current, getSupportedLangs } = useLang();
|
||||||
|
|
||||||
@@ -44,13 +46,15 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
|
|||||||
size={16}
|
size={16}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-gray-500 dark:text-gray-400 w-4 flex-shrink-0 transition-colors duration-150",
|
"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
|
<Subtle
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm text-gray-600 dark:text-gray-100 transition-colors duration-150",
|
"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")}:`}
|
{`${t("languagePicker.label")}:`}
|
||||||
@@ -58,7 +62,8 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
|
|||||||
<Subtle
|
<Subtle
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium text-gray-700 dark:text-gray-200 transition-colors duration-150",
|
"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}
|
{current?.name}
|
||||||
@@ -76,7 +81,9 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
|
|||||||
<span>{language.flag}</span>
|
<span>{language.flag}</span>
|
||||||
<span>{language.name}</span>
|
<span>{language.name}</span>
|
||||||
</div>
|
</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>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ export const BaseMap = ({
|
|||||||
|
|
||||||
const locale = useMemo(() => {
|
const locale = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
"GeolocateControl.FindMyLocation": t("maplibre.GeolocateControl.FindMyLocation"),
|
"GeolocateControl.FindMyLocation": t(
|
||||||
|
"maplibre.GeolocateControl.FindMyLocation",
|
||||||
|
),
|
||||||
"NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"),
|
"NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"),
|
||||||
"NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"),
|
"NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"),
|
||||||
"ScaleControl.Meters": t("unit.meter.suffix"),
|
"ScaleControl.Meters": t("unit.meter.suffix"),
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { type ChannelValidation, makeChannelSchema } from "@app/validation/channel.ts";
|
import {
|
||||||
|
type ChannelValidation,
|
||||||
|
makeChannelSchema,
|
||||||
|
} from "@app/validation/channel.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -23,7 +29,10 @@ const EMPTY_CHANNELS_SIGNAL = {
|
|||||||
Protobuf.Channel.Channel
|
Protobuf.Channel.Channel
|
||||||
>,
|
>,
|
||||||
peek: () =>
|
peek: () =>
|
||||||
new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<number, Protobuf.Channel.Channel>,
|
new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<
|
||||||
|
number,
|
||||||
|
Protobuf.Channel.Channel
|
||||||
|
>,
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -43,7 +52,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
moduleSettings: {
|
moduleSettings: {
|
||||||
...defaultConfig?.settings?.moduleSettings,
|
...defaultConfig?.settings?.moduleSettings,
|
||||||
positionPrecision:
|
positionPrecision:
|
||||||
defaultConfig?.settings?.moduleSettings?.positionPrecision === undefined
|
defaultConfig?.settings?.moduleSettings?.positionPrecision ===
|
||||||
|
undefined
|
||||||
? 10
|
? 10
|
||||||
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
|
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
|
||||||
},
|
},
|
||||||
@@ -62,7 +72,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
moduleSettings: {
|
moduleSettings: {
|
||||||
...effectiveConfig?.settings?.moduleSettings,
|
...effectiveConfig?.settings?.moduleSettings,
|
||||||
positionPrecision:
|
positionPrecision:
|
||||||
effectiveConfig?.settings?.moduleSettings?.positionPrecision === undefined
|
effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
|
||||||
|
undefined
|
||||||
? 10
|
? 10
|
||||||
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
|
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
|
||||||
},
|
},
|
||||||
@@ -70,8 +81,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false);
|
const [preSharedDialogOpen, setPreSharedDialogOpen] =
|
||||||
const [byteCount, setBytes] = useState<number>(effectiveConfig?.settings?.psk.length ?? 16);
|
useState<boolean>(false);
|
||||||
|
const [byteCount, setBytes] = useState<number>(
|
||||||
|
effectiveConfig?.settings?.psk.length ?? 16,
|
||||||
|
);
|
||||||
const ChannelValidationSchema = useMemo(() => {
|
const ChannelValidationSchema = useMemo(() => {
|
||||||
return makeChannelSchema(byteCount);
|
return makeChannelSchema(byteCount);
|
||||||
}, [byteCount]);
|
}, [byteCount]);
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { Channel } from "@app/components/PageComponents/Channels/Channel";
|
|||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Spinner } from "@components/UI/Spinner.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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useChannels, useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useChannels, useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -16,7 +21,10 @@ interface ConfigProps {
|
|||||||
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void;
|
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChannelName = (channel: { index: number; settings?: { name?: string } }) => {
|
export const getChannelName = (channel: {
|
||||||
|
index: number;
|
||||||
|
settings?: { name?: string };
|
||||||
|
}) => {
|
||||||
return channel.settings?.name?.length
|
return channel.settings?.name?.length
|
||||||
? channel.settings.name
|
? channel.settings.name
|
||||||
: channel.index === 0
|
: channel.index === 0
|
||||||
@@ -37,7 +45,9 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
|||||||
const { setDialogOpen } = useDevice();
|
const { setDialogOpen } = useDevice();
|
||||||
const editor = useConfigEditor();
|
const editor = useConfigEditor();
|
||||||
const channels = useChannels();
|
const channels = useChannels();
|
||||||
const dirtyChannels = useSignal(editor?.dirtyChannels ?? EMPTY_DIRTY_CHANNELS_SIGNAL);
|
const dirtyChannels = useSignal(
|
||||||
|
editor?.dirtyChannels ?? EMPTY_DIRTY_CHANNELS_SIGNAL,
|
||||||
|
);
|
||||||
const { t } = useTranslation("channels");
|
const { t } = useTranslation("channels");
|
||||||
|
|
||||||
const allChannels = useMemo(
|
const allChannels = useMemo(
|
||||||
@@ -53,7 +63,12 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
|||||||
);
|
);
|
||||||
const flags = useMemo(
|
const flags = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(allChannels.map((channel) => [channel.index, dirtyChannels.includes(channel.index)])),
|
new Map(
|
||||||
|
allChannels.map((channel) => [
|
||||||
|
channel.index,
|
||||||
|
dirtyChannels.includes(channel.index),
|
||||||
|
]),
|
||||||
|
),
|
||||||
[allChannels, dirtyChannels],
|
[allChannels, dirtyChannels],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,7 +90,10 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
|||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</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} />
|
<UploadIcon className="mr-2" size={14} />
|
||||||
{t("page.import")}
|
{t("page.import")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -85,9 +103,16 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{allChannels.map((channel) => (
|
{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" />}>
|
<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>
|
</Suspense>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Button } from "@app/components/UI/Button";
|
import { Button } from "@app/components/UI/Button";
|
||||||
import type { Connection } from "@app/core/stores/deviceStore/types";
|
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 color = "";
|
||||||
let displayStatus = status;
|
let displayStatus = status;
|
||||||
|
|
||||||
@@ -26,8 +30,13 @@ export function ConnectionStatusBadge({ status }: { status: Connection["status"]
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button variant="subtle" className="inline-flex items-center gap-2">
|
<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
|
||||||
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">{displayStatus}</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>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ export interface HeatmapLayerProps {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLayerProps) => {
|
export const HeatmapLayer = ({
|
||||||
|
id,
|
||||||
|
filteredNodes,
|
||||||
|
mode,
|
||||||
|
isVisible,
|
||||||
|
}: HeatmapLayerProps) => {
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
const data: FeatureCollection = useMemo(() => {
|
const data: FeatureCollection = useMemo(() => {
|
||||||
const features: Feature[] = filteredNodes
|
const features: Feature[] = filteredNodes
|
||||||
@@ -43,7 +48,9 @@ export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLaye
|
|||||||
const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
|
const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
"heatmap-weight":
|
"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],
|
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3],
|
||||||
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
|
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
|
||||||
// Begin color ramp at 0-stop with a 0-transparancy color
|
// Begin color ramp at 0-stop with a 0-transparancy color
|
||||||
@@ -65,7 +72,17 @@ export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLaye
|
|||||||
1,
|
1,
|
||||||
"rgb(178,24,43)",
|
"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
|
// Opacity 0.7 to be visible but not blocking
|
||||||
"heatmap-opacity": 0.7,
|
"heatmap-opacity": 0.7,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -45,14 +45,18 @@ export const NodesLayer = ({
|
|||||||
|
|
||||||
const errors = useNodeErrors();
|
const errors = useNodeErrors();
|
||||||
const errorSet = useMemo(() => new Set(errors.map((e) => e.node)), [errors]);
|
const errorSet = useMemo(() => new Set(errors.map((e) => e.node)), [errors]);
|
||||||
const hasNodeError = useCallback((num: number) => errorSet.has(num), [errorSet]);
|
const hasNodeError = useCallback(
|
||||||
|
(num: number) => errorSet.has(num),
|
||||||
|
[errorSet],
|
||||||
|
);
|
||||||
const { focusLngLat } = useMapFitting(mapRef);
|
const { focusLngLat } = useMapFitting(mapRef);
|
||||||
|
|
||||||
const selectedNode = useMemo(
|
const selectedNode = useMemo(
|
||||||
() =>
|
() =>
|
||||||
popupState?.type !== "node"
|
popupState?.type !== "node"
|
||||||
? undefined
|
? undefined
|
||||||
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
|
: (filteredNodes.find((node) => node.num === popupState.num) ??
|
||||||
|
undefined),
|
||||||
[popupState, filteredNodes],
|
[popupState, filteredNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -79,7 +83,9 @@ export const NodesLayer = ({
|
|||||||
const isExpanded = expandedCluster === key;
|
const isExpanded = expandedCluster === key;
|
||||||
|
|
||||||
// Precompute pixel offsets for expanded state
|
// 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
|
// Always render all node markers in the cluster
|
||||||
for (const [i, node] of nodes.entries()) {
|
for (const [i, node] of nodes.entries()) {
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export function generatePrecisionCircles(
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
for (const node of filteredNodes) {
|
for (const node of filteredNodes) {
|
||||||
if (node.position?.precisionBits === undefined || node.position.precisionBits === 0) {
|
if (
|
||||||
|
node.position?.precisionBits === undefined ||
|
||||||
|
node.position.precisionBits === 0
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const [lng, lat] = toLngLat(node.position);
|
const [lng, lat] = toLngLat(node.position);
|
||||||
@@ -60,7 +63,9 @@ 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(
|
const features: Feature<Polygon, CircleProps>[] = items.map(
|
||||||
({ lng, lat, radiusM, r, g, b, a }) => {
|
({ lng, lat, radiusM, r, g, b, a }) => {
|
||||||
@@ -91,7 +96,13 @@ export const SourcePrecisionCircles = ({
|
|||||||
type="fill"
|
type="fill"
|
||||||
layout={{ visibility: isVisible ? "visible" : "none" }}
|
layout={{ visibility: isVisible ? "visible" : "none" }}
|
||||||
paint={{
|
paint={{
|
||||||
"fill-color": ["rgba", ["get", "r"], ["get", "g"], ["get", "b"], ["get", "a"]],
|
"fill-color": [
|
||||||
|
"rgba",
|
||||||
|
["get", "r"],
|
||||||
|
["get", "g"],
|
||||||
|
["get", "b"],
|
||||||
|
["get", "a"],
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Layer
|
<Layer
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ type Pair = {
|
|||||||
ba?: number; // SNR b->a
|
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) {
|
if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) {
|
||||||
// Straight line
|
// Straight line
|
||||||
return [aLngLat, bLngLat];
|
return [aLngLat, bLngLat];
|
||||||
@@ -176,7 +180,9 @@ function pushIfFeature(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollection {
|
function generateNeighborLines(
|
||||||
|
neighborInfos: NeighborInfos[],
|
||||||
|
): FeatureCollection {
|
||||||
// Collect positions for all referenced nodes, discard pairs with missing positions
|
// Collect positions for all referenced nodes, discard pairs with missing positions
|
||||||
const idToLngLat = new Map<number, LngLat>();
|
const idToLngLat = new Map<number, LngLat>();
|
||||||
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
|
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
|
||||||
@@ -240,7 +246,12 @@ function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollectio
|
|||||||
return { type: "FeatureCollection", features };
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
@@ -290,7 +301,9 @@ export const SNRLayer = ({
|
|||||||
neighborInfo: {
|
neighborInfo: {
|
||||||
...neighborInfo,
|
...neighborInfo,
|
||||||
neighbors: neighborInfo.neighbors.map((n) => {
|
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 };
|
return { ...n, num: node?.num, position: node?.position };
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -312,7 +325,10 @@ export const SNRLayer = ({
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const featureCollection = generateNeighborLines([...remotePairs, ...directPairs]);
|
const featureCollection = generateNeighborLines([
|
||||||
|
...remotePairs,
|
||||||
|
...directPairs,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Source type="geojson" data={featureCollection}>
|
<Source type="geojson" data={featureCollection}>
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ export const WaypointLayer = ({
|
|||||||
offset={[0, 25]}
|
offset={[0, 25]}
|
||||||
onClose={() => setPopupState(undefined)}
|
onClose={() => setPopupState(undefined)}
|
||||||
>
|
>
|
||||||
<WaypointDetail waypoint={popupState.waypoint} myNode={myNode} onEdit={() => {}} />
|
<WaypointDetail
|
||||||
|
waypoint={popupState.waypoint}
|
||||||
|
myNode={myNode}
|
||||||
|
onEdit={() => {}}
|
||||||
|
/>
|
||||||
</PopupWrapper>,
|
</PopupWrapper>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ type StackBadgeProps = {
|
|||||||
onClick: (e: { originalEvent: MouseEvent }) => void;
|
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 (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
longitude={lng}
|
longitude={lng}
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@radix-ui/react-tooltip";
|
} from "@radix-ui/react-tooltip";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface NodeDetailProps {
|
export interface NodeDetailProps {
|
||||||
@@ -38,9 +45,16 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
: rawHardwareType.replaceAll("_", " ")
|
: rawHardwareType.replaceAll("_", " ")
|
||||||
: `${hwModel}`;
|
: `${hwModel}`;
|
||||||
// SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%).
|
// SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%).
|
||||||
const snrQuality = Math.min(Math.max(Math.round((node.snr + 10) * 5), 0), 100);
|
const snrQuality = Math.min(
|
||||||
|
Math.max(Math.round((node.snr + 10) * 5), 0),
|
||||||
|
100,
|
||||||
|
);
|
||||||
const snrTone =
|
const snrTone =
|
||||||
snrQuality >= 67 ? "text-green-600" : snrQuality >= 34 ? "text-yellow-600" : "text-red-600";
|
snrQuality >= 67
|
||||||
|
? "text-green-600"
|
||||||
|
: snrQuality >= 34
|
||||||
|
? "text-yellow-600"
|
||||||
|
: "text-red-600";
|
||||||
function handleDirectMessage() {
|
function handleDirectMessage() {
|
||||||
navigate({ to: `/messages/direct/${node.num}` });
|
navigate({ to: `/messages/direct/${node.num}` });
|
||||||
}
|
}
|
||||||
@@ -101,7 +115,9 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
fill={node.isFavorite ? "black" : "none"}
|
fill={node.isFavorite ? "black" : "none"}
|
||||||
size={15}
|
size={15}
|
||||||
aria-label={
|
aria-label={
|
||||||
node.isFavorite ? t("nodeDetail.favorite.label") : t("nodeDetail.notFavorite.label")
|
node.isFavorite
|
||||||
|
? t("nodeDetail.favorite.label")
|
||||||
|
: t("nodeDetail.notFavorite.label")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,12 +138,15 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex gap-1"
|
className="flex gap-1"
|
||||||
title={new Date(node.lastHeard * 1000).toLocaleString(navigator.language)}
|
title={new Date(node.lastHeard * 1000).toLocaleString(
|
||||||
|
navigator.language,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{node.lastHeard > 0 && (
|
{node.lastHeard > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{t("nodeDetail.status.heard")} <TimeAgo timestamp={node.lastHeard * 1000} />
|
{t("nodeDetail.status.heard")}{" "}
|
||||||
|
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -145,9 +164,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
<div className="flex mt-2 text-sm">
|
<div className="flex mt-2 text-sm">
|
||||||
<div className="flex items-center grow">
|
<div className="flex items-center grow">
|
||||||
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
|
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
|
||||||
{Number.isNaN(node.hopsAway) ? t("unit.hopsAway.unknown") : node.hopsAway}
|
{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>
|
||||||
<div>{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{node.position?.altitude && (
|
{node.position?.altitude && (
|
||||||
<div className="flex items-center grow">
|
<div className="flex items-center grow">
|
||||||
@@ -170,13 +193,17 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
{!!node.deviceMetrics?.channelUtilization && (
|
{!!node.deviceMetrics?.channelUtilization && (
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div>{t("channelUtilization.short")}</div>
|
<div>{t("channelUtilization.short")}</div>
|
||||||
<Mono>{node.deviceMetrics?.channelUtilization.toPrecision(3)}%</Mono>
|
<Mono>
|
||||||
|
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
|
||||||
|
</Mono>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!node.deviceMetrics?.airUtilTx && (
|
{!!node.deviceMetrics?.airUtilTx && (
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div>{t("airtimeUtilization.short")}</div>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { TimeAgo } from "@components/generic/TimeAgo";
|
|||||||
import { Separator } from "@components/UI/Separator.tsx";
|
import { Separator } from "@components/UI/Separator.tsx";
|
||||||
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
||||||
import type { WaypointWithMetadata } from "@core/stores";
|
import type { WaypointWithMetadata } 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/sdk";
|
import type { Protobuf } from "@meshtastic/sdk";
|
||||||
import {
|
import {
|
||||||
ClockFadingIcon,
|
ClockFadingIcon,
|
||||||
@@ -83,10 +88,14 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
|
|||||||
<div className="flex flex-wrap items-start gap-x-3">
|
<div className="flex flex-wrap items-start gap-x-3">
|
||||||
<dt className="inline-flex items-center gap-2 text-slate-500 min-w-0">
|
<dt className="inline-flex items-center gap-2 text-slate-500 min-w-0">
|
||||||
<ClockPlusIcon size={14} aria-hidden />
|
<ClockPlusIcon size={14} aria-hidden />
|
||||||
<span className="truncate">{t("waypointDetail.createdDate")}</span>
|
<span className="truncate">
|
||||||
|
{t("waypointDetail.createdDate")}
|
||||||
|
</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="ms-auto text-right">
|
<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} />
|
<TimeAgo timestamp={waypoint.metadata.created} />
|
||||||
</time>
|
</time>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -100,7 +109,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
|
|||||||
<span className="truncate">{t("waypointDetail.updated")}</span>
|
<span className="truncate">{t("waypointDetail.updated")}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="ms-auto text-right">
|
<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} />
|
<TimeAgo timestamp={waypoint.metadata.updated} />
|
||||||
</time>
|
</time>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -132,7 +143,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
|
|||||||
<dd className="ms-auto text-right">
|
<dd className="ms-auto text-right">
|
||||||
<data value={Math.round(distance)}>
|
<data value={Math.round(distance)}>
|
||||||
{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>
|
</data>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx";
|
import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx";
|
||||||
import { Checkbox } from "@components/UI/Checkbox/index.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 { cn } from "@core/utils/cn.ts";
|
||||||
import { LayersIcon } from "lucide-react";
|
import { LayersIcon } from "lucide-react";
|
||||||
import { type ReactNode, useMemo } from "react";
|
import { type ReactNode, useMemo } from "react";
|
||||||
@@ -40,7 +44,12 @@ interface CheckboxProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxItem = ({ label, checked, onChange, className }: CheckboxProps) => {
|
const CheckboxItem = ({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: CheckboxProps) => {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -140,7 +149,9 @@ export function MapLayerTool({
|
|||||||
<CheckboxItem
|
<CheckboxItem
|
||||||
label={label}
|
label={label}
|
||||||
checked={visibilityState[key as keyof VisibilityState]}
|
checked={visibilityState[key as keyof VisibilityState]}
|
||||||
onChange={() => handleCheckboxChange(key as keyof VisibilityState)}
|
onChange={() =>
|
||||||
|
handleCheckboxChange(key as keyof VisibilityState)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{key === "heatmap" && visibilityState.heatmap && (
|
{key === "heatmap" && visibilityState.heatmap && (
|
||||||
<div className="pl-6 pt-2 flex flex-col gap-1">
|
<div className="pl-6 pt-2 flex flex-col gap-1">
|
||||||
@@ -151,7 +162,9 @@ export function MapLayerTool({
|
|||||||
checked={heatmapMode === "density"}
|
checked={heatmapMode === "density"}
|
||||||
onChange={() => setHeatmapMode("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>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ const DateDelimiter = ({ label }: { label: string }) => (
|
|||||||
<li aria-label={label}>
|
<li aria-label={label}>
|
||||||
<div className="my-2 flex h-3 items-center justify-center">
|
<div className="my-2 flex h-3 items-center justify-center">
|
||||||
<Separator className="bg-slate-100 dark:bg-slate-800" />
|
<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" />
|
<Separator className="bg-slate-100 dark:bg-slate-800" />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -106,7 +108,9 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
|
|||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
|
|
||||||
const locale = useMemo(
|
const locale = useMemo(
|
||||||
() => i18n.language || (typeof navigator !== "undefined" ? navigator.language : "en-US"),
|
() =>
|
||||||
|
i18n.language ||
|
||||||
|
(typeof navigator !== "undefined" ? navigator.language : "en-US"),
|
||||||
[i18n.language],
|
[i18n.language],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ interface MessageActionsMenuProps {
|
|||||||
onReply?: () => void;
|
onReply?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageActionsMenu = ({ onAddReaction, onReply }: MessageActionsMenuProps) => {
|
export const MessageActionsMenu = ({
|
||||||
|
onAddReaction,
|
||||||
|
onReply,
|
||||||
|
}: MessageActionsMenuProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const hoverIconBarClass = cn(
|
const hoverIconBarClass = cn(
|
||||||
"absolute top-2 right-2",
|
"absolute top-2 right-2",
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import type { ConversationKey } from "@meshtastic/sdk";
|
import type { ConversationKey } from "@meshtastic/sdk";
|
||||||
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 { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { MessageInput, type MessageInputProps } from "./MessageInput.tsx";
|
import { MessageInput, type MessageInputProps } from "./MessageInput.tsx";
|
||||||
|
|
||||||
vi.mock("@components/UI/Button.tsx", () => ({
|
vi.mock("@components/UI/Button.tsx", () => ({
|
||||||
Button: vi.fn(({ type, className, children, onClick, onSubmit, ...rest }) => (
|
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}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)),
|
)),
|
||||||
@@ -127,7 +139,9 @@ describe("MessageInput", () => {
|
|||||||
expect(mockOnSend).toHaveBeenCalledTimes(1);
|
expect(mockOnSend).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOnSend).toHaveBeenCalledWith(testMessage);
|
expect(mockOnSend).toHaveBeenCalledWith(testMessage);
|
||||||
expect((inputElement as HTMLInputElement).value).toBe("");
|
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(mockClear).toHaveBeenCalledTimes(1);
|
expect(mockClear).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -193,11 +207,13 @@ describe("MessageInput", () => {
|
|||||||
mockDraftText = "Broadcast draft";
|
mockDraftText = "Broadcast draft";
|
||||||
renderComponent({ conversation: broadcastConv });
|
renderComponent({ conversation: broadcastConv });
|
||||||
|
|
||||||
expect((screen.getByTestId("message-input-field") as HTMLInputElement).value).toBe(
|
expect(
|
||||||
"Broadcast draft",
|
(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 formElement = screen.getByRole("form");
|
||||||
const newMessage = "New broadcast msg";
|
const newMessage = "New broadcast msg";
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,20 @@ export interface MessageInputProps {
|
|||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageInput = ({ onSend, conversation, maxBytes }: MessageInputProps) => {
|
export const MessageInput = ({
|
||||||
|
onSend,
|
||||||
|
conversation,
|
||||||
|
maxBytes,
|
||||||
|
}: MessageInputProps) => {
|
||||||
const { text: persistedDraft, setText, clear } = useDraft(conversation);
|
const { text: persistedDraft, setText, clear } = useDraft(conversation);
|
||||||
const { t } = useTranslation("messages");
|
const { t } = useTranslation("messages");
|
||||||
|
|
||||||
const calculateBytes = (value: string) => new Blob([value]).size;
|
const calculateBytes = (value: string) => new Blob([value]).size;
|
||||||
|
|
||||||
const [localDraft, setLocalDraft] = useState(persistedDraft);
|
const [localDraft, setLocalDraft] = useState(persistedDraft);
|
||||||
const [messageBytes, setMessageBytes] = useState(() => calculateBytes(persistedDraft));
|
const [messageBytes, setMessageBytes] = useState(() =>
|
||||||
|
calculateBytes(persistedDraft),
|
||||||
|
);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@components/UI/Tooltip.tsx";
|
} from "@components/UI/Tooltip.tsx";
|
||||||
import { useMyNodeAsProto, useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import {
|
||||||
|
useMyNodeAsProto,
|
||||||
|
useNodeAsProto,
|
||||||
|
} from "@core/hooks/useNodesAsProto.ts";
|
||||||
import { useAppStore, useDevice } from "@core/stores";
|
import { useAppStore, useDevice } from "@core/stores";
|
||||||
import { type Message, MessageState } from "@core/stores/messageStore";
|
import { type Message, MessageState } from "@core/stores/messageStore";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
@@ -151,7 +154,8 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
|||||||
const longName = messageUser?.user?.longName;
|
const longName = messageUser?.user?.longName;
|
||||||
const derivedShortName = messageUser?.user?.shortName || fallbackName;
|
const derivedShortName = messageUser?.user?.shortName || fallbackName;
|
||||||
const derivedDisplayName = longName || derivedShortName;
|
const derivedDisplayName = longName || derivedShortName;
|
||||||
const isFavorite = messageUser?.num !== myNodeNum && messageUser?.isFavorite;
|
const isFavorite =
|
||||||
|
messageUser?.num !== myNodeNum && messageUser?.isFavorite;
|
||||||
return {
|
return {
|
||||||
displayName: derivedDisplayName,
|
displayName: derivedDisplayName,
|
||||||
shortName: derivedShortName,
|
shortName: derivedShortName,
|
||||||
@@ -163,7 +167,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
|||||||
const messageStatusInfo = getMessageStatusInfo(message.state);
|
const messageStatusInfo = getMessageStatusInfo(message.state);
|
||||||
const StatusIconComponent = messageStatusInfo.icon;
|
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 locale = i18n.language;
|
||||||
|
|
||||||
const formattedTime = useMemo(
|
const formattedTime = useMemo(
|
||||||
@@ -200,7 +207,12 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
|||||||
return (
|
return (
|
||||||
<li className={messageItemWrapperClass}>
|
<li className={messageItemWrapperClass}>
|
||||||
<div className="grid grid-cols-[auto_1fr] gap-x-2">
|
<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 flex-col gap-0.5 min-w-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -208,7 +220,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
|||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
{messageDate && (
|
{messageDate && (
|
||||||
<time dateTime={messageDate.toISOString()} className={dateTextStyle}>
|
<time
|
||||||
|
dateTime={messageDate.toISOString()}
|
||||||
|
className={dateTextStyle}
|
||||||
|
>
|
||||||
<span aria-hidden="true">{formattedTime}</span>
|
<span aria-hidden="true">{formattedTime}</span>
|
||||||
<span className="sr-only">{fullDateTime}</span>
|
<span className="sr-only">{fullDateTime}</span>
|
||||||
</time>
|
</time>
|
||||||
@@ -217,7 +232,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
|||||||
<StatusTooltip statusInfo={messageStatusInfo}>
|
<StatusTooltip statusInfo={messageStatusInfo}>
|
||||||
<span aria-label={messageStatusInfo.ariaLabel} role="img">
|
<span aria-label={messageStatusInfo.ariaLabel} role="img">
|
||||||
<StatusIconComponent
|
<StatusIconComponent
|
||||||
className={cn("size-4 shrink-0", messageStatusInfo.iconClassName)}
|
className={cn(
|
||||||
|
"size-4 shrink-0",
|
||||||
|
messageStatusInfo.iconClassName,
|
||||||
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -68,7 +68,14 @@ describe("TraceRoute", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders the route to destination with SNR values", () => {
|
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("Source Node")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Destination Node")).toBeInTheDocument();
|
expect(screen.getByText("Destination Node")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type AmbientLightingValidation,
|
type AmbientLightingValidation,
|
||||||
AmbientLightingValidationSchema,
|
AmbientLightingValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/ambientLighting.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,13 +17,19 @@ interface AmbientLightingModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig },
|
value: {} as {
|
||||||
|
ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig;
|
||||||
|
},
|
||||||
peek: () =>
|
peek: () =>
|
||||||
({}) as { ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig },
|
({}) as {
|
||||||
|
ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const AmbientLighting = ({ onFormInit }: AmbientLightingModuleConfigProps) => {
|
export const AmbientLighting = ({
|
||||||
|
onFormInit,
|
||||||
|
}: AmbientLightingModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
|
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
|
||||||
|
|
||||||
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||||
import { type AudioValidation, AudioValidationSchema } from "@app/validation/moduleConfig/audio.ts";
|
import {
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
type AudioValidation,
|
||||||
|
AudioValidationSchema,
|
||||||
|
} from "@app/validation/moduleConfig/audio.ts";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -12,7 +18,8 @@ interface AudioModuleConfigProps {
|
|||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
|
value: {} as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
|
||||||
peek: () => ({}) as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
|
peek: () =>
|
||||||
|
({}) as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -68,7 +75,8 @@ export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
|
|||||||
label: t("audio.bitrate.label"),
|
label: t("audio.bitrate.label"),
|
||||||
description: t("audio.bitrate.description"),
|
description: t("audio.bitrate.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type CannedMessageValidation,
|
type CannedMessageValidation,
|
||||||
CannedMessageValidationSchema,
|
CannedMessageValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/cannedMessage.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,12 +17,19 @@ interface CannedMessageModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig },
|
cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as {
|
||||||
|
cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) => {
|
export const CannedMessage = ({
|
||||||
|
onFormInit,
|
||||||
|
}: CannedMessageModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
|
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
|
||||||
|
|
||||||
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
||||||
@@ -89,7 +99,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
|
|||||||
label: t("cannedMessage.inputbrokerEventPress.label"),
|
label: t("cannedMessage.inputbrokerEventPress.label"),
|
||||||
description: t("cannedMessage.inputbrokerEventPress.description"),
|
description: t("cannedMessage.inputbrokerEventPress.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig
|
||||||
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,7 +110,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
|
|||||||
label: t("cannedMessage.inputbrokerEventCw.label"),
|
label: t("cannedMessage.inputbrokerEventCw.label"),
|
||||||
description: t("cannedMessage.inputbrokerEventCw.description"),
|
description: t("cannedMessage.inputbrokerEventCw.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig
|
||||||
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -107,7 +121,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
|
|||||||
label: t("cannedMessage.inputbrokerEventCcw.label"),
|
label: t("cannedMessage.inputbrokerEventCcw.label"),
|
||||||
description: t("cannedMessage.inputbrokerEventCcw.description"),
|
description: t("cannedMessage.inputbrokerEventCcw.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig
|
||||||
|
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type DetectionSensorValidation,
|
type DetectionSensorValidation,
|
||||||
DetectionSensorValidationSchema,
|
DetectionSensorValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/detectionSensor.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,13 +17,19 @@ interface DetectionSensorModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig },
|
value: {} as {
|
||||||
|
detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig;
|
||||||
|
},
|
||||||
peek: () =>
|
peek: () =>
|
||||||
({}) as { detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig },
|
({}) as {
|
||||||
|
detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps) => {
|
export const DetectionSensor = ({
|
||||||
|
onFormInit,
|
||||||
|
}: DetectionSensorModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
|
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
|
||||||
|
|
||||||
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
||||||
@@ -64,7 +73,9 @@ export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "minimumBroadcastSecs",
|
name: "minimumBroadcastSecs",
|
||||||
label: t("detectionSensor.minimumBroadcastSecs.label"),
|
label: t("detectionSensor.minimumBroadcastSecs.label"),
|
||||||
description: t("detectionSensor.minimumBroadcastSecs.description"),
|
description: t(
|
||||||
|
"detectionSensor.minimumBroadcastSecs.description",
|
||||||
|
),
|
||||||
properties: { suffix: t("unit.second.plural") },
|
properties: { suffix: t("unit.second.plural") },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,9 +107,13 @@ export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps
|
|||||||
type: "select",
|
type: "select",
|
||||||
name: "detectionTriggerType",
|
name: "detectionTriggerType",
|
||||||
label: t("detectionSensor.detectionTriggerType.label"),
|
label: t("detectionSensor.detectionTriggerType.label"),
|
||||||
description: t("detectionSensor.detectionTriggerType.description"),
|
description: t(
|
||||||
|
"detectionSensor.detectionTriggerType.description",
|
||||||
|
),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig_TriggerType,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig
|
||||||
|
.ModuleConfig_DetectionSensorConfig_TriggerType,
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type ExternalNotificationValidation,
|
type ExternalNotificationValidation,
|
||||||
ExternalNotificationValidationSchema,
|
ExternalNotificationValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/externalNotification.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -24,7 +27,9 @@ const EMPTY_MODULES_SIGNAL = {
|
|||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleConfigProps) => {
|
export const ExternalNotification = ({
|
||||||
|
onFormInit,
|
||||||
|
}: ExternalNotificationModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "externalNotification" });
|
useWaitForConfig({ moduleConfigCase: "externalNotification" });
|
||||||
|
|
||||||
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
|
||||||
@@ -56,7 +61,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
|
|||||||
fieldGroups={[
|
fieldGroups={[
|
||||||
{
|
{
|
||||||
label: t("externalNotification.externalNotificationConfig.label"),
|
label: t("externalNotification.externalNotificationConfig.label"),
|
||||||
description: t("externalNotification.externalNotificationConfig.description"),
|
description: t(
|
||||||
|
"externalNotification.externalNotificationConfig.description",
|
||||||
|
),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -68,7 +75,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("externalNotification.notificationsOnMessage.label"),
|
label: t("externalNotification.notificationsOnMessage.label"),
|
||||||
description: t("externalNotification.notificationsOnMessage.description"),
|
description: t(
|
||||||
|
"externalNotification.notificationsOnMessage.description",
|
||||||
|
),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -80,19 +89,25 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "alertMessageBuzzer",
|
name: "alertMessageBuzzer",
|
||||||
label: t("externalNotification.alertMessageBuzzer.label"),
|
label: t("externalNotification.alertMessageBuzzer.label"),
|
||||||
description: t("externalNotification.alertMessageBuzzer.description"),
|
description: t(
|
||||||
|
"externalNotification.alertMessageBuzzer.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "alertMessageVibra",
|
name: "alertMessageVibra",
|
||||||
label: t("externalNotification.alertMessageVibra.label"),
|
label: t("externalNotification.alertMessageVibra.label"),
|
||||||
description: t("externalNotification.alertMessageVibra.description"),
|
description: t(
|
||||||
|
"externalNotification.alertMessageVibra.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("externalNotification.notificationsOnAlert.label"),
|
label: t("externalNotification.notificationsOnAlert.label"),
|
||||||
description: t("externalNotification.notificationsOnAlert.description"),
|
description: t(
|
||||||
|
"externalNotification.notificationsOnAlert.description",
|
||||||
|
),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -104,7 +119,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "alertBellBuzzer",
|
name: "alertBellBuzzer",
|
||||||
label: t("externalNotification.alertBellBuzzer.label"),
|
label: t("externalNotification.alertBellBuzzer.label"),
|
||||||
description: t("externalNotification.alertBellBuzzer.description"),
|
description: t(
|
||||||
|
"externalNotification.alertBellBuzzer.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
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 { 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -17,7 +23,9 @@ const EMPTY_MODULES_SIGNAL = {
|
|||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const populateDefaults = (cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined) =>
|
const populateDefaults = (
|
||||||
|
cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined,
|
||||||
|
) =>
|
||||||
cfg
|
cfg
|
||||||
? {
|
? {
|
||||||
...cfg,
|
...cfg,
|
||||||
@@ -38,7 +46,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
|
|
||||||
const effectiveMqtt =
|
const effectiveMqtt =
|
||||||
modules.mqtt ??
|
modules.mqtt ??
|
||||||
(getEffectiveModuleConfig("mqtt") as Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined);
|
(getEffectiveModuleConfig("mqtt") as
|
||||||
|
| Protobuf.ModuleConfig.ModuleConfig_MQTTConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
@@ -60,20 +70,37 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
const positionPrecisionOptions =
|
const positionPrecisionOptions =
|
||||||
config.display?.units === 0
|
config.display?.units === 0
|
||||||
? {
|
? {
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")]: 10,
|
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")]:
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")]: 11,
|
10,
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")]: 12,
|
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")]:
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km2_9")]: 13,
|
11,
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km1_5")]: 14,
|
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")]:
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m700")]: 15,
|
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.imperial_mi15")]: 10,
|
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi15")]:
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3")]: 11,
|
10,
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6")]: 12,
|
[t(
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8")]: 13,
|
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3",
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9")]: 14,
|
)]: 11,
|
||||||
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5")]: 15,
|
[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,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,14 +194,21 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "mapReportSettings.shouldReportLocation",
|
name: "mapReportSettings.shouldReportLocation",
|
||||||
label: t("mqtt.mapReportSettings.shouldReportLocation.label"),
|
label: t("mqtt.mapReportSettings.shouldReportLocation.label"),
|
||||||
description: t("mqtt.mapReportSettings.shouldReportLocation.description"),
|
description: t(
|
||||||
disabledBy: [{ fieldName: "enabled" }, { fieldName: "mapReportingEnabled" }],
|
"mqtt.mapReportSettings.shouldReportLocation.description",
|
||||||
|
),
|
||||||
|
disabledBy: [
|
||||||
|
{ fieldName: "enabled" },
|
||||||
|
{ fieldName: "mapReportingEnabled" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "select",
|
type: "select",
|
||||||
name: "mapReportSettings.positionPrecision",
|
name: "mapReportSettings.positionPrecision",
|
||||||
label: t("mqtt.mapReportSettings.positionPrecision.label"),
|
label: t("mqtt.mapReportSettings.positionPrecision.label"),
|
||||||
description: t("mqtt.mapReportSettings.positionPrecision.description"),
|
description: t(
|
||||||
|
"mqtt.mapReportSettings.positionPrecision.description",
|
||||||
|
),
|
||||||
properties: { enumValue: positionPrecisionOptions },
|
properties: { enumValue: positionPrecisionOptions },
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{ fieldName: "enabled" },
|
{ fieldName: "enabled" },
|
||||||
@@ -186,7 +220,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "mapReportSettings.publishIntervalSecs",
|
name: "mapReportSettings.publishIntervalSecs",
|
||||||
label: t("mqtt.mapReportSettings.publishIntervalSecs.label"),
|
label: t("mqtt.mapReportSettings.publishIntervalSecs.label"),
|
||||||
description: t("mqtt.mapReportSettings.publishIntervalSecs.description"),
|
description: t(
|
||||||
|
"mqtt.mapReportSettings.publishIntervalSecs.description",
|
||||||
|
),
|
||||||
properties: { suffix: t("unit.second.plural") },
|
properties: { suffix: t("unit.second.plural") },
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{ fieldName: "enabled" },
|
{ fieldName: "enabled" },
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type NeighborInfoValidation,
|
type NeighborInfoValidation,
|
||||||
NeighborInfoValidationSchema,
|
NeighborInfoValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/neighborInfo.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,8 +17,13 @@ interface NeighborInfoModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig },
|
neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as {
|
||||||
|
neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type PaxcounterValidation,
|
type PaxcounterValidation,
|
||||||
PaxcounterValidationSchema,
|
PaxcounterValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/paxcounter.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,8 +17,13 @@ interface PaxcounterModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig },
|
paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as {
|
||||||
|
paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type RangeTestValidation,
|
type RangeTestValidation,
|
||||||
RangeTestValidationSchema,
|
RangeTestValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/rangeTest.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,8 +17,11 @@ interface RangeTestModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
|
rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type RemoteHardwareValidation,
|
type RemoteHardwareValidation,
|
||||||
RemoteHardwareValidationSchema,
|
RemoteHardwareValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/remoteHardware.ts";
|
} from "@app/validation/moduleConfig/remoteHardware.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 { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -12,10 +15,13 @@ interface RemoteHardwareModuleConfigProps {
|
|||||||
onFormInit: DynamicFormFormInit<RemoteHardwareValidation>;
|
onFormInit: DynamicFormFormInit<RemoteHardwareValidation>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RemoteHardware = ({ onFormInit }: RemoteHardwareModuleConfigProps) => {
|
export const RemoteHardware = ({
|
||||||
|
onFormInit,
|
||||||
|
}: RemoteHardwareModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "remoteHardware" });
|
useWaitForConfig({ moduleConfigCase: "remoteHardware" });
|
||||||
|
|
||||||
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
|
useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
const onSubmit = (data: RemoteHardwareValidation) => {
|
const onSubmit = (data: RemoteHardwareValidation) => {
|
||||||
@@ -53,7 +59,9 @@ export const RemoteHardware = ({ onFormInit }: RemoteHardwareModuleConfigProps)
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "allowUndefinedPinAccess",
|
name: "allowUndefinedPinAccess",
|
||||||
label: t("remoteHardware.allowUndefinedPinAccess.label"),
|
label: t("remoteHardware.allowUndefinedPinAccess.label"),
|
||||||
description: t("remoteHardware.allowUndefinedPinAccess.description"),
|
description: t(
|
||||||
|
"remoteHardware.allowUndefinedPinAccess.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type SerialValidation,
|
type SerialValidation,
|
||||||
SerialValidationSchema,
|
SerialValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/serial.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -15,7 +18,8 @@ interface SerialModuleConfigProps {
|
|||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
|
value: {} as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
|
||||||
peek: () => ({}) as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
|
peek: () =>
|
||||||
|
({}) as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -83,7 +87,8 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
|
|||||||
label: t("serial.baud.label"),
|
label: t("serial.baud.label"),
|
||||||
description: t("serial.baud.description"),
|
description: t("serial.baud.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -99,7 +104,8 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
|
|||||||
label: t("serial.mode.label"),
|
label: t("serial.mode.label"),
|
||||||
description: t("serial.mode.description"),
|
description: t("serial.mode.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
|
enumValue:
|
||||||
|
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type StatusMessageValidation,
|
type StatusMessageValidation,
|
||||||
StatusMessageValidationSchema,
|
StatusMessageValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/statusMessage.ts";
|
} from "@app/validation/moduleConfig/statusMessage.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 { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -12,10 +15,13 @@ interface StatusMessageModuleConfigProps {
|
|||||||
onFormInit: DynamicFormFormInit<StatusMessageValidation>;
|
onFormInit: DynamicFormFormInit<StatusMessageValidation>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusMessage = ({ onFormInit }: StatusMessageModuleConfigProps) => {
|
export const StatusMessage = ({
|
||||||
|
onFormInit,
|
||||||
|
}: StatusMessageModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "statusmessage" });
|
useWaitForConfig({ moduleConfigCase: "statusmessage" });
|
||||||
|
|
||||||
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
|
useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
const onSubmit = (data: StatusMessageValidation) => {
|
const onSubmit = (data: StatusMessageValidation) => {
|
||||||
@@ -24,7 +30,11 @@ export const StatusMessage = ({ onFormInit }: StatusMessageModuleConfigProps) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setChange({ type: "moduleConfig", variant: "statusmessage" }, data, moduleConfig.statusmessage);
|
setChange(
|
||||||
|
{ type: "moduleConfig", variant: "statusmessage" },
|
||||||
|
data,
|
||||||
|
moduleConfig.statusmessage,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type StoreForwardValidation,
|
type StoreForwardValidation,
|
||||||
StoreForwardValidationSchema,
|
StoreForwardValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/storeForward.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,8 +17,13 @@ interface StoreForwardModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig },
|
storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as {
|
||||||
|
storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig;
|
||||||
|
},
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||||
import { type TakValidation, TakValidationSchema } from "@app/validation/moduleConfig/tak.ts";
|
import {
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
type TakValidation,
|
||||||
|
TakValidationSchema,
|
||||||
|
} from "@app/validation/moduleConfig/tak.ts";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
@@ -13,7 +19,8 @@ interface TakModuleConfigProps {
|
|||||||
export const Tak = ({ onFormInit }: TakModuleConfigProps) => {
|
export const Tak = ({ onFormInit }: TakModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "tak" });
|
useWaitForConfig({ moduleConfigCase: "tak" });
|
||||||
|
|
||||||
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
|
useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
const onSubmit = (data: TakValidation) => {
|
const onSubmit = (data: TakValidation) => {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type TelemetryValidation,
|
type TelemetryValidation,
|
||||||
TelemetryValidationSchema,
|
TelemetryValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/telemetry.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -14,8 +17,11 @@ interface TelemetryModuleConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODULES_SIGNAL = {
|
const EMPTY_MODULES_SIGNAL = {
|
||||||
value: {} as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
|
value: {} as {
|
||||||
peek: () => ({}) as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
|
telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig;
|
||||||
|
},
|
||||||
|
peek: () =>
|
||||||
|
({}) as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -56,7 +62,12 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
|||||||
// device_telemetry_enabled is only writable on firmware ≥ v2.7.12. Hide
|
// device_telemetry_enabled is only writable on firmware ≥ v2.7.12. Hide
|
||||||
// the toggle on older firmware so we don't push a value the device will
|
// the toggle on older firmware so we don't push a value the device will
|
||||||
// ignore.
|
// ignore.
|
||||||
const canToggleTelemetry = firmwareAtLeast(metadata.get(0)?.firmwareVersion, 2, 7, 12);
|
const canToggleTelemetry = firmwareAtLeast(
|
||||||
|
metadata.get(0)?.firmwareVersion,
|
||||||
|
2,
|
||||||
|
7,
|
||||||
|
12,
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
@@ -86,7 +97,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
|||||||
type: "toggle" as const,
|
type: "toggle" as const,
|
||||||
name: "deviceTelemetryEnabled" as const,
|
name: "deviceTelemetryEnabled" as const,
|
||||||
label: t("telemetry.deviceTelemetryEnabled.label"),
|
label: t("telemetry.deviceTelemetryEnabled.label"),
|
||||||
description: t("telemetry.deviceTelemetryEnabled.description"),
|
description: t(
|
||||||
|
"telemetry.deviceTelemetryEnabled.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -101,7 +114,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "environmentMeasurementEnabled",
|
name: "environmentMeasurementEnabled",
|
||||||
label: t("telemetry.environmentMeasurementEnabled.label"),
|
label: t("telemetry.environmentMeasurementEnabled.label"),
|
||||||
description: t("telemetry.environmentMeasurementEnabled.description"),
|
description: t(
|
||||||
|
"telemetry.environmentMeasurementEnabled.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
@@ -120,7 +135,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "environmentDisplayFahrenheit",
|
name: "environmentDisplayFahrenheit",
|
||||||
label: t("telemetry.environmentDisplayFahrenheit.label"),
|
label: t("telemetry.environmentDisplayFahrenheit.label"),
|
||||||
description: t("telemetry.environmentDisplayFahrenheit.description"),
|
description: t(
|
||||||
|
"telemetry.environmentDisplayFahrenheit.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type TrafficManagementValidation,
|
type TrafficManagementValidation,
|
||||||
TrafficManagementValidationSchema,
|
TrafficManagementValidationSchema,
|
||||||
} from "@app/validation/moduleConfig/trafficManagement.ts";
|
} from "@app/validation/moduleConfig/trafficManagement.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 { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -12,10 +15,13 @@ interface TrafficManagementModuleConfigProps {
|
|||||||
onFormInit: DynamicFormFormInit<TrafficManagementValidation>;
|
onFormInit: DynamicFormFormInit<TrafficManagementValidation>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigProps) => {
|
export const TrafficManagement = ({
|
||||||
|
onFormInit,
|
||||||
|
}: TrafficManagementModuleConfigProps) => {
|
||||||
useWaitForConfig({ moduleConfigCase: "trafficManagement" });
|
useWaitForConfig({ moduleConfigCase: "trafficManagement" });
|
||||||
|
|
||||||
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
|
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
|
||||||
|
useDevice();
|
||||||
const { t } = useTranslation("moduleConfig");
|
const { t } = useTranslation("moduleConfig");
|
||||||
|
|
||||||
const onSubmit = (data: TrafficManagementValidation) => {
|
const onSubmit = (data: TrafficManagementValidation) => {
|
||||||
@@ -53,19 +59,25 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "positionDedupEnabled",
|
name: "positionDedupEnabled",
|
||||||
label: t("trafficManagement.positionDedupEnabled.label"),
|
label: t("trafficManagement.positionDedupEnabled.label"),
|
||||||
description: t("trafficManagement.positionDedupEnabled.description"),
|
description: t(
|
||||||
|
"trafficManagement.positionDedupEnabled.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
name: "positionPrecisionBits",
|
name: "positionPrecisionBits",
|
||||||
label: t("trafficManagement.positionPrecisionBits.label"),
|
label: t("trafficManagement.positionPrecisionBits.label"),
|
||||||
description: t("trafficManagement.positionPrecisionBits.description"),
|
description: t(
|
||||||
|
"trafficManagement.positionPrecisionBits.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
name: "positionMinIntervalSecs",
|
name: "positionMinIntervalSecs",
|
||||||
label: t("trafficManagement.positionMinIntervalSecs.label"),
|
label: t("trafficManagement.positionMinIntervalSecs.label"),
|
||||||
description: t("trafficManagement.positionMinIntervalSecs.description"),
|
description: t(
|
||||||
|
"trafficManagement.positionMinIntervalSecs.description",
|
||||||
|
),
|
||||||
properties: {
|
properties: {
|
||||||
suffix: t("unit.second.plural"),
|
suffix: t("unit.second.plural"),
|
||||||
},
|
},
|
||||||
@@ -74,13 +86,17 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "nodeinfoDirectResponse",
|
name: "nodeinfoDirectResponse",
|
||||||
label: t("trafficManagement.nodeinfoDirectResponse.label"),
|
label: t("trafficManagement.nodeinfoDirectResponse.label"),
|
||||||
description: t("trafficManagement.nodeinfoDirectResponse.description"),
|
description: t(
|
||||||
|
"trafficManagement.nodeinfoDirectResponse.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
name: "nodeinfoDirectResponseMaxHops",
|
name: "nodeinfoDirectResponseMaxHops",
|
||||||
label: t("trafficManagement.nodeinfoDirectResponseMaxHops.label"),
|
label: t("trafficManagement.nodeinfoDirectResponseMaxHops.label"),
|
||||||
description: t("trafficManagement.nodeinfoDirectResponseMaxHops.description"),
|
description: t(
|
||||||
|
"trafficManagement.nodeinfoDirectResponseMaxHops.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -92,7 +108,9 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "rateLimitWindowSecs",
|
name: "rateLimitWindowSecs",
|
||||||
label: t("trafficManagement.rateLimitWindowSecs.label"),
|
label: t("trafficManagement.rateLimitWindowSecs.label"),
|
||||||
description: t("trafficManagement.rateLimitWindowSecs.description"),
|
description: t(
|
||||||
|
"trafficManagement.rateLimitWindowSecs.description",
|
||||||
|
),
|
||||||
properties: {
|
properties: {
|
||||||
suffix: t("unit.second.plural"),
|
suffix: t("unit.second.plural"),
|
||||||
},
|
},
|
||||||
@@ -101,37 +119,49 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "rateLimitMaxPackets",
|
name: "rateLimitMaxPackets",
|
||||||
label: t("trafficManagement.rateLimitMaxPackets.label"),
|
label: t("trafficManagement.rateLimitMaxPackets.label"),
|
||||||
description: t("trafficManagement.rateLimitMaxPackets.description"),
|
description: t(
|
||||||
|
"trafficManagement.rateLimitMaxPackets.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "dropUnknownEnabled",
|
name: "dropUnknownEnabled",
|
||||||
label: t("trafficManagement.dropUnknownEnabled.label"),
|
label: t("trafficManagement.dropUnknownEnabled.label"),
|
||||||
description: t("trafficManagement.dropUnknownEnabled.description"),
|
description: t(
|
||||||
|
"trafficManagement.dropUnknownEnabled.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
name: "unknownPacketThreshold",
|
name: "unknownPacketThreshold",
|
||||||
label: t("trafficManagement.unknownPacketThreshold.label"),
|
label: t("trafficManagement.unknownPacketThreshold.label"),
|
||||||
description: t("trafficManagement.unknownPacketThreshold.description"),
|
description: t(
|
||||||
|
"trafficManagement.unknownPacketThreshold.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "exhaustHopTelemetry",
|
name: "exhaustHopTelemetry",
|
||||||
label: t("trafficManagement.exhaustHopTelemetry.label"),
|
label: t("trafficManagement.exhaustHopTelemetry.label"),
|
||||||
description: t("trafficManagement.exhaustHopTelemetry.description"),
|
description: t(
|
||||||
|
"trafficManagement.exhaustHopTelemetry.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "exhaustHopPosition",
|
name: "exhaustHopPosition",
|
||||||
label: t("trafficManagement.exhaustHopPosition.label"),
|
label: t("trafficManagement.exhaustHopPosition.label"),
|
||||||
description: t("trafficManagement.exhaustHopPosition.description"),
|
description: t(
|
||||||
|
"trafficManagement.exhaustHopPosition.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "routerPreserveHops",
|
name: "routerPreserveHops",
|
||||||
label: t("trafficManagement.routerPreserveHops.label"),
|
label: t("trafficManagement.routerPreserveHops.label"),
|
||||||
description: t("trafficManagement.routerPreserveHops.description"),
|
description: t(
|
||||||
|
"trafficManagement.routerPreserveHops.description",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import {
|
|||||||
type BluetoothValidation,
|
type BluetoothValidation,
|
||||||
BluetoothValidationSchema,
|
BluetoothValidationSchema,
|
||||||
} from "@app/validation/config/bluetooth.ts";
|
} 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -27,13 +30,18 @@ export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.bluetooth ??
|
radio.bluetooth ??
|
||||||
(getEffectiveConfig("bluetooth") as Protobuf.Config.Config_BluetoothConfig | undefined);
|
(getEffectiveConfig("bluetooth") as
|
||||||
|
| Protobuf.Config.Config_BluetoothConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
const onSubmit = (data: BluetoothValidation) => {
|
const onSubmit = (data: BluetoothValidation) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.setRadioSection("bluetooth", data as unknown as Protobuf.Config.Config_BluetoothConfig);
|
editor.setRadioSection(
|
||||||
|
"bluetooth",
|
||||||
|
data as unknown as Protobuf.Config.Config_BluetoothConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
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 { 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 { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -25,14 +31,19 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.device ??
|
radio.device ??
|
||||||
(getEffectiveConfig("device") as Protobuf.Config.Config_DeviceConfig | undefined);
|
(getEffectiveConfig("device") as
|
||||||
|
| Protobuf.Config.Config_DeviceConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
const { validateRoleSelection } = useUnsafeRolesDialog();
|
const { validateRoleSelection } = useUnsafeRolesDialog();
|
||||||
|
|
||||||
const onSubmit = (data: DeviceValidation) => {
|
const onSubmit = (data: DeviceValidation) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.setRadioSection("device", data as unknown as Protobuf.Config.Config_DeviceConfig);
|
editor.setRadioSection(
|
||||||
|
"device",
|
||||||
|
data as unknown as Protobuf.Config.Config_DeviceConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||||
import { type DisplayValidation, DisplayValidationSchema } from "@app/validation/config/display.ts";
|
import {
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
type DisplayValidation,
|
||||||
|
DisplayValidationSchema,
|
||||||
|
} from "@app/validation/config/display.ts";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -23,13 +29,18 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.display ??
|
radio.display ??
|
||||||
(getEffectiveConfig("display") as Protobuf.Config.Config_DisplayConfig | undefined);
|
(getEffectiveConfig("display") as
|
||||||
|
| Protobuf.Config.Config_DisplayConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
const onSubmit = (data: DisplayValidation) => {
|
const onSubmit = (data: DisplayValidation) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.setRadioSection("display", data as unknown as Protobuf.Config.Config_DisplayConfig);
|
editor.setRadioSection(
|
||||||
|
"display",
|
||||||
|
data as unknown as Protobuf.Config.Config_DisplayConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -129,7 +140,8 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
|
|||||||
label: t("display.compassOrientation.label"),
|
label: t("display.compassOrientation.label"),
|
||||||
description: t("display.compassOrientation.description"),
|
description: t("display.compassOrientation.description"),
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.Config.Config_DisplayConfig_CompassOrientation,
|
enumValue:
|
||||||
|
Protobuf.Config.Config_DisplayConfig_CompassOrientation,
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||||
import { type LoRaValidation, LoRaValidationSchema } from "@app/validation/config/lora.ts";
|
import {
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
type LoRaValidation,
|
||||||
|
LoRaValidationSchema,
|
||||||
|
} from "@app/validation/config/lora.ts";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -24,13 +30,19 @@ export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
|
|
||||||
const effectiveLora =
|
const effectiveLora =
|
||||||
radio.lora ?? (getEffectiveConfig("lora") as Protobuf.Config.Config_LoRaConfig | undefined);
|
radio.lora ??
|
||||||
|
(getEffectiveConfig("lora") as
|
||||||
|
| Protobuf.Config.Config_LoRaConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
const onSubmit = (data: LoRaValidation) => {
|
const onSubmit = (data: LoRaValidation) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.setRadioSection("lora", data as unknown as Protobuf.Config.Config_LoRaConfig);
|
editor.setRadioSection(
|
||||||
|
"lora",
|
||||||
|
data as unknown as Protobuf.Config.Config_LoRaConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
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 { 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 { useDevice } from "@core/stores";
|
||||||
import { convertIntToIpAddress, convertIpAddressToInt } from "@core/utils/ip.ts";
|
import {
|
||||||
|
convertIntToIpAddress,
|
||||||
|
convertIpAddressToInt,
|
||||||
|
} from "@core/utils/ip.ts";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -18,7 +27,9 @@ const EMPTY_RADIO_SIGNAL = {
|
|||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const toFormShape = (cfg: Protobuf.Config.Config_NetworkConfig | undefined) => ({
|
const toFormShape = (
|
||||||
|
cfg: Protobuf.Config.Config_NetworkConfig | undefined,
|
||||||
|
) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
ipv4Config: {
|
ipv4Config: {
|
||||||
ip: convertIntToIpAddress(cfg?.ipv4Config?.ip ?? 0),
|
ip: convertIntToIpAddress(cfg?.ipv4Config?.ip ?? 0),
|
||||||
@@ -27,7 +38,8 @@ const toFormShape = (cfg: Protobuf.Config.Config_NetworkConfig | undefined) => (
|
|||||||
dns: convertIntToIpAddress(cfg?.ipv4Config?.dns ?? 0),
|
dns: convertIntToIpAddress(cfg?.ipv4Config?.dns ?? 0),
|
||||||
},
|
},
|
||||||
enabledProtocols:
|
enabledProtocols:
|
||||||
cfg?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
|
cfg?.enabledProtocols ??
|
||||||
|
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
||||||
@@ -38,7 +50,9 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.network ??
|
radio.network ??
|
||||||
(getEffectiveConfig("network") as Protobuf.Config.Config_NetworkConfig | undefined);
|
(getEffectiveConfig("network") as
|
||||||
|
| Protobuf.Config.Config_NetworkConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
@@ -46,14 +60,20 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const payload = {
|
const payload = {
|
||||||
...data,
|
...data,
|
||||||
ipv4Config: create(Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, {
|
ipv4Config: create(
|
||||||
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
|
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
|
||||||
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
|
{
|
||||||
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
|
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
|
||||||
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
|
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
|
||||||
}),
|
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
|
||||||
|
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
editor.setRadioSection("network", payload as unknown as Protobuf.Config.Config_NetworkConfig);
|
editor.setRadioSection(
|
||||||
|
"network",
|
||||||
|
payload as unknown as Protobuf.Config.Config_NetworkConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +164,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "addressMode",
|
fieldName: "addressMode",
|
||||||
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
selector:
|
||||||
|
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -156,7 +177,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "addressMode",
|
fieldName: "addressMode",
|
||||||
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
selector:
|
||||||
|
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -168,7 +190,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "addressMode",
|
fieldName: "addressMode",
|
||||||
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
selector:
|
||||||
|
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -180,7 +203,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
|||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "addressMode",
|
fieldName: "addressMode",
|
||||||
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
selector:
|
||||||
|
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import {
|
|||||||
PositionValidationSchema,
|
PositionValidationSchema,
|
||||||
} from "@app/validation/config/position.ts";
|
} from "@app/validation/config/position.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { type FlagName, usePositionFlags } from "@core/hooks/usePositionFlags.ts";
|
import {
|
||||||
|
type FlagName,
|
||||||
|
usePositionFlags,
|
||||||
|
} from "@core/hooks/usePositionFlags.ts";
|
||||||
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
@@ -41,16 +47,28 @@ function UseBrowserLocationButton() {
|
|||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) => {
|
(pos) => {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setValue("latitude", Number(pos.coords.latitude.toFixed(7)), { shouldDirty: true });
|
setValue("latitude", Number(pos.coords.latitude.toFixed(7)), {
|
||||||
setValue("longitude", Number(pos.coords.longitude.toFixed(7)), { shouldDirty: true });
|
shouldDirty: true,
|
||||||
if (pos.coords.altitude !== null && !Number.isNaN(pos.coords.altitude)) {
|
});
|
||||||
setValue("altitude", Math.round(pos.coords.altitude), { shouldDirty: true });
|
setValue("longitude", Number(pos.coords.longitude.toFixed(7)), {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
pos.coords.altitude !== null &&
|
||||||
|
!Number.isNaN(pos.coords.altitude)
|
||||||
|
) {
|
||||||
|
setValue("altitude", Math.round(pos.coords.altitude), {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
toast({
|
toast({
|
||||||
title: t("position.useBrowserLocation.failed", "Could not read browser location"),
|
title: t(
|
||||||
|
"position.useBrowserLocation.failed",
|
||||||
|
"Could not read browser location",
|
||||||
|
),
|
||||||
description: err.message,
|
description: err.message,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -84,7 +102,9 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
|
|||||||
|
|
||||||
const effectivePosition =
|
const effectivePosition =
|
||||||
radio.position ??
|
radio.position ??
|
||||||
(getEffectiveConfig("position") as Protobuf.Config.Config_PositionConfig | undefined);
|
(getEffectiveConfig("position") as
|
||||||
|
| Protobuf.Config.Config_PositionConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
|
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
|
||||||
effectivePosition?.positionFlags ?? 0,
|
effectivePosition?.positionFlags ?? 0,
|
||||||
@@ -98,14 +118,23 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
|
|||||||
return {
|
return {
|
||||||
...config.position,
|
...config.position,
|
||||||
...effectivePosition,
|
...effectivePosition,
|
||||||
latitude: currentPosition?.latitudeI ? currentPosition.latitudeI / 1e7 : undefined,
|
latitude: currentPosition?.latitudeI
|
||||||
longitude: currentPosition?.longitudeI ? currentPosition.longitudeI / 1e7 : undefined,
|
? currentPosition.latitudeI / 1e7
|
||||||
|
: undefined,
|
||||||
|
longitude: currentPosition?.longitudeI
|
||||||
|
? currentPosition.longitudeI / 1e7
|
||||||
|
: undefined,
|
||||||
altitude: currentPosition?.altitude ?? 0,
|
altitude: currentPosition?.altitude ?? 0,
|
||||||
} as PositionValidation;
|
} as PositionValidation;
|
||||||
}, [config.position, effectivePosition, currentPosition]);
|
}, [config.position, effectivePosition, currentPosition]);
|
||||||
|
|
||||||
const onSubmit = (data: PositionValidation) => {
|
const onSubmit = (data: PositionValidation) => {
|
||||||
const { latitude: _latitude, longitude: _longitude, altitude: _altitude, ...configData } = data;
|
const {
|
||||||
|
latitude: _latitude,
|
||||||
|
longitude: _longitude,
|
||||||
|
altitude: _altitude,
|
||||||
|
...configData
|
||||||
|
} = data;
|
||||||
const payload = { ...configData, positionFlags: flagsValue };
|
const payload = { ...configData, positionFlags: flagsValue };
|
||||||
|
|
||||||
if (editor) {
|
if (editor) {
|
||||||
@@ -115,7 +144,11 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.fixedPosition && data.latitude !== undefined && data.longitude !== undefined) {
|
if (
|
||||||
|
data.fixedPosition &&
|
||||||
|
data.latitude !== undefined &&
|
||||||
|
data.longitude !== undefined
|
||||||
|
) {
|
||||||
const message = create(Protobuf.Admin.AdminMessageSchema, {
|
const message = create(Protobuf.Admin.AdminMessageSchema, {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
case: "setFixedPosition",
|
case: "setFixedPosition",
|
||||||
@@ -225,14 +258,16 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
|
|||||||
label: t("position.fixedPosition.altitude.label"),
|
label: t("position.fixedPosition.altitude.label"),
|
||||||
description: t("position.fixedPosition.altitude.description", {
|
description: t("position.fixedPosition.altitude.description", {
|
||||||
unit:
|
unit:
|
||||||
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
|
displayUnits ===
|
||||||
|
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
|
||||||
? "Feet"
|
? "Feet"
|
||||||
: "Meters",
|
: "Meters",
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
step: 0.0000001,
|
step: 0.0000001,
|
||||||
suffix:
|
suffix:
|
||||||
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
|
displayUnits ===
|
||||||
|
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
|
||||||
? "Feet"
|
? "Feet"
|
||||||
: "Meters",
|
: "Meters",
|
||||||
},
|
},
|
||||||
@@ -266,7 +301,8 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
|
|||||||
type: "multiSelect",
|
type: "multiSelect",
|
||||||
name: "positionFlags",
|
name: "positionFlags",
|
||||||
value: activeFlags,
|
value: activeFlags,
|
||||||
isChecked: (name: string) => activeFlags?.includes(name as FlagName) ?? false,
|
isChecked: (name: string) =>
|
||||||
|
activeFlags?.includes(name as FlagName) ?? false,
|
||||||
onValueChange: onPositonFlagChange,
|
onValueChange: onPositonFlagChange,
|
||||||
label: t("position.positionFlags.label"),
|
label: t("position.positionFlags.label"),
|
||||||
placeholder: t("position.flags.placeholder"),
|
placeholder: t("position.flags.placeholder"),
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||||
import { type PowerValidation, PowerValidationSchema } from "@app/validation/config/power.ts";
|
import {
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
type PowerValidation,
|
||||||
|
PowerValidationSchema,
|
||||||
|
} from "@app/validation/config/power.ts";
|
||||||
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -23,13 +29,19 @@ export const Power = ({ onFormInit }: PowerConfigProps) => {
|
|||||||
const editor = useConfigEditor();
|
const editor = useConfigEditor();
|
||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.power ?? (getEffectiveConfig("power") as Protobuf.Config.Config_PowerConfig | undefined);
|
radio.power ??
|
||||||
|
(getEffectiveConfig("power") as
|
||||||
|
| Protobuf.Config.Config_PowerConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
const onSubmit = (data: PowerValidation) => {
|
const onSubmit = (data: PowerValidation) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.setRadioSection("power", data as unknown as Protobuf.Config.Config_PowerConfig);
|
editor.setRadioSection(
|
||||||
|
"power",
|
||||||
|
data as unknown as Protobuf.Config.Config_PowerConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
||||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
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 { useDevice } from "@core/stores";
|
||||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
@@ -27,7 +30,9 @@ const EMPTY_RADIO_SIGNAL = {
|
|||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const toFormShape = (cfg: Protobuf.Config.Config_SecurityConfig | undefined) => ({
|
const toFormShape = (
|
||||||
|
cfg: Protobuf.Config.Config_SecurityConfig | undefined,
|
||||||
|
) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
privateKey: fromByteArray(cfg?.privateKey ?? new Uint8Array(0)),
|
privateKey: fromByteArray(cfg?.privateKey ?? new Uint8Array(0)),
|
||||||
publicKey: fromByteArray(cfg?.publicKey ?? new Uint8Array(0)),
|
publicKey: fromByteArray(cfg?.publicKey ?? new Uint8Array(0)),
|
||||||
@@ -46,7 +51,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
|
||||||
const effective =
|
const effective =
|
||||||
radio.security ??
|
radio.security ??
|
||||||
(getEffectiveConfig("security") as Protobuf.Config.Config_SecurityConfig | undefined);
|
(getEffectiveConfig("security") as
|
||||||
|
| Protobuf.Config.Config_SecurityConfig
|
||||||
|
| undefined);
|
||||||
|
|
||||||
const { t } = useTranslation("config");
|
const { t } = useTranslation("config");
|
||||||
|
|
||||||
@@ -64,8 +71,10 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
onFormInit?.(formMethods);
|
onFormInit?.(formMethods);
|
||||||
}, [onFormInit, formMethods]);
|
}, [onFormInit, formMethods]);
|
||||||
|
|
||||||
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState<boolean>(false);
|
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] =
|
||||||
const [managedModeDialogOpen, setManagedModeDialogOpen] = useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [managedModeDialogOpen, setManagedModeDialogOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const onSubmit = (data: RawSecurity) => {
|
const onSubmit = (data: RawSecurity) => {
|
||||||
if (!formState.isReady || !editor) return;
|
if (!formState.isReady || !editor) return;
|
||||||
@@ -80,7 +89,10 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
toByteArray(data.adminKey.at(2) ?? ""),
|
toByteArray(data.adminKey.at(2) ?? ""),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
editor.setRadioSection("security", payload as unknown as Protobuf.Config.Config_SecurityConfig);
|
editor.setRadioSection(
|
||||||
|
"security",
|
||||||
|
payload as unknown as Protobuf.Config.Config_SecurityConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pkiRegenerate = () => {
|
const pkiRegenerate = () => {
|
||||||
@@ -90,7 +102,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
|
|
||||||
const updatePublicKey = async (privateKey: string) => {
|
const updatePublicKey = async (privateKey: string) => {
|
||||||
try {
|
try {
|
||||||
const publicKey = fromByteArray(getX25519PublicKey(toByteArray(privateKey)));
|
const publicKey = fromByteArray(
|
||||||
|
getX25519PublicKey(toByteArray(privateKey)),
|
||||||
|
);
|
||||||
setValue("privateKey", privateKey, { shouldDirty: true });
|
setValue("privateKey", privateKey, { shouldDirty: true });
|
||||||
setValue("publicKey", publicKey, { shouldDirty: true });
|
setValue("publicKey", publicKey, { shouldDirty: true });
|
||||||
|
|
||||||
@@ -176,7 +190,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
|
disabledBy: [
|
||||||
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
|
],
|
||||||
properties: { showCopyButton: true },
|
properties: { showCopyButton: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -188,7 +204,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
|
disabledBy: [
|
||||||
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
|
],
|
||||||
properties: { showCopyButton: true },
|
properties: { showCopyButton: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,7 +218,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
|
disabledBy: [
|
||||||
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
|
],
|
||||||
properties: { showCopyButton: true },
|
properties: { showCopyButton: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
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 { create } from "@bufbuild/protobuf";
|
||||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
import {
|
||||||
|
DynamicForm,
|
||||||
|
type DynamicFormFormInit,
|
||||||
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
||||||
import { Protobuf } from "@meshtastic/sdk";
|
import { Protobuf } from "@meshtastic/sdk";
|
||||||
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
|
||||||
@@ -55,7 +61,9 @@ export const User = ({ onFormInit }: UserConfigProps) => {
|
|||||||
editor.setOwner(merged);
|
editor.setOwner(merged);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hwName = Protobuf.Mesh.HardwareModel[baselineUser.hwModel] ?? String(baselineUser.hwModel);
|
const hwName =
|
||||||
|
Protobuf.Mesh.HardwareModel[baselineUser.hwModel] ??
|
||||||
|
String(baselineUser.hwModel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicForm<UserValidation>
|
<DynamicForm<UserValidation>
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ export const PageLayout = ({
|
|||||||
>
|
>
|
||||||
{/* Header Content */}
|
{/* Header Content */}
|
||||||
<div className="flex flex-1 items-center justify-between min-w-0">
|
<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">
|
<div className="flex items-center space-x-1 md:space-x-2 shrink-0 pr-6">
|
||||||
{actions?.map((action) => {
|
{actions?.map((action) => {
|
||||||
return (
|
return (
|
||||||
@@ -91,9 +93,15 @@ export const PageLayout = ({
|
|||||||
(action.isLoading ? (
|
(action.isLoading ? (
|
||||||
<Spinner size="md" />
|
<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>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
|
|||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import { useMyNodeAsProto, useNodesAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import {
|
||||||
|
useMyNodeAsProto,
|
||||||
|
useNodesAsProto,
|
||||||
|
} from "@core/hooks/useNodesAsProto.ts";
|
||||||
import {
|
import {
|
||||||
type Page,
|
type Page,
|
||||||
useActiveConnection,
|
useActiveConnection,
|
||||||
@@ -61,7 +64,10 @@ const CollapseToggleButton = () => {
|
|||||||
>
|
>
|
||||||
<CircleChevronLeft
|
<CircleChevronLeft
|
||||||
size={24}
|
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>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -145,12 +151,18 @@ export const Sidebar = ({ children }: SidebarProps) => {
|
|||||||
isCollapsed && "justify-center px-0",
|
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
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xl font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap",
|
"text-xl font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap",
|
||||||
"transition-all duration-300 ease-in-out",
|
"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")}
|
{t("app.title")}
|
||||||
@@ -177,7 +189,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
|
|||||||
})}
|
})}
|
||||||
</SidebarSection>
|
</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">
|
<div className=" pt-4 border-t-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700 h-full flex-1">
|
||||||
{myNode === undefined ? (
|
{myNode === undefined ? (
|
||||||
@@ -198,7 +212,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
|
|||||||
setCommandPaletteOpen={() => setCommandPaletteOpen(true)}
|
setCommandPaletteOpen={() => setCommandPaletteOpen(true)}
|
||||||
setDialogOpen={() => setDialogOpen("deviceName", true)}
|
setDialogOpen={() => setDialogOpen("deviceName", true)}
|
||||||
user={myNode.user}
|
user={myNode.user}
|
||||||
firmwareVersion={myMetadata?.firmwareVersion ?? t("unknown.notAvailable")}
|
firmwareVersion={
|
||||||
|
myMetadata?.firmwareVersion ?? t("unknown.notAvailable")
|
||||||
|
}
|
||||||
deviceMetrics={{
|
deviceMetrics={{
|
||||||
batteryLevel: myNode.deviceMetrics?.batteryLevel,
|
batteryLevel: myNode.deviceMetrics?.batteryLevel,
|
||||||
voltage:
|
voltage:
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default function ThemeSwitcher({
|
|||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const preferences: ThemePreference[] = ["light", "dark", "system"];
|
const preferences: ThemePreference[] = ["light", "dark", "system"];
|
||||||
const currentIndex = preferences.indexOf(preference);
|
const currentIndex = preferences.indexOf(preference);
|
||||||
const nextPreference = preferences[(currentIndex + 1) % preferences.length] ?? "system";
|
const nextPreference =
|
||||||
|
preferences[(currentIndex + 1) % preferences.length] ?? "system";
|
||||||
setPreference(nextPreference);
|
setPreference(nextPreference);
|
||||||
toggleShowTooltip();
|
toggleShowTooltip();
|
||||||
};
|
};
|
||||||
@@ -89,7 +90,8 @@ export default function ThemeSwitcher({
|
|||||||
"text-sm",
|
"text-sm",
|
||||||
"text-gray-600 dark:text-gray-300",
|
"text-gray-600 dark:text-gray-300",
|
||||||
"transition-colors duration-150",
|
"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")}
|
{t("theme.changeTheme")}
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ export function Toaster() {
|
|||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(({ id, title, description, action, duration, ...props }) => (
|
{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">
|
<div className="grid gap-1">
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
{description && <ToastDescription>{description}</ToastDescription>}
|
{description && <ToastDescription>{description}</ToastDescription>}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user