best-doctor/ke

View on GitHub
src/common/components/AsyncDualSelectWidget/AsyncDualSelectWidget.tsx

Summary

Maintainability
D
1 day
Test Coverage
B
82%
import { Flex, usePrevious } from '@chakra-ui/react'
import React, { useCallback, useEffect, useState } from 'react'
import type { Provider } from 'admin/providers/interfaces'
import type { Pagination } from 'admin/providers/pagination/interfaces'
import { ArrowLeft, ArrowRight } from 'react-feather'

import { DebounceInput } from '@components/controls'
import {
  SelectButton,
  CenterContainer,
  SideContainer,
  SideContainerTitle,
  StyledSearchIcon,
  StyledCloseIcon,
} from './styles'
import { SelectList } from './SelectList/SelectList'
import { WidgetProps } from '../../../typing'
import { useWidgetInitialization } from '../../hooks/useWidgetInitialization'
import { getCopyHandler, getPayload } from '../../../DetailView/utils/dataAccess'
import { WidgetWrapper } from '../WidgetWrapper'

export type AsyncResult<OptionType, Additional = any> = {
  options: OptionType[]
  hasMore: boolean
  additional?: Additional
}

type LoadOptionsType = {
  options: object
  hasMore: boolean
  additional?: Function
}

type DualSelectWidgetProps = {
  provider: Provider
  dataResourceUrl: string
  getOptionLabel: (option: object | null) => string
  getOptionValue: (option: object | null) => string
  selectedTitle?: string
  availableTitle?: string
  selectButtonTitle?: string
  unselectButtonTitle?: string
} & WidgetProps

