feat(themes): add JSON formatting to theme modal editor (#38739)

This commit is contained in:
Enzo Martellucci
2026-03-20 13:48:00 +01:00
committed by GitHub
parent 82a74c88aa
commit cbb2b2f3c2
2 changed files with 196 additions and 22 deletions

View File

@@ -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/*', {

View File

@@ -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>