huridocs/uwazi

View on GitHub
app/react/V2/Components/Forms/MultiselectList.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
F
2%
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable max-statements */
/* eslint-disable react/no-multi-comp */
import React, { useEffect, useState } from 'react';
import { Translate } from 'app/I18N';
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import { InputField, RadioSelect } from '.';
import { Pill } from '../UI/Pill';
import { Label } from './Label';
import { Checkbox } from './Checkbox';

interface Option {
  label: string | React.ReactNode;
  searchLabel: string;
  value: string;
  items?: Option[];
}

interface MultiselectListProps {
  items: Option[];
  onChange: (selectedItems: string[]) => void;
  label?: string | React.ReactNode;
  hasErrors?: boolean;
  className?: string;
  checkboxes?: boolean;
  value?: string[];
  foldableGroups?: boolean;
  singleSelect?: boolean;
}

const SelectedCounter = ({ selectedItems }: { selectedItems: string[] }) => (
  <>
    <Translate>Selected</Translate> {selectedItems.length ? `(${selectedItems.length})` : ''}
  </>
);

const MultiselectList = ({
  items,
  onChange,
  className,
  label,
  hasErrors,
  value = [],
  checkboxes = false,
  foldableGroups = false,
  singleSelect = false,
}: MultiselectListProps) => {
  const [selectedItems, setSelectedItems] = useState<string[]>(value);
  const [showAll, setShowAll] = useState<boolean>(true);
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [openGroups, setOpenGroups] = useState<string[]>([]);

  useEffect(() => {
    if (onChange) onChange(selectedItems);
  }, [onChange, selectedItems]);

  useEffect(() => {
    let filtered = [...items];
    if (!showAll) {
      filtered = filtered
        .filter(item => {
          const itemiSelected = selectedItems.includes(item.value);
          const containsSelected = item.items?.some(childItem =>
            selectedItems.includes(childItem.value)
          );

          return itemiSelected || containsSelected;
        })
        .map(item => {
          if (item.items) {
            return {
              ...item,
              items: item.items.filter(childItem => selectedItems.includes(childItem.value)),
            };
          }
          return item;
        });
    }

    if (!searchTerm) {
      setFilteredItems(filtered);
      return;
    }

    filtered = filtered
      .filter(({ searchLabel }) => searchLabel.toLowerCase().includes(searchTerm.toLowerCase()))
      .map(item => {
        if (item.items) {
          return {
            ...item,
            items: item.items.filter(({ searchLabel }) =>
              searchLabel.toLowerCase().includes(searchTerm.toLowerCase())
            ),
          };
        }
        return item;
      });

    setFilteredItems(filtered);
  }, [items, searchTerm, showAll, selectedItems]);

  const handleSelect = (_value: string) => {
    let newValue;
    if (singleSelect) {
      newValue = selectedItems.includes(_value) ? [] : [_value];
    } else {
      newValue = selectedItems.includes(_value)
        ? selectedItems.filter(item => item !== _value)
        : [...selectedItems, _value];
    }

    setSelectedItems(newValue);
  };

  const applyFilter = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
    setShowAll(target.value === 'true');
  };

  const renderButtonItem = (item: Option) => {
    if (item.items) {
      return renderGroup(item);
    }

    const selected = selectedItems.includes(item.value);
    const borderSyles = selected
      ? 'border-sucess-200'
      : 'border-transparent hover:border-primary-300';

    return (
      <li key={item.value} className="mb-4">
        <button
          type="button"
          className={`w-full flex text-left p-2.5 border ${borderSyles} rounded-lg items-center`}
          onClick={() => handleSelect(item.value)}
        >
          <span className="flex-1">{item.label}</span>
          <div className="flex-1">
            <Pill className="float-right" color={selected ? 'green' : 'primary'}>
              {selected ? <Translate>Selected</Translate> : <Translate>Select</Translate>}
            </Pill>
          </div>
        </button>
      </li>
    );
  };

  const renderCheckboxItem = (item: Option) => {
    if (item.items) {
      return renderGroup(item);
    }
    const selected = selectedItems.includes(item.value);
    return (
      <li key={item.value} className="mb-2">
        <Checkbox
          name={item.value}
          label={item.label}
          checked={selected}
          onChange={() => handleSelect(item.value)}
        />
      </li>
    );
  };

  const handleGroupToggle = (groupKey: string) => {
    if (openGroups.includes(groupKey)) {
      setOpenGroups(openGroups.filter(group => group !== groupKey));
    } else {
      setOpenGroups([...openGroups, groupKey]);
    }
  };

  const isGroupOpen = (groupKey: string) => openGroups.includes(groupKey);

  const renderItem = (item: Option) =>
    checkboxes ? renderCheckboxItem(item) : renderButtonItem(item);

  const renderGroup = (group: Option) => {
    const isOpen = isGroupOpen(group.value);
    if (foldableGroups) {
      return (
        <li key={group.value} className="mb-4">
          <div
            className={`flex justify-between p-3 mb-4 rounded-lg ${isOpen ? 'bg-indigo-50' : 'bg-gray-50'}`}
            onClick={() => handleGroupToggle(group.value)}
          >
            <span className="block text-sm font-bold text-gray-900">{group.label}</span>
            <button
              className="text-indigo-800 bg-indigo-200 rounded-[6px] text-xs font-medium px-1.5 py-0.5 flex flex-row items-center justify-center gap-1"
              type="button"
            >
              <div className="w-3 h-3 text-sm">
                {isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
              </div>
              <Translate>Properties</Translate>
            </button>
          </div>
          {isOpen && <ul className="pl-4">{group.items?.map(renderItem)}</ul>}
        </li>
      );
    }

    return (
      <li key={group.value} className="mb-4">
        <span className="block mb-4 text-sm font-bold text-gray-900">{group.label}</span>
        <ul className="">{group.items?.map(renderItem)}</ul>
      </li>
    );
  };

  return (
    <div className={`flex flex-col relative ${className}`}>
      <div className="sticky top-0 w-full mb-2">
        <Label htmlFor="search-multiselect" hideLabel={!label} hasErrors={Boolean(hasErrors)}>
          {label}
        </Label>
        <InputField
          id="search-multiselect"
          label="search-multiselect"
          hideLabel
          onChange={e => setSearchTerm(e.target.value)}
          placeholder="Search"
          value={searchTerm}
          clearFieldAction={() => setSearchTerm('')}
        />
        <RadioSelect
          name="filter"
          orientation="horizontal"
          options={[
            {
              label: <Translate>All</Translate>,
              value: 'true',
              defaultChecked: true,
            },
            {
              label: <SelectedCounter selectedItems={selectedItems} />,
              value: 'false',
              disabled: selectedItems.length === 0,
            },
          ]}
          onChange={applyFilter}
          className="px-1 pt-4"
        />
      </div>

      <ul className="px-2 pt-2 w-full overflow-y-scroll max-h-[calc(100vh_-_9rem)]">
        {filteredItems.map(renderItem)}
      </ul>
    </div>
  );
};

export { MultiselectList };