diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json index 8c7f55f9..26a19e91 100644 --- a/web/ui/package-lock.json +++ b/web/ui/package-lock.json @@ -793,45 +793,45 @@ } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", - "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", - "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz", - "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", + "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", - "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" @@ -1190,9 +1190,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.28.12", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.12.tgz", - "integrity": "sha512-c7H3/SMqjzwrFbwHyjPUCVrmrhpDdn1zIZT6ldu50jEBEkZgngF9gF/axD/UmTUZkVYQQW7PgPNp6GWTy4UPSw==", + "version": "5.28.13", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.13.tgz", + "integrity": "sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1209,11 +1209,11 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.28.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.12.tgz", - "integrity": "sha512-AENUgyAuh6GEG9tf+h/oOSYGyy0k8loehlSf9JJLUkiZk3gbbPKrRQ55HpVfb5vrVRuz8hI4d+Apuc8FTuzmnw==", + "version": "5.28.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.14.tgz", + "integrity": "sha512-cZqt03Igb3I9tM72qNX5TAAmeYl75Z+k4Mv92VkXIXc2hCrv0fIywd7GN3JV1BBJl4mr7Cc+OOKKOPy8sNVOkA==", "dependencies": { - "@tanstack/query-core": "5.28.12" + "@tanstack/query-core": "5.28.13" }, "funding": { "type": "github", @@ -1224,9 +1224,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.28.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.12.tgz", - "integrity": "sha512-Vbc/6/sAtgjjFW1MdzQt9lzeRKGazAsmH/9sD266oJdoTTgZ6GPL3kJlYXDaaYEuOuLKAWKM/D+DmyiMYT+OZg==", + "version": "5.28.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.14.tgz", + "integrity": "sha512-4CrFBI1O5wibV1ZdGAnBMmTuc7SiShhxWubxRMyIloeEioxs3DQkFbouGBea5nexuwIxAkvhUB8khpPnNjhxMw==", "dev": true, "dependencies": { "@tanstack/query-devtools": "5.28.10" @@ -1236,7 +1236,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.28.12", + "@tanstack/react-query": "^5.28.14", "react": "^18.0.0" } }, @@ -1462,9 +1462,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", - "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "version": "20.12.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.3.tgz", + "integrity": "sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==", "dependencies": { "undici-types": "~5.26.4" } @@ -2556,11 +2556,11 @@ "name": "argus", "version": "0.16.0", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-brands-svg-icons": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-brands-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", - "@tanstack/react-query": "^5.28.12", + "@tanstack/react-query": "^5.28.14", "@types/bootstrap": "^5.2.10", "@types/jest": "^29.5.12", "@types/react": "^18.2.74", @@ -2582,8 +2582,8 @@ "yaml": "^2.4.1" }, "devDependencies": { - "@tanstack/react-query-devtools": "^5.28.12", - "@types/node": "^20.12.2", + "@tanstack/react-query-devtools": "^5.28.14", + "@types/node": "^20.12.3", "@types/styled-components": "^5.1.34" } }, diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json index 8218d6fd..c69bafc0 100644 --- a/web/ui/react-app/package.json +++ b/web/ui/react-app/package.json @@ -6,11 +6,11 @@ "private": true, "type": "module", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-brands-svg-icons": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-brands-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", - "@tanstack/react-query": "^5.28.12", + "@tanstack/react-query": "^5.28.14", "@types/bootstrap": "^5.2.10", "@types/jest": "^29.5.12", "@types/react": "^18.2.74", @@ -56,8 +56,8 @@ ] }, "devDependencies": { - "@tanstack/react-query-devtools": "^5.28.12", - "@types/node": "^20.12.2", + "@tanstack/react-query-devtools": "^5.28.14", + "@types/node": "^20.12.3", "@types/styled-components": "^5.1.34" } } diff --git a/web/ui/react-app/src/components/approvals/service-info.tsx b/web/ui/react-app/src/components/approvals/service-info.tsx index f583bc1c..9ca59f51 100644 --- a/web/ui/react-app/src/components/approvals/service-info.tsx +++ b/web/ui/react-app/src/components/approvals/service-info.tsx @@ -20,6 +20,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ModalContext } from "contexts/modal"; import { formatRelative } from "date-fns"; +import { isEmptyOrNull } from "utils"; interface Props { service: ServiceSummaryType; @@ -57,7 +58,7 @@ export const ServiceInfo: FC = ({ () => ({ // If version hasn't been found or a new version has been found (and not skipped) warning: - (service?.status?.deployed_version ?? "") === "" || + isEmptyOrNull(service?.status?.deployed_version) || (updateAvailable && !updateSkipped), // If the latest version is the same as the approved version updateApproved: diff --git a/web/ui/react-app/src/components/generic/form-item-colour.tsx b/web/ui/react-app/src/components/generic/form-item-colour.tsx index 2867f38c..21a38bcc 100644 --- a/web/ui/react-app/src/components/generic/form-item-colour.tsx +++ b/web/ui/react-app/src/components/generic/form-item-colour.tsx @@ -45,7 +45,7 @@ const FormItemColour: FC = ({ positionXS = position, }) => { const { register, setValue } = useFormContext(); - const hexColour = useWatch({ name: name }); + const hexColour: string = useWatch({ name: name }); const trimmedHex = hexColour?.replace("#", ""); const error = useError(name, true); const padding = formPadding({ col_xs, col_sm, position, positionXS }); @@ -66,6 +66,7 @@ const FormItemColour: FC = ({ type="text" defaultValue={trimmedHex} placeholder={defaultVal} + maxLength={6} autoFocus={false} {...register(name, { pattern: { @@ -80,10 +81,8 @@ const FormItemColour: FC = ({ style={{ width: "30%" }} type="color" title="Choose your color" - value={hexColour || defaultVal} - onChange={(event) => { - setColour(event.target.value); - }} + value={`#${trimmedHex || defaultVal?.replace("#", "")}`} + onChange={(event) => setColour(event.target.value)} autoFocus={false} /> diff --git a/web/ui/react-app/src/components/generic/form-item-with-preview.tsx b/web/ui/react-app/src/components/generic/form-item-with-preview.tsx index 824c4836..98edcf27 100644 --- a/web/ui/react-app/src/components/generic/form-item-with-preview.tsx +++ b/web/ui/react-app/src/components/generic/form-item-with-preview.tsx @@ -3,6 +3,7 @@ import { FC, useMemo } from "react"; import { useFormContext, useWatch } from "react-hook-form"; import FormLabel from "./form-label"; +import { isEmptyOrNull } from "utils"; import { useError } from "hooks/errors"; interface Props { @@ -70,7 +71,7 @@ const FormItemWithPreview: FC = ({ {...register(name, { validate: (value: string | undefined) => { // Allow empty values - if ((value ?? "") === "") return true; + if (isEmptyOrNull(value)) return true; // Validate that it's a URL (with prefix) try { diff --git a/web/ui/react-app/src/components/generic/form-key-val-map.tsx b/web/ui/react-app/src/components/generic/form-key-val-map.tsx index a8af08ff..f3a53f67 100644 --- a/web/ui/react-app/src/components/generic/form-key-val-map.tsx +++ b/web/ui/react-app/src/components/generic/form-key-val-map.tsx @@ -15,6 +15,7 @@ import FormKeyVal from "./form-key-val"; import { FormLabel } from "components/generic/form"; import { HeaderType } from "types/config"; import { diffObjects } from "utils/diff-objects"; +import { isEmptyArray } from "utils"; interface Props { name: string; @@ -59,7 +60,9 @@ const FormKeyValMap: FC = ({ // useDefaults when the fieldValues are undefined or the same as the defaults const useDefaults = useMemo( () => - (defaults && diffObjects(fieldValues ?? fields ?? [], defaults)) ?? false, + (!isEmptyArray(defaults) && + diffObjects(fieldValues ?? fields ?? [], defaults)) ?? + false, [fieldValues, defaults] ); // trigger validation on change of defaults being used/not diff --git a/web/ui/react-app/src/components/generic/form-list.tsx b/web/ui/react-app/src/components/generic/form-list.tsx index 89292cc8..521f4b97 100644 --- a/web/ui/react-app/src/components/generic/form-list.tsx +++ b/web/ui/react-app/src/components/generic/form-list.tsx @@ -7,6 +7,7 @@ import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { StringFieldArray } from "types/config"; import { diffObjects } from "utils/diff-objects"; +import { isEmptyArray } from "utils"; interface Props { name: string; @@ -45,7 +46,9 @@ const FormList: FC = ({ // useDefaults when the fieldValues are undefined or the same as the defaults const useDefaults = useMemo( () => - (defaults && diffObjects(fieldValues ?? fields ?? [], defaults)) ?? false, + (!isEmptyArray(defaults) && + diffObjects(fieldValues ?? fields ?? [], defaults)) ?? + false, [fieldValues, defaults] ); // trigger validation on change of defaults being used/not @@ -68,7 +71,8 @@ const FormList: FC = ({ // and give the defaults if not overridden useEffect(() => { for (const item of fieldValues ?? fields ?? []) { - if ((item.arg ?? "") === "") { + const keys = Object.keys(item); + if (keys.length > 1 || !keys.includes("arg")) { setValue(name, []); break; } diff --git a/web/ui/react-app/src/components/modals/service-edit/dashboard.tsx b/web/ui/react-app/src/components/modals/service-edit/dashboard.tsx index 18809c9b..1f5bdedf 100644 --- a/web/ui/react-app/src/components/modals/service-edit/dashboard.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/dashboard.tsx @@ -4,7 +4,7 @@ import { FormItem, FormItemWithPreview } from "components/generic/form"; import { Accordion } from "react-bootstrap"; import { BooleanWithDefault } from "components/generic"; import { ServiceDashboardOptionsType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; interface Props { defaults?: ServiceDashboardOptionsType; diff --git a/web/ui/react-app/src/components/modals/service-edit/latest-version-require.tsx b/web/ui/react-app/src/components/modals/service-edit/latest-version-require.tsx index 6d89349c..38d8f1e4 100644 --- a/web/ui/react-app/src/components/modals/service-edit/latest-version-require.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/latest-version-require.tsx @@ -7,10 +7,10 @@ import { } from "types/config"; import { FC, memo, useEffect, useMemo } from "react"; import { FormItem, FormLabel, FormSelect } from "components/generic/form"; +import { firstNonDefault, isEmptyOrNull } from "utils"; import { useFormContext, useWatch } from "react-hook-form"; import Command from "./command"; -import { firstNonDefault } from "components/modals/service-edit/util"; const DockerRegistryOptions = [ { label: "Docker Hub", value: "hub" }, @@ -87,7 +87,7 @@ const EditServiceLatestVersionRequire: FC = ({ useEffect(() => { // Default to Docker Hub if no registry is selected and no default registry. - if ((selectedDockerRegistry ?? "") === "") + if (isEmptyOrNull(selectedDockerRegistry)) setValue("latest_version.require.docker.type", "hub"); }, []); diff --git a/web/ui/react-app/src/components/modals/service-edit/latest-version.tsx b/web/ui/react-app/src/components/modals/service-edit/latest-version.tsx index 84aea555..c009fca3 100644 --- a/web/ui/react-app/src/components/modals/service-edit/latest-version.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/latest-version.tsx @@ -11,7 +11,7 @@ import EditServiceLatestVersionRequire from "./latest-version-require"; import FormURLCommands from "./latest-version-urlcommands"; import { LatestVersionLookupEditType } from "types/service-edit"; import VersionWithRefresh from "./version-with-refresh"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useWatch } from "react-hook-form"; interface Props { diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/bark.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/bark.tsx index 723ab37c..47a88bec 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/bark.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/bark.tsx @@ -8,7 +8,7 @@ import { useEffect, useMemo } from "react"; import { NotifyBarkType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { normaliseForSelect } from "components/modals/service-edit/util"; import { useFormContext } from "react-hook-form"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/discord.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/discord.tsx index 0e533046..cd12494a 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/discord.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/discord.tsx @@ -7,7 +7,7 @@ import { import { BooleanWithDefault } from "components/generic"; import { NotifyDiscordType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { strToBool } from "utils"; import { useMemo } from "react"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/ntfy/actions.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/ntfy/actions.tsx index f38aace9..78caf35c 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/ntfy/actions.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/ntfy/actions.tsx @@ -8,6 +8,7 @@ import { } from "react-bootstrap"; import { FC, memo, useCallback, useEffect, useMemo } from "react"; import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { isEmptyArray, isEmptyOrNull } from "utils"; import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -55,7 +56,7 @@ const NtfyActions: FC = ({ name, label, tooltip, defaults }) => { // useDefaults when the fieldValues are unset or the same as the defaults const useDefaults = useMemo( () => - defaults && + !isEmptyArray(defaults) && diffObjects(fieldValues ?? fields ?? [], defaults, [".action"]), [fieldValues, defaults] ); @@ -82,7 +83,7 @@ const NtfyActions: FC = ({ name, label, tooltip, defaults }) => { useEffect(() => { // ensure we don't have another types actions for (const item of fieldValues ?? fields ?? []) { - if ((item.action ?? "") === "") { + if (isEmptyOrNull(item.action)) { setValue(name, []); break; } diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/opsgenie/targets.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/opsgenie/targets.tsx index c8c804c8..8a16442f 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/opsgenie/targets.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/extra/opsgenie/targets.tsx @@ -16,6 +16,7 @@ import { NotifyOpsGenieTarget } from "types/config"; import OpsGenieTarget from "./target"; import { convertOpsGenieTargetFromString } from "components/modals/service-edit/util"; import { diffObjects } from "utils/diff-objects"; +import { isEmptyArray } from "utils"; interface Props { name: string; @@ -55,7 +56,7 @@ const OpsGenieTargets: FC = ({ name, label, tooltip, defaults }) => { // useDefaults when the fieldValues are undefined or the same as the defaults const useDefaults = useMemo( () => - defaults && + !isEmptyArray(defaults) && diffObjects(fieldValues ?? fields ?? [], defaults, [ ".type", ".sub_type", diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/generic.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/generic.tsx index 343f2d3b..87e8264b 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/generic.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/generic.tsx @@ -12,7 +12,7 @@ import { import { BooleanWithDefault } from "components/generic"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { strToBool } from "utils"; import { useMemo } from "react"; import { useWatch } from "react-hook-form"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/google_chat.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/google_chat.tsx index 5ee64edc..75e17e7b 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/google_chat.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/google_chat.tsx @@ -2,7 +2,7 @@ import { FormLabel, FormTextArea } from "components/generic/form"; import { NotifyGoogleChatType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/gotify.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/gotify.tsx index 9efb1272..ef5f642e 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/gotify.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/gotify.tsx @@ -3,7 +3,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import { BooleanWithDefault } from "components/generic"; import { NotifyGotifyType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { strToBool } from "utils"; import { useMemo } from "react"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/ifttt.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/ifttt.tsx index db7d8480..31c45378 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/ifttt.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/ifttt.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import { NotifyIFTTTType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/join.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/join.tsx index 3321cd02..608f82fd 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/join.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/join.tsx @@ -6,7 +6,7 @@ import { import { NotifyJoinType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/matrix.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/matrix.tsx index 994b42bb..42c37537 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/matrix.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/matrix.tsx @@ -3,7 +3,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import { BooleanWithDefault } from "components/generic"; import { NotifyMatrixType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { strToBool } from "utils"; import { useMemo } from "react"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/mattermost.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/mattermost.tsx index 2096d06e..375b23a9 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/mattermost.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/mattermost.tsx @@ -6,7 +6,7 @@ import { import { NotifyMatterMostType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/ntfy.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/ntfy.tsx index d2a8b627..7527b55a 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/ntfy.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/ntfy.tsx @@ -14,7 +14,7 @@ import { BooleanWithDefault } from "components/generic"; import { NotifyNtfyType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NtfyActions } from "components/modals/service-edit/notify-types/extra"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { strToBool } from "utils"; import { useFormContext } from "react-hook-form"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/opsgenie.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/opsgenie.tsx index 782ab175..ad3736bc 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/opsgenie.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/opsgenie.tsx @@ -13,7 +13,7 @@ import { import { NotifyOpsGenieType } from "types/config"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { OpsGenieTargets } from "components/modals/service-edit/notify-types/extra"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/pushbullet.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/pushbullet.tsx index dc594413..370b06f0 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/pushbullet.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/pushbullet.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyPushbulletType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/pushover.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/pushover.tsx index 723f89e7..929a5fee 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/pushover.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/pushover.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyPushoverType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/rocketchat.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/rocketchat.tsx index c16cf17f..a85761bf 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/rocketchat.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/rocketchat.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyRocketChatType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/shared.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/shared.tsx index ad39221d..0c02fb20 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/shared.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/shared.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel, FormTextArea } from "components/generic/form"; import { memo, useMemo } from "react"; import { NotifyOptionsType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; /** * Returns the form fields for the `notify.X.options` section diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/slack.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/slack.tsx index d8bc1d44..b7300eee 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/slack.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/slack.tsx @@ -7,7 +7,7 @@ import { import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifySlackType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/smtp.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/smtp.tsx index 7515c3c0..fab349e1 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/smtp.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/smtp.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo } from "react"; import { BooleanWithDefault } from "components/generic"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifySMTPType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { normaliseForSelect } from "components/modals/service-edit/util/normalise-selects"; import { strToBool } from "utils"; import { useFormContext } from "react-hook-form"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/teams.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/teams.tsx index 55acbf82..d18a8075 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/teams.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/teams.tsx @@ -2,7 +2,7 @@ import { FormItem, FormItemColour, FormLabel } from "components/generic/form"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyTeamsType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/telegram.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/telegram.tsx index ad14049b..6ba17dad 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/telegram.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/telegram.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo } from "react"; import { BooleanWithDefault } from "components/generic"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyTelegramType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/notify-types/util"; +import { firstNonDefault } from "utils"; import { normaliseForSelect } from "components/modals/service-edit/util"; import { strToBool } from "utils"; import { useFormContext } from "react-hook-form"; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/util.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/util.tsx index 0fc1d108..e13f80da 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/util.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/util.tsx @@ -1,3 +1,5 @@ +import { isEmptyOrNull } from "utils"; + /** * Returns the first non-empty value from a list of values * @@ -9,7 +11,7 @@ export const firstNonDefault: (...args: unknown[]) => string = ( ) => { // Iterate through all arguments and return the first non-empty one for (const arg of args) { - if ((arg ?? "") !== "") return `${arg}`; + if (!isEmptyOrNull(arg)) return `${arg}`; } // If no non-empty argument is found, return an empty string by default return ""; diff --git a/web/ui/react-app/src/components/modals/service-edit/notify-types/zulip.tsx b/web/ui/react-app/src/components/modals/service-edit/notify-types/zulip.tsx index 96ed3fe8..ff5fffae 100644 --- a/web/ui/react-app/src/components/modals/service-edit/notify-types/zulip.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/notify-types/zulip.tsx @@ -2,7 +2,7 @@ import { FormItem, FormLabel } from "components/generic/form"; import NotifyOptions from "components/modals/service-edit/notify-types/shared"; import { NotifyZulipType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; import { useMemo } from "react"; /** diff --git a/web/ui/react-app/src/components/modals/service-edit/options.tsx b/web/ui/react-app/src/components/modals/service-edit/options.tsx index f37f3620..e3ea2daa 100644 --- a/web/ui/react-app/src/components/modals/service-edit/options.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/options.tsx @@ -4,7 +4,7 @@ import { FormCheck, FormItem } from "components/generic/form"; import { Accordion } from "react-bootstrap"; import { BooleanWithDefault } from "components/generic"; import { ServiceOptionsType } from "types/config"; -import { firstNonDefault } from "components/modals/service-edit/util"; +import { firstNonDefault } from "utils"; interface Props { defaults?: ServiceOptionsType; diff --git a/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx b/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx index 84385827..00d3095d 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx @@ -11,8 +11,8 @@ import { ServiceEditOtherData, ServiceEditType, } from "types/service-edit"; +import { firstNonDefault, firstNonEmpty, isEmptyOrNull } from "utils"; -import { firstNonDefault } from "./first-non-default"; import { urlCommandsTrimArray } from "./url-command-trim"; /** @@ -66,8 +66,9 @@ export const convertAPIServiceDataEditToUI = ( ...header, oldIndex: key, })) ?? [], - template_toggle: - (serviceData?.deployed_version?.regex_template ?? "") !== "", + template_toggle: !isEmptyOrNull( + serviceData?.deployed_version?.regex_template + ), }, command: serviceData?.command?.map((args) => ({ args: args.map((arg) => ({ arg })), @@ -83,18 +84,19 @@ export const convertAPIServiceDataEditToUI = ( ...header, oldIndex: index, })) - : otherOptionsData?.webhook?.[whName]?.custom_headers ?? - ( - otherOptionsData?.defaults?.webhook?.[whType] as - | WebHookType - | undefined - )?.custom_headers ?? - ( - otherOptionsData?.hard_defaults?.webhook?.[whType] as - | WebHookType - | undefined - )?.custom_headers ?? - []; + : firstNonEmpty( + otherOptionsData?.webhook?.[whName]?.custom_headers, + ( + otherOptionsData?.defaults?.webhook?.[whType] as + | WebHookType + | undefined + )?.custom_headers, + ( + otherOptionsData?.hard_defaults?.webhook?.[whType] as + | WebHookType + | undefined + )?.custom_headers + ).map(() => ({ key: "", value: "" })); // Return modified item return { @@ -488,10 +490,8 @@ export const convertNotifyParams = ( case "slack": return { ...params, - // Add # to the color if it's a hex code - color: /^[\da-f]{6}$/i.test(params?.color ?? "") - ? `#${params?.color}` - : params?.color, + // Remove hashtag from hex + color: (params?.color ?? "").replace("%23", "#").replace("#", ""), }; // Other diff --git a/web/ui/react-app/src/components/modals/service-edit/util/index.tsx b/web/ui/react-app/src/components/modals/service-edit/util/index.tsx index 1c11e7a5..e7283cea 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/index.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/index.tsx @@ -18,7 +18,6 @@ import { } from "./url-command-trim"; import { convertValuesToString } from "./notify-string-string-map"; -import { firstNonDefault } from "./first-non-default"; import { normaliseForSelect } from "./normalise-selects"; export { @@ -32,7 +31,6 @@ export { convertNotifyToAPI, convertUIServiceDataEditToAPI, convertValuesToString, - firstNonDefault, normaliseForSelect, urlCommandsTrim, urlCommandTrim, diff --git a/web/ui/react-app/src/components/modals/service-edit/util/notify-string-string-map.tsx b/web/ui/react-app/src/components/modals/service-edit/util/notify-string-string-map.tsx index 9dc722a8..c75b6b13 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/notify-string-string-map.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/notify-string-string-map.tsx @@ -5,6 +5,8 @@ import { StringFieldArray, } from "types/config"; +import { isEmptyOrNull } from "utils"; + interface StringAnyMap { [key: string]: | string @@ -37,8 +39,8 @@ export const convertValuesToString = ( if ("responders" === key || "visibleto" === key) { // `value` empty means defaults were used. Skip. if ( - (value as NotifyOpsGenieTarget[]).find( - (item) => (item.value || "") === "" + (value as NotifyOpsGenieTarget[]).find((item) => + isEmptyOrNull(item.value) ) ) { return result; @@ -67,8 +69,8 @@ export const convertValuesToString = ( } else { // `value` empty means defaults were used. Skip. if ( - (value as NotifyOpsGenieTarget[]).find( - (item) => (item.value ?? "") === "" + (value as NotifyOpsGenieTarget[]).find((item) => + isEmptyOrNull(item.value) ) ) { return result; @@ -76,7 +78,14 @@ export const convertValuesToString = ( result[key] = JSON.stringify(flattenHeaderArray(value as HeaderType[])); } } else { - result[key] = String(value); + // Give # to slack hex colours + if (notifyType === "slack" && key === "color") { + result[key] = ( + /^[\da-f]{6}$/i.test(value as string) ? `#${value}` : value + ) as string; + + // Convert to string + } else result[key] = String(value); } return result; }, {} as StringStringMap); diff --git a/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx b/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx index 9f3fff58..0abfc814 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx @@ -79,12 +79,23 @@ export const convertUIServiceDataEditToAPI = ( if (data.webhook) payload.webhook = data.webhook.reduce((acc, webhook) => { webhook = removeEmptyValues(webhook); + // Defaults were being shown if key/value were empty + const removeCustomHeaders = (webhook.custom_headers ?? []).find( + (header) => header.key === "" || header.value === "" + ); acc[webhook.name as string] = { ...webhook, - desired_status_code: webhook?.desired_status_code - ? Number(webhook?.desired_status_code) - : undefined, - max_tries: webhook.max_tries ? Number(webhook.max_tries) : undefined, + custom_headers: removeCustomHeaders + ? undefined + : webhook.custom_headers, + desired_status_code: + webhook?.desired_status_code !== undefined + ? Number(webhook?.desired_status_code) + : undefined, + max_tries: + webhook.max_tries !== undefined + ? Number(webhook.max_tries) + : undefined, }; return acc; }, {} as Dict); diff --git a/web/ui/react-app/src/components/modals/service-edit/util/url-command-trim.tsx b/web/ui/react-app/src/components/modals/service-edit/util/url-command-trim.tsx index 2b3a5e8f..4843a848 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/url-command-trim.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/url-command-trim.tsx @@ -1,4 +1,5 @@ import { URLCommandType } from "types/config"; +import { isEmptyOrNull } from "utils"; /** * Returns a `url_command` object with only the relevant keys for the type @@ -26,7 +27,7 @@ export const urlCommandTrim = ( regex: command.regex, index: command.index ? Number(command.index) : undefined, template: command.template ? command.template : undefined, - template_toggle: (command.template ?? "") !== "", + template_toggle: !isEmptyOrNull(command.template), }; // replace diff --git a/web/ui/react-app/src/components/modals/service-edit/webhook.tsx b/web/ui/react-app/src/components/modals/service-edit/webhook.tsx index 327afc5d..cb70f348 100644 --- a/web/ui/react-app/src/components/modals/service-edit/webhook.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/webhook.tsx @@ -7,12 +7,12 @@ import { FormLabel, FormSelect, } from "components/generic/form"; +import { firstNonDefault, firstNonEmpty } from "utils"; import { useFormContext, useWatch } from "react-hook-form"; import { BooleanWithDefault } from "components/generic"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; -import { firstNonDefault } from "components/modals/service-edit/util"; interface Props { name: string; @@ -80,10 +80,11 @@ const EditServiceWebHook: FC = ({ main?.allow_invalid_certs ?? defaults?.allow_invalid_certs ?? hard_defaults?.allow_invalid_certs, - custom_headers: - main?.custom_headers ?? - defaults?.custom_headers ?? - hard_defaults?.custom_headers, + custom_headers: firstNonEmpty( + main?.custom_headers, + defaults?.custom_headers, + hard_defaults?.custom_headers + ), delay: firstNonDefault( main?.delay, defaults?.delay, diff --git a/web/ui/react-app/src/utils/diff-objects.tsx b/web/ui/react-app/src/utils/diff-objects.tsx index df3752ca..59db82f1 100644 --- a/web/ui/react-app/src/utils/diff-objects.tsx +++ b/web/ui/react-app/src/utils/diff-objects.tsx @@ -1,3 +1,5 @@ +import isEmptyOrNull from "./is-empty-or-null"; + /** * Returns whether a is different from b after allowing for only values at * allowedDefined to be the value in b @@ -48,7 +50,7 @@ export function diffObjects( return true; } else if (typeof b === "string") { // a is undefined/empty - if ((a ?? "") === "") return true; + if (isEmptyOrNull(a)) return true; // a is defined, and on a key that's allowed and is the same as b if ( containsEndsWith( diff --git a/web/ui/react-app/src/components/modals/service-edit/util/first-non-default.tsx b/web/ui/react-app/src/utils/first-non-default.tsx similarity index 68% rename from web/ui/react-app/src/components/modals/service-edit/util/first-non-default.tsx rename to web/ui/react-app/src/utils/first-non-default.tsx index adb4cfaf..9085395a 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/first-non-default.tsx +++ b/web/ui/react-app/src/utils/first-non-default.tsx @@ -1,3 +1,5 @@ +import isEmptyOrNull from "./is-empty-or-null"; + /** * Returns the first non-empty from the list of arguments * @@ -6,13 +8,15 @@ * @param args - The list of arguments to check * @returns The first non-empty string */ -export const firstNonDefault: (...args: unknown[]) => string = ( +const firstNonDefault: (...args: unknown[]) => string = ( ...args: unknown[] ) => { // Iterate through all arguments and return the first non-empty one for (const arg of args) { - if ((arg ?? "") !== "") return `${arg}`; + if (!isEmptyOrNull(arg)) return `${arg}`; } // If no non-empty argument is found, return an empty string by default return ""; }; + +export default firstNonDefault; diff --git a/web/ui/react-app/src/utils/first-non-empty.tsx b/web/ui/react-app/src/utils/first-non-empty.tsx new file mode 100644 index 00000000..355481c1 --- /dev/null +++ b/web/ui/react-app/src/utils/first-non-empty.tsx @@ -0,0 +1,16 @@ +/** + * Returns the first non-zero length argument + * + * @param args - The arguments to check for the first non-zero length + * @returns The first non-zero length argument + */ +const firstNonEmpty = ( + ...args: T[] +): NonNullable => { + for (const arg of args) { + if (arg && (arg as unknown[]).length) return arg; + } + return [] as NonNullable; +}; + +export default firstNonEmpty; diff --git a/web/ui/react-app/src/utils/index.tsx b/web/ui/react-app/src/utils/index.tsx index e2fed459..aa2a9513 100644 --- a/web/ui/react-app/src/utils/index.tsx +++ b/web/ui/react-app/src/utils/index.tsx @@ -7,19 +7,27 @@ import dateIsAfterNow from "./is-after-date"; import { diffObjects } from "./diff-objects"; import fetchJSON from "./fetch-json"; import fetchYAML from "./fetch-yaml"; +import firstNonDefault from "./first-non-default"; +import firstNonEmpty from "./first-non-empty"; import getBasename from "./get-basename"; +import isEmptyArray from "./is-empty"; +import isEmptyOrNull from "./is-empty-or-null"; import removeEmptyValues from "./remove-empty-values"; export { boolToStr, convertToQueryParams, cleanEmpty, + dateIsAfterNow, diffObjects, extractErrors, fetchJSON, fetchYAML, + firstNonDefault, + firstNonEmpty, getBasename, - dateIsAfterNow, + isEmptyArray, + isEmptyOrNull, removeEmptyValues, stringifyQueryParam, strToBool, diff --git a/web/ui/react-app/src/utils/is-empty-or-null.tsx b/web/ui/react-app/src/utils/is-empty-or-null.tsx new file mode 100644 index 00000000..612d89c0 --- /dev/null +++ b/web/ui/react-app/src/utils/is-empty-or-null.tsx @@ -0,0 +1,11 @@ +/** + * Returns whether the value is empty, null, or undefined + * + * @param value - The value to check + * @returns Whether the value is empty, null, or undefined + */ +const isEmptyOrNull = (value: unknown | undefined): boolean => { + return (value ?? "") === ""; +}; + +export default isEmptyOrNull; diff --git a/web/ui/react-app/src/utils/is-empty.tsx b/web/ui/react-app/src/utils/is-empty.tsx new file mode 100644 index 00000000..43568076 --- /dev/null +++ b/web/ui/react-app/src/utils/is-empty.tsx @@ -0,0 +1,10 @@ +/** + * Returns whether the array is empty, null, or undefined + * + * @param arg - The array to check + * @returns Whether the array is empty, null, or undefined + */ +const isEmptyArray = (arg: T): boolean => + !arg || (arg as unknown[]).length === 0; + +export default isEmptyArray; diff --git a/web/ui/react-app/src/utils/remove-empty-values.tsx b/web/ui/react-app/src/utils/remove-empty-values.tsx index bf0b7d5a..d68a442d 100644 --- a/web/ui/react-app/src/utils/remove-empty-values.tsx +++ b/web/ui/react-app/src/utils/remove-empty-values.tsx @@ -4,6 +4,9 @@ * @param obj - The object to remove empty values from * @returns The object with all empty values removed */ + +import isEmptyOrNull from "./is-empty-or-null"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const removeEmptyValues = (obj: { [x: string]: any }) => { for (const key in obj) { @@ -23,7 +26,7 @@ const removeEmptyValues = (obj: { [x: string]: any }) => { continue; } // "" Empty/undefined string - } else if ((obj[key] ?? "") === "") delete obj[key]; + } else if (isEmptyOrNull(obj[key])) delete obj[key]; } return obj; }; diff --git a/web/ui/react-app/src/utils/string-boolean.tsx b/web/ui/react-app/src/utils/string-boolean.tsx index 4d6ba895..23ba372f 100644 --- a/web/ui/react-app/src/utils/string-boolean.tsx +++ b/web/ui/react-app/src/utils/string-boolean.tsx @@ -1,3 +1,5 @@ +import isEmptyOrNull from "./is-empty-or-null"; + /** * Returns the boolean value of a string * @@ -6,8 +8,8 @@ */ export const strToBool = (str?: string | boolean): boolean | null => { if (typeof str === "boolean") return str; - if (str == null || str === "") return null; - return ["true", "yes"].includes(str.toLowerCase()); + if (isEmptyOrNull(str)) return null; + return ["true", "yes"].includes((str as string).toLowerCase()); }; export const boolToStr = (bool?: boolean) =>