dashpresshq/dashpress

View on GitHub
src/frontend/components/app/list-manager/index.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import { msg } from "@lingui/macro";
import { useEffect, useState } from "react";
import SortableList, { SortableItem } from "react-easy-sort";

import type { DataStateKeys } from "@/frontend/lib/data/types";
import { arrayMoveImmutable } from "@/shared/lib/array/move";
import { sortListByOrder } from "@/shared/lib/array/sort";

import { EmptyWrapper } from "../empty-wrapper";
import type { IEmptyWrapperProps } from "../empty-wrapper/types";
import { FormSearch } from "../form/input/search";
import { ListSkeleton } from "../skeleton/list";
import { ViewStateMachine } from "../view-state-machine";
import type { IListMangerItemProps } from "./list-manager-item";
import { ListManagerItem } from "./list-manager-item";
import { defaultSearchFunction, defaultToEmptyArray } from "./utils";

const SEARCH_THRESHOLD = 10;

type StringProps<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

export interface IProps<T, K extends StringProps<T>> {
  items: DataStateKeys<T[]>;
  listLengthGuess: number;
  sort?: {
    orderList: string[];
    key: StringProps<T>;
    on: (data: string[]) => void;
  };
  labelField: K;
  empty?: IEmptyWrapperProps;
  getLabel?: (name: string) => string;
  render: (item: T & { label: string }, index: number) => IListMangerItemProps;
}

export function ListManager<T, K extends StringProps<T>>({
  labelField,
  listLengthGuess,
  getLabel,
  items,
  empty,
  sort,
  render,
}: IProps<T, K>) {
  const itemsLength = items.data.length;
  const [searchString, setSearchString] = useState("");

  const [itemsData, setItemsData] = useState<Array<T>>([]);

  const onSortEnd = (oldOrder: number, newOrder: number) => {
    const newOrderItems = arrayMoveImmutable(itemsData, oldOrder, newOrder);
    setItemsData(newOrderItems);
    sort?.on(newOrderItems.map((item) => item[sort.key] as unknown as string));
  };

  useEffect(() => {
    let itemsData$1 = defaultToEmptyArray(items.data);

    if (sort?.orderList) {
      itemsData$1 = sortListByOrder(sort.orderList, itemsData$1, sort.key);
    }

    setItemsData(itemsData$1);
  }, [items.data.length, sort?.orderList.length]);

  const labelledItems: Array<T & { label: string }> = itemsData.map((item) => ({
    ...item,
    label: getLabel
      ? getLabel(item[labelField] as unknown as string)
      : item[labelField],
  })) as Array<T & { label: string }>;

  const searchResults =
    searchString.length > 0
      ? defaultSearchFunction(labelledItems, searchString, labelField)
      : labelledItems;

  return (
    <ViewStateMachine
      error={items.error}
      loading={items.isLoading}
      loader={<ListSkeleton count={listLengthGuess} />}
    >
      {itemsLength === 0 ? (
        <EmptyWrapper {...{ ...empty }} />
      ) : (
        <ul className="-mx-4 -mb-4 flex flex-col rounded-none pl-0">
          {itemsLength > SEARCH_THRESHOLD ? (
            <FormSearch onChange={setSearchString} />
          ) : null}
          <SortableList
            onSortEnd={onSortEnd}
            // eslint-disable-next-line tailwindcss/no-custom-classname
            className="list"
            draggedItemClassName="[&_.grab-icon]:cursor-grabbing"
          >
            {searchResults.map((item, index) => (
              <SortableItem key={item[labelField] as unknown as string}>
                {/* eslint-disable-next-line tailwindcss/no-custom-classname */}
                <div className="item z-[1000]">
                  <ListManagerItem
                    {...render(item, index)}
                    sortable={
                      !!sort && searchString.length === 0 && itemsLength > 1
                    }
                  />
                </div>
              </SortableItem>
            ))}
          </SortableList>
          {searchResults.length === 0 && searchString.length > 0 ? (
            <EmptyWrapper text={msg`No Search Results`} />
          ) : null}
        </ul>
      )}
    </ViewStateMachine>
  );
}