This is an automated email from the ASF dual-hosted git repository.

sophieyou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new dea9068647 fix(DashboardEditor): CSS template selector UI in dashboard 
properties modal restored (#35106)
dea9068647 is described below

commit dea906864728807ca61b3aeef4bffdb03a0212fa
Author: Rafael Benitez <rebenitez1...@gmail.com>
AuthorDate: Thu Sep 11 19:34:16 2025 -0300

    fix(DashboardEditor): CSS template selector UI in dashboard properties 
modal restored (#35106)
---
 superset-frontend/package-lock.json                |   5 +-
 .../dashboard/components/PropertiesModal/index.tsx |   1 +
 .../sections/StylingSection.test.tsx               | 110 ++++++++++-
 .../PropertiesModal/sections/StylingSection.tsx    | 214 ++++++++++++++++-----
 4 files changed, 274 insertions(+), 56 deletions(-)

diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index d2f9ca5b31..d4c0646fbb 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -60642,7 +60642,7 @@
     },
     "packages/superset-core": {
       "name": "@apache-superset/core",
-      "version": "0.0.1-rc2",
+      "version": "0.0.1-rc3",
       "license": "ISC",
       "devDependencies": {
         "@babel/cli": "^7.26.4",
@@ -60652,7 +60652,8 @@
         "@babel/preset-typescript": "^7.26.0",
         "@types/react": "^17.0.83",
         "install": "^0.13.0",
-        "npm": "^11.1.0"
+        "npm": "^11.1.0",
+        "typescript": "^5.0.0"
       },
       "peerDependencies": {
         "antd": "^5.24.6",
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx 
b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 33a398e8a8..040bc6ee2e 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -715,6 +715,7 @@ const PropertiesModal = ({
                   onThemeChange={handleThemeChange}
                   onColorSchemeChange={onColorSchemeChange}
                   onCustomCssChange={setCustomCss}
+                  addDangerToast={addDangerToast}
                 />
               ),
             },
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
index 1dc7f12dfd..13de2a78d5 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
@@ -16,9 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { render, screen, userEvent } from 'spec/helpers/testing-library';
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
+import { SupersetClient, isFeatureEnabled } from '@superset-ui/core';
 import StylingSection from './StylingSection';
 
+// Mock SupersetClient
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  SupersetClient: {
+    get: jest.fn(),
+  },
+  isFeatureEnabled: jest.fn(),
+}));
+
+const mockSupersetClient = SupersetClient as jest.Mocked<typeof 
SupersetClient>;
+const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
+  typeof isFeatureEnabled
+>;
+
 // Mock ColorSchemeSelect component
 jest.mock('src/dashboard/components/ColorSchemeSelect', () => ({
   __esModule: true,
@@ -33,6 +53,14 @@ jest.mock('src/dashboard/components/ColorSchemeSelect', () 
=> ({
   ),
 }));
 
+const mockCssTemplates = [
+  { template_name: 'Corporate Blue', css: '.dashboard { background: blue; }' },
+  {
+    template_name: 'Modern Dark',
+    css: '.dashboard { background: black; color: white; }',
+  },
+];
+
 const defaultProps = {
   themes: [
     { id: 1, theme_name: 'Dark Theme' },
@@ -45,10 +73,17 @@ const defaultProps = {
   onThemeChange: jest.fn(),
   onColorSchemeChange: jest.fn(),
   onCustomCssChange: jest.fn(),
+  addDangerToast: jest.fn(),
 };
 
 beforeEach(() => {
   jest.clearAllMocks();
+  // Reset mocks
+  mockIsFeatureEnabled.mockReturnValue(false);
+  mockSupersetClient.get.mockResolvedValue({
+    json: { result: mockCssTemplates },
+    response: {} as Response,
+  });
 });
 
 test('renders theme selection when themes are available', () => {
@@ -120,3 +155,76 @@ test('displays current color scheme value', () => {
   const colorSchemeInput = screen.getByLabelText('Select color scheme');
   expect(colorSchemeInput).toHaveValue('testColors');
 });
+
+// CSS Template Tests
+describe('CSS Template functionality', () => {
+  test('does not show CSS template select when feature flag is disabled', () 
=> {
+    mockIsFeatureEnabled.mockReturnValue(false);
+    render(<StylingSection {...defaultProps} />);
+
+    expect(
+      screen.queryByTestId('dashboard-css-template-field'),
+    ).not.toBeInTheDocument();
+  });
+
+  test('fetches CSS templates on mount when feature enabled', async () => {
+    mockIsFeatureEnabled.mockImplementation(flag => flag === 'CSS_TEMPLATES');
+    render(<StylingSection {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(mockSupersetClient.get).toHaveBeenCalledWith({
+        endpoint: expect.stringContaining('/api/v1/css_template/'),
+      });
+    });
+  });
+
+  test('shows CSS template select when feature flag is enabled and templates 
exist', async () => {
+    mockIsFeatureEnabled.mockImplementation(flag => flag === 'CSS_TEMPLATES');
+    render(<StylingSection {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(
+        screen.getByText('Load CSS template (optional)'),
+      ).toBeInTheDocument();
+    });
+
+    expect(
+      screen.getByTestId('dashboard-css-template-select'),
+    ).toBeInTheDocument();
+  });
+
+  test('shows error toast when template fetch fails', async () => {
+    mockIsFeatureEnabled.mockImplementation(flag => flag === 'CSS_TEMPLATES');
+    const addDangerToast = jest.fn();
+    mockSupersetClient.get.mockRejectedValueOnce(new Error('API Error'));
+
+    render(
+      <StylingSection {...defaultProps} addDangerToast={addDangerToast} />,
+    );
+
+    await waitFor(() => {
+      expect(addDangerToast).toHaveBeenCalledWith(
+        'An error occurred while fetching available CSS templates',
+      );
+    });
+  });
+
+  test('does not show CSS template select when no templates available', async 
() => {
+    mockIsFeatureEnabled.mockImplementation(flag => flag === 'CSS_TEMPLATES');
+    mockSupersetClient.get.mockResolvedValueOnce({
+      json: { result: [] },
+      response: {} as Response,
+    });
+
+    render(<StylingSection {...defaultProps} />);
+
+    // Wait for fetch to complete
+    await waitFor(() => {
+      expect(mockSupersetClient.get).toHaveBeenCalled();
+    });
+
+    expect(
+      screen.queryByTestId('dashboard-css-template-field'),
+    ).not.toBeInTheDocument();
+  });
+});
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
index 0c1c7f8820..1931fc692b 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
@@ -16,8 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, styled } from '@superset-ui/core';
-import { CssEditor, Select } from '@superset-ui/core/components';
+import { useCallback, useEffect, useState } from 'react';
+import {
+  t,
+  styled,
+  SupersetClient,
+  isFeatureEnabled,
+  FeatureFlag,
+} from '@superset-ui/core';
+import { CssEditor, Select, Alert } from '@superset-ui/core/components';
+import rison from 'rison';
 import ColorSchemeSelect from 'src/dashboard/components/ColorSchemeSelect';
 import { ModalFormField } from 'src/components/Modal';
 
@@ -26,11 +34,20 @@ const StyledCssEditor = styled(CssEditor)`
   border: 1px solid ${({ theme }) => theme.colorBorder};
 `;
 
+const StyledAlert = styled(Alert)`
+  margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;
+`;
+
 interface Theme {
   id: number;
   theme_name: string;
 }
 
+interface CssTemplate {
+  template_name: string;
+  css: string;
+}
+
 interface StylingSectionProps {
   themes: Theme[];
   selectedThemeId: number | null;
@@ -43,6 +60,7 @@ interface StylingSectionProps {
     options?: { updateMetadata?: boolean },
   ) => void;
   onCustomCssChange: (css: string) => void;
+  addDangerToast?: (message: string) => void;
 }
 
 const StylingSection = ({
@@ -54,63 +72,153 @@ const StylingSection = ({
   onThemeChange,
   onColorSchemeChange,
   onCustomCssChange,
-}: StylingSectionProps) => (
-  <>
-    {themes.length > 0 && (
+  addDangerToast,
+}: StylingSectionProps) => {
+  const [cssTemplates, setCssTemplates] = useState<CssTemplate[]>([]);
+  const [isLoadingTemplates, setIsLoadingTemplates] = useState(false);
+  const [selectedTemplate, setSelectedTemplate] = useState<string | 
null>(null);
+  const [originalTemplateContent, setOriginalTemplateContent] =
+    useState<string>('');
+
+  // Fetch CSS templates
+  const fetchCssTemplates = useCallback(async () => {
+    if (!isFeatureEnabled(FeatureFlag.CssTemplates)) return;
+
+    setIsLoadingTemplates(true);
+    try {
+      const query = rison.encode({ columns: ['template_name', 'css'] });
+      const response = await SupersetClient.get({
+        endpoint: `/api/v1/css_template/?q=${query}`,
+      });
+      setCssTemplates(response.json.result || []);
+    } catch (error) {
+      if (addDangerToast) {
+        addDangerToast(
+          t('An error occurred while fetching available CSS templates'),
+        );
+      }
+    } finally {
+      setIsLoadingTemplates(false);
+    }
+  }, [addDangerToast]);
+
+  useEffect(() => {
+    fetchCssTemplates();
+  }, [fetchCssTemplates]);
+
+  // Handle CSS template selection
+  const handleTemplateSelect = useCallback(
+    (templateName: string) => {
+      if (!templateName) {
+        setSelectedTemplate(null);
+        setOriginalTemplateContent('');
+        return;
+      }
+
+      const template = cssTemplates.find(t => t.template_name === 
templateName);
+      if (template) {
+        setSelectedTemplate(templateName);
+        setOriginalTemplateContent(template.css);
+        onCustomCssChange(template.css);
+      }
+    },
+    [cssTemplates, onCustomCssChange],
+  );
+
+  // Check if current CSS differs from original template
+  const hasTemplateModification =
+    selectedTemplate && customCss !== originalTemplateContent;
+
+  return (
+    <>
+      {themes.length > 0 && (
+        <ModalFormField
+          label={t('Theme')}
+          testId="dashboard-theme-field"
+          helperText={t(
+            'Clear the selection to revert to the system default theme',
+          )}
+        >
+          <Select
+            data-test="dashboard-theme-select"
+            value={selectedThemeId}
+            onChange={onThemeChange}
+            options={themes.map(theme => ({
+              value: theme.id,
+              label: theme.theme_name,
+            }))}
+            allowClear
+            placeholder={t('Select a theme')}
+          />
+        </ModalFormField>
+      )}
       <ModalFormField
-        label={t('Theme')}
-        testId="dashboard-theme-field"
+        label={t('Color scheme')}
+        testId="dashboard-colorscheme-field"
         helperText={t(
-          'Clear the selection to revert to the system default theme',
+          "Any color palette selected here will override the colors applied to 
this dashboard's individual charts",
         )}
       >
-        <Select
-          data-test="dashboard-theme-select"
-          value={selectedThemeId}
-          onChange={onThemeChange}
-          options={themes.map(theme => ({
-            value: theme.id,
-            label: theme.theme_name,
-          }))}
-          allowClear
-          placeholder={t('Select a theme')}
+        <ColorSchemeSelect
+          data-test="dashboard-colorscheme-select"
+          value={colorScheme}
+          onChange={onColorSchemeChange}
+          hasCustomLabelsColor={hasCustomLabelsColor}
+          showWarning={hasCustomLabelsColor}
         />
       </ModalFormField>
-    )}
-    <ModalFormField
-      label={t('Color scheme')}
-      testId="dashboard-colorscheme-field"
-      helperText={t(
-        "Any color palette selected here will override the colors applied to 
this dashboard's individual charts",
-      )}
-    >
-      <ColorSchemeSelect
-        data-test="dashboard-colorscheme-select"
-        value={colorScheme}
-        onChange={onColorSchemeChange}
-        hasCustomLabelsColor={hasCustomLabelsColor}
-        showWarning={hasCustomLabelsColor}
-      />
-    </ModalFormField>
-    <ModalFormField
-      label={t('Custom CSS')}
-      testId="dashboard-css-field"
-      helperText={t(
-        'Apply custom CSS to the dashboard. Use class names or element 
selectors to target specific components.',
+      {isFeatureEnabled(FeatureFlag.CssTemplates) &&
+        cssTemplates.length > 0 && (
+          <ModalFormField
+            label={t('Load CSS template (optional)')}
+            testId="dashboard-css-template-field"
+            helperText={t(
+              'Select a predefined CSS template to apply to your dashboard',
+            )}
+          >
+            <Select
+              data-test="dashboard-css-template-select"
+              onChange={handleTemplateSelect}
+              options={cssTemplates.map(template => ({
+                value: template.template_name,
+                label: template.template_name,
+              }))}
+              placeholder={t('Select a CSS template')}
+              loading={isLoadingTemplates}
+              allowClear
+              value={selectedTemplate}
+            />
+          </ModalFormField>
+        )}
+      {hasTemplateModification && (
+        <StyledAlert
+          type="warning"
+          message={t('Modified from "%s" template', selectedTemplate)}
+          showIcon
+          closable={false}
+          data-test="css-template-modified-warning"
+        />
       )}
-      bottomSpacing={false}
-    >
-      <StyledCssEditor
-        data-test="dashboard-css-editor"
-        onChange={onCustomCssChange}
-        value={customCss}
-        width="100%"
-        minLines={10}
-        maxLines={50}
-        editorProps={{ $blockScrolling: true }}
-      />
-    </ModalFormField>
-  </>
-);
+      <ModalFormField
+        label={t('CSS')}
+        testId="dashboard-css-field"
+        helperText={t(
+          'Apply custom CSS to the dashboard. Use class names or element 
selectors to target specific components.',
+        )}
+        bottomSpacing={false}
+      >
+        <StyledCssEditor
+          data-test="dashboard-css-editor"
+          onChange={onCustomCssChange}
+          value={customCss}
+          width="100%"
+          minLines={10}
+          maxLines={50}
+          editorProps={{ $blockScrolling: true }}
+        />
+      </ModalFormField>
+    </>
+  );
+};
 
 export default StylingSection;

Reply via email to