refactor: switch to using Bun (#718)
This commit is contained in:
@@ -9,4 +9,4 @@ if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then
|
||||
. "$SIMPLE_GIT_HOOKS_RC"
|
||||
fi
|
||||
|
||||
cd packages/web && deno task lint:fix && deno task format
|
||||
# cd packages/web && bun run lint && bun run format
|
||||
+14
-14
@@ -16,29 +16,29 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
bun-version: latest
|
||||
|
||||
- name: Install Dependencies
|
||||
run: deno install
|
||||
run: bun install
|
||||
|
||||
- name: Cache Deno dependencies
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/deno
|
||||
packages/web/deno.lock
|
||||
key: ${{ runner.os }}-deno-${{ hashFiles('packages/web/deno.lock') }}
|
||||
~/.bun/install/cache
|
||||
packages/web/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('packages/web/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-deno-
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Run linter
|
||||
run: deno lint
|
||||
# - name: Run linter
|
||||
# run: bun run lint
|
||||
|
||||
- name: Check formatter
|
||||
run: deno fmt --check
|
||||
# - name: Check formatter
|
||||
# run: bun run check
|
||||
|
||||
- name: Build Package
|
||||
run: deno task --filter web build
|
||||
run: bun run build
|
||||
|
||||
@@ -15,18 +15,20 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache Deno dependencies
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.setup-deno.outputs.deno-cache-path }}
|
||||
key: deno-cache-${{ runner.os }}-${{ hashFiles('**/deno.lock') }}
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
packages/web/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('packages/web/bun.lockb') }}
|
||||
restore-keys: |
|
||||
deno-cache-${{ runner.os }}-*
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# - name: Run tests
|
||||
# working-directory: packages/web
|
||||
@@ -34,15 +36,15 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
working-directory: packages/web
|
||||
run: deno install
|
||||
run: bun install
|
||||
|
||||
- name: Build Package
|
||||
working-directory: packages/web
|
||||
run: deno task build
|
||||
run: bun run build
|
||||
|
||||
- name: Package Output
|
||||
working-directory: packages/web
|
||||
run: deno task package
|
||||
run: bun run package
|
||||
|
||||
- name: Archive compressed build
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+16
-16
@@ -13,34 +13,34 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
bun-version: latest
|
||||
|
||||
- name: Install Dependencies
|
||||
# Commands will run from 'packages/web'
|
||||
working-directory: packages/web
|
||||
run: deno install
|
||||
run: bun install
|
||||
|
||||
- name: Cache Deno dependencies
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/deno
|
||||
packages/web/deno.lock
|
||||
key: ${{ runner.os }}-deno-${{ hashFiles('packages/web/deno.lock') }}
|
||||
~/.bun/install/cache
|
||||
packages/web/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('packages/web/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-deno-
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Run linter
|
||||
working-directory: packages/web
|
||||
run: deno lint
|
||||
# - name: Run linter
|
||||
# working-directory: packages/web
|
||||
# run: bun run lint
|
||||
|
||||
- name: Check formatter
|
||||
working-directory: packages/web
|
||||
run: deno fmt --check
|
||||
# - name: Check formatter
|
||||
# working-directory: packages/web
|
||||
# run: bun run check
|
||||
|
||||
- name: Build Package
|
||||
working-directory: packages/web
|
||||
run: deno task --filter web build
|
||||
run: bun run build
|
||||
|
||||
@@ -19,19 +19,27 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# --- Setup Deno ---
|
||||
# --- Setup Bun ---
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
# --- Setup Deno (for NPM package building) ---
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Cache Deno Dependencies
|
||||
- name: Cache Bun Dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/deno
|
||||
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
packages/web/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-deno-
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# --- Determine Changed Packages ---
|
||||
- name: Get Changed Package Directories
|
||||
@@ -98,12 +106,12 @@ jobs:
|
||||
- name: Run Web App Tests
|
||||
if: steps.web_changes.outputs.web_changed == 'true'
|
||||
working-directory: packages/web
|
||||
run: deno task test
|
||||
run: bun run test
|
||||
|
||||
- name: Create Web App Release Archive
|
||||
if: steps.web_changes.outputs.web_changed == 'true'
|
||||
working-directory: packages/web
|
||||
run: deno task package # Generates dist/build.tar
|
||||
run: bun run package # Generates dist/build.tar
|
||||
|
||||
- name: Upload Web App Archive
|
||||
if: steps.web_changes.outputs.web_changed == 'true'
|
||||
|
||||
Vendored
+2
-5
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.suggest.imports.autoDiscover": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ All `Meshtastic JS` packages (core and transports) are published both to
|
||||
|
||||
This monorepo leverages the following technologies:
|
||||
|
||||
- **Runtime:** Deno
|
||||
- **Runtime:** Bun
|
||||
- **Web Client:** React.js
|
||||
- **Styling:** Tailwind CSS
|
||||
- **Bundling:** Vite
|
||||
@@ -56,7 +56,7 @@ This monorepo leverages the following technologies:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You'll need to have [Deno](https://deno.com/) installed to work with this
|
||||
You'll need to have [Bun](https://bun.sh/) installed to work with this
|
||||
monorepo. Follow the installation instructions on their home page.
|
||||
|
||||
### Development Setup
|
||||
@@ -68,7 +68,7 @@ monorepo. Follow the installation instructions on their home page.
|
||||
```
|
||||
2. **Install dependencies for all packages:**
|
||||
```bash
|
||||
deno task install:all
|
||||
bun install
|
||||
```
|
||||
This command installs all necessary dependencies for all packages within the
|
||||
monorepo.
|
||||
@@ -80,7 +80,7 @@ monorepo. Follow the installation instructions on their home page.
|
||||
To start the development server for the web client:
|
||||
|
||||
```bash
|
||||
deno task --filter web dev
|
||||
bun run --filter web dev
|
||||
```
|
||||
|
||||
This will typically run the web client on http://localhost:3000 and requires a
|
||||
@@ -89,11 +89,11 @@ Chromium browser
|
||||
## Meshtastic JS Packages
|
||||
|
||||
While the js packages are primarily libraries, you can run their tests or
|
||||
specific development scripts if defined within their deno.json files. For
|
||||
specific development scripts if defined within their package.json files. For
|
||||
example, to run tests for a specific package:
|
||||
|
||||
```bash
|
||||
deno task --filter core test
|
||||
bun run --filter core test
|
||||
```
|
||||
|
||||
### Feedback
|
||||
@@ -102,24 +102,24 @@ If you encounter any issues with nightly builds, please report them in our
|
||||
[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps
|
||||
improve the stability of future releases
|
||||
|
||||
### Why Deno?
|
||||
### Why Bun?
|
||||
|
||||
Meshtastic Web uses Deno as its development platform for several compelling
|
||||
Meshtastic Web uses Bun as its development platform for several compelling
|
||||
reasons:
|
||||
|
||||
- **Built-in Security**: Deno's security-first approach requires explicit
|
||||
permissions for file, network, and environment access, reducing vulnerability
|
||||
risks.
|
||||
- **Fast Performance**: Bun is built from the ground up for speed, offering
|
||||
significantly faster package installation and bundling compared to other
|
||||
JavaScript runtimes.
|
||||
- **TypeScript Support**: Native TypeScript support without additional
|
||||
configuration, enhancing code quality and developer experience.
|
||||
- **Modern JavaScript**: First-class support for ESM imports, top-level await,
|
||||
and other modern JavaScript features.
|
||||
- **Simplified Tooling**: Built-in formatter, linter, test runner, and bundler
|
||||
eliminate the need for multiple third-party tools.
|
||||
- **All-in-One Tooling**: Built-in package manager, bundler, test runner, and
|
||||
transpiler eliminate the need for multiple third-party tools.
|
||||
- **Node.js Compatibility**: Drop-in replacement for Node.js with better
|
||||
performance and built-in tooling.
|
||||
- **Reproducible Builds**: Lockfile ensures consistent builds across all
|
||||
environments.
|
||||
- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable
|
||||
between server and client environments.
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"workspace": [
|
||||
"./packages/web",
|
||||
"./packages/core",
|
||||
"./packages/transport-deno",
|
||||
"./packages/transport-node",
|
||||
"./packages/transport-http",
|
||||
"./packages/transport-web-bluetooth",
|
||||
"./packages/transport-web-serial"
|
||||
],
|
||||
"tasks": {
|
||||
"build:npm": "deno run -A scripts/build_npm_package.ts"
|
||||
},
|
||||
"imports": {
|
||||
"@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.2.3",
|
||||
"@deno/dnt": "jsr:@deno/dnt@^0.42.1",
|
||||
"@meshtastic/protobufs": "jsr:@meshtastic/protobufs@^2.7.0",
|
||||
"@types/node": "npm:@types/node@^22.13.10",
|
||||
"ste-simple-events": "npm:ste-simple-events@^3.0.11",
|
||||
"tslog": "npm:tslog@^4.9.3"
|
||||
},
|
||||
"nodeModulesDir": "auto",
|
||||
"lint": {
|
||||
"exclude": [
|
||||
"*routeTree.gen.ts",
|
||||
"*.test.ts",
|
||||
"*.test.tsx"
|
||||
],
|
||||
"report": "pretty"
|
||||
},
|
||||
"fmt": {
|
||||
"exclude": [
|
||||
"*routeTree.gen.ts",
|
||||
"*.test.ts",
|
||||
"*.test.tsx"
|
||||
]
|
||||
},
|
||||
"unstable": [
|
||||
"sloppy-imports"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"npm",
|
||||
"build",
|
||||
"coverage",
|
||||
"out",
|
||||
".vscode-test"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "meshtastic-web",
|
||||
"version": "2.7.0-0",
|
||||
"type": "module",
|
||||
"description": "Meshtastic web client monorepo",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/meshtastic/web.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/meshtastic/web/issues"
|
||||
},
|
||||
"homepage": "https://meshtastic.org",
|
||||
"workspaces": ["packages/web"],
|
||||
"scripts": {
|
||||
"build:npm": "deno run -A scripts/build_npm_package.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"bun": "^1.1.18",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { Logger } from "tslog";
|
||||
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
||||
import * as Protobuf from "@meshtastic/protobufs";
|
||||
|
||||
import { EventSystem, Queue, Xmodem } from "./utils/mod.ts";
|
||||
import { decodePacket } from "./utils/transform/decodePacket.ts";
|
||||
import { Constants } from "./constants.ts";
|
||||
import type { Destination, PacketMetadata, Transport } from "./types.ts";
|
||||
import { ChannelNumber, DeviceStatusEnum, Emitter } from "./types.ts";
|
||||
import { EventSystem, Queue, Xmodem } from "./utils/mod.ts";
|
||||
import { decodePacket } from "./utils/transform/decodePacket.ts";
|
||||
|
||||
export class MeshDevice {
|
||||
public transport: Transport;
|
||||
@@ -182,7 +182,8 @@ export class MeshDevice {
|
||||
},
|
||||
},
|
||||
from: this.myNodeInfo.myNodeNum,
|
||||
to: destination === "broadcast"
|
||||
to:
|
||||
destination === "broadcast"
|
||||
? Constants.broadcastNum
|
||||
: destination === "self"
|
||||
? this.myNodeInfo.myNodeNum
|
||||
@@ -262,10 +263,7 @@ export class MeshDevice {
|
||||
public async setModuleConfig(
|
||||
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.SetModuleConfig],
|
||||
"⚙️ Setting module config",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.SetModuleConfig], "⚙️ Setting module config");
|
||||
|
||||
const moduleConfigMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -351,10 +349,7 @@ export class MeshDevice {
|
||||
* Triggers Device to enter DFU mode
|
||||
*/
|
||||
public async enterDfuMode(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.EnterDfuMode],
|
||||
"🔌 Entering DFU mode",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.EnterDfuMode], "🔌 Entering DFU mode");
|
||||
|
||||
const enterDfuModeRequest = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -411,10 +406,7 @@ export class MeshDevice {
|
||||
public async getConfig(
|
||||
configType: Protobuf.Admin.AdminMessage_ConfigType,
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.GetConfig],
|
||||
"⚙️ Requesting config",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.GetConfig], "⚙️ Requesting config");
|
||||
|
||||
const getRadioRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -457,10 +449,7 @@ export class MeshDevice {
|
||||
|
||||
/** Gets devices Owner */
|
||||
public async getOwner(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.GetOwner],
|
||||
"👤 Requesting owner",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.GetOwner], "👤 Requesting owner");
|
||||
|
||||
const getOwnerRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -572,10 +561,7 @@ export class MeshDevice {
|
||||
* that no longer exist.
|
||||
*/
|
||||
public async resetNodes(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.ResetNodes],
|
||||
"📻 Resetting NodeDB",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.ResetNodes], "📻 Resetting NodeDB");
|
||||
|
||||
const resetNodes = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -683,10 +669,7 @@ export class MeshDevice {
|
||||
* Factory resets the current device
|
||||
*/
|
||||
public async factoryResetDevice(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.FactoryReset],
|
||||
"♻️ Factory resetting device",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.FactoryReset], "♻️ Factory resetting device");
|
||||
|
||||
const factoryReset = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -706,10 +689,7 @@ export class MeshDevice {
|
||||
* Factory resets the current config
|
||||
*/
|
||||
public async factoryResetConfig(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.FactoryReset],
|
||||
"♻️ Factory resetting config",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.FactoryReset], "♻️ Factory resetting config");
|
||||
|
||||
const factoryReset = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -749,10 +729,7 @@ export class MeshDevice {
|
||||
* Serial connection requires a heartbeat ping to stay connected, otherwise times out after 15 minutes
|
||||
*/
|
||||
public heartbeat(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.Ping],
|
||||
"❤️ Send heartbeat ping to radio",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.Ping], "❤️ Send heartbeat ping to radio");
|
||||
|
||||
const toRadio = create(Protobuf.Mesh.ToRadioSchema, {
|
||||
payloadVariant: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SimpleEventDispatcher } from "ste-simple-events";
|
||||
import type * as Protobuf from "@meshtastic/protobufs";
|
||||
import { SimpleEventDispatcher } from "ste-simple-events";
|
||||
import type { PacketMetadata } from "../types.ts";
|
||||
import type * as Types from "../types.ts";
|
||||
|
||||
@@ -9,22 +9,16 @@ export class EventSystem {
|
||||
*
|
||||
* @event onLogEvent
|
||||
*/
|
||||
public readonly onLogEvent: SimpleEventDispatcher<
|
||||
Types.LogEventPacket
|
||||
> = new SimpleEventDispatcher<
|
||||
Types.LogEventPacket
|
||||
>();
|
||||
public readonly onLogEvent: SimpleEventDispatcher<Types.LogEventPacket> =
|
||||
new SimpleEventDispatcher<Types.LogEventPacket>();
|
||||
|
||||
/**
|
||||
* Fires when a new FromRadio message has been received from the device
|
||||
*
|
||||
* @event onFromRadio
|
||||
*/
|
||||
public readonly onFromRadio: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.FromRadio
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.FromRadio
|
||||
>();
|
||||
public readonly onFromRadio: SimpleEventDispatcher<Protobuf.Mesh.FromRadio> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.FromRadio>();
|
||||
|
||||
/**
|
||||
* Fires when a new FromRadio message containing a Data packet has been
|
||||
@@ -32,22 +26,16 @@ export class EventSystem {
|
||||
*
|
||||
* @event onMeshPacket
|
||||
*/
|
||||
public readonly onMeshPacket: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.MeshPacket
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.MeshPacket
|
||||
>();
|
||||
public readonly onMeshPacket: SimpleEventDispatcher<Protobuf.Mesh.MeshPacket> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.MeshPacket>();
|
||||
|
||||
/**
|
||||
* Fires when a new MyNodeInfo message has been received from the device
|
||||
*
|
||||
* @event onMyNodeInfo
|
||||
*/
|
||||
public readonly onMyNodeInfo: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.MyNodeInfo
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.MyNodeInfo
|
||||
>();
|
||||
public readonly onMyNodeInfo: SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a NodeInfo packet has been
|
||||
@@ -55,44 +43,32 @@ export class EventSystem {
|
||||
*
|
||||
* @event onNodeInfoPacket
|
||||
*/
|
||||
public readonly onNodeInfoPacket: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.NodeInfo
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.NodeInfo
|
||||
>();
|
||||
public readonly onNodeInfoPacket: SimpleEventDispatcher<Protobuf.Mesh.NodeInfo> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.NodeInfo>();
|
||||
|
||||
/**
|
||||
* Fires when a new Channel message is received
|
||||
*
|
||||
* @event onChannelPacket
|
||||
*/
|
||||
public readonly onChannelPacket: SimpleEventDispatcher<
|
||||
Protobuf.Channel.Channel
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Channel.Channel
|
||||
>();
|
||||
public readonly onChannelPacket: SimpleEventDispatcher<Protobuf.Channel.Channel> =
|
||||
new SimpleEventDispatcher<Protobuf.Channel.Channel>();
|
||||
|
||||
/**
|
||||
* Fires when a new Config message is received
|
||||
*
|
||||
* @event onConfigPacket
|
||||
*/
|
||||
public readonly onConfigPacket: SimpleEventDispatcher<
|
||||
Protobuf.Config.Config
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Config.Config
|
||||
>();
|
||||
public readonly onConfigPacket: SimpleEventDispatcher<Protobuf.Config.Config> =
|
||||
new SimpleEventDispatcher<Protobuf.Config.Config>();
|
||||
|
||||
/**
|
||||
* Fires when a new ModuleConfig message is received
|
||||
*
|
||||
* @event onModuleConfigPacket
|
||||
*/
|
||||
public readonly onModuleConfigPacket: SimpleEventDispatcher<
|
||||
Protobuf.ModuleConfig.ModuleConfig
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.ModuleConfig.ModuleConfig
|
||||
>();
|
||||
public readonly onModuleConfigPacket: SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig> =
|
||||
new SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a ATAK packet has been
|
||||
@@ -102,9 +78,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onAtakPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Text packet has been
|
||||
@@ -114,9 +88,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onMessagePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Remote Hardware packet has
|
||||
@@ -138,9 +110,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onPositionPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Position>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Position>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a User packet has been
|
||||
@@ -150,9 +120,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onUserPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.User>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.User>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Routing packet has been
|
||||
@@ -162,9 +130,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onRoutingPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Routing>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Routing>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>>();
|
||||
|
||||
/**
|
||||
* Fires when the device receives a Metadata packet
|
||||
@@ -173,9 +139,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onDeviceMetadataPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.DeviceMetadata>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.DeviceMetadata>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.DeviceMetadata>>();
|
||||
|
||||
/**
|
||||
* Fires when the device receives a Canned Message Module message packet
|
||||
@@ -184,9 +148,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onCannedMessageModulePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Waypoint packet has been
|
||||
@@ -196,9 +158,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onWaypointPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Waypoint>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Waypoint>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing an Audio packet has been
|
||||
@@ -208,9 +168,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onAudioPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Detection Sensor packet has been
|
||||
@@ -220,9 +178,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onDetectionSensorPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Ping packet has been
|
||||
@@ -232,9 +188,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onPingPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a IP Tunnel packet has been
|
||||
@@ -244,9 +198,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onIpTunnelPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Paxcounter packet has been
|
||||
@@ -256,9 +208,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onPaxcounterPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.PaxCount.Paxcount>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.PaxCount.Paxcount>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.PaxCount.Paxcount>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Serial packet has been
|
||||
@@ -268,9 +218,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onSerialPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Store and Forward packet
|
||||
@@ -280,9 +228,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onStoreForwardPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Store and Forward packet
|
||||
@@ -292,9 +238,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onRangeTestPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Telemetry packet has been
|
||||
@@ -304,9 +248,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onTelemetryPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Telemetry.Telemetry>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Telemetry.Telemetry>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Telemetry.Telemetry>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a ZPS packet has been
|
||||
@@ -316,9 +258,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onZpsPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Simulator packet has been
|
||||
@@ -328,9 +268,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onSimulatorPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Trace Route packet has been
|
||||
@@ -340,9 +278,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onTraceRoutePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.RouteDiscovery>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.RouteDiscovery>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.RouteDiscovery>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Neighbor Info packet has been
|
||||
@@ -352,9 +288,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onNeighborInfoPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.NeighborInfo>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.NeighborInfo>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.NeighborInfo>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing an ATAK packet has been
|
||||
@@ -364,9 +298,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onAtakPluginPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Map Report packet has been
|
||||
@@ -376,9 +308,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onMapReportPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Private packet has been
|
||||
@@ -388,9 +318,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onPrivatePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing an ATAK Forwarder packet has been
|
||||
@@ -400,20 +328,15 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onAtakForwarderPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when the devices connection or configuration status changes
|
||||
*
|
||||
* @event onDeviceStatus
|
||||
*/
|
||||
public readonly onDeviceStatus: SimpleEventDispatcher<
|
||||
Types.DeviceStatusEnum
|
||||
> = new SimpleEventDispatcher<
|
||||
Types.DeviceStatusEnum
|
||||
>();
|
||||
public readonly onDeviceStatus: SimpleEventDispatcher<Types.DeviceStatusEnum> =
|
||||
new SimpleEventDispatcher<Types.DeviceStatusEnum>();
|
||||
|
||||
/**
|
||||
* Fires when a new FromRadio message containing a LogRecord packet has been
|
||||
@@ -421,11 +344,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onLogRecord
|
||||
*/
|
||||
public readonly onLogRecord: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.LogRecord
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.LogRecord
|
||||
>();
|
||||
public readonly onLogRecord: SimpleEventDispatcher<Protobuf.Mesh.LogRecord> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.LogRecord>();
|
||||
|
||||
/**
|
||||
* Fires when the device receives a meshPacket, returns a timestamp
|
||||
@@ -448,20 +368,14 @@ export class EventSystem {
|
||||
*
|
||||
* @event onpendingSettingsChange
|
||||
*/
|
||||
public readonly onPendingSettingsChange: SimpleEventDispatcher<
|
||||
boolean
|
||||
> = new SimpleEventDispatcher<
|
||||
boolean
|
||||
>();
|
||||
public readonly onPendingSettingsChange: SimpleEventDispatcher<boolean> =
|
||||
new SimpleEventDispatcher<boolean>();
|
||||
|
||||
/**
|
||||
* Fires when a QueueStatus message is generated
|
||||
*
|
||||
* @event onQueueStatus
|
||||
*/
|
||||
public readonly onQueueStatus: SimpleEventDispatcher<
|
||||
Protobuf.Mesh.QueueStatus
|
||||
> = new SimpleEventDispatcher<
|
||||
Protobuf.Mesh.QueueStatus
|
||||
>();
|
||||
public readonly onQueueStatus: SimpleEventDispatcher<Protobuf.Mesh.QueueStatus> =
|
||||
new SimpleEventDispatcher<Protobuf.Mesh.QueueStatus>();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SimpleEventDispatcher } from "ste-simple-events";
|
||||
import { fromBinary } from "@bufbuild/protobuf";
|
||||
import * as Protobuf from "@meshtastic/protobufs";
|
||||
import { SimpleEventDispatcher } from "ste-simple-events";
|
||||
import type { PacketError, QueueItem } from "../types.ts";
|
||||
|
||||
export class Queue {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fromBinary } from "@bufbuild/protobuf";
|
||||
import type { DeviceOutput } from "../../types.ts";
|
||||
import { Constants, Protobuf, Types } from "../../../mod.ts";
|
||||
import type { MeshDevice } from "../../../mod.ts";
|
||||
import type { DeviceOutput } from "../../types.ts";
|
||||
|
||||
export const decodePacket = (device: MeshDevice) =>
|
||||
new WritableStream<DeviceOutput>({
|
||||
|
||||
@@ -43,11 +43,9 @@ export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
|
||||
packet[malformedDetectorIndex + 1] === 0xc3
|
||||
) {
|
||||
console.warn(
|
||||
`⚠️ Malformed packet found, discarding: ${
|
||||
byteBuffer
|
||||
`⚠️ Malformed packet found, discarding: ${byteBuffer
|
||||
.subarray(0, malformedDetectorIndex - 1)
|
||||
.toString()
|
||||
}`,
|
||||
.toString()}`,
|
||||
);
|
||||
|
||||
byteBuffer = byteBuffer.subarray(malformedDetectorIndex);
|
||||
|
||||
@@ -7,9 +7,9 @@ export const toDeviceStream: TransformStream<Uint8Array, Uint8Array> =
|
||||
const bufLen = chunk.length;
|
||||
const header = new Uint8Array([
|
||||
0x94,
|
||||
0xC3,
|
||||
(bufLen >> 8) & 0xFF,
|
||||
bufLen & 0xFF,
|
||||
0xc3,
|
||||
(bufLen >> 8) & 0xff,
|
||||
bufLen & 0xff,
|
||||
]);
|
||||
controller.enqueue(new Uint8Array([...header, ...chunk]));
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crc16ccitt from "crc/calculators/crc16ccitt";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import * as Protobuf from "@meshtastic/protobufs";
|
||||
import crc16ccitt from "crc/calculators/crc16ccitt";
|
||||
|
||||
//if counter > 35 then reset counter/clear/error/reject promise
|
||||
type XmodemProps = (toRadio: Uint8Array, id?: number) => Promise<number>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Utils } from "@meshtastic/core";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { Socket } from "node:net";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { Utils } from "@meshtastic/core";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
|
||||
export class TransportNode implements Types.Transport {
|
||||
private readonly _toDevice: WritableStream<Uint8Array>;
|
||||
@@ -40,9 +40,9 @@ export class TransportNode implements Types.Transport {
|
||||
console.error("Socket connection error:", err);
|
||||
});
|
||||
|
||||
const fromDeviceSource = Readable.toWeb(connection) as ReadableStream<
|
||||
Uint8Array
|
||||
>;
|
||||
const fromDeviceSource = Readable.toWeb(
|
||||
connection,
|
||||
) as ReadableStream<Uint8Array>;
|
||||
this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
|
||||
|
||||
// Stream for data going FROM the application TO the Meshtastic device.
|
||||
@@ -52,9 +52,8 @@ export class TransportNode implements Types.Transport {
|
||||
// The readable end of the transform is then piped to the Node.js socket.
|
||||
// A similar assertion is needed here because `Writable.toWeb` also returns
|
||||
// a generically typed stream (`WritableStream<any>`).
|
||||
toDeviceTransform.readable.pipeTo(
|
||||
Writable.toWeb(connection) as WritableStream<Uint8Array>,
|
||||
)
|
||||
toDeviceTransform.readable
|
||||
.pipeTo(Writable.toWeb(connection) as WritableStream<Uint8Array>)
|
||||
.catch((err) => {
|
||||
console.error("Error piping data to socket:", err);
|
||||
connection.destroy(err as Error);
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"@types/web-bluetooth"
|
||||
]
|
||||
"types": ["@types/web-bluetooth"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { Types } from "@meshtastic/core";
|
||||
export class TransportWebBluetooth implements Types.Transport {
|
||||
private _toDevice: WritableStream<Uint8Array>;
|
||||
private _fromDevice: ReadableStream<Types.DeviceOutput>;
|
||||
private _fromDeviceController?: ReadableStreamDefaultController<
|
||||
Types.DeviceOutput
|
||||
>;
|
||||
private _fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>;
|
||||
private _isFirstWrite = true;
|
||||
|
||||
private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic;
|
||||
@@ -52,7 +50,8 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
);
|
||||
|
||||
if (
|
||||
!toRadioCharacteristic || !fromRadioCharacteristic ||
|
||||
!toRadioCharacteristic ||
|
||||
!fromRadioCharacteristic ||
|
||||
!fromNumCharacteristic
|
||||
) {
|
||||
throw new Error("Failed to find required characteristics");
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"@types/w3c-web-serial"
|
||||
]
|
||||
"types": ["@types/w3c-web-serial"]
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -98,7 +98,7 @@ improve the stability of future releases
|
||||
## Development & Building
|
||||
|
||||
You'll need to download the package manager used with this repo. You can install
|
||||
it by visiting [deno.com](https://deno.com/) and following the installation
|
||||
it by visiting [bun.sh](https://bun.sh/) and following the installation
|
||||
instructions listed on the home page.
|
||||
|
||||
### Development
|
||||
@@ -106,13 +106,13 @@ instructions listed on the home page.
|
||||
Install the dependencies.
|
||||
|
||||
```bash
|
||||
deno i
|
||||
bun install
|
||||
```
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
deno task dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Building and Packaging
|
||||
@@ -120,33 +120,33 @@ deno task dev
|
||||
Build the project:
|
||||
|
||||
```bash
|
||||
deno task build
|
||||
bun run build
|
||||
```
|
||||
|
||||
GZip the output:
|
||||
|
||||
```bash
|
||||
deno task package
|
||||
bun run package
|
||||
```
|
||||
|
||||
### Why Deno?
|
||||
### Why Bun?
|
||||
|
||||
Meshtastic Web uses Deno as its development platform for several compelling
|
||||
Meshtastic Web uses Bun as its development platform for several compelling
|
||||
reasons:
|
||||
|
||||
- **Built-in Security**: Deno's security-first approach requires explicit
|
||||
permissions for file, network, and environment access, reducing vulnerability
|
||||
risks.
|
||||
- **Fast Performance**: Bun is built from the ground up for speed, offering
|
||||
significantly faster package installation and bundling compared to other
|
||||
JavaScript runtimes.
|
||||
- **TypeScript Support**: Native TypeScript support without additional
|
||||
configuration, enhancing code quality and developer experience.
|
||||
- **Modern JavaScript**: First-class support for ESM imports, top-level await,
|
||||
and other modern JavaScript features.
|
||||
- **Simplified Tooling**: Built-in formatter, linter, test runner, and bundler
|
||||
eliminate the need for multiple third-party tools.
|
||||
- **All-in-One Tooling**: Built-in package manager, bundler, test runner, and
|
||||
transpiler eliminate the need for multiple third-party tools.
|
||||
- **Node.js Compatibility**: Drop-in replacement for Node.js with better
|
||||
performance and built-in tooling.
|
||||
- **Reproducible Builds**: Lockfile ensures consistent builds across all
|
||||
environments.
|
||||
- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable
|
||||
between server and client environments.
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"files": {
|
||||
"includes": ["**", "!node_modules/**", "!**/*.css", "!dist/**", "!build/**", "!coverage/**", "!**/*.d.ts"],
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noDebugger": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"useBlockStatements": "error",
|
||||
"useSingleVarDeclarator": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"imports": {
|
||||
"@app/": "./src/",
|
||||
"@public/": "./public/",
|
||||
"@pages/": "./src/pages/",
|
||||
"@components/": "./src/components/",
|
||||
"@core/": "./src/core/",
|
||||
"@layouts/": "./src/layouts/",
|
||||
"@std/path": "jsr:@std/path@^1.1.0"
|
||||
},
|
||||
"tasks": {
|
||||
"build": "vite build",
|
||||
"build:analyze": "BUNDLE_ANALYZE=true deno task build",
|
||||
"lint": "deno lint src/",
|
||||
"lint:fix": "deno lint --fix src/",
|
||||
"format": "deno fmt src/",
|
||||
"dev": "deno task dev:ui",
|
||||
"dev:ui": "VITE_APP_VERSION=development deno run -A npm:vite dev",
|
||||
"test": "deno run -A npm:vitest",
|
||||
"check": "deno check",
|
||||
"preview": "deno run -A npm:vite preview",
|
||||
"generate:routes": "deno run -A npm:@tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
|
||||
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||
},
|
||||
"include": ["src", "./vite-env.d.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext",
|
||||
"deno.window",
|
||||
"deno.ns"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node",
|
||||
"npm:@types/w3c-web-serial",
|
||||
"npm:@types/web-bluetooth"
|
||||
],
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"exclude": [
|
||||
"routeTree.gen.ts",
|
||||
"node_modules/",
|
||||
"dist",
|
||||
"build",
|
||||
"coverage",
|
||||
"out",
|
||||
".vscode-test"
|
||||
]
|
||||
}
|
||||
@@ -18,6 +18,11 @@
|
||||
href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/cascadia-code.min.css"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="src/index.css"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<meta name="theme-color" content="#67ea94" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Meshtastic Web Client" />
|
||||
|
||||
+27
-17
@@ -4,7 +4,6 @@
|
||||
"type": "module",
|
||||
"description": "Meshtastic web client",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/meshtastic/web.git"
|
||||
@@ -13,14 +12,25 @@
|
||||
"url": "https://github.com/meshtastic/web/issues"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "deno task lint:fix && deno task format"
|
||||
"pre-commit": "bun run check:fix"
|
||||
},
|
||||
"homepage": "https://meshtastic.org",
|
||||
"scripts": {
|
||||
"build": "bunx --bun vite build",
|
||||
"build:analyze": "BUNDLE_ANALYZE=true bun run build",
|
||||
"check": "biome check src/",
|
||||
"check:fix": "biome check --write src/",
|
||||
"dev": "bunx --bun vite",
|
||||
"test": "bunx --bun vitest",
|
||||
"ts:check": "bun run tsc --noEmit",
|
||||
"preview": "bunx --bun vite preview",
|
||||
"generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
|
||||
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.6.0",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@meshtastic/core": "npm:@jsr/meshtastic__core@2.6.4",
|
||||
"@meshtastic/js": "npm:@jsr/meshtastic__js@2.6.0-0",
|
||||
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http",
|
||||
"@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth",
|
||||
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial",
|
||||
@@ -41,19 +51,20 @@
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-router": "^1.125.4",
|
||||
"@tanstack/react-router-devtools": "^1.125.4",
|
||||
"@tanstack/router-cli": "^1.125.4",
|
||||
"@tanstack/router-devtools": "^1.125.4",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-router": "^1.127.9",
|
||||
"@tanstack/react-router-devtools": "^1.127.9",
|
||||
"@tanstack/router-cli": "^1.127.8",
|
||||
"@tanstack/router-devtools": "^1.127.9",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/node": "^24.0.14",
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"idb-keyval": "^6.2.2",
|
||||
@@ -69,26 +80,25 @@
|
||||
"react-map-gl": "8.0.4",
|
||||
"react-qrcode-logo": "^3.0.0",
|
||||
"rfc4648": "^1.5.4",
|
||||
"zod": "^3.25.75",
|
||||
"zod": "^4.0.5",
|
||||
"zustand": "5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/router-plugin": "^1.125.5",
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@tanstack/router-plugin": "^1.127.9",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/chrome": "^0.0.329",
|
||||
"@types/chrome": "^0.1.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/serviceworker": "^0.0.140",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/serviceworker": "^0.0.142",
|
||||
"@types/w3c-web-serial": "^1.0.8",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"gzipper": "^8.2.1",
|
||||
"happy-dom": "^18.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
@@ -96,7 +106,7 @@
|
||||
"tar": "^7.4.3",
|
||||
"testing-library": "^0.0.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0",
|
||||
"vite": "^7.0.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -1,20 +1,19 @@
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||
import { CommandPalette } from "@components/CommandPalette/index.tsx";
|
||||
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
|
||||
import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
|
||||
import { Toaster } from "@components/Toaster.tsx";
|
||||
import { ErrorPage } from "@components/UI/ErrorPage.tsx";
|
||||
import Footer from "@components/UI/Footer.tsx";
|
||||
import { useTheme } from "@core/hooks/useTheme.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { ErrorPage } from "@components/UI/ErrorPage.tsx";
|
||||
import { MapProvider } from "react-map-gl/maplibre";
|
||||
import { CommandPalette } from "@components/CommandPalette/index.tsx";
|
||||
import { SidebarProvider } from "@core/stores/sidebarStore.tsx";
|
||||
import { useTheme } from "@core/hooks/useTheme.ts";
|
||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { MapProvider } from "react-map-gl/maplibre";
|
||||
|
||||
export function App() {
|
||||
const { getDevice } = useDeviceStore();
|
||||
@@ -34,7 +33,7 @@ export function App() {
|
||||
setConnectDialogOpen(open);
|
||||
}}
|
||||
/>
|
||||
<Toaster />
|
||||
{/* <Toaster /> */}
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<DeviceWrapper device={device}>
|
||||
<div
|
||||
@@ -43,8 +42,7 @@ export function App() {
|
||||
>
|
||||
<SidebarProvider>
|
||||
<div className="h-full flex flex-col">
|
||||
{device
|
||||
? (
|
||||
{device ? (
|
||||
<div className="h-full flex w-full">
|
||||
<DialogManager />
|
||||
<KeyBackupReminder />
|
||||
@@ -53,8 +51,7 @@ export function App() {
|
||||
<Outlet />
|
||||
</MapProvider>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
) : (
|
||||
<>
|
||||
<Dashboard />
|
||||
<Footer />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeviceContext } from "@core/stores/deviceStore.ts";
|
||||
import type { Device } from "@core/stores/deviceStore.ts";
|
||||
import { DeviceContext } from "@core/stores/deviceStore.ts";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface DeviceWrapperProps {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@components/UI/Button.tsx", () => ({
|
||||
Button: ({ children, name, disabled, onClick }: {
|
||||
Button: ({
|
||||
children,
|
||||
name,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant: string;
|
||||
name: string;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@components/UI/Checkbox.tsx", () => ({
|
||||
Checkbox: (
|
||||
{ id, checked, onChange }: {
|
||||
Checkbox: ({
|
||||
id,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
},
|
||||
) => (
|
||||
}) => (
|
||||
<input
|
||||
data-testid="checkbox"
|
||||
type="checkbox"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import type React from "react";
|
||||
|
||||
export const Dialog = ({ children, open }: {
|
||||
export const Dialog = ({
|
||||
children,
|
||||
open,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) => open ? <div data-testid="dialog">{children}</div> : null;
|
||||
}) => (open ? <div data-testid="dialog">{children}</div> : null);
|
||||
|
||||
export const DialogContent = ({
|
||||
children,
|
||||
@@ -12,19 +15,19 @@ export const DialogContent = ({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => <div data-testid="dialog-content" className={className}>{children}</div>;
|
||||
}) => (
|
||||
<div data-testid="dialog-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DialogHeader = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => <div data-testid="dialog-header">{children}</div>;
|
||||
export const DialogHeader = ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="dialog-header">{children}</div>
|
||||
);
|
||||
|
||||
export const DialogTitle = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => <div data-testid="dialog-title">{children}</div>;
|
||||
export const DialogTitle = ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="dialog-title">{children}</div>
|
||||
);
|
||||
|
||||
export const DialogDescription = ({
|
||||
children,
|
||||
@@ -33,7 +36,9 @@ export const DialogDescription = ({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div data-testid="dialog-description" className={className}>{children}</div>
|
||||
<div data-testid="dialog-description" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DialogFooter = ({
|
||||
@@ -42,4 +47,8 @@ export const DialogFooter = ({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => <div data-testid="dialog-footer" className={className}>{children}</div>;
|
||||
}) => (
|
||||
<div data-testid="dialog-footer" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@components/UI/Label.tsx", () => ({
|
||||
Label: (
|
||||
{ children, htmlFor, className }: {
|
||||
Label: ({
|
||||
children,
|
||||
htmlFor,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
htmlFor: string;
|
||||
className?: string;
|
||||
},
|
||||
) => (
|
||||
}) => (
|
||||
<label data-testid="label" htmlFor={htmlFor} className={className}>
|
||||
{children}
|
||||
</label>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@components/UI/Typography/Link.tsx", () => ({
|
||||
Link: (
|
||||
{ children, href, className }: {
|
||||
Link: ({
|
||||
children,
|
||||
href,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
className?: string;
|
||||
},
|
||||
) => <a data-testid="link" href={href} className={className}>{children}</a>,
|
||||
}) => (
|
||||
<a data-testid="link" href={href} className={className}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BatteryFullIcon,
|
||||
BatteryLowIcon,
|
||||
BatteryMediumIcon,
|
||||
PlugZapIcon,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DeviceMetrics } from "./types.ts";
|
||||
import type { DeviceMetrics } from "./types.ts";
|
||||
|
||||
type BatteryStatusKey = keyof typeof BATTERY_STATUS;
|
||||
|
||||
@@ -14,10 +14,6 @@ interface BatteryStatusProps {
|
||||
deviceMetrics?: DeviceMetrics | null;
|
||||
}
|
||||
|
||||
interface BatteryStatusProps {
|
||||
deviceMetrics?: DeviceMetrics | null;
|
||||
}
|
||||
|
||||
interface StatusConfig {
|
||||
Icon: React.ElementType;
|
||||
className: string;
|
||||
@@ -89,10 +85,7 @@ const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
|
||||
} = statusConfigMap[statusKey];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 mt-0.5 "
|
||||
aria-label={t("batteryStatus.title")}
|
||||
>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<BatteryIcon size={22} className={iconClassName} />
|
||||
{statusText}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -6,8 +7,11 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@components/UI/Command.tsx";
|
||||
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useCommandState } from "cmdk";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
@@ -32,11 +36,7 @@ import {
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
@@ -125,12 +125,15 @@ export const CommandPalette = () => {
|
||||
label: t("manage.command.switchNode"),
|
||||
icon: ArrowLeftRightIcon,
|
||||
subItems: getDevices().map((device) => ({
|
||||
label: getNode(device.hardware.myNodeNum)?.user?.longName ??
|
||||
label:
|
||||
getNode(device.hardware.myNodeNum)?.user?.longName ??
|
||||
t("unknown.shortName"),
|
||||
icon: (
|
||||
<Avatar
|
||||
text={getNode(device.hardware.myNodeNum)?.user?.shortName ??
|
||||
t("unknown.shortName")}
|
||||
text={
|
||||
getNode(device.hardware.myNodeNum)?.user?.shortName ??
|
||||
t("unknown.shortName")
|
||||
}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
@@ -286,9 +289,6 @@ export const CommandPalette = () => {
|
||||
className={cn(
|
||||
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
|
||||
)}
|
||||
aria-description={pinnedItems.includes(group.label)
|
||||
? t("unpinGroup.label")
|
||||
: t("pinGroup.label")}
|
||||
>
|
||||
<span
|
||||
data-label
|
||||
@@ -345,7 +345,9 @@ const SubItem = ({
|
||||
action: () => void;
|
||||
}) => {
|
||||
const search = useCommandState((state) => state.search);
|
||||
if (!search) return null;
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandItem onSelect={action}>
|
||||
|
||||
@@ -8,15 +8,16 @@ import {
|
||||
Search as SearchIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import BatteryStatus from "./BatteryStatus.tsx";
|
||||
import { Subtle } from "./UI/Typography/Subtle.tsx";
|
||||
import { Avatar } from "./UI/Avatar.tsx";
|
||||
import type { DeviceMetrics } from "./types.ts";
|
||||
import { Button } from "./UI/Button.tsx";
|
||||
import React, { Fragment } from "react";
|
||||
import type React from "react";
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ThemeSwitcher from "./ThemeSwitcher.tsx";
|
||||
import BatteryStatus from "./BatteryStatus.tsx";
|
||||
import LanguageSwitcher from "./LanguageSwitcher.tsx";
|
||||
import ThemeSwitcher from "./ThemeSwitcher.tsx";
|
||||
import type { DeviceMetrics } from "./types.ts";
|
||||
import { Avatar } from "./UI/Avatar.tsx";
|
||||
import { Button } from "./UI/Button.tsx";
|
||||
import { Subtle } from "./UI/Typography/Subtle.tsx";
|
||||
|
||||
interface DeviceInfoPanelProps {
|
||||
isCollapsed: boolean;
|
||||
@@ -70,7 +71,8 @@ export const DeviceInfoPanel = ({
|
||||
id: "voltage",
|
||||
label: t("batteryVoltage.title"),
|
||||
icon: ZapIcon,
|
||||
value: voltage !== undefined
|
||||
value:
|
||||
voltage !== undefined
|
||||
? `${voltage?.toPrecision(3)} V`
|
||||
: t("unknown.notAvailable", "N/A"),
|
||||
},
|
||||
@@ -135,8 +137,7 @@ export const DeviceInfoPanel = ({
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0">
|
||||
</div>
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<div
|
||||
@@ -151,10 +152,7 @@ export const DeviceInfoPanel = ({
|
||||
{deviceInfoItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-2.5 text-sm"
|
||||
>
|
||||
<div key={item.id} className="flex items-center gap-2.5 text-sm">
|
||||
{IconComponent && (
|
||||
<IconComponent
|
||||
size={16}
|
||||
@@ -173,8 +171,7 @@ export const DeviceInfoPanel = ({
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0">
|
||||
</div>
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<div
|
||||
@@ -190,9 +187,7 @@ export const DeviceInfoPanel = ({
|
||||
const Icon = buttonItem.icon;
|
||||
if (buttonItem.render) {
|
||||
return (
|
||||
<Fragment key={buttonItem.id}>
|
||||
{buttonItem.render()}
|
||||
</Fragment>
|
||||
<Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
+11
-17
@@ -1,8 +1,8 @@
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
// Ensure the path is correct for import
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
|
||||
vi.mock("@core/stores/messageStore", () => ({
|
||||
useMessageStore: vi.fn(() => ({
|
||||
@@ -26,9 +26,7 @@ describe("DeleteMessagesDialog", () => {
|
||||
});
|
||||
|
||||
it("calls onOpenChange with false when the close button (X) is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
||||
const closeButton = screen.queryByTestId("dialog-close-button");
|
||||
if (!closeButton) {
|
||||
throw new Error(
|
||||
@@ -41,15 +39,15 @@ describe("DeleteMessagesDialog", () => {
|
||||
});
|
||||
|
||||
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(/This action will clear all message history./))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/This action will clear all message history./),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Clear Messages" }))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear Messages" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the dialog when open is false", () => {
|
||||
@@ -60,18 +58,14 @@ describe("DeleteMessagesDialog", () => {
|
||||
});
|
||||
|
||||
it("calls onOpenChange with false when the dismiss button is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("calls deleteAllMessages and onOpenChange with false when the clear messages button is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Clear Messages" }));
|
||||
expect(mockClearAllMessages).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
|
||||
export interface DeleteMessagesDialogProps {
|
||||
open: boolean;
|
||||
@@ -41,11 +41,7 @@ export const DeleteMessagesDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCloseDialog}
|
||||
name="dismiss"
|
||||
>
|
||||
<Button variant="outline" onClick={handleCloseDialog} name="dismiss">
|
||||
{t("button.dismiss")}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { GenericInput } from "@components/Form/FormInput.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GenericInput } from "@components/Form/FormInput.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Label } from "../UI/Label.tsx";
|
||||
import z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Label } from "../UI/Label.tsx";
|
||||
|
||||
export interface User {
|
||||
longName: string;
|
||||
@@ -76,15 +76,11 @@ export const DeviceNameDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("deviceName.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("deviceName.description")}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("deviceName.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label htmlFor="longName">
|
||||
{t("deviceName.longName")}
|
||||
</Label>
|
||||
<Label htmlFor="longName">{t("deviceName.longName")}</Label>
|
||||
<GenericInput
|
||||
control={control}
|
||||
field={{
|
||||
@@ -104,9 +100,7 @@ export const DeviceNameDialog = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="shortName">
|
||||
{t("deviceName.shortName")}
|
||||
</Label>
|
||||
<Label htmlFor="shortName">{t("deviceName.shortName")}</Label>
|
||||
<GenericInput
|
||||
control={control}
|
||||
field={{
|
||||
@@ -134,10 +128,7 @@ export const DeviceNameDialog = ({
|
||||
>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
name="save"
|
||||
>
|
||||
<Button type="submit" name="save">
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||
import { RebootOTADialog } from "@components/Dialog/RebootOTADialog.tsx";
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
|
||||
export const DialogManager = () => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create, fromBinary } from "@bufbuild/protobuf";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -18,6 +17,7 @@ import { Protobuf } from "@meshtastic/core";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||
|
||||
export interface ImportDialogProps {
|
||||
open: boolean;
|
||||
@@ -25,10 +25,7 @@ export interface ImportDialogProps {
|
||||
loraConfig?: Protobuf.Config.Config_LoRaConfig;
|
||||
}
|
||||
|
||||
export const ImportDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ImportDialogProps) => {
|
||||
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
||||
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
||||
@@ -69,7 +66,7 @@ export const ImportDialog = ({
|
||||
setValidUrl(false);
|
||||
setChannelSet(undefined);
|
||||
}
|
||||
}, [importDialogInput]);
|
||||
}, [importDialogInput, t]);
|
||||
|
||||
const apply = () => {
|
||||
channelSet?.settings.map(
|
||||
@@ -77,7 +74,8 @@ export const ImportDialog = ({
|
||||
connection?.setChannel(
|
||||
create(Protobuf.Channel.ChannelSchema, {
|
||||
index,
|
||||
role: index === 0
|
||||
role:
|
||||
index === 0
|
||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||
settings: ch,
|
||||
@@ -104,9 +102,7 @@ export const ImportDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("import.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("import.description")}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("import.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>{t("import.channelSetUrl")}</Label>
|
||||
@@ -127,25 +123,21 @@ export const ImportDialog = ({
|
||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
/* <Select
|
||||
{/* <Select
|
||||
label="Modem Preset"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.modemPreset}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select> */
|
||||
}
|
||||
</Select> */}
|
||||
</div>
|
||||
{
|
||||
/* <Select
|
||||
{/* <Select
|
||||
label="Region"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.region}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select> */
|
||||
}
|
||||
</Select> */}
|
||||
|
||||
<span className="text-md block font-medium text-text-primary">
|
||||
{t("import.channels")}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -7,9 +10,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../UI/Dialog.tsx";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface LocationResponseDialogProps {
|
||||
location: Types.PacketMetadata<Protobuf.Mesh.Position> | undefined;
|
||||
@@ -26,16 +26,19 @@ export const LocationResponseDialog = ({
|
||||
const { getNode } = useDevice();
|
||||
|
||||
const from = getNode(location?.from ?? 0);
|
||||
const longName = from?.user?.longName ??
|
||||
const longName =
|
||||
from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
const shortName = from?.user?.shortName ??
|
||||
const shortName =
|
||||
from?.user?.shortName ??
|
||||
(from
|
||||
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
|
||||
: t("unknown.shortName"));
|
||||
|
||||
const position = location?.data;
|
||||
|
||||
const hasCoordinates = position &&
|
||||
const hasCoordinates =
|
||||
position &&
|
||||
typeof position.latitudeI === "number" &&
|
||||
typeof position.longitudeI === "number" &&
|
||||
typeof position.altitude === "number";
|
||||
@@ -53,8 +56,7 @@ export const LocationResponseDialog = ({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{hasCoordinates
|
||||
? (
|
||||
{hasCoordinates ? (
|
||||
<div className="ml-5 flex">
|
||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||
<p>
|
||||
@@ -80,8 +82,7 @@ export const LocationResponseDialog = ({
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
) : (
|
||||
// Optional: Show a message if coordinates are not available
|
||||
<p className="text-textPrimary">
|
||||
{t("locationResponse.noCoordinates")}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -8,9 +9,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
export interface ManagedModeDialogProps {
|
||||
open: boolean;
|
||||
@@ -36,7 +36,7 @@ export const ManagedModeDialog = ({
|
||||
<Trans
|
||||
i18nKey="managedMode.description"
|
||||
components={{
|
||||
"bold": <p className="font-bold inline" />,
|
||||
bold: <p className="font-bold inline" />,
|
||||
}}
|
||||
/>
|
||||
</DialogDescription>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
} from "@core/hooks/useBrowserFeatureDetection.ts";
|
||||
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||
@@ -18,6 +14,10 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
} from "@core/hooks/useBrowserFeatureDetection.ts";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "../UI/Typography/Link.tsx";
|
||||
@@ -45,24 +45,23 @@ interface FeatureErrorProps {
|
||||
|
||||
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
|
||||
"Web Bluetooth": {
|
||||
href:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth",
|
||||
},
|
||||
"Web Serial": {
|
||||
href:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
||||
i18nKey: "newDeviceDialog.validation.requiresWebSerial",
|
||||
},
|
||||
"Secure Context": {
|
||||
href:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||
i18nKey: "newDeviceDialog.validation.requiresSecureContext",
|
||||
},
|
||||
};
|
||||
|
||||
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
if (missingFeatures.length === 0) return null;
|
||||
if (missingFeatures.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const browserFeatures = missingFeatures.filter(
|
||||
(feature) => feature !== "Secure Context",
|
||||
@@ -70,9 +69,9 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
const needsSecureContext = missingFeatures.includes("Secure Context");
|
||||
|
||||
const needsFeature =
|
||||
(tabId === "BLE" && browserFeatures.includes("Web Bluetooth"))
|
||||
tabId === "BLE" && browserFeatures.includes("Web Bluetooth")
|
||||
? "Web Bluetooth"
|
||||
: (tabId === "Serial" && browserFeatures.includes("Web Serial"))
|
||||
: tabId === "Serial" && browserFeatures.includes("Web Serial")
|
||||
? "Web Serial"
|
||||
: undefined;
|
||||
|
||||
@@ -97,9 +96,11 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
{needsFeature && needsSecureContext && " "}
|
||||
{needsSecureContext && (
|
||||
<Trans
|
||||
i18nKey={browserFeatures.length > 0
|
||||
i18nKey={
|
||||
browserFeatures.length > 0
|
||||
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
|
||||
: "newDeviceDialog.validation.requiresSecureContext"}
|
||||
: "newDeviceDialog.validation.requiresSecureContext"
|
||||
}
|
||||
components={{
|
||||
"0": (
|
||||
<Link
|
||||
@@ -117,10 +118,7 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NewDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NewDeviceProps) => {
|
||||
export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
|
||||
@@ -135,14 +133,16 @@ export const NewDeviceDialog = ({
|
||||
id: "BLE",
|
||||
label: t("newDeviceDialog.tabBluetooth"),
|
||||
element: BLE,
|
||||
isDisabled: unsupported.includes("Web Bluetooth") ||
|
||||
isDisabled:
|
||||
unsupported.includes("Web Bluetooth") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
{
|
||||
id: "Serial",
|
||||
label: t("newDeviceDialog.tabSerial"),
|
||||
element: Serial,
|
||||
isDisabled: unsupported.includes("Web Serial") ||
|
||||
isDisabled:
|
||||
unsupported.includes("Web Serial") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
];
|
||||
@@ -165,18 +165,10 @@ export const NewDeviceDialog = ({
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id}>
|
||||
<fieldset disabled={tab.isDisabled}>
|
||||
{(tab.id !== "HTTP" &&
|
||||
tab.isDisabled)
|
||||
? (
|
||||
<ErrorMessage
|
||||
missingFeatures={unsupported}
|
||||
tabId={tab.id}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<tab.element
|
||||
closeDialog={() => onOpenChange(false)}
|
||||
/>
|
||||
{tab.id !== "HTTP" && tab.isDisabled ? (
|
||||
<ErrorMessage missingFeatures={unsupported} tabId={tab.id} />
|
||||
) : (
|
||||
<tab.element closeDialog={() => onOpenChange(false)} />
|
||||
)}
|
||||
</fieldset>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { DeviceImage } from "@components/generic/DeviceImage.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Uptime } from "@components/generic/Uptime.tsx";
|
||||
import { toast } from "@core/hooks/useToast.ts";
|
||||
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
|
||||
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@components/UI/Accordion.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -25,7 +16,23 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Separator } from "@components/UI/Seperator.tsx";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@components/UI/Tooltip.tsx";
|
||||
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
|
||||
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
|
||||
import { toast } from "@core/hooks/useToast.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
BellIcon,
|
||||
BellOffIcon,
|
||||
@@ -35,16 +42,8 @@ import {
|
||||
TrashIcon,
|
||||
WaypointsIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@components/UI/Tooltip.tsx";
|
||||
import { Separator } from "@components/UI/Seperator.tsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export interface NodeDetailsDialogProps {
|
||||
open: boolean;
|
||||
@@ -72,21 +71,29 @@ export const NodeDetailsDialog = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
setIsFavoriteState(node?.isFavorite);
|
||||
setIsIgnoredState(node?.isIgnored);
|
||||
}, [node]);
|
||||
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleDirectMessage() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
navigate({ to: `/messages/direct/${node.num}` });
|
||||
setDialogOpen("nodeDetails", false);
|
||||
}
|
||||
|
||||
function handleRequestPosition() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("toast.requestingPosition.title", { ns: "ui" }),
|
||||
@@ -94,13 +101,15 @@ export const NodeDetailsDialog = ({
|
||||
connection?.requestPosition(node.num).then(() =>
|
||||
toast({
|
||||
title: t("toast.positionRequestSent.title", { ns: "ui" }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
function handleTraceroute() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("toast.sendingTraceroute.title", { ns: "ui" }),
|
||||
@@ -108,13 +117,15 @@ export const NodeDetailsDialog = ({
|
||||
connection?.traceRoute(node.num).then(() =>
|
||||
toast({
|
||||
title: t("toast.tracerouteSent.title", { ns: "ui" }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
function handleNodeRemove() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNodeNumToBeRemoved(node?.num);
|
||||
setDialogOpen("nodeRemoval", true);
|
||||
@@ -122,14 +133,18 @@ export const NodeDetailsDialog = ({
|
||||
}
|
||||
|
||||
function handleToggleFavorite() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateFavorite({ nodeNum: node.num, isFavorite: !isFavoriteState });
|
||||
setIsFavoriteState(!isFavoriteState);
|
||||
}
|
||||
|
||||
function handleToggleIgnored() {
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateIgnored({ nodeNum: node.num, isIgnored: !isIgnoredState });
|
||||
setIsIgnoredState(!isIgnoredState);
|
||||
@@ -202,7 +217,7 @@ export const NodeDetailsDialog = ({
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<div className="flex flex-1 justify-start"></div>
|
||||
<div className="flex flex-1 justify-start" />
|
||||
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
@@ -255,7 +270,10 @@ export const NodeDetailsDialog = ({
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.details")}
|
||||
</p>
|
||||
<p>{t("nodeDetails.nodeNumber")}{node.num}</p>
|
||||
<p>
|
||||
{t("nodeDetails.nodeNumber")}
|
||||
{node.num}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.nodeHexPrefix")}
|
||||
{numberToHexUnpadded(node.num)}
|
||||
@@ -268,21 +286,25 @@ export const NodeDetailsDialog = ({
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.lastHeard")}
|
||||
{node.lastHeard === 0
|
||||
? t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
|
||||
: <TimeAgo timestamp={node.lastHeard * 1000} />}
|
||||
{node.lastHeard === 0 ? (
|
||||
t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
|
||||
) : (
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("nodeDetails.hardware")}
|
||||
{(Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
t("unknown.shortName"))
|
||||
.replace(/_/g, " ")}
|
||||
{(
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
t("unknown.shortName")
|
||||
).replace(/_/g, " ")}
|
||||
</p>
|
||||
</div>
|
||||
<DeviceImage
|
||||
className="h-45 w-45 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={Protobuf.Mesh
|
||||
.HardwareModel[node.user?.hwModel ?? 0]}
|
||||
deviceType={
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,11 +315,9 @@ export const NodeDetailsDialog = ({
|
||||
{t("nodeDetails.position")}
|
||||
</p>
|
||||
|
||||
{node.position
|
||||
? (
|
||||
{node.position ? (
|
||||
<>
|
||||
{node.position.latitudeI &&
|
||||
node.position.longitudeI && (
|
||||
{node.position.latitudeI && node.position.longitudeI && (
|
||||
<p>
|
||||
{t("locationResponse.coordinates")}
|
||||
<a
|
||||
@@ -321,8 +341,9 @@ export const NodeDetailsDialog = ({
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: <p>{t("unknown.shortName")}</p>}
|
||||
) : (
|
||||
<p>{t("unknown.shortName")}</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleRequestPosition}
|
||||
name="requestPosition"
|
||||
@@ -338,14 +359,13 @@ export const NodeDetailsDialog = ({
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||
{t("nodeDetails.deviceMetrics")}
|
||||
</p>
|
||||
{deviceMetricsMap.map(
|
||||
(metric) =>
|
||||
metric.value !== undefined && (
|
||||
{deviceMetricsMap
|
||||
.filter((metric) => metric.value !== undefined)
|
||||
.map((metric) => (
|
||||
<p key={metric.key}>
|
||||
{metric.label}: {metric.format(metric.value)}
|
||||
{metric.label}: {metric.format(metric?.value ?? 0)}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
))}
|
||||
{node.deviceMetrics.uptimeSeconds && (
|
||||
<p>
|
||||
{t("nodeDetails.uptime")}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -13,6 +11,8 @@ import { fromByteArray } from "base64-js";
|
||||
import { DownloadIcon, PrinterIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
|
||||
export interface PkiBackupDialogProps {
|
||||
open: boolean;
|
||||
@@ -30,7 +30,9 @@ export const PkiBackupDialog = ({
|
||||
|
||||
const decodeKeyData = React.useCallback(
|
||||
(key: Uint8Array<ArrayBufferLike>) => {
|
||||
if (!key) return "";
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
return fromByteArray(key ?? new Uint8Array(0));
|
||||
},
|
||||
[],
|
||||
@@ -41,20 +43,20 @@ export const PkiBackupDialog = ({
|
||||
}, [setDialogOpen]);
|
||||
|
||||
const renderPrintWindow = React.useCallback(() => {
|
||||
if (!privateKey || !publicKey) return;
|
||||
if (!privateKey || !publicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const printWindow = globalThis.open("", "_blank");
|
||||
if (printWindow) {
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>${
|
||||
t("pkiBackup.header", {
|
||||
<title>${t("pkiBackup.header", {
|
||||
interpolation: { escapeValue: false },
|
||||
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
|
||||
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
|
||||
})
|
||||
}</title>
|
||||
})}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
h1 { font-size: 18px; }
|
||||
@@ -62,13 +64,11 @@ export const PkiBackupDialog = ({
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${
|
||||
t("pkiBackup.header", {
|
||||
<h1>${t("pkiBackup.header", {
|
||||
interpolation: { escapeValue: false },
|
||||
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
|
||||
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
|
||||
})
|
||||
}</h1>
|
||||
})}</h1>
|
||||
<h3>${t("pkiBackup.secureBackup")}</h3>
|
||||
<h3>${t("pkiBackup.publicKey")}</h3>
|
||||
<p>${decodeKeyData(publicKey)}</p>
|
||||
@@ -82,10 +82,12 @@ export const PkiBackupDialog = ({
|
||||
printWindow.print();
|
||||
closeDialog();
|
||||
}
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog, t]);
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]);
|
||||
|
||||
const createDownloadKeyFile = React.useCallback(() => {
|
||||
if (!privateKey || !publicKey) return;
|
||||
if (!privateKey || !publicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decodedPrivateKey = decodeKeyData(privateKey);
|
||||
const decodedPublicKey = decodeKeyData(publicKey);
|
||||
@@ -116,7 +118,7 @@ export const PkiBackupDialog = ({
|
||||
document.body.removeChild(link);
|
||||
closeDialog();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog, t]);
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -124,9 +126,7 @@ export const PkiBackupDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("pkiBackup.secureBackup")}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
|
||||
<DialogDescription>
|
||||
<span className="font-bold break-before-auto">
|
||||
{t("pkiBackup.loseKeysWarning")}
|
||||
|
||||
@@ -34,8 +34,7 @@ export const PkiRegenerateDialog = ({
|
||||
const { t } = useTranslation("dialog");
|
||||
const dialogText = {
|
||||
title: text.title || t("pkiRegenerate.title"),
|
||||
description: text.description ||
|
||||
t("pkiRegenerate.description"),
|
||||
description: text.description || t("pkiRegenerate.description"),
|
||||
button: text.button || t("button.regenerate"),
|
||||
};
|
||||
return (
|
||||
@@ -44,9 +43,7 @@ export const PkiRegenerateDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dialogText.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dialogText.description}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{dialogText.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -14,8 +13,9 @@ import { Label } from "@components/UI/Label.tsx";
|
||||
import { Protobuf, type Types } from "@meshtastic/core";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { QRCode } from "react-qrcode-logo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { QRCode } from "react-qrcode-logo";
|
||||
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||
|
||||
export interface QRDialogProps {
|
||||
open: boolean;
|
||||
@@ -67,9 +67,7 @@ export const QRDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("qr.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("qr.description")}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("qr.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex gap-3 px-4 py-5 sm:p-6">
|
||||
@@ -81,12 +79,10 @@ export const QRDialog = ({
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
|
||||
? t("page.broadcastLabel", { ns: "channels" })
|
||||
: `${
|
||||
t("page.channelIndex", {
|
||||
: `${t("page.channelIndex", {
|
||||
ns: "channels",
|
||||
index: channel.index,
|
||||
})
|
||||
}${channel.index}`}
|
||||
})}${channel.index}`}
|
||||
</Label>
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
@@ -94,9 +90,7 @@ export const QRDialog = ({
|
||||
onChange={() => {
|
||||
if (selectedChannels.includes(channel.index)) {
|
||||
setSelectedChannels(
|
||||
selectedChannels.filter((c) =>
|
||||
c !== channel.index
|
||||
),
|
||||
selectedChannels.filter((c) => c !== channel.index),
|
||||
);
|
||||
} else {
|
||||
setSelectedChannels([
|
||||
@@ -140,10 +134,7 @@ export const QRDialog = ({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Label>{t("qr.sharableUrl")}</Label>
|
||||
<Input
|
||||
value={qrCodeUrl}
|
||||
disabled
|
||||
/>
|
||||
<Input value={qrCodeUrl} disabled />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -10,18 +10,15 @@ import {
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface RebootDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RebootDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RebootDialogProps) => {
|
||||
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
|
||||
@@ -32,12 +29,8 @@ export const RebootDialog = ({
|
||||
<DialogContent>
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("reboot.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("reboot.description")}
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("reboot.title")}</DialogTitle>
|
||||
<DialogDescription>{t("reboot.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Input
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { RebootOTADialog } from "./RebootOTADialog.tsx";
|
||||
import {
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
ClassAttributes,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { JSX } from "react/jsx-runtime";
|
||||
import type { JSX } from "react/jsx-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { RebootOTADialog } from "./RebootOTADialog.tsx";
|
||||
|
||||
const rebootOtaMock = vi.fn();
|
||||
let mockConnection: { rebootOta: (delay: number) => void } | undefined = {
|
||||
@@ -25,10 +25,9 @@ vi.mock("@components/UI/Button.tsx", async () => {
|
||||
return {
|
||||
...actual,
|
||||
Button: (
|
||||
props:
|
||||
& JSX.IntrinsicAttributes
|
||||
& ClassAttributes<HTMLButtonElement>
|
||||
& ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
props: JSX.IntrinsicAttributes &
|
||||
ClassAttributes<HTMLButtonElement> &
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) => <button {...props} />,
|
||||
};
|
||||
});
|
||||
@@ -38,10 +37,9 @@ vi.mock("@components/UI/Input.tsx", async () => {
|
||||
return {
|
||||
...actual,
|
||||
Input: (
|
||||
props:
|
||||
& JSX.IntrinsicAttributes
|
||||
& ClassAttributes<HTMLInputElement>
|
||||
& InputHTMLAttributes<HTMLInputElement>,
|
||||
props: JSX.IntrinsicAttributes &
|
||||
ClassAttributes<HTMLInputElement> &
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
) => <input {...props} />,
|
||||
};
|
||||
});
|
||||
@@ -76,8 +74,9 @@ describe("RebootOTADialog", () => {
|
||||
it("renders dialog with default input value", () => {
|
||||
render(<RebootOTADialog open onOpenChange={() => {}} />);
|
||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||
expect(screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/reboot to ota mode now/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -11,6 +9,8 @@ import {
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface RebootOTADialogProps {
|
||||
@@ -20,9 +20,10 @@ export interface RebootOTADialogProps {
|
||||
|
||||
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
||||
|
||||
export const RebootOTADialog = (
|
||||
{ open, onOpenChange }: RebootOTADialogProps,
|
||||
) => {
|
||||
export const RebootOTADialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RebootOTADialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
||||
@@ -39,13 +40,15 @@ export const RebootOTADialog = (
|
||||
setInputValue(val);
|
||||
|
||||
const parsed = Number(val);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
setTime(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRebootWithTimeout = async () => {
|
||||
if (!connection) return;
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
setIsScheduled(true);
|
||||
|
||||
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
|
||||
@@ -63,7 +66,9 @@ export const RebootOTADialog = (
|
||||
};
|
||||
|
||||
const handleInstantReboot = async () => {
|
||||
if (!connection) return;
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
await connection.rebootOta(DEFAULT_REBOOT_DELAY);
|
||||
onOpenChange(false);
|
||||
@@ -74,12 +79,8 @@ export const RebootOTADialog = (
|
||||
<DialogContent>
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("rebootOta.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("rebootOta.description")}
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("rebootOta.title")}</DialogTitle>
|
||||
<DialogDescription>{t("rebootOta.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-2 p-2 items-center relative">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
|
||||
vi.mock("@core/stores/messageStore");
|
||||
vi.mock("./useRefreshKeysDialog");
|
||||
@@ -12,8 +12,10 @@ const mockUseMessageStore = vi.mocked(useMessageStore);
|
||||
const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
|
||||
|
||||
const getInitialState = () =>
|
||||
useDeviceStore.getInitialState?.() ??
|
||||
{ devices: new Map(), remoteDevices: new Map() };
|
||||
useDeviceStore.getInitialState?.() ?? {
|
||||
devices: new Map(),
|
||||
remoteDevices: new Map(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useDeviceStore.setState(getInitialState(), true);
|
||||
@@ -31,7 +33,9 @@ test("does not render dialog if no error exists for active chat", () => {
|
||||
useDeviceStore.getState().addDevice(deviceId);
|
||||
|
||||
const currentDeviceState = useDeviceStore.getState().getDevice(deviceId);
|
||||
if (!currentDeviceState) throw new Error("Device not found");
|
||||
if (!currentDeviceState) {
|
||||
throw new Error("Device not found");
|
||||
}
|
||||
|
||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
||||
mockUseRefreshKeysDialog.mockReturnValue({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -5,21 +6,21 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
|
||||
export interface RefreshKeysDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RefreshKeysDialog = (
|
||||
{ open, onOpenChange }: RefreshKeysDialogProps,
|
||||
) => {
|
||||
export const RefreshKeysDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RefreshKeysDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { activeChat } = useMessageStore();
|
||||
const { nodeErrors, getNode } = useDevice();
|
||||
@@ -40,9 +41,9 @@ export const RefreshKeysDialog = (
|
||||
}),
|
||||
description: `${t("refreshKeys.description.unableToSendDmPrefix")}${
|
||||
nodeWithError?.user?.longName ?? ""
|
||||
} (${nodeWithError?.user?.shortName ?? ""})${
|
||||
t("refreshKeys.description.keyMismatchReasonSuffix")
|
||||
}`,
|
||||
} (${nodeWithError?.user?.shortName ?? ""})${t(
|
||||
"refreshKeys.description.keyMismatchReasonSuffix",
|
||||
)}`,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,9 +70,7 @@ export const RefreshKeysDialog = (
|
||||
<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>
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function useRefreshKeysDialog() {
|
||||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } =
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -11,7 +9,9 @@ import {
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||
|
||||
export interface RemoveNodeDialogProps {
|
||||
open: boolean;
|
||||
@@ -38,9 +38,7 @@ export const RemoveNodeDialog = ({
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("removeNode.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("removeNode.description")}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("removeNode.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="gap-4">
|
||||
<form onSubmit={onSubmit}>
|
||||
|
||||
@@ -10,18 +10,15 @@ import {
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { ClockIcon, PowerIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface ShutdownDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const ShutdownDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ShutdownDialogProps) => {
|
||||
export const ShutdownDialog = ({ open, onOpenChange }: ShutdownDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
|
||||
@@ -32,12 +29,8 @@ export const ShutdownDialog = ({
|
||||
<DialogContent>
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("shutdown.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("shutdown.description")}
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("shutdown.title")}</DialogTitle>
|
||||
<DialogDescription>{t("shutdown.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-2 p-4">
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -7,11 +12,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../UI/Dialog.tsx";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
|
||||
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface TracerouteResponseDialogProps {
|
||||
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
|
||||
@@ -31,9 +31,11 @@ export const TracerouteResponseDialog = ({
|
||||
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
|
||||
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
|
||||
const from = getNode(traceroute?.from ?? 0);
|
||||
const fromLongName = from?.user?.longName ??
|
||||
const fromLongName =
|
||||
from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
const fromShortName = from?.user?.shortName ??
|
||||
const fromShortName =
|
||||
from?.user?.shortName ??
|
||||
(from
|
||||
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
|
||||
: t("unknown.shortName"));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -8,11 +10,9 @@ import {
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useState } from "react";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface RouterRoleDialogProps {
|
||||
@@ -20,9 +20,10 @@ export interface RouterRoleDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const UnsafeRolesDialog = (
|
||||
{ open, onOpenChange }: RouterRoleDialogProps,
|
||||
) => {
|
||||
export const UnsafeRolesDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RouterRoleDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [confirmState, setConfirmState] = useState(false);
|
||||
const { setDialogOpen } = useDevice();
|
||||
|
||||
+12
-15
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
UNSAFE_ROLES,
|
||||
useUnsafeRolesDialog,
|
||||
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
@@ -7,18 +13,11 @@ import {
|
||||
type Mock,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import {
|
||||
UNSAFE_ROLES,
|
||||
useUnsafeRolesDialog,
|
||||
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||
const actual = await importOriginal<
|
||||
typeof import("@tanstack/react-router")
|
||||
>();
|
||||
const actual =
|
||||
await importOriginal<typeof import("@tanstack/react-router")>();
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
@@ -75,9 +74,8 @@ describe("useUnsafeRolesDialog", () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const safeRole = "SAFE_ROLE";
|
||||
|
||||
const validationResult = await result.current.validateRoleSelection(
|
||||
safeRole,
|
||||
);
|
||||
const validationResult =
|
||||
await result.current.validateRoleSelection(safeRole);
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
|
||||
@@ -151,9 +149,8 @@ describe("useUnsafeRolesDialog", () => {
|
||||
mockDevice.setDialogOpen.mockClear();
|
||||
(eventBus.on as Mock).mockClear();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
unsafeRole,
|
||||
);
|
||||
const validationPromise =
|
||||
result.current.validateRoleSelection(unsafeRole);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback } from "react";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const UNSAFE_ROLES = ["ROUTER", "ROUTER_LATE", "REPEATER"];
|
||||
export type UnsafeRole = typeof UNSAFE_ROLES[number];
|
||||
export type UnsafeRole = (typeof UNSAFE_ROLES)[number];
|
||||
|
||||
export const useUnsafeRolesDialog = () => {
|
||||
const { setDialogOpen } = useDevice();
|
||||
@@ -21,9 +21,11 @@ export const useUnsafeRolesDialog = () => {
|
||||
setDialogOpen("unsafeRoles", true);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const handleResponse = (
|
||||
{ action }: { action: "confirm" | "dismiss" },
|
||||
) => {
|
||||
const handleResponse = ({
|
||||
action,
|
||||
}: {
|
||||
action: "confirm" | "dismiss";
|
||||
}) => {
|
||||
eventBus.off("dialog:unsafeRoles", handleResponse);
|
||||
resolve(action === "confirm");
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import {
|
||||
DynamicFormField,
|
||||
type FieldProps,
|
||||
} from "@components/Form/DynamicFormField.tsx";
|
||||
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { dotPaths } from "@core/utils/dotPath.ts";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
type Control,
|
||||
type DefaultValues,
|
||||
@@ -13,16 +18,11 @@ import {
|
||||
get,
|
||||
type Path,
|
||||
type SubmitHandler,
|
||||
useForm,
|
||||
type UseFormReturn,
|
||||
useForm,
|
||||
} from "react-hook-form";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { ZodType } from "zod/v4";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { dotPaths } from "@core/utils/dotPath.ts";
|
||||
import type { ZodType } from "zod/v4";
|
||||
|
||||
interface DisabledBy<T> {
|
||||
fieldName: Path<T>;
|
||||
@@ -86,16 +86,9 @@ export function DynamicForm<T extends FieldValues>({
|
||||
formId,
|
||||
}: DynamicFormProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
addError,
|
||||
removeError,
|
||||
} = useAppStore();
|
||||
const { addError, removeError } = useAppStore();
|
||||
|
||||
let methods = propMethods;
|
||||
if (!methods) {
|
||||
methods = useForm<
|
||||
T
|
||||
>({
|
||||
const internalMethods = useForm<T>({
|
||||
mode: "onChange",
|
||||
defaultValues: defaultValues,
|
||||
resolver: validationSchema
|
||||
@@ -105,22 +98,22 @@ export function DynamicForm<T extends FieldValues>({
|
||||
resetOptions: { keepDefaultValues: true },
|
||||
values,
|
||||
});
|
||||
}
|
||||
|
||||
const methods = propMethods ?? internalMethods;
|
||||
|
||||
const { handleSubmit, control, getValues, formState, getFieldState } =
|
||||
methods;
|
||||
|
||||
useEffect(() => {
|
||||
if (!propMethods) {
|
||||
onFormInit?.(methods);
|
||||
onFormInit?.(internalMethods);
|
||||
}
|
||||
}, [onFormInit, propMethods, methods]);
|
||||
}, [onFormInit, propMethods, internalMethods]);
|
||||
|
||||
useEffect(() => {
|
||||
const errorKeys = Object.keys(formState.errors);
|
||||
if (formId) {
|
||||
if (
|
||||
errorKeys.length === 0
|
||||
) {
|
||||
if (errorKeys.length === 0) {
|
||||
dotPaths(getValues()).forEach((key) => {
|
||||
removeError(key);
|
||||
});
|
||||
@@ -132,19 +125,27 @@ export function DynamicForm<T extends FieldValues>({
|
||||
addError(formId, "");
|
||||
}
|
||||
}
|
||||
}, [formState.errors]);
|
||||
}, [formState.errors, addError, formId, getValues, removeError]);
|
||||
|
||||
const isDisabled = (
|
||||
disabledBy?: DisabledBy<T>[],
|
||||
disabled?: boolean,
|
||||
): boolean => {
|
||||
if (disabled) return true;
|
||||
if (!disabledBy) return false;
|
||||
if (disabled) {
|
||||
return true;
|
||||
}
|
||||
if (!disabledBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return disabledBy.some((field) => {
|
||||
const value = getValues(field.fieldName);
|
||||
if (value === "always") return true;
|
||||
if (typeof value === "boolean") return field.invert ? value : !value;
|
||||
if (value === "always") {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return field.invert ? value : !value;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return field.invert
|
||||
? field.selector !== value
|
||||
@@ -181,14 +182,16 @@ export function DynamicForm<T extends FieldValues>({
|
||||
fieldName={field.name}
|
||||
description={field.description}
|
||||
valid={!error}
|
||||
validationText={error
|
||||
validationText={
|
||||
error
|
||||
? String(
|
||||
t([`formValidation.${error.type}`, error.message], {
|
||||
returnObjects: false,
|
||||
...error.params,
|
||||
}),
|
||||
)
|
||||
: ""}
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<DynamicFormField
|
||||
field={field}
|
||||
@@ -203,11 +206,7 @@ export function DynamicForm<T extends FieldValues>({
|
||||
</div>
|
||||
))}
|
||||
{hasSubmitButton && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
disabled={!formState.isValid}
|
||||
>
|
||||
<Button type="submit" variant="outline" disabled={!formState.isValid}>
|
||||
{t("button.submit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
type MultiSelectFieldProps,
|
||||
MultiSelectInput,
|
||||
} from "./FormMultiSelect.tsx";
|
||||
import {
|
||||
GenericInput,
|
||||
type InputFieldProps,
|
||||
@@ -19,6 +15,10 @@ import {
|
||||
ToggleInput,
|
||||
} from "@components/Form/FormToggle.tsx";
|
||||
import type { Control, FieldValues } from "react-hook-form";
|
||||
import {
|
||||
type MultiSelectFieldProps,
|
||||
MultiSelectInput,
|
||||
} from "./FormMultiSelect.tsx";
|
||||
|
||||
export type FieldProps<T> =
|
||||
| InputFieldProps<T>
|
||||
|
||||
@@ -58,7 +58,9 @@ export function GenericInput<T extends FieldValues>({
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.inputChange) field.inputChange(e);
|
||||
if (field.inputChange) {
|
||||
field.inputChange(e);
|
||||
}
|
||||
|
||||
controllerField.onChange(
|
||||
field.type === "number"
|
||||
@@ -76,14 +78,17 @@ export function GenericInput<T extends FieldValues>({
|
||||
<Input
|
||||
type={field.type}
|
||||
step={field.properties?.step}
|
||||
value={field.type === "number"
|
||||
value={
|
||||
field.type === "number"
|
||||
? String(controllerField.value)
|
||||
: controllerField.value}
|
||||
: controllerField.value
|
||||
}
|
||||
id={field.name}
|
||||
onChange={handleInputChange}
|
||||
showCopyButton={field.properties?.showCopyButton}
|
||||
showPasswordToggle={field.properties?.showPasswordToggle ||
|
||||
field.type === "password"}
|
||||
showPasswordToggle={
|
||||
field.properties?.showPasswordToggle || field.type === "password"
|
||||
}
|
||||
className={field.properties?.className}
|
||||
{...restProperties}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import type { FLAGS_CONFIG } from "@core/hooks/usePositionFlags.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import type { FieldValues } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { FLAGS_CONFIG } from "@core/hooks/usePositionFlags.ts";
|
||||
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "multiSelect";
|
||||
@@ -15,9 +15,7 @@ export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
isChecked: (name: string) => boolean;
|
||||
value: string[];
|
||||
properties: BaseFormBuilderProps<T>["properties"] & {
|
||||
enumValue:
|
||||
| { [s: string]: string | number }
|
||||
| typeof FLAGS_CONFIG;
|
||||
enumValue: { [s: string]: string | number } | typeof FLAGS_CONFIG;
|
||||
formatEnumName?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -39,7 +37,7 @@ export function MultiSelectInput<T extends FieldValues>({
|
||||
([key, configOrValue]) => {
|
||||
if (isNewConfigStructure) {
|
||||
const config =
|
||||
configOrValue as typeof FLAGS_CONFIG[keyof typeof FLAGS_CONFIG];
|
||||
configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
|
||||
return {
|
||||
key,
|
||||
display: t(config.i18nKey),
|
||||
|
||||
@@ -2,11 +2,11 @@ import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import type { ButtonVariant } from "../UI/Button.tsx";
|
||||
import { Generator } from "@components/UI/Generator.tsx";
|
||||
import { Controller, type FieldValues, useFormContext } from "react-hook-form";
|
||||
import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, type FieldValues, useFormContext } from "react-hook-form";
|
||||
import type { ButtonVariant } from "../UI/Button.tsx";
|
||||
|
||||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "passwordGenerator";
|
||||
@@ -38,24 +38,22 @@ export function PasswordGenerator<T extends FieldValues>({
|
||||
|
||||
useEffect(() => {
|
||||
trigger(field.name);
|
||||
}, [field.devicePSKBitCount, field.name, trigger]);
|
||||
}, [field.name, trigger]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
render={(
|
||||
{
|
||||
field: { value, onChange, ...rest },
|
||||
},
|
||||
) => (
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Generator
|
||||
type={field.hide && !isVisible ? "password" : "text"}
|
||||
id={field.id}
|
||||
devicePSKBitCount={field.devicePSKBitCount}
|
||||
bits={field.bits}
|
||||
inputChange={(e) => {
|
||||
if (field.inputChange) field.inputChange(e);
|
||||
if (field.inputChange) {
|
||||
field.inputChange(e);
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
selectChange={field.selectChange ?? (() => {})}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/UI/Select.tsx";
|
||||
import { type FieldValues, useController } from "react-hook-form";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { type FieldValues, useController } from "react-hook-form";
|
||||
|
||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "select";
|
||||
@@ -73,10 +73,14 @@ export function SelectInput<T extends FieldValues>({
|
||||
|
||||
if (field.validate) {
|
||||
const isValid = await field.validate(selectedKey);
|
||||
if (!isValid) return;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.selectChange) field.selectChange(newValue, selectedKey);
|
||||
if (field.selectChange) {
|
||||
field.selectChange(newValue, selectedKey);
|
||||
}
|
||||
onChange(Number.parseInt(newValue));
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import type {
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "toggle";
|
||||
@@ -22,9 +22,7 @@ export function ToggleInput<T extends FieldValues>({
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
render={(
|
||||
{ field: { value, onChange, ...rest } },
|
||||
) => (
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(v) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ZodType } from "zod/v4";
|
||||
import type {
|
||||
FieldError,
|
||||
FieldValues,
|
||||
@@ -6,6 +5,7 @@ import type {
|
||||
ResolverOptions,
|
||||
ResolverResult,
|
||||
} from "react-hook-form";
|
||||
import type { ZodType } from "zod/v4";
|
||||
|
||||
export function createZodResolver<T extends FieldValues>(
|
||||
schema: ZodType<T, unknown>,
|
||||
@@ -32,7 +32,8 @@ export function createZodResolver<T extends FieldValues>(
|
||||
const { path, code, message, ...params } = issue;
|
||||
const key = path.join(".");
|
||||
|
||||
const suffix = "format" in params
|
||||
const suffix =
|
||||
"format" in params
|
||||
? params.format
|
||||
: "origin" in params
|
||||
? params.origin
|
||||
@@ -40,10 +41,9 @@ export function createZodResolver<T extends FieldValues>(
|
||||
? params.expected
|
||||
: "";
|
||||
|
||||
const newCode = code.replace(
|
||||
/_([a-z])/g,
|
||||
(_, char) => char.toUpperCase(),
|
||||
) + (suffix ? `.${suffix}` : "");
|
||||
const newCode =
|
||||
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) +
|
||||
(suffix ? `.${suffix}` : "");
|
||||
|
||||
const fieldError: FieldError & { params?: Record<string, unknown> } = {
|
||||
type: newCode,
|
||||
|
||||
@@ -11,6 +11,5 @@ export const KeyBackupReminder = () => {
|
||||
onAccept: () => setDialogOpen("pkiBackup", true),
|
||||
enabled: true,
|
||||
});
|
||||
// deno-lint-ignore jsx-no-useless-fragment
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Check, Languages } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LangCode, supportedLanguages } from "@app/i18n-config.ts";
|
||||
import { type LangCode, supportedLanguages } from "@app/i18n-config.ts";
|
||||
import useLang from "@core/hooks/useLang.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Check, Languages } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "./UI/Button.tsx";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -9,8 +12,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "./UI/DropdownMenu.tsx";
|
||||
import { Subtle } from "./UI/Typography/Subtle.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Button } from "./UI/Button.tsx";
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
disableHover?: boolean;
|
||||
@@ -22,9 +23,12 @@ export default function LanguageSwitcher({
|
||||
const { i18n } = useTranslation("ui");
|
||||
const { set: setLanguage, currentLanguage } = useLang();
|
||||
|
||||
const handleLanguageChange = async (languageCode: LangCode) => {
|
||||
const handleLanguageChange = useCallback(
|
||||
async (languageCode: LangCode) => {
|
||||
await setLanguage(languageCode, true);
|
||||
};
|
||||
},
|
||||
[setLanguage],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { useTheme } from "@core/hooks/useTheme.ts";
|
||||
import { useEffect, useRef } from "react";
|
||||
import MapGl, {
|
||||
AttributionControl,
|
||||
type MapRef,
|
||||
NavigationControl,
|
||||
ScaleControl,
|
||||
} from "react-map-gl/maplibre";
|
||||
import { useTheme } from "@core/hooks/useTheme.ts";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface MapProps {
|
||||
children?: React.ReactNode;
|
||||
onLoad?: (map: MapRef) => void;
|
||||
}
|
||||
|
||||
export const Map = ({ children, onLoad }: MapProps) => {
|
||||
export const BaseMap = ({ children, onLoad }: MapProps) => {
|
||||
const { theme } = useTheme();
|
||||
const darkMode = theme === "dark";
|
||||
const mapRef = useRef<MapRef | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (map && onLoad) onLoad(map);
|
||||
if (map && onLoad) {
|
||||
onLoad(map);
|
||||
}
|
||||
}, [onLoad]);
|
||||
|
||||
return (
|
||||
@@ -45,14 +47,12 @@ export const Map = ({ children, onLoad }: MapProps) => {
|
||||
}}
|
||||
/>
|
||||
{/* { Disabled for now until we can use i18n for the geolocate control} */}
|
||||
{
|
||||
/* <GeolocateControl
|
||||
{/* <GeolocateControl
|
||||
position="top-right"
|
||||
i18nIsDynamicList
|
||||
positionOptions={{ enableHighAccuracy: true }}
|
||||
trackUserLocation
|
||||
/> */
|
||||
}
|
||||
/> */}
|
||||
<NavigationControl position="top-right" showCompass={false} />
|
||||
<ScaleControl />
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { makeChannelSchema } from "@app/validation/channel.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -8,8 +9,7 @@ import { fromByteArray, toByteArray } from "base64-js";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||
import { infer as zodInfer } from "zod/v4";
|
||||
import type { infer as zodInfer } from "zod/v4";
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel.Channel;
|
||||
@@ -20,9 +20,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
const { t } = useTranslation(["channels", "ui", "dialog"]);
|
||||
const { toast } = useToast();
|
||||
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [pass, setPass] = useState<string>(
|
||||
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
);
|
||||
@@ -30,12 +29,9 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
channel?.settings?.psk.length ?? 16,
|
||||
);
|
||||
|
||||
const ChannelValidationSchema = useMemo(
|
||||
() => {
|
||||
const ChannelValidationSchema = useMemo(() => {
|
||||
return makeChannelSchema(byteCount);
|
||||
},
|
||||
[byteCount],
|
||||
);
|
||||
}, [byteCount]);
|
||||
|
||||
type ChannelValidation = zodInfer<typeof ChannelValidationSchema>;
|
||||
|
||||
@@ -52,10 +48,12 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
},
|
||||
});
|
||||
connection?.setChannel(channel).then(() => {
|
||||
console.debug(t("toast.savedChannel.title", {
|
||||
console.debug(
|
||||
t("toast.savedChannel.title", {
|
||||
ns: "ui",
|
||||
channelName: channel.settings?.name,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: t("toast.savedChannel.title", {
|
||||
ns: "ui",
|
||||
@@ -127,7 +125,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
disabled: channel.index === 0,
|
||||
description: t("role.description"),
|
||||
properties: {
|
||||
enumValue: channel.index === 0
|
||||
enumValue:
|
||||
channel.index === 0
|
||||
? { [t("role.options.primary")]: 1 }
|
||||
: {
|
||||
[t("role.options.disabled")]: 0,
|
||||
@@ -182,78 +181,35 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
label: t("positionPrecision.label"),
|
||||
description: t("positionPrecision.description"),
|
||||
properties: {
|
||||
enumValue: config.display?.units === 0
|
||||
enumValue:
|
||||
config.display?.units === 0
|
||||
? {
|
||||
[t("positionPrecision.options.none")]: 0,
|
||||
[
|
||||
t("positionPrecision.options.metric_km23")
|
||||
]: 10,
|
||||
[
|
||||
t("positionPrecision.options.metric_km12")
|
||||
]: 11,
|
||||
[
|
||||
t("positionPrecision.options.metric_km5_8")
|
||||
]: 12,
|
||||
[
|
||||
t("positionPrecision.options.metric_km2_9")
|
||||
]: 13,
|
||||
[
|
||||
t("positionPrecision.options.metric_km1_5")
|
||||
]: 14,
|
||||
[
|
||||
t("positionPrecision.options.metric_m700")
|
||||
]: 15,
|
||||
[
|
||||
t("positionPrecision.options.metric_m350")
|
||||
]: 16,
|
||||
[
|
||||
t("positionPrecision.options.metric_m200")
|
||||
]: 17,
|
||||
[
|
||||
t("positionPrecision.options.metric_m90")
|
||||
]: 18,
|
||||
[
|
||||
t("positionPrecision.options.metric_m50")
|
||||
]: 19,
|
||||
[
|
||||
t("positionPrecision.options.precise")
|
||||
]: 32,
|
||||
[t("positionPrecision.options.metric_km23")]: 10,
|
||||
[t("positionPrecision.options.metric_km12")]: 11,
|
||||
[t("positionPrecision.options.metric_km5_8")]: 12,
|
||||
[t("positionPrecision.options.metric_km2_9")]: 13,
|
||||
[t("positionPrecision.options.metric_km1_5")]: 14,
|
||||
[t("positionPrecision.options.metric_m700")]: 15,
|
||||
[t("positionPrecision.options.metric_m350")]: 16,
|
||||
[t("positionPrecision.options.metric_m200")]: 17,
|
||||
[t("positionPrecision.options.metric_m90")]: 18,
|
||||
[t("positionPrecision.options.metric_m50")]: 19,
|
||||
[t("positionPrecision.options.precise")]: 32,
|
||||
}
|
||||
: {
|
||||
[t("positionPrecision.options.none")]: 0,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi15")
|
||||
]: 10,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi7_3")
|
||||
]: 11,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi3_6")
|
||||
]: 12,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi1_8")
|
||||
]: 13,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi0_9")
|
||||
]: 14,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi0_5")
|
||||
]: 15,
|
||||
[
|
||||
t("positionPrecision.options.imperial_mi0_2")
|
||||
]: 16,
|
||||
[
|
||||
t("positionPrecision.options.imperial_ft600")
|
||||
]: 17,
|
||||
[
|
||||
t("positionPrecision.options.imperial_ft300")
|
||||
]: 18,
|
||||
[
|
||||
t("positionPrecision.options.imperial_ft150")
|
||||
]: 19,
|
||||
[
|
||||
t("positionPrecision.options.precise")
|
||||
]: 32,
|
||||
[t("positionPrecision.options.imperial_mi15")]: 10,
|
||||
[t("positionPrecision.options.imperial_mi7_3")]: 11,
|
||||
[t("positionPrecision.options.imperial_mi3_6")]: 12,
|
||||
[t("positionPrecision.options.imperial_mi1_8")]: 13,
|
||||
[t("positionPrecision.options.imperial_mi0_9")]: 14,
|
||||
[t("positionPrecision.options.imperial_mi0_5")]: 15,
|
||||
[t("positionPrecision.options.imperial_mi0_2")]: 16,
|
||||
[t("positionPrecision.options.imperial_ft600")]: 17,
|
||||
[t("positionPrecision.options.imperial_ft300")]: 18,
|
||||
[t("positionPrecision.options.imperial_ft150")]: 19,
|
||||
[t("positionPrecision.options.precise")]: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type BluetoothValidation,
|
||||
BluetoothValidationSchema,
|
||||
@@ -8,14 +9,16 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface BluetoothConfigProps {
|
||||
onFormInit: DynamicFormFormInit<BluetoothValidation>;
|
||||
}
|
||||
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
|
||||
useWaitForConfig({ configCase: "bluetooth" });
|
||||
|
||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import {
|
||||
useDevice,
|
||||
ValidConfigType,
|
||||
ValidModuleConfigType,
|
||||
} from "@core/stores/deviceStore.ts";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ConfigSuspender({
|
||||
configCase,
|
||||
moduleConfigCase,
|
||||
children,
|
||||
}: {
|
||||
configCase?: ValidConfigType;
|
||||
moduleConfigCase?: ValidModuleConfigType;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { config, moduleConfig } = useDevice();
|
||||
|
||||
let cfg = undefined;
|
||||
if (configCase) {
|
||||
cfg = config[configCase];
|
||||
} else if (moduleConfigCase) {
|
||||
cfg = moduleConfig[moduleConfigCase];
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
|
||||
const [ready, setReady] = useState(() => cfg !== undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (cfg !== undefined) setReady(true);
|
||||
}, [cfg]);
|
||||
|
||||
if (!ready) throw new Promise(() => {}); // triggers suspense fallback
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type DeviceValidation,
|
||||
DeviceValidationSchema,
|
||||
} from "@app/validation/config/device.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DeviceConfigProps {
|
||||
onFormInit: DynamicFormFormInit<DeviceValidation>;
|
||||
}
|
||||
export const Device = ({ onFormInit }: DeviceConfigProps) => {
|
||||
useWaitForConfig({ configCase: "device" });
|
||||
|
||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
@@ -113,8 +116,8 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
|
||||
properties: {
|
||||
fieldLength: {
|
||||
max: 64,
|
||||
currentValueLength: getEffectiveConfig("device")?.tzdef
|
||||
?.length,
|
||||
currentValueLength:
|
||||
getEffectiveConfig("device")?.tzdef?.length,
|
||||
showCharacterCount: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type DisplayValidation,
|
||||
DisplayValidationSchema,
|
||||
@@ -8,14 +9,15 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface DisplayConfigProps {
|
||||
onFormInit: DynamicFormFormInit<DisplayValidation>;
|
||||
}
|
||||
export const Display = ({ onFormInit }: DisplayConfigProps) => {
|
||||
useWaitForConfig({ configCase: "display" });
|
||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type LoRaValidation,
|
||||
LoRaValidationSchema,
|
||||
@@ -8,14 +9,16 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface LoRaConfigProps {
|
||||
onFormInit: DynamicFormFormInit<LoRaValidation>;
|
||||
}
|
||||
export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
|
||||
useWaitForConfig({ configCase: "lora" });
|
||||
|
||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type NetworkValidation,
|
||||
NetworkValidationSchema,
|
||||
@@ -8,18 +9,20 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import {
|
||||
convertIntToIpAddress,
|
||||
convertIpAddressToInt,
|
||||
} from "@core/utils/ip.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface NetworkConfigProps {
|
||||
onFormInit: DynamicFormFormInit<NetworkValidation>;
|
||||
}
|
||||
export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
||||
useWaitForConfig({ configCase: "network" });
|
||||
|
||||
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
@@ -72,10 +75,12 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
||||
),
|
||||
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
|
||||
},
|
||||
enabledProtocols: config.network?.enabledProtocols ??
|
||||
enabledProtocols:
|
||||
config.network?.enabledProtocols ??
|
||||
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
|
||||
}}
|
||||
values={{
|
||||
values={
|
||||
{
|
||||
...networkConfig,
|
||||
ipv4Config: {
|
||||
ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0),
|
||||
@@ -87,9 +92,11 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
|
||||
),
|
||||
dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0),
|
||||
},
|
||||
enabledProtocols: networkConfig?.enabledProtocols ??
|
||||
enabledProtocols:
|
||||
networkConfig?.enabledProtocols ??
|
||||
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
|
||||
} as NetworkValidation}
|
||||
} as NetworkValidation
|
||||
}
|
||||
fieldGroups={[
|
||||
{
|
||||
label: t("network.title"),
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
type FlagName,
|
||||
usePositionFlags,
|
||||
} from "@core/hooks/usePositionFlags.ts";
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type PositionValidation,
|
||||
PositionValidationSchema,
|
||||
@@ -11,16 +8,22 @@ import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import {
|
||||
type FlagName,
|
||||
usePositionFlags,
|
||||
} from "@core/hooks/usePositionFlags.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface PositionConfigProps {
|
||||
onFormInit: DynamicFormFormInit<PositionValidation>;
|
||||
}
|
||||
export const Position = ({ onFormInit }: PositionConfigProps) => {
|
||||
useWaitForConfig({ configCase: "position" });
|
||||
|
||||
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type PowerValidation,
|
||||
PowerValidationSchema,
|
||||
@@ -8,14 +9,16 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface PowerConfigProps {
|
||||
onFormInit: DynamicFormFormInit<PowerValidation>;
|
||||
}
|
||||
export const Power = ({ onFormInit }: PowerConfigProps) => {
|
||||
useWaitForConfig({ configCase: "power" });
|
||||
|
||||
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
|
||||
useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
||||
import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type ParsedSecurity,
|
||||
type RawSecurity,
|
||||
RawSecuritySchema,
|
||||
} from "@app/validation/config/security.ts";
|
||||
import { useEffect, useState } from "react";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { type DefaultValues, useForm } from "react-hook-form";
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SecurityConfigProps {
|
||||
onFormInit: DynamicFormFormInit<RawSecurity>;
|
||||
}
|
||||
export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
||||
useWaitForConfig({ configCase: "security" });
|
||||
|
||||
const {
|
||||
config,
|
||||
setWorkingConfig,
|
||||
@@ -43,19 +46,11 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
||||
privateKey: fromByteArray(
|
||||
securityConfig?.privateKey ?? new Uint8Array(0),
|
||||
),
|
||||
publicKey: fromByteArray(
|
||||
securityConfig?.publicKey ?? new Uint8Array(0),
|
||||
),
|
||||
publicKey: fromByteArray(securityConfig?.publicKey ?? new Uint8Array(0)),
|
||||
adminKey: [
|
||||
fromByteArray(
|
||||
securityConfig?.adminKey?.at(0) ?? new Uint8Array(0),
|
||||
),
|
||||
fromByteArray(
|
||||
securityConfig?.adminKey?.at(1) ?? new Uint8Array(0),
|
||||
),
|
||||
fromByteArray(
|
||||
securityConfig?.adminKey?.at(2) ?? new Uint8Array(0),
|
||||
),
|
||||
fromByteArray(securityConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
|
||||
fromByteArray(securityConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
|
||||
fromByteArray(securityConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -73,15 +68,15 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
||||
onFormInit?.(formMethods);
|
||||
}, [onFormInit, formMethods]);
|
||||
|
||||
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [managedModeDialogOpen, setManagedModeDialogOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [managedModeDialogOpen, setManagedModeDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const onSubmit = (data: RawSecurity) => {
|
||||
if (!formState.isReady) return;
|
||||
if (!formState.isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: ParsedSecurity = {
|
||||
...data,
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
|
||||
export const BLE = (
|
||||
{ closeDialog }: TabElementProps,
|
||||
) => {
|
||||
export const BLE = ({ closeDialog }: TabElementProps) => {
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
@@ -77,10 +75,12 @@ export const BLE = (
|
||||
if (exists === -1) {
|
||||
setBleDevices(bleDevices.concat(device));
|
||||
}
|
||||
}).catch((error: Error) => {
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error("Error requesting device:", error);
|
||||
setConnectionInProgress(false);
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
setConnectionInProgress(false);
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@core/stores/appStore.ts", () => ({
|
||||
@@ -35,8 +35,9 @@ describe("HTTP Component", () => {
|
||||
render(<HTTP closeDialog={vi.fn()} />);
|
||||
expect(screen.getByText("IP Address/Hostname")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Use HTTPS")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeInTheDocument();
|
||||
});
|
||||
@@ -45,8 +46,9 @@ describe("HTTP Component", () => {
|
||||
render(<HTTP closeDialog={vi.fn()} />);
|
||||
const inputField = screen.getByRole("textbox");
|
||||
fireEvent.change(inputField, { target: { value: "meshtastic.local" } });
|
||||
expect(screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("toggles HTTPS switch and updates prefix", () => {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { TabElementProps } from "@components/Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useController, useForm } from "react-hook-form";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface FormData {
|
||||
@@ -21,9 +21,7 @@ interface FormData {
|
||||
tls: boolean;
|
||||
}
|
||||
|
||||
export const HTTP = (
|
||||
{ closeDialog }: TabElementProps,
|
||||
) => {
|
||||
export const HTTP = ({ closeDialog }: TabElementProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const isURLHTTPS = location.protocol === "https:";
|
||||
@@ -39,7 +37,7 @@ export const HTTP = (
|
||||
)
|
||||
? "meshtastic.local"
|
||||
: globalThis.location.host,
|
||||
tls: isURLHTTPS ? true : false,
|
||||
tls: !!isURLHTTPS,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,9 +45,10 @@ export const HTTP = (
|
||||
field: { value: tlsValue, onChange: setTLS },
|
||||
} = useController({ name: "tls", control });
|
||||
|
||||
const [connectionError, setConnectionError] = useState<
|
||||
{ host: string; secure: boolean } | null
|
||||
>(null);
|
||||
const [connectionError, setConnectionError] = useState<{
|
||||
host: string;
|
||||
secure: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setConnectionInProgress(true);
|
||||
@@ -85,9 +84,11 @@ export const HTTP = (
|
||||
<div>
|
||||
<Label>{t("newDeviceDialog.httpConnection.label")}</Label>
|
||||
<Input
|
||||
prefix={tlsValue
|
||||
prefix={
|
||||
tlsValue
|
||||
? `${t("newDeviceDialog.https")}://`
|
||||
: `${t("newDeviceDialog.http")}://`}
|
||||
: `${t("newDeviceDialog.http")}://`
|
||||
}
|
||||
placeholder={t("newDeviceDialog.httpConnection.placeholder")}
|
||||
className="text-slate-900 dark:text-slate-100"
|
||||
{...register("ip")}
|
||||
@@ -138,7 +139,8 @@ export const HTTP = (
|
||||
? t(
|
||||
"newDeviceDialog.connectionFailedAlert.acceptTlsWarningSuffix",
|
||||
)
|
||||
: ""}.{" "}
|
||||
: ""}
|
||||
.{" "}
|
||||
<Link
|
||||
href="https://meshtastic.org/docs/software/web-client/#http"
|
||||
className="underline font-medium text-amber-800 dark:text-amber-800"
|
||||
@@ -151,10 +153,7 @@ export const HTTP = (
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
<Button type="submit" variant="default">
|
||||
<span>
|
||||
{connectionInProgress
|
||||
? t("newDeviceDialog.connecting")
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
|
||||
export const Serial = (
|
||||
{ closeDialog }: TabElementProps,
|
||||
) => {
|
||||
export const Serial = ({ closeDialog }: TabElementProps) => {
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
@@ -54,13 +52,13 @@ export const Serial = (
|
||||
disabled={connectionInProgress}
|
||||
>
|
||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||
{serialPorts.map((port, index) => {
|
||||
{serialPorts.map((port) => {
|
||||
const { usbProductId, usbVendorId } = port.getInfo();
|
||||
const vendor = usbVendorId ?? t("unknown.shortName");
|
||||
const product = usbProductId ?? t("unknown.shortName");
|
||||
return (
|
||||
<Button
|
||||
key={`${vendor}-${product}-${index}`}
|
||||
key={`${vendor}-${product}`}
|
||||
disabled={port.readable !== null}
|
||||
variant="default"
|
||||
onClick={async () => {
|
||||
@@ -70,7 +68,6 @@ export const Serial = (
|
||||
}}
|
||||
>
|
||||
{t("newDeviceDialog.serialConnection.deviceIdentifier", {
|
||||
index: index,
|
||||
vendorId: vendor,
|
||||
productId: product,
|
||||
})}
|
||||
@@ -86,12 +83,16 @@ export const Serial = (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={async () => {
|
||||
await navigator.serial.requestPort().then((port) => {
|
||||
await navigator.serial
|
||||
.requestPort()
|
||||
.then((port) => {
|
||||
setSerialPorts(serialPorts.concat(port));
|
||||
// No need to setConnectionInProgress(false) here if requestPort is quick
|
||||
}).catch((error: Error) => {
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error("Error requesting port:", error);
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
setConnectionInProgress(false);
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import BatteryStatus from "@components/BatteryStatus.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import { Separator } from "@components/UI/Seperator.tsx";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import { formatQuantity } from "@core/utils/string.ts";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import type { Protobuf as ProtobufType } from "@meshtastic/core";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
Dot,
|
||||
LockIcon,
|
||||
@@ -15,16 +24,7 @@ import {
|
||||
MountainSnow,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import BatteryStatus from "@components/BatteryStatus.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export interface NodeDetailProps {
|
||||
node: ProtobufType.Mesh.NodeInfo;
|
||||
@@ -60,16 +60,14 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0
|
||||
? (
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||
<LockIcon
|
||||
className="text-green-600 mb-1.5"
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
aria-label={t("node_detail_public_key_enabled_aria_label")}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
) : (
|
||||
<LockOpenIcon
|
||||
className="text-yellow-500 mb-1.5"
|
||||
size={12}
|
||||
@@ -105,9 +103,11 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
<Star
|
||||
fill={node.isFavorite ? "black" : "none"}
|
||||
size={15}
|
||||
aria-label={node.isFavorite
|
||||
aria-label={
|
||||
node.isFavorite
|
||||
? t("nodeDetail.favorite.label")
|
||||
: t("nodeDetail.notFavorite.label")}
|
||||
: t("nodeDetail.notFavorite.label")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
|
||||
import type { Message } from "@core/stores/messageStore/types.ts";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { Message } from "@core/stores/messageStore/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface ChannelChatProps {
|
||||
|
||||
@@ -41,10 +41,7 @@ export const MessageActionsMenu = ({
|
||||
const iconSizeClass = "size-4";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(hoverIconBarClass)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={cn(hoverIconBarClass)}>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
@@ -6,13 +7,10 @@ import {
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MessageInput, MessageInputProps } from "./MessageInput.tsx";
|
||||
import { Types } from "@meshtastic/core";
|
||||
import { MessageInput, type MessageInputProps } from "./MessageInput.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}
|
||||
@@ -26,11 +24,8 @@ vi.mock("@components/UI/Button.tsx", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@components/UI/Input.tsx", () => ({
|
||||
Input: vi.fn((
|
||||
{ autoFocus, minLength, name, placeholder, value, onChange },
|
||||
) => (
|
||||
Input: vi.fn(({ minLength, name, placeholder, value, onChange }) => (
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
minLength={minLength}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
@@ -225,16 +220,14 @@ describe("MessageInput", () => {
|
||||
it("should work with broadcast destination for drafts", () => {
|
||||
const broadcastDest: Types.Destination = "broadcast";
|
||||
mockGetDraft.mockImplementation((key) =>
|
||||
key === broadcastDest ? "Broadcast draft" : ""
|
||||
key === broadcastDest ? "Broadcast draft" : "",
|
||||
);
|
||||
|
||||
renderComponent({ to: broadcastDest });
|
||||
|
||||
expect(mockGetDraft).toHaveBeenCalledWith(broadcastDest);
|
||||
expect(
|
||||
(screen.getByTestId(
|
||||
"message-input-field",
|
||||
) as HTMLInputElement).value,
|
||||
(screen.getByTestId("message-input-field") as HTMLInputElement).value,
|
||||
).toBe("Broadcast draft");
|
||||
|
||||
const inputElement = screen.getByTestId(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import { startTransition, useState } from "react";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface MessageInputProps {
|
||||
@@ -12,11 +12,7 @@ export interface MessageInputProps {
|
||||
maxBytes: number;
|
||||
}
|
||||
|
||||
export const MessageInput = ({
|
||||
onSend,
|
||||
to,
|
||||
maxBytes,
|
||||
}: MessageInputProps) => {
|
||||
export const MessageInput = ({ onSend, to, maxBytes }: MessageInputProps) => {
|
||||
const { setDraft, getDraft, clearDraft } = useMessageStore();
|
||||
const { t } = useTranslation("messages");
|
||||
|
||||
@@ -25,7 +21,7 @@ export const MessageInput = ({
|
||||
const initialDraft = getDraft(to);
|
||||
const [localDraft, setLocalDraft] = useState(initialDraft);
|
||||
const [messageBytes, setMessageBytes] = useState(() =>
|
||||
calculateBytes(initialDraft)
|
||||
calculateBytes(initialDraft),
|
||||
);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -41,7 +37,9 @@ export const MessageInput = ({
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!localDraft.trim()) return;
|
||||
if (!localDraft.trim()) {
|
||||
return;
|
||||
}
|
||||
// Reset bytes *before* sending (consider if onSend failure needs different handling)
|
||||
setMessageBytes(0);
|
||||
|
||||
@@ -56,9 +54,8 @@ export const MessageInput = ({
|
||||
<div className="flex gap-2">
|
||||
<form className="w-full" name="messageInput" onSubmit={handleSubmit}>
|
||||
<div className="flex grow gap-1">
|
||||
<label className="w-full">
|
||||
<label className="w-full" htmlFor="messageInput">
|
||||
<Input
|
||||
autoFocus
|
||||
minLength={1}
|
||||
name="messageInput"
|
||||
placeholder={t("sendMessage.placeholder")}
|
||||
@@ -70,15 +67,13 @@ export const MessageInput = ({
|
||||
|
||||
<label
|
||||
data-testid="byte-counter"
|
||||
htmlFor="messageInput"
|
||||
className="flex items-center w-20 p-1 text-sm place-content-end"
|
||||
>
|
||||
{messageBytes}/{maxBytes}
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
<Button type="submit" variant="default">
|
||||
<SendIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
@@ -6,18 +7,18 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@components/UI/Tooltip.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import {
|
||||
MessageState,
|
||||
useMessageStore,
|
||||
} from "@core/stores/messageStore/index.ts";
|
||||
import { Protobuf, Types } from "@meshtastic/core";
|
||||
import { Message } from "@core/stores/messageStore/types.ts";
|
||||
import type { Message } from "@core/stores/messageStore/types.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { type Protobuf, Types } from "@meshtastic/core";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||
import { type ReactNode, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented
|
||||
|
||||
interface MessageStatusInfo {
|
||||
@@ -27,12 +28,13 @@ interface MessageStatusInfo {
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
const StatusTooltip = (
|
||||
{ statusInfo, children }: {
|
||||
const StatusTooltip = ({
|
||||
statusInfo,
|
||||
children,
|
||||
}: {
|
||||
statusInfo: MessageStatusInfo;
|
||||
children: ReactNode;
|
||||
},
|
||||
) => (
|
||||
}) => (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
@@ -77,15 +79,19 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
||||
[t],
|
||||
);
|
||||
|
||||
const UNKNOWN_STATUS = useMemo((): MessageStatusInfo => ({
|
||||
const UNKNOWN_STATUS = useMemo(
|
||||
(): MessageStatusInfo => ({
|
||||
displayText: t("deliveryStatus.unknown.displayText"),
|
||||
icon: AlertCircle,
|
||||
ariaLabel: t("deliveryStatus.unknown.label"),
|
||||
iconClassName: "text-red-500 dark:text-red-400",
|
||||
}), [t]);
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const getMessageStatusInfo = useMemo(
|
||||
() => (state: MessageState): MessageStatusInfo =>
|
||||
() =>
|
||||
(state: MessageState): MessageStatusInfo =>
|
||||
MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS,
|
||||
[MESSAGE_STATUS_MAP, UNKNOWN_STATUS],
|
||||
);
|
||||
@@ -103,8 +109,8 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
||||
const longName = messageUser?.user?.longName;
|
||||
const derivedShortName = messageUser?.user?.shortName || fallbackName;
|
||||
const derivedDisplayName = longName || derivedShortName;
|
||||
const isFavorite = messageUser?.num !== myNodeNum &&
|
||||
messageUser?.isFavorite;
|
||||
const isFavorite =
|
||||
messageUser?.num !== myNodeNum && messageUser?.isFavorite;
|
||||
return {
|
||||
displayName: derivedDisplayName,
|
||||
shortName: derivedShortName,
|
||||
@@ -116,7 +122,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
||||
const StatusIconComponent = messageStatusInfo.icon;
|
||||
|
||||
const messageDate = useMemo(
|
||||
() => message.date ? new Date(message.date) : null,
|
||||
() => (message.date ? new Date(message.date) : null),
|
||||
[message.date],
|
||||
);
|
||||
const locale = i18n.language;
|
||||
@@ -199,11 +205,9 @@ export const MessageItem = ({ message }: MessageItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
{/* Actions Menu Placeholder */}
|
||||
{
|
||||
/* <div className="absolute top-1 right-1">
|
||||
{/* <div className="absolute top-1 right-1">
|
||||
<MessageActionsMenu onReply={() => console.log("Reply")} />
|
||||
</div> */
|
||||
}
|
||||
</div> */}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { mockDeviceStore } from "@core/stores/deviceStore.mock.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@core/stores/deviceStore");
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("TraceRoute", () => {
|
||||
macaddr: new Uint8Array([0x01, 0x02, 0x03, 0x04]),
|
||||
id: "source-node",
|
||||
isLicensed: false,
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"],
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||
} as Protobuf.Mesh.NodeInfo["user"],
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("TraceRoute", () => {
|
||||
macaddr: new Uint8Array([0x05, 0x06, 0x07, 0x08]),
|
||||
id: "destination-node",
|
||||
isLicensed: false,
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"],
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||
} as Protobuf.Mesh.NodeInfo["user"],
|
||||
};
|
||||
|
||||
@@ -123,13 +123,7 @@ describe("TraceRoute", () => {
|
||||
});
|
||||
|
||||
it("renders '??' for missing SNR values", () => {
|
||||
render(
|
||||
<TraceRoute
|
||||
from={fromUser}
|
||||
to={toUser}
|
||||
route={[1]}
|
||||
/>,
|
||||
);
|
||||
render(<TraceRoute from={fromUser} to={toUser} route={[1]} />);
|
||||
|
||||
expect(screen.getByText("Node A")).toBeInTheDocument();
|
||||
// Check for translated '??' placeholder
|
||||
|
||||
@@ -22,9 +22,7 @@ interface RoutePathProps {
|
||||
snr?: number[];
|
||||
}
|
||||
|
||||
const RoutePath = (
|
||||
{ title, from, to, path, snr }: RoutePathProps,
|
||||
) => {
|
||||
const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
|
||||
const { getNode } = useDevice();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type AmbientLightingValidation,
|
||||
AmbientLightingValidationSchema,
|
||||
@@ -8,17 +8,19 @@ import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface AmbientLightingModuleConfigProps {
|
||||
onFormInit: DynamicFormFormInit<AmbientLightingValidation>;
|
||||
}
|
||||
|
||||
export const AmbientLighting = (
|
||||
{ onFormInit }: AmbientLightingModuleConfigProps,
|
||||
) => {
|
||||
export const AmbientLighting = ({
|
||||
onFormInit,
|
||||
}: AmbientLightingModuleConfigProps) => {
|
||||
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
|
||||
const {
|
||||
moduleConfig,
|
||||
setWorkingModuleConfig,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type AudioValidation,
|
||||
AudioValidationSchema,
|
||||
@@ -8,15 +9,16 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface AudioModuleConfigProps {
|
||||
onFormInit: DynamicFormFormInit<AudioValidation>;
|
||||
}
|
||||
|
||||
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
|
||||
useWaitForConfig({ moduleConfigCase: "audio" });
|
||||
const {
|
||||
moduleConfig,
|
||||
setWorkingModuleConfig,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type CannedMessageValidation,
|
||||
CannedMessageValidationSchema,
|
||||
@@ -8,17 +9,19 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface CannedMessageModuleConfigProps {
|
||||
onFormInit: DynamicFormFormInit<CannedMessageValidation>;
|
||||
}
|
||||
|
||||
export const CannedMessage = (
|
||||
{ onFormInit }: CannedMessageModuleConfigProps,
|
||||
) => {
|
||||
export const CannedMessage = ({
|
||||
onFormInit,
|
||||
}: CannedMessageModuleConfigProps) => {
|
||||
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
|
||||
|
||||
const {
|
||||
moduleConfig,
|
||||
setWorkingModuleConfig,
|
||||
@@ -92,7 +95,8 @@ export const CannedMessage = (
|
||||
label: t("cannedMessage.inputbrokerEventCw.label"),
|
||||
description: t("cannedMessage.inputbrokerEventCw.description"),
|
||||
properties: {
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
@@ -102,7 +106,8 @@ export const CannedMessage = (
|
||||
label: t("cannedMessage.inputbrokerEventCcw.label"),
|
||||
description: t("cannedMessage.inputbrokerEventCcw.description"),
|
||||
properties: {
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
@@ -112,7 +117,8 @@ export const CannedMessage = (
|
||||
label: t("cannedMessage.inputbrokerEventPress.label"),
|
||||
description: t("cannedMessage.inputbrokerEventPress.description"),
|
||||
properties: {
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type DetectionSensorValidation,
|
||||
DetectionSensorValidationSchema,
|
||||
@@ -8,17 +8,20 @@ import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface DetectionSensorModuleConfigProps {
|
||||
onFormInit: DynamicFormFormInit<DetectionSensorValidation>;
|
||||
}
|
||||
|
||||
export const DetectionSensor = (
|
||||
{ onFormInit }: DetectionSensorModuleConfigProps,
|
||||
) => {
|
||||
export const DetectionSensor = ({
|
||||
onFormInit,
|
||||
}: DetectionSensorModuleConfigProps) => {
|
||||
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
|
||||
|
||||
const {
|
||||
moduleConfig,
|
||||
setWorkingModuleConfig,
|
||||
@@ -135,7 +138,8 @@ export const DetectionSensor = (
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_DetectionSensorConfig_TriggerType,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
||||
import {
|
||||
type ExternalNotificationValidation,
|
||||
ExternalNotificationValidationSchema,
|
||||
@@ -8,17 +9,19 @@ import {
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
|
||||
interface ExternalNotificationModuleConfigProps {
|
||||
onFormInit: DynamicFormFormInit<ExternalNotificationValidation>;
|
||||
}
|
||||
|
||||
export const ExternalNotification = (
|
||||
{ onFormInit }: ExternalNotificationModuleConfigProps,
|
||||
) => {
|
||||
export const ExternalNotification = ({
|
||||
onFormInit,
|
||||
}: ExternalNotificationModuleConfigProps) => {
|
||||
useWaitForConfig({ moduleConfigCase: "externalNotification" });
|
||||
|
||||
const {
|
||||
moduleConfig,
|
||||
setWorkingModuleConfig,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user