From e9bd6a1f9311eec43be2080734a650efae89517e Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 5 Dec 2025 11:22:26 +0300 Subject: [PATCH 1/9] feat(settings): simplify hook, split meta and LS cases --- .../TopQueries/QueriesTableWithDrawer.tsx | 9 +- .../TopQueries/RunningQueriesData.tsx | 5 +- .../Diagnostics/TopQueries/TopQueriesData.tsx | 5 +- src/services/api/index.ts | 5 +- src/services/api/metaSettings.ts | 2 +- src/store/configureStore.ts | 1 + .../reducers/authentication/authentication.ts | 5 +- src/store/reducers/authentication/types.ts | 2 + .../reducers/capabilities/capabilities.ts | 6 +- src/store/reducers/settings/api.ts | 111 +++++++++--------- src/store/reducers/settings/constants.ts | 8 +- src/store/reducers/settings/settings.ts | 42 +++++-- src/store/reducers/settings/useSetting.ts | 89 ++++---------- src/store/reducers/settings/utils.ts | 8 +- src/store/utils.ts | 7 ++ src/uiFactory/types.ts | 1 - src/utils/hooks/useSetting.ts | 4 +- 17 files changed, 149 insertions(+), 161 deletions(-) create mode 100644 src/store/utils.ts diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx index f1ef2fb474..2c42c82aa3 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx @@ -18,7 +18,8 @@ const b = cn('kv-top-queries'); interface SimpleTableWithDrawerProps { columns: Column[]; data: KeyValueRow[]; - loading?: boolean; + isFetching?: boolean; + isLoading?: boolean; onRowClick?: ( row: KeyValueRow | null, index?: number, @@ -39,7 +40,8 @@ interface SimpleTableWithDrawerProps { export function QueriesTableWithDrawer({ columns, data, - loading, + isFetching, + isLoading, onRowClick, columnsWidthLSKey, emptyDataMessage, @@ -104,7 +106,8 @@ export function QueriesTableWithDrawer({ columnsWidthLSKey={columnsWidthLSKey} columns={columns} data={data} - isFetching={loading} + isFetching={isFetching} + isLoading={isLoading} settings={tableSettings} onRowClick={handleRowClick} rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index c5fc7125ff..330ea68bdc 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -96,11 +96,12 @@ export const RunningQueriesData = ({ {error ? : null} - + {error ? : null} - + string | undefined); defaults: undefined | AxiosRequestConfig; } @@ -54,6 +54,7 @@ export class YdbEmbeddedAPI { csrfTokenGetter = () => undefined, defaults = {}, useRelativePath = false, + useMetaSettings = false, }: YdbEmbeddedAPIProps) { const axiosParams: AxiosWrapperOptions = {config: {withCredentials, ...defaults}}; const baseApiParams = {singleClusterMode, proxyMeta, useRelativePath}; @@ -62,7 +63,7 @@ export class YdbEmbeddedAPI { if (webVersion) { this.meta = new MetaAPI(axiosParams, baseApiParams); } - if (uiFactory.useMetaSettings) { + if (useMetaSettings) { this.metaSettings = new MetaSettingsAPI(axiosParams, baseApiParams); } diff --git a/src/services/api/metaSettings.ts b/src/services/api/metaSettings.ts index a8ec9505d4..a5642913e9 100644 --- a/src/services/api/metaSettings.ts +++ b/src/services/api/metaSettings.ts @@ -25,7 +25,7 @@ export class MetaSettingsAPI extends BaseMetaAPI { preventBatching, }: GetSingleSettingParams & {preventBatching?: boolean}) { if (preventBatching) { - return this.get(this.getPath('/meta/user_settings'), {name, user}); + return this.get(this.getPath('/meta/user_settings'), {name, user}); } return new Promise((resolve, reject) => { diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 8ce666ba69..7cac00693c 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -74,6 +74,7 @@ export function configureStore({ proxyMeta: false, csrfTokenGetter: undefined, useRelativePath: false, + useMetaSettings: false, defaults: undefined, }), } = {}) { diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index f8b1ecd9a7..6b2d73af8e 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -11,6 +11,7 @@ const initialState: AuthenticationState = { isAuthenticated: true, user: undefined, id: undefined, + metaUser: undefined, }; export const slice = createSlice({ @@ -45,13 +46,13 @@ export const slice = createSlice({ selectIsUserAllowedToMakeChanges: (state) => state.isUserAllowedToMakeChanges, selectIsViewerUser: (state) => state.isViewerUser, selectUser: (state) => state.user, - selectID: (state) => state.id, + selectMetaUser: (state) => state.metaUser ?? state.id, }, }); export default slice.reducer; export const {setIsAuthenticated, setUser} = slice.actions; -export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser, selectID} = +export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser, selectMetaUser} = slice.selectors; export const authenticationApi = api.injectEndpoints({ diff --git a/src/store/reducers/authentication/types.ts b/src/store/reducers/authentication/types.ts index 0a8879b31d..f72b5d75be 100644 --- a/src/store/reducers/authentication/types.ts +++ b/src/store/reducers/authentication/types.ts @@ -5,4 +5,6 @@ export interface AuthenticationState { user: string | undefined; id: string | undefined; + + metaUser: string | undefined; } diff --git a/src/store/reducers/capabilities/capabilities.ts b/src/store/reducers/capabilities/capabilities.ts index 1c4a4f9682..c0a71b9da3 100644 --- a/src/store/reducers/capabilities/capabilities.ts +++ b/src/store/reducers/capabilities/capabilities.ts @@ -2,6 +2,7 @@ import {createSelector} from '@reduxjs/toolkit'; import type {Capability, MetaCapability, SecuritySetting} from '../../../types/api/capabilities'; import type {AppDispatch, RootState} from '../../defaultStore'; +import {serializeError} from '../../utils'; import {api} from './../api'; @@ -30,10 +31,7 @@ export const capabilitiesApi = api.injectEndpoints({ } catch (error) { // If capabilities endpoint is not available, there will be an error // That means no new features are available - // Serialize the error to make it Redux-compatible - const serializedError = - error instanceof Error ? {message: error.message, name: error.name} : error; - return {error: serializedError}; + return {error: serializeError(error)}; } }, }), diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index f4595de3c6..a3ec4a767a 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -1,24 +1,32 @@ -import {isNil} from 'lodash'; - import type { GetSettingsParams, GetSingleSettingParams, SetSingleSettingParams, - Setting, } from '../../../types/api/settings'; import type {AppDispatch} from '../../defaultStore'; +import {serializeError} from '../../utils'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; +import {getSettingDefault, parseSettingValue, stringifySettingValue} from './utils'; export const settingsApi = api.injectEndpoints({ endpoints: (builder) => ({ - getSingleSetting: builder.query({ - queryFn: async ({name, user}: GetSingleSettingParams, baseApi) => { + getSingleSetting: builder.query>({ + queryFn: async ({name, user}) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + if (!name || !window.api.metaSettings) { + throw new Error( + 'Cannot get setting, no MetaSettings API or neccessary params are missing', + ); } + + const defaultValue = getSettingDefault(name) as unknown; + + if (!user) { + return {data: defaultValue}; + } + const data = await window.api.metaSettings.getSingleSetting({ name, user, @@ -26,43 +34,50 @@ export const settingsApi = api.injectEndpoints({ preventBatching: SETTINGS_OPTIONS[name]?.preventBatching, }); - const dispatch = baseApi.dispatch as AppDispatch; - - // Try to sync local value if there is no backend value - syncLocalValueToMetaIfNoData(data, dispatch); - - return {data}; + return {data: parseSettingValue(data?.value) ?? defaultValue}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, }), setSingleSetting: builder.mutation({ - queryFn: async (params: SetSingleSettingParams) => { + queryFn: async ({ + name, + user, + value, + }: Partial> & {value: unknown}) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + if (!name || !user || !window.api.metaSettings) { + throw new Error( + 'Cannot set setting, no MetaSettings API or neccessary params are missing', + ); } - const data = await window.api.metaSettings.setSingleSetting(params); + const data = await window.api.metaSettings.setSingleSetting({ + name, + user, + value: stringifySettingValue(value), + }); if (data.status !== 'SUCCESS') { - throw new Error('Setting status is not SUCCESS'); + throw new Error('Cannot set setting - status is not SUCCESS'); } return {data}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, async onQueryStarted(args, {dispatch, queryFulfilled}) { const {name, user, value} = args; + if (!name) { + return; + } + // Optimistically update existing cache entry const patchResult = dispatch( - settingsApi.util.updateQueryData('getSingleSetting', {name, user}, (draft) => { - return {...draft, name, user, value}; - }), + settingsApi.util.updateQueryData('getSingleSetting', {name, user}, () => value), ); try { await queryFulfilled; @@ -72,41 +87,41 @@ export const settingsApi = api.injectEndpoints({ }, }), getSettings: builder.query({ - queryFn: async ({name, user}: GetSettingsParams, baseApi) => { + queryFn: async ({name, user}: Partial, baseApi) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + if (!window.api.metaSettings || !name || !user) { + throw new Error( + 'Cannot get settings, no MetaSettings API or neccessary params are missing', + ); } const data = await window.api.metaSettings.getSettings({name, user}); - const patches: Promise[] = []; + const patches: Promise[] = []; const dispatch = baseApi.dispatch as AppDispatch; - // Upsert received data in getSingleSetting cache + // Upsert received data in getSingleSetting cache to prevent further redundant requests name.forEach((settingName) => { - const settingData = data[settingName] ?? {}; + const settingData = data[settingName]; + + const defaultValue = getSettingDefault(settingName); const cacheEntryParams: GetSingleSettingParams = { name: settingName, user, }; - const newValue = {name: settingName, user, value: settingData?.value}; + const newSetting = { + name: settingName, + user, + value: parseSettingValue(settingData?.value) ?? defaultValue, + }; const patch = dispatch( settingsApi.util.upsertQueryData( 'getSingleSetting', cacheEntryParams, - newValue, + newSetting, ), - ).then(() => { - // Try to sync local value if there is no backend value - // Do it after upsert if finished to ensure proper values update order - // 1. New entry added to cache with nil value - // 2. Positive entry update - local storage value replace nil in cache - // 3.1. Set is successful, local value in cache - // 3.2. Set is not successful, cache value reverted to previous nil - syncLocalValueToMetaIfNoData(settingData, dispatch); - }); + ); patches.push(patch); }); @@ -116,24 +131,10 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, }), }), overrideExisting: 'throw', }); - -function syncLocalValueToMetaIfNoData(params: Setting, dispatch: AppDispatch) { - const localValue = localStorage.getItem(params.name); - - if (isNil(params.value) && !isNil(localValue)) { - dispatch( - settingsApi.endpoints.setSingleSetting.initiate({ - name: params.name, - user: params.user, - value: localValue, - }), - ); - } -} diff --git a/src/store/reducers/settings/constants.ts b/src/store/reducers/settings/constants.ts index eff66c6f89..8937dc8fa3 100644 --- a/src/store/reducers/settings/constants.ts +++ b/src/store/reducers/settings/constants.ts @@ -76,14 +76,8 @@ export const DEFAULT_USER_SETTINGS = { [SETTING_KEYS.STORAGE_TYPE]: STORAGE_TYPES.groups, } as const satisfies Record; -export const SETTINGS_OPTIONS: Record = { +export const SETTINGS_OPTIONS: Record = { [SETTING_KEYS.THEME]: { preventBatching: true, }, - [SETTING_KEYS.SAVED_QUERIES]: { - preventSyncWithLS: true, - }, - [SETTING_KEYS.QUERIES_HISTORY]: { - preventSyncWithLS: true, - }, } as const; diff --git a/src/store/reducers/settings/settings.ts b/src/store/reducers/settings/settings.ts index 4267f8fde2..de60ee52ee 100644 --- a/src/store/reducers/settings/settings.ts +++ b/src/store/reducers/settings/settings.ts @@ -1,12 +1,14 @@ import type {Store} from '@reduxjs/toolkit'; -import {createSlice} from '@reduxjs/toolkit'; +import {createSelector, createSlice} from '@reduxjs/toolkit'; +import {isNil} from 'lodash'; import {settingsManager} from '../../../services/settings'; import {parseJson} from '../../../utils/utils'; -import type {AppDispatch} from '../../defaultStore'; +import type {AppDispatch, RootState} from '../../defaultStore'; import {DEFAULT_USER_SETTINGS} from './constants'; import type {SettingsState} from './types'; +import {getSettingDefault, readSettingValueFromLS, setSettingValueToLS} from './utils'; const userSettings = settingsManager.extractSettingsFromLS(DEFAULT_USER_SETTINGS); const systemSettings = window.systemSettings || {}; @@ -24,23 +26,39 @@ const settingsSlice = createSlice({ state.userSettings[action.payload.name] = action.payload.value; }), }), - selectors: { - getSettingValue: (state, name?: string) => { - if (!name) { - return undefined; - } - - return state.userSettings[name]; - }, - }, }); -export const {getSettingValue} = settingsSlice.selectors; +/** + * Reads LS value or use default when store value undefined + * + * If there is value in store, returns it + */ +export const getSettingValue = createSelector( + (state: RootState) => state.settings.userSettings, + (_state: RootState, name?: string) => name, + (userSettings, name) => { + if (!name) { + return undefined; + } + + const storeValue = userSettings[name]; + + if (!isNil(storeValue)) { + return storeValue; + } + + const defaultValue = getSettingDefault(name); + const savedValue = readSettingValueFromLS(name); + + return savedValue ?? defaultValue; + }, +); export const setSettingValue = (name: string | undefined, value: unknown) => { return (dispatch: AppDispatch) => { if (name) { dispatch(settingsSlice.actions.setSettingValue({name, value})); + setSettingValueToLS(name, value); } }; }; diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 4a51f3a7e4..80c7d6f0fb 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -1,22 +1,10 @@ import React from 'react'; -import {skipToken} from '@reduxjs/toolkit/query'; - -import {uiFactory} from '../../../uiFactory/uiFactory'; -import {useTypedDispatch} from '../../../utils/hooks/useTypedDispatch'; +import {useSetting as useLSSetting} from '../../../utils/hooks'; import {useTypedSelector} from '../../../utils/hooks/useTypedSelector'; -import {selectID, selectUser} from '../authentication/authentication'; +import {selectMetaUser} from '../authentication/authentication'; import {settingsApi} from './api'; -import type {SettingKey} from './constants'; -import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; -import {getSettingValue, setSettingValue} from './settings'; -import { - parseSettingValue, - readSettingValueFromLS, - setSettingValueToLS, - stringifySettingValue, -} from './utils'; type SaveSettingValue = (value: T | undefined) => void; @@ -25,71 +13,40 @@ export function useSetting(name?: string): { saveValue: SaveSettingValue; isLoading: boolean; } { - const dispatch = useTypedDispatch(); - - const preventSyncWithLS = Boolean(name && SETTINGS_OPTIONS[name]?.preventSyncWithLS); - - const settingValue = useTypedSelector((state) => getSettingValue(state, name)) as T | undefined; - - const authUserSID = useTypedSelector(selectUser); - const anonymousUserId = useTypedSelector(selectID); - - const user = authUserSID || anonymousUserId; - const shouldUseMetaSettings = uiFactory.useMetaSettings && user && name; - - const shouldUseOnlyExternalSettings = shouldUseMetaSettings && preventSyncWithLS; + const user = useTypedSelector(selectMetaUser); - const params = React.useMemo(() => { - return shouldUseMetaSettings ? {user, name} : skipToken; - }, [shouldUseMetaSettings, user, name]); - - const {currentData: metaSetting, isLoading: isSettingLoading} = - settingsApi.useGetSingleSettingQuery(params); + const {currentData: settingFromMeta, isLoading} = settingsApi.useGetSingleSettingQuery({ + user, + name, + }); const [setMetaSetting] = settingsApi.useSetSingleSettingMutation(); - // Add loading state to settings that are stored externally - const isLoading = shouldUseMetaSettings ? isSettingLoading : false; - - // Load initial value - React.useEffect(() => { - let value = name ? (DEFAULT_USER_SETTINGS[name as SettingKey] as T | undefined) : undefined; + const [settingFromLS, saveSettingToLS] = useLSSetting(name); - if (!shouldUseOnlyExternalSettings) { - const savedValue = readSettingValueFromLS(name); - value = savedValue ?? value; + const settingValue = React.useMemo(() => { + if (!name) { + return undefined; } - - dispatch(setSettingValue(name, value)); - }, [name, shouldUseOnlyExternalSettings, dispatch]); - - // Sync value from backend with LS and store - React.useEffect(() => { - if (shouldUseMetaSettings && metaSetting?.value) { - if (!shouldUseOnlyExternalSettings) { - setSettingValueToLS(name, metaSetting.value); - } - const parsedValue = parseSettingValue(metaSetting.value); - dispatch(setSettingValue(name, parsedValue)); + if (window.api.metaSettings) { + return settingFromMeta; } - }, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); + return settingFromLS; + }, [settingFromMeta, settingFromLS]); const saveValue = React.useCallback>( (value) => { - if (shouldUseMetaSettings) { - setMetaSetting({ - user, - name: name, - value: stringifySettingValue(value), - }); + if (!name) { + return; } - - if (!shouldUseOnlyExternalSettings) { - setSettingValueToLS(name, value); + if (window.api.metaSettings) { + setMetaSetting({user, name, value}); + } else { + saveSettingToLS(value); } }, - [shouldUseMetaSettings, shouldUseOnlyExternalSettings, user, name, setMetaSetting], + [user, name, setMetaSetting, saveSettingToLS], ); - return {value: settingValue, saveValue, isLoading} as const; + return {value: settingValue as T | undefined, saveValue, isLoading} as const; } diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 916e1e5d42..8113e445db 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -1,7 +1,10 @@ import type {SettingValue} from '../../../types/api/settings'; import {parseJson} from '../../../utils/utils'; -export function stringifySettingValue(value?: T): string { +import type {SettingKey} from './constants'; +import {DEFAULT_USER_SETTINGS} from './constants'; + +export function stringifySettingValue(value?: unknown): string { return typeof value === 'string' ? value : JSON.stringify(value); } export function parseSettingValue(value?: SettingValue) { @@ -34,3 +37,6 @@ export function setSettingValueToLS(name: string | undefined, value: unknown): v localStorage.setItem(name, preparedValue); } catch {} } +export function getSettingDefault(name: string) { + return DEFAULT_USER_SETTINGS[name as SettingKey]; +} diff --git a/src/store/utils.ts b/src/store/utils.ts new file mode 100644 index 0000000000..ab0abf4d87 --- /dev/null +++ b/src/store/utils.ts @@ -0,0 +1,7 @@ +/** Serialize the error to make it Redux-compatible */ +export function serializeError(error: unknown) { + if (error instanceof Error) { + return {message: error.message, name: error.name}; + } + return error; +} diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index a3aea7469f..169fe2d4f7 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -51,7 +51,6 @@ export interface UIFactory; diff --git a/src/utils/hooks/useSetting.ts b/src/utils/hooks/useSetting.ts index 5432062152..c9438ae056 100644 --- a/src/utils/hooks/useSetting.ts +++ b/src/utils/hooks/useSetting.ts @@ -1,12 +1,11 @@ import React from 'react'; -import {settingsManager} from '../../services/settings'; import {getSettingValue, setSettingValue} from '../../store/reducers/settings/settings'; import {useTypedDispatch} from './useTypedDispatch'; import {useTypedSelector} from './useTypedSelector'; -export const useSetting = (key: string, defaultValue?: T): [T, (value: T) => void] => { +export const useSetting = (key?: string, defaultValue?: T): [T, (value: T) => void] => { const dispatch = useTypedDispatch(); const settingValue = useTypedSelector((state) => { @@ -17,7 +16,6 @@ export const useSetting = (key: string, defaultValue?: T): [T, (value: T) => const setValue = React.useCallback( (value: T) => { dispatch(setSettingValue(key, value)); - settingsManager.setUserSettingsValue(key, value); }, [dispatch, key], ); From c25b134a1b6ed7e120b6abe4bcac14a3125ce089 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 5 Dec 2025 11:46:25 +0300 Subject: [PATCH 2/9] update comment --- src/store/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/store/utils.ts b/src/store/utils.ts index ab0abf4d87..b56423709c 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,4 +1,8 @@ -/** Serialize the error to make it Redux-compatible */ +/** + * Serialize the error to make it Redux-compatible + * + * It prevents redux console error on string error in code - `throw new Error("description")` + */ export function serializeError(error: unknown) { if (error instanceof Error) { return {message: error.message, name: error.name}; From a4ab27217b4f65eebb89035e79c8b7b328915660 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 5 Dec 2025 12:21:37 +0300 Subject: [PATCH 3/9] fix: bots review --- src/store/reducers/settings/api.ts | 21 +++++++++------------ src/store/reducers/settings/useSetting.ts | 6 +++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index a3ec4a767a..39a909aba3 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -15,9 +15,9 @@ export const settingsApi = api.injectEndpoints({ getSingleSetting: builder.query>({ queryFn: async ({name, user}) => { try { - if (!name || !window.api.metaSettings) { + if (!name || !window.api?.metaSettings) { throw new Error( - 'Cannot get setting, no MetaSettings API or neccessary params are missing', + 'Cannot get setting, no MetaSettings API or necessary params are missing', ); } @@ -47,9 +47,9 @@ export const settingsApi = api.injectEndpoints({ value, }: Partial> & {value: unknown}) => { try { - if (!name || !user || !window.api.metaSettings) { + if (!name || !user || !window.api?.metaSettings) { throw new Error( - 'Cannot set setting, no MetaSettings API or neccessary params are missing', + 'Cannot set setting, no MetaSettings API or necessary params are missing', ); } @@ -89,9 +89,9 @@ export const settingsApi = api.injectEndpoints({ getSettings: builder.query({ queryFn: async ({name, user}: Partial, baseApi) => { try { - if (!window.api.metaSettings || !name || !user) { + if (!window.api?.metaSettings || !name || !user) { throw new Error( - 'Cannot get settings, no MetaSettings API or neccessary params are missing', + 'Cannot get settings, no MetaSettings API or necessary params are missing', ); } const data = await window.api.metaSettings.getSettings({name, user}); @@ -109,17 +109,14 @@ export const settingsApi = api.injectEndpoints({ name: settingName, user, }; - const newSetting = { - name: settingName, - user, - value: parseSettingValue(settingData?.value) ?? defaultValue, - }; + const newSettingValue = + parseSettingValue(settingData?.value) ?? defaultValue; const patch = dispatch( settingsApi.util.upsertQueryData( 'getSingleSetting', cacheEntryParams, - newSetting, + newSettingValue, ), ); diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 80c7d6f0fb..ec5b0ebe1b 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -28,18 +28,18 @@ export function useSetting(name?: string): { if (!name) { return undefined; } - if (window.api.metaSettings) { + if (window.api?.metaSettings) { return settingFromMeta; } return settingFromLS; - }, [settingFromMeta, settingFromLS]); + }, [name, settingFromMeta, settingFromLS]); const saveValue = React.useCallback>( (value) => { if (!name) { return; } - if (window.api.metaSettings) { + if (window.api?.metaSettings) { setMetaSetting({user, name, value}); } else { saveSettingToLS(value); From c9989efaec335b5f380c7c6737724b005567a619 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 11:30:18 +0300 Subject: [PATCH 4/9] fix: do not set setting with invalid params --- src/store/reducers/settings/api.ts | 12 +++--------- src/store/reducers/settings/useSetting.ts | 8 +++++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index 39a909aba3..f11bf2b1ab 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -45,12 +45,10 @@ export const settingsApi = api.injectEndpoints({ name, user, value, - }: Partial> & {value: unknown}) => { + }: Omit & {value: unknown}) => { try { - if (!name || !user || !window.api?.metaSettings) { - throw new Error( - 'Cannot set setting, no MetaSettings API or necessary params are missing', - ); + if (!window.api?.metaSettings) { + throw new Error('MetaSettings API is not available'); } const data = await window.api.metaSettings.setSingleSetting({ @@ -71,10 +69,6 @@ export const settingsApi = api.injectEndpoints({ async onQueryStarted(args, {dispatch, queryFulfilled}) { const {name, user, value} = args; - if (!name) { - return; - } - // Optimistically update existing cache entry const patchResult = dispatch( settingsApi.util.updateQueryData('getSingleSetting', {name, user}, () => value), diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index ec5b0ebe1b..15ae4ef2e1 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -1,5 +1,7 @@ import React from 'react'; +import {isNil} from 'lodash'; + import {useSetting as useLSSetting} from '../../../utils/hooks'; import {useTypedSelector} from '../../../utils/hooks/useTypedSelector'; import {selectMetaUser} from '../authentication/authentication'; @@ -39,10 +41,10 @@ export function useSetting(name?: string): { if (!name) { return; } - if (window.api?.metaSettings) { - setMetaSetting({user, name, value}); - } else { + if (isNil(window.api?.metaSettings)) { saveSettingToLS(value); + } else if (window.api?.metaSettings && user) { + setMetaSetting({user, name, value}); } }, [user, name, setMetaSetting, saveSettingToLS], From d7dd85ee96081619df5af9b8b9ca57401dc5f118 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 11:35:31 +0300 Subject: [PATCH 5/9] fix: return default from hook instead of api --- src/store/reducers/settings/api.ts | 17 ++++------------- src/store/reducers/settings/useSetting.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index f11bf2b1ab..e8b7eabed8 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -8,25 +8,19 @@ import {serializeError} from '../../utils'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; -import {getSettingDefault, parseSettingValue, stringifySettingValue} from './utils'; +import {parseSettingValue, stringifySettingValue} from './utils'; export const settingsApi = api.injectEndpoints({ endpoints: (builder) => ({ getSingleSetting: builder.query>({ queryFn: async ({name, user}) => { try { - if (!name || !window.api?.metaSettings) { + if (!name || !user || !window.api?.metaSettings) { throw new Error( 'Cannot get setting, no MetaSettings API or necessary params are missing', ); } - const defaultValue = getSettingDefault(name) as unknown; - - if (!user) { - return {data: defaultValue}; - } - const data = await window.api.metaSettings.getSingleSetting({ name, user, @@ -34,7 +28,7 @@ export const settingsApi = api.injectEndpoints({ preventBatching: SETTINGS_OPTIONS[name]?.preventBatching, }); - return {data: parseSettingValue(data?.value) ?? defaultValue}; + return {data: parseSettingValue(data?.value)}; } catch (error) { return {error: serializeError(error)}; } @@ -97,14 +91,11 @@ export const settingsApi = api.injectEndpoints({ name.forEach((settingName) => { const settingData = data[settingName]; - const defaultValue = getSettingDefault(settingName); - const cacheEntryParams: GetSingleSettingParams = { name: settingName, user, }; - const newSettingValue = - parseSettingValue(settingData?.value) ?? defaultValue; + const newSettingValue = parseSettingValue(settingData?.value); const patch = dispatch( settingsApi.util.upsertQueryData( diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 15ae4ef2e1..b80a963bae 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -7,6 +7,7 @@ import {useTypedSelector} from '../../../utils/hooks/useTypedSelector'; import {selectMetaUser} from '../authentication/authentication'; import {settingsApi} from './api'; +import {getSettingDefault} from './utils'; type SaveSettingValue = (value: T | undefined) => void; @@ -30,10 +31,16 @@ export function useSetting(name?: string): { if (!name) { return undefined; } + const defaultValue = getSettingDefault(name) as unknown; + + let value; + if (window.api?.metaSettings) { - return settingFromMeta; + value = settingFromMeta; + } else { + value = settingFromLS; } - return settingFromLS; + return value ?? defaultValue; }, [name, settingFromMeta, settingFromLS]); const saveValue = React.useCallback>( From 71b22241b4fce874149c7c6caa053816757ff392 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 12:14:59 +0300 Subject: [PATCH 6/9] fix: bot review - api params always defined --- src/store/reducers/settings/api.ts | 16 ++++++---------- src/store/reducers/settings/useSetting.ts | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index e8b7eabed8..ca3c06fe31 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -12,13 +12,11 @@ import {parseSettingValue, stringifySettingValue} from './utils'; export const settingsApi = api.injectEndpoints({ endpoints: (builder) => ({ - getSingleSetting: builder.query>({ + getSingleSetting: builder.query({ queryFn: async ({name, user}) => { try { - if (!name || !user || !window.api?.metaSettings) { - throw new Error( - 'Cannot get setting, no MetaSettings API or necessary params are missing', - ); + if (!window.api?.metaSettings) { + throw new Error('MetaSettings API is not available'); } const data = await window.api.metaSettings.getSingleSetting({ @@ -75,12 +73,10 @@ export const settingsApi = api.injectEndpoints({ }, }), getSettings: builder.query({ - queryFn: async ({name, user}: Partial, baseApi) => { + queryFn: async ({name, user}: GetSettingsParams, baseApi) => { try { - if (!window.api?.metaSettings || !name || !user) { - throw new Error( - 'Cannot get settings, no MetaSettings API or necessary params are missing', - ); + if (!window.api?.metaSettings) { + throw new Error('MetaSettings API is not available'); } const data = await window.api.metaSettings.getSettings({name, user}); diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index b80a963bae..9d8388dd1d 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -1,5 +1,6 @@ import React from 'react'; +import {skipToken} from '@reduxjs/toolkit/query'; import {isNil} from 'lodash'; import {useSetting as useLSSetting} from '../../../utils/hooks'; @@ -18,10 +19,14 @@ export function useSetting(name?: string): { } { const user = useTypedSelector(selectMetaUser); - const {currentData: settingFromMeta, isLoading} = settingsApi.useGetSingleSettingQuery({ - user, - name, - }); + const params = React.useMemo(() => { + if (user && name && window.api?.metaSettings) { + return {user, name}; + } + return skipToken; + }, [user, name]); + + const {currentData: settingFromMeta, isLoading} = settingsApi.useGetSingleSettingQuery(params); const [setMetaSetting] = settingsApi.useSetSingleSettingMutation(); @@ -31,9 +36,9 @@ export function useSetting(name?: string): { if (!name) { return undefined; } - const defaultValue = getSettingDefault(name) as unknown; + const defaultValue = getSettingDefault(name); - let value; + let value: unknown; if (window.api?.metaSettings) { value = settingFromMeta; @@ -50,7 +55,7 @@ export function useSetting(name?: string): { } if (isNil(window.api?.metaSettings)) { saveSettingToLS(value); - } else if (window.api?.metaSettings && user) { + } else if (user) { setMetaSetting({user, name, value}); } }, From dfa745a8c1564791a95f7845f00b5c7e787cfab0 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 14:43:54 +0300 Subject: [PATCH 7/9] fix: make condition straightforward --- src/store/reducers/settings/useSetting.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 9d8388dd1d..46affb50c1 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -53,10 +53,11 @@ export function useSetting(name?: string): { if (!name) { return; } + if (window.api.metaSettings && user) { + setMetaSetting({user, name, value}); + } if (isNil(window.api?.metaSettings)) { saveSettingToLS(value); - } else if (user) { - setMetaSetting({user, name, value}); } }, [user, name, setMetaSetting, saveSettingToLS], From 7ff81e23b1ce1542fab9496f67cffcb7219b9052 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 14:56:07 +0300 Subject: [PATCH 8/9] fix: move serializeReduxError to common utils --- src/store/reducers/capabilities/capabilities.ts | 4 ++-- src/store/reducers/settings/api.ts | 8 ++++---- .../utils.ts => utils/errors/serializeReduxError.ts} | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/{store/utils.ts => utils/errors/serializeReduxError.ts} (83%) diff --git a/src/store/reducers/capabilities/capabilities.ts b/src/store/reducers/capabilities/capabilities.ts index c0a71b9da3..79edc6d2a0 100644 --- a/src/store/reducers/capabilities/capabilities.ts +++ b/src/store/reducers/capabilities/capabilities.ts @@ -1,8 +1,8 @@ import {createSelector} from '@reduxjs/toolkit'; import type {Capability, MetaCapability, SecuritySetting} from '../../../types/api/capabilities'; +import {serializeReduxError} from '../../../utils/errors/serializeReduxError'; import type {AppDispatch, RootState} from '../../defaultStore'; -import {serializeError} from '../../utils'; import {api} from './../api'; @@ -31,7 +31,7 @@ export const capabilitiesApi = api.injectEndpoints({ } catch (error) { // If capabilities endpoint is not available, there will be an error // That means no new features are available - return {error: serializeError(error)}; + return {error: serializeReduxError(error)}; } }, }), diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index ca3c06fe31..e42bf09b64 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -3,8 +3,8 @@ import type { GetSingleSettingParams, SetSingleSettingParams, } from '../../../types/api/settings'; +import {serializeReduxError} from '../../../utils/errors/serializeReduxError'; import type {AppDispatch} from '../../defaultStore'; -import {serializeError} from '../../utils'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; @@ -28,7 +28,7 @@ export const settingsApi = api.injectEndpoints({ return {data: parseSettingValue(data?.value)}; } catch (error) { - return {error: serializeError(error)}; + return {error: serializeReduxError(error)}; } }, }), @@ -55,7 +55,7 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { - return {error: serializeError(error)}; + return {error: serializeReduxError(error)}; } }, async onQueryStarted(args, {dispatch, queryFulfilled}) { @@ -109,7 +109,7 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { - return {error: serializeError(error)}; + return {error: serializeReduxError(error)}; } }, }), diff --git a/src/store/utils.ts b/src/utils/errors/serializeReduxError.ts similarity index 83% rename from src/store/utils.ts rename to src/utils/errors/serializeReduxError.ts index b56423709c..c8da09db23 100644 --- a/src/store/utils.ts +++ b/src/utils/errors/serializeReduxError.ts @@ -3,7 +3,7 @@ * * It prevents redux console error on string error in code - `throw new Error("description")` */ -export function serializeError(error: unknown) { +export function serializeReduxError(error: unknown) { if (error instanceof Error) { return {message: error.message, name: error.name}; } From 5d48c261c8a8fa10cc821d1a5ee01f8f7b52972f Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 10 Dec 2025 15:03:19 +0300 Subject: [PATCH 9/9] fix: missed optional chaining --- src/store/reducers/settings/useSetting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 46affb50c1..72402ae386 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -53,7 +53,7 @@ export function useSetting(name?: string): { if (!name) { return; } - if (window.api.metaSettings && user) { + if (window.api?.metaSettings && user) { setMetaSetting({user, name, value}); } if (isNil(window.api?.metaSettings)) {