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