const AsyncDualSelectWidget = (props: DualSelectWidgetProps): JSX.Element => {
  const {
    name,
    style,
    helpText,
    notifier,
    provider,
    dataResourceUrl,
    selectedTitle = 'Selected Items',
    availableTitle = 'Available Items',
    selectButtonTitle = 'SELECT',
    unselectButtonTitle = 'UNSELECT',
    getOptionLabel,
    getOptionValue,
    containerStore,
    targetPayload,
    submitChange,
    copyValue,
    useClipboard,
  } = props

  const context = containerStore.getState()

  const { targetUrl, content, isRequired, widgetDescription } = useWidgetInitialization({
    ...props,
    context,
  })

  const handleChange = useCallback(
    (values: object[]): void => {
      const inputPayload = getPayload(values, name, targetPayload)
      submitChange({ url: targetUrl, payload: inputPayload })
    },
    [name, submitChange, targetPayload, targetUrl]
  )

  const [nextUrl, setNextUrl] = useState<string | null | undefined>('')
  const [options, setOptions] = useState<object[]>([])

  const getOptionsHandler = useCallback(
    async (url: string, searchQueryValue = ''): Promise<LoadOptionsType> => {
      const res = await provider
        .getPage(url, [
          {
            filterName: 'search',
            value: searchQueryValue,
          },
        ])
        .then(([data, , meta]: [object, object, Pagination]) => {
          const hasMore = !!meta.nextUrl
          setNextUrl(hasMore ? meta.nextUrl : '')
          return {
            options: data,
            hasMore,
          }
        })
      return res
    },
    [provider]
  )

  const loadOptions = useCallback(
    async ({ first = false, searchQueryValue = '' }): Promise<AsyncResult<LoadOptionsType>> => {
      let url = dataResourceUrl
      if (!first && !!nextUrl) {
        url = nextUrl
      }

      if (first || nextUrl) {
        const res = await getOptionsHandler(url, searchQueryValue)
        return res as AsyncResult<LoadOptionsType>
      }

      return Promise.resolve({
        options: [],
        hasMore: false,
      })
    },
    [dataResourceUrl, getOptionsHandler, nextUrl]
  )

  const [availableSelected, setAvailableSelected] = useState<string[]>([])
  const [selectedSelected, setSelectedSelected] = useState<string[]>([])

  const [selectedItems, setSelectedItems] = useState<string[] | null>(null)
  const selectedOptions =
    (selectedItems !== null && options.filter((o) => selectedItems.includes(getOptionValue(o)))) || []
  const availableOptions =
    (selectedItems !== null && options.filter((o) => !selectedItems.includes(getOptionValue(o)))) || []

  const isDisableSelect = availableSelected.length === 0
  const isDisableUnselect = selectedSelected.length === 0

  useEffect(() => {
    setSelectedItems([])
    loadOptions({ first: true }).then((res) => {
      setOptions(res.options)
    })
  }, [loadOptions])

  const allDeselect = useCallback(() => {
    setSelectedSelected([])
    setAvailableSelected([])
  }, [])

  const onChange = useCallback(() => {
    handleChange((selectedItems !== null && options.filter((o) => selectedItems.includes(getOptionValue(o)))) || [])
    allDeselect()
  }, [allDeselect, getOptionValue, handleChange, options, selectedItems])

  const selectButtonHandler = useCallback(() => {
    setSelectedItems(availableSelected.concat((selectedItems !== null && selectedItems) || []))
  }, [availableSelected, selectedItems])

  const unselectButtonHandler = useCallback(() => {
    setSelectedItems((selectedItems !== null && selectedItems.filter((si) => !selectedSelected.includes(si))) || [])
  }, [selectedItems, selectedSelected])

  const previousSelectedItems = usePrevious(selectedItems)
  useEffect(() => {
    if (
      selectedItems !== null &&
      previousSelectedItems !== null &&
      selectedItems.length !== previousSelectedItems.length
    ) {
      onChange()
    }
  }, [onChange, previousSelectedItems, selectedItems])

  const handleCopyValue = getCopyHandler(content, copyValue)

  const [searchActive, setSearchActive] = useState(false)
  const [searchValue, setSearchValue] = useState<string>('')
  const handleSearchToggle = useCallback(() => {
    if (searchActive) {
      setSearchValue('')
      loadOptions({ first: true }).then((res) => {
        setOptions(res.options)
      })
    }
    setSearchActive(!searchActive)
  }, [loadOptions, searchActive])

  return (
    <WidgetWrapper
      name={name}
      style={style}
      helpText={helpText || 'Items'}
      description={widgetDescription}
      required={isRequired}
      notifier={notifier}
      useClipboard={useClipboard}
      copyValue={handleCopyValue}
    >
      <Flex>
        <SideContainer data-testid="ds-left-list">
          <SideContainerTitle fontSize="md">
            {searchActive ? (
              <DebounceInput
                value={searchValue}
                onChange={(newValue) => {
                  loadOptions({ first: true, searchQueryValue: newValue }).then((res) => {
                    setOptions(res.options)
                  })
                  setSearchValue(newValue)
                }}
                style={{ paddingLeft: 5 }}
                borderWidth="1px"
                borderColor="gray.300"
                height="20px"
                debounceTimeout={700}
              />
            ) : (
              availableTitle
            )}
            {searchActive ? (
              <StyledCloseIcon onClick={handleSearchToggle} />
            ) : (
              <StyledSearchIcon
                style={{ ...(searchActive ? { color: 'dodgerblue' } : undefined) }}
                onClick={handleSearchToggle}
              />
            )}
          </SideContainerTitle>
          <SelectList
            values={availableOptions}
            selectedValues={availableSelected}
            disabledValues={[]}
            handleChange={(items) => {
              setAvailableSelected(items)
            }}
            handleScrollBottom={() => {
              loadOptions({}).then((res) => {
                setOptions(options.concat(res.options))
              })
            }}
            getOptionValue={getOptionValue}
            getOptionLabel={getOptionLabel}
          />
        </SideContainer>
        <CenterContainer>
          <SelectButton
            isDisabled={isDisableSelect}
            rightIcon={<ArrowRight />}
            variant="outline"
            onClick={selectButtonHandler}
          >
            {selectButtonTitle}
          </SelectButton>
          <SelectButton
            isDisabled={isDisableUnselect}
            leftIcon={<ArrowLeft />}
            variant="outline"
            onClick={unselectButtonHandler}
          >
            {unselectButtonTitle}
          </SelectButton>
        </CenterContainer>
        <SideContainer data-testid="ds-right-list">
          <SideContainerTitle fontSize="md">{selectedTitle}</SideContainerTitle>
          <SelectList
            values={selectedOptions}
            selectedValues={selectedSelected}
            handleChange={(items) => {
              setSelectedSelected(items)
            }}
            getOptionValue={getOptionValue}
            getOptionLabel={getOptionLabel}
          />
        </SideContainer>
      </Flex>
    </WidgetWrapper>
  )
}

export { AsyncDualSelectWidget }