diff --git a/superset-frontend/src/features/themes/ThemeModal.test.tsx b/superset-frontend/src/features/themes/ThemeModal.test.tsx index 4994863b074..2b4c29efc4f 100644 --- a/superset-frontend/src/features/themes/ThemeModal.test.tsx +++ b/superset-frontend/src/features/themes/ThemeModal.test.tsx @@ -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( + , + { 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( + , + { 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( + , + { 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( + , + { 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( + , + { 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( + , + { 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/*', { diff --git a/superset-frontend/src/features/themes/ThemeModal.tsx b/superset-frontend/src/features/themes/ThemeModal.tsx index 59d843d1d90..db7692443bb 100644 --- a/superset-frontend/src/features/themes/ThemeModal.tsx +++ b/superset-frontend/src/features/themes/ThemeModal.tsx @@ -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 = ({ [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 = ({ 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 = ({ annotations={toEditorAnnotations(validation.annotations)} /> - {canDevelopThemes && ( -
- - - -
- )} + + + )} + {canDevelopThemes && ( + + + + )} + +