diff --git a/web/e2e/specs/settings/object-filters.spec.ts b/web/e2e/specs/settings/object-filters.spec.ts new file mode 100644 index 0000000000..5db405d38f --- /dev/null +++ b/web/e2e/specs/settings/object-filters.spec.ts @@ -0,0 +1,141 @@ +/** + * Object filters settings tests -- MEDIUM tier. + * + * `objects.filters` is an additionalProperties map, so each label's filter is + * an entry RJSF adds at runtime. RJSF has changed how a cleared field nested + * inside such an entry is stored (`""` before 6.9, omitted after). These tests + * pin what Frigate does with it: the save payload deletes only the cleared + * key, and restoring the value leaves the section clean. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; +import { configFactory } from "../../fixtures/mock-data/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_SCHEMA = JSON.parse( + readFileSync( + resolve(__dirname, "../../fixtures/mock-data/config-schema.json"), + "utf-8", + ), +); + +const SETTINGS_URL = "/settings?page=cameraObjects&camera=front_door"; +const UNSAVED = "You have unsaved changes"; + +// A non-default value can only come from the YAML, so deleting it is safe +const MIN_AREA = 5000; + +async function installRoutes(page: Page) { + const config = configFactory({ + cameras: { + front_door: { + objects: { + filters: { + person: { + min_area: MIN_AREA, + max_area: 24000000, + min_ratio: 0, + max_ratio: 24000000, + threshold: 0.7, + min_score: 0.5, + }, + }, + }, + }, + }, + }); + + let lastSavedConfig: unknown = null; + + await page.route("**/api/config/schema.json", (route) => + route.fulfill({ json: CONFIG_SCHEMA }), + ); + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ json: {} }), + ); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + + return { capturedConfig: () => lastSavedConfig }; +} + +async function openPersonMinArea(page: Page) { + await page.getByText("Object filters", { exact: true }).click(); + await page.locator('[aria-expanded="false"]', { hasText: /^Person/ }).click(); + + const minArea = page.getByRole("textbox", { name: "Minimum object area" }); + await expect(minArea).toHaveValue(String(MIN_AREA)); + return minArea; +} + +test.describe("object filters additionalProperties entries @medium", () => { + test("clearing a nested filter field deletes only that key on save", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page); + await frigateApp.goto(SETTINGS_URL); + + const minArea = await openPersonMinArea(frigateApp.page); + await minArea.fill(""); + + await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible(); + await frigateApp.page + .getByRole("button", { name: "Save", exact: true }) + .click(); + + // Empty string is the backend's remove sentinel. Sibling filter fields + // must not appear, or the save would rewrite values the user didn't touch. + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + cameras: { + front_door: { objects: { filters: { person: { min_area: "" } } } }, + }, + }, + }); + const saved = capture.capturedConfig() as { + config_data: { + cameras: { + front_door: { objects: { filters: { person: object } } }; + }; + }; + }; + expect(saved.config_data.cameras.front_door.objects.filters.person).toEqual( + { min_area: "" }, + ); + }); + + test("restoring a cleared nested filter field leaves the section clean", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page); + await frigateApp.goto(SETTINGS_URL); + + const minArea = await openPersonMinArea(frigateApp.page); + const save = frigateApp.page.getByRole("button", { + name: "Save", + exact: true, + }); + + await minArea.fill(""); + await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible(); + await expect(save).toBeEnabled(); + + await minArea.fill(String(MIN_AREA)); + await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden(); + await expect(save).toBeDisabled(); + }); +}); diff --git a/web/package-lock.json b/web/package-lock.json index 83807ceccd..d907619875 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,10 +34,10 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.2.8", - "@rjsf/core": "^6.4.1", - "@rjsf/shadcn": "^6.5.2", - "@rjsf/utils": "^6.4.1", - "@rjsf/validator-ajv8": "^6.4.1", + "@rjsf/core": "^6.10.0", + "@rjsf/shadcn": "^6.10.0", + "@rjsf/utils": "^6.10.0", + "@rjsf/validator-ajv8": "^6.10.0", "apexcharts": "^7.3.0", "axios": "^1.18.0", "class-variance-authority": "^0.7.1", @@ -5168,15 +5168,6 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@react-icons/all-files": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz", - "integrity": "sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, "node_modules/@remix-run/router": { "version": "1.23.4", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", @@ -5187,73 +5178,63 @@ } }, "node_modules/@rjsf/core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.5.2.tgz", - "integrity": "sha512-Fx+aVNQRYQyoY0vM8zYDZkuOiNe+5PLsxUySUdHfjljlT23mJnTCpPKMkxWJwh4UEWeSN0xjmknW9LfIwuQmOg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.10.0.tgz", + "integrity": "sha512-fdyaPnhIe+NzTFZcYcYcp/QqVcAJhhloIsU6vN+hp/IhiGJrzgB9REMVVUlKYNzP37xZ/BsEq7u7RC24KKdLAQ==", "license": "Apache-2.0", "dependencies": { - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", - "markdown-to-jsx": "^8.0.0", - "prop-types": "^15.8.1" + "markdown-to-jsx": "^9.8.2" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@rjsf/utils": "^6.5.x", + "@rjsf/utils": "^6.10.0", "react": ">=18" } }, "node_modules/@rjsf/shadcn": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.5.2.tgz", - "integrity": "sha512-h3FsGRy07Gw4MrORlxNTuSb7icuyiUM0KXimg1jgtlYLhYvMKaHzJgdL/SrAyBWiUtXIsxuBhPU3rFxnA9Ml6Q==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.10.0.tgz", + "integrity": "sha512-rEUvRannFGWrdH0Mdl4E9NPv9htnUv8hZhMXR+hzO3wpCOlKGmMvThvzqLnkIFtolLu296OW+cTWK6x/POHqOQ==", "license": "MIT", "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.0", - "@react-icons/all-files": "^4.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", - "lucide-react": "^0.548.0", - "tailwind-merge": "^3.5.0", - "tailwindcss-animate": "^1.0.7", - "uuid": "^14.0.0" + "lucide-react": "^1.16.0", + "tailwind-merge": "^3.6.0" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@rjsf/core": "^6.5.x", - "@rjsf/utils": "^6.5.x", + "@rjsf/core": "^6.10.0", + "@rjsf/utils": "^6.10.0", "react": ">=18" } }, "node_modules/@rjsf/shadcn/node_modules/lucide-react": { - "version": "0.548.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", - "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.46.0.tgz", + "integrity": "sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@rjsf/shadcn/node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", "license": "MIT", "funding": { "type": "github", @@ -5261,17 +5242,15 @@ } }, "node_modules/@rjsf/utils": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.5.2.tgz", - "integrity": "sha512-qBVQ5qf9BKMOQy/DjMl/IjD5s6akRDx18cgiSYunKt/CYc+kPzHyCVA2TU0rXCsigNlhgnfM8piC3LEHye6vpA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-GQa+dr28FgGOaRRhIOX3QEv1GkzoklrZYf4f1bTEDNuAWWWyO1oznw62ryZBj4FI/XKPaE/VyJQlssOSbD5KiQ==", "license": "Apache-2.0", "dependencies": { "@x0k/json-schema-merge": "^1.0.3", "fast-equals": "^6.0.0", - "fast-uri": "^3.1.0", + "fast-uri": "^4.1.4", "jsonpointer": "^5.0.1", - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", "react-is": "^18.3.1" }, "engines": { @@ -5290,28 +5269,42 @@ "node": ">=6.0.0" } }, + "node_modules/@rjsf/utils/node_modules/fast-uri": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz", + "integrity": "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/@rjsf/validator-ajv8": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.4.1.tgz", - "integrity": "sha512-Gx28sRIV7E4CYs2c7BxOGLX44p5IlJE+IaD7GbVk1S+6TxDATqFBSYYZukLB+/vNk3urpndQMreQLKW3W7POHQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.10.0.tgz", + "integrity": "sha512-B/cNuPIQNBiJo4ByoJUXwGylu51JA5Jhkj4gsZ794dl85xxfcQ43HKfR1Lg55SoCUOzHc0Sf7zI/0q3R3sKEEQ==", "license": "Apache-2.0", "dependencies": { - "ajv": "^8.17.1", - "ajv-formats": "^2.1.1", - "lodash": "^4.17.23", - "lodash-es": "^4.17.23" + "ajv": "^8.20.0", + "ajv-formats": "^2.1.1" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@rjsf/utils": "^6.4.x" + "@rjsf/utils": "^6.10.0" } }, "node_modules/@rjsf/validator-ajv8/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -10043,12 +10036,6 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -10155,19 +10142,30 @@ } }, "node_modules/markdown-to-jsx": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-8.0.0.tgz", - "integrity": "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA==", + "version": "9.10.2", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.10.2.tgz", + "integrity": "sha512-iR9GadlIox0q1uXnpqdxpF02Vb1WDmZ/QIXWjBR5htzjUEEhyIWbM65LuVKavdwJaVo4q95C/F2OOyNjWe91ig==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 18" }, "peerDependencies": { - "react": ">= 0.14.0" + "react": ">= 16.0.0", + "solid-js": ">=1.0.0", + "vue": ">=3.0.0" }, "peerDependenciesMeta": { "react": { "optional": true + }, + "react-native": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "vue": { + "optional": true } } }, @@ -13841,19 +13839,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", diff --git a/web/package.json b/web/package.json index 33b5db9ee7..1b2e11a530 100644 --- a/web/package.json +++ b/web/package.json @@ -48,10 +48,10 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.2.8", - "@rjsf/core": "^6.4.1", - "@rjsf/shadcn": "^6.5.2", - "@rjsf/utils": "^6.4.1", - "@rjsf/validator-ajv8": "^6.4.1", + "@rjsf/core": "^6.10.0", + "@rjsf/shadcn": "^6.10.0", + "@rjsf/utils": "^6.10.0", + "@rjsf/validator-ajv8": "^6.10.0", "apexcharts": "^7.3.0", "axios": "^1.18.0", "class-variance-authority": "^0.7.1",