import React, { useEffect, useMemo, useState } from 'react' import { type Meta, type StoryObj } from '@storybook/react-vite' import { DocsTemplate } from '../../../.storybook' import MultiSelectComponent, { type MultiSelectDataItemBase, } from './MultiSelect' import { generateArray, generateDummyNestedMultiSelectData, multiSelectDisabledList, multiSelectOptions, useApiMultiSelectStory, } from './MultiSelectStoryHelpers' import { CheckboxState, type DateCheckState, getFlattenNestedTreeList, type TableColumnConfigProps, } from '../Form/NestedCheckboxHelper' import { toast } from '../Toast/Toast' const meta: Meta = { title: 'Components/FormComponents/MultiSelect', component: MultiSelectComponent, parameters: { design: { type: 'figma', url: 'https://www.figma.com/design/RvhKD82948FMQnh5MyCi0o/Web-Design-System?node-id=4098-3396&t=9R4mlUY42yEaNlLp-4', }, docs: { page: () => ( The MultiSelect component is a user interface element that allows users to choose multiple options from a list of items. It is commonly used for multi-select forms, filtering options, or other scenarios where users need to make multiple selections simultaneously. The MultiSelect component facilitates efficient and convenient data input, especially when dealing with sets of choices. } infoBullets={[ Implement the MultiSelect component in forms, filtering interfaces, or settings sections where users need to select multiple options. , The MultiSelect component dynamically adds in a{' '} SearchBar when there are at least 10 options in the list or when the onSearchChange prop is passed in. , ]} /> ), }, }, } export default meta type Story = StoryObj // Data generation const nestedMultiSelectData = generateDummyNestedMultiSelectData() const options = multiSelectOptions const disabledOptionsList = multiSelectDisabledList // Define common data item type type StoryDataItem = MultiSelectDataItemBase & { name: string } // Story wrapper component for standard MultiSelect usage const StandardMultiSelect = (args: any) => { const [selected, setSelected] = useState( args.selectedOptions || [], ) return (
{ setSelected(selectedList) args.callout?.(selectedList) }} formLabelProps={args.formLabelProps} disabled={args.disabled} loading={args.loading} hideSelectAll={args.hideSelectAll} isReorderable={args.isReorderable} disabledItemsSection={args.disabledItemsSection} emptyStateProps={args.emptyStateProps} searchBarProps={args.searchBarProps} {...args} />
) } // Story wrapper component for API-driven MultiSelect (pagination) const PaginatedMultiSelect = (args: any) => { const { apiData, selectedData, fetchNew, setSelectedData } = useApiMultiSelectStory() return (
{ setSelectedData(selectedList) args.callout?.(selectedList) }} searchBarProps={{ show: false, }} {...args} />
) } // Story wrapper component for nested MultiSelect const NestedMultiSelect = (args: any) => { const [selected, setSelected] = useState(args.selectedOptions || []) // Handle nested functionality const flattenData = useMemo(() => { if (!args.isNested) return [] const dataToFlatten = args.options || nestedMultiSelectData return getFlattenNestedTreeList({ nestedData: dataToFlatten, childrenKey: 'children', parentKey: 'parent_category_id', idKey: 'id', }) }, [args.isNested, args.options]) const defaultItemStates = useMemo( () => flattenData.map((i: any) => ({ id: i['id'] as string | number, state: CheckboxState.UNCHECKED, })), [flattenData], ) const identifierKeys = { displayKey: 'name', idKey: 'id', parentKey: 'parent_category_id', } const tableConfig: TableColumnConfigProps[] = [ { label: '', dataKey: 'name', children: (data: any) => ( {data.name as string} ), }, ] const [itemStates, setItemStates] = useState(defaultItemStates) // Update itemStates when flattenData changes useEffect(() => { setItemStates(defaultItemStates) }, [defaultItemStates]) return (
{ setSelected(selectedList) args.callout?.(selectedList) }} nestedConfig={{ data: flattenData, itemStates: itemStates, setItemStates: setItemStates, identifierKeys: identifierKeys, tableConfig, ignoreNodeHierarchy: args.ignoreNodeHierarchy, }} searchBarProps={{ show: true, }} formLabelProps={args.formLabelProps} emptyStateProps={args.emptyStateProps} {...args} />
) } // Story wrapper for custom disabled key functionality const CustomDisabledKeyMultiSelect = (args: any) => { const [selected, setSelected] = useState([]) const [listOptions, setListOptions] = useState(args.options || []) const [isMaxItemsSelected, setIsMaxItemsSelected] = useState(false) useEffect(() => { const baseOptions = args.options || [] const unselectedItems = baseOptions .filter( (option: any) => !selected.find( (selectedOption: any) => selectedOption.name === option.name, ), ) .map((option: any) => ({ ...option, disabled: isMaxItemsSelected, })) setListOptions(unselectedItems) }, [selected, isMaxItemsSelected, args.options]) return (
{ setSelected(selectedList) if (selectedList.length >= 5) { setIsMaxItemsSelected(true) } else { setIsMaxItemsSelected(false) } args.callout?.(selectedList) }} searchBarProps={{ placeholder: args.searchBarProps?.placeholder, show: true, }} formLabelProps={args.formLabelProps} disabledItemsSection={args.disabledItemsSection} {...args} />
) } const StandardMultiSelectWithMinWidth = (args: any) => { const [selected, setSelected] = useState( args.selectedOptions || [], ) return (
{ setSelected(selectedList) args.callout?.(selectedList) }} formLabelProps={args.formLabelProps} disabled={args.disabled} loading={args.loading} hideSelectAll={args.hideSelectAll} isReorderable={args.isReorderable} disabledItemsSection={args.disabledItemsSection} emptyStateProps={args.emptyStateProps} searchBarProps={args.searchBarProps} {...args} />
) } // Basic Stories export const Basic: Story = { render: (args) => , args: { options: options, selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const FewOptions: Story = { render: (args) => , args: { options: generateArray(3), selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const NoOptions: Story = { render: (args) => , args: { options: [], selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const Required: Story = { render: (args) => , args: { options: options, selectedOptions: [], formLabelProps: { label: 'People', required: true, }, }, } export const LabelTooltip: Story = { render: (args) => , args: { options: options, selectedOptions: [], formLabelProps: { label: 'People', tooltip: { tooltipContent: 'This is the tooltip content.', }, }, }, } export const RightLabel: Story = { render: (args) => , args: { options: options, selectedOptions: [], formLabelProps: { label: 'People', rightLabel: 'Right Label', }, }, } export const WithoutLabel: Story = { render: (args) => , args: { options: options, selectedOptions: [], formLabelProps: undefined, }, } export const SelectedOptions: Story = { render: (args) => , args: { options: options, selectedOptions: [options[8], options[5]], formLabelProps: { label: 'People' }, }, } export const DisabledOptions: Story = { render: (args) => , args: { options: disabledOptionsList, selectedOptions: [ disabledOptionsList[22], disabledOptionsList[32], disabledOptionsList[5], ], formLabelProps: { label: 'People' }, }, } export const ManyOptions: Story = { render: (args) => , args: { options: generateArray(1000), selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const LongNameOptions: Story = { render: (args) => , args: { options: [ { ...options[0], name: 'This is a long option name that will wrap to the next line and continue with another sentence to demonstrate the wrapping behavior', }, ...options.slice(1), ], selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const WordBreak: Story = { render: (args) => , args: { options: [ { ...options[0], name: 'This is a loooooooooooooooooooooooooooooooooooooooooong character name', }, ...options.slice(1), ], selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const WithSecondaryOptions: Story = { render: (args) => , args: { options: generateArray(50, true), selectedOptions: [], formLabelProps: { label: 'People' }, }, } export const Disabled: Story = { render: (args) => , args: { options: options, selectedOptions: [], disabled: true, formLabelProps: { label: 'People' }, }, } export const Loading: Story = { render: (args) => , args: { options: options, selectedOptions: [], loading: true, formLabelProps: { label: 'People' }, }, } export const HideSelectAll: Story = { render: (args) => , args: { options: options, selectedOptions: [], hideSelectAll: true, formLabelProps: { label: 'People' }, }, } export const ReorderableMultiSelect: Story = { render: (args) => , args: { options: options, selectedOptions: [], isReorderable: true, formLabelProps: { label: 'People' }, }, } export const DisabledItemsSection: Story = { render: (args) => , args: { options: disabledOptionsList, selectedOptions: [], disabledItemsSection: { enabled: true, }, formLabelProps: { label: 'People' }, }, } export const DisabledSectionHeaderTitle: Story = { render: (args) => , args: { options: disabledOptionsList.map((item) => item.disabled ? { ...item, secondaryOption: 'Secondary Text' } : item, ), selectedOptions: [], disabledItemsSection: { enabled: true, primaryTitle: 'Removed', secondaryTitle: 'Reason', }, formLabelProps: { label: 'People' }, }, } export const DisabledSectionWithNoDisabledItems: Story = { render: (args) => , args: { options: options, selectedOptions: [], disabledItemsSection: { enabled: true, }, formLabelProps: { label: 'People' }, }, } export const DisabledSectionWithCustomDisabledKey: Story = { render: (args) => , args: { options: disabledOptionsList.map((item) => item.disabled ? { ...item, secondaryOption: 'Secondary Text', notAllowed: true, } : item, ), selectedOptions: [], formLabelProps: { label: 'People', rightLabel: 'Max 5 options can be selected', }, disabledItemsSection: { enabled: true, primaryTitle: 'Removed', secondaryTitle: 'Reason', disabledItemsKey: 'notAllowed', }, }, } export const MultiSelectWithPagination: Story = { render: (args) => , args: {}, } export const Nested: Story = { render: (args) => , args: { options: nestedMultiSelectData, selectedOptions: [], emptyStateProps: { primaryText: 'No Category Found', secondaryText: 'Try searching for a different category', }, formLabelProps: { label: 'Category', }, selectPlaceholder: '-- Select Category --', }, } export const DisabledHierarchy: Story = { render: (args) => ( ), args: { options: nestedMultiSelectData, selectedOptions: [], }, parameters: { docs: { description: { story: `This example demonstrates the MultiSelect with disabled hierarchy behavior in nested mode. When \`ignoreNodeHierarchy\` is set to \`true\`, selecting or deselecting a node will not affect its parent or child nodes. This is useful when you want to allow independent selection of nodes regardless of their hierarchical relationship.`, }, }, }, } export const OnFocus: Story = { render: (args) => , args: { options: options, selectedOptions: [], onFocus: () => { toast({ type: 'success', message: 'onFocus called' }) }, }, } export const minWidthMultiSelect: Story = { render: (args) => , args: { options: generateArray(30), selectedOptions: [], formLabelProps: { label: 'People' }, }, }