From 60ea4ddaa22b3db6cfb3cd9ec14b2c361e9d6eb2 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:54:59 -0700 Subject: [PATCH 1/2] feat(config): clean read-only UX in disabled config sections The config editor previously left a number of interactive affordances visible in fully read-only sections, where the disabled state was either ambiguous, misleading, or actively broken. ConfigRow no longer applies a row-level pointer-events-none when disabled. The catch-all overreached into the SectionHeader (blocking caret expansion and other purely visual controls) and is now unnecessary because every value control inside SectionControls self-blocks via its own disabled attribute. The inline ToggleField at renderInlineField missed its disabled prop, so inline toggles in disabled sections still actuated and silently mutated editedValues without triggering the save bar. Fixed. BooleanChip is the disabled-state stand-in for ToggleField, but the SectionControls slot is a flex column with default align-items stretch, so the chip rendered full-width across the value area. Added self-start so it sits at its natural pill width. NestedGroup gains a disabled prop. When set, the caret/collapse affordance is dropped and the group renders flat with a static heading. Carets in fully read-only sections were misleading because they hinted at interactivity that no longer mattered. Threaded through the four NestedGroup call sites in FieldRenderer plus the More settings group in EndpointsRenderer. ListField now swaps native select for a static span when disabled and the field has enum options. Browsers render their own dropdown caret on disabled selects which conflicted with the new flat read-only look. CustomEndpointsRenderer and McpServersRenderer hide their Create button entirely when disabled rather than rendering an inert button. When disabled and there are no entries, a muted No custom endpoints configured or No MCP servers configured placeholder is shown so the expanded section is not just empty space. Two new English locale keys were added; locize-i18n-sync handles the rest. No behavior change for users with write capability on a section. All removals are gated on the existing disabled prop. --- src/components/configuration/ConfigRow.tsx | 1 - .../configuration/FieldRenderer.tsx | 46 +++++++++++++++- .../configuration/fields/ListField.tsx | 38 ++++++++------ .../sections/EndpointsRenderer.tsx | 52 +++++++++++-------- .../sections/McpServersRenderer.tsx | 30 +++++++---- src/locales/en/translation.json | 2 + 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/src/components/configuration/ConfigRow.tsx b/src/components/configuration/ConfigRow.tsx index c3a0b16..1e453d6 100644 --- a/src/components/configuration/ConfigRow.tsx +++ b/src/components/configuration/ConfigRow.tsx @@ -80,7 +80,6 @@ export function ConfigRow({ className={cn( 'config-row flex w-full gap-6 rounded-md px-2.5 py-2 transition-opacity', hasSubContent ? 'items-start' : 'items-center', - disabled && 'pointer-events-none', isPendingReset && 'opacity-50', !isPendingReset && !isConfigured && !isTouched && 'opacity-50', )} diff --git a/src/components/configuration/FieldRenderer.tsx b/src/components/configuration/FieldRenderer.tsx index 0a33409..b61c3b7 100644 --- a/src/components/configuration/FieldRenderer.tsx +++ b/src/components/configuration/FieldRenderer.tsx @@ -93,6 +93,7 @@ function ArrayObjectNestedGroup({ totalCount={items.length} depth={field.depth} onAdd={handleAdd} + disabled={disabled} > {arrayField} @@ -146,6 +147,7 @@ function RecordObjectNestedGroup({ totalCount={entries.length} depth={field.depth} onAdd={handleAdd} + disabled={disabled} > {recordField} @@ -549,7 +551,10 @@ function BooleanChip({ value }: { value: boolean }) { const localize = useLocalize(); return ( {localize(value ? 'com_ui_true' : 'com_ui_false')} @@ -565,6 +570,7 @@ export function NestedGroup({ depth = 0, onAdd, addLabel, + disabled, children, }: { label: string; @@ -575,6 +581,12 @@ export function NestedGroup({ /** When provided, renders a "+ Add" button inline in the header. */ onAdd?: () => void; addLabel?: string; + /** + * When true, the caret/collapse affordance is dropped and the group renders + * flat with a static heading. Used in fully read-only sections where the + * expand/collapse interaction would be misleading. + */ + disabled?: boolean; children: ReactNode; }) { const localize = useLocalize(); @@ -585,6 +597,33 @@ export function NestedGroup({ { defaultExpanded: hasConfigured, onAdd }, ); + if (disabled) { + return ( +
0 ? 'mt-3' : 'mt-4', 'flex flex-col')} + style={indent ? { paddingLeft: indent } : undefined} + > +
+ {label} + {totalCount > 0 && ( + + {configuredCount}/{totalCount} + + )} +
+ {children} +
+ ); + } + return (
addFieldRef.current?.() : undefined} + onAdd={!disabled && hasHideable ? () => addFieldRef.current?.() : undefined} addLabel={localize('com_config_add_field')} + disabled={disabled} > onChange(field.key, checked)} + disabled={disabled} aria-label={fieldLabel} /> @@ -1217,6 +1258,7 @@ export function FieldRenderer({ configuredCount={nestedCounts.configured} totalCount={nestedCounts.total} depth={group.field.depth} + disabled={disabled} > handleChange(index, e.target.value)} - disabled={disabled} - aria-label={itemLabel} - className="config-input flex-1" - > - {options.map((opt) => ( - - ))} - - ); + if (disabled) { + const matchedLabel = options.find((o) => o.value === value)?.label ?? value; + control = ( + + {matchedLabel} + + ); + } else { + control = ( + + ); + } } else if (variant === 'inline-edit') { control = ( @@ -614,30 +615,39 @@ export function CustomEndpointsRenderer(props: t.FieldRendererProps) { onChange(path, [...items, entry]); }; + const isEmpty = items.length === 0; + return (
-
- +
+ )} + {disabled && isEmpty ? ( +
+ {localize('com_config_no_custom_endpoints')} +
+ ) : ( + onChange(path, v)} + onEntryChange={(index, v) => onChange(`${path}.${index}`, v)} disabled={disabled} - className="config-add-btn" - > - - {localize('com_config_create_endpoint')} - -
- onChange(path, v)} - onEntryChange={(index, v) => onChange(`${path}.${index}`, v)} - disabled={disabled} - hideAddButton - renderFields={renderGroupedEndpointFields} - entryIdPrefix={`section-${path.split('.')[0]}-custom`} - /> + hideAddButton + renderFields={renderGroupedEndpointFields} + entryIdPrefix={`section-${path.split('.')[0]}-custom`} + /> + )} setCreateOpen(false)} diff --git a/src/components/configuration/sections/McpServersRenderer.tsx b/src/components/configuration/sections/McpServersRenderer.tsx index 8b577fe..7e390e1 100644 --- a/src/components/configuration/sections/McpServersRenderer.tsx +++ b/src/components/configuration/sections/McpServersRenderer.tsx @@ -906,19 +906,27 @@ export function McpServersRenderer(props: t.FieldRendererProps) { [onChange, path], ); + const isEmpty = entries.length === 0; + return (
-
- -
+ {!disabled && ( +
+ +
+ )} + {disabled && isEmpty && ( +
+ {localize('com_config_no_mcp_servers')} +
+ )} {entries.map(([key, entryValue]) => ( Date: Tue, 16 Jun 2026 00:53:58 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=AA=92=20fix:=20Tighten=20Disabled-Se?= =?UTF-8?q?ction=20Empty=20State=20+=20Preserve=20data-section-id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two P3 codex findings on the read-only UX commit. McpServersRenderer rendered the generic com_config_no_entries fallback even when the new disabled-and-empty readonly message also fired, producing duplicate empty states; gating the generic fallback on !disabled matches the same pattern EndpointsRenderer uses. The NestedGroup disabled branch lost the data-section-id={sectionId} attribute that the enabled branch carries on its toggle button, so read-only nested sections dropped out of useActiveSection's scroll-tracking scan even though they remained reachable by id; restoring the attribute on the disabled header div keeps active-section highlighting consistent between read-only and editable sections. --- src/components/configuration/FieldRenderer.tsx | 5 ++++- src/components/configuration/sections/McpServersRenderer.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/configuration/FieldRenderer.tsx b/src/components/configuration/FieldRenderer.tsx index b61c3b7..fec710b 100644 --- a/src/components/configuration/FieldRenderer.tsx +++ b/src/components/configuration/FieldRenderer.tsx @@ -606,7 +606,10 @@ export function NestedGroup({ className={cn(depth > 0 ? 'mt-3' : 'mt-4', 'flex flex-col')} style={indent ? { paddingLeft: indent } : undefined} > -
+
{label} {totalCount > 0 && ( ))} - {entries.length === 0 && ( + {!disabled && entries.length === 0 && (

{localize('com_config_no_entries')}