import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { extractClosestEdge, type Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"; import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; import { triggerPostMoveFlash } from "@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash"; import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region"; import { IBase, IBaseRow, IBaseView, FilterGroup, KANBAN_CARD_DRAG_TYPE, KANBAN_COLUMN_DRAG_TYPE } from "@/ee/base/types/base.types"; import { useKanbanColumns } from "@/ee/base/hooks/use-kanban-columns"; import { useUpdateViewMutation } from "@/ee/base/queries/base-view-query"; import { useKanbanMoveCardMutation } from "@/ee/base/queries/base-row-query"; import { buildColumnFilter } from "@/ee/base/services/kanban-column-filter"; import { resolveCardDrop } from "@/ee/base/hooks/use-kanban-card-drop"; import { useKanbanBoardAutoScroll } from "@/ee/base/hooks/use-kanban-autoscroll"; import { useRowDetailModal } from "@/ee/base/hooks/use-row-detail-modal"; import { KanbanColumn } from "@/ee/base/components/kanban/kanban-column"; import { KanbanEmptyState } from "@/ee/base/components/kanban/kanban-empty-state"; import classes from "@/ee/base/styles/kanban.module.css"; type BaseKanbanProps = { base: IBase; view: IBaseView; pageId: string; embedded?: boolean; editable: boolean; viewFilter: FilterGroup | undefined; }; export function BaseKanban({ base, view, pageId, embedded, editable, viewFilter }: BaseKanbanProps) { const { t } = useTranslation(); const { groupByPropertyId, columns, hasValidGroupBy } = useKanbanColumns(base, view); const updateView = useUpdateViewMutation(); const moveCard = useKanbanMoveCardMutation(); const { openRow } = useRowDetailModal(pageId); const openRowRef = useRef(openRow); useLayoutEffect(() => { openRowRef.current = openRow; }); const handleOpenRow = useCallback((id: string) => openRowRef.current(id), []); const boardRef = useRef(null); useKanbanBoardAutoScroll(boardRef, pageId); const cardRefs = useRef>(new Map()); const registerCardRef = useCallback((rowId: string, columnKey: string, el: HTMLDivElement | null) => { if (el) { cardRefs.current.set(rowId, { columnKey, el }); } else { cardRefs.current.delete(rowId); } }, []); const columnRows = useRef>(new Map()); const registerColumnRows = useCallback((key: string, rows: IBaseRow[]) => { columnRows.current.set(key, rows); }, []); const hideColumn = useCallback( (key: string) => { const next = Array.from(new Set([...(view.config?.hiddenChoiceIds ?? []), key])); updateView.mutate({ viewId: view.id, pageId, config: { hiddenChoiceIds: next } }); }, [updateView, view.id, view.config?.hiddenChoiceIds, pageId], ); const onCardDropRef = useRef<(args: { draggedRowId: string; sourceColumnKey: string; targetColumnKey: string; targetRowId: string | null; edge: Edge | null; }) => void>(() => {}); useLayoutEffect(() => { onCardDropRef.current = ({ draggedRowId, sourceColumnKey, targetColumnKey, targetRowId, edge }) => { if (!groupByPropertyId) return; const targetColumnRows = columnRows.current.get(targetColumnKey) ?? []; const result = resolveCardDrop({ draggedRowId, targetRowId, edge: edge === "left" || edge === "right" ? null : edge, targetColumnKey, sourceColumnKey, targetColumnRows, }); if (!result) return; const sourceFilter = buildColumnFilter(viewFilter, groupByPropertyId, sourceColumnKey); const destFilter = buildColumnFilter(viewFilter, groupByPropertyId, targetColumnKey); moveCard.mutate({ pageId, rowId: draggedRowId, sourceColumnFilter: sourceFilter, destColumnFilter: destFilter, columnChanged: result.columnChanged, groupByPropertyId, destChoiceValue: result.destChoiceValue, position: result.position, }); const el = cardRefs.current.get(draggedRowId)?.el; if (el) triggerPostMoveFlash(el); const targetColumnName = columns.find((c) => c.key === targetColumnKey)?.name ?? ""; liveRegion.announce(t("Moved card to {{column}}", { column: targetColumnName })); }; }); useEffect(() => { return monitorForElements({ canMonitor: ({ source }) => source.data?.type === KANBAN_CARD_DRAG_TYPE && source.data?.pageId === pageId, onDrop: ({ location, source }) => { const target = location.current.dropTargets[0]; if (!target) return; const draggedRowId = source.data.rowId as string; const sourceColumnKey = source.data.columnKey as string; const targetColumnKey = target.data.columnKey as string; const isColumnBody = target.data.isColumnBody === true; const targetRowId = isColumnBody ? null : (target.data.rowId as string); const edge = isColumnBody ? null : extractClosestEdge(target.data); onCardDropRef.current({ draggedRowId, sourceColumnKey, targetColumnKey, targetRowId, edge }); }, }); }, [pageId]); const onColumnDropRef = useRef<(args: { sourceColumnKey: string; targetColumnKey: string; edge: Edge | null; }) => void>(() => {}); useLayoutEffect(() => { onColumnDropRef.current = ({ sourceColumnKey, targetColumnKey, edge }) => { const fullOrder: string[] = view.config?.choiceOrder?.length ? view.config.choiceOrder : columns.map((c) => c.key); const startIndex = fullOrder.indexOf(sourceColumnKey); const indexOfTarget = fullOrder.indexOf(targetColumnKey); if (startIndex === -1 || indexOfTarget === -1) { const visibleKeys = columns.map((c) => c.key); const visStart = visibleKeys.indexOf(sourceColumnKey); const visTarget = visibleKeys.indexOf(targetColumnKey); if (visStart === -1 || visTarget === -1) return; const finishIndex = getReorderDestinationIndex({ startIndex: visStart, indexOfTarget: visTarget, closestEdgeOfTarget: edge, axis: "horizontal", }); if (finishIndex === visStart) return; const reorderedVisible = reorder({ list: visibleKeys, startIndex: visStart, finishIndex }); updateView.mutate({ viewId: view.id, pageId, config: { choiceOrder: [...reorderedVisible, ...(view.config?.hiddenChoiceIds ?? [])] } }); } else { const finishIndex = getReorderDestinationIndex({ startIndex, indexOfTarget, closestEdgeOfTarget: edge, axis: "horizontal", }); if (finishIndex === startIndex) return; const newChoiceOrder = reorder({ list: fullOrder, startIndex, finishIndex }); updateView.mutate({ viewId: view.id, pageId, config: { choiceOrder: newChoiceOrder } }); } const targetColumnName = columns.find((c) => c.key === targetColumnKey)?.name ?? ""; liveRegion.announce(t("Moved column to {{column}}", { column: targetColumnName })); }; }); useEffect(() => { return monitorForElements({ canMonitor: ({ source }) => source.data?.type === KANBAN_COLUMN_DRAG_TYPE && source.data?.pageId === pageId, onDrop: ({ location, source }) => { const target = location.current.dropTargets[0]; if (!target) return; const sourceColumnKey = source.data.columnKey as string; const targetColumnKey = target.data.columnKey as string; const edge = extractClosestEdge(target.data); onColumnDropRef.current({ sourceColumnKey, targetColumnKey, edge }); }, }); }, [pageId]); if (!hasValidGroupBy) { return ; } return (
{columns.map((column) => ( ))}
); }