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