From cbd33e8e97e3f96056ea0a601e1f136c56d39c05 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Thu, 1 Jun 2023 17:52:23 +0200 Subject: [PATCH] SONAR-19462 Use react-query for fetching authentication settings --- .../main/js/api/mocks/SettingsServiceMock.ts | 4 + .../authentication/ConfigurationForm.tsx | 36 ++---- .../GithubAuthenticationTab.tsx | 22 ++-- .../authentication/SamlAuthenticationTab.tsx | 35 +++--- .../__tests__/Authentication-it.tsx | 22 ++-- .../authentication/hook/useConfiguration.ts | 47 +++----- .../hook/useGithubConfiguration.ts | 46 +++----- .../hook/useSamlConfiguration.ts | 7 +- .../main/js/apps/settings/queries/settings.ts | 103 ++++++++++++++++++ 9 files changed, 191 insertions(+), 131 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/settings/queries/settings.ts diff --git a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts index cc1d04f61dc..c58f7d8473e 100644 --- a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts @@ -160,6 +160,10 @@ export default class SettingsServiceMock { } this.set(definition.key, value); + const def = this.#definitions.find((d) => d.key === definition.key); + if (def === undefined) { + this.#definitions.push(definition as ExtendedSettingDefinition); + } return this.reply(undefined); }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx index 8cb25488698..ae6d06d1432 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx @@ -17,11 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isEmptyArray } from 'formik'; -import { isEmpty, keyBy } from 'lodash'; +import { keyBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { resetSettingValue, setSettingValue } from '../../../../api/settings'; import DocLink from '../../../../components/common/DocLink'; import Modal from '../../../../components/controls/Modal'; import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons'; @@ -29,6 +27,7 @@ import { Alert } from '../../../../components/ui/Alert'; import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; import { translate } from '../../../../helpers/l10n'; import { Dict } from '../../../../types/types'; +import { useSaveValuesMutation } from '../../queries/settings'; import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication'; import AuthenticationFormField from './AuthenticationFormField'; import { SettingValue } from './hook/useConfiguration'; @@ -40,7 +39,6 @@ interface Props { setNewValue: (key: string, value: string | boolean) => void; canBeSave: boolean; onClose: () => void; - onReload: () => Promise; tab: AuthenticationTabs; excludedField: string[]; hasLegacyConfiguration?: boolean; @@ -64,34 +62,22 @@ export default function ConfigurationForm(props: Props) { } = props; const [errors, setErrors] = React.useState>({}); + const { mutateAsync: changeConfig } = useSaveValuesMutation(); + const headerLabel = translate('settings.authentication.form', create ? 'create' : 'edit', tab); const handleSubmit = async (event: React.SyntheticEvent) => { event.preventDefault(); if (canBeSave) { - const r = await Promise.all( - Object.values(values) - .filter((v) => v.newValue !== undefined) - .map(async ({ key, newValue, definition }) => { - try { - if (isEmptyArray(newValue)) { - await resetSettingValue({ keys: definition.key }); - } else { - await setSettingValue(definition, newValue); - } - return { key, success: true }; - } catch (error) { - return { key, success: false }; - } - }) - ); - const errors = r + const data = await changeConfig(Object.values(values)); + const errors = data .filter(({ success }) => !success) .map(({ key }) => ({ key, message: translate('default_save_field_error_message') })); + setErrors(keyBy(errors, 'key')); - if (isEmpty(errors)) { - await props.onReload(); + + if (errors.length === 0) { props.onClose(); } } else { @@ -122,11 +108,11 @@ export default function ConfigurationForm(props: Props) { { setShowEditModal(true); }; - const handleCancelConfiguration = () => { + const handleCloseConfiguration = () => { + refetch(); setShowEditModal(false); }; @@ -150,7 +151,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps {translate('settings.authentication.form.edit')} - @@ -320,13 +325,12 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx index f5672920161..f51e9f893c3 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx @@ -17,10 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isEmpty } from 'lodash'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { resetSettingValue, setSettingValue } from '../../../../api/settings'; import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import ConfirmModal from '../../../../components/controls/ConfirmModal'; @@ -34,6 +32,7 @@ import { Alert } from '../../../../components/ui/Alert'; import { translate } from '../../../../helpers/l10n'; import { getBaseUrl } from '../../../../helpers/system'; import { ExtendedSettingDefinition } from '../../../../types/settings'; +import { useSaveValueMutation } from '../../queries/settings'; import { getPropertyName } from '../../utils'; import DefinitionDescription from '../DefinitionDescription'; import ConfigurationForm from './ConfigurationForm'; @@ -60,7 +59,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { const { hasScim, scimStatus, - loading, + isLoading, samlEnabled, name, groupValue, @@ -73,12 +72,12 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { newScimStatus, setNewScimStatus, setNewGroupSetting, - reload, - deleteConfiguration, + deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration }, } = useSamlConfiguration(definitions); const toggleScim = useToggleScimMutation(); const { data } = useIdentityProvierQuery(); + const { mutate: saveSetting } = useSaveValueMutation(); const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Scim; @@ -90,29 +89,22 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { setShowEditModal(false); }; - const handleToggleEnable = async () => { + const handleToggleEnable = () => { const value = values[SAML_ENABLED_FIELD]; - await setSettingValue(value.definition, !samlEnabled); - await reload(); + saveSetting({ newValue: !samlEnabled, definition: value.definition }); }; - const handleSaveGroup = async () => { + const handleSaveGroup = () => { if (groupValue.newValue !== undefined) { - if (isEmpty(groupValue.newValue)) { - await resetSettingValue({ keys: groupValue.definition.key }); - } else { - await setSettingValue(groupValue.definition, groupValue.newValue); - } - await reload(); + saveSetting({ newValue: groupValue.newValue, definition: groupValue.definition }); } }; const handleConfirmChangeProvisioning = async () => { await toggleScim.mutateAsync(!!newScimStatus); if (!newScimStatus) { - await handleSaveGroup(); + handleSaveGroup(); } - await reload(); }; return ( @@ -168,7 +160,11 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { {translate('settings.authentication.form.edit')} - @@ -323,13 +319,12 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index 02a8b996a33..01996b13ff1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { act, screen, within } from '@testing-library/react'; +import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import React from 'react'; @@ -288,7 +288,7 @@ describe('SAML tab', () => { expect(await saml.disableConfigButton.find()).toBeInTheDocument(); await user.click(saml.disableConfigButton.get()); - expect(saml.disableConfigButton.query()).not.toBeInTheDocument(); + await waitFor(() => expect(saml.disableConfigButton.query()).not.toBeInTheDocument()); expect(await saml.enableConfigButton.find()).toBeInTheDocument(); }); @@ -309,7 +309,7 @@ describe('SAML tab', () => { await user.type(saml.groupAttribute.get(), 'group'); expect(saml.saveScim.get()).toBeEnabled(); await user.click(saml.saveScim.get()); - expect(await saml.saveScim.find()).toBeDisabled(); + await waitFor(() => expect(saml.saveScim.query()).toBeDisabled()); await user.click(saml.scimProvisioningButton.get()); expect(saml.saveScim.get()).toBeEnabled(); @@ -393,7 +393,7 @@ describe('Github tab', () => { expect(await github.disableConfigButton.find()).toBeInTheDocument(); await user.click(github.disableConfigButton.get()); - expect(github.disableConfigButton.query()).not.toBeInTheDocument(); + await waitFor(() => expect(github.disableConfigButton.query()).not.toBeInTheDocument()); expect(await github.enableConfigButton.find()).toBeInTheDocument(); }); @@ -403,6 +403,7 @@ describe('Github tab', () => { const user = userEvent.setup(); renderAuthentication(); + await user.click(await github.tab.find()); await github.createConfiguration(user); await user.click(await github.enableConfigButton.find()); @@ -431,7 +432,8 @@ describe('Github tab', () => { expect(github.saveGithubProvisioning.get()).toBeEnabled(); await user.click(github.saveGithubProvisioning.get()); - expect(await github.saveGithubProvisioning.find()).toBeDisabled(); + + await waitFor(() => expect(github.saveGithubProvisioning.query()).toBeDisabled()); await user.click(github.githubProvisioningButton.get()); @@ -515,7 +517,7 @@ describe('Github tab', () => { renderAuthentication([Feature.GithubProvisioning]); await github.enableConfiguration(user); - expect(github.configurationValiditySuccess.get()).toBeInTheDocument(); + await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument()); }); it('should display that config is valid for both provisioning with multiple orgs', async () => { @@ -528,7 +530,7 @@ describe('Github tab', () => { renderAuthentication([Feature.GithubProvisioning]); await github.enableConfiguration(user); - expect(github.configurationValiditySuccess.get()).toBeInTheDocument(); + await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument()); expect(github.configurationValiditySuccess.get()).toHaveTextContent('2'); await act(() => user.click(github.viewConfigValidityDetailsButton.get())); @@ -560,7 +562,7 @@ describe('Github tab', () => { renderAuthentication([Feature.GithubProvisioning]); await github.enableConfiguration(user); - expect(github.configurationValidityError.get()).toBeInTheDocument(); + await waitFor(() => expect(github.configurationValidityError.query()).toBeInTheDocument()); expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage); await act(() => user.click(github.viewConfigValidityDetailsButton.get())); @@ -586,7 +588,7 @@ describe('Github tab', () => { renderAuthentication([Feature.GithubProvisioning]); await github.enableConfiguration(user); - expect(github.configurationValiditySuccess.get()).toBeInTheDocument(); + await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument()); expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage); await act(() => user.click(github.viewConfigValidityDetailsButton.get())); @@ -622,7 +624,7 @@ describe('Github tab', () => { renderAuthentication([Feature.GithubProvisioning]); await github.enableConfiguration(user); - expect(github.configurationValiditySuccess.get()).toBeInTheDocument(); + await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument()); await act(() => user.click(github.viewConfigValidityDetailsButton.get())); github.getOrgs().forEach((org) => { diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts index 73eb9d79c9c..e1d87ca0909 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts @@ -17,11 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { every, isEmpty, keyBy } from 'lodash'; -import React, { useCallback, useState } from 'react'; -import { getValues, resetSettingValue } from '../../../../../api/settings'; +import { UseMutationResult } from '@tanstack/react-query'; +import { every, isEmpty, keyBy, update } from 'lodash'; +import { useCallback, useEffect, useState } from 'react'; import { ExtendedSettingDefinition } from '../../../../../types/settings'; import { Dict } from '../../../../../types/types'; +import { useGetValuesQuery, useResetSettingsMutation } from '../../../queries/settings'; export type SettingValue = | { @@ -47,23 +48,17 @@ export default function useConfiguration( definitions: ExtendedSettingDefinition[], optionalFields: string[] ) { - const [loading, setLoading] = useState(true); + const keys = definitions.map((definition) => definition.key); const [values, setValues] = useState>({}); - const reload = useCallback(async () => { - const keys = definitions.map((definition) => definition.key); - - setLoading(true); - - try { - const values = await getValues({ - keys, - }); + const { isLoading, data } = useGetValuesQuery(keys); + useEffect(() => { + if (data !== undefined) { setValues( keyBy( definitions.map((definition) => { - const value = values.find((v) => v.key === definition.key); + const value = data.find((v) => v.key === definition.key); const multiValues = definition.multiValues ?? false; if (multiValues) { return { @@ -87,16 +82,8 @@ export default function useConfiguration( 'key' ) ); - } finally { - setLoading(false); } - }, [...definitions]); - - React.useEffect(() => { - (async () => { - await reload(); - })(); - }, [...definitions]); + }, [data, definitions]); const setNewValue = (key: string, newValue?: string | boolean | string[]) => { const value = values[key]; @@ -133,10 +120,11 @@ export default function useConfiguration( (v) => !v.isNotSet ); - const deleteConfiguration = useCallback(async () => { - await resetSettingValue({ keys: Object.keys(values).join(',') }); - await reload(); - }, [reload, values]); + const deleteMutation = update( + useResetSettingsMutation(), + 'mutate', + (mutate) => () => mutate(Object.keys(values)) + ) as Omit, 'mutateAsync'>; const isValueChange = useCallback( (setting: string) => { @@ -148,12 +136,11 @@ export default function useConfiguration( return { values, - reload, setNewValue, canBeSave, - loading, + isLoading, hasConfiguration, isValueChange, - deleteConfiguration, + deleteMutation, }; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts index bd90eec9a9b..384dfbf8c48 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts @@ -17,14 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isEmpty, some } from 'lodash'; -import { useCallback, useContext, useState } from 'react'; -import { resetSettingValue, setSettingValue } from '../../../../../api/settings'; +import { some } from 'lodash'; +import { useContext, useState } from 'react'; import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext'; import { Feature } from '../../../../../types/features'; import { ExtendedSettingDefinition } from '../../../../../types/settings'; +import { useSaveValueMutation, useSaveValuesMutation } from '../../../queries/settings'; import { - useCheckGitHubConfigQuery, useGithubStatusQuery, useToggleGithubProvisioningMutation, } from '../queries/identity-provider'; @@ -55,7 +54,7 @@ export interface SamlSettingValue { export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) { const config = useConfiguration(definitions, OPTIONAL_FIELDS); - const { values, isValueChange, setNewValue, reload: reloadConfig } = config; + const { values, isValueChange, setNewValue } = config; const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes( Feature.GithubProvisioning @@ -72,52 +71,37 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin GITHUB_JIT_FIELDS.forEach((s) => setNewValue(s)); }; + const { mutate: saveSetting } = useSaveValueMutation(); + const { mutate: saveSettings } = useSaveValuesMutation(); + const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true'; - const { refetch } = useCheckGitHubConfigQuery(enabled); const appId = values[GITHUB_APP_ID_FIELD]?.value as string; const url = values[GITHUB_API_URL_FIELD]?.value; const clientIdIsNotSet = values[GITHUB_CLIENT_ID_FIELD]?.isNotSet; - const reload = useCallback(async () => { - await reloadConfig(); - // Temporary solution that will be solved once we migrate to react-query - refetch(); - }, [reloadConfig]); - const changeProvisioning = async () => { if (newGithubProvisioningStatus !== githubProvisioningStatus) { await toggleGithubProvisioning.mutateAsync(!!newGithubProvisioningStatus); } - await saveGroup(); + if (!newGithubProvisioningStatus || !githubProvisioningStatus) { + saveGroup(); + } }; - const saveGroup = async () => { - await Promise.all( - GITHUB_JIT_FIELDS.map(async (settingKey) => { - const value = values[settingKey]; - if (value.newValue !== undefined) { - if (isEmpty(value.newValue) && typeof value.newValue !== 'boolean') { - await resetSettingValue({ keys: value.definition.key }); - } else { - await setSettingValue(value.definition, value.newValue); - } - } - }) - ); - await reload(); + const saveGroup = () => { + const newValues = GITHUB_JIT_FIELDS.map((settingKey) => values[settingKey]); + saveSettings(newValues); }; - const toggleEnable = async () => { + const toggleEnable = () => { const value = values[GITHUB_ENABLED_FIELD]; - await setSettingValue(value.definition, !enabled); - await reload(); + saveSetting({ newValue: !enabled, definition: value.definition }); }; const hasLegacyConfiguration = appId === undefined && !clientIdIsNotSet; return { ...config, - reload, url, enabled, appId, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts index 2204d44ec83..d80ffeeda92 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts @@ -43,7 +43,7 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit const [newScimStatus, setNewScimStatus] = React.useState(); const hasScim = React.useContext(AvailableFeaturesContext).includes(Feature.Scim); const config = useConfiguration(definitions, OPTIONAL_FIELDS); - const { reload: reloadConfig, values, setNewValue, isValueChange } = config; + const { values, setNewValue, isValueChange } = config; const { data: scimStatus } = useScimStatusQuery(); @@ -59,10 +59,6 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit const hasScimConfigChange = isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus); - const reload = React.useCallback(async () => { - await reloadConfig(); - }, [reloadConfig]); - return { ...config, hasScim, @@ -73,7 +69,6 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit groupValue, values, setNewValue, - reload, hasScimConfigChange, newScimStatus, setNewScimStatus, diff --git a/server/sonar-web/src/main/js/apps/settings/queries/settings.ts b/server/sonar-web/src/main/js/apps/settings/queries/settings.ts new file mode 100644 index 00000000000..6c88431f664 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/queries/settings.ts @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { getValues, resetSettingValue, setSettingValue } from '../../../api/settings'; +import { ExtendedSettingDefinition } from '../../../types/settings'; + +type SettingValue = string | boolean | string[]; + +export function useGetValuesQuery(keys: string[]) { + return useQuery(['settings', 'values', keys] as const, ({ queryKey: [_a, _b, keys] }) => { + return getValues({ keys }); + }); +} + +export function useResetSettingsMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (keys: string[]) => resetSettingValue({ keys: keys.join(',') }), + onSuccess: () => { + queryClient.invalidateQueries(['settings']); + }, + }); +} + +export function useSaveValuesMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ( + values: { + newValue?: SettingValue; + definition: ExtendedSettingDefinition; + }[] + ) => { + return Promise.all( + values + .filter((v) => v.newValue !== undefined) + .map(async ({ newValue, definition }) => { + try { + if (isDefaultValue(newValue as string | boolean | string[], definition)) { + await resetSettingValue({ keys: definition.key }); + } else { + await setSettingValue(definition, newValue); + } + return { key: definition.key, success: true }; + } catch (error) { + return { key: definition.key, success: false }; + } + }) + ); + }, + onSuccess: () => { + queryClient.invalidateQueries(['settings']); + }, + }); +} + +export function useSaveValueMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + newValue, + definition, + }: { + newValue: SettingValue; + definition: ExtendedSettingDefinition; + }) => { + if (isDefaultValue(newValue, definition)) { + await resetSettingValue({ keys: definition.key }); + } else { + await setSettingValue(definition, newValue); + } + }, + onSuccess: () => { + queryClient.invalidateQueries(['settings']); + }, + }); +} + +function isDefaultValue(value: SettingValue, definition: ExtendedSettingDefinition) { + const defaultValue = definition.defaultValue ?? ''; + if (definition.multiValues) { + return defaultValue === (value as string[]).join(','); + } + + return defaultValue === String(value); +} -- 2.39.5