Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
4b0b2a9
✅ fix: Wave 2 Integration, Verification Fixes and Schema Coverage Gua…
danny-avila Oct 20, 2026
4cf7bed
🧭 feat: Purpose-Built AI Providers Editor (Wave 2)
danny-avila Oct 20, 2026
3fb0ac0
🛠️ feat: MCP Servers Editor (Wave 2)
danny-avila Oct 20, 2026
b35ef73
🔌 feat: Custom Endpoints List and Editor Drawer (Wave 2)
danny-avila Oct 20, 2026
6c84a38
🧩 feat: Model Specs Editor (Wave 2)
danny-avila Oct 20, 2026
843d799
✨ feat: Features Tab Editors (Wave 2)
danny-avila Oct 20, 2026
2455ccf
🗂️ feat: Files & Storage and System Editors (Wave 2)
danny-avila Oct 20, 2026
28da139
✅ test(config): Give Full-Tab Coverage Tests an Explicit Timeout
danny-avila Oct 20, 2026
995c1b3
🐛 fix: Keep Pending Edits Reachable When Showing Set Values Only
danny-avila Oct 22, 2026
a897480
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
0585188
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
f2a3c5b
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
4ebfc09
🐛 fix: Harden MCP Server Auth, Rename, Duplicate and Transport Edits
danny-avila Oct 22, 2026
9b8f6da
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
020ad7d
🐛 fix: Restore Saved Memory Agent IDs and Read Memory State Correctly
danny-avila Oct 22, 2026
ae3dac8
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
348b6f2
🐛 fix: Accurate Storage, Registration, Credits, Skill Sync and Rate L…
danny-avila Oct 22, 2026
00609dd
Merge branch 'feat/config-w2-6-filesys' into feat/config-w2-7-integra…
danny-avila Oct 22, 2026
eed760d
🐛 fix: Validate Imported MCP Servers Per Target and Explain Invalid I…
danny-avila Oct 22, 2026
b9d1d02
🔒 fix: Mask Secret Values in Collapsed Provider Summaries
danny-avila Oct 22, 2026
9aea2b8
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
ecbfe5d
🐛 fix: Restore Undone Endpoint Deletions in Their Original Order
danny-avila Oct 22, 2026
e8e08ac
🐛 fix: Keep Pending Inherited Endpoint Overrides Listed Under Set Val…
danny-avila Oct 22, 2026
1030105
🐛 fix: Store Complete Built-In Parameter Definitions
danny-avila Oct 22, 2026
de1e539
🐛 fix: Insert Header Placeholders Into Header Values Only
danny-avila Oct 22, 2026
0f404a7
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
3d5460b
🐛 fix: Keep Spec Order When One Save Adds and Removes Specs
danny-avila Oct 22, 2026
16abc8a
🐛 fix: Keep a New Spec's Required Fields Under Set Values Only
danny-avila Oct 22, 2026
c09f17f
🐛 fix: Block Saving Blank or Space-Padded Duplicate Spec Names
danny-avila Oct 22, 2026
3e04456
🐛 fix: Index Only the Prompt Fields a Spec Editor Shows
danny-avila Oct 22, 2026
ebdab4c
🐛 fix: Compare a Spec Rename With the Name It Was Loaded With
danny-avila Oct 22, 2026
5a1c35d
🐛 fix: Don't Inherit Spec Fields Through a Replaced Spec List
danny-avila Oct 22, 2026
e7d8998
🐛 fix: Clear the Agent ID When a Spec Leaves the Agents Endpoint
danny-avila Oct 22, 2026
821bf2a
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
e03a2e5
🔒 fix: Require Stored MCP Secrets Again on Rename and Drop Rename Rec…
danny-avila Oct 22, 2026
5b5be53
🐛 fix: Keep an Overridden Inherited MCP Server Visible Under Set Valu…
danny-avila Oct 22, 2026
a3fbfd8
🐛 fix: Pick MCP Copy Names Free in Any Letter Case
danny-avila Oct 22, 2026
5955f12
🔒 fix: Show Stored MCP Secrets as Their Masked Preview With Replace
danny-avila Oct 22, 2026
c7d1b88
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
418417b
🐛 fix: Keep a Stored Memory Agent ID When Switching Back Before Saving
danny-avila Oct 22, 2026
6b58e7f
🐛 fix: Forget the Chosen Memory Agent Definition When the Edit Sessio…
danny-avila Oct 22, 2026
2401894
🐛 fix: Show Every Setting of a Web Search Service in Use, Pending One…
danny-avila Oct 22, 2026
31a367b
🐛 fix: Keep Feature Groups With Pending Edits Under Set Values Only
danny-avila Oct 22, 2026
76c81de
🐛 fix: Keep a Newly Edited Memory Agent Under Set Values Only
danny-avila Oct 22, 2026
e4aea2a
🐛 fix: Drop a Pending Domain List When Its Last Domain Is Removed
danny-avila Oct 22, 2026
67b42b3
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
355e53f
🐛 fix: Keep Pending Upload Limits Under Set Values Only
danny-avila Oct 22, 2026
e622304
🐛 fix: Read Rate Limits From librechat.yaml and List Unplaced Schema …
danny-avila Oct 22, 2026
abda3ec
🐛 fix: Show Startup-Only Registration Settings From librechat.yaml
danny-avila Oct 22, 2026
962b3ea
🐛 fix: Render Storage File Types the Fixed List Does Not Know
danny-avila Oct 22, 2026
9a17b72
Merge branch 'feat/config-w2-6-filesys' into feat/config-w2-7-integra…
danny-avila Oct 22, 2026
c1e3573
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
666288f
🎨 style: Format MCP Secret Preview and Rename Validation Code
danny-avila Oct 22, 2026
1247f3e
🎨 style: Format Web Search Service Tests
danny-avila Oct 22, 2026
dad24c5
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
505af38
Merge branch 'feat/config-w2-6-filesys' into feat/config-w2-7-integra…
danny-avila Oct 22, 2026
1aca69b
🐛 fix: Keep Configured Azure Groups Visible When Showing Set Values Only
danny-avila Oct 22, 2026
eb3ff9a
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
28238c1
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
5888feb
🐛 fix: Resolve Swapped and Chained Entry Renames as One Batch
danny-avila Oct 22, 2026
1c9f458
🐛 fix: Keep Card State With a Spec When the List Is Reordered
danny-avila Oct 22, 2026
626b7ae
🐛 fix: Align the Spec Editor's Model, Parameter and Unknown Rows With…
danny-avila Oct 22, 2026
8ec1a05
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
4914645
🐛 fix: Ask a Renamed MCP Server Only for Secrets Its Authentication S…
danny-avila Oct 22, 2026
193ab79
🐛 fix: Clear MCP Delete Undo State When the Edit Session Ends
danny-avila Oct 22, 2026
56b3109
🐛 fix: Keep a Renamed MCP Server's Field Edits When Its Rename Is Rev…
danny-avila Oct 22, 2026
dede842
🎨 style: Format the Spec Editor's Parameter Imports
danny-avila Oct 22, 2026
6e47384
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
b0e21b0
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
7a09cb8
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
e279e7e
🐛 fix: Keep Storage Backends the Panel Does Not Know
danny-avila Oct 22, 2026
c7ab348
🐛 fix: List Only Set Rate Limiters When Showing Set Values Only
danny-avila Oct 22, 2026
dba753c
🐛 fix: List Unknown GitHub Skill Sync Settings in the GitHub Group
danny-avila Oct 22, 2026
ddc823b
🐛 fix: List Unknown Keys of librechat.yaml-Only Objects in Their Group
danny-avila Oct 22, 2026
adcc44b
🐛 fix: Confirm an Empty MIME List Even When It Matches the List Beneath
danny-avila Oct 22, 2026
e5b3d02
Merge branch 'feat/config-w2-6-filesys' into feat/config-w2-7-integra…
danny-avila Oct 22, 2026
bf90e66
🐛 fix: Remove a Setting Whole When a Reverted Rename Had Dropped It
danny-avila Oct 22, 2026
a283353
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
11caaa1
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
c4967a8
Merge branch 'feat/config-w2-6-filesys' into feat/config-w2-7-integra…
danny-avila Oct 22, 2026
9dc5195
Merge main (previous level's squash abeed9b) into feat/config-w2-7-in…
danny-avila Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions e2e/a11y/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ for (const theme of THEMES) {
expect(await dialogViolations(page)).toEqual([]);
});

