fix(admin): let a setting whose default is empty actually be cleared (#280)
Linting / lint (pull_request) Successful in 8m44s
SonarQube Analysis / sonarqube (pull_request) Successful in 40m1s

intakeNotifyEmail and intakeCeilingResetAt both document empty as their default and as a working configuration — no notification address, and no ceiling reset recorded. The validator refused every empty text value, so either could be set and then never removed through the admin at all; the only way back was a DELETE against admin_settings. An admin who turned intake notifications on could not turn them off.

Whether empty is a mistake is a fact about the setting rather than about its type, so it is now declared on the setting, in the DEFINITIONS row that already carries its type and fallback. A new setting states it once, in the place someone adding one is already editing, and nothing else has to know. That is what makes this different from special-casing two names in the validator, which would have left the next such setting to rediscover the same bug.

The blanket refusal stays the default, because for a setting with a non-empty fallback an empty value really is a mistake: an empty greeting format renders every greeting as nothing at all, which reads as a broken email rather than as something a person cleared. Both those cases keep their tests.

Whitespace is normalised to empty rather than stored. Somebody clearing a field they cannot see the end of leaves spaces behind, and they meant cleared.

The tests check that the clearing survives the request rather than only being echoed back — the last one sets a value, clears it, and then reads it again through GET, which is the assertion that would have caught this had it existed.

Closes #280

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 16:51:32 -05:00
co-authored by Claude Opus 5
parent 9cc1ac1918
commit be6800181d
3 changed files with 78 additions and 7 deletions
+17 -5
View File
@@ -8,6 +8,7 @@ import {
CHOICE_SETTINGS,
CHOICE_OPTIONS,
isValidChoice,
mayBeEmpty,
SettingName
} from '../adminSettings';
@@ -44,13 +45,24 @@ function readHours(name: SettingName, raw: unknown): Reading {
function readText(name: SettingName, raw: unknown): Reading {
if (raw === undefined) return SKIP;
if (typeof raw !== 'string' || raw.trim() === '') {
// Wrong for the two settings whose documented default is empty —
// intakeNotifyEmail and intakeCeilingResetAt cannot currently be cleared.
// Left as it was here deliberately: this change is the complexity refactor,
// and folding a behaviour fix into it would hide the fix. See #280.
if (typeof raw !== 'string') {
return { ok: false, error: `${name} cannot be empty` };
}
if (raw.trim() === '') {
// Whether empty is a mistake is a fact about the setting, not about the
// type, so it is asked of the setting (#280). intakeNotifyEmail and
// intakeCeilingResetAt both document empty as their default and as a
// working configuration — meaning "do not notify" and "no reset recorded" —
// and the blanket rule meant an address could be set and never removed
// except by a DELETE against the table.
if (!mayBeEmpty(name)) {
return { ok: false, error: `${name} cannot be empty` };
}
// Normalised, so whitespace is stored as cleared rather than as spaces.
// Someone clearing a field they cannot see the end of leaves whitespace,
// and they meant empty.
return { ok: true, value: '' };
}
return { ok: true, value: raw };
}