From 649e632ce71d4410e74e4b9621d61ae4cafa2cd2 Mon Sep 17 00:00:00 2001 From: Farbod Ghasemi Date: Sun, 7 Jun 2026 04:35:15 +0330 Subject: [PATCH] fix: code config values and UI settings save both ignored defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. Server (setup.ts): `plugins.ts` code config values were silently ignored for any key that already had a hard-coded default. The original code merged `configBase.default` into `configRaw` before building the priority chain, so the hard-coded default always won. Fixed by separating the raw DB read (`dbConfig`) from the Zod validation object, then applying the correct priority order: DB value → code config (plugins.ts) → hard-coded default. 2. UI (AdditionalSettingsPanel): `NumberInput` from @strapi/design-system exposes the parsed number via `onValueChange`, not `onChange`. The previous `onChange` handler received a raw value that was not a string field name, so `isString(fieldName)` evaluated to false and `setFormValueItem` was never called. `formValue.allowedLevels` stayed at 2 while the input display (driven by the Form component's internal `values`) correctly showed the user's input — causing the stale value to be submitted on save. Fixed by switching to `onValueChange`. Also included: - useInitialConfig: guard with useRef so the form is only seeded from the server once per mount, preventing background refetches from overwriting user edits - useSaveConfig: invalidate the config query after a successful save - SettingsPage: move queryClient.invalidateQueries() into useEffect so it runs once on mount instead of on every render - Test infrastructure: add outDir to server/tsconfig.json and tsconfig path to jest.config.ts so ts-jest resolves correctly - Regression test: configSetup respects code config allowedLevels when DB is empty --- .../AdditionalSettingsPanel/index.tsx | 14 ++------- admin/src/pages/SettingsPage/hooks/useAPI.ts | 6 +++- .../SettingsPage/hooks/useInitialConfig.ts | 7 +++-- admin/src/pages/SettingsPage/index.tsx | 6 ++-- jest.config.ts | 1 + server/src/config/setup.ts | 28 +++++++++--------- server/tests/config/setup.test.ts | 29 +++++++++++++++---- server/tsconfig.json | 4 ++- 8 files changed, 58 insertions(+), 37 deletions(-) diff --git a/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx b/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx index 623e5717..66ff16f7 100644 --- a/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx +++ b/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx @@ -1,5 +1,4 @@ import { useIntl } from 'react-intl'; -import { isObject } from 'lodash'; import { Box, Flex, Grid, NumberInput, Toggle, Typography } from '@strapi/design-system'; import { Field } from '@sensinum/strapi-utils'; @@ -34,20 +33,11 @@ export const AdditionalSettingsPanel = () => { { - if (isObject(eventOrPath)) { - const parsedVal = parseInt(eventOrPath.target.value); - return handleChange( - eventOrPath.target.name, - isNaN(parsedVal) ? 0 : parsedVal, - onChange - ); - } - return handleChange(eventOrPath, value, onChange); + onValueChange={(value: number | undefined) => { + handleChange('allowedLevels', value ?? 0, onChange); }} value={values.allowedLevels} disabled={restartStatus.required} diff --git a/admin/src/pages/SettingsPage/hooks/useAPI.ts b/admin/src/pages/SettingsPage/hooks/useAPI.ts index 6ffb8243..d92b9c6c 100644 --- a/admin/src/pages/SettingsPage/hooks/useAPI.ts +++ b/admin/src/pages/SettingsPage/hooks/useAPI.ts @@ -1,5 +1,5 @@ import { getFetchClient } from '@strapi/strapi/admin'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getApiClient } from '../../../api'; import { resolveGlobalLikeId } from '../utils'; @@ -61,6 +61,7 @@ export const useContentTypes = () => { export const useSaveConfig = () => { const fetch = getFetchClient(); const apiClient = getApiClient(fetch); + const queryClient = useQueryClient(); return useMutation({ mutationFn(data: UiFormSchema) { @@ -85,5 +86,8 @@ export const useSaveConfig = () => { }, }); }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: apiClient.readSettingsConfigIndex() }); + }, }); }; diff --git a/admin/src/pages/SettingsPage/hooks/useInitialConfig.ts b/admin/src/pages/SettingsPage/hooks/useInitialConfig.ts index 23a85ac8..4ba0b3d3 100644 --- a/admin/src/pages/SettingsPage/hooks/useInitialConfig.ts +++ b/admin/src/pages/SettingsPage/hooks/useInitialConfig.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { UiFormSchema } from '../schemas'; import { ConfigSchema } from '../../../schemas'; @@ -24,8 +24,11 @@ type UseInitialConfigParams = { }; export const useInitialConfig = ({ config, setFormValue }: UseInitialConfigParams) => { + const initialized = useRef(false); + useEffect(() => { - if (config) { + if (config && !initialized.current) { + initialized.current = true; const { additionalFields, contentTypes, diff --git a/admin/src/pages/SettingsPage/index.tsx b/admin/src/pages/SettingsPage/index.tsx index be105c7e..bb867ae2 100644 --- a/admin/src/pages/SettingsPage/index.tsx +++ b/admin/src/pages/SettingsPage/index.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { get, isNil, isObject, isString, set } from 'lodash'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { Button, Flex } from '@strapi/design-system'; @@ -203,7 +203,9 @@ const Inner = () => { }; export default function SettingsPage() { - queryClient.invalidateQueries(); + useEffect(() => { + queryClient.invalidateQueries(); + }, []); return ( diff --git a/jest.config.ts b/jest.config.ts index 966d4b55..80cbda77 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,6 +13,7 @@ const config: JestConfigWithTsJest = { reporters: ['default', 'jest-junit'], globals: { 'ts-jest': { + tsconfig: './server/tsconfig.json', diagnostics: { warnOnly: true, }, diff --git a/server/src/config/setup.ts b/server/src/config/setup.ts index 58fc508d..49a0b65b 100644 --- a/server/src/config/setup.ts +++ b/server/src/config/setup.ts @@ -33,22 +33,21 @@ export const configSetup = async ({ name: 'navigation', }); const getFromPluginDefaults: PluginDefaultConfigGetter = await strapi.plugin('navigation').config; + const dbConfig = forceDefault + ? ({} as Partial) + : (((await pluginStore.get({ key: 'config' })) ?? {}) as Partial); + const configRaw = forceDefault ? ({} as NavigationPluginConfigDBSchema) - : { - ...configBase.default, - ...((await pluginStore.get({ - key: 'config', - })) ?? configBase.default), - }; + : ({ ...configBase.default, ...dbConfig } as NavigationPluginConfigDBSchema); - let config = isEmpty(configRaw) - ? configRaw - : (DynamicSchemas.configSchema.parse(configRaw) as unknown as ConfigSchema); + if (!isEmpty(configRaw)) { + DynamicSchemas.configSchema.parse(configRaw); + } - const getWithFallback = getWithFallbackFactory(config, getFromPluginDefaults); + const getWithFallback = getWithFallbackFactory(dbConfig, getFromPluginDefaults); - config = { + const config: ConfigSchema = { additionalFields: getWithFallback('additionalFields'), contentTypes: getWithFallback('contentTypes'), contentTypesNameFields: getWithFallback('contentTypesNameFields'), @@ -75,9 +74,12 @@ export const configSetup = async ({ }; const getWithFallbackFactory = - (config: NavigationPluginConfigDBSchema, fallback: PluginDefaultConfigGetter) => + (dbConfig: Partial, fallback: PluginDefaultConfigGetter) => >(key: PluginConfigKeys) => { - const value = config?.[key] ?? fallback(key); + const value = + dbConfig?.[key] ?? + fallback(key) ?? + (configBase.default as Record)[key]; assertNotEmpty(value, new Error(`[Navigation] Config "${key}" is undefined`)); diff --git a/server/tests/config/setup.test.ts b/server/tests/config/setup.test.ts index d62b4116..9efecbfb 100644 --- a/server/tests/config/setup.test.ts +++ b/server/tests/config/setup.test.ts @@ -37,7 +37,7 @@ describe('Navigation', () => { it('should read all from default plugin config when nothing is present in database', async () => { // Given getStore.mockReturnValue({}); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); getContentTypes.mockReturnValue({}); // When @@ -66,7 +66,7 @@ describe('Navigation', () => { getStore.mockReturnValue({ allowedLevels: faker.string.alphanumeric(), }); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); getContentTypes.mockReturnValue({}); // Then @@ -88,7 +88,7 @@ describe('Navigation', () => { getStore.mockReturnValue({ additionalFields: invalidField, }); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); getContentTypes.mockReturnValue({}); // Then @@ -107,7 +107,24 @@ describe('Navigation', () => { getStore.mockReturnValue({ allowedLevels, }); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); + getContentTypes.mockReturnValue({}); + + // When + const result = await configSetup({ strapi }); + + // Then + expect(result).toHaveProperty('allowedLevels', allowedLevels); + }); + + it('should use code config values when DB is empty', async () => { + // Given + const allowedLevels = 5; // non-default value (default is 2) + + getStore.mockReturnValue(null); + getFromConfig.mockImplementation((key: string) => + key === 'allowedLevels' ? allowedLevels : undefined + ); getContentTypes.mockReturnValue({}); // When @@ -146,7 +163,7 @@ describe('Navigation', () => { getStore.mockReturnValue({ contentTypes: allContentTypes, }); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); // When const result = await configSetup({ strapi }); @@ -174,7 +191,7 @@ describe('Navigation', () => { preferCustomContentTypes, isCacheEnabled, }); - getFromConfig.mockReturnValue({}); + getFromConfig.mockReturnValue(undefined); getContentTypes.mockReturnValue({}); // When diff --git a/server/tsconfig.json b/server/tsconfig.json index 6b79e8cf..fa131d9d 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,6 +4,8 @@ "compilerOptions": { "rootDir": "../", "baseUrl": ".", - "strict": true + "outDir": "./dist", + "strict": true, + "types": ["jest"] } }