diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index b964f48ee78ab..e49f00be537aa 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -858,6 +858,45 @@ test('does not duplicate options when using numeric values', async () => { await waitFor(() => expect(getAllSelectOptions().length).toBe(1)); }); +test('pasting an existing option does not duplicate it', async () => { + const options = jest.fn(async () => ({ + data: [OPTIONS[0]], + totalCount: 1, + })); + render(); + await open(); + const input = getElementByClassName('.ant-select-selection-search-input'); + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => OPTIONS[0].label, + }, + }); + fireEvent(input, paste); + expect(await findAllSelectOptions()).toHaveLength(1); +}); + +test('pasting an existing option does not duplicate it in multiple mode', async () => { + const options = jest.fn(async () => ({ + data: [ + { label: 'John', value: 1 }, + { label: 'Liam', value: 2 }, + { label: 'Olivia', value: 3 }, + ], + totalCount: 3, + })); + render(); + await open(); + const input = getElementByClassName('.ant-select-selection-search-input'); + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => 'John,Liam,Peter', + }, + }); + fireEvent(input, paste); + // Only Peter should be added + expect(await findAllSelectOptions()).toHaveLength(4); +}); + /* TODO: Add tests that require scroll interaction. Needs further investigation. - Fetches more data when scrolling and more data is available diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index 320d6ec3bdb47..20de7bb5911c0 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -49,6 +49,8 @@ import { dropDownRenderHelper, handleFilterOptionHelper, mapOptions, + getOption, + isObject, } from './utils'; import { AsyncSelectProps, @@ -523,19 +525,33 @@ const AsyncSelect = forwardRef( [ref], ); + const getPastedTextValue = useCallback( + (text: string) => { + const option = getOption(text, fullSelectOptions, true); + const value: AntdLabeledValue = { + label: text, + value: text, + }; + if (option) { + value.label = isObject(option) ? option.label : option; + value.value = isObject(option) ? option.value! : option; + } + return value; + }, + [fullSelectOptions], + ); + const onPaste = (e: ClipboardEvent) => { const pastedText = e.clipboardData.getData('text'); if (isSingleMode) { - setSelectValue({ label: pastedText, value: pastedText }); + setSelectValue(getPastedTextValue(pastedText)); } else { const token = tokenSeparators.find(token => pastedText.includes(token)); const array = token ? uniq(pastedText.split(token)) : [pastedText]; + const values = array.map(item => getPastedTextValue(item)); setSelectValue(previous => [ ...((previous || []) as AntdLabeledValue[]), - ...array.map(value => ({ - label: value, - value, - })), + ...values, ]); } }; diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx index 2b204cec1cd52..52e566df177ad 100644 --- a/superset-frontend/src/components/Select/Select.test.tsx +++ b/superset-frontend/src/components/Select/Select.test.tsx @@ -972,6 +972,45 @@ test('does not duplicate options when using numeric values', async () => { await waitFor(() => expect(getAllSelectOptions().length).toBe(1)); }); +test('pasting an existing option does not duplicate it', async () => { + render(, + ); + await open(); + const input = getElementByClassName('.ant-select-selection-search-input'); + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => 'John,Liam,Peter', + }, + }); + fireEvent(input, paste); + // Only Peter should be added + expect(await findAllSelectOptions()).toHaveLength(4); +}); + /* TODO: Add tests that require scroll interaction. Needs further investigation. - Fetches more data when scrolling and more data is available diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index 89c62ef8bd98f..907850a456fe5 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -51,6 +51,8 @@ import { mapValues, mapOptions, hasCustomLabels, + getOption, + isObject, } from './utils'; import { RawValue, SelectOptionsType, SelectProps } from './types'; import { @@ -530,27 +532,42 @@ const Select = forwardRef( actualMaxTagCount -= 1; } + const getPastedTextValue = useCallback( + (text: string) => { + const option = getOption(text, fullSelectOptions, true); + if (labelInValue) { + const value: AntdLabeledValue = { + label: text, + value: text, + }; + if (option) { + value.label = isObject(option) ? option.label : option; + value.value = isObject(option) ? option.value! : option; + } + return value; + } + return option ? (isObject(option) ? option.value! : option) : text; + }, + [fullSelectOptions, labelInValue], + ); + const onPaste = (e: ClipboardEvent) => { const pastedText = e.clipboardData.getData('text'); if (isSingleMode) { - setSelectValue( - labelInValue ? { label: pastedText, value: pastedText } : pastedText, - ); + setSelectValue(getPastedTextValue(pastedText)); } else { const token = tokenSeparators.find(token => pastedText.includes(token)); const array = token ? uniq(pastedText.split(token)) : [pastedText]; + const values = array.map(item => getPastedTextValue(item)); if (labelInValue) { setSelectValue(previous => [ ...((previous || []) as AntdLabeledValue[]), - ...array.map(value => ({ - label: value, - value, - })), + ...(values as AntdLabeledValue[]), ]); } else { setSelectValue(previous => [ ...((previous || []) as string[]), - ...array, + ...(values as string[]), ]); } } diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx index 7de201caeb6a7..0b638f4f0128f 100644 --- a/superset-frontend/src/components/Select/utils.tsx +++ b/superset-frontend/src/components/Select/utils.tsx @@ -49,27 +49,33 @@ export function getValue( return isLabeledValue(option) ? option.value : option; } -export function hasOption( +export function getOption( value: V, options?: V | LabeledValue | (V | LabeledValue)[], checkLabel = false, -): boolean { +): V | LabeledValue { const optionsArray = ensureIsArray(options); // When comparing the values we use the equality // operator to automatically convert different types - return ( - optionsArray.find( - x => + return optionsArray.find( + x => + // eslint-disable-next-line eqeqeq + x == value || + (isObject(x) && // eslint-disable-next-line eqeqeq - x == value || - (isObject(x) && - // eslint-disable-next-line eqeqeq - (('value' in x && x.value == value) || - (checkLabel && 'label' in x && x.label === value))), - ) !== undefined + (('value' in x && x.value == value) || + (checkLabel && 'label' in x && x.label === value))), ); } +export function hasOption( + value: V, + options?: V | LabeledValue | (V | LabeledValue)[], + checkLabel = false, +): boolean { + return getOption(value, options, checkLabel) !== undefined; +} + /** * It creates a comparator to check for a specific property. * Can be used with string and number property values.