Browse Source

Command Menu improvements (#857)

* Add dialogs for reset and clear actions + clear stores

* Catch failures

* Improve tests, don't reset store on failure

* Remove unnecessary i18n string

---------

Co-authored-by: philon- <[email protected]>
pull/863/head
Jeremy Gallant 9 months ago
committed by GitHub
parent
commit
8cc451546d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      packages/web/public/i18n/locales/en/commandPalette.json
  2. 24
      packages/web/public/i18n/locales/en/dialog.json
  3. 27
      packages/web/src/components/CommandPalette/index.tsx
  4. 69
      packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.test.tsx
  5. 35
      packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx
  6. 28
      packages/web/src/components/Dialog/DialogManager.tsx
  7. 63
      packages/web/src/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog.test.tsx
  8. 39
      packages/web/src/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog.tsx
  9. 94
      packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx
  10. 48
      packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx
  11. 95
      packages/web/src/components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.test.tsx
  12. 48
      packages/web/src/components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx
  13. 8
      packages/web/src/core/stores/deviceStore/index.ts
  14. 3
      packages/web/src/core/stores/index.ts

4
packages/web/public/i18n/locales/en/commandPalette.json

@ -15,7 +15,6 @@
"messages": "Messages",
"map": "Map",
"config": "Config",
"channels": "Channels",
"nodes": "Nodes"
}
},
@ -45,7 +44,8 @@
"label": "Debug",
"command": {
"reconfigure": "Reconfigure",
"clearAllStoredMessages": "Clear All Stored Message"
"clearAllStoredMessages": "Clear All Stored Messages",
"clearAllStores": "Clear All Local Storage"
}
}
}

24
packages/web/public/i18n/locales/en/dialog.json

@ -194,5 +194,29 @@
"title": "Client Notification",
"TraceRoute can only be sent once every 30 seconds": "TraceRoute can only be sent once every 30 seconds",
"Compromised keys were detected and regenerated.": "Compromised keys were detected and regenerated."
},
"resetNodeDb": {
"title": "Reset Node Database",
"description": "This will clear all nodes from the connected device's node database and clear all message history in the client. This cannot be undone. Are you sure you want to continue?",
"confirm": "Reset Node Database",
"failedTitle": "There was an error resetting the Node DB. Please try again."
},
"clearAllStores": {
"title": "Clear All Local Storage",
"description": "This will clear all locally stored data, including message history and node databases for all previously connected devices. This will require you to reconnect to your node once complete and cannot be undone. Are you sure you want to continue?",
"confirm": "Clear all local storage",
"failedTitle": "There was an error clearing local storage. Please try again."
},
"factoryResetDevice": {
"title": "Factory Reset Device",
"description": "This will factory reset the connected device, erasing all configurations and data on the device as well as all nodes and messages saved in the client. This cannot be undone. Are you sure you want to continue?",
"confirm": "Factory Reset Device",
"failedTitle": "There was an error performing the factory reset. Please try again."
},
"factoryResetConfig": {
"title": "Factory Reset Config",
"description": "This will factory reset the configuration on the connected device, erasing all configurations on the device. This cannot be undone. Are you sure you want to continue?",
"confirm": "Factory Reset Config",
"failedTitle": "There was an error performing the factory reset. Please try again."
}
}

27
packages/web/src/components/CommandPalette/index.tsx

@ -25,7 +25,6 @@ import {
EraserIcon,
FactoryIcon,
HardDriveUpload,
LayersIcon,
LinkIcon,
type LucideIcon,
MapIcon,
@ -71,7 +70,7 @@ export const CommandPalette = () => {
} = useAppStore();
const { getDevices } = useDeviceStore();
const { setDialogOpen, connection } = useDevice();
const { getNode, removeAllNodeErrors, removeAllNodes } = useNodeDB();
const { getNode } = useNodeDB();
const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups",
});
@ -106,13 +105,6 @@ export const CommandPalette = () => {
},
tags: ["settings"],
},
{
label: t("goto.command.channels"),
icon: LayersIcon,
action() {
navigate({ to: "/channels" });
},
},
{
label: t("goto.command.nodes"),
icon: UsersIcon,
@ -206,9 +198,7 @@ export const CommandPalette = () => {
label: t("contextual.command.resetNodeDb"),
icon: TrashIcon,
action() {
connection?.resetNodes();
removeAllNodeErrors();
removeAllNodes(true);
setDialogOpen("resetNodeDb", true);
},
},
{
@ -224,16 +214,14 @@ export const CommandPalette = () => {
label: t("contextual.command.factoryResetDevice"),
icon: FactoryIcon,
action() {
connection?.factoryResetDevice();
removeAllNodeErrors();
removeAllNodes();
setDialogOpen("factoryResetDevice", true);
},
},
{
label: t("contextual.command.factoryResetConfig"),
icon: FactoryIcon,
action() {
connection?.factoryResetConfig();
setDialogOpen("factoryResetConfig", true);
},
},
],
@ -257,6 +245,13 @@ export const CommandPalette = () => {
setDialogOpen("deleteMessages", true);
},
},
{
label: t("debug.command.clearAllStores"),
icon: EraserIcon,
action() {
setDialogOpen("clearAllStores", true);
},
},
],
},
];