test('the change review of a spec list taking over librechat.yaml (its warning text)', async ({
page,
}) => {
await openConfigTab(page, 'specs');
const card = page.locator('[data-entry-card]').first();
test.skip((await card.count()) === 0, 'No model specs');
await card.locator('button[aria-expanded]').first().click();
const label = page.locator('#modelSpecs-list-0-label');
await label.fill(`${await label.inputValue()} (review)`);
await label.press('Tab');
await page.getByRole('button', { name: 'Review & save' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
expect(await dialogViolations(page)).toEqual([]);
});

test('the import wizard', async ({ page }) => {
await openConfigTab(page, 'system');
await page.getByRole('button', { name: 'Import YAML' }).click();
Expand All @@ -111,6 +126,84 @@ for (const theme of THEMES) {
});
}

/** The Wave 2 editors' overlays at phone width, where their bodies scroll. */
for (const theme of THEMES) {
test.describe(`axe on editor overlays: ${theme} 390px`, () => {
test.use({ colorScheme: theme, viewport: { width: 390, height: 844 } });

test('the Azure group details drawer', async ({ page }) => {
await openConfigTab(page, 'providers');
await page.locator('#section-endpoints\\.azureOpenAI .settings-section-trigger').click();
/** The group with the longest model list, so the drawer body scrolls at this height. */
const counts = await page
.locator('.azure-groups-table tbody tr .azure-col-number')
.allInnerTexts();
const longest = counts.reduce(
(best, text, i) => (Number(text) > Number(counts[best]) ? i : best),
0,
);
await page
.getByRole('button', { name: /^Details of Azure group/ })
.nth(longest)
.click();
await expect(page.getByRole('dialog')).toBeVisible();
expect(await dialogViolations(page)).toEqual([]);
});

test('the custom endpoint editor drawer', async ({ page }) => {
await openConfigTab(page, 'custom');
await page.locator('li[data-endpoint-row] button[aria-haspopup="dialog"]').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
expect(await dialogViolations(page)).toEqual([]);
});

test('Create endpoint', async ({ page }) => {
await openConfigTab(page, 'custom');
await page.getByRole('button', { name: 'Create endpoint' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
expect(await dialogViolations(page)).toEqual([]);
});

test('Create MCP server', async ({ page }) => {
await openConfigTab(page, 'mcp');
await page.getByRole('button', { name: 'Create MCP server' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
expect(await dialogViolations(page)).toEqual([]);
});
});
}

test.describe('row action menus', () => {
test.use({ colorScheme: 'light', viewport: { width: 1440, height: 900 } });

test('custom: an action that opens the editor leaves focus in it', async ({ page }) => {
await openConfigTab(page, 'custom');
const trigger = page.getByRole('button', { name: /^Actions for / }).first();
await trigger.focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('menu')).toBeVisible();
await page.keyboard.press('Enter');
const drawer = page.getByRole('dialog');
await expect(drawer).toBeVisible();
await expect
.poll(() => drawer.evaluate((el) => el.contains(document.activeElement)))
.toBe(true);
});

for (const tab of ['custom', 'mcp'] as const) {
test(`${tab}: Escape returns focus to the menu button`, async ({ page }) => {
await openConfigTab(page, tab);
const trigger = page.getByRole('button', { name: /^Actions for / }).first();
await trigger.focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('menu')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('menu')).toBeHidden();
await expect(trigger).toBeFocused();
});
}
});

test.describe('settings layout invariants (1440px, light)', () => {
test.use({
colorScheme: 'light',
Expand Down Expand Up @@ -218,3 +311,55 @@ for (const variant of VISUAL_VARIANTS) {
}
});
}

test.describe('dialogs give focus back to what opened them', () => {
test.use({ colorScheme: 'light', viewport: { width: 1440, height: 900 } });

test('Create MCP server, closed with Escape', async ({ page }) => {
await openConfigTab(page, 'mcp');
const create = page.getByRole('button', { name: 'Create MCP server' });
await create.focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
await expect(create).toBeFocused();
});

test('the change review, closed with Escape', async ({ page }) => {
await openConfigTab(page, 'system');
const field = page.locator('#balance-refillAmount');
await field.fill('12345');
await field.press('Tab');
const review = page.getByRole('button', { name: 'Review & save' });
await review.focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
await expect(review).toBeFocused();
});
});

test.describe('custom endpoint drawer', () => {
test.use({ colorScheme: 'light', viewport: { width: 1440, height: 900 } });

test('a section link scrolls the drawer to its section', async ({ page }) => {
await openConfigTab(page, 'custom');
await page.locator('li[data-endpoint-row] button[aria-haspopup="dialog"]').first().click();
const drawer = page.locator('[data-endpoint-drawer]');
await expect(drawer).toBeVisible();
const links = drawer.locator('nav a');
await links.last().click();
await expect(links.last()).toHaveAttribute('aria-current', 'location');
const scrolled = () =>
drawer.locator('nav').evaluate((nav) => {
let node = nav.parentElement;
while (node && !/(auto|scroll)/.test(getComputedStyle(node).overflowY)) {
node = node.parentElement;
}
return node?.scrollTop ?? 0;
});
await expect.poll(scrolled).toBeGreaterThan(0);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion e2e/config-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ test.skip(!LIVE, LIVE_SKIP_REASON);

const TABS = ['providers', 'custom', 'specs', 'mcp', 'features', 'files', 'system'];
/** Collapsed sections, groups and cards (not a model chip's inline editor or a select). */
/** Collapsed sections, groups and cards (not menus, selects, tabs, or a chip's "why" disclosure). */
const COLLAPSED =
'form [aria-expanded="false"]:not([aria-haspopup]):not([role=combobox]):not([role=tab]):not(.icon-btn)';
'form [aria-expanded="false"]:not([aria-haspopup]):not([role=combobox]):not([role=tab]):not(.icon-btn):not(.state-chip-link)';

async function openTab(page: Page, tab: string, extra = '') {
await page.goto(`/configuration?tab=${tab}${extra}`);
Expand Down
2 changes: 1 addition & 1 deletion e2e/import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test('imports into a role that has no profile yet', async ({ page }) => {
const admin = page.getByRole('radio', { name: /ADMIN/ });
await admin.click();
const { dialog, apply } = await previewAndApply(page, /into Role: ADMIN$/);
await expect(dialog.getByText('Custom welcome')).toBeVisible();
await expect(dialog.getByText('Welcome message')).toBeVisible();
await apply.click();
await expect(page.getByText(/Imported 1 value into Role: ADMIN/).first()).toBeVisible();
});
Expand Down
12 changes: 7 additions & 5 deletions e2e/specs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ async function revertList(page: Page) {
await saveAndWait(page);
}

/** Adds a spec with the four required fields, and returns its card. */
/** Adds a spec with the four required fields (it opens on Endpoint, then Model), and returns its card. */
async function addSpec(page: Page, label: string, endpoint: string, model: string) {
await page.getByRole('button', { name: 'Add spec' }).click();
const card = cards(page).last();
await expect(card.getByLabel(/^Label\*?$/)).toBeFocused();
await card.getByLabel(/^Label\*?$/).fill(label);
await card.getByLabel(/^Label\*?$/).press('Tab');
await expect(card.getByLabel(/^Endpoint\*?$/)).toBeFocused();
await card.getByLabel(/^Endpoint\*?$/).selectOption(endpoint);
await card.getByRole('combobox', { name: /^Model\*?$/ }).fill(model);
await card.getByRole('combobox', { name: /^Model\*?$/ }).press('Tab');
await card.getByLabel(/^Label\*?$/).fill(label);
await card.getByLabel(/^Label\*?$/).press('Tab');
return card;
}

Expand Down Expand Up @@ -180,13 +180,15 @@ test.describe('Model specs', () => {
page,
}) => {
await openSpecs(page, '&scope=role:USER');
const customize = page.getByRole('button', { name: /^Customize list for/ });
const customize = page.getByRole('button', { name: /^Override for Role: USER/ });
test.skip((await customize.count()) === 0, 'Needs a USER role profile without its own list');
await expect(page.getByRole('button', { name: 'Add spec' })).toHaveCount(0);
await expect(cards(page).first()).toBeVisible();
await customize.click();
await expect(page.getByRole('button', { name: 'Add spec' })).toBeVisible();
await expect(page.getByText(/replaces Base's list/)).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
await expect(page.getByRole('button', { name: 'Add spec' })).toHaveCount(0);
});

test('the tab filter finds spec fields and phone width has no sideways scroll', async ({
Expand Down
2 changes: 1 addition & 1 deletion src/components/access/GroupsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) {
<button
type="button"
onClick={() => setEditTarget(group)}
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline)"
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
>
<div className="text-sm font-medium text-(--cui-color-text-default) hover:underline">
{group.name}
Expand Down
2 changes: 1 addition & 1 deletion src/components/access/RolesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) {
<button
type="button"
onClick={() => setEditTarget(role)}
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline)"
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-(--cui-color-text-default) hover:underline">
Expand Down
97 changes: 55 additions & 42 deletions src/components/configuration/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { CollectionEditsContext } from '@/contexts/CollectionEditsContext';
import { ConfigErrorsProvider } from '@/contexts/ConfigErrorsContext';
import { buildTocEntries, findTocEntry, toJumpTargets } from './toc';
import { RevealTargetContext } from '@/contexts/RevealTargetContext';
import { GlobalRevealContext } from '@/contexts/GlobalRevealContext';
import { ConfigTableOfContents } from './ConfigTableOfContents';
import { ResetBaseConfigDialog } from './ResetBaseConfigDialog';
import { NavigationGuardDialog } from './NavigationGuardDialog';
Expand Down Expand Up @@ -400,6 +401,15 @@ export function ConfigPage({
if (highlightField && filterPaths && !isRowVisible(filterPaths, highlightField)) clearFilter();
}, [highlightField, filterPaths, clearFilter]);

/** "Overridden by Global →" chips, on any tab, open the Global overrides setting that wins (D11). */
const revealGlobal = useCallback(
(path: string) =>
navigate({
search: (prev: t.ConfigSearchParams) => ({ ...prev, tab: tabForPath(path), field: path }),
}),
[navigate],
);

const configuredPaths = layers.activeConfiguredPaths;
const revealSetting = useCallback(
(item: t.ConfigSearchItem) => {
Expand Down Expand Up @@ -572,48 +582,50 @@ export function ConfigPage({
{isReady && (
<ScopeActionsProvider value={scopeActions}>
<RevealTargetContext.Provider value={highlightField ?? null}>
<CollectionEditsContext.Provider value={edits.collectionEdits}>
<ConfigTabContent
sections={sectionsForActiveTab}
configValues={activeConfigValues}
editedValues={editedValues}
onFieldChange={edits.handleFieldChange}
onResetField={edits.handleResetField}
onDiscardField={edits.handleDiscardField}
previewMode={false}
previewScope={editingScope}
previewChangedPaths={layers.scopeChangedPaths}
resolvedValues={layers.scopeResolvedValues}
permissions={permissions}
showChangedOnly={false}
readOnly={!canEditActiveTab || scopeNotFound != null}
configuredPaths={layers.activeConfiguredPaths}
dbOverridePaths={layers.overridePaths}
listOverrideCounts={layers.listOverrideCounts}
touchedPaths={edits.touchedPaths}
pendingResets={edits.pendingResets}
sectionPermissions={sectionPermissions}
schemaDefaults={layers.schemaDefaults}
showConfiguredOnly={customizedOnly}
isEditingScope={isEditingScope}
baseRecordKeys={layers.baseRecordKeys}
onValidationError={notifyError}
editSessionId={edits.editSessionId}
unknownSettings={topLevelUnknown}
onShowAll={showAllSettings}
filterText={filterText}
onClearFilter={clearFilter}
visibleSectionIds={filterSectionIds}
filterPaths={filterPaths}
valueLayers={layers.valueLayers}
visibilityEdits={edits.visibilityEdits}
onHideField={permissions.canAssign ? edits.handleHideField : undefined}
onUnhideField={
permissions.canAssign ? edits.handleUnhideField : undefined
}
onRenameEntry={edits.handleRenameEntry}
/>
</CollectionEditsContext.Provider>
<GlobalRevealContext.Provider value={revealGlobal}>
<CollectionEditsContext.Provider value={edits.collectionEdits}>
<ConfigTabContent
sections={sectionsForActiveTab}
configValues={activeConfigValues}
editedValues={editedValues}
onFieldChange={edits.handleFieldChange}
onResetField={edits.handleResetField}
onDiscardField={edits.handleDiscardField}
previewMode={false}
previewScope={editingScope}
previewChangedPaths={layers.scopeChangedPaths}
resolvedValues={layers.scopeResolvedValues}
permissions={permissions}
showChangedOnly={false}
readOnly={!canEditActiveTab || scopeNotFound != null}
configuredPaths={layers.activeConfiguredPaths}
dbOverridePaths={layers.overridePaths}
listOverrideCounts={layers.listOverrideCounts}
touchedPaths={edits.touchedPaths}
pendingResets={edits.pendingResets}
sectionPermissions={sectionPermissions}
schemaDefaults={layers.schemaDefaults}
showConfiguredOnly={customizedOnly}
isEditingScope={isEditingScope}
baseRecordKeys={layers.baseRecordKeys}
onValidationError={notifyError}
editSessionId={edits.editSessionId}
unknownSettings={topLevelUnknown}
onShowAll={showAllSettings}
filterText={filterText}
onClearFilter={clearFilter}
visibleSectionIds={filterSectionIds}
filterPaths={filterPaths}
valueLayers={layers.valueLayers}
visibilityEdits={edits.visibilityEdits}
onHideField={permissions.canAssign ? edits.handleHideField : undefined}
onUnhideField={
permissions.canAssign ? edits.handleUnhideField : undefined
}
onRenameEntry={edits.handleRenameEntry}
/>
</CollectionEditsContext.Provider>
</GlobalRevealContext.Provider>
</RevealTargetContext.Provider>
</ScopeActionsProvider>
)}
Expand Down Expand Up @@ -666,6 +678,7 @@ export function ConfigPage({
open={resetBase.resetBaseOpen}
resetting={resetBase.resettingBase}
error={resetBase.resetBaseError}
termsUrl={resetBase.resetBaseTermsUrl}
onConfirm={resetBase.handleResetBaseConfig}
onCancel={resetBase.cancelResetBase}
overrides={baseOverrides}
Expand Down
7 changes: 6 additions & 1 deletion src/components/configuration/ConfigRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ export function ConfigRow({
onUndo={undo}
undoLabel={undoLabel}
/>
{showsShadow(state) && <ShadowBadge shadowedBy={state.shadowedBy} />}
{showsShadow(state) && (
<ShadowBadge
shadowedBy={state.shadowedBy}
source={localize('com_config_ui_state_edited')}
/>
)}
</>
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/configuration/ConfigTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { RevealTargetContext } from '@/contexts/RevealTargetContext';
import { FieldRenderer, SingleFieldRenderer } from './renderer';
import { RowFilterContext } from '@/contexts/RowFilterContext';
import { deriveFieldValue, isSectionDisabled } from '@/utils';
import { SECTION_META, isPathInSection } from './configMeta';
import { ReadOnlySection } from './ReadOnlySection';
import { UnknownSettings } from './UnknownSettings';
import { ConfigSection } from './ConfigSection';
import { SECTION_META, isPathInSection } from './configMeta';
import { CodeField } from './fields/CodeField';
import { InfoBanner } from './InfoBanner';
import { ScopeGate } from './ScopeGate';
Expand Down Expand Up @@ -371,7 +371,7 @@ export function ConfigTabContent({
return (
<ReadOnlySection
reason={section.readOnlyReason}
detail={detailKey ? localize(detailKey) : undefined}
message={detailKey ? localize(detailKey) : undefined}
isEmpty={isEmpty && !filterPaths}
reveal={reveal}
>
Expand Down
Loading
Loading