Skip to content

Commit e51f70b

Browse files
authored
[2.x] fix: keep a saved falsy setting in settings modals too (#4901)
#4782 fixed this in `AdminPage` but the identical line in `SettingsModal` survived, so the bug still reached every settings *modal*: settings live in a string column, a saved `false` arrives as an empty string, and choosing the fallback on falsiness discarded it and reseeded the default. A boolean setting whose default is truthy could not be switched off — it came back on with every reload. `SettingsModal` is public API, imported by extensions as `flarum/admin/components/SettingsModal`, and core's custom header, footer and CSS modals extend it, as does flarum/audit. The existing test file now covers both classes side by side rather than only the one the original issue named, which is what would have caught this the first time. Its docblock is corrected too: it claimed a saved `'0'` was also discarded, but the string is truthy in JavaScript, unlike the number — the empty string is the only value a setting can hold that JavaScript reads as absent.
1 parent d69eebc commit e51f70b

2 files changed

Lines changed: 63 additions & 7 deletions

File tree

framework/core/js/src/admin/components/SettingsModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ export default abstract class SettingsModal<CustomAttrs extends ISettingsModalAt
3838
}
3939

4040
setting(key: string, fallback: string = ''): Stream<SettingValue> {
41-
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
41+
// `??`, not `||`: settings live in a string column, so a saved `false`
42+
// arrives as an empty string. Falling back on falsiness discarded it and
43+
// reseeded the default, which meant a setting whose default is truthy could
44+
// never be switched off — it came back on with every reload. Only the
45+
// absence of a saved value should reach for the fallback.
46+
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] ?? fallback);
4247

4348
return this.settings[key];
4449
}

framework/core/js/tests/integration/admin/components/AdminPageSetting.test.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import bootstrapAdmin from '@flarum/jest-config/src/bootstrap/admin';
22
import AdminPage from '../../../../src/admin/components/AdminPage';
3+
import SettingsModal from '../../../../src/admin/components/SettingsModal';
34
import { app } from '../../../../src/admin';
45

56
beforeAll(() => bootstrapAdmin());
@@ -8,12 +9,17 @@ beforeAll(() => bootstrapAdmin());
89
* `setting(key, fallback)` seeds a stream from the saved value, falling back to
910
* the given default when there is nothing saved.
1011
*
11-
* Settings are stored in a string column, so a saved `false` comes back as `''`
12-
* and a saved `0` as `'0'` — both falsy in JavaScript. Choosing the fallback on
13-
* falsiness therefore discarded values an administrator had deliberately saved,
14-
* and a setting whose default is truthy could never be turned off: it reverted
15-
* on every reload (flarum/framework#4781). Only the *absence* of a saved value
16-
* should reach for the default.
12+
* Settings are stored in a string column, so a saved `false` comes back as an
13+
* empty string — the one value a setting can hold that JavaScript reads as
14+
* absent. Choosing the fallback on falsiness therefore discarded a value the
15+
* administrator had deliberately saved, and a setting whose default is truthy
16+
* could never be turned off: it reverted on every reload
17+
* (flarum/framework#4781). Only the *absence* of a saved value should reach for
18+
* the default.
19+
*
20+
* Two classes expose this, and both are public API: `AdminPage` for settings
21+
* pages, and `SettingsModal` for settings modals. They are covered together
22+
* because the bug was fixed in the first while surviving in the second.
1723
*/
1824
describe('AdminPage.setting()', () => {
1925
class TestPage extends AdminPage {
@@ -65,3 +71,48 @@ describe('AdminPage.setting()', () => {
6571
expect(seeded('0', '10')).toBe('0');
6672
});
6773
});
74+
75+
/**
76+
* The same contract, on the class settings *modals* are built from. `flarum/audit`
77+
* and core's own custom header, footer and CSS modals all extend it, and it is
78+
* part of the public API extensions import as
79+
* `flarum/admin/components/SettingsModal`.
80+
*/
81+
describe('SettingsModal.setting()', () => {
82+
class TestModal extends SettingsModal {
83+
title() {
84+
return 'Test';
85+
}
86+
87+
form() {
88+
return null;
89+
}
90+
}
91+
92+
const seeded = (saved: string | undefined, fallback: string): unknown => {
93+
const key = 'test.modal.setting';
94+
95+
if (saved === undefined) {
96+
delete app.data.settings[key];
97+
} else {
98+
app.data.settings[key] = saved;
99+
}
100+
101+
const modal = new TestModal();
102+
(modal as any).settings = {};
103+
104+
return modal.setting(key, fallback)();
105+
};
106+
107+
test('a saved value is used', () => {
108+
expect(seeded('1', '')).toBe('1');
109+
});
110+
111+
test('the fallback is used when nothing is saved', () => {
112+
expect(seeded(undefined, 'the-default')).toBe('the-default');
113+
});
114+
115+
test('a saved empty string is kept rather than falling back', () => {
116+
expect(seeded('', '1')).toBe('');
117+
});
118+
});

0 commit comments

Comments
 (0)