Browse Source
* Config reset work WIP * Config reset WIP * Fix tests, tsc, linting * Form reset adjustments * Add ManagedModeDialog * Remove debug logging * Add Suspense * Review fixes --------- Co-authored-by: philon- <[email protected]>pull/670/head
committed by
GitHub
46 changed files with 1379 additions and 280 deletions
@ -0,0 +1,72 @@ |
|||
import { Button } from "@components/UI/Button.tsx"; |
|||
import { |
|||
Dialog, |
|||
DialogClose, |
|||
DialogContent, |
|||
DialogDescription, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.tsx"; |
|||
import { Trans, useTranslation } from "react-i18next"; |
|||
import { Checkbox } from "@components/UI/Checkbox/index.tsx"; |
|||
import { useState } from "react"; |
|||
|
|||
export interface ManagedModeDialogProps { |
|||
open: boolean; |
|||
onOpenChange: () => void; |
|||
onSubmit: () => void; |
|||
} |
|||
|
|||
export const ManagedModeDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
onSubmit, |
|||
}: ManagedModeDialogProps) => { |
|||
const { t } = useTranslation("dialog"); |
|||
const [confirmState, setConfirmState] = useState(false); |
|||
|
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent> |
|||
<DialogClose /> |
|||
<DialogHeader> |
|||
<DialogTitle>{t("managedMode.title")}</DialogTitle> |
|||
<DialogDescription> |
|||
<Trans |
|||
i18nKey="managedMode.description" |
|||
components={{ |
|||
"bold": <p className="font-bold inline" />, |
|||
}} |
|||
/> |
|||
</DialogDescription> |
|||
</DialogHeader> |
|||
<div className="flex items-center gap-2"> |
|||
<Checkbox |
|||
id="managedMode" |
|||
checked={confirmState} |
|||
onChange={() => setConfirmState(!confirmState)} |
|||
name="confirmUnderstanding" |
|||
> |
|||
<p className="dark:text-white pt-1"> |
|||
{t("managedMode.confirmUnderstanding")} |
|||
</p> |
|||
</Checkbox> |
|||
</div> |
|||
<DialogFooter> |
|||
<Button |
|||
variant="destructive" |
|||
name="regenerate" |
|||
disabled={!confirmState} |
|||
onClick={() => { |
|||
setConfirmState(false); |
|||
onSubmit(); |
|||
}} |
|||
> |
|||
{t("button.confirm")} |
|||
</Button> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,37 @@ |
|||
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; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { deepCompareConfig } from "./deepCompareConfig.ts"; |
|||
|
|||
describe("deepCompareConfig", () => { |
|||
it("returns true for identical primitives", () => { |
|||
expect(deepCompareConfig(5, 5)).toBe(true); |
|||
expect(deepCompareConfig("foo", "foo")).toBe(true); |
|||
expect(deepCompareConfig(true, true)).toBe(true); |
|||
}); |
|||
|
|||
it("returns false for different primitives", () => { |
|||
expect(deepCompareConfig(5, 6)).toBe(false); |
|||
expect(deepCompareConfig("foo", "bar")).toBe(false); |
|||
expect(deepCompareConfig(true, false)).toBe(false); |
|||
}); |
|||
|
|||
it("handles nulls correctly", () => { |
|||
expect(deepCompareConfig(null, null)).toBe(true); |
|||
expect(deepCompareConfig(null, undefined)).toBe(false); |
|||
expect(deepCompareConfig(null, {})).toBe(false); |
|||
}); |
|||
|
|||
it("allows undefined in working when allowUndefined is true", () => { |
|||
expect(deepCompareConfig({ a: 1 }, { a: undefined }, true)).toBe(true); |
|||
expect(deepCompareConfig([1, 2, 3], [1, undefined, 3], true)).toBe(true); |
|||
}); |
|||
|
|||
it("rejects undefined in working when allowUndefined is false", () => { |
|||
expect(deepCompareConfig({ a: 1 }, { a: undefined }, false)).toBe(false); |
|||
}); |
|||
|
|||
it("compares arrays deeply", () => { |
|||
expect(deepCompareConfig([1, [2, 3]], [1, [2, 3]])).toBe(true); |
|||
expect(deepCompareConfig([1, [2, 3]], [1, [2, 4]])).toBe(false); |
|||
}); |
|||
|
|||
it("compares objects deeply", () => { |
|||
const existing = { x: 10, y: { z: 20 } }; |
|||
const workingEqual = { x: 10, y: { z: 20 } }; |
|||
const workingDiff = { x: 10, y: { z: 21 } }; |
|||
|
|||
expect(deepCompareConfig(existing, workingEqual)).toBe(true); |
|||
expect(deepCompareConfig(existing, workingDiff)).toBe(false); |
|||
}); |
|||
|
|||
it("ignores $typeName key in existing", () => { |
|||
const existing = { $typeName: "Test", a: 1 }; |
|||
const working = { a: 1 }; |
|||
expect(deepCompareConfig(existing, working)).toBe(true); |
|||
}); |
|||
|
|||
it("fails when working has extra keys", () => { |
|||
expect(deepCompareConfig({ a: 1 }, { a: 1, b: 2 })).toBe(false); |
|||
}); |
|||
|
|||
it("allows working arrays to be shorter if allowUndefined is true", () => { |
|||
expect(deepCompareConfig([1, 2, 3, 4], [1, 2], true)).toBe(true); |
|||
expect(deepCompareConfig([1, 2, 3, 4], [1, 2], false)).toBe(false); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,58 @@ |
|||
function isObject(value: unknown): value is Record<string, unknown> { |
|||
return typeof value === "object" && value !== null && !Array.isArray(value); |
|||
} |
|||
|
|||
export function deepCompareConfig( |
|||
a: unknown, |
|||
b: unknown, |
|||
allowUndefined = false, |
|||
): boolean { |
|||
if (a === b) { |
|||
return true; |
|||
} |
|||
|
|||
// If allowUndefined is true, and one is undefined, they are considered equal. // This check is placed early to simplify subsequent logic.
|
|||
if (allowUndefined && (a === undefined || b === undefined)) { |
|||
return true; |
|||
} |
|||
|
|||
if (typeof a !== typeof b || a === null || b === null) { |
|||
return false; |
|||
} |
|||
|
|||
if (Array.isArray(a) && Array.isArray(b)) { |
|||
if (a.length !== b.length && !allowUndefined) { |
|||
return false; |
|||
} |
|||
|
|||
const longestLength = Math.max(a.length, b.length); |
|||
for (let i = 0; i < longestLength; i++) { |
|||
if (!deepCompareConfig(a[i], b[i], allowUndefined)) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
if (isObject(a) && isObject(b)) { |
|||
const aKeys = Object.keys(a); |
|||
const bKeys = Object.keys(b); |
|||
const allKeys = new Set([...aKeys, ...bKeys]); |
|||
|
|||
for (const key of allKeys) { |
|||
if (key === "$typeName") { |
|||
continue; |
|||
} |
|||
|
|||
const aValue = a[key]; |
|||
const bValue = b[key]; |
|||
|
|||
if (!deepCompareConfig(aValue, bValue, allowUndefined)) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
Loading…
Reference in new issue