From 3b1c993e5080b26cd7671de4cc14961668b51faf Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" Date: Fri, 20 May 2022 09:03:51 -0300 Subject: [PATCH] feat: Adds the "Select all" option to the Select component --- superset-frontend/package-lock.json | 39 +++ superset-frontend/package.json | 1 + .../src/components/Select/Select.stories.tsx | 40 ++- .../src/components/Select/Select.test.tsx | 89 +++++++ .../src/components/Select/Select.tsx | 238 +++++++++++++++--- 5 files changed, 353 insertions(+), 54 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 31416e0842d8..93ee1e3e5744 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -139,6 +139,7 @@ "shortid": "^2.2.6", "tinycolor2": "^1.4.2", "urijs": "^1.19.8", + "use-deep-compare-effect": "^1.8.1", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", "yargs": "^15.4.1" @@ -26096,6 +26097,14 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "node_modules/dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==", + "engines": { + "node": ">=6" + } + }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -50358,6 +50367,22 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-deep-compare-effect": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz", + "integrity": "sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "dequal": "^2.0.2" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13" + } + }, "node_modules/use-immer": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.6.0.tgz", @@ -76185,6 +76210,11 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==" + }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -95053,6 +95083,15 @@ "ts-essentials": "^2.0.3" } }, + "use-deep-compare-effect": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz", + "integrity": "sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==", + "requires": { + "@babel/runtime": "^7.12.5", + "dequal": "^2.0.2" + } + }, "use-immer": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.6.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index acdf9952d099..0d9c7e89f432 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -199,6 +199,7 @@ "shortid": "^2.2.6", "tinycolor2": "^1.4.2", "urijs": "^1.19.8", + "use-deep-compare-effect": "^1.8.1", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", "yargs": "^15.4.1" diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx index 5526c2fc2ac0..5671b78f3ff9 100644 --- a/superset-frontend/src/components/Select/Select.stories.tsx +++ b/superset-frontend/src/components/Select/Select.stories.tsx @@ -18,6 +18,7 @@ */ import React, { ReactNode, useState, useCallback } from 'react'; import ControlHeader from 'src/explore/components/ControlHeader'; +import { SelectValue } from 'antd/lib/select'; import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select'; export default { @@ -210,17 +211,6 @@ InteractiveSelect.argTypes = { description: `It adds a header on top of the Select. Can be any ReactNode.`, control: { type: 'inline-radio', options: ['none', 'text', 'control'] }, }, - pageSize: { - description: `It defines how many results should be included in the query response. - Works in async mode only (See the options property). - `, - }, - fetchOnlyOnSearch: { - description: `It fires a request against the server only after searching. - Works in async mode only (See the options property). - Undefined by default. - `, - }, }; InteractiveSelect.story = { @@ -387,6 +377,7 @@ export const AsyncSelect = ({ responseTime: number; }) => { const [requests, setRequests] = useState([]); + const [value, setValue] = useState(); const getResults = (username?: string) => { let results: { label: string; value: string }[] = []; @@ -466,6 +457,7 @@ export const AsyncSelect = ({ ? { label: 'Valentina', value: 'Valentina' } : undefined } + onChange={value => setValue(value)} />
{request}

))}
+
+ {value ? JSON.stringify(value) : ''} +
); }; @@ -491,8 +497,6 @@ export const AsyncSelect = ({ AsyncSelect.args = { allowClear: false, allowNewOptions: false, - fetchOnlyOnSearch: false, - pageSize: 10, withError: false, withInitialValue: false, tokenSeparators: ['\n', '\t', ';'], @@ -511,6 +515,9 @@ AsyncSelect.argTypes = { }, }, pageSize: { + description: `It defines how many results should be included in the query response. + Works in async mode only (See the options property). + `, defaultValue: 10, control: { type: 'range', @@ -519,6 +526,13 @@ AsyncSelect.argTypes = { step: 10, }, }, + fetchOnlyOnSearch: { + description: `It fires a request against the server only after searching. + Works in async mode only (See the options property). + Undefined by default. + `, + defaultValue: false, + }, responseTime: { defaultValue: 0.5, name: 'responseTime (seconds)', diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx index 37a39204369f..634f59670c45 100644 --- a/superset-frontend/src/components/Select/Select.test.tsx +++ b/superset-frontend/src/components/Select/Select.test.tsx @@ -25,6 +25,8 @@ const ARIA_LABEL = 'Test'; const NEW_OPTION = 'Kyle'; const NO_DATA = 'No Data'; const LOADING = 'Loading...'; +const SELECT_ALL = 'Select all'; + const OPTIONS = [ { label: 'John', value: 1, gender: 'Male' }, { label: 'Liam', value: 2, gender: 'Male' }, @@ -50,6 +52,7 @@ const OPTIONS = [ { label: 'Cher', value: 22, gender: 'Female' }, { label: 'Her', value: 23, gender: 'Male' }, ].sort((option1, option2) => option1.label.localeCompare(option2.label)); + const NULL_OPTION = { label: '', value: null } as unknown as { label: string; value: number; @@ -412,6 +415,92 @@ test('adds the null option when selected in multiple mode', async () => { expect(values[1]).toHaveTextContent(NULL_OPTION.label); }); +test('renders "Select all" for multiple select', async () => { + render(); + await open(); + expect(screen.queryByText(SELECT_ALL)).not.toBeInTheDocument(); +}); + +test('does not render "Select all" for an empty multiple select', async () => { + render(); + await open(); + await type('Select'); + expect(await findSelectOption(SELECT_ALL)).not.toBeInTheDocument(); +}); + +test('does not render "Select all" as one of the tags after selection', async () => { + render(); + await open(); + userEvent.click(await findSelectOption(selected.label)); + const options = await findAllSelectOptions(); + expect(options[0]).toHaveTextContent(SELECT_ALL); + expect(options[1]).toHaveTextContent(selected.label); +}); + +test('selects all values', async () => { + render(); + await open(); + userEvent.click(await findSelectOption(SELECT_ALL)); + const values = await findAllSelectValues(); + expect(values.length).toBe(OPTIONS.length); + userEvent.click(await findSelectOption(SELECT_ALL)); + expect(values.length).toBe(0); +}); + +test('deselecting a value also deselects "Select all"', async () => { + render(); + await open(); + const options = await findAllSelectOptions(); + options.forEach((option, index) => { + // skip select all + if (index > 0) { + userEvent.click(option); + } + }); + const values = await findAllSelectValues(); + expect(values[0]).toHaveTextContent(SELECT_ALL); +}); + test('static - renders the select with default props', () => { render(