This is a little embarrassing...
diff --git a/src/components/UI/Typography/Link.tsx b/src/components/UI/Typography/Link.tsx
index 076ce63a..f16bb61b 100644
--- a/src/components/UI/Typography/Link.tsx
+++ b/src/components/UI/Typography/Link.tsx
@@ -12,7 +12,7 @@ export const Link = ({ href, children, className }: LinkProps) => (
target="_blank"
rel="noopener noreferrer"
className={cn(
- "font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",
+ "font-medium text-slate-900 underline underline-offset-4 dark:text-slate-900",
className,
)}
>
diff --git a/src/core/hooks/useLocalStorage.ts b/src/core/hooks/useLocalStorage.ts
new file mode 100644
index 00000000..f7e3c20e
--- /dev/null
+++ b/src/core/hooks/useLocalStorage.ts
@@ -0,0 +1,179 @@
+// taken from https://react-hooked.vercel.app/docs/useLocalStorage/
+
+import { useCallback, useEffect, useState } from "react";
+
+import type { Dispatch, SetStateAction } from "react";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface WindowEventMap {
+ "local-storage": CustomEvent;
+ }
+}
+
+type UseLocalStorageOptions = {
+ serializer?: (value: T) => string;
+ deserializer?: (value: string) => T;
+ initializeWithValue?: boolean;
+};
+
+const IS_SERVER = typeof window === "undefined";
+
+/**
+ * Hook for persisting state to localStorage.
+ *
+ * @param {string} key - The key to use for localStorage.
+ * @param {T | (() => T)} initialValue - The initial value to use, if not found in localStorage.
+ * @param {UseLocalStorageOptions} options - Options for the hook.
+ * @returns A tuple of [storedValue, setValue, removeValue].
+ */
+export default function useLocalStorage(
+ key: string,
+ initialValue: T | (() => T),
+ options: UseLocalStorageOptions = {},
+): [T, Dispatch>, () => void] {
+ const { initializeWithValue = true } = options;
+
+ const serializer = useCallback<(value: T) => string>(
+ (value) => {
+ if (options.serializer) {
+ return options.serializer(value);
+ }
+
+ return JSON.stringify(value);
+ },
+ [options],
+ );
+
+ const deserializer = useCallback<(value: string) => T>(
+ (value) => {
+ if (options.deserializer) {
+ return options.deserializer(value);
+ }
+ // Support 'undefined' as a value
+ if (value === "undefined") {
+ return undefined as unknown as T;
+ }
+
+ const defaultValue =
+ initialValue instanceof Function ? initialValue() : initialValue;
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(value);
+ } catch (error) {
+ console.error("Error parsing JSON:", error);
+ return defaultValue; // Return initialValue if parsing fails
+ }
+
+ return parsed as T;
+ },
+ [options, initialValue],
+ );
+
+ // Get from local storage then
+ // parse stored json or return initialValue
+ const readValue = useCallback((): T => {
+ const initialValueToUse =
+ initialValue instanceof Function ? initialValue() : initialValue;
+
+ // Prevent build error "window is undefined" but keep working
+ if (IS_SERVER) {
+ return initialValueToUse;
+ }
+
+ try {
+ const raw = window.localStorage.getItem(key);
+ return raw ? deserializer(raw) : initialValueToUse;
+ } catch (error) {
+ console.warn(`Error reading localStorage key “${key}”:`, error);
+ return initialValueToUse;
+ }
+ }, [initialValue, key, deserializer]);
+
+ const [storedValue, setStoredValue] = useState(() => {
+ if (initializeWithValue) {
+ return readValue();
+ }
+
+ return initialValue instanceof Function ? initialValue() : initialValue;
+ });
+
+ // Return a wrapped version of useState's setter function that ...
+ // ... persists the new value to localStorage.
+ const setValue: Dispatch> = useCallback(
+ (value) => {
+ // Prevent build error "window is undefined" but keeps working
+ if (IS_SERVER) {
+ console.warn(
+ `Tried setting localStorage key “${key}” even though environment is not a client`,
+ );
+ }
+
+ try {
+ // Allow value to be a function so we have the same API as useState
+ const newValue = value instanceof Function ? value(readValue()) : value;
+
+ // Save to local storage
+ window.localStorage.setItem(key, serializer(newValue));
+
+ // Save state
+ setStoredValue(newValue);
+
+ // We dispatch a custom event so every similar useLocalStorage hook is notified
+ window.dispatchEvent(new StorageEvent("local-storage", { key }));
+ } catch (error) {
+ console.warn(`Error setting localStorage key “${key}”:`, error);
+ }
+ },
+ [key, serializer, readValue],
+ );
+
+ const removeValue = useCallback(() => {
+ // Prevent build error "window is undefined" but keeps working
+ if (IS_SERVER) {
+ console.warn(
+ `Tried removing localStorage key “${key}” even though environment is not a client`,
+ );
+ }
+
+ const defaultValue =
+ initialValue instanceof Function ? initialValue() : initialValue;
+
+ // Remove the key from local storage
+ window.localStorage.removeItem(key);
+
+ // Save state with default value
+ setStoredValue(defaultValue);
+
+ // We dispatch a custom event so every similar useLocalStorage hook is notified
+ window.dispatchEvent(new StorageEvent("local-storage", { key }));
+ }, [key]);
+
+ useEffect(() => {
+ setStoredValue(readValue());
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [key]);
+
+ const handleStorageChange = useCallback(
+ (event: StorageEvent | CustomEvent) => {
+ if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
+ return;
+ }
+ setStoredValue(readValue());
+ },
+ [key, readValue],
+ );
+
+ useEffect(() => {
+ addEventListener("storage", handleStorageChange);
+ // this is a custom event, triggered in writeValueToLocalStorage
+ addEventListener("local-storage", handleStorageChange);
+ return () => {
+ removeEventListener("storage", handleStorageChange);
+ removeEventListener("local-storage", handleStorageChange);
+ };
+ }, []);
+
+ return [storedValue, setValue, removeValue];
+}
\ No newline at end of file
diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts
index 0822473f..53adeb8d 100644
--- a/src/core/stores/deviceStore.ts
+++ b/src/core/stores/deviceStore.ts
@@ -26,7 +26,8 @@ export type DialogVariant =
| "deviceName"
| "nodeRemoval"
| "pkiBackup"
- | "nodeDetails";
+ | "nodeDetails"
+ | "unsafeRoles";
export interface Device {
id: number;
@@ -63,6 +64,7 @@ export interface Device {
nodeRemoval: boolean;
pkiBackup: boolean;
nodeDetails: boolean;
+ unsafeRoles: boolean;
};
setStatus: (status: Types.DeviceStatusEnum) => void;
@@ -146,6 +148,7 @@ export const useDeviceStore = createStore((set, get) => ({
nodeRemoval: false,
pkiBackup: false,
nodeDetails: false,
+ unsafeRoles: false,
},
pendingSettingsChanges: false,
messageDraft: "",
@@ -303,7 +306,7 @@ export const useDeviceStore = createStore((set, get) => ({
.findIndex(
(wmc) =>
wmc.payloadVariant.case ===
- moduleConfig.payloadVariant.case,
+ moduleConfig.payloadVariant.case,
);
if (workingModuleConfigIndex !== -1) {
device.workingModuleConfig[workingModuleConfigIndex] =
diff --git a/src/pages/Config/DeviceConfig.tsx b/src/pages/Config/DeviceConfig.tsx
index 0f6df3b1..45ae5f19 100644
--- a/src/pages/Config/DeviceConfig.tsx
+++ b/src/pages/Config/DeviceConfig.tsx
@@ -1,5 +1,5 @@
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
-import { Device } from "@components/PageComponents/Config/Device.tsx";
+import { Device } from "../../components/PageComponents/Config/Device/index.tsx";
import { Display } from "@components/PageComponents/Config/Display.tsx";
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
import { Network } from "@components/PageComponents/Config/Network.tsx";
diff --git a/src/tests/setupTests.ts b/src/tests/setupTests.ts
index d9295846..cc80d782 100644
--- a/src/tests/setupTests.ts
+++ b/src/tests/setupTests.ts
@@ -1,5 +1,10 @@
+import { vi } from 'vitest';
import "@testing-library/jest-dom";
+// Enable auto mocks for our UI components
+//vi.mock('@components/UI/Dialog.tsx');
+//vi.mock('@components/UI/Typography/Link.tsx');
+
globalThis.ResizeObserver = class {
observe() { }
unobserve() { }
diff --git a/vite.config.ts b/vite.config.ts
index 79a89ea4..986169fb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,9 @@
-import { defineConfig } from 'vite';
+import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
-import path from 'node:path';
import { execSync } from 'node:child_process';
+import process from "node:process";
+import path from 'node:path';
let hash = '';
try {
@@ -32,7 +33,6 @@ export default defineConfig({
},
resolve: {
alias: {
- // Using Node's path and process.cwd() instead of Deno.cwd()
'@app': path.resolve(process.cwd(), './src'),
'@pages': path.resolve(process.cwd(), './src/pages'),
'@components': path.resolve(process.cwd(), './src/components'),
@@ -49,6 +49,10 @@ export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
+ mockReset: true,
+ clearMocks: true,
+ restoreMocks: true,
+ root: path.resolve(process.cwd(), './src'),
include: ['**/*.{test,spec}.{ts,tsx}'],
setupFiles: ["./src/tests/setupTests.ts"],