69
packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.test.tsx

@ -0,0 +1,69 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ClearAllStoresDialog } from "./ClearAllStoresDialog.tsx";
const mockClearAllStores = vi.fn();
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
clearAllStores: () => mockClearAllStores(),
}));
describe("ClearAllStoresDialog", () => {
const mockOnOpenChange = vi.fn();
// Capture window.location.href assignment without triggering real navigation
const originalLocation = window.location;
let assignedHref: string | undefined;
beforeEach(() => {
mockOnOpenChange.mockClear();
mockClearAllStores.mockClear();
assignedHref = undefined;
Object.defineProperty(window, "location", {
configurable: true,
value: {
...originalLocation,
get href() {
return originalLocation.href;
},
set href(val: string) {
assignedHref = val;
},
},
});
});
// restore the real location object after each test
afterEach(() => {
Object.defineProperty(window, "location", {
configurable: true,
value: originalLocation,
});
});
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Clear all local storage" }),
);
expect(mockClearAllStores).toHaveBeenCalledTimes(1);
expect(assignedHref).toBe("/"); // forced reload target
// We reload instead of toggling the dialog, so ensure we didn't call onOpenChange
expect(mockOnOpenChange).not.toHaveBeenCalled();
});
it("calls onOpenChange with false when cancel is clicked", () => {
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(mockClearAllStores).not.toHaveBeenCalled();
expect(assignedHref).toBeUndefined(); // no navigation
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
});

35
packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx

@ -0,0 +1,35 @@
import { clearAllStores } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
export interface ClearAllStoresDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const ClearAllStoresDialog = ({
open,
onOpenChange,
}: ClearAllStoresDialogProps) => {
const { t } = useTranslation("dialog");
const handleClearAllStores = () => {
clearAllStores();
// Reload the app to ensure all state is cleared
window.location.href = "/";
};
return (
<DialogWrapper
open={open}
onOpenChange={onOpenChange}
type="confirm"
variant="destructive"
title={t("clearAllStores.title")}
description={t("clearAllStores.description")}
confirmText={t("clearAllStores.confirm")}
onConfirm={handleClearAllStores}
/>
);
};

28
packages/web/src/components/Dialog/DialogManager.tsx

@ -1,3 +1,6 @@
import { FactoryResetConfigDialog } from "@app/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog";
import { FactoryResetDeviceDialog } from "@app/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog";
import { ClearAllStoresDialog } from "@components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx";
import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx";
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
@ -8,6 +11,7 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
import { ResetNodeDbDialog } from "@components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
import { useDevice } from "@core/stores";
@ -91,6 +95,30 @@ export const DialogManager = () => {
setDialogOpen("clientNotification", open);
}}
/>
<ResetNodeDbDialog
open={dialog.resetNodeDb}
onOpenChange={(open) => {
setDialogOpen("resetNodeDb", open);
}}
/>
<ClearAllStoresDialog
open={dialog.clearAllStores}
onOpenChange={(open) => {
setDialogOpen("clearAllStores", open);
}}
/>
<FactoryResetDeviceDialog
open={dialog.factoryResetDevice}
onOpenChange={(open) => {
setDialogOpen("factoryResetDevice", open);
}}
/>
<FactoryResetConfigDialog
open={dialog.factoryResetConfig}
onOpenChange={(open) => {
setDialogOpen("factoryResetConfig", open);
}}
/>
</>
);
};

63
packages/web/src/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog.test.tsx

