Skip to content

Commit 649e632

Browse files
committed
fix: code config values and UI settings save both ignored defaults
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
1 parent ac22348 commit 649e632

8 files changed

Lines changed: 58 additions & 37 deletions

File tree

admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useIntl } from 'react-intl';
2-
import { isObject } from 'lodash';
32

43
import { Box, Flex, Grid, NumberInput, Toggle, Typography } from '@strapi/design-system';
54
import { Field } from '@sensinum/strapi-utils';
@@ -34,20 +33,11 @@ export const AdditionalSettingsPanel = () => {
3433
<NumberInput
3534
width="100%"
3635
name="allowedLevels"
37-
type="number"
3836
placeholder={formatMessage(
3937
getTrad('pages.settings.form.allowedLevels.placeholder')
4038
)}
41-
onChange={(eventOrPath: FormChangeEvent, value?: any) => {
42-
if (isObject(eventOrPath)) {
43-
const parsedVal = parseInt(eventOrPath.target.value);
44-
return handleChange(
45-
eventOrPath.target.name,
46-
isNaN(parsedVal) ? 0 : parsedVal,
47-
onChange
48-
);
49-
}
50-
return handleChange(eventOrPath, value, onChange);
39+
onValueChange={(value: number | undefined) => {
40+
handleChange('allowedLevels', value ?? 0, onChange);
5141
}}
5242
value={values.allowedLevels}
5343
disabled={restartStatus.required}

admin/src/pages/SettingsPage/hooks/useAPI.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getFetchClient } from '@strapi/strapi/admin';
2-
import { useMutation, useQuery } from '@tanstack/react-query';
2+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
33

44
import { getApiClient } from '../../../api';
55
import { resolveGlobalLikeId } from '../utils';
@@ -61,6 +61,7 @@ export const useContentTypes = () => {
6161
export const useSaveConfig = () => {
6262
const fetch = getFetchClient();
6363
const apiClient = getApiClient(fetch);
64+
const queryClient = useQueryClient();
6465

6566
return useMutation({
6667
mutationFn(data: UiFormSchema) {
@@ -85,5 +86,8 @@ export const useSaveConfig = () => {
8586
},
8687
});
8788
},
89+
onSuccess() {
90+
queryClient.invalidateQueries({ queryKey: apiClient.readSettingsConfigIndex() });
91+
},
8892
});
8993
};

admin/src/pages/SettingsPage/hooks/useInitialConfig.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import { UiFormSchema } from '../schemas';
33
import { ConfigSchema } from '../../../schemas';
44

@@ -24,8 +24,11 @@ type UseInitialConfigParams = {
2424
};
2525

2626
export const useInitialConfig = ({ config, setFormValue }: UseInitialConfigParams) => {
27+
const initialized = useRef(false);
28+
2729
useEffect(() => {
28-
if (config) {
30+
if (config && !initialized.current) {
31+
initialized.current = true;
2932
const {
3033
additionalFields,
3134
contentTypes,

admin/src/pages/SettingsPage/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
22
import { get, isNil, isObject, isString, set } from 'lodash';
3-
import { useMemo, useState } from 'react';
3+
import { useEffect, useMemo, useState } from 'react';
44
import { useIntl } from 'react-intl';
55

66
import { Button, Flex } from '@strapi/design-system';
@@ -203,7 +203,9 @@ const Inner = () => {
203203
};
204204

205205
export default function SettingsPage() {
206-
queryClient.invalidateQueries();
206+
useEffect(() => {
207+
queryClient.invalidateQueries();
208+
}, []);
207209

208210
return (
209211
<QueryClientProvider client={queryClient}>

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const config: JestConfigWithTsJest = {
1313
reporters: ['default', 'jest-junit'],
1414
globals: {
1515
'ts-jest': {
16+
tsconfig: './server/tsconfig.json',
1617
diagnostics: {
1718
warnOnly: true,
1819
},

server/src/config/setup.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,21 @@ export const configSetup = async ({
3333
name: 'navigation',
3434
});
3535
const getFromPluginDefaults: PluginDefaultConfigGetter = await strapi.plugin('navigation').config;
36+
const dbConfig = forceDefault
37+
? ({} as Partial<NavigationPluginConfigDBSchema>)
38+
: (((await pluginStore.get({ key: 'config' })) ?? {}) as Partial<NavigationPluginConfigDBSchema>);
39+
3640
const configRaw = forceDefault
3741
? ({} as NavigationPluginConfigDBSchema)
38-
: {
39-
...configBase.default,
40-
...((await pluginStore.get({
41-
key: 'config',
42-
})) ?? configBase.default),
43-
};
42+
: ({ ...configBase.default, ...dbConfig } as NavigationPluginConfigDBSchema);
4443

45-
let config = isEmpty(configRaw)
46-
? configRaw
47-
: (DynamicSchemas.configSchema.parse(configRaw) as unknown as ConfigSchema);
44+
if (!isEmpty(configRaw)) {
45+
DynamicSchemas.configSchema.parse(configRaw);
46+
}
4847

49-
const getWithFallback = getWithFallbackFactory(config, getFromPluginDefaults);
48+
const getWithFallback = getWithFallbackFactory(dbConfig, getFromPluginDefaults);
5049

51-
config = {
50+
const config: ConfigSchema = {
5251
additionalFields: getWithFallback<NavigationItemAdditionalField[]>('additionalFields'),
5352
contentTypes: getWithFallback<string[]>('contentTypes'),
5453
contentTypesNameFields: getWithFallback<PluginConfigNameFields>('contentTypesNameFields'),
@@ -75,9 +74,12 @@ export const configSetup = async ({
7574
};
7675

7776
const getWithFallbackFactory =
78-
(config: NavigationPluginConfigDBSchema, fallback: PluginDefaultConfigGetter) =>
77+
(dbConfig: Partial<NavigationPluginConfigDBSchema>, fallback: PluginDefaultConfigGetter) =>
7978
<T extends ReturnType<PluginDefaultConfigGetter>>(key: PluginConfigKeys) => {
80-
const value = config?.[key] ?? fallback(key);
79+
const value =
80+
dbConfig?.[key] ??
81+
fallback(key) ??
82+
(configBase.default as Record<string, unknown>)[key];
8183

8284
assertNotEmpty(value, new Error(`[Navigation] Config "${key}" is undefined`));
8385

server/tests/config/setup.test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('Navigation', () => {
3737
it('should read all from default plugin config when nothing is present in database', async () => {
3838
// Given
3939
getStore.mockReturnValue({});
40-
getFromConfig.mockReturnValue({});
40+
getFromConfig.mockReturnValue(undefined);
4141
getContentTypes.mockReturnValue({});
4242

4343
// When
@@ -66,7 +66,7 @@ describe('Navigation', () => {
6666
getStore.mockReturnValue({
6767
allowedLevels: faker.string.alphanumeric(),
6868
});
69-
getFromConfig.mockReturnValue({});
69+
getFromConfig.mockReturnValue(undefined);
7070
getContentTypes.mockReturnValue({});
7171

7272
// Then
@@ -88,7 +88,7 @@ describe('Navigation', () => {
8888
getStore.mockReturnValue({
8989
additionalFields: invalidField,
9090
});
91-
getFromConfig.mockReturnValue({});
91+
getFromConfig.mockReturnValue(undefined);
9292
getContentTypes.mockReturnValue({});
9393

9494
// Then
@@ -107,7 +107,24 @@ describe('Navigation', () => {
107107
getStore.mockReturnValue({
108108
allowedLevels,
109109
});
110-
getFromConfig.mockReturnValue({});
110+
getFromConfig.mockReturnValue(undefined);
111+
getContentTypes.mockReturnValue({});
112+
113+
// When
114+
const result = await configSetup({ strapi });
115+
116+
// Then
117+
expect(result).toHaveProperty('allowedLevels', allowedLevels);
118+
});
119+
120+
it('should use code config values when DB is empty', async () => {
121+
// Given
122+
const allowedLevels = 5; // non-default value (default is 2)
123+
124+
getStore.mockReturnValue(null);
125+
getFromConfig.mockImplementation((key: string) =>
126+
key === 'allowedLevels' ? allowedLevels : undefined
127+
);
111128
getContentTypes.mockReturnValue({});
112129

113130
// When
@@ -146,7 +163,7 @@ describe('Navigation', () => {
146163
getStore.mockReturnValue({
147164
contentTypes: allContentTypes,
148165
});
149-
getFromConfig.mockReturnValue({});
166+
getFromConfig.mockReturnValue(undefined);
150167

151168
// When
152169
const result = await configSetup({ strapi });
@@ -174,7 +191,7 @@ describe('Navigation', () => {
174191
preferCustomContentTypes,
175192
isCacheEnabled,
176193
});
177-
getFromConfig.mockReturnValue({});
194+
getFromConfig.mockReturnValue(undefined);
178195
getContentTypes.mockReturnValue({});
179196

180197
// When

server/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"compilerOptions": {
55
"rootDir": "../",
66
"baseUrl": ".",
7-
"strict": true
7+
"outDir": "./dist",
8+
"strict": true,
9+
"types": ["jest"]
810
}
911
}

0 commit comments

Comments
 (0)