mirror of
https://github.com/meshtastic/web.git
synced 2026-07-31 06:56:28 -04:00
fix(web): clear pre-existing typecheck errors (36 → 0)
Sweep covering several unrelated cohorts: - Delete dead `NewDeviceDialog.tsx` (commented-out in App.tsx, imports point at non-existent `@components/PageComponents/Connect/*`). - Add `better-result` to sdk-react deps so `useChat`/`useDirectChat`/ `useFavoriteNode`/`useIgnoreNode` resolve. - Fix `test-utils.tsx`: route at `routeTree.gen.ts` was never generated; re-export the existing `routeTree` constant from `routes.tsx` and pass the missing router context + DeviceWrapper deviceId. - Pre-existing `noUncheckedIndexedAccess` violations in `pskSchema.test.ts`, `x25519.ts`, `Table/index.tsx` + test, `useCopyToClipboard` (Timeout vs number) — non-null asserts where the index is guaranteed by the surrounding code, plus a defensive `if (!cell) return 0` in the table sort. - Immer `WritableDraft<T>` mismatches against SDK's `MeshDevice`/`Device` classes — cast through `Draft<X>` where the inferred recursive draft type can't represent SDK class internals. - `deviceStore.mock` updated for the `connectionPhase`/`connectionId` + setters added when the connection lifecycle was extracted. - `HeatmapLayer` accepts the `isVisible` prop everyone else's layer accepts. - `SNRTooltipProps.to` widened to `string | undefined` (single-node hover has no `to`). - Zod resolver returns `Record<string, never>` for the error-path values shape that react-hook-form expects. - Mock `NodeInfo` in `useFilterNode.test.ts` gains the `isMuted` proto field that landed upstream.
This commit is contained in:
@@ -22,14 +22,7 @@ export function App() {
|
||||
const device = getDevice(selectedDeviceId);
|
||||
|
||||
return (
|
||||
// <ThemeProvider defaultTheme="system" storageKey="theme">
|
||||
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||
{/* <NewDeviceDialog
|
||||
open={connectDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setConnectDialogOpen(open);
|
||||
}}
|
||||
/> */}
|
||||
<Toaster />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<DeviceWrapper deviceId={selectedDeviceId}>
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
} from "@core/hooks/useBrowserFeatureDetection.ts";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "../UI/Typography/Link.tsx";
|
||||
|
||||
export interface TabElementProps {
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export interface TabManifest {
|
||||
id: "HTTP" | "BLE" | "Serial";
|
||||
label: string;
|
||||
element: React.FC<TabElementProps>;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export interface NewDeviceProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface FeatureErrorProps {
|
||||
missingFeatures: BrowserFeature[];
|
||||
tabId: "HTTP" | "BLE" | "Serial";
|
||||
}
|
||||
|
||||
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
|
||||
"Web Bluetooth": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth",
|
||||
},
|
||||
"Web Serial": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
||||
i18nKey: "newDeviceDialog.validation.requiresWebSerial",
|
||||
},
|
||||
"Secure Context": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||
i18nKey: "newDeviceDialog.validation.requiresSecureContext",
|
||||
},
|
||||
};
|
||||
|
||||
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
if (missingFeatures.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
|
||||
const needsSecureContext = missingFeatures.includes("Secure Context");
|
||||
|
||||
const needsFeature =
|
||||
tabId === "BLE" && browserFeatures.includes("Web Bluetooth")
|
||||
? "Web Bluetooth"
|
||||
: tabId === "Serial" && browserFeatures.includes("Web Serial")
|
||||
? "Web Serial"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<AlertCircle size={40} className="mr-2 shrink-0 text-white" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-sm text-white">
|
||||
{needsFeature && (
|
||||
<Trans
|
||||
i18nKey={errors[needsFeature].i18nKey}
|
||||
components={[
|
||||
<Link
|
||||
key="0"
|
||||
href={errors[needsFeature].href}
|
||||
className="underline hover:text-slate-200 text-white dark:text-white dark:hover:text-slate-300"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{needsFeature && needsSecureContext && " "}
|
||||
{needsSecureContext && (
|
||||
<Trans
|
||||
i18nKey={
|
||||
browserFeatures.length > 0
|
||||
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
|
||||
: "newDeviceDialog.validation.requiresSecureContext"
|
||||
}
|
||||
components={{
|
||||
"0": (
|
||||
<Link
|
||||
href={errors["Secure Context"].href}
|
||||
className="underline hover:text-slate-200"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
|
||||
const tabs: TabManifest[] = [
|
||||
{
|
||||
id: "HTTP",
|
||||
label: t("newDeviceDialog.tabHttp"),
|
||||
element: HTTP,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: "BLE",
|
||||
label: t("newDeviceDialog.tabBluetooth"),
|
||||
element: BLE,
|
||||
isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"),
|
||||
},
|
||||
{
|
||||
id: "Serial",
|
||||
label: t("newDeviceDialog.tabSerial"),
|
||||
element: Serial,
|
||||
isDisabled: unsupported.includes("Web Serial") || unsupported.includes("Secure Context"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("newDeviceDialog.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="HTTP">
|
||||
<TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id}>
|
||||
<fieldset disabled={tab.isDisabled}>
|
||||
{tab.id !== "HTTP" && tab.isDisabled ? (
|
||||
<ErrorMessage missingFeatures={unsupported} tabId={tab.id} />
|
||||
) : (
|
||||
<tab.element closeDialog={() => onOpenChange(false)} />
|
||||
)}
|
||||
</fieldset>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
FieldError,
|
||||
FieldErrors,
|
||||
FieldValues,
|
||||
Resolver,
|
||||
ResolverOptions,
|
||||
@@ -49,8 +50,8 @@ export function createZodResolver<T extends FieldValues>(
|
||||
}
|
||||
|
||||
return {
|
||||
values: {} as T,
|
||||
errors,
|
||||
values: {} as Record<string, never>,
|
||||
errors: errors as FieldErrors<T>,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ export interface HeatmapLayerProps {
|
||||
id: string;
|
||||
filteredNodes: Protobuf.Mesh.NodeInfo[];
|
||||
mode: HeatmapMode;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) => {
|
||||
export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLayerProps) => {
|
||||
if (!isVisible) return null;
|
||||
const data: FeatureCollection = useMemo(() => {
|
||||
const features: Feature[] = filteredNodes
|
||||
.filter((node) => hasPos(node.position))
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface SNRTooltipProps {
|
||||
pos: { x: number; y: number };
|
||||
snr: number;
|
||||
from: string;
|
||||
to: string;
|
||||
to: string | undefined;
|
||||
}
|
||||
|
||||
type NeighborPlus = Protobuf.Mesh.Neighbor & {
|
||||
|
||||
@@ -13,6 +13,7 @@ function createMockNode(): Protobuf.Mesh.NodeInfo {
|
||||
viaMqtt: false,
|
||||
isFavorite: true,
|
||||
isIgnored: false,
|
||||
isMuted: false,
|
||||
hopsAway: 2,
|
||||
isKeyManuallyVerified: false,
|
||||
user: {
|
||||
|
||||
@@ -112,17 +112,17 @@ describe("Generic Table", () => {
|
||||
expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST1", "TST3"]);
|
||||
|
||||
// Click "Short Name" to sort asc
|
||||
fireEvent.click(columnHeaders[0]);
|
||||
fireEvent.click(columnHeaders[0]!);
|
||||
// TST2 is favorite, so it's first. Then TST1, TST3, TST4 alphabetically.
|
||||
expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST3", "TST4"]);
|
||||
|
||||
// Click "Short Name" again to sort desc
|
||||
fireEvent.click(columnHeaders[0]);
|
||||
fireEvent.click(columnHeaders[0]!);
|
||||
// TST2 is favorite, so it's first. Then TST4, TST3, TST1 reverse alphabetically.
|
||||
expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST3", "TST1"]);
|
||||
|
||||
// Click "Connection" to sort by hops asc
|
||||
fireEvent.click(columnHeaders[2]);
|
||||
fireEvent.click(columnHeaders[2]!);
|
||||
// TST2 is favorite (and also has 0 hops). Then sorted by hops: TST1 (1), TST4 (3), TST3 (4).
|
||||
expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST4", "TST3"]);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ export const Table = ({ headings, rows }: TableProps) => {
|
||||
|
||||
const aCell = a.cells[columnIndex];
|
||||
const bCell = b.cells[columnIndex];
|
||||
if (!aCell || !bCell) return 0;
|
||||
|
||||
let aValue: string | number;
|
||||
let bValue: string | number;
|
||||
|
||||
@@ -6,7 +6,7 @@ interface UseCopyToClipboardProps {
|
||||
|
||||
export function useCopyToClipboard({ timeout = 2000 }: UseCopyToClipboardProps = {}) {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -16,6 +16,8 @@ export const mockDeviceStore: Device = {
|
||||
id: 0,
|
||||
myNodeNum: 123456,
|
||||
status: 5 as const,
|
||||
connectionPhase: "disconnected",
|
||||
connectionId: null,
|
||||
channels: new Map(),
|
||||
config: {} as Protobuf.LocalOnly.LocalConfig,
|
||||
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig,
|
||||
@@ -50,6 +52,8 @@ export const mockDeviceStore: Device = {
|
||||
neighborInfo: new Map(),
|
||||
|
||||
setStatus: vi.fn(),
|
||||
setConnectionPhase: vi.fn(),
|
||||
setConnectionId: vi.fn(),
|
||||
setConfig: vi.fn(),
|
||||
setModuleConfig: vi.fn(),
|
||||
getEffectiveConfig: vi.fn(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { evictOldestEntries } from "@core/stores/utils/evictOldestEntries.ts";
|
||||
import { createStorage } from "@core/stores/utils/indexDB.ts";
|
||||
import { type MeshDevice, Protobuf, Types } from "@meshtastic/sdk";
|
||||
import { produce } from "immer";
|
||||
import { type Draft, produce } from "immer";
|
||||
import { create as createStore, type StateCreator } from "zustand";
|
||||
import { type PersistOptions, persist, subscribeWithSelector } from "zustand/middleware";
|
||||
import type {
|
||||
@@ -476,7 +476,7 @@ function deviceFactory(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
device.connection = connection;
|
||||
device.connection = connection as unknown as Draft<MeshDevice>;
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -616,7 +616,7 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (set, ge
|
||||
const device = deviceFactory(id, get, set);
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
draft.devices = new Map(draft.devices).set(id, device);
|
||||
draft.devices = new Map(draft.devices).set(id, device as unknown as Draft<Device>);
|
||||
|
||||
// Enforce retention limit
|
||||
evictOldestEntries(draft.devices, DEVICESTORE_RETENTION_NUM);
|
||||
@@ -737,7 +737,7 @@ const persistOptions: PersistOptions<PrivateDeviceState, DevicePersisted> = {
|
||||
);
|
||||
}
|
||||
}
|
||||
draft.devices = rebuilt;
|
||||
draft.devices = rebuilt as unknown as Map<number, Draft<Device>>;
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -5,9 +5,9 @@ export function getX25519PrivateKey(): Uint8Array {
|
||||
|
||||
// scalar clamping for curve25519, according to
|
||||
// https://www.rfc-editor.org/rfc/rfc7748#section-5
|
||||
key[0] &= 248;
|
||||
key[31] &= 127;
|
||||
key[31] |= 64;
|
||||
key[0]! &= 248;
|
||||
key[31]! &= 127;
|
||||
key[31]! |= 64;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ const connectionsRoute = createRoute({
|
||||
component: Connections,
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
messagesRoute,
|
||||
messagesWithParamsRoute,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactElement } from "react";
|
||||
import "@app/i18n-config.ts";
|
||||
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||
import { routeTree } from "../routeTree.gen.ts";
|
||||
import { routeTree } from "../routes.tsx";
|
||||
|
||||
const Providers = () => {
|
||||
const memoryHistory = createMemoryHistory({
|
||||
@@ -14,10 +14,14 @@ const Providers = () => {
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
history: memoryHistory,
|
||||
context: {
|
||||
stores: {} as never,
|
||||
i18n: {} as never,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DeviceWrapper>
|
||||
<DeviceWrapper deviceId={0}>
|
||||
<RouterProvider router={router} />
|
||||
</DeviceWrapper>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("stringSchema", () => {
|
||||
const result = stringSchema().safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.length);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.length);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("stringSchema", () => {
|
||||
const result = stringSchema().safeParse("not_base64!");
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.format);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.format);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("stringSchema", () => {
|
||||
const result = stringSchema().safeParse("");
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.required);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.required);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("stringSchema", () => {
|
||||
const result = stringSchema().safeParse(valid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.format);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.format);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("stringSchema", () => {
|
||||
const result = stringSchema().safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.length);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.length);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("stringSchema", () => {
|
||||
const result = bytesSchema().safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.length);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.length);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("stringSchema", () => {
|
||||
const result = bytesSchema().safeParse(new Uint8Array(0));
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.required);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.required);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("stringSchema", () => {
|
||||
const result = bytesSchema().safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(msgs.length);
|
||||
expect(result.error.issues[0]!.message).toBe(msgs.length);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@meshtastic/sdk": "workspace:*"
|
||||
"@meshtastic/sdk": "workspace:*",
|
||||
"better-result": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -343,6 +343,9 @@ importers:
|
||||
'@meshtastic/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../sdk
|
||||
better-result:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.1
|
||||
devDependencies:
|
||||
'@testing-library/react':
|
||||
specifier: ^16.0.0
|
||||
|
||||
Reference in New Issue
Block a user