teableio/teable

View on GitHub
apps/nextjs-app/src/features/app/blocks/view/kanban/components/KanbanContainer.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { DragDropContext, Droppable } from '@hello-pangea/dnd';
import type { DropResult } from '@hello-pangea/dnd';
import { FieldKeyType, FieldType } from '@teable/core';
import type { IUpdateRecordRo } from '@teable/openapi';
import { generateLocalId } from '@teable/sdk/components';
import { useTableId, useViewId } from '@teable/sdk/hooks';
import { Record } from '@teable/sdk/model';
import { keyBy } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { UNCATEGORIZED_STACK_ID } from '../constant';
import type { IKanbanContext } from '../context';
import { useKanban } from '../hooks';
import { useKanbanStackCollapsedStore } from '../store';
import { getCellValueByStack, moveTo, reorder } from '../utils';
import type { ICardMap } from './interface';
import { KanbanStackContainer } from './KanbanStackContainer';
import { KanbanStackCreator } from './KanbanStackCreator';

const EMPTY_LIST: never[] = [];

export const KanbanContainer = () => {
  const tableId = useTableId();
  const viewId = useViewId();
  const { collapsedStackMap } = useKanbanStackCollapsedStore();
  const { permission, stackField, stackCollection } = useKanban() as Required<IKanbanContext>;

  const [cardMap, setCardMap] = useState<ICardMap>({});
  const [stackIds, setStackIds] = useState(stackCollection.map(({ id }) => id));

  const localId = generateLocalId(tableId, viewId);
  const { stackCreatable } = permission;
  const { id: fieldId, type: fieldType, isLookup } = stackField;
  const isSingleSelectField = fieldType === FieldType.SingleSelect && !isLookup;

  const collapsedStackIdSet = useMemo(() => {
    return new Set(collapsedStackMap[localId] ?? []);
  }, [localId, collapsedStackMap]);

  useEffect(() => {
    setStackIds(stackCollection.map(({ id }) => id));
  }, [stackCollection]);

  const stackMap = useMemo(() => keyBy(stackCollection, 'id'), [stackCollection]);

  const setCardMapInner = useCallback((partialCardMap: ICardMap) => {
    setCardMap((prev) => ({ ...prev, ...partialCardMap }));
  }, []);

  // eslint-disable-next-line sonarjs/cognitive-complexity
  const onDragEnd = (result: DropResult) => {
    const { source, destination } = result;

    if (!destination) return;

    const { droppableId: sourceStackId, index: sourceIndex } = source;
    const { droppableId: targetStackId, index: targetIndex } = destination;

    if (sourceStackId === viewId) {
      const newStackIds = reorder(stackIds, sourceIndex, targetIndex);

      if (!isSingleSelectField || sourceIndex === targetIndex) {
        return;
      }

      setStackIds(newStackIds);

      const { choices } = stackField.options;
      const choiceMap = keyBy(choices, 'name');
      const newChoices = newStackIds
        .map((choiceId) => {
          if (choiceId === UNCATEGORIZED_STACK_ID) return;
          const stack = stackMap[choiceId];
          if (stack == null) return;
          return choiceMap[stack.data as string];
        })
        .filter(Boolean);
      stackField.convert({
        type: fieldType,
        options: { ...stackField.options, choices: newChoices },
      });
      return;
    }

    if (sourceStackId === targetStackId) {
      const cards = cardMap[sourceStackId];
      const cardCount = cards?.length;

      if (!cardCount) return;

      if (sourceIndex < cardCount && targetIndex < cardCount) {
        if (tableId && viewId) {
          Record.updateRecordOrders(tableId, viewId, {
            anchorId: cards[targetIndex].id,
            position: targetIndex > sourceIndex ? 'after' : 'before',
            recordIds: [cards[sourceIndex].id],
          });
        }

        const newCards = reorder(cards, sourceIndex, targetIndex);

        setCardMapInner({ [sourceStackId]: newCards });
      }
      return;
    }

    const sourceCards = cardMap[sourceStackId];
    const targetCards = cardMap[targetStackId];
    const sourceCardId = sourceCards?.[sourceIndex]?.id;
    const targetCardId = targetCards?.[targetIndex]?.id;

    if (tableId && viewId && sourceCardId) {
      const stack = stackCollection.find(({ id }) => id === targetStackId);

      if (stack == null) return;

      const fieldValue = getCellValueByStack(stack);

      const recordRo: IUpdateRecordRo = {
        fieldKeyType: FieldKeyType.Id,
        record: {
          fields: {
            [fieldId]: fieldValue,
          },
        },
      };

      // Drag a card to the end of another stack
      if (targetCardId == null) {
        if (targetIndex !== 0) {
          const lastTargetCardId = targetCards?.[targetIndex - 1]?.id;
          if (lastTargetCardId != null) {
            recordRo.order = {
              viewId,
              anchorId: lastTargetCardId,
              position: 'after',
            };
          }
        }
      } else {
        recordRo.order = {
          viewId,
          anchorId: targetCardId,
          position: 'before',
        };
      }

      Record.updateRecord(tableId, sourceCardId, recordRo);
    }

    const { sourceList, targetList } = moveTo({
      source: sourceCards,
      target: targetCards,
      sourceIndex,
      targetIndex,
    });

    setCardMapInner({
      [sourceStackId]: sourceList,
      [targetStackId]: targetList,
    });
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className="flex h-full">
        <Droppable droppableId={viewId!} direction="horizontal" type="column">
          {(provided) => {
            const { droppableProps, placeholder } = provided;

            return (
              <div ref={provided.innerRef} {...droppableProps} className="flex shrink-0">
                {stackIds.map((stackId, index) => {
                  const stack = stackMap[stackId];
                  if (stack == null) return null;
                  const isCollapsed = collapsedStackIdSet.has(stackId as string);
                  return (
                    <KanbanStackContainer
                      key={stackId}
                      index={index}
                      stack={stack}
                      cards={cardMap[stackId] ?? EMPTY_LIST}
                      setCardMap={setCardMapInner}
                      disabled={!isSingleSelectField}
                      isCollapsed={isCollapsed}
                    />
                  );
                })}
                {placeholder}
              </div>
            );
          }}
        </Droppable>
        {stackCreatable && isSingleSelectField && (
          <div className="pr-2">
            <KanbanStackCreator />
          </div>
        )}
      </div>
    </DragDropContext>
  );
};