diff --git a/eslint.config.mjs b/eslint.config.mjs index 3629f9176..6956cd1b5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,6 +17,8 @@ const compat = new FlatCompat({ export default defineConfig([ { + files: ["**/*.{js,mjs,cjs,jsx}"], + extends: fixupConfigRules(compat.extends("next/core-web-vitals", "prettier", "plugin:react-hooks/recommended")), plugins: { diff --git a/src/components/errorboundry.jsx b/src/components/errorboundry.jsx index 1b2a3b1ee..fd18c18e1 100644 --- a/src/components/errorboundry.jsx +++ b/src/components/errorboundry.jsx @@ -15,7 +15,6 @@ export default class ErrorBoundary extends React.Component { // You can also log error messages to an error reporting service here if (error || errorInfo) { - // eslint-disable-next-line no-console console.error("component error: %s, info: %s", error, errorInfo); } } diff --git a/src/components/quicklaunch.jsx b/src/components/quicklaunch.jsx index b1f4de0ad..f89192a66 100644 --- a/src/components/quicklaunch.jsx +++ b/src/components/quicklaunch.jsx @@ -147,7 +147,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea let descriptionMatch; if (searchDescriptions) { descriptionMatch = r.description?.toLowerCase().includes(searchString); - r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch; // eslint-disable-line no-param-reassign + r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch; } return nameMatch || descriptionMatch; }); @@ -244,7 +244,6 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea {parts.map((part, i) => part.toLowerCase() === searchString.toLowerCase() ? ( - // eslint-disable-next-line react/no-array-index-key {part} diff --git a/src/components/services/widget/error.jsx b/src/components/services/widget/error.jsx index 2040dc29e..381008f6e 100644 --- a/src/components/services/widget/error.jsx +++ b/src/components/services/widget/error.jsx @@ -13,13 +13,13 @@ export default function Error({ error }) { const { t } = useTranslation(); if (typeof error === "string") { - error = { message: error }; // eslint-disable-line no-param-reassign + error = { message: error }; } else if (typeof error === "number") { - error = { message: `Error ${error}` }; // eslint-disable-line no-param-reassign + error = { message: `Error ${error}` }; } if (error?.data?.error) { - error = error.data.error; // eslint-disable-line no-param-reassign + error = error.data.error; } return ( diff --git a/src/components/widgets/resources/network.jsx b/src/components/widgets/resources/network.jsx index 8224d8e70..8d57097c7 100644 --- a/src/components/widgets/resources/network.jsx +++ b/src/components/widgets/resources/network.jsx @@ -7,7 +7,6 @@ import Resource from "../widget/resource"; export default function Network({ options, refresh = 1500 }) { const { t } = useTranslation(); - // eslint-disable-next-line no-param-reassign if (options.network === true) options.network = "default"; const { data, error } = useSWR(`/api/widgets/resources?type=network&interfaceName=${options.network}`, { diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx index fde6a8282..3fb810a92 100644 --- a/src/components/widgets/search/search.jsx +++ b/src/components/widgets/search/search.jsx @@ -11,7 +11,7 @@ import { } from "@headlessui/react"; import classNames from "classnames"; import { useTranslation } from "next-i18next/pages"; -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect, useMemo, useState } from "react"; import { BiLogoBing } from "react-icons/bi"; import { FiSearch } from "react-icons/fi"; import { SiBaidu, SiBrave, SiDuckduckgo, SiGoogle } from "react-icons/si"; @@ -57,12 +57,12 @@ export const searchProviders = { }, }; -function getAvailableProviderIds(options) { - if (options.provider && Array.isArray(options.provider)) { - return options.provider.filter((value) => searchProviders.hasOwnProperty(value)); +function getAvailableProviderIds(provider) { + if (provider && Array.isArray(provider)) { + return provider.filter((value) => searchProviders.hasOwnProperty(value)); } - if (options.provider && searchProviders[options.provider]) { - return [options.provider]; + if (provider && searchProviders[provider]) { + return [provider]; } return null; } @@ -82,7 +82,8 @@ export function getStoredProvider() { export default function Search({ options }) { const { t } = useTranslation(); - const availableProviderIds = getAvailableProviderIds(options) ?? []; + // options is a fresh object each render, so memo on provider itself + const availableProviderIds = useMemo(() => getAvailableProviderIds(options.provider) ?? [], [options.provider]); const [query, setQuery] = useState(""); const [selectedProvider, setSelectedProvider] = useState(searchProviders[availableProviderIds[0] ?? "google"]); @@ -184,7 +185,6 @@ export default function Search({ options }) { autoCapitalize="off" autoCorrect="off" autoComplete="off" - // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={options.focus} onBlur={(e) => e.preventDefault()} onKeyDown={handleSearchKeyDown} diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx index 54460fcbe..ed16f2b53 100644 --- a/src/components/widgets/unifi_console/unifi_console.jsx +++ b/src/components/widgets/unifi_console/unifi_console.jsx @@ -14,7 +14,6 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Widget({ options }) { const { t } = useTranslation(); - // eslint-disable-next-line no-param-reassign, no-multi-assign options.service_group = options.service_name = "unifi_console"; const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index }); @@ -39,8 +38,8 @@ export default function Widget({ options }) { const lan = defaultSite.health.find((h) => h.subsystem === "lan"); const wlan = defaultSite.health.find((h) => h.subsystem === "wlan"); [wan, lan, wlan].forEach((s) => { - s.up = s.status === "ok"; // eslint-disable-line no-param-reassign - s.show = s.status !== "unknown"; // eslint-disable-line no-param-reassign + s.up = s.status === "ok"; + s.show = s.status !== "unknown"; }); const name = wan.gw_name ?? defaultSite.desc; const uptime = wan["gw_system-stats"] ? wan["gw_system-stats"].uptime : null; diff --git a/src/components/widgets/widget/container.jsx b/src/components/widgets/widget/container.jsx index fe6397e0b..6c48b9209 100644 --- a/src/components/widgets/widget/container.jsx +++ b/src/components/widgets/widget/container.jsx @@ -10,7 +10,6 @@ import WidgetIcon from "./widget_icon"; export function getAllClasses(options, additionalClassNames = "") { if (options?.style?.header === "boxedWidgets") { if (options?.style?.cardBlur !== undefined) { - // eslint-disable-next-line no-param-reassign additionalClassNames = [ additionalClassNames, `backdrop-blur${options.style.cardBlur.length ? "-" : ""}${options.style.cardBlur}`, diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 3767d366d..809e7b216 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { SessionProvider } from "next-auth/react"; import { appWithTranslation } from "next-i18next/pages"; import Head from "next/head"; @@ -13,7 +12,6 @@ import { ThemeProvider } from "utils/contexts/theme"; import nextI18nextConfig from "../../next-i18next.config"; -// eslint-disable-next-line no-unused-vars const tailwindSafelist = [ // TODO: remove pending https://github.com/tailwindlabs/tailwindcss/pull/17147 "backdrop-blur", diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 02adabe16..d5c5296cf 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/no-array-index-key */ import classNames from "classnames"; import BookmarksGroup from "components/bookmarks/group"; import ErrorBoundary from "components/errorboundry"; @@ -175,7 +174,7 @@ function Index({ initialSettings, fallback }) {
-                  Reason: "{error.reason}" at line {error.mark?.line}
+                  Reason: "{error.reason}" at line {error.mark?.line}
                 
Check logs for details.
diff --git a/src/test-utils/render-with-providers.jsx b/src/test-utils/render-with-providers.jsx index 7d6073fa9..2f9768912 100644 --- a/src/test-utils/render-with-providers.jsx +++ b/src/test-utils/render-with-providers.jsx @@ -1,5 +1,4 @@ import { render } from "@testing-library/react"; - import { SettingsContext } from "utils/contexts/settings"; export function renderWithProviders(ui, { settings = {} } = {}) { diff --git a/src/utils/contexts/color.jsx b/src/utils/contexts/color.jsx index 909b8e986..f75ced94f 100644 --- a/src/utils/contexts/color.jsx +++ b/src/utils/contexts/color.jsx @@ -32,7 +32,6 @@ export function ColorProvider({ initialTheme, children }) { useEffect(() => { if (initialTheme !== undefined) setColor(initialTheme ?? getInitialColor()); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialTheme]); useEffect(() => { diff --git a/src/utils/contexts/theme.jsx b/src/utils/contexts/theme.jsx index 76dd37800..81acff46a 100644 --- a/src/utils/contexts/theme.jsx +++ b/src/utils/contexts/theme.jsx @@ -33,7 +33,6 @@ export function ThemeProvider({ initialTheme, children }) { useEffect(() => { if (initialTheme !== undefined) setTheme(initialTheme ?? getInitialTheme()); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialTheme]); useEffect(() => { diff --git a/src/widgets/arcane/component.jsx b/src/widgets/arcane/component.jsx index fe5b614a2..20307bfdc 100644 --- a/src/widgets/arcane/component.jsx +++ b/src/widgets/arcane/component.jsx @@ -16,14 +16,16 @@ export default function Component({ service }) { widget.fields = widget.fields.slice(0, MAX_FIELDS); } - if (widget?.env == null || widget.env === "") { + const envNotSet = widget.env == null || widget.env === ""; + + const { data: containers, error: containersError } = useWidgetAPI(widget, envNotSet ? "" : "containers"); + const { data: images, error: imagesError } = useWidgetAPI(widget, envNotSet ? "" : "images"); + const { data: updates, error: updatesError } = useWidgetAPI(widget, envNotSet ? "" : "updates"); + + if (envNotSet) { return ; } - const { data: containers, error: containersError } = useWidgetAPI(widget, "containers"); - const { data: images, error: imagesError } = useWidgetAPI(widget, "images"); - const { data: updates, error: updatesError } = useWidgetAPI(widget, "updates"); - const error = containersError ?? imagesError ?? updatesError ?? containers?.detail ?? images?.detail ?? updates?.detail; if (error) { diff --git a/src/widgets/arcane/component.test.jsx b/src/widgets/arcane/component.test.jsx index 5e6a78587..466cbcc71 100644 --- a/src/widgets/arcane/component.test.jsx +++ b/src/widgets/arcane/component.test.jsx @@ -20,11 +20,15 @@ describe("widgets/arcane/component", () => { }); it("shows an environment required error when env is missing", () => { + useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined })); + renderWithProviders(, { settings: { hideErrors: false }, }); - expect(useWidgetAPI).not.toHaveBeenCalled(); + // hooks always run; the empty endpoint is what skips the request + expect(useWidgetAPI).toHaveBeenCalledTimes(3); + useWidgetAPI.mock.calls.forEach((call) => expect(call[1]).toBe("")); expect(screen.getByText("arcane.environment_required")).toBeInTheDocument(); }); diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx index a6ef84872..d4327ef60 100644 --- a/src/widgets/diskstation/component.jsx +++ b/src/widgets/diskstation/component.jsx @@ -27,7 +27,6 @@ export default function Component({ service }) { } // uptime info - // eslint-disable-next-line no-unused-vars const [hour, minutes, seconds] = infoData.data.up_time.split(":"); const days = Math.floor(hour / 24); const uptime = `${t("common.number", { value: days })} ${t("diskstation.days")}`; diff --git a/src/widgets/glances/metrics/gpu.jsx b/src/widgets/glances/metrics/gpu.jsx index 8faca8fda..4c99e698d 100644 --- a/src/widgets/glances/metrics/gpu.jsx +++ b/src/widgets/glances/metrics/gpu.jsx @@ -28,7 +28,6 @@ export default function Component({ service }) { useEffect(() => { if (data && !data.error) { - // eslint-disable-next-line eqeqeq const gpuData = data.find((item) => item[item.key] == gpuName); if (gpuData) { @@ -56,7 +55,6 @@ export default function Component({ service }) { ); } - // eslint-disable-next-line eqeqeq const gpuData = data.find((item) => item[item.key] == gpuName); if (!gpuData) { diff --git a/src/widgets/sparkyfitness/component.jsx b/src/widgets/sparkyfitness/component.jsx index 82152b375..e156ef619 100644 --- a/src/widgets/sparkyfitness/component.jsx +++ b/src/widgets/sparkyfitness/component.jsx @@ -1,6 +1,7 @@ import Block from "components/services/widget/block"; import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; + import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { diff --git a/src/widgets/spoolman/component.jsx b/src/widgets/spoolman/component.jsx index b5661c91e..a30b67f7e 100644 --- a/src/widgets/spoolman/component.jsx +++ b/src/widgets/spoolman/component.jsx @@ -8,7 +8,6 @@ export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; - // eslint-disable-next-line prefer-const let { data: spoolData, error: spoolError } = useWidgetAPI(widget, "spools"); if (spoolError) { @@ -20,7 +19,6 @@ export default function Component({ service }) { return ( {[...Array(nBlocksGuess)].map((_, i) => ( - // eslint-disable-next-line react/no-array-index-key ))} diff --git a/src/widgets/tautulli/component.jsx b/src/widgets/tautulli/component.jsx index e7a578ab7..ea4edf7d4 100644 --- a/src/widgets/tautulli/component.jsx +++ b/src/widgets/tautulli/component.jsx @@ -1,4 +1,3 @@ -/* eslint-disable camelcase */ import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs"; diff --git a/src/widgets/tracearr/component.jsx b/src/widgets/tracearr/component.jsx index cdba1d82b..e2586d223 100644 --- a/src/widgets/tracearr/component.jsx +++ b/src/widgets/tracearr/component.jsx @@ -1,4 +1,3 @@ -/* eslint-disable camelcase */ import Block from "components/services/widget/block"; import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; diff --git a/src/widgets/unifi/component.jsx b/src/widgets/unifi/component.jsx index 689459faf..11770fd36 100644 --- a/src/widgets/unifi/component.jsx +++ b/src/widgets/unifi/component.jsx @@ -38,8 +38,8 @@ export default function Component({ service }) { const lan = defaultSite.health.find((h) => h.subsystem === "lan"); const wlan = defaultSite.health.find((h) => h.subsystem === "wlan"); [wan, lan, wlan].forEach((s) => { - s.up = s.status === "ok"; // eslint-disable-line no-param-reassign - s.show = s.status !== "unknown"; // eslint-disable-line no-param-reassign + s.up = s.status === "ok"; + s.show = s.status !== "unknown"; }); const uptime = wan["gw_system-stats"]