@ -0,0 +1,63 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FactoryResetConfigDialog } from "./FactoryResetConfigDialog.tsx";
const mockFactoryReset = vi.fn();
const mockToast = vi.fn();
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
useDevice: () => ({
connection: {
factoryResetConfig: mockFactoryReset,
},
}),
}));
vi.mock("@core/hooks/useToast.ts", () => ({
toast: (...args: unknown[]) => mockToast(...args),
}));
describe("FactoryResetConfigDialog", () => {
const mockOnOpenChange = vi.fn();
beforeEach(() => {
mockOnOpenChange.mockClear();
mockFactoryReset.mockReset();
mockToast.mockClear();
mockFactoryReset.mockResolvedValue(undefined);
});
it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Config" }),
);
expect(mockFactoryReset).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
// success path: no toast
expect(mockToast).not.toHaveBeenCalled();
});
it("calls onOpenChange(false) and does not call factoryResetConfig when cancel is clicked", async () => {
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
expect(mockFactoryReset).not.toHaveBeenCalled();
});
});

39
packages/web/src/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog.tsx

@ -0,0 +1,39 @@
import { toast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
export interface FactoryResetConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const FactoryResetConfigDialog = ({
open,
onOpenChange,
}: FactoryResetConfigDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const handleFactoryResetConfig = () => {
connection?.factoryResetConfig().catch((error) => {
toast({
title: t("factoryResetConfig.failedTitle"),
});
console.error("Failed to factory reset config:", error);
});
};
return (
<DialogWrapper
open={open}
onOpenChange={onOpenChange}
type="confirm"
variant="destructive"
title={t("factoryResetConfig.title")}
description={t("factoryResetConfig.description")}
confirmText={t("factoryResetConfig.confirm")}
onConfirm={handleFactoryResetConfig}
/>
);
};

94
packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx

@ -0,0 +1,94 @@
// FactoryResetDeviceDialog.test.tsx
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FactoryResetDeviceDialog } from "./FactoryResetDeviceDialog.tsx";
const mockFactoryResetDevice = vi.fn();
const mockDeleteAllMessages = vi.fn();
const mockRemoveAllNodeErrors = vi.fn();
const mockRemoveAllNodes = vi.fn();
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
useDevice: () => ({
connection: {
factoryResetDevice: mockFactoryResetDevice,
},
}),
useMessages: () => ({
deleteAllMessages: mockDeleteAllMessages,
}),
useNodeDB: () => ({
removeAllNodeErrors: mockRemoveAllNodeErrors,
removeAllNodes: mockRemoveAllNodes,
}),
}));
describe("FactoryResetDeviceDialog", () => {
const mockOnOpenChange = vi.fn();
beforeEach(() => {
mockOnOpenChange.mockClear();
mockFactoryResetDevice.mockClear();
mockDeleteAllMessages.mockClear();
mockRemoveAllNodeErrors.mockClear();
mockRemoveAllNodes.mockClear();
});
it("calls factoryResetDevice, closes dialog, and after reset resolves clears messages and node DB", async () => {
// Control the promise returned by factoryResetDevice
let resolveReset: (() => void) | undefined;
mockFactoryResetDevice.mockImplementation(
() =>
new Promise<void>((resolve) => {
resolveReset = resolve;
}),
);
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Device" }),
);
// Called immediately
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
// DialogWrapper awaits onConfirm (which returns undefined), so close happens on next microtask
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
// Nothing else should have happened yet (the promise hasn't resolved)
expect(mockDeleteAllMessages).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
// Resolve the reset
resolveReset?.();
// Now the .then() chain should fire
await waitFor(() => {
expect(mockDeleteAllMessages).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodeErrors).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodes).toHaveBeenCalledTimes(1);
});
});
it("calls onOpenChange(false) and does not call factoryResetDevice when cancel is clicked", async () => {
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
expect(mockFactoryResetDevice).not.toHaveBeenCalled();
expect(mockDeleteAllMessages).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
});
});

48
packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx

@ -0,0 +1,48 @@
import { toast } from "@core/hooks/useToast.ts";
import { useDevice, useMessages, useNodeDB } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
export interface FactoryResetDeviceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const FactoryResetDeviceDialog = ({
open,
onOpenChange,
}: FactoryResetDeviceDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
const { deleteAllMessages } = useMessages();
const handleFactoryResetDevice = () => {
connection
?.factoryResetDevice()
.then(() => {
deleteAllMessages();
removeAllNodeErrors();
removeAllNodes();
})
.catch((error) => {
toast({
title: t("factoryResetDevice.failedTitle"),
});
console.error("Failed to factory reset device:", error);
});
};
return (
<DialogWrapper
open={open}
onOpenChange={onOpenChange}
type="confirm"
variant="destructive"
title={t("factoryResetDevice.title")}
description={t("factoryResetDevice.description")}
confirmText={t("factoryResetDevice.confirm")}
onConfirm={handleFactoryResetDevice}
/>
);
};

