mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
fix(Themes): Local label inconsistent behaviors (#35663)
This commit is contained in:
@@ -37,6 +37,7 @@ const STORAGE_KEYS = {
|
||||
THEME_MODE: 'superset-theme-mode',
|
||||
CRUD_THEME_ID: 'superset-crud-theme-id',
|
||||
DEV_THEME_OVERRIDE: 'superset-dev-theme-override',
|
||||
APPLIED_THEME_ID: 'superset-applied-theme-id',
|
||||
} as const;
|
||||
|
||||
const MEDIA_QUERY_DARK_SCHEME = '(prefers-color-scheme: dark)';
|
||||
@@ -303,7 +304,12 @@ export class ThemeController {
|
||||
public setThemeMode(mode: ThemeMode): void {
|
||||
this.validateModeUpdatePermission(mode);
|
||||
|
||||
if (this.currentMode === mode) return;
|
||||
if (
|
||||
this.currentMode === mode &&
|
||||
!this.devThemeOverride &&
|
||||
!this.crudThemeId
|
||||
)
|
||||
return;
|
||||
|
||||
// Clear any local overrides when explicitly selecting a theme mode
|
||||
// This ensures the selected mode takes effect and provides clear UX
|
||||
@@ -367,8 +373,12 @@ export class ThemeController {
|
||||
* Sets a temporary theme override for development purposes.
|
||||
* This does not persist the theme but allows live preview.
|
||||
* @param theme - The theme configuration to apply temporarily
|
||||
* @param themeId - Optional theme ID to track which theme was applied (for UI display)
|
||||
*/
|
||||
public setTemporaryTheme(theme: AnyThemeConfig): void {
|
||||
public setTemporaryTheme(
|
||||
theme: AnyThemeConfig,
|
||||
themeId?: number | null,
|
||||
): void {
|
||||
this.validateThemeUpdatePermission();
|
||||
|
||||
this.devThemeOverride = theme;
|
||||
@@ -377,6 +387,11 @@ export class ThemeController {
|
||||
JSON.stringify(theme),
|
||||
);
|
||||
|
||||
// Store the theme ID if provided
|
||||
if (themeId !== undefined) {
|
||||
this.setAppliedThemeId(themeId);
|
||||
}
|
||||
|
||||
const mergedTheme = this.getThemeForMode(this.currentMode);
|
||||
if (mergedTheme) this.updateTheme(mergedTheme);
|
||||
}
|
||||
@@ -392,6 +407,7 @@ export class ThemeController {
|
||||
|
||||
this.storage.removeItem(STORAGE_KEYS.DEV_THEME_OVERRIDE);
|
||||
this.storage.removeItem(STORAGE_KEYS.CRUD_THEME_ID);
|
||||
this.storage.removeItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
|
||||
// Clear dashboard themes cache
|
||||
this.dashboardThemes.clear();
|
||||
@@ -413,6 +429,34 @@ export class ThemeController {
|
||||
return this.devThemeOverride !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the applied theme ID (for UI display purposes).
|
||||
*/
|
||||
public getAppliedThemeId(): number | null {
|
||||
try {
|
||||
const storedId = this.storage.getItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
return storedId ? parseInt(storedId, 10) : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get applied theme ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the applied theme ID (for UI display purposes).
|
||||
*/
|
||||
public setAppliedThemeId(themeId: number | null): void {
|
||||
try {
|
||||
if (themeId !== null) {
|
||||
this.storage.setItem(STORAGE_KEYS.APPLIED_THEME_ID, themeId.toString());
|
||||
} else {
|
||||
this.storage.removeItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to set applied theme ID:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if OS preference detection is allowed.
|
||||
* Allowed when dark theme is available (including base dark theme)
|
||||
|
||||
@@ -117,6 +117,11 @@ export function SupersetThemeProvider({
|
||||
[themeController],
|
||||
);
|
||||
|
||||
const getAppliedThemeId = useCallback(
|
||||
() => themeController.getAppliedThemeId(),
|
||||
[themeController],
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
theme: currentTheme,
|
||||
@@ -132,6 +137,7 @@ export function SupersetThemeProvider({
|
||||
canSetTheme,
|
||||
canDetectOSPreference,
|
||||
createDashboardThemeProvider,
|
||||
getAppliedThemeId,
|
||||
}),
|
||||
[
|
||||
currentTheme,
|
||||
@@ -147,6 +153,7 @@ export function SupersetThemeProvider({
|
||||
canSetTheme,
|
||||
canDetectOSPreference,
|
||||
createDashboardThemeProvider,
|
||||
getAppliedThemeId,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1137,4 +1137,236 @@ describe('ThemeController', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('setThemeMode clears dev override and crud theme from storage', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Simulate having overrides after initialization using Reflect to access private properties
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
Reflect.set(controller, 'crudThemeId', '123');
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Change theme mode - should clear the overrides
|
||||
controller.setThemeMode(ThemeMode.DARK);
|
||||
|
||||
// Verify both storage keys were removed
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
});
|
||||
|
||||
test('setThemeMode can be called with same mode when overrides exist', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Simulate having dev override after initialization using Reflect
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
|
||||
// Call setThemeMode with DEFAULT mode - should clear override
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
// Verify override was removed even though mode is the same
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
|
||||
// Theme should still be updated to clear the override
|
||||
expect(mockSetConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setThemeMode with no override and same mode does not trigger update', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Set mode to DEFAULT
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Call again with same mode and no override - should skip
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
// Should not trigger any updates
|
||||
expect(mockSetConfig).not.toHaveBeenCalled();
|
||||
expect(mockLocalStorage.removeItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('hasDevOverride returns true when dev override is set', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Simulate dev override after initialization using Reflect
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
|
||||
expect(controller.hasDevOverride()).toBe(true);
|
||||
});
|
||||
|
||||
test('hasDevOverride returns false when no dev override in storage', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.hasDevOverride()).toBe(false);
|
||||
});
|
||||
|
||||
test('clearLocalOverrides removes dev override, crud theme, and applied theme ID', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Clear overrides
|
||||
controller.clearLocalOverrides();
|
||||
|
||||
// Verify all storage keys are removed
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
);
|
||||
|
||||
// Should reset to default theme
|
||||
expect(mockSetConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('getAppliedThemeId returns stored theme ID', () => {
|
||||
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
||||
if (key === 'superset-applied-theme-id') {
|
||||
return '42';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.getAppliedThemeId()).toBe(42);
|
||||
});
|
||||
|
||||
test('getAppliedThemeId returns null when no theme ID is stored', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.getAppliedThemeId()).toBeNull();
|
||||
});
|
||||
|
||||
test('setAppliedThemeId stores theme ID in storage', () => {
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
controller.setAppliedThemeId(123);
|
||||
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
'123',
|
||||
);
|
||||
});
|
||||
|
||||
test('setAppliedThemeId removes theme ID when null is passed', () => {
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
controller.setAppliedThemeId(null);
|
||||
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user