refactor(input): Migrate Input component to Ant Design 5 (#30730)

This commit is contained in:
Mehmet Salih Yavuz 2024-11-13 14:07:45 +03:00 committed by GitHub
parent 6e665c3e07
commit a7069e60e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 237 additions and 45 deletions

View File

@ -234,7 +234,7 @@ describe('Time range filter', () => {
cy.get('[data-test=time-range-trigger]').click();
cy.get('[data-test=custom-frame]').then(() => {
cy.get('.ant-input-number-input-wrap > input')
cy.get('.antd5-input-number-input-wrap > input')
.invoke('attr', 'value')
.should('eq', '7');
});

View File

@ -70,7 +70,7 @@ export const GlobalStyles = () => (
}
}
.column-config-popover {
& .ant-input-number {
& .antd5-input-number {
width: 100%;
}
&& .btn-group svg {

View File

@ -45,7 +45,6 @@ import {
import rison from 'rison';
import { debounce } from 'lodash';
import { FixedSizeList as List } from 'react-window';
import { AntdInput } from 'src/components';
import Icons from 'src/components/Icons';
import { Input } from 'src/components/Input';
import { useToasts } from 'src/components/MessageToasts/withToasts';
@ -55,6 +54,7 @@ import {
supersetGetCache,
} from 'src/utils/cachedSupersetGet';
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
import { InputRef } from 'antd-v5';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import DrillByModal from './DrillByModal';
import { getSubmenuYOffset } from '../utils';
@ -114,11 +114,12 @@ export const DrillByMenuItems = ({
const { addDangerToast } = useToasts();
const [isLoadingColumns, setIsLoadingColumns] = useState(true);
const [searchInput, setSearchInput] = useState('');
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
const [dataset, setDataset] = useState<Dataset>();
const [columns, setColumns] = useState<Column[]>([]);
const [showModal, setShowModal] = useState(false);
const [currentColumn, setCurrentColumn] = useState();
const ref = useRef<AntdInput>(null);
const ref = useRef<InputRef>(null);
const showSearch =
loadDrillByOptions || columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
const handleSelection = useCallback(
@ -138,11 +139,11 @@ export const DrillByMenuItems = ({
useEffect(() => {
if (open) {
ref.current?.input.focus({ preventScroll: true });
ref.current?.input?.focus({ preventScroll: true });
} else {
// Reset search input when menu is closed
ref.current?.setValue('');
setSearchInput('');
setDebouncedSearchInput('');
}
}, [open]);
@ -207,19 +208,27 @@ export const DrillByMenuItems = ({
hasDrillBy,
]);
const handleInput = debounce(
(value: string) => setSearchInput(value),
FAST_DEBOUNCE,
const debouncedSetSearchInput = useMemo(
() =>
debounce((value: string) => {
setDebouncedSearchInput(value);
}, FAST_DEBOUNCE),
[],
);
const handleInput = (value: string) => {
setSearchInput(value);
debouncedSetSearchInput(value);
};
const filteredColumns = useMemo(
() =>
columns.filter(column =>
(column.verbose_name || column.column_name)
.toLowerCase()
.includes(searchInput.toLowerCase()),
.includes(debouncedSearchInput.toLowerCase()),
),
[columns, searchInput],
[columns, debouncedSearchInput],
);
const submenuYOffset = useMemo(
@ -311,6 +320,7 @@ export const DrillByMenuItems = ({
margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
box-shadow: none;
`}
value={searchInput}
/>
)}
{isLoadingColumns ? (

View File

@ -41,13 +41,13 @@ import Dataset from 'src/types/Dataset';
import { useDebouncedEffect } from 'src/explore/exploreUtils';
import { SLOW_DEBOUNCE } from 'src/constants';
import Loading from 'src/components/Loading';
import { AntdInput } from 'src/components';
import { Input } from 'src/components/Input';
import {
PAGE_SIZE as DATASET_PAGE_SIZE,
SORT_BY as DATASET_SORT_BY,
} from 'src/features/datasets/constants';
import withToasts from 'src/components/MessageToasts/withToasts';
import { InputRef } from 'antd-v5';
import FacePile from '../FacePile';
const CONFIRM_WARNING_MESSAGE = t(
@ -115,7 +115,7 @@ const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
const [sortBy, setSortBy] = useState<SortByType>(DATASET_SORT_BY);
const [confirmChange, setConfirmChange] = useState(false);
const [confirmedDataset, setConfirmedDataset] = useState<Datasource>();
const searchRef = useRef<AntdInput>(null);
const searchRef = useRef<InputRef>(null);
const {
state: { loading, resourceCollection, resourceCount },

View File

@ -0,0 +1,138 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { InputProps, TextAreaProps } from 'antd-v5/lib/input';
import { InputNumberProps } from 'antd-v5/lib/input-number';
import { AntdThemeProvider } from 'src/components/AntdThemeProvider';
import { Input, TextArea, InputNumber } from '.';
export default {
title: 'Input',
component: Input,
};
export const InteractiveInput = (args: InputProps) => (
<AntdThemeProvider>
<Input {...args} />
</AntdThemeProvider>
);
export const InteractiveInputNumber = (args: InputNumberProps) => (
<AntdThemeProvider>
<InputNumber {...args} />
</AntdThemeProvider>
);
export const InteractiveTextArea = (args: TextAreaProps) => (
<AntdThemeProvider>
<TextArea {...args} />
</AntdThemeProvider>
);
InteractiveInput.args = {
allowClear: false,
disabled: false,
showCount: false,
type: 'text',
variant: 'outlined',
};
InteractiveInput.argTypes = {
defaultValue: {
control: {
type: 'text',
},
},
id: {
control: {
type: 'text',
},
},
maxLength: {
control: {
type: 'number',
},
},
status: {
control: {
type: 'select',
},
options: ['error', 'warning'],
},
size: {
control: {
type: 'select',
},
options: ['large', 'middle', 'small'],
},
variant: {
control: {
type: 'select',
},
options: ['outlined', 'borderless', 'filled'],
},
};
InteractiveTextArea.args = {
...InteractiveInput.args,
autoSize: false,
};
InteractiveTextArea.argTypes = InteractiveInput.argTypes;
InteractiveInputNumber.args = {
autoFocus: false,
disabled: false,
keyboard: true,
max: Number.MAX_SAFE_INTEGER,
min: Number.MIN_SAFE_INTEGER,
readonly: false,
step: 1,
stringMode: false,
};
InteractiveInputNumber.argTypes = {
controls: {
control: { type: 'boolean' },
},
decimalSeperator: {
control: { type: 'text' },
},
placeholder: {
control: { type: 'text' },
},
defaultValue: {
control: { type: 'number' },
},
precision: {
control: { type: 'number' },
},
status: InteractiveInput.argTypes.status,
size: InteractiveInput.argTypes.size,
variant: InteractiveInput.argTypes.variant,
};

View File

@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render } from 'spec/helpers/testing-library';
import { Input, InputNumber, TextArea } from '.';
test('should render Input', () => {
const { container } = render(<Input />);
expect(container).toBeInTheDocument();
});
test('should render InputNumber', () => {
const { container } = render(<InputNumber />);
expect(container).toBeInTheDocument();
});
test('should render TextArea', () => {
const { container } = render(<TextArea />);
expect(container).toBeInTheDocument();
});

View File

@ -16,20 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import { Input as AntdInput, InputNumber as AntdInputNumber } from 'antd';
export const Input = styled(AntdInput)`
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
import { Input as AntdInput, InputNumber as AntdInputNumber } from 'antd-v5';
export const InputNumber = styled(AntdInputNumber)`
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const Input = AntdInput;
export const TextArea = styled(AntdInput.TextArea)`
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const InputNumber = AntdInputNumber;
export const { TextArea } = AntdInput;

View File

@ -34,7 +34,8 @@ test('renders two inputs', () => {
test('receives null on non-numeric', async () => {
render(<BoundsControl {...defaultProps} />);
const minInput = screen.getAllByRole('spinbutton')[0];
userEvent.type(minInput, 'text');
userEvent.type(minInput, '1');
userEvent.clear(minInput);
await waitFor(() =>
expect(defaultProps.onChange).toHaveBeenCalledWith([null, null]),
);

View File

@ -79,11 +79,11 @@ export default function BoundsControl({
setMinMax([parseNumber(min), parseNumber(max)]);
}, [min, max]);
const onMinChange = (value: number | string | undefined) => {
const onMinChange = (value: number | string | undefined | null) => {
update([parseNumber(value), minMax[1]]);
};
const onMaxChange = (value: number | string | undefined) => {
const onMaxChange = (value: number | string | undefined | null) => {
update([minMax[0], parseNumber(value)]);
};
@ -94,7 +94,7 @@ export default function BoundsControl({
<MinInput
data-test="min-bound"
placeholder={t('Min')}
// emit (string | number | undefined)
// emit (string | number | undefined | null)
onChange={onMinChange}
// accept (number | undefined)
value={minMax[0] === null ? undefined : minMax[0]}
@ -102,7 +102,7 @@ export default function BoundsControl({
<MaxInput
data-test="max-bound"
placeholder={t('Max')}
// emit (number | string | undefined)
// emit (number | string | undefined | null)
onChange={onMaxChange}
// accept (number | undefined)
value={minMax[1] === null ? undefined : minMax[1]}

View File

@ -65,7 +65,7 @@ const ContentStyleWrapper = styled.div`
margin-top: 8px;
}
.ant-input-number {
.antd5-input-number {
width: 100%;
}

View File

@ -143,7 +143,7 @@ const SearchWrapper = styled.div`
margin-bottom: ${theme.gridUnit}px;
margin-left: ${theme.gridUnit * 3}px;
margin-right: ${theme.gridUnit * 3}px;
.ant-input-affix-wrapper {
.antd5-input-affix-wrapper {
padding-left: ${theme.gridUnit * 2}px;
}
`}

View File

@ -1004,8 +1004,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
}
};
const onCustomWidthChange = (value: number | null | undefined) => {
updateAlertState('custom_width', value);
const onCustomWidthChange = (value: number | string | null | undefined) => {
const numValue =
value === null ||
value === undefined ||
(typeof value === 'string' && Number.isNaN(Number(value)))
? null
: Number(value);
updateAlertState('custom_width', numValue);
};
const onTimeoutVerifyChange = (

View File

@ -16,11 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useCallback, useRef, FocusEvent, FC } from 'react';
import { useState, useCallback, FocusEvent, FC } from 'react';
import { t, useTheme } from '@superset-ui/core';
import { AntdInput, Select } from 'src/components';
import { Select } from 'src/components';
import { Input } from 'src/components/Input';
import { CronPicker, CronError } from 'src/components/CronPicker';
import { StyledInputContainer } from '../AlertReportModal';
@ -51,7 +51,6 @@ export const AlertReportCronScheduler: FC<AlertReportCronSchedulerProps> = ({
onChange,
}) => {
const theme = useTheme();
const inputRef = useRef<AntdInput>(null);
const [scheduleFormat, setScheduleFormat] = useState<ScheduleType>(
ScheduleType.Picker,
);
@ -59,9 +58,8 @@ export const AlertReportCronScheduler: FC<AlertReportCronSchedulerProps> = ({
const customSetValue = useCallback(
(newValue: string) => {
onChange(newValue);
inputRef.current?.setValue(newValue);
},
[inputRef, onChange],
[onChange],
);
const handleBlur = useCallback(
@ -72,8 +70,8 @@ export const AlertReportCronScheduler: FC<AlertReportCronSchedulerProps> = ({
);
const handlePressEnter = useCallback(() => {
onChange(inputRef.current?.input.value || '');
}, [onChange]);
onChange(value || '');
}, [onChange, value]);
const [error, onError] = useState<CronError>();
@ -106,7 +104,6 @@ export const AlertReportCronScheduler: FC<AlertReportCronSchedulerProps> = ({
<Input
type="text"
name="crontab"
ref={inputRef}
style={error ? { borderColor: theme.colors.error.base } : {}}
placeholder={t('CRON expression')}
value={value}

View File

@ -40,7 +40,7 @@ import { FilterType, RLSObject, RoleObject, TableObject } from './types';
const noMargins = css`
margin: 0;
.ant-input {
.antd5-input {
margin: 0;
}
`;

View File

@ -67,6 +67,20 @@ const baseConfig: ThemeConfig = {
paddingLG: supersetTheme.gridUnit * 6,
fontWeightStrong: supersetTheme.typography.weights.medium,
},
Input: {
colorBorder: supersetTheme.colors.secondary.light3,
colorBgContainer: supersetTheme.colors.grayscale.light5,
activeShadow: `0 0 0 ${supersetTheme.gridUnit / 2}px ${
supersetTheme.colors.primary.light3
}`,
},
InputNumber: {
colorBorder: supersetTheme.colors.secondary.light3,
colorBgContainer: supersetTheme.colors.grayscale.light5,
activeShadow: `0 0 0 ${supersetTheme.gridUnit / 2}px ${
supersetTheme.colors.primary.light3
}`,
},
Tag: {
borderRadiusSM: 2,
defaultBg: supersetTheme.colors.grayscale.light4,