import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { Popover, Switch, Stack, Text, Group, UnstyledButton, ScrollArea } from "@mantine/core"; import { IconGripVertical, type IconLetterT } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { IBase, IBaseProperty, IBaseView } from "@/ee/base/types/base.types"; import { useUpdateViewMutation } from "@/ee/base/queries/base-view-query"; import { propertyTypes } from "@/ee/base/property-types/property-type.registry"; import { BaseDropEdgeIndicator } from "@/ee/base/components/grid/base-drop-edge-indicator"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements, } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { attachClosestEdge, 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 cellClasses from "@/ee/base/styles/cells.module.css"; import propClasses from "@/ee/base/styles/property.module.css"; const DRAG_TYPE = "base-card-property"; type KanbanCardPropertiesProps = { opened: boolean; onClose: () => void; base: IBase; view: IBaseView; pageId: string; children: React.ReactNode; }; export function KanbanCardProperties({ opened, onClose, base, view, pageId, children, }: KanbanCardPropertiesProps) { const { t } = useTranslation(); const updateView = useUpdateViewMutation(); const nonPrimaryProperties = base.properties.filter((p) => !p.isPrimary); const visibleIds = view.config?.visiblePropertyIds ?? []; const savedOrder = view.config?.propertyOrder ?? []; const orderedProperties = [ ...savedOrder .map((id) => nonPrimaryProperties.find((p) => p.id === id)) .filter((p): p is IBaseProperty => p !== undefined), ...nonPrimaryProperties.filter((p) => !savedOrder.includes(p.id)), ]; const primaryProperty = base.properties.find((p) => p.isPrimary); const PrimaryIcon = primaryProperty ? propertyTypes.find((pt) => pt.type === primaryProperty.type)?.icon : undefined; const handleToggle = useCallback( (propertyId: string, checked: boolean) => { const next = checked ? [...visibleIds, propertyId] : visibleIds.filter((id) => id !== propertyId); updateView.mutate({ viewId: view.id, pageId, config: { visiblePropertyIds: next } }); }, [updateView, view.id, visibleIds, pageId], ); const handleReorder = useCallback( (activeId: string, targetId: string, edge: Edge) => { const startIndex = orderedProperties.findIndex((p) => p.id === activeId); const indexOfTarget = orderedProperties.findIndex((p) => p.id === targetId); if (startIndex === -1 || indexOfTarget === -1) return; const finishIndex = getReorderDestinationIndex({ startIndex, indexOfTarget, closestEdgeOfTarget: edge, axis: "vertical", }); if (finishIndex === startIndex) return; const reordered = reorder({ list: orderedProperties, startIndex, finishIndex }); updateView.mutate({ viewId: view.id, pageId, config: { propertyOrder: reordered.map((p) => p.id) }, }); }, [orderedProperties, updateView, view.id, pageId], ); return ( { if (!o) onClose(); }} onClose={onClose} position="bottom-end" shadow="md" width={260} trapFocus closeOnEscape closeOnClickOutside withinPortal > {children} {t("Card properties")} {primaryProperty && (
{PrimaryIcon && } {primaryProperty.name} {}} styles={{ track: { cursor: "default" } }} />
)} {orderedProperties.map((p) => { const isVisible = visibleIds.includes(p.id); const typeConfig = propertyTypes.find((pt) => pt.type === p.type); const TypeIcon = typeConfig?.icon; return ( ); })}
); } type SortablePropertyRowProps = { property: IBaseProperty; isVisible: boolean; TypeIcon: typeof IconLetterT | undefined; onToggle: (propertyId: string, checked: boolean) => void; onReorder: (activeId: string, targetId: string, edge: Edge) => void; }; function SortablePropertyRow({ property, isVisible, TypeIcon, onToggle, onReorder, }: SortablePropertyRowProps) { const rowRef = useRef(null); const handleRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [closestEdge, setClosestEdge] = useState(null); const onReorderRef = useRef(onReorder); useLayoutEffect(() => { onReorderRef.current = onReorder; }); useEffect(() => { const row = rowRef.current; const handle = handleRef.current; if (!row || !handle) return; return combine( draggable({ element: row, dragHandle: handle, getInitialData: () => ({ type: DRAG_TYPE, propertyId: property.id }), onDragStart: () => setIsDragging(true), onDrop: () => setIsDragging(false), }), dropTargetForElements({ element: row, canDrop: ({ source }) => source.data.type === DRAG_TYPE && source.data.propertyId !== property.id, getData: ({ input, element }) => attachClosestEdge( { propertyId: property.id }, { input, element, allowedEdges: ["top", "bottom"] }, ), onDrag: ({ self }) => setClosestEdge(extractClosestEdge(self.data)), onDragLeave: () => setClosestEdge(null), onDrop: ({ source, self }) => { setClosestEdge(null); const edge = extractClosestEdge(self.data); if (!edge) return; onReorderRef.current(source.data.propertyId as string, property.id, edge); }, }), ); }, [property.id]); return (
onToggle(property.id, !isVisible)} style={{ paddingLeft: 4 }} >
e.stopPropagation()}>
{TypeIcon && } {property.name} {}} onClick={(e) => e.stopPropagation()} styles={{ track: { cursor: "pointer" } }} />
{closestEdge && }
); }