From ae08c9cb600b00256440bccb336745f01acdf60b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:02:01 +0100 Subject: [PATCH] fix(webapp): admin feature flag number inputs and scrolling (#3979) The global feature flags admin page had a few rough edges. The percentage flags are numeric (`z.coerce.number()`) but rendered as free-text inputs, so you could type non-numeric values that only failed validation after submitting - and the error surfaced behind the confirm dialog. The control-type detection now recognises numbers and renders a proper number input, with the min/max range as the placeholder so the type is clear even when the field is unset. The save error also shows inside the confirm dialog now, not just behind it. The action buttons were unreachable without zooming out. The admin layout wrapped each page in a plain block, so `h-full` page content overran the viewport by the height of the tab bar and got clipped by the `overflow-hidden` body. Making the layout a flex column bounds each page to the space below the tabs, so the existing per-page scroll works and the feature flags page scrolls like the Users/Orgs tabs. Also capped the confirm dialog's diff list so its footer stays on screen when there are many changes. --- .../components/admin/FeatureFlagsDialog.tsx | 15 ++++++++ .../app/components/admin/FlagControls.tsx | 35 +++++++++++++++++++ .../webapp/app/routes/admin.feature-flags.tsx | 24 ++++++++++++- apps/webapp/app/routes/admin.tsx | 8 +++-- apps/webapp/app/v3/featureFlags.ts | 10 ++++++ 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx index df8669d36..b1050e60f 100644 --- a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx +++ b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx @@ -18,6 +18,7 @@ import { UNSET_VALUE, BooleanControl, EnumControl, + NumberControl, StringControl, WorkerGroupControl, type WorkerGroup, @@ -242,6 +243,20 @@ export function FeatureFlagsDialog({ }} dimmed={!isOverridden} /> + ) : control.type === "number" ? ( + { + if (val === undefined) { + unsetFlag(key); + } else { + setFlagValue(key, val); + } + }} + dimmed={!isOverridden} + /> ) : control.type === "string" ? ( void; + min?: number; + max?: number; + dimmed: boolean; +}) { + // Number and string fields share the same input shape; surface the range + // (or "number") as the placeholder so an unset field still signals its type. + const placeholder = min !== undefined && max !== undefined ? `${min}-${max}` : "number"; + return ( + { + const next = e.target.valueAsNumber; + onChange(Number.isNaN(next) ? undefined : next); + }} + placeholder={placeholder} + className={cn("w-40", dimmed && "opacity-50")} + /> + ); +} + export function StringControl({ value, onChange, diff --git a/apps/webapp/app/routes/admin.feature-flags.tsx b/apps/webapp/app/routes/admin.feature-flags.tsx index 02faa7add..71a645a88 100644 --- a/apps/webapp/app/routes/admin.feature-flags.tsx +++ b/apps/webapp/app/routes/admin.feature-flags.tsx @@ -35,6 +35,7 @@ import { UNSET_VALUE, BooleanControl, EnumControl, + NumberControl, StringControl, WorkerGroupControl, type WorkerGroup, @@ -352,6 +353,22 @@ export default function AdminFeatureFlagsRoute() { /> )} + {control.type === "number" && ( + { + if (val === undefined) { + unsetFlag(key); + } else { + setFlagValue(key, val); + } + }} + dimmed={!isSet} + /> + )} + {control.type === "string" && ( ); @@ -467,6 +485,7 @@ function ConfirmDialog({ lockedKeys, onConfirm, isSaving, + saveError, }: { open: boolean; onOpenChange: (open: boolean) => void; @@ -476,6 +495,7 @@ function ConfirmDialog({ lockedKeys: readonly string[]; onConfirm: () => void; isSaving: boolean; + saveError: string | null; }) { const editableKeys = Object.keys(controlTypes) .filter((key) => !lockedKeys.includes(key)) @@ -519,7 +539,7 @@ function ConfirmDialog({ These changes affect all organizations globally. Please review carefully. -
+
{changes.length === 0 ? (

No changes to apply.

) : ( @@ -546,6 +566,8 @@ function ConfirmDialog({ )}
+ {saveError && {saveError}} +