Skip to content

Commit 67899ab

Browse files
fix(theming): address PR comments + some fixes and improvements
1 parent 1559605 commit 67899ab

File tree

10 files changed

+661
-277
lines changed

10 files changed

+661
-277
lines changed

superset-frontend/packages/superset-ui-core/src/theme/Theme.test.tsx

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,8 @@ describe('Theme', () => {
427427
});
428428
});
429429

430-
describe('BASE_THEME integration tests', () => {
431-
it('merges BASE_THEME tokens with empty user theme', () => {
430+
describe('base theme integration tests', () => {
431+
it('merges base theme tokens with empty user theme', () => {
432432
const baseTheme: AnyThemeConfig = {
433433
token: {
434434
colorPrimary: '#2893B3',
@@ -452,7 +452,7 @@ describe('Theme', () => {
452452
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
453453
});
454454

455-
it('allows user theme to override specific BASE_THEME tokens', () => {
455+
it('allows user theme to override specific base theme tokens', () => {
456456
const baseTheme: AnyThemeConfig = {
457457
token: {
458458
colorPrimary: '#2893B3',
@@ -481,7 +481,7 @@ describe('Theme', () => {
481481
expect(theme.theme.borderRadius).toBe(4);
482482
});
483483

484-
it('handles BASE_THEME_DARK with dark algorithm correctly', () => {
484+
it('handles base theme with dark algorithm correctly', () => {
485485
const baseTheme: AnyThemeConfig = {
486486
token: {
487487
colorPrimary: '#2893B3',
@@ -508,8 +508,8 @@ describe('Theme', () => {
508508
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
509509
});
510510

511-
it('works with real-world Superset BASE_THEME configuration', () => {
512-
// Simulate actual Superset BASE_THEME
511+
it('works with real-world Superset base theme configuration', () => {
512+
// Simulate actual Superset base theme (THEME_DEFAULT/THEME_DARK from config)
513513
const supersetBaseTheme: AnyThemeConfig = {
514514
token: {
515515
colorPrimary: '#2893B3',
@@ -548,7 +548,7 @@ describe('Theme', () => {
548548
expect(darkSerialized.algorithm).toBe(ThemeAlgorithm.DARK);
549549
});
550550

551-
it('handles component overrides in BASE_THEME', () => {
551+
it('handles component overrides in base theme', () => {
552552
const baseTheme: AnyThemeConfig = {
553553
token: {
554554
colorPrimary: '#2893B3',
@@ -704,5 +704,85 @@ describe('Theme', () => {
704704
// fontFamily reverts to Ant Design default since base theme is not reapplied
705705
expect(theme.theme.fontFamily).not.toBe('Inter');
706706
});
707+
708+
it('minimal theme preserves ALL base theme tokens except overridden ones', () => {
709+
// Simulate a comprehensive base theme with many tokens
710+
const baseTheme: AnyThemeConfig = {
711+
token: {
712+
colorPrimary: '#2893B3',
713+
colorError: '#e04355',
714+
colorWarning: '#fcc700',
715+
colorSuccess: '#5ac189',
716+
colorInfo: '#66bcfe',
717+
fontFamily: 'Inter, Helvetica',
718+
fontSize: 14,
719+
borderRadius: 4,
720+
lineWidth: 1,
721+
controlHeight: 32,
722+
// Custom Superset tokens
723+
brandLogoAlt: 'CustomLogo',
724+
menuHoverBackgroundColor: '#eeeeee',
725+
} as Record<string, any>,
726+
algorithm: antdThemeImport.defaultAlgorithm,
727+
};
728+
729+
// Minimal theme that only overrides primary color and algorithm
730+
const minimalTheme: AnyThemeConfig = {
731+
token: {
732+
colorPrimary: '#ff05dd', // Only override this
733+
},
734+
algorithm: antdThemeImport.darkAlgorithm, // Change to dark
735+
};
736+
737+
const theme = Theme.fromConfig(minimalTheme, baseTheme);
738+
739+
// User's override should apply
740+
expect(theme.theme.colorPrimary).toBe('#ff05dd');
741+
742+
// ALL base theme tokens should be preserved
743+
expect(theme.theme.colorError).toBe('#e04355');
744+
expect(theme.theme.colorWarning).toBe('#fcc700');
745+
expect(theme.theme.colorSuccess).toBe('#5ac189');
746+
expect(theme.theme.colorInfo).toBe('#66bcfe');
747+
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
748+
expect(theme.theme.fontSize).toBe(14);
749+
expect(theme.theme.borderRadius).toBe(4);
750+
expect(theme.theme.lineWidth).toBe(1);
751+
expect(theme.theme.controlHeight).toBe(32);
752+
753+
// Custom tokens should also be preserved
754+
expect((theme.theme as any).brandLogoAlt).toBe('CustomLogo');
755+
expect((theme.theme as any).menuHoverBackgroundColor).toBe('#eeeeee');
756+
757+
// Algorithm should be updated
758+
const serialized = theme.toSerializedConfig();
759+
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
760+
});
761+
762+
it('arrays in themes are replaced entirely, not merged by index', () => {
763+
const baseTheme: AnyThemeConfig = {
764+
token: {
765+
colorPrimary: '#2893B3',
766+
},
767+
algorithm: [
768+
antdThemeImport.compactAlgorithm,
769+
antdThemeImport.defaultAlgorithm,
770+
],
771+
};
772+
773+
const userTheme: AnyThemeConfig = {
774+
algorithm: [antdThemeImport.darkAlgorithm], // Replace with single item array
775+
};
776+
777+
const theme = Theme.fromConfig(userTheme, baseTheme);
778+
const serialized = theme.toSerializedConfig();
779+
780+
// User's array should completely replace base array
781+
expect(Array.isArray(serialized.algorithm)).toBe(true);
782+
expect(serialized.algorithm).toHaveLength(1);
783+
expect(serialized.algorithm).toContain(ThemeAlgorithm.DARK);
784+
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.COMPACT);
785+
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DEFAULT);
786+
});
707787
});
708788
});

superset-frontend/packages/superset-ui-core/src/theme/Theme.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
CacheProvider as EmotionCacheProvider,
2626
} from '@emotion/react';
2727
import createCache from '@emotion/cache';
28-
import { noop } from 'lodash';
28+
import { noop, mergeWith } from 'lodash';
2929
import { GlobalStyles } from './GlobalStyles';
3030
import {
3131
AntdThemeConfig,
@@ -63,33 +63,9 @@ export class Theme {
6363
let mergedConfig: AnyThemeConfig | undefined = config;
6464

6565
if (baseTheme && config) {
66-
mergedConfig = {
67-
...baseTheme,
68-
...config,
69-
} as AnyThemeConfig;
70-
71-
if (baseTheme.token || config.token) {
72-
(mergedConfig as any).token = {
73-
...(baseTheme.token || {}),
74-
...(config.token || {}),
75-
};
76-
}
77-
78-
if (baseTheme.components || config.components) {
79-
const mergedComponents: any = { ...(baseTheme.components || {}) };
80-
81-
if (config.components) {
82-
const configComponents = config.components as any;
83-
Object.keys(configComponents).forEach(componentName => {
84-
mergedComponents[componentName] = {
85-
...(mergedComponents[componentName] || {}),
86-
...configComponents[componentName],
87-
};
88-
});
89-
}
90-
91-
(mergedConfig as any).components = mergedComponents;
92-
}
66+
mergedConfig = mergeWith({}, baseTheme, config, (objValue, srcValue) =>
67+
Array.isArray(srcValue) ? srcValue : undefined,
68+
);
9369
} else if (baseTheme && !config) {
9470
mergedConfig = baseTheme;
9571
}

superset-frontend/packages/superset-ui-core/src/theme/utils/themeUtils.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818
*/
1919
import tinycolor from 'tinycolor2';
2020
import { useTheme as useEmotionTheme } from '@emotion/react';
21-
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
21+
import { theme as antdTheme } from 'antd';
22+
import type {
23+
SupersetTheme,
24+
FontSizeKey,
25+
ColorVariants,
26+
AnyThemeConfig,
27+
} from '../types';
2228

2329
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
2430
xs: 'fontSizeXS',
@@ -113,6 +119,22 @@ export function isThemeDark(theme: SupersetTheme): boolean {
113119
return tinycolor(theme.colorBgContainer).isDark();
114120
}
115121

122+
/**
123+
* Check if a theme configuration uses dark algorithm
124+
* @param config - The theme configuration to check
125+
* @returns true if the config uses dark algorithm, false otherwise
126+
*/
127+
export function isThemeConfigDark(config: AnyThemeConfig): boolean {
128+
const darkAlg = antdTheme.darkAlgorithm;
129+
130+
if (config.algorithm === darkAlg) return true;
131+
132+
if (Array.isArray(config.algorithm))
133+
return config.algorithm.some((alg: any) => alg === darkAlg);
134+
135+
return false;
136+
}
137+
116138
/**
117139
* Hook to determine if the current theme is dark mode
118140
* @returns true if theme is dark, false if light

0 commit comments

Comments
 (0)