mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
refactor(cli): fully remove React anti patterns, improve type safety and fix UX oversights in SettingsDialog.tsx (#18963)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
getScopeItems,
|
||||
getScopeMessageForSetting,
|
||||
} from './dialogScopeUtils.js';
|
||||
import { settingExistsInScope } from './settingsUtils.js';
|
||||
import { isInSettingsScope } from './settingsUtils.js';
|
||||
|
||||
vi.mock('../config/settings', () => ({
|
||||
SettingScope: {
|
||||
@@ -24,7 +24,7 @@ vi.mock('../config/settings', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./settingsUtils', () => ({
|
||||
settingExistsInScope: vi.fn(),
|
||||
isInSettingsScope: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('dialogScopeUtils', () => {
|
||||
@@ -53,7 +53,7 @@ describe('dialogScopeUtils', () => {
|
||||
});
|
||||
|
||||
it('should return empty string if not modified in other scopes', () => {
|
||||
vi.mocked(settingExistsInScope).mockReturnValue(false);
|
||||
vi.mocked(isInSettingsScope).mockReturnValue(false);
|
||||
const message = getScopeMessageForSetting(
|
||||
'key',
|
||||
SettingScope.User,
|
||||
@@ -63,7 +63,7 @@ describe('dialogScopeUtils', () => {
|
||||
});
|
||||
|
||||
it('should return message indicating modification in other scopes', () => {
|
||||
vi.mocked(settingExistsInScope).mockReturnValue(true);
|
||||
vi.mocked(isInSettingsScope).mockReturnValue(true);
|
||||
|
||||
const message = getScopeMessageForSetting(
|
||||
'key',
|
||||
@@ -88,7 +88,7 @@ describe('dialogScopeUtils', () => {
|
||||
return { settings: {} };
|
||||
});
|
||||
|
||||
vi.mocked(settingExistsInScope).mockImplementation(
|
||||
vi.mocked(isInSettingsScope).mockImplementation(
|
||||
(_key, settings: unknown) => {
|
||||
if (settings === workspaceSettings) return true;
|
||||
if (settings === systemSettings) return false;
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
LoadableSettingScope,
|
||||
LoadedSettings,
|
||||
} from '../config/settings.js';
|
||||
import type { LoadableSettingScope, Settings } from '../config/settings.js';
|
||||
import { isLoadableSettingScope, SettingScope } from '../config/settings.js';
|
||||
import { settingExistsInScope } from './settingsUtils.js';
|
||||
import { isInSettingsScope } from './settingsUtils.js';
|
||||
|
||||
/**
|
||||
* Shared scope labels for dialog components that need to display setting scopes
|
||||
@@ -43,7 +40,9 @@ export function getScopeItems(): Array<{
|
||||
export function getScopeMessageForSetting(
|
||||
settingKey: string,
|
||||
selectedScope: LoadableSettingScope,
|
||||
settings: LoadedSettings,
|
||||
settings: {
|
||||
forScope: (scope: LoadableSettingScope) => { settings: Settings };
|
||||
},
|
||||
): string {
|
||||
const otherScopes = Object.values(SettingScope)
|
||||
.filter(isLoadableSettingScope)
|
||||
@@ -51,7 +50,7 @@ export function getScopeMessageForSetting(
|
||||
|
||||
const modifiedInOtherScopes = otherScopes.filter((scope) => {
|
||||
const scopeSettings = settings.forScope(scope).settings;
|
||||
return settingExistsInScope(settingKey, scopeSettings);
|
||||
return isInSettingsScope(settingKey, scopeSettings);
|
||||
});
|
||||
|
||||
if (modifiedInOtherScopes.length === 0) {
|
||||
@@ -60,7 +59,7 @@ export function getScopeMessageForSetting(
|
||||
|
||||
const modifiedScopesStr = modifiedInOtherScopes.join(', ');
|
||||
const currentScopeSettings = settings.forScope(selectedScope).settings;
|
||||
const existsInCurrentScope = settingExistsInScope(
|
||||
const existsInCurrentScope = isInSettingsScope(
|
||||
settingKey,
|
||||
currentScopeSettings,
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
// Schema utilities
|
||||
getSettingsByCategory,
|
||||
@@ -22,18 +22,10 @@ import {
|
||||
getDialogSettingsByCategory,
|
||||
getDialogSettingsByType,
|
||||
getDialogSettingKeys,
|
||||
// Business logic utilities
|
||||
getSettingValue,
|
||||
isSettingModified,
|
||||
// Business logic utilities,
|
||||
TEST_ONLY,
|
||||
settingExistsInScope,
|
||||
setPendingSettingValue,
|
||||
hasRestartRequiredSettings,
|
||||
getRestartRequiredFromModified,
|
||||
isInSettingsScope,
|
||||
getDisplayValue,
|
||||
isDefaultValue,
|
||||
isValueInherited,
|
||||
getEffectiveDisplayValue,
|
||||
} from './settingsUtils.js';
|
||||
import {
|
||||
getSettingsSchema,
|
||||
@@ -255,41 +247,15 @@ describe('SettingsUtils', () => {
|
||||
describe('getEffectiveValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
const value = getEffectiveValue('ui.requiresRestart', settings);
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default value when not set anywhere', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({});
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
const value = getEffectiveValue('ui.requiresRestart', settings);
|
||||
expect(value).toBe(false); // default value
|
||||
});
|
||||
|
||||
@@ -297,27 +263,18 @@ describe('SettingsUtils', () => {
|
||||
const settings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: false } },
|
||||
});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
});
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
|
||||
it('should return undefined for invalid settings', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({});
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'invalidSetting',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
const value = getEffectiveValue('invalidSetting', settings);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -483,7 +440,9 @@ describe('SettingsUtils', () => {
|
||||
expect(dialogKeys.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle nested settings display correctly', () => {
|
||||
const nestedDialogKey = 'context.fileFiltering.respectGitIgnore';
|
||||
|
||||
function mockNestedDialogSchema() {
|
||||
vi.mocked(getSettingsSchema).mockReturnValue({
|
||||
context: {
|
||||
type: 'object',
|
||||
@@ -517,128 +476,27 @@ describe('SettingsUtils', () => {
|
||||
},
|
||||
},
|
||||
} as unknown as SettingsSchemaType);
|
||||
}
|
||||
|
||||
// Test the specific issue with fileFiltering.respectGitIgnore
|
||||
const key = 'context.fileFiltering.respectGitIgnore';
|
||||
const initialSettings = makeMockSettings({});
|
||||
const pendingSettings = makeMockSettings({});
|
||||
it('should include nested file filtering setting in dialog keys', () => {
|
||||
mockNestedDialogSchema();
|
||||
|
||||
// Set the nested setting to true
|
||||
const updatedPendingSettings = setPendingSettingValue(
|
||||
key,
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
// Check if the setting exists in pending settings
|
||||
const existsInPending = settingExistsInScope(
|
||||
key,
|
||||
updatedPendingSettings,
|
||||
);
|
||||
expect(existsInPending).toBe(true);
|
||||
|
||||
// Get the value from pending settings
|
||||
const valueFromPending = getSettingValue(
|
||||
key,
|
||||
updatedPendingSettings,
|
||||
{},
|
||||
);
|
||||
expect(valueFromPending).toBe(true);
|
||||
|
||||
// Test getDisplayValue should show the pending change
|
||||
const displayValue = getDisplayValue(
|
||||
key,
|
||||
initialSettings,
|
||||
{},
|
||||
new Set(),
|
||||
updatedPendingSettings,
|
||||
);
|
||||
expect(displayValue).toBe('true'); // Should show true (no * since value matches default)
|
||||
|
||||
// Test that modified settings also show the * indicator
|
||||
const modifiedSettings = new Set([key]);
|
||||
const displayValueWithModified = getDisplayValue(
|
||||
key,
|
||||
initialSettings,
|
||||
{},
|
||||
modifiedSettings,
|
||||
{},
|
||||
);
|
||||
expect(displayValueWithModified).toBe('true*'); // Should show true* because it's in modified settings and default is true
|
||||
const dialogKeys = getDialogSettingKeys();
|
||||
expect(dialogKeys).toContain(nestedDialogKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Business Logic Utilities', () => {
|
||||
describe('getSettingValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
|
||||
const value = getSettingValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
|
||||
const value = getSettingValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default value for invalid setting', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({});
|
||||
|
||||
const value = getSettingValue(
|
||||
'invalidSetting',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(value).toBe(false); // Default fallback
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSettingModified', () => {
|
||||
it('should return true when value differs from default', () => {
|
||||
expect(isSettingModified('ui.requiresRestart', true)).toBe(true);
|
||||
expect(
|
||||
isSettingModified('ui.accessibility.enableLoadingPhrases', false),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value matches default', () => {
|
||||
expect(isSettingModified('ui.requiresRestart', false)).toBe(false);
|
||||
expect(
|
||||
isSettingModified('ui.accessibility.enableLoadingPhrases', true),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settingExistsInScope', () => {
|
||||
describe('isInSettingsScope', () => {
|
||||
it('should return true for top-level settings that exist', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
expect(settingExistsInScope('ui.requiresRestart', settings)).toBe(true);
|
||||
expect(isInSettingsScope('ui.requiresRestart', settings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for top-level settings that do not exist', () => {
|
||||
const settings = makeMockSettings({});
|
||||
expect(settingExistsInScope('ui.requiresRestart', settings)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isInSettingsScope('ui.requiresRestart', settings)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for nested settings that exist', () => {
|
||||
@@ -646,121 +504,25 @@ describe('SettingsUtils', () => {
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
});
|
||||
expect(
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for nested settings that do not exist', () => {
|
||||
const settings = makeMockSettings({});
|
||||
expect(
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when parent exists but child does not', () => {
|
||||
const settings = makeMockSettings({ ui: { accessibility: {} } });
|
||||
expect(
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPendingSettingValue', () => {
|
||||
it('should set top-level setting value', () => {
|
||||
const pendingSettings = makeMockSettings({});
|
||||
const result = setPendingSettingValue(
|
||||
'ui.hideWindowTitle',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.ui?.hideWindowTitle).toBe(true);
|
||||
});
|
||||
|
||||
it('should set nested setting value', () => {
|
||||
const pendingSettings = makeMockSettings({});
|
||||
const result = setPendingSettingValue(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.ui?.accessibility?.enableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve existing nested settings', () => {
|
||||
const pendingSettings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: false } },
|
||||
});
|
||||
const result = setPendingSettingValue(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.ui?.accessibility?.enableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should not mutate original settings', () => {
|
||||
const pendingSettings = makeMockSettings({});
|
||||
setPendingSettingValue('ui.requiresRestart', true, pendingSettings);
|
||||
|
||||
expect(pendingSettings).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRestartRequiredSettings', () => {
|
||||
it('should return true when modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'advanced.autoConfigureMemory',
|
||||
'ui.requiresRestart',
|
||||
]);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>(['test']);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty set', () => {
|
||||
const modifiedSettings = new Set<string>();
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRestartRequiredFromModified', () => {
|
||||
it('should return only settings that require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'ui.requiresRestart',
|
||||
'test',
|
||||
]);
|
||||
const result = getRestartRequiredFromModified(modifiedSettings);
|
||||
|
||||
expect(result).toContain('ui.requiresRestart');
|
||||
expect(result).not.toContain('test');
|
||||
});
|
||||
|
||||
it('should return empty array when no settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'requiresRestart',
|
||||
'hideTips',
|
||||
]);
|
||||
const result = getRestartRequiredFromModified(modifiedSettings);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDisplayValue', () => {
|
||||
describe('enum behavior', () => {
|
||||
enum StringEnum {
|
||||
@@ -830,14 +592,8 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { theme: NumberEnum.THREE },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.theme',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
const result = getDisplayValue('ui.theme', settings, mergedSettings);
|
||||
|
||||
expect(result).toBe('Three*');
|
||||
});
|
||||
@@ -867,13 +623,11 @@ describe('SettingsUtils', () => {
|
||||
},
|
||||
},
|
||||
} as unknown as SettingsSchemaType);
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.theme',
|
||||
makeMockSettings({}),
|
||||
makeMockSettings({}),
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('Three');
|
||||
});
|
||||
@@ -886,14 +640,8 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { theme: StringEnum.BAR },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.theme',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
const result = getDisplayValue('ui.theme', settings, mergedSettings);
|
||||
expect(result).toBe('Bar*');
|
||||
});
|
||||
|
||||
@@ -907,14 +655,8 @@ describe('SettingsUtils', () => {
|
||||
} as unknown as SettingsSchemaType);
|
||||
const settings = makeMockSettings({ ui: { theme: 'xyz' } });
|
||||
const mergedSettings = makeMockSettings({ ui: { theme: 'xyz' } });
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.theme',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
const result = getDisplayValue('ui.theme', settings, mergedSettings);
|
||||
expect(result).toBe('xyz*');
|
||||
});
|
||||
|
||||
@@ -926,242 +668,71 @@ describe('SettingsUtils', () => {
|
||||
},
|
||||
},
|
||||
} as unknown as SettingsSchemaType);
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.theme',
|
||||
makeMockSettings({}),
|
||||
makeMockSettings({}),
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('Bar');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show value without * when setting matches default', () => {
|
||||
const settings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
}); // false matches default, so no *
|
||||
it('should show value with * when setting exists in scope', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('false*');
|
||||
expect(result).toBe('true*');
|
||||
});
|
||||
|
||||
it('should show default value when setting is not in scope', () => {
|
||||
it('should not show * when key is not in scope', () => {
|
||||
const settings = makeMockSettings({}); // no setting in scope
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('false'); // shows default value
|
||||
});
|
||||
|
||||
it('should show value with * when changed from default', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } }); // true is different from default (false)
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('true*');
|
||||
});
|
||||
|
||||
it('should show default value without * when setting does not exist in scope', () => {
|
||||
const settings = makeMockSettings({}); // setting doesn't exist in scope, show default
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
);
|
||||
expect(result).toBe('false'); // default value (false) without *
|
||||
});
|
||||
|
||||
it('should show value with * when user changes from default', () => {
|
||||
const settings = makeMockSettings({}); // setting doesn't exist in scope originally
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
const modifiedSettings = new Set<string>(['ui.requiresRestart']);
|
||||
const pendingSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
}); // user changed to true
|
||||
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
pendingSettings,
|
||||
);
|
||||
expect(result).toBe('true*'); // changed from default (false) to true
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDefaultValue', () => {
|
||||
it('should return true when setting does not exist in scope', () => {
|
||||
const settings = makeMockSettings({}); // setting doesn't exist
|
||||
|
||||
const result = isDefaultValue('ui.requiresRestart', settings);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when setting exists in scope', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } }); // setting exists
|
||||
|
||||
const result = isDefaultValue('ui.requiresRestart', settings);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when nested setting does not exist in scope', () => {
|
||||
const settings = makeMockSettings({}); // nested setting doesn't exist
|
||||
|
||||
const result = isDefaultValue(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when nested setting exists in scope', () => {
|
||||
it('should show value with * when setting exists in scope, even when it matches default', () => {
|
||||
const settings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
}); // nested setting exists
|
||||
|
||||
const result = isDefaultValue(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValueInherited', () => {
|
||||
it('should return false for top-level settings that exist in scope', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
|
||||
const result = isValueInherited(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for top-level settings that do not exist in scope', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
|
||||
const result = isValueInherited(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for nested settings that exist in scope', () => {
|
||||
const settings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
});
|
||||
|
||||
const result = isValueInherited(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for nested settings that do not exist in scope', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { accessibility: { enableLoadingPhrases: true } },
|
||||
});
|
||||
|
||||
const result = isValueInherited(
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectiveDisplayValue', () => {
|
||||
it('should return value from settings when available', () => {
|
||||
const settings = makeMockSettings({ ui: { requiresRestart: true } });
|
||||
ui: { requiresRestart: false },
|
||||
}); // false matches default, but key is explicitly set in scope
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: false },
|
||||
});
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(result).toBe('false*');
|
||||
});
|
||||
|
||||
it('should return value from merged settings when not in scope', () => {
|
||||
const settings = makeMockSettings({});
|
||||
it('should show schema default (not inherited merged value) when key is not in scope', () => {
|
||||
const settings = makeMockSettings({}); // no setting in current scope
|
||||
const mergedSettings = makeMockSettings({
|
||||
ui: { requiresRestart: true },
|
||||
});
|
||||
}); // inherited merged value differs from schema default (false)
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
const result = getDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default value for undefined values', () => {
|
||||
const settings = makeMockSettings({});
|
||||
const mergedSettings = makeMockSettings({});
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'ui.requiresRestart',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
expect(result).toBe(false); // Default value
|
||||
expect(result).toBe('false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
Settings,
|
||||
LoadedSettings,
|
||||
LoadableSettingScope,
|
||||
} from '../config/settings.js';
|
||||
import type { Settings } from '../config/settings.js';
|
||||
import type {
|
||||
SettingDefinition,
|
||||
SettingsSchema,
|
||||
@@ -52,9 +48,6 @@ function clearFlattenedSchema() {
|
||||
_FLATTENED_SCHEMA = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings grouped by category
|
||||
*/
|
||||
export function getSettingsByCategory(): Record<
|
||||
string,
|
||||
Array<SettingDefinition & { key: string }>
|
||||
@@ -75,25 +68,16 @@ export function getSettingsByCategory(): Record<
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting definition by key
|
||||
*/
|
||||
export function getSettingDefinition(
|
||||
key: string,
|
||||
): (SettingDefinition & { key: string }) | undefined {
|
||||
return getFlattenedSchema()[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting requires restart
|
||||
*/
|
||||
export function requiresRestart(key: string): boolean {
|
||||
return getFlattenedSchema()[key]?.requiresRestart ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value for a setting
|
||||
*/
|
||||
export function getDefaultValue(key: string): SettingsValue {
|
||||
return getFlattenedSchema()[key]?.default;
|
||||
}
|
||||
@@ -120,9 +104,6 @@ export function getEffectiveDefaultValue(
|
||||
return getDefaultValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all setting keys that require restart
|
||||
*/
|
||||
export function getRestartRequiredSettings(): string[] {
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.requiresRestart)
|
||||
@@ -130,35 +111,55 @@ export function getRestartRequiredSettings(): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively gets a value from a nested object using a key path array.
|
||||
* Get restart-required setting keys that are also visible in the dialog.
|
||||
* Non-dialog restart keys (e.g. parent container objects like mcpServers, tools)
|
||||
* are excluded because users cannot change them through the dialog.
|
||||
*/
|
||||
export function getNestedValue(
|
||||
obj: Record<string, unknown>,
|
||||
path: string[],
|
||||
): unknown {
|
||||
const [first, ...rest] = path;
|
||||
if (!first || !(first in obj)) {
|
||||
return undefined;
|
||||
}
|
||||
const value = obj[first];
|
||||
if (rest.length === 0) {
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === 'object' && value !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return getNestedValue(value as Record<string, unknown>, rest);
|
||||
}
|
||||
return undefined;
|
||||
export function getDialogRestartRequiredSettings(): string[] {
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter(
|
||||
(definition) =>
|
||||
definition.requiresRestart && definition.showInDialog !== false,
|
||||
)
|
||||
.map((definition) => definition.key);
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function isSettingsValue(value: unknown): value is SettingsValue {
|
||||
if (value === undefined) return true;
|
||||
if (value === null) return false;
|
||||
const type = typeof value;
|
||||
return (
|
||||
type === 'string' ||
|
||||
type === 'number' ||
|
||||
type === 'boolean' ||
|
||||
type === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective value for a setting, considering inheritance from higher scopes
|
||||
* Always returns a value (never undefined) - falls back to default if not set anywhere
|
||||
* Gets a value from a nested object using a key path array iteratively.
|
||||
*/
|
||||
export function getNestedValue(obj: unknown, path: string[]): unknown {
|
||||
let current = obj;
|
||||
for (const key of path) {
|
||||
if (!isRecord(current) || !(key in current)) {
|
||||
return undefined;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective value for a setting falling back to the default value
|
||||
*/
|
||||
export function getEffectiveValue(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
mergedSettings: Settings,
|
||||
): SettingsValue {
|
||||
const definition = getSettingDefinition(key);
|
||||
if (!definition) {
|
||||
@@ -168,33 +169,19 @@ export function getEffectiveValue(
|
||||
const path = key.split('.');
|
||||
|
||||
// Check the current scope's settings first
|
||||
let value = getNestedValue(settings as Record<string, unknown>, path);
|
||||
if (value !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return value as SettingsValue;
|
||||
}
|
||||
|
||||
// Check the merged settings for an inherited value
|
||||
value = getNestedValue(mergedSettings as Record<string, unknown>, path);
|
||||
if (value !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return value as SettingsValue;
|
||||
const value = getNestedValue(settings, path);
|
||||
if (value !== undefined && isSettingsValue(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Return default value if no value is set anywhere
|
||||
return definition.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all setting keys from the schema
|
||||
*/
|
||||
export function getAllSettingKeys(): string[] {
|
||||
return Object.keys(getFlattenedSchema());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by type
|
||||
*/
|
||||
export function getSettingsByType(
|
||||
type: SettingsType,
|
||||
): Array<SettingDefinition & { key: string }> {
|
||||
@@ -203,9 +190,6 @@ export function getSettingsByType(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings that require restart
|
||||
*/
|
||||
export function getSettingsRequiringRestart(): Array<
|
||||
SettingDefinition & {
|
||||
key: string;
|
||||
@@ -223,22 +207,22 @@ export function isValidSettingKey(key: string): boolean {
|
||||
return key in getFlattenedSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category for a setting
|
||||
*/
|
||||
export function getSettingCategory(key: string): string | undefined {
|
||||
return getFlattenedSchema()[key]?.category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting should be shown in the settings dialog
|
||||
*/
|
||||
export function shouldShowInDialog(key: string): boolean {
|
||||
return getFlattenedSchema()[key]?.showInDialog ?? true; // Default to true for backward compatibility
|
||||
}
|
||||
|
||||
export function getDialogSettingKeys(): string[] {
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.showInDialog !== false)
|
||||
.map((definition) => definition.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings that should be shown in the dialog, grouped by category
|
||||
* Get all settings that should be shown in the dialog, grouped by category like "Advanced", "General", etc.
|
||||
*/
|
||||
export function getDialogSettingsByCategory(): Record<
|
||||
string,
|
||||
@@ -262,9 +246,6 @@ export function getDialogSettingsByCategory(): Record<
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by type that should be shown in the dialog
|
||||
*/
|
||||
export function getDialogSettingsByType(
|
||||
type: SettingsType,
|
||||
): Array<SettingDefinition & { key: string }> {
|
||||
@@ -274,197 +255,30 @@ export function getDialogSettingsByType(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all setting keys that should be shown in the dialog
|
||||
*/
|
||||
export function getDialogSettingKeys(): string[] {
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.showInDialog !== false)
|
||||
.map((definition) => definition.key);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BUSINESS LOGIC UTILITIES (Higher-level utilities for setting operations)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the current value for a setting in a specific scope
|
||||
* Always returns a value (never undefined) - falls back to default if not set anywhere
|
||||
*/
|
||||
export function getSettingValue(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
mergedSettings: Settings,
|
||||
): boolean {
|
||||
const definition = getSettingDefinition(key);
|
||||
if (!definition) {
|
||||
return false; // Default fallback for invalid settings
|
||||
}
|
||||
|
||||
const value = getEffectiveValue(key, settings, mergedSettings);
|
||||
// Ensure we return a boolean value, converting from the more general type
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return false; // Final fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting value is modified from its default
|
||||
*/
|
||||
export function isSettingModified(key: string, value: boolean): boolean {
|
||||
const defaultValue = getDefaultValue(key);
|
||||
// Handle type comparison properly
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return value !== defaultValue;
|
||||
}
|
||||
// If default is not a boolean, consider it modified if value is true
|
||||
return value === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting exists in the original settings file for a scope
|
||||
*/
|
||||
export function settingExistsInScope(
|
||||
export function isInSettingsScope(
|
||||
key: string,
|
||||
scopeSettings: Settings,
|
||||
): boolean {
|
||||
const path = key.split('.');
|
||||
const value = getNestedValue(scopeSettings as Record<string, unknown>, path);
|
||||
const value = getNestedValue(scopeSettings, path);
|
||||
return value !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sets a value in a nested object using a key path array.
|
||||
*/
|
||||
function setNestedValue(
|
||||
obj: Record<string, unknown>,
|
||||
path: string[],
|
||||
value: unknown,
|
||||
): Record<string, unknown> {
|
||||
const [first, ...rest] = path;
|
||||
if (!first) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (rest.length === 0) {
|
||||
obj[first] = value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (!obj[first] || typeof obj[first] !== 'object') {
|
||||
obj[first] = {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
setNestedValue(obj[first] as Record<string, unknown>, rest, value);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value in the pending settings
|
||||
*/
|
||||
export function setPendingSettingValue(
|
||||
key: string,
|
||||
value: boolean,
|
||||
pendingSettings: Settings,
|
||||
): Settings {
|
||||
const path = key.split('.');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const newSettings = JSON.parse(JSON.stringify(pendingSettings));
|
||||
setNestedValue(newSettings, path, value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic setter: Set a setting value (boolean, number, string, etc.) in the pending settings
|
||||
*/
|
||||
export function setPendingSettingValueAny(
|
||||
key: string,
|
||||
value: SettingsValue,
|
||||
pendingSettings: Settings,
|
||||
): Settings {
|
||||
const path = key.split('.');
|
||||
const newSettings = structuredClone(pendingSettings);
|
||||
setNestedValue(newSettings, path, value);
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any modified settings require a restart
|
||||
*/
|
||||
export function hasRestartRequiredSettings(
|
||||
modifiedSettings: Set<string>,
|
||||
): boolean {
|
||||
return Array.from(modifiedSettings).some((key) => requiresRestart(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the restart required settings from a set of modified settings
|
||||
*/
|
||||
export function getRestartRequiredFromModified(
|
||||
modifiedSettings: Set<string>,
|
||||
): string[] {
|
||||
return Array.from(modifiedSettings).filter((key) => requiresRestart(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save modified settings to the appropriate scope
|
||||
*/
|
||||
export function saveModifiedSettings(
|
||||
modifiedSettings: Set<string>,
|
||||
pendingSettings: Settings,
|
||||
loadedSettings: LoadedSettings,
|
||||
scope: LoadableSettingScope,
|
||||
): void {
|
||||
modifiedSettings.forEach((settingKey) => {
|
||||
const path = settingKey.split('.');
|
||||
const value = getNestedValue(
|
||||
pendingSettings as Record<string, unknown>,
|
||||
path,
|
||||
);
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existsInOriginalFile = settingExistsInScope(
|
||||
settingKey,
|
||||
loadedSettings.forScope(scope).settings,
|
||||
);
|
||||
|
||||
const isDefaultValue = value === getDefaultValue(settingKey);
|
||||
|
||||
if (existsInOriginalFile || !isDefaultValue) {
|
||||
loadedSettings.setValue(scope, settingKey, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display value for a setting, showing current scope value with default change indicator
|
||||
* Appends a star (*) to settings that exist in the scope
|
||||
*/
|
||||
export function getDisplayValue(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
scopeSettings: Settings,
|
||||
_mergedSettings: Settings,
|
||||
modifiedSettings: Set<string>,
|
||||
pendingSettings?: Settings,
|
||||
): string {
|
||||
// Prioritize pending changes if user has modified this setting
|
||||
const definition = getSettingDefinition(key);
|
||||
const existsInScope = isInSettingsScope(key, scopeSettings);
|
||||
|
||||
let value: SettingsValue;
|
||||
if (pendingSettings && settingExistsInScope(key, pendingSettings)) {
|
||||
// Show the value from the pending (unsaved) edits when it exists
|
||||
value = getEffectiveValue(key, pendingSettings, {});
|
||||
} else if (settingExistsInScope(key, settings)) {
|
||||
// Show the value defined at the current scope if present
|
||||
value = getEffectiveValue(key, settings, {});
|
||||
if (existsInScope) {
|
||||
value = getEffectiveValue(key, scopeSettings);
|
||||
} else {
|
||||
// Fall back to the schema default when the key is unset in this scope
|
||||
value = getDefaultValue(key);
|
||||
}
|
||||
|
||||
@@ -475,50 +289,108 @@ export function getDisplayValue(
|
||||
valueString = option?.label ?? `${value}`;
|
||||
}
|
||||
|
||||
// Check if value is different from default OR if it's in modified settings OR if there are pending changes
|
||||
const defaultValue = getDefaultValue(key);
|
||||
const isChangedFromDefault = value !== defaultValue;
|
||||
const isInModifiedSettings = modifiedSettings.has(key);
|
||||
|
||||
// Mark as modified if setting exists in current scope OR is in modified settings
|
||||
if (settingExistsInScope(key, settings) || isInModifiedSettings) {
|
||||
return `${valueString}*`; // * indicates setting is set in current scope
|
||||
}
|
||||
if (isChangedFromDefault || isInModifiedSettings) {
|
||||
return `${valueString}*`; // * indicates changed from default value
|
||||
if (existsInScope) {
|
||||
return `${valueString}*`;
|
||||
}
|
||||
|
||||
return valueString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting doesn't exist in current scope (should be greyed out)
|
||||
*/
|
||||
export function isDefaultValue(key: string, settings: Settings): boolean {
|
||||
return !settingExistsInScope(key, settings);
|
||||
/**Utilities for parsing Settings that can be inline edited by the user typing out values */
|
||||
function tryParseJsonStringArray(input: string): string[] | null {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(input);
|
||||
if (
|
||||
Array.isArray(parsed) &&
|
||||
parsed.every((item): item is string => typeof item === 'string')
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting value is inherited (not set at current scope)
|
||||
*/
|
||||
export function isValueInherited(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
_mergedSettings: Settings,
|
||||
): boolean {
|
||||
return !settingExistsInScope(key, settings);
|
||||
function tryParseJsonObject(input: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(input);
|
||||
if (isRecord(parsed) && !Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective value for display, considering inheritance
|
||||
* Always returns a boolean value (never undefined)
|
||||
*/
|
||||
export function getEffectiveDisplayValue(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
mergedSettings: Settings,
|
||||
): boolean {
|
||||
return getSettingValue(key, settings, mergedSettings);
|
||||
function parseStringArrayValue(input: string): string[] {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') return [];
|
||||
|
||||
return (
|
||||
tryParseJsonStringArray(trimmed) ??
|
||||
input
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
function parseObjectValue(input: string): Record<string, unknown> | null {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tryParseJsonObject(trimmed);
|
||||
}
|
||||
|
||||
export function parseEditedValue(
|
||||
type: SettingsType,
|
||||
newValue: string,
|
||||
): SettingsValue | null {
|
||||
if (type === 'number') {
|
||||
if (newValue.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numParsed = Number(newValue.trim());
|
||||
if (Number.isNaN(numParsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return numParsed;
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
return parseStringArrayValue(newValue);
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
return parseObjectValue(newValue);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
export function getEditValue(
|
||||
type: SettingsType,
|
||||
rawValue: SettingsValue,
|
||||
): string | undefined {
|
||||
if (rawValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type === 'array' && Array.isArray(rawValue)) {
|
||||
return rawValue.join(', ');
|
||||
}
|
||||
|
||||
if (type === 'object' && rawValue !== null && typeof rawValue === 'object') {
|
||||
return JSON.stringify(rawValue);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const TEST_ONLY = { clearFlattenedSchema };
|
||||
|
||||
Reference in New Issue
Block a user