Skip to content

Commit cb88d88

Browse files
authored
fix(PropertiesModal): do not show validation errors while loading (#35215)
1 parent 608e3ba commit cb88d88

File tree

5 files changed

+91
-77
lines changed

5 files changed

+91
-77
lines changed

superset-frontend/src/components/Modal/StandardModal.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
import { ReactNode } from 'react';
2020
import { styled, t } from '@superset-ui/core';
21-
import { Modal } from '@superset-ui/core/components';
21+
import { Modal, Loading, Flex } from '@superset-ui/core/components';
2222
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
2323

2424
interface StandardModalProps {
@@ -39,6 +39,7 @@ interface StandardModalProps {
3939
destroyOnClose?: boolean;
4040
maskClosable?: boolean;
4141
wrapProps?: object;
42+
contentLoading?: boolean;
4243
}
4344

4445
// Standard modal widths
@@ -113,12 +114,13 @@ export function StandardModal({
113114
destroyOnClose = true,
114115
maskClosable = false,
115116
wrapProps,
117+
contentLoading = false,
116118
}: StandardModalProps) {
117119
const primaryButtonName = saveText || (isEditMode ? t('Save') : t('Add'));
118120

119121
return (
120122
<StyledModal
121-
disablePrimaryButton={saveDisabled || saveLoading}
123+
disablePrimaryButton={saveDisabled || saveLoading || contentLoading}
122124
primaryButtonLoading={saveLoading}
123125
primaryTooltipMessage={errorTooltip}
124126
onHandledPrimaryAction={onSave}
@@ -139,7 +141,13 @@ export function StandardModal({
139141
)
140142
}
141143
>
142-
{children}
144+
{contentLoading ? (
145+
<Flex justify="center" align="center" style={{ minHeight: 200 }}>
146+
<Loading />
147+
</Flex>
148+
) : (
149+
children
150+
)}
143151
</StyledModal>
144152
);
145153
}

superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,19 @@ describe('PropertiesModal', () => {
354354
mockedIsFeatureEnabled.mockReturnValue(false);
355355
const props = createProps();
356356
props.onlyApply = false;
357-
render(<PropertiesModal {...props} />, {
357+
// Pass dashboardInfo to avoid loading state
358+
const propsWithDashboardInfo = {
359+
...props,
360+
dashboardInfo: {
361+
...dashboardInfo,
362+
json_metadata: mockedJsonMetadata,
363+
},
364+
};
365+
render(<PropertiesModal {...propsWithDashboardInfo} />, {
358366
useRedux: true,
359367
});
368+
369+
// Wait for the form to be visible
360370
expect(
361371
await screen.findByTestId('dashboard-edit-properties-form'),
362372
).toBeInTheDocument();
@@ -379,9 +389,19 @@ describe('PropertiesModal', () => {
379389
mockedIsFeatureEnabled.mockReturnValue(false);
380390
const props = createProps();
381391
props.onlyApply = true;
382-
render(<PropertiesModal {...props} />, {
392+
// Pass dashboardInfo to avoid loading state
393+
const propsWithDashboardInfo = {
394+
...props,
395+
dashboardInfo: {
396+
...dashboardInfo,
397+
json_metadata: mockedJsonMetadata,
398+
},
399+
};
400+
render(<PropertiesModal {...propsWithDashboardInfo} />, {
383401
useRedux: true,
384402
});
403+
404+
// Wait for the form to be visible
385405
expect(
386406
await screen.findByTestId('dashboard-edit-properties-form'),
387407
).toBeInTheDocument();

superset-frontend/src/dashboard/components/PropertiesModal/index.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ const PropertiesModal = ({
112112
const dispatch = useDispatch();
113113
const [form] = Form.useForm();
114114

115-
const [isLoading, setIsLoading] = useState(false);
115+
const [isLoading, setIsLoading] = useState(true);
116116
const [isApplying, setIsApplying] = useState(false);
117117
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
118118
const [jsonMetadata, setJsonMetadata] = useState('');
@@ -207,7 +207,6 @@ const PropertiesModal = ({
207207
);
208208

209209
const fetchDashboardDetails = useCallback(() => {
210-
setIsLoading(true);
211210
// We fetch the dashboard details because not all code
212211
// that renders this component have all the values we need.
213212
// At some point when we have a more consistent frontend
@@ -382,10 +381,6 @@ const PropertiesModal = ({
382381
if (onlyApply) {
383382
setIsApplying(true);
384383
try {
385-
console.log('Apply CSS debug:', {
386-
css_being_sent: customCss,
387-
onSubmitProps_css: onSubmitProps.css,
388-
});
389384
onSubmit(onSubmitProps);
390385
onHide();
391386
addSuccessToast(t('Dashboard properties updated'));
@@ -422,10 +417,15 @@ const PropertiesModal = ({
422417

423418
useEffect(() => {
424419
if (show) {
420+
// Reset loading state when modal opens
421+
setIsLoading(true);
422+
425423
if (!currentDashboardInfo) {
426424
fetchDashboardDetails();
427425
} else {
428426
handleDashboardData(currentDashboardInfo);
427+
// Data is immediately available, so we can stop loading
428+
setIsLoading(false);
429429
}
430430

431431
// Fetch themes (excluding system themes)
@@ -621,10 +621,9 @@ const PropertiesModal = ({
621621
}}
622622
title={t('Dashboard properties')}
623623
isEditMode
624-
saveDisabled={
625-
isLoading || dashboardInfo?.isManagedExternally || hasErrors
626-
}
624+
saveDisabled={dashboardInfo?.isManagedExternally || hasErrors}
627625
saveLoading={isApplying}
626+
contentLoading={isLoading}
628627
errorTooltip={
629628
dashboardInfo?.isManagedExternally
630629
? t(
@@ -665,7 +664,6 @@ const PropertiesModal = ({
665664
children: (
666665
<BasicInfoSection
667666
form={form}
668-
isLoading={isLoading}
669667
validationStatus={validationStatus}
670668
/>
671669
),

superset-frontend/src/dashboard/components/PropertiesModal/sections/BasicInfoSection.test.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import { Form } from '@superset-ui/core/components';
2121
import BasicInfoSection from './BasicInfoSection';
2222

2323
const defaultProps = {
24-
form: {} as any,
25-
isLoading: false,
24+
form: {
25+
getFieldValue: jest.fn(() => 'Test Dashboard'),
26+
} as any,
2627
validationStatus: {
2728
basic: { hasErrors: false, errors: [], name: 'Basic' },
2829
},
@@ -50,16 +51,6 @@ test('shows required asterisk for name field', () => {
5051
expect(screen.getByText('*')).toBeInTheDocument();
5152
});
5253

53-
test('disables inputs when loading', () => {
54-
render(
55-
<Form>
56-
<BasicInfoSection {...defaultProps} isLoading />
57-
</Form>,
58-
);
59-
60-
expect(screen.getByTestId('dashboard-title-input')).toBeDisabled();
61-
});
62-
6354
test('shows error message when name is empty and has validation errors', () => {
6455
const mockForm = {
6556
getFieldValue: jest.fn(field => (field === 'title' ? '' : 'test')),

superset-frontend/src/dashboard/components/PropertiesModal/sections/BasicInfoSection.tsx

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,62 +23,59 @@ import { ValidationObject } from 'src/components/Modal/useModalValidation';
2323

2424
interface BasicInfoSectionProps {
2525
form: FormInstance;
26-
isLoading: boolean;
2726
validationStatus: ValidationObject;
2827
}
2928

3029
const BasicInfoSection = ({
3130
form,
32-
isLoading,
3331
validationStatus,
34-
}: BasicInfoSectionProps) => (
35-
<>
36-
<ModalFormField
37-
label={t('Name')}
38-
required
39-
testId="dashboard-name-field"
40-
error={
41-
validationStatus.basic?.hasErrors &&
42-
(!form.getFieldValue('title') ||
43-
form.getFieldValue('title').trim().length === 0)
44-
? t('Dashboard name is required')
45-
: undefined
46-
}
47-
>
48-
<FormItem
49-
name="title"
50-
noStyle
51-
rules={[
52-
{
53-
required: true,
54-
message: t('Dashboard name is required'),
55-
whitespace: true,
56-
},
57-
]}
32+
}: BasicInfoSectionProps) => {
33+
const titleValue = form.getFieldValue('title');
34+
const hasError =
35+
validationStatus.basic?.hasErrors &&
36+
(!titleValue || titleValue.trim().length === 0);
37+
38+
return (
39+
<>
40+
<ModalFormField
41+
label={t('Name')}
42+
required
43+
testId="dashboard-name-field"
44+
error={hasError ? t('Dashboard name is required') : undefined}
45+
>
46+
<FormItem
47+
name="title"
48+
noStyle
49+
rules={[
50+
{
51+
required: true,
52+
message: t('Dashboard name is required'),
53+
whitespace: true,
54+
},
55+
]}
56+
>
57+
<Input
58+
placeholder={t('The display name of your dashboard')}
59+
data-test="dashboard-title-input"
60+
type="text"
61+
/>
62+
</FormItem>
63+
</ModalFormField>
64+
<ModalFormField
65+
label={t('URL Slug')}
66+
testId="dashboard-slug-field"
67+
bottomSpacing={false}
5868
>
59-
<Input
60-
placeholder={t('The display name of your dashboard')}
61-
data-test="dashboard-title-input"
62-
type="text"
63-
disabled={isLoading}
64-
/>
65-
</FormItem>
66-
</ModalFormField>
67-
<ModalFormField
68-
label={t('URL Slug')}
69-
testId="dashboard-slug-field"
70-
bottomSpacing={false}
71-
>
72-
<FormItem name="slug" noStyle>
73-
<Input
74-
placeholder={t('A readable URL for your dashboard')}
75-
data-test="dashboard-slug-input"
76-
type="text"
77-
disabled={isLoading}
78-
/>
79-
</FormItem>
80-
</ModalFormField>
81-
</>
82-
);
69+
<FormItem name="slug" noStyle>
70+
<Input
71+
placeholder={t('A readable URL for your dashboard')}
72+
data-test="dashboard-slug-input"
73+
type="text"
74+
/>
75+
</FormItem>
76+
</ModalFormField>
77+
</>
78+
);
79+
};
8380

8481
export default BasicInfoSection;

0 commit comments

Comments
 (0)