From e17131f3008cdbac6aaf959a83feb804bbd8df58 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 17 Aug 2023 14:33:15 +0800 Subject: [PATCH 01/47] [BOOKINGSG-4362][WK] update keypath to nested array in single nested selector --- src/input-nested-select/input-nested-select.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/input-nested-select/input-nested-select.tsx b/src/input-nested-select/input-nested-select.tsx index ae6c00cee..a0af68089 100644 --- a/src/input-nested-select/input-nested-select.tsx +++ b/src/input-nested-select/input-nested-select.tsx @@ -234,12 +234,14 @@ export const InputNestedSelect = ({ if ((options && options.length > 0) || onRetry) { return ( Date: Fri, 18 Aug 2023 13:16:52 +0800 Subject: [PATCH 02/47] [BOOKINGSG-4362][WK] update to plural selectedKeyPaths --- .../input-nested-select.tsx | 20 ++++++------- src/shared/nested-dropdown-list/list-item.tsx | 13 ++++---- .../nested-dropdown-list-helper.ts | 30 +++++++++++-------- .../nested-dropdown-list.tsx | 6 ++-- src/shared/nested-dropdown-list/types.ts | 2 +- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/input-nested-select/input-nested-select.tsx b/src/input-nested-select/input-nested-select.tsx index a0af68089..a33dc474b 100644 --- a/src/input-nested-select/input-nested-select.tsx +++ b/src/input-nested-select/input-nested-select.tsx @@ -59,8 +59,8 @@ export const InputNestedSelect = ({ // ============================================================================= // CONST, STATE // ============================================================================= - const [selectedKeyPath, setSelectedKeyPath] = useState( - _selectedKeyPath || [] + const [selectedKeyPaths, setSelectedKeyPaths] = useState( + _selectedKeyPath?.flat().length ? [_selectedKeyPath] : [] ); const [selectedItem, setSelectedItem] = useState>(); @@ -74,10 +74,12 @@ export const InputNestedSelect = ({ // EFFECTS // ============================================================================= useEffect(() => { - const newKeyPath = _selectedKeyPath || []; - setSelectedKeyPath(newKeyPath); + const newKeyPath = _selectedKeyPath?.flat().length + ? [_selectedKeyPath] + : []; - updateSelectedItemFromKey(options, newKeyPath); + setSelectedKeyPaths(newKeyPath); + updateSelectedItemFromKey(options, _selectedKeyPath || []); }, [_selectedKeyPath, options]); // ============================================================================= @@ -99,7 +101,7 @@ export const InputNestedSelect = ({ ) => { const { keyPath, value, label } = item; - setSelectedKeyPath(keyPath); + setSelectedKeyPaths([keyPath]); setSelectedItem({ label, value }); setShowOptions(false); triggerOptionDisplayCallback(false); @@ -135,7 +137,7 @@ export const InputNestedSelect = ({ const getDisplayValue = (): string => { const { label, value } = selectedItem; - if (valueToStringFunction) { + if (valueToStringFunction && value) { return valueToStringFunction(value) || value.toString(); } else { return label; @@ -239,9 +241,7 @@ export const InputNestedSelect = ({ listStyleWidth={listStyleWidth} visible={showOptions} mode={mode} - selectedKeyPaths={ - selectedKeyPath.length ? [selectedKeyPath] : [] - } + selectedKeyPaths={selectedKeyPaths} selectableCategory={selectableCategory} itemsLoadState={optionsLoadState} itemTruncationType={optionTruncationType} diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index 0ab0e6612..098ca5263 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -17,7 +17,7 @@ import { interface ListItemProps { item: CombinedFormattedOptionProps; - selectedKeyPath?: string[] | undefined; + selectedKeyPaths: string[][]; selectableCategory?: boolean | undefined; searchValue: string | undefined; itemTruncationType?: TruncateType | undefined; @@ -30,7 +30,7 @@ interface ListItemProps { export const ListItem = ({ item, - selectedKeyPath, + selectedKeyPaths, selectableCategory, searchValue, itemTruncationType, @@ -66,9 +66,10 @@ export const ListItem = ({ // ============================================================================= // HELPER FUNCTIONS // ============================================================================= - const checkListItemSelected = (keyPath: string[]): boolean => { - return JSON.stringify(selectedKeyPath) === JSON.stringify(keyPath); - }; + const checkListItemSelected = (keyPath: string[]): boolean => + selectedKeyPaths.some( + (key) => JSON.stringify(key) === JSON.stringify(keyPath) + ); const hasExceededContainer = ( item: CombinedFormattedOptionProps @@ -132,7 +133,7 @@ export const ListItem = ({ = Map< export namespace NestedDropdownListHelper { export const getInitialDropdown = ( currentItems: FormattedOptionMap, - selectedKeyPath?: string[] | undefined + selectedKeyPaths: string[][] ) => { - let keyPath = selectedKeyPath; + let keyPaths = selectedKeyPaths; - if (!keyPath || !keyPath.length) { - keyPath = getInitialSubItem(currentItems); - keyPath = keyPath.slice(0, -1); + if (!keyPaths || !keyPaths.length) { + keyPaths = getInitialSubItem(currentItems); + console.log("keyPaths: ", keyPaths); } const list = produce( currentItems, - (draft: Map>) => { - const targetKey = []; - keyPath.forEach((key) => { - targetKey.push(key); - const item = getItemAtKeyPath(draft, targetKey); - item.expanded = true; + (draft: FormattedOptionMap) => { + let targetKey: string[] = []; + + keyPaths.forEach((keyPathArray) => { + targetKey = []; + keyPathArray.forEach((key) => { + targetKey.push(key); + const item = getItemAtKeyPath(draft, targetKey); + item.expanded = true; + }); }); } ); @@ -81,7 +85,7 @@ export namespace NestedDropdownListHelper { // ============================================================================= const getInitialSubItem = ( list: Map> | undefined -): string[] => { +): string[][] => { for (const item of list.values()) { if (item.subItems && item.subItems.size) { return getInitialSubItem(item.subItems); @@ -89,5 +93,5 @@ const getInitialSubItem = ( } const value = list.values().next().value; - return value.keyPath; + return [value.keyPath]; }; diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index f51352843..589add225 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -42,7 +42,7 @@ export const NestedDropdownList = ({ searchPlaceholder = "Search", visible, mode = "default", - selectedKeyPath, + selectedKeyPaths, selectableCategory, itemsLoadState = "success", itemTruncationType = "end", @@ -372,7 +372,7 @@ export const NestedDropdownList = ({ // otherwise expand the first selected item or first subitem tree const list = NestedDropdownListHelper.getInitialDropdown( currentItems, - selectedKeyPath + selectedKeyPaths ); return list; @@ -410,7 +410,7 @@ export const NestedDropdownList = ({ listItems?: L1OptionProps[] | undefined; visible?: boolean | undefined; /** Specifies key path of selected option */ - selectedKeyPath?: string[] | undefined; + selectedKeyPaths: string[][]; /** Specifies if items are expanded or collapsed when the dropdown is opened */ mode?: Mode | undefined; /** If specified, the category label is selectable */ From 1387c34c45422c4b0e1e491cdf2765a7643a73dc Mon Sep 17 00:00:00 2001 From: Wilker Date: Fri, 18 Aug 2023 13:59:27 +0800 Subject: [PATCH 03/47] [BOOKINGSG-4362][WK] clean up code --- src/input-nested-select/input-nested-select.tsx | 8 +++----- .../nested-dropdown-list/nested-dropdown-list-helper.ts | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/input-nested-select/input-nested-select.tsx b/src/input-nested-select/input-nested-select.tsx index a33dc474b..4353ac859 100644 --- a/src/input-nested-select/input-nested-select.tsx +++ b/src/input-nested-select/input-nested-select.tsx @@ -60,7 +60,7 @@ export const InputNestedSelect = ({ // CONST, STATE // ============================================================================= const [selectedKeyPaths, setSelectedKeyPaths] = useState( - _selectedKeyPath?.flat().length ? [_selectedKeyPath] : [] + _selectedKeyPath ? [_selectedKeyPath] : [] ); const [selectedItem, setSelectedItem] = useState>(); @@ -74,9 +74,7 @@ export const InputNestedSelect = ({ // EFFECTS // ============================================================================= useEffect(() => { - const newKeyPath = _selectedKeyPath?.flat().length - ? [_selectedKeyPath] - : []; + const newKeyPath = _selectedKeyPath ? [_selectedKeyPath] : []; setSelectedKeyPaths(newKeyPath); updateSelectedItemFromKey(options, _selectedKeyPath || []); @@ -137,7 +135,7 @@ export const InputNestedSelect = ({ const getDisplayValue = (): string => { const { label, value } = selectedItem; - if (valueToStringFunction && value) { + if (valueToStringFunction) { return valueToStringFunction(value) || value.toString(); } else { return label; diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 53ed0f200..01ca8d7fc 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -14,8 +14,7 @@ export namespace NestedDropdownListHelper { let keyPaths = selectedKeyPaths; if (!keyPaths || !keyPaths.length) { - keyPaths = getInitialSubItem(currentItems); - console.log("keyPaths: ", keyPaths); + keyPaths = [getInitialSubItem(currentItems)]; } const list = produce( @@ -85,7 +84,7 @@ export namespace NestedDropdownListHelper { // ============================================================================= const getInitialSubItem = ( list: Map> | undefined -): string[][] => { +): string[] => { for (const item of list.values()) { if (item.subItems && item.subItems.size) { return getInitialSubItem(item.subItems); @@ -93,5 +92,5 @@ const getInitialSubItem = ( } const value = list.values().next().value; - return [value.keyPath]; + return value.keyPath; }; From 66ab8cef63e7f37c0283a7a132632f4101a5f355 Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 7 Aug 2023 11:34:21 +0800 Subject: [PATCH 04/47] [BOOKINGSG-4362][WK] initial setup for multi nested --- src/form/form-nested-multi-select.tsx | 29 ++++ src/form/index.ts | 2 + src/index.ts | 1 + src/input-nested-multi-select/index.ts | 2 + .../form-nested-multi-select.stories.mdx | 77 +++++++++ .../form-nested-multi-select/props-table.tsx | 148 ++++++++++++++++++ 6 files changed, 259 insertions(+) create mode 100644 src/form/form-nested-multi-select.tsx create mode 100644 src/input-nested-multi-select/index.ts create mode 100644 stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx create mode 100644 stories/form/form-nested-multi-select/props-table.tsx diff --git a/src/form/form-nested-multi-select.tsx b/src/form/form-nested-multi-select.tsx new file mode 100644 index 000000000..7cce15140 --- /dev/null +++ b/src/form/form-nested-multi-select.tsx @@ -0,0 +1,29 @@ +import { InputNestedMultiSelect } from "../input-nested-multi-select"; +import { FormWrapper } from "./form-wrapper"; +import { FormNestedSelectProps } from "./types"; + +export const FormNestedMultiSelect = ({ + label, + errorMessage, + id = "form-nested-select", + "data-error-testid": errorTestId, + "data-testid": testId, + ...otherProps +}: FormNestedSelectProps): JSX.Element => { + return ( + + + + ); +}; diff --git a/src/form/index.ts b/src/form/index.ts index 9cf760faa..449fe660f 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -6,6 +6,7 @@ import { FormInputGroup } from "./form-input-group"; import { FormLabel } from "./form-label"; import { FormMultiSelect } from "./form-multi-select"; import { FormNestedSelect } from "./form-nested-select"; +import { FormNestedMultiSelect } from "./form-nested-multi-select"; import { FormPhoneNumberInput } from "./form-phone-number-input"; import { FormPredictiveTextInput } from "./form-predictive-text-input"; import { FormRangeSelect } from "./form-range-select"; @@ -23,6 +24,7 @@ export const Form = { Label: FormLabel, MultiSelect: FormMultiSelect, NestedSelect: FormNestedSelect, + NestedMultiSelect: FormNestedMultiSelect, Select: FormSelect, RangeSelect: FormRangeSelect, Textarea: FormTextarea, diff --git a/src/index.ts b/src/index.ts index f7080da65..17b27b468 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export * from "./input"; export * from "./input-group"; export * from "./input-multi-select"; export * from "./input-nested-select"; +export * from "./input-nested-multi-select"; export * from "./input-range-select"; export * from "./input-select"; export * from "./input-textarea"; diff --git a/src/input-nested-multi-select/index.ts b/src/input-nested-multi-select/index.ts new file mode 100644 index 000000000..bdbc3a314 --- /dev/null +++ b/src/input-nested-multi-select/index.ts @@ -0,0 +1,2 @@ +export * from "./input-nested-multi-select"; +export * from "./types"; diff --git a/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx b/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx new file mode 100644 index 000000000..8c0a32b08 --- /dev/null +++ b/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx @@ -0,0 +1,77 @@ +import { Canvas, Meta, Story } from "@storybook/addon-docs"; +import { useState } from "react"; +import { InputNestedMultiSelect } from "src/input-nested-multi-select"; +import { Form } from "src/form"; +import { Text } from "src/text"; +import { + Heading3, + Heading4, + Secondary, + StoryContainer, + Title, +} from "../../storybook-common"; +import { Container } from "../shared-doc-elements"; +import { PropsTable } from "./props-table"; +import { options } from "../form-nested-select/nested-data-list.ts"; + + + +Form.NestedMultiSelect + +Overview + +A field that provides a nested set of options for a user to select. + +```tsx +import { Form } from "@lifesg/react-design-system/form"; +``` + + + + {() => { + const [selectedKeyPaths, setSelectedKeyPaths] = useState([ + ["999", "820", "10002"], + ]); + return ( + + + + { + setSelectedKeyPaths(keyPaths); + }} + /> + + + ); + }} + + + +Using the field as a standalone + +In the case that you require the nested select field as a standalone, you can do this. + +```tsx +import { InputNestedSelect } from "@lifesg/react-design-system/input-nested-select"; +``` + + + + + + + + + + + +Component API + + diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx new file mode 100644 index 000000000..5070620b1 --- /dev/null +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import { ApiTable } from "../../storybook-common/api-table"; +import { ApiTableSectionProps } from "../../storybook-common/api-table/types"; +import { SHARED_FORM_PROPS_DATA } from "../shared-props-data"; + +const DATA: ApiTableSectionProps[] = [ + { + name: "InputNestedSelect specific props", + attributes: [ + { + name: "options", + mandatory: true, + description: "A list of options that a user can choose from", + propTypes: ["L1OptionProps[]"], + }, + { + name: "selectedKeyPath", + description: "The key path of the selected option", + propTypes: ["string[]"], + }, + { + name: "placeholder", + description: "The placeholder text of the component", + propTypes: ["string"], + defaultValue: "Select", + }, + { + name: "disabled", + description: + "Indicates if the component is disabled and selection is not allowed", + propTypes: ["boolean"], + }, + { + name: "error", + description: ( + <> + Indicates if an error display is to be set  (Not + needed if you indicated errorMessage) + + ), + propTypes: ["boolean"], + }, + { + name: "className", + description: "Class selector for the component", + propTypes: ["string"], + }, + { + name: "data-testid", + description: "The test identifier of the component", + propTypes: ["string"], + }, + { + name: "readOnly", + description: + "Indicates if the component has a read only state and selection is not allowed", + propTypes: ["boolean"], + }, + { + name: "mode", + description: + "Determines if items are expanded or collapsed when the dropdown is opened", + propTypes: [`"default"`, `"expand"`, `"collapse"`], + defaultValue: `"default"`, + }, + { + name: "valueToStringFunction", + description: + "The function to convert a value to a string. Only single callback used for both selects. Assumption: values are homogenous for both selects.", + propTypes: ["(value: V1 | V2 | V3) => string"], + }, + { + name: "selectableCategory", + description: "When specified, allows selection of categories", + propTypes: ["boolean"], + defaultValue: `"false"`, + }, + { + name: "optionsLoadState", + description: + "The visual state to represent the progress when options are loaded asynchronously", + propTypes: [`"success"`, `"loading"`, `"failed"`], + defaultValue: `"success"`, + }, + { + name: "optionTruncationType", + description: + "Specifies the trunction type of the options display. Truncated text will be replaced with ellipsis", + propTypes: [`"end"`, `"middle"`], + defaultValue: `"end"`, + }, + { + name: "hideNoResultsDisplay", + description: + "If specified, the default no results display will not be rendered", + propTypes: ["boolean"], + defaultValue: `"false"`, + }, + { + name: "listStyleWidth", + description: + "Style option: The width of the option display. (E.g. '100%' or '12rem')", + propTypes: ["string"], + }, + { + name: "enableSearch", + description: + "When specified, it will allow a text base search for the items in the list", + propTypes: ["boolean"], + defaultValue: "false", + }, + { + name: "searchPlaceholder", + description: "The placeholder for the search field", + propTypes: ["string"], + }, + { + name: "onSelectOption", + description: "Called when an option is selected", + propTypes: ["(keyPath: string[], value: V1 | V2 | V3) => void"], + }, + { + name: "onShowOptions", + description: "Called when the options dropdown is expanded", + propTypes: ["() => void"], + }, + { + name: "onHideOptions", + description: "Called when options dropdown is minimised", + propTypes: ["() => void"], + }, + { + name: "onRetry", + description: + "Called when retry button is clicked to retry loading the options", + propTypes: ["() => void"], + }, + { + name: "onSearch", + description: "Called when a search is being executed", + propTypes: ["() => void"], + }, + ], + }, + ...SHARED_FORM_PROPS_DATA, +]; + +export const PropsTable = () => ; From 933e5ec1decbc00aa33c9dc31439a2b092a2f679 Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 7 Aug 2023 11:50:56 +0800 Subject: [PATCH 05/47] [BOOKINGSG-4362][WK] update multi nested select types --- src/input-nested-multi-select/types.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/input-nested-multi-select/types.ts diff --git a/src/input-nested-multi-select/types.ts b/src/input-nested-multi-select/types.ts new file mode 100644 index 000000000..6c708ab7f --- /dev/null +++ b/src/input-nested-multi-select/types.ts @@ -0,0 +1,21 @@ +import { InputNestedSelectProps } from "../input-nested-select"; + +// ============================================================================= +// INPUT SELECT PROPS +// ============================================================================= + +type OmitTypes = "selectedKeyPath" | "onSelectOption" | "selectableCategory"; +export interface InputNestedMultiSelectProps + extends Omit, OmitTypes> { + /** Specifies key path to select particular option label */ + selectedKeyPaths?: string[][] | undefined; + onSelectOptions?: + | ((keyPath: string[], value: V1 | V2 | V3) => void) + | undefined; +} + +/** To be exposed for Form component inheritance */ +export type InputNestedMultiSelectPartialProps = Omit< + InputNestedMultiSelectProps, + "error" +>; From 6290d236383a76b10eb6346f4a5d96c3409403ed Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 7 Aug 2023 11:54:01 +0800 Subject: [PATCH 06/47] [BOOKINGSG-4362][WK] update updateSelectedItemFromKey/handleListItemClick fns and selectedKeyPath to plural --- .../input-nested-multi-select.tsx | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/input-nested-multi-select/input-nested-multi-select.tsx diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx new file mode 100644 index 000000000..82cb85cd9 --- /dev/null +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -0,0 +1,297 @@ +import React, { useEffect, useRef, useState } from "react"; +import isEmpty from "lodash/isEmpty"; +import { NestedDropdownList } from "../shared/nested-dropdown-list/nested-dropdown-list"; +import { DropdownWrapper } from "../shared/dropdown-wrapper"; +import { CombinedFormattedOptionProps } from "../shared/nested-dropdown-list/types"; +import { + Divider, + IconContainer, + LabelContainer, + PlaceholderLabel, + Selector, + StyledChevronIcon, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { StringHelper } from "../util"; +import { InputNestedMultiSelectProps } from "./types"; +import { CombinedOptionProps } from "../input-nested-select"; + +interface SelectedItemType { + label: string; + value: V1 | V2 | V3; +} + +export const InputNestedMultiSelect = ({ + placeholder = "Select", + options, + disabled, + error, + className, + "data-testid": testId, + id, + selectedKeyPaths: _selectedKeyPaths, + mode, + valueToStringFunction, + enableSearch, + searchPlaceholder, + hideNoResultsDisplay, + listStyleWidth, + readOnly, + onSearch, + onSelectOptions, + onShowOptions, + onHideOptions, + onRetry, + optionsLoadState = "success", + optionTruncationType = "end", + ...otherProps +}: InputNestedMultiSelectProps): JSX.Element => { + // ============================================================================= + // CONST, STATE + // ============================================================================= + const [selectedKeyPaths, setSelectedKeyPaths] = useState( + _selectedKeyPaths || [] + ); + const [selectedItems, setSelectedItems] = + useState[]>(); + + const [showOptions, setShowOptions] = useState(false); + + const selectorRef = useRef(); + const labelContainerRef = useRef(); + + // ============================================================================= + // EFFECTS + // ============================================================================= + useEffect(() => { + const newKeyPath = _selectedKeyPaths || []; + setSelectedKeyPaths(newKeyPath); + + updateSelectedItemFromKey(options, newKeyPath); + }, [_selectedKeyPaths, options]); + + // ============================================================================= + // EVENT HANDLERS + // ============================================================================= + const handleSelectorClick = (event: React.MouseEvent) => { + event.preventDefault(); + + if (disabled || readOnly) { + return; + } + + setShowOptions(!showOptions); + triggerOptionDisplayCallback(!showOptions); + }; + + const handleListItemClick = ( + item: CombinedFormattedOptionProps + ) => { + const { keyPath, value } = item; + + const isKeyPathExists = selectedKeyPaths.some( + (selectedKeyPath) => + JSON.stringify(selectedKeyPath) === JSON.stringify(keyPath) + ); + + if (!isKeyPathExists) { + setSelectedKeyPaths([...selectedKeyPaths, keyPath]); + setSelectedItems([...selectedItems, item]); + } else { + const resultKeyPaths = selectedKeyPaths.filter( + (selectedKeyPath) => + JSON.stringify(selectedKeyPath) !== JSON.stringify(keyPath) + ); + + setSelectedKeyPaths(resultKeyPaths); + // remove item, please + // setSelectedItems({ label, value }); + setSelectedItems([...selectedItems, item]); + } + + setShowOptions(false); + triggerOptionDisplayCallback(false); + + if (selectorRef.current) { + selectorRef.current.focus(); + } + + if (onSelectOptions) { + onSelectOptions(keyPath, value); + } + }; + + const handleListDismiss = (setSelectorFocus?: boolean | undefined) => { + if (showOptions) { + setShowOptions(false); + triggerOptionDisplayCallback(false); + } + + if (setSelectorFocus && selectorRef.current) { + selectorRef.current.focus(); + } + }; + + const handleWrapperBlur = () => { + setShowOptions(false); + triggerOptionDisplayCallback(false); + }; + + // ============================================================================= + // HELPER FUNCTION + // ============================================================================= + const getDisplayValue = (): string => { + const { label, value } = selectedItems[0]; + + if (selectedItems.length > 1) { + return `${selectedItems.length} selected`; + } else if (valueToStringFunction) { + return valueToStringFunction(value) || value.toString(); + } else { + return label; + } + }; + + const updateSelectedItemFromKey = ( + options: CombinedOptionProps[], + keyPaths: string[][] + ) => { + const findSelectedItem = ( + items: CombinedOptionProps[], + keyPaths: string[] + ): CombinedOptionProps | undefined => { + const [currentKey, ...nextKeyPath] = keyPaths; + + if (isEmpty(items) || !currentKey) { + return undefined; + } + + const item = items.find((item) => item.key === currentKey); + + if (!item || !nextKeyPath.length) { + return item; + } + + return findSelectedItem(item.subItems, nextKeyPath); + }; + + const selectedItems = []; + for (let i = 0; i < selectedKeyPaths.length; i++) { + const item = findSelectedItem(options, keyPaths[i]); + selectedItems.push(item); + } + + // TODO: condition check for nested array + if (selectedItems) { + setSelectedItems(selectedItems); + } else { + setSelectedItems(undefined); + } + }; + + const truncateValue = (value: string) => { + if (optionTruncationType === "middle") { + let widthOfElement = 0; + if (labelContainerRef && labelContainerRef.current) { + widthOfElement = + labelContainerRef.current.getBoundingClientRect().width; + } + return StringHelper.truncateOneLine(value, widthOfElement, 120, 6); + } + + return value; + }; + + const triggerOptionDisplayCallback = (show: boolean) => { + if (!show && onHideOptions) { + onHideOptions(); + } + + if (show && onShowOptions) { + onShowOptions(); + } + }; + + // ============================================================================= + // RENDER FUNCTION + // ============================================================================= + const renderLabel = () => { + if (isEmpty(selectedItems)) { + return ( + + {placeholder} + + ); + } else { + return ( + + {truncateValue(getDisplayValue())} + + ); + } + }; + + const renderSelectorContent = () => ( + <> + + {renderLabel()} + + {!readOnly && ( + + + + )} + + ); + + const renderOptionList = () => { + if ((options && options.length > 0) || onRetry) { + return ( + + ); + } + + return null; + }; + + return ( + + + {renderSelectorContent()} + + {showOptions && } + {renderOptionList()} + + ); +}; From c9366d333bab671788ea878241d1bbd09c6cf35a Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 7 Aug 2023 15:04:21 +0800 Subject: [PATCH 07/47] [BOOKINGSG-4362][WK] update selectedItem for multi select --- .../input-nested-multi-select.tsx | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 82cb85cd9..2c8855e12 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -16,11 +16,15 @@ import { StringHelper } from "../util"; import { InputNestedMultiSelectProps } from "./types"; import { CombinedOptionProps } from "../input-nested-select"; -interface SelectedItemType { +interface SelectedItemType extends BaseSelectedItem { label: string; value: V1 | V2 | V3; } +type BaseSelectedItem = { + keyPath: string[]; +}; + export const InputNestedMultiSelect = ({ placeholder = "Select", options, @@ -98,15 +102,18 @@ export const InputNestedMultiSelect = ({ setSelectedKeyPaths([...selectedKeyPaths, keyPath]); setSelectedItems([...selectedItems, item]); } else { - const resultKeyPaths = selectedKeyPaths.filter( + const filteredKeyPaths = selectedKeyPaths.filter( (selectedKeyPath) => JSON.stringify(selectedKeyPath) !== JSON.stringify(keyPath) ); + const filteredSelectedItems = selectedItems.filter( + (selectItem) => + JSON.stringify(selectItem.keyPath) !== + JSON.stringify(keyPath) + ); - setSelectedKeyPaths(resultKeyPaths); - // remove item, please - // setSelectedItems({ label, value }); - setSelectedItems([...selectedItems, item]); + setSelectedKeyPaths(filteredKeyPaths); + setSelectedItems(filteredSelectedItems); } setShowOptions(false); @@ -156,10 +163,12 @@ export const InputNestedMultiSelect = ({ options: CombinedOptionProps[], keyPaths: string[][] ) => { + let count = 0; + const findSelectedItem = ( items: CombinedOptionProps[], keyPaths: string[] - ): CombinedOptionProps | undefined => { + ): (CombinedOptionProps & BaseSelectedItem) | undefined => { const [currentKey, ...nextKeyPath] = keyPaths; if (isEmpty(items) || !currentKey) { @@ -169,7 +178,12 @@ export const InputNestedMultiSelect = ({ const item = items.find((item) => item.key === currentKey); if (!item || !nextKeyPath.length) { - return item; + const result = { + ...item, + keyPath: selectedKeyPaths[count], + }; + count = count + 1; + return result; } return findSelectedItem(item.subItems, nextKeyPath); @@ -181,12 +195,7 @@ export const InputNestedMultiSelect = ({ selectedItems.push(item); } - // TODO: condition check for nested array - if (selectedItems) { - setSelectedItems(selectedItems); - } else { - setSelectedItems(undefined); - } + setSelectedItems(selectedItems); }; const truncateValue = (value: string) => { From dcd1c4c524330298ebefe3c2e6c02067f56fd557 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 10 Aug 2023 10:34:04 +0800 Subject: [PATCH 08/47] [BOOKINGSG-4362][WK] handle add/remove in multiple nested --- .../input-nested-multi-select.tsx | 56 ++++++++++++------- src/input-nested-multi-select/types.ts | 2 +- .../nested-dropdown-list-helper.ts | 7 ++- .../nested-dropdown-list.tsx | 4 +- src/shared/nested-dropdown-list/types.ts | 2 +- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 2c8855e12..51e45a5b5 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -91,29 +91,39 @@ export const InputNestedMultiSelect = ({ const handleListItemClick = ( item: CombinedFormattedOptionProps ) => { - const { keyPath, value } = item; - + const { keyPath } = item; const isKeyPathExists = selectedKeyPaths.some( - (selectedKeyPath) => - JSON.stringify(selectedKeyPath) === JSON.stringify(keyPath) + (key) => JSON.stringify(key) === JSON.stringify(keyPath) ); + let resultSelectedKeyPaths = []; + let resultSelectedItems = []; if (!isKeyPathExists) { - setSelectedKeyPaths([...selectedKeyPaths, keyPath]); + // Add + resultSelectedKeyPaths = [...selectedKeyPaths, keyPath]; + resultSelectedItems = [...selectedItems, item]; + + setSelectedKeyPaths(resultSelectedKeyPaths); setSelectedItems([...selectedItems, item]); } else { - const filteredKeyPaths = selectedKeyPaths.filter( - (selectedKeyPath) => - JSON.stringify(selectedKeyPath) !== JSON.stringify(keyPath) - ); - const filteredSelectedItems = selectedItems.filter( - (selectItem) => - JSON.stringify(selectItem.keyPath) !== - JSON.stringify(keyPath) + // Remove + resultSelectedKeyPaths = selectedKeyPaths.filter( + (key) => JSON.stringify(key) !== JSON.stringify(keyPath) ); - - setSelectedKeyPaths(filteredKeyPaths); - setSelectedItems(filteredSelectedItems); + resultSelectedItems = selectedItems + .map(({ keyPath: key, label, value }) => { + if (JSON.stringify(key) !== JSON.stringify(keyPath)) { + return { + label, + value, + keyPath: key, + }; + } + }) + .filter(Boolean); + + setSelectedKeyPaths(resultSelectedKeyPaths); + setSelectedItems(resultSelectedItems); } setShowOptions(false); @@ -124,7 +134,8 @@ export const InputNestedMultiSelect = ({ } if (onSelectOptions) { - onSelectOptions(keyPath, value); + const returnValue = resultSelectedItems.map((item) => item.value); + onSelectOptions(resultSelectedKeyPaths, returnValue); } }; @@ -191,8 +202,12 @@ export const InputNestedMultiSelect = ({ const selectedItems = []; for (let i = 0; i < selectedKeyPaths.length; i++) { - const item = findSelectedItem(options, keyPaths[i]); - selectedItems.push(item); + const { value, label, keyPath } = findSelectedItem( + options, + keyPaths[i] + ); + + selectedItems.push({ value, label, keyPath }); } setSelectedItems(selectedItems); @@ -258,11 +273,12 @@ export const InputNestedMultiSelect = ({ return ( /** Specifies key path to select particular option label */ selectedKeyPaths?: string[][] | undefined; onSelectOptions?: - | ((keyPath: string[], value: V1 | V2 | V3) => void) + | ((keyPaths: string[][], values: Array) => void) | undefined; } diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 01ca8d7fc..8a33d3073 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -1,5 +1,9 @@ import { produce } from "immer"; -import { CombinedFormattedOptionProps, FormattedOption } from "./types"; +import { + CombinedFormattedOptionProps, + FormattedOption, + Variant, +} from "./types"; export type FormattedOptionMap = Map< string, @@ -8,6 +12,7 @@ export type FormattedOptionMap = Map< export namespace NestedDropdownListHelper { export const getInitialDropdown = ( + variant: Variant, currentItems: FormattedOptionMap, selectedKeyPaths: string[][] ) => { diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 589add225..0edc2328a 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -46,6 +46,7 @@ export const NestedDropdownList = ({ selectableCategory, itemsLoadState = "success", itemTruncationType = "end", + variant = "single", onBlur, onDismiss, onRetry, @@ -371,6 +372,7 @@ export const NestedDropdownList = ({ // otherwise expand the first selected item or first subitem tree const list = NestedDropdownListHelper.getInitialDropdown( + variant, currentItems, selectedKeyPaths ); @@ -414,6 +416,7 @@ export const NestedDropdownList = ({ selectableCategory={selectableCategory} searchValue={searchValue} itemTruncationType={itemTruncationType} + variant={variant} visible={visible} onBlur={handleBlur} onExpand={handleExpand} @@ -513,7 +516,6 @@ export const NestedDropdownList = ({ TODO: 3. renderSelectAll 16. renderBottomCta - 18. bold matched character 19. middle truncation */ diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index 30dcbf861..8c2088cf2 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -3,7 +3,7 @@ import { L1OptionProps } from "../../input-nested-select/types"; export type TruncateType = "middle" | "end"; export type ItemsLoadStateType = "loading" | "fail" | "success"; export type Mode = "default" | "expand" | "collapse"; - +export type Variant = "single" | "multi"; export interface DropdownStyleProps { listStyleWidth?: string | undefined; } From 99f886ae4983876a9548d96dba1a74d596f6c82d Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 15 Aug 2023 12:19:35 +0800 Subject: [PATCH 09/47] [BOOKINGSG-4362][WK] update stories --- .../form-nested-multi-select.stories.mdx | 58 ++++++++++++++++++- .../form-nested-multi-select/props-table.tsx | 12 ++-- .../form-nested-select/nested-data-list.ts | 2 + 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx b/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx index 8c0a32b08..ad26c9d54 100644 --- a/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx +++ b/stories/form/form-nested-multi-select/form-nested-multi-select.stories.mdx @@ -36,17 +36,32 @@ import { Form } from "@lifesg/react-design-system/form"; { + onSelectOptions={(keyPaths, values) => { setSelectedKeyPaths(keyPaths); }} /> + + + ); @@ -54,12 +69,49 @@ import { Form } from "@lifesg/react-design-system/form"; +Specifying mode + + + + + + + + + + + + +With search capabilities + + + + + + + + + + + Using the field as a standalone In the case that you require the nested select field as a standalone, you can do this. ```tsx -import { InputNestedSelect } from "@lifesg/react-design-system/input-nested-select"; +import { InputNestedMultiSelect } from "@lifesg/react-design-system/input-nested-multi-select"; ``` diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx index 5070620b1..94b6b212c 100644 --- a/stories/form/form-nested-multi-select/props-table.tsx +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -14,9 +14,9 @@ const DATA: ApiTableSectionProps[] = [ propTypes: ["L1OptionProps[]"], }, { - name: "selectedKeyPath", - description: "The key path of the selected option", - propTypes: ["string[]"], + name: "selectedKeyPaths", + description: "The key path of the selected options", + propTypes: ["string[][]"], }, { name: "placeholder", @@ -115,9 +115,11 @@ const DATA: ApiTableSectionProps[] = [ propTypes: ["string"], }, { - name: "onSelectOption", + name: "onSelectOptions", description: "Called when an option is selected", - propTypes: ["(keyPath: string[], value: V1 | V2 | V3) => void"], + propTypes: [ + "(keyPaths: string[][], value: Array) => void", + ], }, { name: "onShowOptions", diff --git a/stories/form/form-nested-select/nested-data-list.ts b/stories/form/form-nested-select/nested-data-list.ts index 6b6d6e553..822ba9d29 100644 --- a/stories/form/form-nested-select/nested-data-list.ts +++ b/stories/form/form-nested-select/nested-data-list.ts @@ -90,6 +90,7 @@ export const options = [ { label: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sollicitudin dolor ut est rutrum vulputate. Maecenas lacinia viverra metus", value: { + id: 510, name: "Long sub category a", }, key: "510", @@ -97,6 +98,7 @@ export const options = [ { label: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tempor varius elit nec iaculis. Sed sed mauris iaculis, pretium dui vel, lacinia est.", value: { + id: 23, name: "Long item a", }, key: "23", From 7c85145bdc74afb9f26edfdb8a2c163e6698abab Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 15 Aug 2023 12:47:08 +0800 Subject: [PATCH 10/47] [BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive --- .../input-nested-multi-select.tsx | 162 +++++++++++----- .../nested-dropdown-list/list-item.styles.tsx | 64 +++++-- src/shared/nested-dropdown-list/list-item.tsx | 115 ++++++++--- .../nested-dropdown-list-helper.ts | 157 ++++++++++++++- .../nested-dropdown-list.styles.tsx | 14 +- .../nested-dropdown-list.tsx | 178 +++++++++++++----- src/shared/nested-dropdown-list/types.ts | 18 +- 7 files changed, 561 insertions(+), 147 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 51e45a5b5..0ec7510f8 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useRef, useState } from "react"; import isEmpty from "lodash/isEmpty"; import { NestedDropdownList } from "../shared/nested-dropdown-list/nested-dropdown-list"; import { DropdownWrapper } from "../shared/dropdown-wrapper"; -import { CombinedFormattedOptionProps } from "../shared/nested-dropdown-list/types"; +import { + CombinedFormattedOptionProps, + SelectedItem, +} from "../shared/nested-dropdown-list/types"; import { Divider, IconContainer, @@ -16,15 +19,6 @@ import { StringHelper } from "../util"; import { InputNestedMultiSelectProps } from "./types"; import { CombinedOptionProps } from "../input-nested-select"; -interface SelectedItemType extends BaseSelectedItem { - label: string; - value: V1 | V2 | V3; -} - -type BaseSelectedItem = { - keyPath: string[]; -}; - export const InputNestedMultiSelect = ({ placeholder = "Select", options, @@ -54,10 +48,11 @@ export const InputNestedMultiSelect = ({ // CONST, STATE // ============================================================================= const [selectedKeyPaths, setSelectedKeyPaths] = useState( - _selectedKeyPaths || [] + _selectedKeyPaths || [[]] ); - const [selectedItems, setSelectedItems] = - useState[]>(); + const [selectedItems, setSelectedItems] = useState< + SelectedItem[] + >([]); const [showOptions, setShowOptions] = useState(false); @@ -68,7 +63,7 @@ export const InputNestedMultiSelect = ({ // EFFECTS // ============================================================================= useEffect(() => { - const newKeyPath = _selectedKeyPaths || []; + const newKeyPath = _selectedKeyPaths || [[]]; setSelectedKeyPaths(newKeyPath); updateSelectedItemFromKey(options, newKeyPath); @@ -88,25 +83,20 @@ export const InputNestedMultiSelect = ({ triggerOptionDisplayCallback(!showOptions); }; - const handleListItemClick = ( + const handleSelectItem = ( item: CombinedFormattedOptionProps ) => { - const { keyPath } = item; - const isKeyPathExists = selectedKeyPaths.some( + const { value, label, keyPath } = item; + const isRemove = selectedKeyPaths.some( (key) => JSON.stringify(key) === JSON.stringify(keyPath) ); - let resultSelectedKeyPaths = []; - let resultSelectedItems = []; + let resultSelectedKeyPaths: string[][] = []; + let resultSelectedItems: SelectedItem[] = []; - if (!isKeyPathExists) { - // Add + if (!isRemove) { resultSelectedKeyPaths = [...selectedKeyPaths, keyPath]; - resultSelectedItems = [...selectedItems, item]; - - setSelectedKeyPaths(resultSelectedKeyPaths); - setSelectedItems([...selectedItems, item]); + resultSelectedItems = [...selectedItems, { label, keyPath, value }]; } else { - // Remove resultSelectedKeyPaths = selectedKeyPaths.filter( (key) => JSON.stringify(key) !== JSON.stringify(keyPath) ); @@ -114,28 +104,89 @@ export const InputNestedMultiSelect = ({ .map(({ keyPath: key, label, value }) => { if (JSON.stringify(key) !== JSON.stringify(keyPath)) { return { - label, value, + label, keyPath: key, }; } }) .filter(Boolean); - - setSelectedKeyPaths(resultSelectedKeyPaths); - setSelectedItems(resultSelectedItems); } - setShowOptions(false); - triggerOptionDisplayCallback(false); + setSelectedKeyPaths(resultSelectedKeyPaths); + setSelectedItems(resultSelectedItems); + triggerOptionDisplayCallback(false); if (selectorRef.current) { selectorRef.current.focus(); } - if (onSelectOptions) { - const returnValue = resultSelectedItems.map((item) => item.value); - onSelectOptions(resultSelectedKeyPaths, returnValue); + performOnSelectOptions(resultSelectedKeyPaths, resultSelectedItems); + }; + + const handleSelectItems = ( + items: SelectedItem[], + keyPaths: string[][] + ) => { + const isRemove = keyPaths.every((keyPath) => + selectedKeyPaths.some( + (key) => JSON.stringify(keyPath) === JSON.stringify(key) + ) + ); + + let resultSelectedKeyPaths: string[][] = []; + let resultSelectedItems: SelectedItem[] = []; + + if (!isRemove) { + resultSelectedKeyPaths = [ + ...new Map( + [...selectedKeyPaths, ...keyPaths].map((k) => [ + k.join("-"), + k, + ]) + ).values(), + ]; + resultSelectedItems = [ + ...new Map( + [...selectedItems, ...items].map((i) => [ + i.keyPath.join("-"), + i, + ]) + ).values(), + ]; + } else { + resultSelectedKeyPaths = selectedKeyPaths.filter((selectedKey) => + keyPaths.every( + (key) => JSON.stringify(selectedKey) !== JSON.stringify(key) + ) + ); + resultSelectedItems = selectedItems.filter((selectedItem) => + items.every( + (item) => + JSON.stringify(selectedItem.keyPath) !== + JSON.stringify(item.keyPath) + ) + ); + } + + setSelectedItems(resultSelectedItems); + setSelectedKeyPaths(resultSelectedKeyPaths); + + performOnSelectOptions(resultSelectedKeyPaths, resultSelectedItems); + }; + + const handleSelectAll = ( + keyPaths: string[][], + items: SelectedItem[] + ) => { + if (keyPaths && keyPaths.length > 0) { + setSelectedKeyPaths(keyPaths); + setSelectedItems(items); + performOnSelectOptions(keyPaths, items); + } else { + setSelectedKeyPaths([[]]); + setSelectedItems([]); + performOnSelectOptions(); } }; @@ -158,6 +209,16 @@ export const InputNestedMultiSelect = ({ // ============================================================================= // HELPER FUNCTION // ============================================================================= + const performOnSelectOptions = ( + keyPaths: string[][] = [[]], + items: SelectedItem[] = [] + ) => { + if (onSelectOptions) { + const returnValue = items.map((item) => item.value); + onSelectOptions(keyPaths, returnValue); + } + }; + const getDisplayValue = (): string => { const { label, value } = selectedItems[0]; @@ -179,7 +240,7 @@ export const InputNestedMultiSelect = ({ const findSelectedItem = ( items: CombinedOptionProps[], keyPaths: string[] - ): (CombinedOptionProps & BaseSelectedItem) | undefined => { + ): SelectedItem | undefined => { const [currentKey, ...nextKeyPath] = keyPaths; if (isEmpty(items) || !currentKey) { @@ -187,27 +248,32 @@ export const InputNestedMultiSelect = ({ } const item = items.find((item) => item.key === currentKey); + const { label, value, subItems } = item; if (!item || !nextKeyPath.length) { const result = { - ...item, + label, + value, keyPath: selectedKeyPaths[count], }; count = count + 1; return result; } - return findSelectedItem(item.subItems, nextKeyPath); + return findSelectedItem(subItems, nextKeyPath); }; const selectedItems = []; - for (let i = 0; i < selectedKeyPaths.length; i++) { - const { value, label, keyPath } = findSelectedItem( - options, - keyPaths[i] - ); - selectedItems.push({ value, label, keyPath }); + for (let i = 0; i < selectedKeyPaths.length; i++) { + const item = findSelectedItem(options, keyPaths[i]); + if (!item) break; + + selectedItems.push({ + value: item.value, + label: item.label, + keyPath: item.keyPath, + }); } setSelectedItems(selectedItems); @@ -273,19 +339,21 @@ export const InputNestedMultiSelect = ({ return ( diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index d24194a07..635542dab 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -1,5 +1,6 @@ import styled, { css } from "styled-components"; import { Color } from "../../color"; +import { Checkbox } from "../../checkbox"; import { TextStyleHelper } from "../../text"; import { TruncateType } from "./types"; import { IconButton } from "../../icon-button"; @@ -12,17 +13,23 @@ import { TriangleForwardFillIcon } from "@lifesg/react-icons/triangle-forward-fi interface ListProps { $expanded: boolean; + $level_3: boolean; + $multiSelect: boolean; } interface ListItemSelectorProps { $selected: boolean; - $level_3: boolean; + $multiSelect: boolean; } interface LabelProps { $truncateType?: TruncateType; } +interface CheckboxInputProps { + $type: "category" | "label"; +} + interface ArrowButtonProps extends Pick {} // ============================================================================= // STYLING @@ -36,6 +43,7 @@ export const Category = styled.div` `; export const ListItemSelector = styled.button` + display: flex; width: 100%; border: none; cursor: pointer; @@ -43,6 +51,7 @@ export const ListItemSelector = styled.button` text-align: left; padding: 0.5rem; min-height: 2.625rem; + cursor: pointer; :hover, :visited, @@ -52,20 +61,13 @@ export const ListItemSelector = styled.button` } :hover { - background-color: ${Color.Accent.Light[5]}; + background-color: ${(props) => + props.$multiSelect ? "transparent" : Color.Accent.Light[5]}; } ${(props) => { - if (props.$level_3) { - return css` - margin-left: 0.5rem; - width: calc(100% - 0.5rem); - `; - } - }} - - ${(props) => { - if (props.$selected) { + const { $selected, $multiSelect } = props; + if (!$multiSelect && $selected) { return css` background: ${Color.Accent.Light[5]}; `; @@ -118,6 +120,28 @@ export const TruncateSecondLine = styled.div` text-align: right; `; +export const ButtonSection = styled.div` + display: flex; +`; + +export const CheckboxInput = styled(Checkbox)` + min-width: 1.5rem; + max-width: 1.5rem; + + ${(props) => { + switch (props.$type) { + case "category": + return css` + margin-left: 0.5rem; + `; + case "label": + return css` + margin-right: 0.5rem; + `; + } + }}; +`; + export const ArrowButton = styled(IconButton)` border: none; background: transparent; @@ -168,6 +192,20 @@ export const Title = styled.button` export const List = styled.ul` display: ${(props) => (props.$expanded ? "flex" : "none")}; flex-direction: column; - cursor: pointer; margin-left: 2.125rem; + + ${(props) => { + const { $level_3, $multiSelect } = props; + if ($level_3) { + if ($multiSelect) { + return css` + margin-left: 4.25rem; + `; + } else { + return css` + margin-left: 2.625rem; + `; + } + } + }} `; diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index 098ca5263..b41761662 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -4,7 +4,9 @@ import { StringHelper } from "../../util"; import { ArrowButton, Bold, + ButtonSection, Category, + CheckboxInput, Label, List, ListItemSelector, @@ -21,24 +23,29 @@ interface ListItemProps { selectableCategory?: boolean | undefined; searchValue: string | undefined; itemTruncationType?: TruncateType | undefined; + multiSelect: boolean; visible: boolean; onBlur: () => void; onExpand: (parentKeys: string[]) => void; onRef: (ref: HTMLButtonElement, keyPaths: string[]) => void; onSelect: (item: CombinedFormattedOptionProps) => void; + onSelectCategory: (item: CombinedFormattedOptionProps) => void; } export const ListItem = ({ item, + // selectedKeyPath, selectedKeyPaths, selectableCategory, searchValue, itemTruncationType, + multiSelect, visible, onBlur, onExpand, onRef, onSelect, + onSelectCategory, }: ListItemProps): JSX.Element => { // ============================================================================= // CONST, REF, STATE @@ -52,11 +59,17 @@ export const ListItem = ({ event.preventDefault(); onExpand(item.keyPath); }; + const handleSelect = (event: React.MouseEvent) => { event.preventDefault(); onSelect(item); }; + const handleSelectParent = (event: React.ChangeEvent) => { + event.preventDefault(); + onSelectCategory(item); + }; + const handleBlur = () => { if (onBlur) { onBlur(); @@ -112,7 +125,7 @@ export const ListItem = ({ const endIndex = startIndex + searchTerm.length; if (startIndex == -1) { - return <>{item.label}; + return <>{label}; } return ( @@ -128,7 +141,11 @@ export const ListItem = ({ const nextSubItems = item.subItems.values(); return ( - + {[...nextSubItems].map((item) => ( ({ selectableCategory={selectableCategory} searchValue={searchValue} itemTruncationType={itemTruncationType} + multiSelect={multiSelect} visible={visible} onBlur={onBlur} onExpand={onExpand} onRef={onRef} onSelect={onSelect} + onSelectCategory={onSelectCategory} /> ))} ); }; - const renderTitleItem = () => { - if (selectableCategory) { - return ( - - onRef(ref, item.keyPath)} - onClick={handleExpand} - $expanded={item.expanded} - aria-expanded={item.expanded} - > - - - - <span>{item.label}</span> - - - ); - } - + const renderCategoryIcon = () => { return ( - + onRef(ref, item.keyPath)} $expanded={item.expanded} aria-expanded={item.expanded} + onClick={handleExpand} > - + {multiSelect && ( + <CheckboxInput + displaySize="small" + $type="category" + checked={item.checked} + onChange={handleSelectParent} + /> + )} + </ButtonSection> + ); + }; + + const renderCategoryItem = () => { + let categoryProps = {}; + let titleProps = {}; + + if (selectableCategory) { + titleProps = { + onClick: handleSelect, + }; + } + + if (multiSelect) { + titleProps = { + onClick: handleExpand, + tabIndex: -1, + }; + } else { + categoryProps = { + onClick: handleExpand, + }; + } + + return ( + <Category {...categoryProps}> + {renderCategoryIcon()} + <Title {...titleProps}> <span>{item.label}</span> ); }; + const renderLabel = () => { + return ( + <> + {multiSelect && ( + + )} + + + ); + }; + if (!item.subItems) { return (
  • @@ -191,16 +249,11 @@ export const ListItem = ({ type="button" tabIndex={visible ? 0 : -1} $selected={checkListItemSelected(item.keyPath)} - $level_3={item.keyPath.length === 3} + $multiSelect={multiSelect} onBlur={handleBlur} onClick={handleSelect} > - + {renderLabel()}
  • ); @@ -208,7 +261,7 @@ export const ListItem = ({ return (
  • - {renderTitleItem()} + {renderCategoryItem()} {renderListItem()}
  • ); diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 8a33d3073..399207fd4 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -2,17 +2,64 @@ import { produce } from "immer"; import { CombinedFormattedOptionProps, FormattedOption, - Variant, + Mode, + SelectedItem, } from "./types"; +import { CombinedOptionProps } from "../../input-nested-select"; export type FormattedOptionMap = Map< string, FormattedOption >; +interface UpdateSelectedAllType + extends GetAllKeyPathsAndItemsType { + list: FormattedOptionMap; +} + +interface GetAllKeyPathsAndItemsType { + keyPaths: string[][]; + items: SelectedItem[]; +} + export namespace NestedDropdownListHelper { + export const getInitialItems = ( + list: CombinedOptionProps[], + parentKeys: string[], + mode: Mode + ): FormattedOptionMap => { + const formatted = ( + options: CombinedOptionProps[], + parentKeys: string[] + ): FormattedOptionMap => { + return options.reduce((result, option) => { + const { key, label, value, subItems } = option; + const stringKey = key.toString(); + + const keyPath = [...parentKeys, stringKey]; + + const item = { + label, + value, + expanded: mode === "expand", + isSearchTerm: false, + checked: false, + keyPath, + subItems: subItems + ? formatted(subItems, keyPath) + : undefined, + }; + + result.set(stringKey, item); + + return result; + }, new Map()); + }; + + return formatted(list, parentKeys); + }; + export const getInitialDropdown = ( - variant: Variant, currentItems: FormattedOptionMap, selectedKeyPaths: string[][] ) => { @@ -41,6 +88,40 @@ export namespace NestedDropdownListHelper { return list; }; + export const updateSelectedAll = ( + currentItems: FormattedOptionMap, + isSelectedAll: boolean + ): UpdateSelectedAllType => { + let list = currentItems; + let keyPaths: string[][] = []; + let items: SelectedItem[] = []; + + if (isSelectedAll) { + const { keyPaths: _keyPaths, items: _items } = + getAllKeyPathsAndItems(list); + + keyPaths = _keyPaths; + items = _items; + + list = produce( + currentItems, + (draft: FormattedOptionMap) => { + keyPaths.forEach((key) => { + const parentKey = key.slice(0, -1); + const item = getItemAtKeyPath(draft, parentKey); + item.expanded = true; + }); + } + ); + } + + return { + keyPaths, + items, + list, + }; + }; + export const getVisibleKeyPaths = ( list: Map> ): string[][] => { @@ -82,6 +163,45 @@ export namespace NestedDropdownListHelper { return item; }; + + export const updateCategoryChecked = ( + list: FormattedOptionMap, + selectedKeyPaths: string[][] + ) => { + const resetList = produce( + list, + (draft: Map>) => { + const resetChecked = ( + items: Map> + ) => { + if (!items || !items.size) return; + for (const item of items.values()) { + item.checked = false; + if (item.subItems) resetChecked(item.subItems); + } + }; + resetChecked(draft); + } + ); + + return produce(resetList, (draft: FormattedOptionMap) => { + let targetKey: string[] = []; + selectedKeyPaths.forEach((keyPathArr) => { + targetKey = []; + const relevantKeys = keyPathArr.slice(0, -1); + relevantKeys.forEach((key) => { + targetKey.push(key); + const item = NestedDropdownListHelper.getItemAtKeyPath( + draft, + targetKey + ); + if (item) { + item.checked = true; + } + }); + }); + }); + }; } // ============================================================================= @@ -99,3 +219,36 @@ const getInitialSubItem = ( const value = list.values().next().value; return value.keyPath; }; + +const getAllKeyPathsAndItems = ( + list: FormattedOptionMap +) => { + const keyPaths: string[][] = []; + const items: SelectedItem[] = []; + + const getKeyPath = ( + _items: Map> + ): GetAllKeyPathsAndItemsType => { + if (!_items || !_items.size) return; + + for (const item of _items.values()) { + const { keyPath, label, value } = item; + if (item.subItems && item.subItems.size) { + getKeyPath(item.subItems); + } else { + keyPaths.push(item.keyPath); + items.push({ + label, + value, + keyPath, + }); + } + } + }; + + getKeyPath(list); + return { + keyPaths, + items, + }; +}; diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.styles.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.styles.tsx index b4e0a09bd..6ec51ddbe 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.styles.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.styles.tsx @@ -65,17 +65,19 @@ export const LabelIcon = styled(ExclamationCircleFillIcon)` color: ${Color.Validation.Red.Icon}; `; +export const SelectAllContainer = styled.div` + width: 100%; + display: flex; + justify-content: flex-end; + padding: 0.5rem 0; +`; + export const DropdownCommonButton = styled.button` ${TextStyleHelper.getTextStyle("Body", "semibold")} + color: ${Color.Primary}; background-color: transparent; - background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; outline: none; - ${(props) => { - return ` - color: ${Color.Primary(props)}; - `; - }} `; diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 0edc2328a..4d58cffae 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -3,10 +3,14 @@ import get from "lodash/get"; import { useEffect, useMemo, useRef, useState } from "react"; import { useSpring } from "react-spring"; import { Spinner } from "../../button/button.style"; -import { CombinedOptionProps } from "../../input-nested-select"; import { useEventListener } from "../../util/use-event-listener"; import { ListItem } from "./list-item"; import { DropdownSearch } from "../dropdown-list/dropdown-search"; +import { + FormattedOptionMap, + NestedDropdownListHelper, +} from "./nested-dropdown-list-helper"; +import { CombinedFormattedOptionProps, NestedDropdownListProps } from "./types"; import { Container, DropdownCommonButton, @@ -14,12 +18,8 @@ import { List, ResultStateContainer, ResultStateText, + SelectAllContainer, } from "./nested-dropdown-list.styles"; -import { CombinedFormattedOptionProps, NestedDropdownListProps } from "./types"; -import { - FormattedOptionMap, - NestedDropdownListHelper, -} from "./nested-dropdown-list-helper"; enableMapSet(); @@ -42,16 +42,22 @@ export const NestedDropdownList = ({ searchPlaceholder = "Search", visible, mode = "default", +<<<<<<< HEAD +======= + multiSelect, + selectedKeyPath, +>>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) selectedKeyPaths, selectableCategory, itemsLoadState = "success", itemTruncationType = "end", - variant = "single", onBlur, onDismiss, + onSelectAll, onRetry, onSearch, onSelectItem, + onSelectItems, ...otherProps }: NestedDropdownListProps): JSX.Element => { // ============================================================================= @@ -59,36 +65,7 @@ export const NestedDropdownList = ({ // ============================================================================= const initialItems = useMemo((): FormattedOptionMap => { if (!_listItems || !_listItems.length) return new Map([]); - - const formatted = ( - options: CombinedOptionProps[], - parentKeys: string[] - ): FormattedOptionMap => { - return options.reduce((result, option) => { - const { key, label, value, subItems } = option; - const stringKey = key.toString(); - - const keyPath = [...parentKeys, stringKey]; - - const item = { - label, - value, - expanded: mode === "expand", - selected: false, - isSearchTerm: false, - keyPath, - subItems: subItems - ? formatted(subItems, keyPath) - : undefined, - }; - - result.set(stringKey, item); - - return result; - }, new Map()); - }; - - return formatted(_listItems, []); + return NestedDropdownListHelper.getInitialItems(_listItems, [], mode); }, [_listItems]); const [searchValue, setSearchValue] = useState(""); @@ -129,6 +106,16 @@ export const NestedDropdownList = ({ listItemRefs.current[target[0]].ref.focus(); } + if (multiSelect) { + const multiSelectList = + NestedDropdownListHelper.updateCategoryChecked( + list, + selectedKeyPaths + ); + + setCurrentItems(multiSelectList); + } + // Give some time for the custom call-to-action to be rendered setTimeout(() => { setContentHeight(getContentHeight()); @@ -154,6 +141,18 @@ export const NestedDropdownList = ({ filterAndUpdateList(searchValue); }, [searchValue]); + useEffect(() => { + if (visible && multiSelect) { + const targetList = isSearch ? filteredItems : currentItems; + const list = NestedDropdownListHelper.updateCategoryChecked( + targetList, + selectedKeyPaths + ); + + isSearch ? setFilteredItems(list) : setCurrentItems(list); + } + }, [selectedKeyPaths, isSearch]); + useEventListener("keydown", handleKeyboardPress, "document"); // ============================================================================= @@ -161,11 +160,18 @@ export const NestedDropdownList = ({ // ============================================================================= const handleSelect = (item: CombinedFormattedOptionProps) => { - onSelectItem(item); + const { label, keyPath, value } = item; + onSelectItem({ label, keyPath, value }); }; - const handleExpand = (keyPath: string[]) => { + const handleSelectCategory = ( + item: CombinedFormattedOptionProps + ) => { const targetList = isSearch ? filteredItems : currentItems; + const { keyPath } = item; + + const { selectedItems, keyPaths } = + NestedDropdownListHelper.getSubItemKeyPathAndItem(item); const list = produce( targetList, @@ -174,12 +180,18 @@ export const NestedDropdownList = ({ draft, keyPath ); - item.expanded = !item.expanded; + + item.expanded = true; + + if (item.subItems && item.subItems.size) { + for (const nextItem of item.subItems.values()) { + nextItem.expanded = true; + } + } } ); - const keyPaths = NestedDropdownListHelper.getVisibleKeyPaths(list); - setVisibleKeyPaths(keyPaths); + onSelectItems(selectedItems, keyPaths); if (isSearch) { setFilteredItems(list); } else { @@ -187,6 +199,40 @@ export const NestedDropdownList = ({ } }; + const handleSelectAll = () => { + const isSelectedAll = !selectedKeyPaths.flat().length || false; + + const { keyPaths, items, list } = + NestedDropdownListHelper.updateSelectedAll( + currentItems, + isSelectedAll + ); + + if (onSelectAll) { + setCurrentItems(list); + isSelectedAll ? onSelectAll(keyPaths, items) : onSelectAll([], []); + } + }; + + const handleExpand = (keyPath: string[]) => { + const targetList = isSearch ? filteredItems : currentItems; + + const list = produce( + targetList, + (draft: FormattedOptionMap) => { + const item = NestedDropdownListHelper.getItemAtKeyPath( + draft, + keyPath + ); + item.expanded = !item.expanded; + } + ); + const keyPaths = NestedDropdownListHelper.getVisibleKeyPaths(list); + + setVisibleKeyPaths(keyPaths); + isSearch ? setFilteredItems(list) : setCurrentItems(list); + }; + const handleListItemRef = ( ref: HTMLButtonElement, keyPath: string[], @@ -372,9 +418,14 @@ export const NestedDropdownList = ({ // otherwise expand the first selected item or first subitem tree const list = NestedDropdownListHelper.getInitialDropdown( - variant, currentItems, +<<<<<<< HEAD selectedKeyPaths +======= + selectedKeyPath, + selectedKeyPaths, + multiSelect +>>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) ); return list; @@ -398,6 +449,16 @@ export const NestedDropdownList = ({ setFilteredItems(filtered); resetVisbileKeyPaths(filtered); setIsSearch(isSearch); + + if (multiSelect) { + const multiSelectList = + NestedDropdownListHelper.updateCategoryChecked( + filtered, + selectedKeyPaths + ); + + setFilteredItems(multiSelectList); + } } }; @@ -412,16 +473,21 @@ export const NestedDropdownList = ({ >>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) selectedKeyPaths={selectedKeyPaths} selectableCategory={selectableCategory} searchValue={searchValue} itemTruncationType={itemTruncationType} - variant={variant} + multiSelect={multiSelect} visible={visible} onBlur={handleBlur} onExpand={handleExpand} onRef={handleListItemRef} onSelect={handleSelect} + onSelectCategory={handleSelectCategory} /> )); } @@ -446,6 +512,28 @@ export const NestedDropdownList = ({ return null; }; + const renderSelectAll = () => { + if ( + multiSelect && + initialItems.size > 0 && + !isSearch && + itemsLoadState === "success" + ) { + return ( + + + {selectedKeyPaths.flat().length === 0 + ? "Select all" + : "Unselect all"} + + + ); + } + }; + const renderNoResults = () => { if (isSearch) { if (!hideNoResultsDisplay && !filteredItems.size) { @@ -504,6 +592,7 @@ export const NestedDropdownList = ({ {...otherProps} > {renderSearchInput()} + {renderSelectAll()} {renderLoading()} {renderNoResults()} {renderTryAgain()} @@ -514,7 +603,6 @@ export const NestedDropdownList = ({ /** TODO: - 3. renderSelectAll 16. renderBottomCta 19. middle truncation */ diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index 8c2088cf2..172d2f151 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -3,13 +3,21 @@ import { L1OptionProps } from "../../input-nested-select/types"; export type TruncateType = "middle" | "end"; export type ItemsLoadStateType = "loading" | "fail" | "success"; export type Mode = "default" | "expand" | "collapse"; -export type Variant = "single" | "multi"; export interface DropdownStyleProps { listStyleWidth?: string | undefined; } +export interface SelectedItem { + label: string; + keyPath: string[]; + value: V1 | V2 | V3; +} + export interface DropdownEventHandlerProps { - onSelectItem: (item: CombinedFormattedOptionProps) => void; + onSelectItem: (item: SelectedItem) => void; + onSelectItems?: + | ((items: SelectedItem[], keyPaths: string[][]) => void) + | undefined; } export interface DropdownSearchProps { @@ -28,6 +36,7 @@ export interface NestedDropdownListProps DropdownStyleProps { listItems?: L1OptionProps[] | undefined; visible?: boolean | undefined; + multiSelect?: boolean | undefined; /** Specifies key path of selected option */ selectedKeyPaths: string[][]; /** Specifies if items are expanded or collapsed when the dropdown is opened */ @@ -43,6 +52,9 @@ export interface NestedDropdownListProps itemTruncationType?: TruncateType | undefined; onDismiss?: ((setSelectorFocus?: boolean | undefined) => void) | undefined; + onSelectAll?: + | ((keyPaths: string[][], items: SelectedItem[]) => void) + | undefined; onRetry?: (() => void) | undefined; onBlur?: (() => void) | undefined; } @@ -59,7 +71,7 @@ interface BaseFormattedOptionProps { label: string; keyPath: string[]; expanded: boolean; - selected: boolean; + checked: boolean; isSearchTerm: boolean; } From 7cc9ee921f750b755b7f3b5cc7a54ab22d96bd3d Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 16 Aug 2023 11:40:19 +0800 Subject: [PATCH 11/47] [BOOKINGSG-4362] update keyboard keyPaths once clicked on category checkbox --- .../nested-dropdown-list/nested-dropdown-list.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 4d58cffae..b16d122e9 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -191,16 +191,16 @@ export const NestedDropdownList = ({ } ); + const visibleKeyPaths = + NestedDropdownListHelper.getVisibleKeyPaths(list); + setVisibleKeyPaths(visibleKeyPaths); + isSearch ? setFilteredItems(list) : setCurrentItems(list); + onSelectItems(selectedItems, keyPaths); - if (isSearch) { - setFilteredItems(list); - } else { - setCurrentItems(list); - } }; const handleSelectAll = () => { - const isSelectedAll = !selectedKeyPaths.flat().length || false; + const isSelectedAll = !selectedKeyPaths.flat().length; const { keyPaths, items, list } = NestedDropdownListHelper.updateSelectedAll( @@ -444,11 +444,10 @@ export const NestedDropdownList = ({ setIsSearch(false); } else if (searchValue.trim().length >= 3) { listItemRefs.current = {}; - const isSearch = true; const filtered = updateSearchState(); setFilteredItems(filtered); resetVisbileKeyPaths(filtered); - setIsSearch(isSearch); + setIsSearch(true); if (multiSelect) { const multiSelectList = From 8c575c8e4541aa42aa89a00cb18528010c1da2a0 Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 16 Aug 2023 12:12:14 +0800 Subject: [PATCH 12/47] [BOOKINGSG-4362][WK] typo type in form component --- src/form/form-nested-multi-select.tsx | 4 ++-- src/form/types.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/form/form-nested-multi-select.tsx b/src/form/form-nested-multi-select.tsx index 7cce15140..1573e4f0a 100644 --- a/src/form/form-nested-multi-select.tsx +++ b/src/form/form-nested-multi-select.tsx @@ -1,6 +1,6 @@ import { InputNestedMultiSelect } from "../input-nested-multi-select"; import { FormWrapper } from "./form-wrapper"; -import { FormNestedSelectProps } from "./types"; +import { FormNestedMultiSelectProps } from "./types"; export const FormNestedMultiSelect = ({ label, @@ -9,7 +9,7 @@ export const FormNestedMultiSelect = ({ "data-error-testid": errorTestId, "data-testid": testId, ...otherProps -}: FormNestedSelectProps): JSX.Element => { +}: FormNestedMultiSelectProps): JSX.Element => { return ( extends InputNestedSelectPartialProps, BaseFormElementProps {} +export interface FormNestedMultiSelectProps + extends InputNestedMultiSelectPartialProps, + BaseFormElementProps {} + export interface FormDateInputProps extends DateInputProps, BaseFormElementProps {} From 043622575f8b75611c68cb5f74bce7770469f7d2 Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 16 Aug 2023 12:13:30 +0800 Subject: [PATCH 13/47] [BOOKINGSG-4362][WK] update form id --- src/form/form-nested-multi-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form/form-nested-multi-select.tsx b/src/form/form-nested-multi-select.tsx index 1573e4f0a..4c196ce78 100644 --- a/src/form/form-nested-multi-select.tsx +++ b/src/form/form-nested-multi-select.tsx @@ -5,7 +5,7 @@ import { FormNestedMultiSelectProps } from "./types"; export const FormNestedMultiSelect = ({ label, errorMessage, - id = "form-nested-select", + id = "form-nested-multi-select", "data-error-testid": errorTestId, "data-testid": testId, ...otherProps From bde8d82530e221d2e8a843423a264d1be74db86a Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 16 Aug 2023 12:45:46 +0800 Subject: [PATCH 14/47] [BOOKINGSG-4362][WK] full width in category label --- src/shared/nested-dropdown-list/list-item.styles.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index 635542dab..1b3e06246 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -177,6 +177,7 @@ export const Title = styled.button` background: transparent; border: none; cursor: pointer; + width: 100%; padding: 0; overflow-wrap: anywhere; From 86fea57dea7bae5266ac6ea504537ef8623f4778 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 17 Aug 2023 14:35:33 +0800 Subject: [PATCH 15/47] [BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem --- .../input-nested-multi-select.tsx | 245 ++++++++++++------ .../nested-dropdown-list.tsx | 18 +- 2 files changed, 183 insertions(+), 80 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 0ec7510f8..bf1c289d1 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -48,7 +48,7 @@ export const InputNestedMultiSelect = ({ // CONST, STATE // ============================================================================= const [selectedKeyPaths, setSelectedKeyPaths] = useState( - _selectedKeyPaths || [[]] + _selectedKeyPaths || [] ); const [selectedItems, setSelectedItems] = useState< SelectedItem[] @@ -63,7 +63,7 @@ export const InputNestedMultiSelect = ({ // EFFECTS // ============================================================================= useEffect(() => { - const newKeyPath = _selectedKeyPaths || [[]]; + const newKeyPath = _selectedKeyPaths || []; setSelectedKeyPaths(newKeyPath); updateSelectedItemFromKey(options, newKeyPath); @@ -87,30 +87,57 @@ export const InputNestedMultiSelect = ({ item: CombinedFormattedOptionProps ) => { const { value, label, keyPath } = item; - const isRemove = selectedKeyPaths.some( - (key) => JSON.stringify(key) === JSON.stringify(keyPath) - ); let resultSelectedKeyPaths: string[][] = []; let resultSelectedItems: SelectedItem[] = []; - if (!isRemove) { - resultSelectedKeyPaths = [...selectedKeyPaths, keyPath]; - resultSelectedItems = [...selectedItems, { label, keyPath, value }]; + const selectedItem = getItemAtKeyPath(keyPath); + + if (selectedItem.subItems) { + const { targetItems, targetKeyPaths } = getSubItemAndKeypath( + selectedItem, + keyPath + ); + + const isRemove = targetKeyPaths.every((keyPath) => + selectedKeyPaths.some( + (key) => JSON.stringify(keyPath) === JSON.stringify(key) + ) + ); + + if (!isRemove) { + const { keys, items } = getAddSubItems( + targetKeyPaths, + targetItems + ); + + resultSelectedKeyPaths = keys; + resultSelectedItems = items; + } else { + const { keys, items } = getRemoveSubItems( + targetKeyPaths, + targetItems + ); + + resultSelectedKeyPaths = keys; + resultSelectedItems = items; + } } else { - resultSelectedKeyPaths = selectedKeyPaths.filter( - (key) => JSON.stringify(key) !== JSON.stringify(keyPath) + const isRemove = selectedKeyPaths.some( + (key) => JSON.stringify(key) === JSON.stringify(keyPath) ); - resultSelectedItems = selectedItems - .map(({ keyPath: key, label, value }) => { - if (JSON.stringify(key) !== JSON.stringify(keyPath)) { - return { - value, - label, - keyPath: key, - }; - } - }) - .filter(Boolean); + + if (!isRemove) { + resultSelectedKeyPaths = [...selectedKeyPaths, keyPath]; + resultSelectedItems = [ + ...selectedItems, + { label, keyPath, value }, + ]; + } else { + const { keys, items } = getRemoveOption(keyPath); + + resultSelectedKeyPaths = keys; + resultSelectedItems = items; + } } setSelectedKeyPaths(resultSelectedKeyPaths); @@ -124,57 +151,6 @@ export const InputNestedMultiSelect = ({ performOnSelectOptions(resultSelectedKeyPaths, resultSelectedItems); }; - const handleSelectItems = ( - items: SelectedItem[], - keyPaths: string[][] - ) => { - const isRemove = keyPaths.every((keyPath) => - selectedKeyPaths.some( - (key) => JSON.stringify(keyPath) === JSON.stringify(key) - ) - ); - - let resultSelectedKeyPaths: string[][] = []; - let resultSelectedItems: SelectedItem[] = []; - - if (!isRemove) { - resultSelectedKeyPaths = [ - ...new Map( - [...selectedKeyPaths, ...keyPaths].map((k) => [ - k.join("-"), - k, - ]) - ).values(), - ]; - resultSelectedItems = [ - ...new Map( - [...selectedItems, ...items].map((i) => [ - i.keyPath.join("-"), - i, - ]) - ).values(), - ]; - } else { - resultSelectedKeyPaths = selectedKeyPaths.filter((selectedKey) => - keyPaths.every( - (key) => JSON.stringify(selectedKey) !== JSON.stringify(key) - ) - ); - resultSelectedItems = selectedItems.filter((selectedItem) => - items.every( - (item) => - JSON.stringify(selectedItem.keyPath) !== - JSON.stringify(item.keyPath) - ) - ); - } - - setSelectedItems(resultSelectedItems); - setSelectedKeyPaths(resultSelectedKeyPaths); - - performOnSelectOptions(resultSelectedKeyPaths, resultSelectedItems); - }; - const handleSelectAll = ( keyPaths: string[][], items: SelectedItem[] @@ -219,6 +195,75 @@ export const InputNestedMultiSelect = ({ } }; + const getAddSubItems = ( + _keyPaths: string[][], + _items: SelectedItem[] + ) => { + const keys = [ + ...new Map( + [...selectedKeyPaths, ..._keyPaths].map((k) => [k.join("-"), k]) + ).values(), + ]; + const items = [ + ...new Map( + [...selectedItems, ..._items].map((i) => [ + i.keyPath.join("-"), + i, + ]) + ).values(), + ]; + + return { + keys, + items, + }; + }; + + const getRemoveSubItems = ( + _keyPaths: string[][], + _items: SelectedItem[] + ) => { + const keys = selectedKeyPaths.filter((selectedKey) => + _keyPaths.every( + (key) => JSON.stringify(selectedKey) !== JSON.stringify(key) + ) + ); + const items = selectedItems.filter((selectedItem) => + _items.every( + (item) => + JSON.stringify(selectedItem.keyPath) !== + JSON.stringify(item.keyPath) + ) + ); + + return { + keys, + items, + }; + }; + + const getRemoveOption = (keyPath: string[]) => { + const keys = selectedKeyPaths.filter( + (key) => JSON.stringify(key) !== JSON.stringify(keyPath) + ); + const items = selectedItems + .map(({ keyPath: key, label, value }) => { + if (JSON.stringify(key) !== JSON.stringify(keyPath)) { + return { + value, + label, + keyPath: key, + }; + } + }) + .filter(Boolean); + + return { + keys, + items, + }; + }; + const getDisplayValue = (): string => { const { label, value } = selectedItems[0]; @@ -231,6 +276,59 @@ export const InputNestedMultiSelect = ({ } }; + const getItemAtKeyPath = (_keyPath: string[]) => { + const find = ( + items: CombinedOptionProps[], + keyPath: string[] + ): CombinedOptionProps => { + const [currentKey, ...nextKeyPath] = keyPath; + + if (isEmpty(items) || !currentKey) return undefined; + + const item = items.find((item) => item.key === currentKey); + + if (!item || !nextKeyPath.length) return item; + + return find(item.subItems, nextKeyPath); + }; + + const item = find(options, _keyPath); + + return item; + }; + + const getSubItemAndKeypath = ( + _item: CombinedOptionProps, + selectedKeyPath: string[] + ) => { + const targetItems: SelectedItem[] = []; + const targetKeyPaths: string[][] = []; + const parentKey = selectedKeyPath.slice(0, -1); + + const find = ( + item: CombinedOptionProps, + parentKey: string[] + ) => { + const relaventKey = [...parentKey, item.key]; + + if (!item.subItems) { + const { label, value } = item; + targetItems.push({ label, keyPath: relaventKey, value }); + targetKeyPaths.push(relaventKey); + return; + } + + item.subItems.forEach((subItem) => find(subItem, relaventKey)); + }; + + find(_item, parentKey); + + return { + targetKeyPaths, + targetItems, + }; + }; + const updateSelectedItemFromKey = ( options: CombinedOptionProps[], keyPaths: string[][] @@ -338,7 +436,7 @@ export const InputNestedMultiSelect = ({ if ((options && options.length > 0) || onRetry) { return ( ({ onDismiss={handleListDismiss} onSelectAll={handleSelectAll} onSelectItem={handleSelectItem} - onSelectItems={handleSelectItems} onSearch={onSearch} onRetry={onRetry} /> diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index b16d122e9..6c9e400f0 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -45,8 +45,11 @@ export const NestedDropdownList = ({ <<<<<<< HEAD ======= multiSelect, +<<<<<<< HEAD selectedKeyPath, >>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) +======= +>>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) selectedKeyPaths, selectableCategory, itemsLoadState = "success", @@ -57,7 +60,6 @@ export const NestedDropdownList = ({ onRetry, onSearch, onSelectItem, - onSelectItems, ...otherProps }: NestedDropdownListProps): JSX.Element => { // ============================================================================= @@ -168,10 +170,7 @@ export const NestedDropdownList = ({ item: CombinedFormattedOptionProps ) => { const targetList = isSearch ? filteredItems : currentItems; - const { keyPath } = item; - - const { selectedItems, keyPaths } = - NestedDropdownListHelper.getSubItemKeyPathAndItem(item); + const { label, keyPath, value } = item; const list = produce( targetList, @@ -196,7 +195,7 @@ export const NestedDropdownList = ({ setVisibleKeyPaths(visibleKeyPaths); isSearch ? setFilteredItems(list) : setCurrentItems(list); - onSelectItems(selectedItems, keyPaths); + onSelectItem({ label, keyPath, value }); }; const handleSelectAll = () => { @@ -419,6 +418,7 @@ export const NestedDropdownList = ({ // otherwise expand the first selected item or first subitem tree const list = NestedDropdownListHelper.getInitialDropdown( currentItems, +<<<<<<< HEAD <<<<<<< HEAD selectedKeyPaths ======= @@ -426,6 +426,9 @@ export const NestedDropdownList = ({ selectedKeyPaths, multiSelect >>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) +======= + selectedKeyPaths +>>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) ); return list; @@ -473,9 +476,12 @@ export const NestedDropdownList = ({ key={key} item={item} <<<<<<< HEAD +<<<<<<< HEAD ======= selectedKeyPath={selectedKeyPath} >>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) +======= +>>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) selectedKeyPaths={selectedKeyPaths} selectableCategory={selectableCategory} searchValue={searchValue} From d3e6fa00dd582f1ad85747712d703229fbb00c56 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 17 Aug 2023 14:36:18 +0800 Subject: [PATCH 16/47] [BOOKINGSG-4362][WK] create shared type interface --- src/input-nested-multi-select/types.ts | 18 +++++++++++++++--- src/input-nested-select/types.ts | 16 ++++++++++------ src/shared/nested-dropdown-list/types.ts | 3 --- .../form-nested-multi-select/props-table.tsx | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/input-nested-multi-select/types.ts b/src/input-nested-multi-select/types.ts index be7cef3f1..df1c787a4 100644 --- a/src/input-nested-multi-select/types.ts +++ b/src/input-nested-multi-select/types.ts @@ -1,12 +1,24 @@ -import { InputNestedSelectProps } from "../input-nested-select"; +import { + InputNestedSelectOptionsProps, + InputNestedSelectSharedProps, +} from "../input-nested-select"; +import { InputSelectSharedProps } from "../input-select"; +import { + DropdownSearchProps, + DropdownStyleProps, +} from "../shared/nested-dropdown-list/types"; // ============================================================================= // INPUT SELECT PROPS // ============================================================================= -type OmitTypes = "selectedKeyPath" | "onSelectOption" | "selectableCategory"; export interface InputNestedMultiSelectProps - extends Omit, OmitTypes> { + extends React.HTMLAttributes, + InputNestedSelectOptionsProps, + Omit, "options">, + InputNestedSelectSharedProps, + DropdownSearchProps, + DropdownStyleProps { /** Specifies key path to select particular option label */ selectedKeyPaths?: string[][] | undefined; onSelectOptions?: diff --git a/src/input-nested-select/types.ts b/src/input-nested-select/types.ts index 51f8cb6c9..0235d883d 100644 --- a/src/input-nested-select/types.ts +++ b/src/input-nested-select/types.ts @@ -8,7 +8,7 @@ import { Mode, } from "../shared/nested-dropdown-list/types"; -interface InputNestedSelectOptionsProps +export interface InputNestedSelectOptionsProps extends Omit, "options"> { options: L1OptionProps[]; } @@ -16,24 +16,28 @@ interface InputNestedSelectOptionsProps // ============================================================================= // INPUT SELECT PROPS // ============================================================================= +export interface InputNestedSelectSharedProps { + readOnly?: boolean | undefined; + /** Specifies if items are expanded or collapsed when the dropdown is opened */ + mode?: Mode | undefined; + /** Function to convert selected value into a string */ + valueToStringFunction?: ((value: V1 | V2 | V3) => string) | undefined; +} + export interface InputNestedSelectProps extends React.HTMLAttributes, InputNestedSelectOptionsProps, + InputNestedSelectSharedProps, Omit, "options">, DropdownSearchProps, DropdownStyleProps { - readOnly?: boolean | undefined; /** Specifies key path of the selected option */ selectedKeyPath?: string[] | undefined; - /** Specifies if items are expanded or collapsed when the dropdown is opened */ - mode?: Mode | undefined; /** If specified, the category label is selectable */ selectableCategory?: boolean | undefined; onSelectOption?: | ((keyPath: string[], value: V1 | V2 | V3) => void) | undefined; - /** Function to convert selected value into a string */ - valueToStringFunction?: ((value: V1 | V2 | V3) => string) | undefined; } /** To be exposed for Form component inheritance */ diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index 172d2f151..6226f7428 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -15,9 +15,6 @@ export interface SelectedItem { export interface DropdownEventHandlerProps { onSelectItem: (item: SelectedItem) => void; - onSelectItems?: - | ((items: SelectedItem[], keyPaths: string[][]) => void) - | undefined; } export interface DropdownSearchProps { diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx index 94b6b212c..dcb4ced0c 100644 --- a/stories/form/form-nested-multi-select/props-table.tsx +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -118,7 +118,7 @@ const DATA: ApiTableSectionProps[] = [ name: "onSelectOptions", description: "Called when an option is selected", propTypes: [ - "(keyPaths: string[][], value: Array) => void", + "(keyPaths: string[][], values: Array) => void", ], }, { From 521ae727e47f09d605012c11e689fa3f2e2e8559 Mon Sep 17 00:00:00 2001 From: Wilker Date: Fri, 18 Aug 2023 14:33:46 +0800 Subject: [PATCH 17/47] [BOOKINGSG-4362][WK] remove typo after merged cherry pick into long lived --- .../nested-dropdown-list.tsx | 24 ------------------- src/shared/nested-dropdown-list/types.ts | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 6c9e400f0..28d804ce5 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -42,14 +42,7 @@ export const NestedDropdownList = ({ searchPlaceholder = "Search", visible, mode = "default", -<<<<<<< HEAD -======= multiSelect, -<<<<<<< HEAD - selectedKeyPath, ->>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) -======= ->>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) selectedKeyPaths, selectableCategory, itemsLoadState = "success", @@ -418,17 +411,7 @@ export const NestedDropdownList = ({ // otherwise expand the first selected item or first subitem tree const list = NestedDropdownListHelper.getInitialDropdown( currentItems, -<<<<<<< HEAD -<<<<<<< HEAD selectedKeyPaths -======= - selectedKeyPath, - selectedKeyPaths, - multiSelect ->>>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) -======= - selectedKeyPaths ->>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) ); return list; @@ -475,13 +458,6 @@ export const NestedDropdownList = ({ >>>>>> 3d333f00 ([BOOKINGSG-4362][WK] handle selectedAll and checkbox interactive) -======= ->>>>>>> 7c709910 ([BOOKINGSG-4362][WK] update selectedKeyPaths and onSelectItem) selectedKeyPaths={selectedKeyPaths} selectableCategory={selectableCategory} searchValue={searchValue} diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index 6226f7428..aaa442c18 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -35,7 +35,7 @@ export interface NestedDropdownListProps visible?: boolean | undefined; multiSelect?: boolean | undefined; /** Specifies key path of selected option */ - selectedKeyPaths: string[][]; + selectedKeyPaths: string[][] | []; /** Specifies if items are expanded or collapsed when the dropdown is opened */ mode?: Mode | undefined; /** If specified, the category label is selectable */ From 99eec0499477b9bdd633a9c80d0c75b42afe2cdb Mon Sep 17 00:00:00 2001 From: Wilker Date: Sun, 20 Aug 2023 20:46:03 +0800 Subject: [PATCH 18/47] [BOOKINGSG-4362][WK] fix typo selectedKeyPaths initial value --- .../input-nested-multi-select.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index bf1c289d1..7b03ba41b 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -160,7 +160,7 @@ export const InputNestedMultiSelect = ({ setSelectedItems(items); performOnSelectOptions(keyPaths, items); } else { - setSelectedKeyPaths([[]]); + setSelectedKeyPaths([]); setSelectedItems([]); performOnSelectOptions(); } @@ -337,9 +337,9 @@ export const InputNestedMultiSelect = ({ const findSelectedItem = ( items: CombinedOptionProps[], - keyPaths: string[] + keyPath: string[] ): SelectedItem | undefined => { - const [currentKey, ...nextKeyPath] = keyPaths; + const [currentKey, ...nextKeyPath] = keyPath; if (isEmpty(items) || !currentKey) { return undefined; @@ -365,7 +365,6 @@ export const InputNestedMultiSelect = ({ for (let i = 0; i < selectedKeyPaths.length; i++) { const item = findSelectedItem(options, keyPaths[i]); - if (!item) break; selectedItems.push({ value: item.value, From 98d80ce9833458b51332655e4940c41aa0992bfe Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 21 Aug 2023 15:43:42 +0800 Subject: [PATCH 19/47] [BOOKINGSG-4362][WK] update list item margin-left --- .../nested-dropdown-list/list-item.styles.tsx | 32 +++++++++---------- src/shared/nested-dropdown-list/list-item.tsx | 11 +++---- .../nested-dropdown-list.tsx | 2 +- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index 1b3e06246..5132aa057 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -13,7 +13,6 @@ import { TriangleForwardFillIcon } from "@lifesg/react-icons/triangle-forward-fi interface ListProps { $expanded: boolean; - $level_3: boolean; $multiSelect: boolean; } @@ -26,6 +25,10 @@ interface LabelProps { $truncateType?: TruncateType; } +interface ItemProps { + $level: number; +} + interface CheckboxInputProps { $type: "category" | "label"; } @@ -75,6 +78,18 @@ export const ListItemSelector = styled.button` }} `; +export const Item = styled.li` + ${(props) => { + switch (props.$level) { + case 2: + case 3: + return css` + margin-left: 2.125rem; + `; + } + }} +`; + export const Label = styled.div` ${TextStyleHelper.getTextStyle("Body", "regular")} overflow: hidden; @@ -194,19 +209,4 @@ export const List = styled.ul` display: ${(props) => (props.$expanded ? "flex" : "none")}; flex-direction: column; margin-left: 2.125rem; - - ${(props) => { - const { $level_3, $multiSelect } = props; - if ($level_3) { - if ($multiSelect) { - return css` - margin-left: 4.25rem; - `; - } else { - return css` - margin-left: 2.625rem; - `; - } - } - }} `; diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index b41761662..9624d8dc0 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -7,6 +7,7 @@ import { ButtonSection, Category, CheckboxInput, + Item, Label, List, ListItemSelector, @@ -141,11 +142,7 @@ export const ListItem = ({ const nextSubItems = item.subItems.values(); return ( - + {[...nextSubItems].map((item) => ( ({ if (!item.subItems) { return ( -
  • + onRef(ref, item.keyPath)} type="button" @@ -255,7 +252,7 @@ export const ListItem = ({ > {renderLabel()} -
  • + ); } diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 28d804ce5..2b7c80755 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -508,7 +508,7 @@ export const NestedDropdownList = ({ > {selectedKeyPaths.flat().length === 0 ? "Select all" - : "Unselect all"} + : "Clear all"} ); From e6c22f2b064150ae3d1374eb9500f5ac9dd03516 Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 21 Aug 2023 15:46:51 +0800 Subject: [PATCH 20/47] [BOOKINGSG-4362][WK] update handleSelectItem fn --- .../input-nested-multi-select.tsx | 179 +++++------------- 1 file changed, 47 insertions(+), 132 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 7b03ba41b..4e486bce5 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -64,9 +64,10 @@ export const InputNestedMultiSelect = ({ // ============================================================================= useEffect(() => { const newKeyPath = _selectedKeyPaths || []; - setSelectedKeyPaths(newKeyPath); + const selectedItems = getSelectedItemFromKey(options, newKeyPath); - updateSelectedItemFromKey(options, newKeyPath); + setSelectedKeyPaths(newKeyPath); + setSelectedItems(selectedItems); }, [_selectedKeyPaths, options]); // ============================================================================= @@ -86,69 +87,58 @@ export const InputNestedMultiSelect = ({ const handleSelectItem = ( item: CombinedFormattedOptionProps ) => { - const { value, label, keyPath } = item; - let resultSelectedKeyPaths: string[][] = []; - let resultSelectedItems: SelectedItem[] = []; - + const { keyPath } = item; const selectedItem = getItemAtKeyPath(keyPath); + let newKeyPaths = [...selectedKeyPaths]; if (selectedItem.subItems) { - const { targetItems, targetKeyPaths } = getSubItemAndKeypath( - selectedItem, - keyPath - ); - - const isRemove = targetKeyPaths.every((keyPath) => - selectedKeyPaths.some( + const keys = getSubItemKeyPaths(selectedItem, keyPath); + const isRemoveAll = keys.every((keyPath) => + newKeyPaths.some( (key) => JSON.stringify(keyPath) === JSON.stringify(key) ) ); - if (!isRemove) { - const { keys, items } = getAddSubItems( - targetKeyPaths, - targetItems + if (isRemoveAll) { + newKeyPaths = newKeyPaths.filter((key) => + keys.every( + (keyPath) => + JSON.stringify(key) !== JSON.stringify(keyPath) + ) ); - - resultSelectedKeyPaths = keys; - resultSelectedItems = items; } else { - const { keys, items } = getRemoveSubItems( - targetKeyPaths, - targetItems - ); - - resultSelectedKeyPaths = keys; - resultSelectedItems = items; + newKeyPaths = [ + ...new Map( + [...newKeyPaths, ...keys].map((k) => [k.join("-"), k]) + ).values(), + ]; } } else { - const isRemove = selectedKeyPaths.some( - (key) => JSON.stringify(key) === JSON.stringify(keyPath) - ); - - if (!isRemove) { - resultSelectedKeyPaths = [...selectedKeyPaths, keyPath]; - resultSelectedItems = [ - ...selectedItems, - { label, keyPath, value }, - ]; - } else { - const { keys, items } = getRemoveOption(keyPath); - - resultSelectedKeyPaths = keys; - resultSelectedItems = items; + let isRemove = false; + let removeIndex: number = null; + for (let i = 0; i < newKeyPaths.length; i++) { + if ( + JSON.stringify(newKeyPaths[i]) === JSON.stringify(keyPath) + ) { + isRemove = true; + removeIndex = i; + break; + } } + + if (isRemove) newKeyPaths.splice(removeIndex, 1); + else newKeyPaths.push(keyPath); } - setSelectedKeyPaths(resultSelectedKeyPaths); - setSelectedItems(resultSelectedItems); + const newSelectedItems = getSelectedItemFromKey(options, newKeyPaths); + setSelectedKeyPaths(newKeyPaths); + setSelectedItems(newSelectedItems); triggerOptionDisplayCallback(false); - if (selectorRef.current) { - selectorRef.current.focus(); - } - performOnSelectOptions(resultSelectedKeyPaths, resultSelectedItems); + if (selectorRef.current) selectorRef.current.focus(); + + performOnSelectOptions(newKeyPaths, newSelectedItems); }; const handleSelectAll = ( @@ -195,75 +185,6 @@ export const InputNestedMultiSelect = ({ } }; - const getAddSubItems = ( - _keyPaths: string[][], - _items: SelectedItem[] - ) => { - const keys = [ - ...new Map( - [...selectedKeyPaths, ..._keyPaths].map((k) => [k.join("-"), k]) - ).values(), - ]; - const items = [ - ...new Map( - [...selectedItems, ..._items].map((i) => [ - i.keyPath.join("-"), - i, - ]) - ).values(), - ]; - - return { - keys, - items, - }; - }; - - const getRemoveSubItems = ( - _keyPaths: string[][], - _items: SelectedItem[] - ) => { - const keys = selectedKeyPaths.filter((selectedKey) => - _keyPaths.every( - (key) => JSON.stringify(selectedKey) !== JSON.stringify(key) - ) - ); - const items = selectedItems.filter((selectedItem) => - _items.every( - (item) => - JSON.stringify(selectedItem.keyPath) !== - JSON.stringify(item.keyPath) - ) - ); - - return { - keys, - items, - }; - }; - - const getRemoveOption = (keyPath: string[]) => { - const keys = selectedKeyPaths.filter( - (key) => JSON.stringify(key) !== JSON.stringify(keyPath) - ); - const items = selectedItems - .map(({ keyPath: key, label, value }) => { - if (JSON.stringify(key) !== JSON.stringify(keyPath)) { - return { - value, - label, - keyPath: key, - }; - } - }) - .filter(Boolean); - - return { - keys, - items, - }; - }; - const getDisplayValue = (): string => { const { label, value } = selectedItems[0]; @@ -297,11 +218,10 @@ export const InputNestedMultiSelect = ({ return item; }; - const getSubItemAndKeypath = ( + const getSubItemKeyPaths = ( _item: CombinedOptionProps, selectedKeyPath: string[] ) => { - const targetItems: SelectedItem[] = []; const targetKeyPaths: string[][] = []; const parentKey = selectedKeyPath.slice(0, -1); @@ -309,27 +229,22 @@ export const InputNestedMultiSelect = ({ item: CombinedOptionProps, parentKey: string[] ) => { - const relaventKey = [...parentKey, item.key]; + const releventKey = [...parentKey, item.key]; if (!item.subItems) { - const { label, value } = item; - targetItems.push({ label, keyPath: relaventKey, value }); - targetKeyPaths.push(relaventKey); + targetKeyPaths.push(releventKey); return; } - item.subItems.forEach((subItem) => find(subItem, relaventKey)); + item.subItems.forEach((subItem) => find(subItem, releventKey)); }; find(_item, parentKey); - return { - targetKeyPaths, - targetItems, - }; + return targetKeyPaths; }; - const updateSelectedItemFromKey = ( + const getSelectedItemFromKey = ( options: CombinedOptionProps[], keyPaths: string[][] ) => { @@ -352,7 +267,7 @@ export const InputNestedMultiSelect = ({ const result = { label, value, - keyPath: selectedKeyPaths[count], + keyPath: keyPaths[count], }; count = count + 1; return result; @@ -363,7 +278,7 @@ export const InputNestedMultiSelect = ({ const selectedItems = []; - for (let i = 0; i < selectedKeyPaths.length; i++) { + for (let i = 0; i < keyPaths.length; i++) { const item = findSelectedItem(options, keyPaths[i]); selectedItems.push({ @@ -373,7 +288,7 @@ export const InputNestedMultiSelect = ({ }); } - setSelectedItems(selectedItems); + return selectedItems; }; const truncateValue = (value: string) => { From 32460e7d910bc687dae138d632bf1ff4c013cf73 Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 21 Aug 2023 16:17:30 +0800 Subject: [PATCH 21/47] [BOOKINGSG-4362][WK] fix style for single select --- src/shared/nested-dropdown-list/list-item.styles.tsx | 10 +++++++--- src/shared/nested-dropdown-list/list-item.tsx | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index 5132aa057..c4a8b7719 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -27,6 +27,7 @@ interface LabelProps { interface ItemProps { $level: number; + $multiSelect: boolean; } interface CheckboxInputProps { @@ -83,9 +84,12 @@ export const Item = styled.li` switch (props.$level) { case 2: case 3: - return css` - margin-left: 2.125rem; - `; + if (props.$multiSelect) { + return css` + margin-left: 2.125rem; + `; + } + break; } }} `; diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index 9624d8dc0..b9d61d6ee 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -240,7 +240,11 @@ export const ListItem = ({ if (!item.subItems) { return ( - + onRef(ref, item.keyPath)} type="button" From 71f467c0cff86a94939478b1e5f2281340f2f8eb Mon Sep 17 00:00:00 2001 From: Wilker Date: Mon, 21 Aug 2023 16:40:54 +0800 Subject: [PATCH 22/47] [BOOKINGSG-4362][WK] remove unnecessary flat method --- src/shared/nested-dropdown-list/nested-dropdown-list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 2b7c80755..b29280791 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -192,7 +192,7 @@ export const NestedDropdownList = ({ }; const handleSelectAll = () => { - const isSelectedAll = !selectedKeyPaths.flat().length; + const isSelectedAll = !selectedKeyPaths.length; const { keyPaths, items, list } = NestedDropdownListHelper.updateSelectedAll( @@ -506,7 +506,7 @@ export const NestedDropdownList = ({ onClick={handleSelectAll} type="button" > - {selectedKeyPaths.flat().length === 0 + {selectedKeyPaths.length === 0 ? "Select all" : "Clear all"} From 5582f607a0bb5cd1f9ae61875dfcafaad2ff76fb Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 11:33:49 +0800 Subject: [PATCH 23/47] [BOOKINGSG-4362][WK] simplied handleSelectItem and fix expended issue if category is collapsed --- .../input-nested-multi-select.tsx | 76 +++++++++---------- .../nested-dropdown-list-helper.ts | 6 +- .../nested-dropdown-list.tsx | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 4e486bce5..2feefa487 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -87,47 +87,41 @@ export const InputNestedMultiSelect = ({ const handleSelectItem = ( item: CombinedFormattedOptionProps ) => { - const { keyPath } = item; - const selectedItem = getItemAtKeyPath(keyPath); - let newKeyPaths = [...selectedKeyPaths]; + const selectedItem = getItemAtKeyPath(item.keyPath); + let newKeyPaths: string[][] = []; if (selectedItem.subItems) { - const keys = getSubItemKeyPaths(selectedItem, keyPath); - const isRemoveAll = keys.every((keyPath) => - newKeyPaths.some( - (key) => JSON.stringify(keyPath) === JSON.stringify(key) - ) + const selectableOptionKeyPaths = getSubItemKeyPaths( + selectedItem, + item.keyPath ); - if (isRemoveAll) { - newKeyPaths = newKeyPaths.filter((key) => - keys.every( - (keyPath) => - JSON.stringify(key) !== JSON.stringify(keyPath) - ) - ); - } else { - newKeyPaths = [ - ...new Map( - [...newKeyPaths, ...keys].map((k) => [k.join("-"), k]) - ).values(), - ]; + const selectedCount = selectedKeyPaths.filter((keyPath) => + keyPath.join("-").startsWith(item.keyPath.join("-")) + ).length; + + newKeyPaths = selectedKeyPaths.filter( + (keyPath) => + !keyPath.join("-").startsWith(item.keyPath.join("-")) + ); + + if (selectedCount < selectableOptionKeyPaths.length) { + newKeyPaths = selectableOptionKeyPaths; } } else { - let isRemove = false; - let removeIndex: number = null; - for (let i = 0; i < newKeyPaths.length; i++) { - if ( - JSON.stringify(newKeyPaths[i]) === JSON.stringify(keyPath) - ) { - isRemove = true; - removeIndex = i; - break; - } - } + const selected = selectedKeyPaths.some((keyPath) => + keyPath.join("-").startsWith(item.keyPath.join("-")) + ); - if (isRemove) newKeyPaths.splice(removeIndex, 1); - else newKeyPaths.push(keyPath); + if (selected) { + const filteredItems = selectedItems.filter( + ({ keyPath }) => + JSON.stringify(keyPath) !== JSON.stringify(item.keyPath) + ); + newKeyPaths = filteredItems.map((i) => i.keyPath); + } else { + newKeyPaths = [...selectedKeyPaths, item.keyPath]; + } } const newSelectedItems = getSelectedItemFromKey(options, newKeyPaths); @@ -176,7 +170,7 @@ export const InputNestedMultiSelect = ({ // HELPER FUNCTION // ============================================================================= const performOnSelectOptions = ( - keyPaths: string[][] = [[]], + keyPaths: string[][] = [], items: SelectedItem[] = [] ) => { if (onSelectOptions) { @@ -281,11 +275,13 @@ export const InputNestedMultiSelect = ({ for (let i = 0; i < keyPaths.length; i++) { const item = findSelectedItem(options, keyPaths[i]); - selectedItems.push({ - value: item.value, - label: item.label, - keyPath: item.keyPath, - }); + if (item) { + selectedItems.push({ + value: item.value, + label: item.label, + keyPath: item.keyPath, + }); + } } return selectedItems; diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 399207fd4..78be1c082 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -76,7 +76,7 @@ export namespace NestedDropdownListHelper { keyPaths.forEach((keyPathArray) => { targetKey = []; - keyPathArray.forEach((key) => { + keyPathArray.slice(0, -1).forEach((key) => { targetKey.push(key); const item = getItemAtKeyPath(draft, targetKey); item.expanded = true; @@ -106,6 +106,10 @@ export namespace NestedDropdownListHelper { list = produce( currentItems, (draft: FormattedOptionMap) => { + currentItems.forEach((i) => { + const item = getItemAtKeyPath(draft, i.keyPath); + item.expanded = true; + }); keyPaths.forEach((key) => { const parentKey = key.slice(0, -1); const item = getItemAtKeyPath(draft, parentKey); diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index b29280791..fb18a56bf 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -501,7 +501,7 @@ export const NestedDropdownList = ({ itemsLoadState === "success" ) { return ( - + Date: Tue, 22 Aug 2023 11:55:44 +0800 Subject: [PATCH 24/47] [BOOKINGSG-4362][WK] remove comments out props --- src/shared/nested-dropdown-list/list-item.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index b9d61d6ee..fd967da4e 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -35,7 +35,6 @@ interface ListItemProps { export const ListItem = ({ item, - // selectedKeyPath, selectedKeyPaths, selectableCategory, searchValue, From 7861b7aba8c52107be1969751c054f9103f384ab Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 12:05:22 +0800 Subject: [PATCH 25/47] [BOOKINGSG-4362][WK] update style in without category in L1 --- src/shared/nested-dropdown-list/list-item.styles.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index c4a8b7719..5fb32f5ee 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -82,6 +82,7 @@ export const ListItemSelector = styled.button` export const Item = styled.li` ${(props) => { switch (props.$level) { + case 1: case 2: case 3: if (props.$multiSelect) { From 79766b08f6d5882fadff0c5fe72d174efe291e4c Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 12:06:19 +0800 Subject: [PATCH 26/47] [BOOKINGSG-4362][WK] handle expanded option without category in L1 --- src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 78be1c082..723bdc0ef 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -111,7 +111,8 @@ export namespace NestedDropdownListHelper { item.expanded = true; }); keyPaths.forEach((key) => { - const parentKey = key.slice(0, -1); + const parentKey = + key.length === 1 ? key : key.slice(0, -1); const item = getItemAtKeyPath(draft, parentKey); item.expanded = true; }); From 4fd9659b11070e36ecd429baed4485b96c128367 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 12:16:18 +0800 Subject: [PATCH 27/47] [BOOKINGSG-4362][WK] fix expanded value in L1 without category --- .../nested-dropdown-list/nested-dropdown-list-helper.ts | 6 +++--- stories/form/form-nested-select/nested-data-list.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 723bdc0ef..203545b3f 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -108,11 +108,11 @@ export namespace NestedDropdownListHelper { (draft: FormattedOptionMap) => { currentItems.forEach((i) => { const item = getItemAtKeyPath(draft, i.keyPath); - item.expanded = true; + if (item.subItems) item.expanded = true; }); keyPaths.forEach((key) => { - const parentKey = - key.length === 1 ? key : key.slice(0, -1); + const parentKey = key.slice(0, -1); + if (!parentKey.length) return; const item = getItemAtKeyPath(draft, parentKey); item.expanded = true; }); diff --git a/stories/form/form-nested-select/nested-data-list.ts b/stories/form/form-nested-select/nested-data-list.ts index 822ba9d29..ce4a3b86d 100644 --- a/stories/form/form-nested-select/nested-data-list.ts +++ b/stories/form/form-nested-select/nested-data-list.ts @@ -1,4 +1,12 @@ export const options = [ + { + label: "Sub Category A", + value: { + id: 820, + name: "Sub category a", + }, + key: "820", + }, { label: "Category 1", value: { id: 999, name: "category 1" }, From a35968b3edb33fb566ba8241f94f3cdeb22c6402 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 12:18:13 +0800 Subject: [PATCH 28/47] [BOOKINGSG-4362][WK] remove option without category in L1 --- stories/form/form-nested-select/nested-data-list.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/stories/form/form-nested-select/nested-data-list.ts b/stories/form/form-nested-select/nested-data-list.ts index ce4a3b86d..822ba9d29 100644 --- a/stories/form/form-nested-select/nested-data-list.ts +++ b/stories/form/form-nested-select/nested-data-list.ts @@ -1,12 +1,4 @@ export const options = [ - { - label: "Sub Category A", - value: { - id: 820, - name: "Sub category a", - }, - key: "820", - }, { label: "Category 1", value: { id: 999, name: "category 1" }, From aeed978a7d013f41de4a54535246515756d5d498 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 13:11:14 +0800 Subject: [PATCH 29/47] [BOOKINGSG-4362][WK] remove single array type in selectedKeyPaths --- src/shared/nested-dropdown-list/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index aaa442c18..a3cf39271 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -35,7 +35,7 @@ export interface NestedDropdownListProps visible?: boolean | undefined; multiSelect?: boolean | undefined; /** Specifies key path of selected option */ - selectedKeyPaths: string[][] | []; + selectedKeyPaths: string[][]; /** Specifies if items are expanded or collapsed when the dropdown is opened */ mode?: Mode | undefined; /** If specified, the category label is selectable */ @@ -68,7 +68,9 @@ interface BaseFormattedOptionProps { label: string; keyPath: string[]; expanded: boolean; + selected: boolean; checked: boolean; + indeterminate: boolean; isSearchTerm: boolean; } From ff4d4d7f94b6be65528f3614554038001367e075 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 13:29:03 +0800 Subject: [PATCH 30/47] [BOOKINGSG-4362][WK] remove selected/indeterminate state in recursive type --- src/shared/nested-dropdown-list/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index a3cf39271..6226f7428 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -68,9 +68,7 @@ interface BaseFormattedOptionProps { label: string; keyPath: string[]; expanded: boolean; - selected: boolean; checked: boolean; - indeterminate: boolean; isSearchTerm: boolean; } From 8254c663108fc18167757e61d5995737fa5e5d7f Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 14:54:28 +0800 Subject: [PATCH 31/47] [BOOKINGSG-4362][WK] fix remove unexpected category issue --- .../input-nested-multi-select.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 2feefa487..8b04c18dc 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -106,7 +106,13 @@ export const InputNestedMultiSelect = ({ ); if (selectedCount < selectableOptionKeyPaths.length) { - newKeyPaths = selectableOptionKeyPaths; + newKeyPaths = [ + ...new Map( + [...selectedKeyPaths, ...selectableOptionKeyPaths].map( + (k) => [k.join("-"), k] + ) + ).values(), + ]; } } else { const selected = selectedKeyPaths.some((keyPath) => From 46116cf3a792c26f51a16ee744042644614ecba2 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 16:07:19 +0800 Subject: [PATCH 32/47] [BOOKINGSG-4362][WK] simplied updatedSelectAll fn --- .../nested-dropdown-list-helper.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 203545b3f..f9fe10060 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -74,12 +74,14 @@ export namespace NestedDropdownListHelper { (draft: FormattedOptionMap) => { let targetKey: string[] = []; - keyPaths.forEach((keyPathArray) => { + keyPaths.forEach((keyPath) => { targetKey = []; - keyPathArray.slice(0, -1).forEach((key) => { + keyPath.forEach((key) => { targetKey.push(key); const item = getItemAtKeyPath(draft, targetKey); - item.expanded = true; + if (item.subItems) { + item.expanded = true; + } }); }); } @@ -106,16 +108,21 @@ export namespace NestedDropdownListHelper { list = produce( currentItems, (draft: FormattedOptionMap) => { - currentItems.forEach((i) => { - const item = getItemAtKeyPath(draft, i.keyPath); - if (item.subItems) item.expanded = true; - }); - keyPaths.forEach((key) => { - const parentKey = key.slice(0, -1); - if (!parentKey.length) return; - const item = getItemAtKeyPath(draft, parentKey); - item.expanded = true; - }); + for (const item of draft.values()) { + const update = ( + item: CombinedFormattedOptionProps + ) => { + if (!item.subItems) return; + + item.expanded = true; + + return item.subItems.forEach((subItem) => + update(subItem) + ); + }; + + update(item); + } } ); } From 05cb65b02a59fe60d233f83fb3c4f485717fe1f1 Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 16:08:44 +0800 Subject: [PATCH 33/47] [BOOKINGSG-4362][WK] remove switch case --- .../nested-dropdown-list/list-item.styles.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/shared/nested-dropdown-list/list-item.styles.tsx b/src/shared/nested-dropdown-list/list-item.styles.tsx index 5fb32f5ee..042530c2d 100644 --- a/src/shared/nested-dropdown-list/list-item.styles.tsx +++ b/src/shared/nested-dropdown-list/list-item.styles.tsx @@ -81,16 +81,10 @@ export const ListItemSelector = styled.button` export const Item = styled.li` ${(props) => { - switch (props.$level) { - case 1: - case 2: - case 3: - if (props.$multiSelect) { - return css` - margin-left: 2.125rem; - `; - } - break; + if (props.$multiSelect) { + return css` + margin-left: 2.125rem; + `; } }} `; From 4e40019af45803d7d3d9716363e9945cd4175d9f Mon Sep 17 00:00:00 2001 From: Wilker Date: Tue, 22 Aug 2023 20:01:23 +0800 Subject: [PATCH 34/47] [BOOKINGSG-4362][WK] simplified handleSelectItem fn --- .../input-nested-multi-select.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 8b04c18dc..fbef71144 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -97,7 +97,7 @@ export const InputNestedMultiSelect = ({ ); const selectedCount = selectedKeyPaths.filter((keyPath) => - keyPath.join("-").startsWith(item.keyPath.join("-")) + isSubItem(keyPath, item.keyPath) ).length; newKeyPaths = selectedKeyPaths.filter( @@ -116,7 +116,7 @@ export const InputNestedMultiSelect = ({ } } else { const selected = selectedKeyPaths.some((keyPath) => - keyPath.join("-").startsWith(item.keyPath.join("-")) + isSubItem(keyPath, item.keyPath) ); if (selected) { @@ -218,6 +218,10 @@ export const InputNestedMultiSelect = ({ return item; }; + const isSubItem = (listItemKeyPath: string[], categoryKeyPath: string[]) => + JSON.stringify(categoryKeyPath) === + JSON.stringify(listItemKeyPath.slice(0, categoryKeyPath.length)); + const getSubItemKeyPaths = ( _item: CombinedOptionProps, selectedKeyPath: string[] From 5b48b4df36fa7e6d5afde4da6dc6efae66854c2f Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 23 Aug 2023 00:33:51 +0800 Subject: [PATCH 35/47] [BOOKINGSG-4362][WK] replace logics to isSubItem --- src/input-nested-multi-select/input-nested-multi-select.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index fbef71144..e63bf9e87 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -100,9 +100,8 @@ export const InputNestedMultiSelect = ({ isSubItem(keyPath, item.keyPath) ).length; - newKeyPaths = selectedKeyPaths.filter( - (keyPath) => - !keyPath.join("-").startsWith(item.keyPath.join("-")) + newKeyPaths = selectedKeyPaths.filter((keyPath) => + isSubItem(keyPath, item.keyPath) ); if (selectedCount < selectableOptionKeyPaths.length) { From d1eb836bd97fcc6e700d808d8ef2a39f051f569c Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 23 Aug 2023 09:24:08 +0800 Subject: [PATCH 36/47] [BOOKINGSG-4362][WK] remove subItem keyPath if all subItem is checked --- .../input-nested-multi-select.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index e63bf9e87..a89035fd5 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -100,10 +100,6 @@ export const InputNestedMultiSelect = ({ isSubItem(keyPath, item.keyPath) ).length; - newKeyPaths = selectedKeyPaths.filter((keyPath) => - isSubItem(keyPath, item.keyPath) - ); - if (selectedCount < selectableOptionKeyPaths.length) { newKeyPaths = [ ...new Map( @@ -112,6 +108,18 @@ export const InputNestedMultiSelect = ({ ) ).values(), ]; + } else { + const subItemKeyPaths = selectedKeyPaths.filter((keyPath) => + isSubItem(keyPath, item.keyPath) + ); + + newKeyPaths = selectedKeyPaths.filter((keyPath) => + subItemKeyPaths.every( + (removeKey) => + JSON.stringify(keyPath) !== + JSON.stringify(removeKey) + ) + ); } } else { const selected = selectedKeyPaths.some((keyPath) => From 69b1f83834ef5c7875066eb7a7e9fddc657db9f8 Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 23 Aug 2023 10:14:29 +0800 Subject: [PATCH 37/47] [BOOKINGSG-4362][WK] simplified remove related keyPaths --- .../input-nested-multi-select.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index a89035fd5..8adbfde0b 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -109,16 +109,8 @@ export const InputNestedMultiSelect = ({ ).values(), ]; } else { - const subItemKeyPaths = selectedKeyPaths.filter((keyPath) => - isSubItem(keyPath, item.keyPath) - ); - - newKeyPaths = selectedKeyPaths.filter((keyPath) => - subItemKeyPaths.every( - (removeKey) => - JSON.stringify(keyPath) !== - JSON.stringify(removeKey) - ) + newKeyPaths = selectedKeyPaths.filter( + (keyPath) => !isSubItem(keyPath, item.keyPath) ); } } else { From b97acaf554d66e9d371eac2ddcd662d1d297aef0 Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 23 Aug 2023 13:19:59 +0800 Subject: [PATCH 38/47] [BOOKINGSG-4616] enhancement inderminate checked box --- src/shared/nested-dropdown-list/list-item.tsx | 4 +- .../nested-dropdown-list-helper.ts | 142 +++++++++++++----- .../nested-dropdown-list.tsx | 29 +++- src/shared/nested-dropdown-list/types.ts | 2 + 4 files changed, 135 insertions(+), 42 deletions(-) diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index fd967da4e..f5a7097bf 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -179,6 +179,7 @@ export const ListItem = ({ displaySize="small" $type="category" checked={item.checked} + indeterminate={item.indeterminate} onChange={handleSelectParent} /> )} @@ -224,6 +225,7 @@ export const ListItem = ({ )} @@ -248,7 +250,7 @@ export const ListItem = ({ ref={(ref) => onRef(ref, item.keyPath)} type="button" tabIndex={visible ? 0 : -1} - $selected={checkListItemSelected(item.keyPath)} + $selected={item.selected} $multiSelect={multiSelect} onBlur={handleBlur} onClick={handleSelect} diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index f9fe10060..e092492c0 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -43,7 +43,9 @@ export namespace NestedDropdownListHelper { value, expanded: mode === "expand", isSearchTerm: false, + selected: false, checked: false, + indeterminate: undefined, keyPath, subItems: subItems ? formatted(subItems, keyPath) @@ -79,9 +81,17 @@ export namespace NestedDropdownListHelper { keyPath.forEach((key) => { targetKey.push(key); const item = getItemAtKeyPath(draft, targetKey); - if (item.subItems) { - item.expanded = true; - } + + const selected = + JSON.stringify(item.keyPath) === + JSON.stringify( + selectedKeyPaths[0]?.slice( + 0, + item.keyPath.length + ) + ); + if (item.subItems) item.expanded = true; + if (selected) item.selected = true; }); }); } @@ -176,43 +186,105 @@ export namespace NestedDropdownListHelper { return item; }; - export const updateCategoryChecked = ( - list: FormattedOptionMap, + export const getCategoryChecked = ( + targetList: FormattedOptionMap, selectedKeyPaths: string[][] ) => { - const resetList = produce( - list, - (draft: Map>) => { - const resetChecked = ( - items: Map> - ) => { - if (!items || !items.size) return; - for (const item of items.values()) { - item.checked = false; - if (item.subItems) resetChecked(item.subItems); - } - }; - resetChecked(draft); + const update = (item: CombinedFormattedOptionProps) => { + const matched = selectedKeyPaths.some( + (key) => JSON.stringify(key) === JSON.stringify(item.keyPath) + ); + + if (!item.subItems) { + if (matched) { + return { + ...item, + checked: true, + }; + } else { + return { + ...item, + checked: false, + }; + } } - ); - return produce(resetList, (draft: FormattedOptionMap) => { - let targetKey: string[] = []; - selectedKeyPaths.forEach((keyPathArr) => { - targetKey = []; - const relevantKeys = keyPathArr.slice(0, -1); - relevantKeys.forEach((key) => { - targetKey.push(key); - const item = NestedDropdownListHelper.getItemAtKeyPath( - draft, - targetKey - ); - if (item) { - item.checked = true; - } - }); + const subItems: Map< + string, + CombinedFormattedOptionProps + > = new Map(); + + item.subItems.forEach((subItem) => { + const result = update(subItem) as CombinedFormattedOptionProps< + V1, + V2, + V3 + >; + if (result) { + const key = result.keyPath[result.keyPath.length - 1]; + + subItems.set(key, result); + } }); - }); + + const checkedStatus = Array.from(subItems).map( + (item) => item[1].checked + ); + const isAllChecked = checkedStatus.every(Boolean); + + const someChecked = + checkedStatus.filter((status) => status === false).length !== + checkedStatus.length; + + const result = { + ...item, + checked: isAllChecked, + indeterminate: isAllChecked + ? undefined + : someChecked + ? true + : undefined, + subItems, + }; + + return result; + }; + + const list = new Map(); + + for (const [key, item] of targetList) { + const result = update(item); + + if (result && result.subItems && result.subItems.size) { + const indeterminateStatus = Array.from(result.subItems).map( + (item) => item[1].indeterminate + ); + const checkedStatus = Array.from(result.subItems).map( + (item) => item[1].checked + ); + + const isAllIndeterminate = indeterminateStatus.every(Boolean); + const isAllChecked = checkedStatus.every(Boolean); + const someInderminate = + indeterminateStatus.filter(Boolean).length; + const someChecked = checkedStatus.filter(Boolean).length; + + const indeterminate = + isAllChecked || isAllIndeterminate + ? undefined + : someChecked || someInderminate + ? true + : undefined; + + list.set(key, { + ...result, + indeterminate, + checked: isAllChecked, + }); + } + } + + return list; }; } diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index fb18a56bf..4d715d2ff 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -91,9 +91,6 @@ export const NestedDropdownList = ({ const list = getInitialDropdown(); const keyPaths = NestedDropdownListHelper.getVisibleKeyPaths(list); - setCurrentItems(list); - setVisibleKeyPaths(keyPaths); - if (searchInputRef.current) { searchInputRef.current.focus(); } else if (listItemRefs.current) { @@ -103,14 +100,17 @@ export const NestedDropdownList = ({ if (multiSelect) { const multiSelectList = - NestedDropdownListHelper.updateCategoryChecked( + NestedDropdownListHelper.getCategoryChecked( list, selectedKeyPaths ); setCurrentItems(multiSelectList); + } else { + setCurrentItems(list); } + setVisibleKeyPaths(keyPaths); // Give some time for the custom call-to-action to be rendered setTimeout(() => { setContentHeight(getContentHeight()); @@ -139,7 +139,7 @@ export const NestedDropdownList = ({ useEffect(() => { if (visible && multiSelect) { const targetList = isSearch ? filteredItems : currentItems; - const list = NestedDropdownListHelper.updateCategoryChecked( + const list = NestedDropdownListHelper.getCategoryChecked( targetList, selectedKeyPaths ); @@ -156,6 +156,7 @@ export const NestedDropdownList = ({ const handleSelect = (item: CombinedFormattedOptionProps) => { const { label, keyPath, value } = item; + updateSelectedState(keyPath); onSelectItem({ label, keyPath, value }); }; @@ -325,6 +326,22 @@ export const NestedDropdownList = ({ return listHeight; }; + const updateSelectedState = (keyPath: string[]) => { + const targetList = isSearch ? filteredItems : currentItems; + const list = produce( + targetList, + (draft: FormattedOptionMap) => { + const item = NestedDropdownListHelper.getItemAtKeyPath( + draft, + keyPath + ); + item.selected = true; + } + ); + + isSearch ? setFilteredItems(list) : setCurrentItems(list); + }; + const updateSearchState = (): FormattedOptionMap => { const search = ( item: CombinedFormattedOptionProps, @@ -437,7 +454,7 @@ export const NestedDropdownList = ({ if (multiSelect) { const multiSelectList = - NestedDropdownListHelper.updateCategoryChecked( + NestedDropdownListHelper.getCategoryChecked( filtered, selectedKeyPaths ); diff --git a/src/shared/nested-dropdown-list/types.ts b/src/shared/nested-dropdown-list/types.ts index 6226f7428..a3cf39271 100644 --- a/src/shared/nested-dropdown-list/types.ts +++ b/src/shared/nested-dropdown-list/types.ts @@ -68,7 +68,9 @@ interface BaseFormattedOptionProps { label: string; keyPath: string[]; expanded: boolean; + selected: boolean; checked: boolean; + indeterminate: boolean; isSearchTerm: boolean; } From e8b0148e4cb953fb6c5998026fa18ac2b3b19104 Mon Sep 17 00:00:00 2001 From: Wilker Date: Wed, 23 Aug 2023 13:27:18 +0800 Subject: [PATCH 39/47] [BOOKINGSG-4616] remove unnecessary props in list item --- src/shared/nested-dropdown-list/list-item.tsx | 11 +---------- .../nested-dropdown-list/nested-dropdown-list.tsx | 1 - 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index f5a7097bf..d4560be01 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -20,7 +20,6 @@ import { interface ListItemProps { item: CombinedFormattedOptionProps; - selectedKeyPaths: string[][]; selectableCategory?: boolean | undefined; searchValue: string | undefined; itemTruncationType?: TruncateType | undefined; @@ -35,7 +34,6 @@ interface ListItemProps { export const ListItem = ({ item, - selectedKeyPaths, selectableCategory, searchValue, itemTruncationType, @@ -79,11 +77,6 @@ export const ListItem = ({ // ============================================================================= // HELPER FUNCTIONS // ============================================================================= - const checkListItemSelected = (keyPath: string[]): boolean => - selectedKeyPaths.some( - (key) => JSON.stringify(key) === JSON.stringify(keyPath) - ); - const hasExceededContainer = ( item: CombinedFormattedOptionProps ) => { @@ -146,7 +139,6 @@ export const ListItem = ({ ({ {multiSelect && ( )} diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 4d715d2ff..0a215fd25 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -475,7 +475,6 @@ export const NestedDropdownList = ({ Date: Wed, 23 Aug 2023 14:07:14 +0800 Subject: [PATCH 40/47] [BOOKINGSG-4616][WK] update stories --- src/input-nested-multi-select/types.ts | 1 + .../form/form-nested-multi-select/props-table.tsx | 13 ------------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/input-nested-multi-select/types.ts b/src/input-nested-multi-select/types.ts index df1c787a4..e5b2300cc 100644 --- a/src/input-nested-multi-select/types.ts +++ b/src/input-nested-multi-select/types.ts @@ -21,6 +21,7 @@ export interface InputNestedMultiSelectProps DropdownStyleProps { /** Specifies key path to select particular option label */ selectedKeyPaths?: string[][] | undefined; + /** Called when option label is selected */ onSelectOptions?: | ((keyPaths: string[][], values: Array) => void) | undefined; diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx index dcb4ced0c..153356b65 100644 --- a/stories/form/form-nested-multi-select/props-table.tsx +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -69,12 +69,6 @@ const DATA: ApiTableSectionProps[] = [ "The function to convert a value to a string. Only single callback used for both selects. Assumption: values are homogenous for both selects.", propTypes: ["(value: V1 | V2 | V3) => string"], }, - { - name: "selectableCategory", - description: "When specified, allows selection of categories", - propTypes: ["boolean"], - defaultValue: `"false"`, - }, { name: "optionsLoadState", description: @@ -82,13 +76,6 @@ const DATA: ApiTableSectionProps[] = [ propTypes: [`"success"`, `"loading"`, `"failed"`], defaultValue: `"success"`, }, - { - name: "optionTruncationType", - description: - "Specifies the trunction type of the options display. Truncated text will be replaced with ellipsis", - propTypes: [`"end"`, `"middle"`], - defaultValue: `"end"`, - }, { name: "hideNoResultsDisplay", description: From 50198367ed7b0927afb34995e56fef032b1f43c5 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 07:49:29 +0800 Subject: [PATCH 41/47] [BOOKINGSG-4616][WK] recursively update state value --- .../nested-dropdown-list-helper.ts | 176 ++++++++---------- .../nested-dropdown-list.tsx | 24 +-- 2 files changed, 78 insertions(+), 122 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index e092492c0..92b1b7518 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -45,7 +45,7 @@ export namespace NestedDropdownListHelper { isSearchTerm: false, selected: false, checked: false, - indeterminate: undefined, + indeterminate: false, keyPath, subItems: subItems ? formatted(subItems, keyPath) @@ -167,6 +167,79 @@ export namespace NestedDropdownListHelper { return keyPaths; }; + export const getUpdateCheckbox = ( + list: FormattedOptionMap, + selectedKeyPaths: string[][] + ) => { + const result = produce( + list, + (draft: FormattedOptionMap) => { + const update = ( + items: Map> + ) => { + for (const item of items.values()) { + if (!item.subItems) { + const checked = selectedKeyPaths.some( + (keyPath) => + JSON.stringify(keyPath) === + JSON.stringify(item.keyPath) + ); + item.checked = checked; + } else { + update(item.subItems); + + const subItems: Map< + string, + CombinedFormattedOptionProps + > = item.subItems; + + const { checked, indeterminate } = Array.from( + subItems + ).reduce( + (result, subItemMap) => { + const item = subItemMap[1]; + result.checked.push(item.checked); + result.indeterminate.push( + item.indeterminate + ); + + return result; + }, + { + checked: [], + indeterminate: [], + } + ); + + const isAllChecked = checked.every(Boolean); + const isPartialChecked = checked.some(Boolean); + const isPartialIndeterminate = + indeterminate.some(Boolean); + + if (isAllChecked) { + item.checked = true; + item.indeterminate = false; + } else if ( + isPartialChecked || + isPartialIndeterminate + ) { + item.indeterminate = true; + item.checked = false; + } else { + item.indeterminate = false; + item.checked = false; + } + } + } + }; + + update(draft); + } + ); + + return result; + }; + export const getItemAtKeyPath = ( draft: FormattedOptionMap, keyPath: string[] @@ -185,107 +258,6 @@ export namespace NestedDropdownListHelper { return item; }; - - export const getCategoryChecked = ( - targetList: FormattedOptionMap, - selectedKeyPaths: string[][] - ) => { - const update = (item: CombinedFormattedOptionProps) => { - const matched = selectedKeyPaths.some( - (key) => JSON.stringify(key) === JSON.stringify(item.keyPath) - ); - - if (!item.subItems) { - if (matched) { - return { - ...item, - checked: true, - }; - } else { - return { - ...item, - checked: false, - }; - } - } - - const subItems: Map< - string, - CombinedFormattedOptionProps - > = new Map(); - - item.subItems.forEach((subItem) => { - const result = update(subItem) as CombinedFormattedOptionProps< - V1, - V2, - V3 - >; - if (result) { - const key = result.keyPath[result.keyPath.length - 1]; - - subItems.set(key, result); - } - }); - - const checkedStatus = Array.from(subItems).map( - (item) => item[1].checked - ); - const isAllChecked = checkedStatus.every(Boolean); - - const someChecked = - checkedStatus.filter((status) => status === false).length !== - checkedStatus.length; - - const result = { - ...item, - checked: isAllChecked, - indeterminate: isAllChecked - ? undefined - : someChecked - ? true - : undefined, - subItems, - }; - - return result; - }; - - const list = new Map(); - - for (const [key, item] of targetList) { - const result = update(item); - - if (result && result.subItems && result.subItems.size) { - const indeterminateStatus = Array.from(result.subItems).map( - (item) => item[1].indeterminate - ); - const checkedStatus = Array.from(result.subItems).map( - (item) => item[1].checked - ); - - const isAllIndeterminate = indeterminateStatus.every(Boolean); - const isAllChecked = checkedStatus.every(Boolean); - const someInderminate = - indeterminateStatus.filter(Boolean).length; - const someChecked = checkedStatus.filter(Boolean).length; - - const indeterminate = - isAllChecked || isAllIndeterminate - ? undefined - : someChecked || someInderminate - ? true - : undefined; - - list.set(key, { - ...result, - indeterminate, - checked: isAllChecked, - }); - } - } - - return list; - }; } // ============================================================================= diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 0a215fd25..223ff5440 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -100,7 +100,7 @@ export const NestedDropdownList = ({ if (multiSelect) { const multiSelectList = - NestedDropdownListHelper.getCategoryChecked( + NestedDropdownListHelper.getUpdateCheckbox( list, selectedKeyPaths ); @@ -139,7 +139,8 @@ export const NestedDropdownList = ({ useEffect(() => { if (visible && multiSelect) { const targetList = isSearch ? filteredItems : currentItems; - const list = NestedDropdownListHelper.getCategoryChecked( + + const list = NestedDropdownListHelper.getUpdateCheckbox( targetList, selectedKeyPaths ); @@ -156,7 +157,6 @@ export const NestedDropdownList = ({ const handleSelect = (item: CombinedFormattedOptionProps) => { const { label, keyPath, value } = item; - updateSelectedState(keyPath); onSelectItem({ label, keyPath, value }); }; @@ -326,22 +326,6 @@ export const NestedDropdownList = ({ return listHeight; }; - const updateSelectedState = (keyPath: string[]) => { - const targetList = isSearch ? filteredItems : currentItems; - const list = produce( - targetList, - (draft: FormattedOptionMap) => { - const item = NestedDropdownListHelper.getItemAtKeyPath( - draft, - keyPath - ); - item.selected = true; - } - ); - - isSearch ? setFilteredItems(list) : setCurrentItems(list); - }; - const updateSearchState = (): FormattedOptionMap => { const search = ( item: CombinedFormattedOptionProps, @@ -454,7 +438,7 @@ export const NestedDropdownList = ({ if (multiSelect) { const multiSelectList = - NestedDropdownListHelper.getCategoryChecked( + NestedDropdownListHelper.getUpdateCheckbox( filtered, selectedKeyPaths ); From 759b9d0c16f2e3f9219e2e3b312a112b56bb8802 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 07:50:59 +0800 Subject: [PATCH 42/47] [BOOKINGSG-4616][WK] fix unclickable issue after checked on category --- src/shared/nested-dropdown-list/list-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/nested-dropdown-list/list-item.tsx b/src/shared/nested-dropdown-list/list-item.tsx index d4560be01..4f88a3e5c 100644 --- a/src/shared/nested-dropdown-list/list-item.tsx +++ b/src/shared/nested-dropdown-list/list-item.tsx @@ -64,7 +64,7 @@ export const ListItem = ({ }; const handleSelectParent = (event: React.ChangeEvent) => { - event.preventDefault(); + event.stopPropagation(); onSelectCategory(item); }; From 6244e6fbb84f05420734c06d392cffd5854fce40 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 07:51:49 +0800 Subject: [PATCH 43/47] [BOOKINGSG-4616][WK] update stories/type description --- src/input-nested-multi-select/types.ts | 4 ++-- src/input-nested-select/types.ts | 1 + stories/form/form-nested-multi-select/props-table.tsx | 9 ++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/input-nested-multi-select/types.ts b/src/input-nested-multi-select/types.ts index e5b2300cc..e61b3f15e 100644 --- a/src/input-nested-multi-select/types.ts +++ b/src/input-nested-multi-select/types.ts @@ -19,9 +19,9 @@ export interface InputNestedMultiSelectProps InputNestedSelectSharedProps, DropdownSearchProps, DropdownStyleProps { - /** Specifies key path to select particular option label */ + /** Specifies key paths to select particular option label */ selectedKeyPaths?: string[][] | undefined; - /** Called when option label is selected */ + /** Called when a selection is made. Returns the key paths and values of selected items in the next selection state */ onSelectOptions?: | ((keyPaths: string[][], values: Array) => void) | undefined; diff --git a/src/input-nested-select/types.ts b/src/input-nested-select/types.ts index 0235d883d..126540548 100644 --- a/src/input-nested-select/types.ts +++ b/src/input-nested-select/types.ts @@ -35,6 +35,7 @@ export interface InputNestedSelectProps selectedKeyPath?: string[] | undefined; /** If specified, the category label is selectable */ selectableCategory?: boolean | undefined; + /** Called when an option is selected. Returns the option's key path and value */ onSelectOption?: | ((keyPath: string[], value: V1 | V2 | V3) => void) | undefined; diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx index 153356b65..02cb63f4f 100644 --- a/stories/form/form-nested-multi-select/props-table.tsx +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -15,7 +15,7 @@ const DATA: ApiTableSectionProps[] = [ }, { name: "selectedKeyPaths", - description: "The key path of the selected options", + description: "The key paths of the selected options", propTypes: ["string[][]"], }, { @@ -76,6 +76,13 @@ const DATA: ApiTableSectionProps[] = [ propTypes: [`"success"`, `"loading"`, `"failed"`], defaultValue: `"success"`, }, + { + name: "optionTruncationType", + description: + "Specifies the trunction type of the options display. Truncated text will be replaced with ellipsis", + propTypes: [`"end"`, `"middle"`], + defaultValue: `"end"`, + }, { name: "hideNoResultsDisplay", description: From 8ead8909a8900c1d23d048f483c5081d7f8b72c4 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 10:21:15 +0800 Subject: [PATCH 44/47] [BOOKINGSG-4616][WK] update selected state for all selectedKeyPaths --- .../nested-dropdown-list-helper.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts index 92b1b7518..cb6c1bf4a 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts +++ b/src/shared/nested-dropdown-list/nested-dropdown-list-helper.ts @@ -82,14 +82,12 @@ export namespace NestedDropdownListHelper { targetKey.push(key); const item = getItemAtKeyPath(draft, targetKey); - const selected = - JSON.stringify(item.keyPath) === - JSON.stringify( - selectedKeyPaths[0]?.slice( - 0, - item.keyPath.length - ) - ); + const selected = selectedKeyPaths.some( + (keyPath) => + JSON.stringify(keyPath) === + JSON.stringify(item.keyPath) + ); + if (item.subItems) item.expanded = true; if (selected) item.selected = true; }); @@ -223,11 +221,11 @@ export namespace NestedDropdownListHelper { isPartialChecked || isPartialIndeterminate ) { - item.indeterminate = true; item.checked = false; + item.indeterminate = true; } else { - item.indeterminate = false; item.checked = false; + item.indeterminate = false; } } } From 0931c610d250bfb8e8fdf44eb9283c36fbe906ea Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 13:03:55 +0800 Subject: [PATCH 45/47] [BOOKINGSG-4616][WK] safe check listItemRef if it exist --- src/shared/nested-dropdown-list/nested-dropdown-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx index 223ff5440..962558f33 100644 --- a/src/shared/nested-dropdown-list/nested-dropdown-list.tsx +++ b/src/shared/nested-dropdown-list/nested-dropdown-list.tsx @@ -95,7 +95,7 @@ export const NestedDropdownList = ({ searchInputRef.current.focus(); } else if (listItemRefs.current) { const target = keyPaths[focusedIndex]; - listItemRefs.current[target[0]].ref.focus(); + listItemRefs.current[target[0]]?.ref.focus(); } if (multiSelect) { From c5833ae698a8671aef55f8d0c0349e6dbda6a2ae Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 13:04:55 +0800 Subject: [PATCH 46/47] [BOOKINGSG-4616][WK] amend stories --- stories/form/form-nested-multi-select/props-table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stories/form/form-nested-multi-select/props-table.tsx b/stories/form/form-nested-multi-select/props-table.tsx index 02cb63f4f..4f508b873 100644 --- a/stories/form/form-nested-multi-select/props-table.tsx +++ b/stories/form/form-nested-multi-select/props-table.tsx @@ -5,7 +5,7 @@ import { SHARED_FORM_PROPS_DATA } from "../shared-props-data"; const DATA: ApiTableSectionProps[] = [ { - name: "InputNestedSelect specific props", + name: "InputNestedMultiSelect specific props", attributes: [ { name: "options", @@ -88,7 +88,7 @@ const DATA: ApiTableSectionProps[] = [ description: "If specified, the default no results display will not be rendered", propTypes: ["boolean"], - defaultValue: `"false"`, + defaultValue: "false", }, { name: "listStyleWidth", From a0da8a3c6e5bf8e3248d0f37657f1f6bbba8dc18 Mon Sep 17 00:00:00 2001 From: Wilker Date: Thu, 24 Aug 2023 13:16:02 +0800 Subject: [PATCH 47/47] [BOOKINGSG-4616][WK] remove trigger onHideOption during select item --- src/input-nested-multi-select/input-nested-multi-select.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index 8adbfde0b..deba99e06 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -133,7 +133,6 @@ export const InputNestedMultiSelect = ({ setSelectedKeyPaths(newKeyPaths); setSelectedItems(newSelectedItems); - triggerOptionDisplayCallback(false); if (selectorRef.current) selectorRef.current.focus();