95
packages/web/src/components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.test.tsx

@ -0,0 +1,95 @@
// ResetNodeDbDialog.test.tsx
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ResetNodeDbDialog } from "./ResetNodeDbDialog.tsx";
const mockResetNodes = vi.fn();
const mockDeleteAllMessages = vi.fn();
const mockRemoveAllNodeErrors = vi.fn();
const mockRemoveAllNodes = vi.fn();
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
useDevice: () => ({
connection: {
resetNodes: mockResetNodes,
},
}),
useMessages: () => ({
deleteAllMessages: mockDeleteAllMessages,
}),
useNodeDB: () => ({
removeAllNodeErrors: mockRemoveAllNodeErrors,
removeAllNodes: mockRemoveAllNodes,
}),
}));
describe("ResetNodeDbDialog", () => {
const mockOnOpenChange = vi.fn();
beforeEach(() => {
mockOnOpenChange.mockClear();
mockResetNodes.mockClear();
mockDeleteAllMessages.mockClear();
mockRemoveAllNodeErrors.mockClear();
mockRemoveAllNodes.mockClear();
});
it("calls resetNodes, closes dialog, and after resolve clears messages and node DB (with true flag)", async () => {
// Control the promise returned by resetNodes
let resolveReset: (() => void) | undefined;
mockResetNodes.mockImplementation(
() =>
new Promise<void>((resolve) => {
resolveReset = resolve;
}),
);
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(
screen.getByRole("button", { name: "Reset Node Database" }),
);
// Called immediately
expect(mockResetNodes).toHaveBeenCalledTimes(1);
// DialogWrapper awaits onConfirm (which returns undefined), so close happens on next microtask
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
// Nothing else should have happened yet (the promise hasn't resolved)
expect(mockDeleteAllMessages).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
// Resolve the reset
resolveReset?.();
// Now the .then() chain should fire
await waitFor(() => {
expect(mockDeleteAllMessages).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodeErrors).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodes).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodes).toHaveBeenCalledWith(true);
});
});
it("calls onOpenChange(false) and does not call resetNodes when cancel is clicked", async () => {
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
expect(mockResetNodes).not.toHaveBeenCalled();
expect(mockDeleteAllMessages).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
});
});

48
packages/web/src/components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx

@ -0,0 +1,48 @@
import { toast } from "@core/hooks/useToast.ts";
import { useDevice, useMessages, useNodeDB } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
export interface ResetNodeDbDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const ResetNodeDbDialog = ({
open,
onOpenChange,
}: ResetNodeDbDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
const { deleteAllMessages } = useMessages();
const handleResetNodeDb = () => {
connection
?.resetNodes()
.then(() => {
deleteAllMessages();
removeAllNodeErrors();
removeAllNodes(true);
})
.catch((error) => {
toast({
title: t("resetNodeDb.failedTitle"),
});
console.error("Failed to reset Node DB:", error);
});
};
return (
<DialogWrapper
open={open}
onOpenChange={onOpenChange}
type="confirm"
variant="destructive"
title={t("resetNodeDb.title")}
description={t("resetNodeDb.description")}
confirmText={t("resetNodeDb.confirm")}
onConfirm={handleResetNodeDb}
/>
);
};

8
packages/web/src/core/stores/deviceStore/index.ts

@ -57,6 +57,10 @@ export interface Device {
deleteMessages: boolean;
managedMode: boolean;
clientNotification: boolean;
resetNodeDb: boolean;
clearAllStores: boolean;
factoryResetDevice: boolean;
factoryResetConfig: boolean;
};
clientNotifications: Protobuf.Mesh.ClientNotification[];
@ -166,6 +170,10 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
deleteMessages: false,
managedMode: false,
clientNotification: false,
resetNodeDb: false,
clearAllStores: false,
factoryResetDevice: false,
factoryResetConfig: false,
},
pendingSettingsChanges: false,
messageDraft: "",

3
packages/web/src/core/stores/index.ts

@ -30,6 +30,9 @@ export {
useSidebar, // TODO: Bring hook into this file
} from "@core/stores/sidebarStore";
// Re-export idb-keyval functions for clearing all stores, expand this if we add more local storage types
export { clear as clearAllStores } from "idb-keyval";
// Define hooks to access the stores
export const useNodeDB = bindStoreToDevice(
useNodeDBStore,

Loading…
Cancel
Save