mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat(themes): add JSON formatting to theme modal editor (#38739)
This commit is contained in:
@@ -712,6 +712,138 @@ test('applies theme locally when clicking Apply button', async () => {
|
||||
expect(mockThemeContext.setTemporaryTheme).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows Format button when modal is in edit mode', () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: /format/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not show Format button for read-only system themes', async () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
theme={mockSystemTheme}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
await screen.findByText('System Theme - Read Only');
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /format/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('disables Format button when JSON is invalid', async () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
const jsonEditor = screen.getByTestId('json-editor');
|
||||
userEvent.clear(jsonEditor);
|
||||
userEvent.type(jsonEditor, '{invalid json');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /format/i })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('enables Format button when JSON is valid', async () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
await addValidJsonData();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /format/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('Format button pretty-prints minified JSON', async () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
const minifiedJson = '{"token":{"colorPrimary":"#1890ff"}}';
|
||||
const jsonEditor = screen.getByTestId('json-editor');
|
||||
userEvent.clear(jsonEditor);
|
||||
userEvent.type(jsonEditor, minifiedJson);
|
||||
|
||||
const formatButton = screen.getByRole('button', { name: /format/i });
|
||||
userEvent.click(formatButton);
|
||||
|
||||
const expectedFormatted = JSON.stringify(
|
||||
{ token: { colorPrimary: '#1890ff' } },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(jsonEditor).toHaveValue(expectedFormatted);
|
||||
});
|
||||
});
|
||||
|
||||
test('Format button is disabled when JSON editor is empty', async () => {
|
||||
render(
|
||||
<ThemeModal
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
onThemeAdd={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
show
|
||||
canDevelop={false}
|
||||
/>,
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
// The editor initializes with `{}` — clear it to reach the empty state
|
||||
const jsonEditor = screen.getByTestId('json-editor');
|
||||
userEvent.clear(jsonEditor);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /format/i })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('disables Apply button when JSON configuration is invalid', async () => {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get('glob:*/api/v1/theme/*', {
|
||||
|
||||
@@ -71,6 +71,15 @@ const toEditorAnnotations = (
|
||||
message: ann.text,
|
||||
}));
|
||||
|
||||
const formatJsonData = (jsonData?: string): string | undefined => {
|
||||
if (!jsonData) return jsonData;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(jsonData), null, 2);
|
||||
} catch {
|
||||
return jsonData;
|
||||
}
|
||||
};
|
||||
|
||||
interface ThemeModalProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast?: (msg: string) => void;
|
||||
@@ -316,6 +325,15 @@ const ThemeModal: FunctionComponent<ThemeModalProps> = ({
|
||||
[currentTheme],
|
||||
);
|
||||
|
||||
const onFormat = useCallback(() => {
|
||||
if (currentTheme?.json_data) {
|
||||
const formatted = formatJsonData(currentTheme.json_data);
|
||||
if (formatted !== currentTheme.json_data) {
|
||||
onJsonDataChange(formatted || '');
|
||||
}
|
||||
}
|
||||
}, [currentTheme?.json_data, onJsonDataChange]);
|
||||
|
||||
const validate = () => {
|
||||
if (isReadOnly || !currentTheme) {
|
||||
setDisableSave(true);
|
||||
@@ -357,8 +375,12 @@ const ThemeModal: FunctionComponent<ThemeModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (resource) {
|
||||
setCurrentTheme(resource);
|
||||
setInitialTheme(resource);
|
||||
const formatted = {
|
||||
...resource,
|
||||
json_data: formatJsonData(resource.json_data),
|
||||
};
|
||||
setCurrentTheme(formatted);
|
||||
setInitialTheme(formatted);
|
||||
}
|
||||
}, [resource]);
|
||||
|
||||
@@ -522,27 +544,47 @@ const ThemeModal: FunctionComponent<ThemeModalProps> = ({
|
||||
annotations={toEditorAnnotations(validation.annotations)}
|
||||
/>
|
||||
</StyledEditorWrapper>
|
||||
{canDevelopThemes && (
|
||||
<div className="apply-button-container">
|
||||
<Tooltip
|
||||
title={t('Set local theme for testing (preview only)')}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
icon={<Icons.ThunderboltOutlined />}
|
||||
onClick={onApply}
|
||||
disabled={
|
||||
!currentTheme?.json_data ||
|
||||
!isValidJson(currentTheme.json_data) ||
|
||||
validation.hasErrors
|
||||
}
|
||||
buttonStyle="secondary"
|
||||
<div className="apply-button-container">
|
||||
<Space>
|
||||
{!isReadOnly && (
|
||||
<Tooltip
|
||||
title={t('Format JSON configuration')}
|
||||
placement="top"
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
icon={<Icons.AlignLeftOutlined />}
|
||||
buttonStyle="secondary"
|
||||
onClick={onFormat}
|
||||
disabled={
|
||||
!currentTheme?.json_data ||
|
||||
!isValidJson(currentTheme.json_data)
|
||||
}
|
||||
>
|
||||
{t('Format')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canDevelopThemes && (
|
||||
<Tooltip
|
||||
title={t('Set local theme for testing (preview only)')}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
icon={<Icons.ThunderboltOutlined />}
|
||||
onClick={onApply}
|
||||
disabled={
|
||||
!currentTheme?.json_data ||
|
||||
!isValidJson(currentTheme.json_data) ||
|
||||
validation.hasErrors
|
||||
}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</StyledFormWrapper>
|
||||
|
||||
Reference in New Issue
Block a user