Dnd library migration fixes and changes (#23752)
Follow-up to #23211. Fixed issues, and simplified where possible. The core idea: every sortable list now resolves its drop position the same way — "sortable over sortable", comparing the pointer against the hovered item's midpoint — instead of each surface owning bespoke droppable slots and end-drop zones. ## Refactors - New `resolveDropFromPointer` handles both axes in one util; items can tag their own `orientation`, so one provider can drive lists of mixed axes. - Dropped `DragDropItemDroppableSlot` and `DragDropItemDropLine` path. Record table/board headers, page-layout tabs & widgets, and fields config all derive the drop index from the hovered sortable, matching record-board cards. - `DragDropItemSortableCell` is now the single sortable primitive, with drag optionally delegated to an explicit `DragDropItemSortableHandle`. - Removed end-drop constants/types; lists now place a trailing append target and resolve the append position in the consumer's own index space. ## Fixes - Dragging a row within grouped records threw an error — the drag overlay now resolves the source row's record-group context. - Multi-select drag counter chip didn't show — drag state was read from the wrong component scope instead of the active `recordIndexId`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23752?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
6e30405489
commit
17ff17bdec
+5
-11
@@ -7,7 +7,7 @@ import { RecordBoardColumnDndKitProvider } from '@/object-record/record-board/re
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
|
||||
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
|
||||
import { DragDropItemDroppableSlot } from '@/ui/utilities/drag-and-drop/components/DragDropItemDroppableSlot';
|
||||
import { DragDropItemDropTargetSlot } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTargetSlot';
|
||||
import { DragDropItemSortableCell } from '@/ui/utilities/drag-and-drop/components/DragDropItemSortableCell';
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
@@ -20,12 +20,9 @@ export const RecordBoardColumnDnd = () => {
|
||||
|
||||
return (
|
||||
<RecordBoardColumnDndKitProvider>
|
||||
<DragDropItemDroppableSlot
|
||||
droppableId={RECORD_BOARD_COLUMN_DROPPABLE_ID}
|
||||
index={0}
|
||||
>
|
||||
<DragDropItemDropTargetSlot>
|
||||
<DragDropItemDropTarget index={0} orientation="vertical" overlay />
|
||||
</DragDropItemDroppableSlot>
|
||||
</DragDropItemDropTargetSlot>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<Fragment key={recordGroupId}>
|
||||
<DragDropItemSortableCell
|
||||
@@ -42,16 +39,13 @@ export const RecordBoardColumnDnd = () => {
|
||||
/>
|
||||
</RecordGroupContext.Provider>
|
||||
</DragDropItemSortableCell>
|
||||
<DragDropItemDroppableSlot
|
||||
droppableId={RECORD_BOARD_COLUMN_DROPPABLE_ID}
|
||||
index={index + 1}
|
||||
>
|
||||
<DragDropItemDropTargetSlot>
|
||||
<DragDropItemDropTarget
|
||||
index={index + 1}
|
||||
orientation="vertical"
|
||||
overlay
|
||||
/>
|
||||
</DragDropItemDroppableSlot>
|
||||
</DragDropItemDropTargetSlot>
|
||||
</Fragment>
|
||||
))}
|
||||
<RecordBoardAddGroupColumn />
|
||||
|
||||
+49
-60
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID } from '@/object-record/record-group/constants/RecordGroupReorderConfirmationModalId';
|
||||
@@ -6,18 +6,15 @@ import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useRe
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
|
||||
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
|
||||
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { type DragDropItemData } from '@/ui/utilities/drag-and-drop/types/DragDropItemData';
|
||||
import { resolveDragDropItemDrop } from '@/ui/utilities/drag-and-drop/utils/resolveDragDropItemDrop';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { type DragDropProviderDragEndEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragEndEvent';
|
||||
import { type DragDropProviderDragMoveEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragMoveEvent';
|
||||
@@ -52,7 +49,6 @@ export const useRecordBoardColumnDndKit = (): {
|
||||
viewType: ViewType.KANBAN,
|
||||
});
|
||||
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const isRecordGroupDraggableSort = useAtomComponentSelectorValue(
|
||||
recordIndexRecordGroupIsDraggableSortComponentSelector,
|
||||
@@ -63,9 +59,6 @@ export const useRecordBoardColumnDndKit = (): {
|
||||
visibleRecordGroupIdsComponentFamilySelector,
|
||||
ViewType.KANBAN,
|
||||
);
|
||||
const recordIndexKanbanColumnWidth = useAtomComponentStateValue(
|
||||
recordIndexKanbanColumnWidthComponentState,
|
||||
);
|
||||
|
||||
const [, setRecordIndexRecordGroupSort] = useAtomComponentState(
|
||||
recordIndexRecordGroupSortComponentState,
|
||||
@@ -79,62 +72,50 @@ export const useRecordBoardColumnDndKit = (): {
|
||||
null,
|
||||
);
|
||||
|
||||
const resolveDropFromPointerX = ({
|
||||
pointerX,
|
||||
sourceIndex,
|
||||
}: {
|
||||
pointerX: number;
|
||||
sourceIndex: number;
|
||||
}) => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
if (!isDefined(scrollWrapperElement)) return null;
|
||||
// The pointer can leave every sortable (column bodies, trailing empty
|
||||
// space); the last resolved boundary is kept so the drop always lands where
|
||||
// the insertion indicator was last shown. A ref because it is
|
||||
// gesture-scoped bookkeeping read back inside drag callbacks.
|
||||
// oxlint-disable-next-line twenty/no-state-useref
|
||||
const lastDropTargetIndexRef = useRef<number | null>(null);
|
||||
|
||||
const columnWidths = visibleRecordGroupIds.map(
|
||||
() => recordIndexKanbanColumnWidth,
|
||||
);
|
||||
|
||||
if (columnWidths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveDragDropItemDrop({
|
||||
pointerX,
|
||||
sourceIndex,
|
||||
scrollWrapperElement,
|
||||
columnWidths,
|
||||
});
|
||||
};
|
||||
const lastIndex = visibleRecordGroupIds.length;
|
||||
|
||||
const handleDragStart = (_event: DragStartPayload) => {
|
||||
lastDropTargetIndexRef.current = null;
|
||||
setActiveDropTargetIndex(null);
|
||||
};
|
||||
|
||||
const handleDragMove = (event: DragMovePayload) => {
|
||||
const { operation } = event;
|
||||
const sourceIndex = operation.source?.data.index;
|
||||
const { target, position } = event.operation;
|
||||
|
||||
if (!isDefined(sourceIndex)) {
|
||||
setActiveDropTargetIndex(null);
|
||||
return;
|
||||
const resolvedDropTargetIndex =
|
||||
resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'vertical',
|
||||
getDroppableItemCount: () => lastIndex,
|
||||
})?.dropTargetIndex ?? null;
|
||||
|
||||
if (isDefined(resolvedDropTargetIndex)) {
|
||||
lastDropTargetIndexRef.current = resolvedDropTargetIndex;
|
||||
}
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerX({
|
||||
pointerX: operation.position.current.x,
|
||||
sourceIndex,
|
||||
});
|
||||
const dropTargetIndex =
|
||||
resolvedDropTargetIndex ?? lastDropTargetIndexRef.current;
|
||||
|
||||
setActiveDropTargetIndex((currentActiveDropTargetIndex) => {
|
||||
const nextActiveDropTargetIndex = resolvedDrop?.dropTargetIndex ?? null;
|
||||
|
||||
return currentActiveDropTargetIndex === nextActiveDropTargetIndex
|
||||
setActiveDropTargetIndex((currentActiveDropTargetIndex) =>
|
||||
currentActiveDropTargetIndex === dropTargetIndex
|
||||
? currentActiveDropTargetIndex
|
||||
: nextActiveDropTargetIndex;
|
||||
});
|
||||
: dropTargetIndex,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const { source, target, position } = event.operation;
|
||||
|
||||
const lastDropTargetIndex = lastDropTargetIndexRef.current;
|
||||
lastDropTargetIndexRef.current = null;
|
||||
|
||||
setActiveDropTargetIndex(null);
|
||||
setDragSelectionStartEnabled(true);
|
||||
@@ -144,26 +125,34 @@ export const useRecordBoardColumnDndKit = (): {
|
||||
}
|
||||
|
||||
const sourceIndex = source.data.index;
|
||||
const resolvedDrop = resolveDropFromPointerX({
|
||||
pointerX: operation.position.current.x,
|
||||
sourceIndex,
|
||||
});
|
||||
|
||||
if (!isDefined(resolvedDrop)) return;
|
||||
if (resolvedDrop.sourceIndex === resolvedDrop.destinationIndex) return;
|
||||
const dropTargetIndex =
|
||||
resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'vertical',
|
||||
getDroppableItemCount: () => lastIndex,
|
||||
})?.dropTargetIndex ?? lastDropTargetIndex;
|
||||
|
||||
if (!isDefined(dropTargetIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationIndex =
|
||||
dropTargetIndex > sourceIndex ? dropTargetIndex - 1 : dropTargetIndex;
|
||||
|
||||
if (!isRecordGroupDraggableSort) {
|
||||
setPendingReorder({
|
||||
fromIndex: resolvedDrop.sourceIndex,
|
||||
toIndex: resolvedDrop.destinationIndex,
|
||||
fromIndex: sourceIndex,
|
||||
toIndex: destinationIndex,
|
||||
});
|
||||
openModal(RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
reorderRecordGroups({
|
||||
fromIndex: resolvedDrop.sourceIndex,
|
||||
toIndex: resolvedDrop.destinationIndex,
|
||||
fromIndex: sourceIndex,
|
||||
toIndex: destinationIndex,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+7
-5
@@ -8,7 +8,7 @@ import { isRecordBoardDropProcessingComponentState } from '@/object-record/recor
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { getBoardCardDropBehavior } from '@/object-record/record-board/utils/getBoardCardDropBehavior';
|
||||
import { getDestinationIndex } from '@/ui/utilities/drag-and-drop/utils/getDestinationIndex';
|
||||
import { resolveDropFromPointerY } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointerY';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/hooks/useEndRecordDrag';
|
||||
@@ -114,9 +114,10 @@ export const useRecordBoardDndKit = (): {
|
||||
const handleDragMove = (event: DragMovePayload) => {
|
||||
const { target, position } = event.operation;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerY({
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointerY: position.current.y,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount,
|
||||
});
|
||||
|
||||
@@ -142,9 +143,10 @@ export const useRecordBoardDndKit = (): {
|
||||
const sourceDroppableId = (source.data as DragDropItemData).droppableId;
|
||||
const sourceIndex = (source.data as DragDropItemData).index;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerY({
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointerY: position.current.y,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount,
|
||||
});
|
||||
if (!isDefined(resolvedDrop)) {
|
||||
|
||||
+7
-5
@@ -11,7 +11,7 @@ import { useStartRecordDrag } from '@/object-record/record-drag/hooks/useStartRe
|
||||
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
|
||||
import { type DragDropItemData } from '@/ui/utilities/drag-and-drop/types/DragDropItemData';
|
||||
import { getDestinationIndex } from '@/ui/utilities/drag-and-drop/utils/getDestinationIndex';
|
||||
import { resolveDropFromPointerY } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointerY';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { useAtomComponentFamilySelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorCallbackState';
|
||||
import { type DragDropProviderDragEndEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragEndEvent';
|
||||
import { type DragDropProviderDragMoveEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragMoveEvent';
|
||||
@@ -86,9 +86,10 @@ export const useRecordCalendarMonthDndKit = (): {
|
||||
const handleDragMove = (event: DragMovePayload) => {
|
||||
const { target, position } = event.operation;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerY({
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointerY: position.current.y,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount,
|
||||
});
|
||||
|
||||
@@ -116,9 +117,10 @@ export const useRecordCalendarMonthDndKit = (): {
|
||||
const sourceDroppableId = (source.data as DragDropItemData).droppableId;
|
||||
const sourceIndex = (source.data as DragDropItemData).index;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerY({
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointerY: position.current.y,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount,
|
||||
});
|
||||
if (!isDefined(resolvedDrop)) {
|
||||
|
||||
+42
-12
@@ -1,3 +1,10 @@
|
||||
import { pointerIntersection } from '@dnd-kit/collision';
|
||||
import { useDroppable } from '@dnd-kit/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { getContiguousIncrementalValues } from 'twenty-shared/utils';
|
||||
|
||||
import { isDraggingRecordComponentState } from '@/object-record/record-drag/states/isDraggingRecordComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordTableNoRecordGroupAddNew } from '@/object-record/record-table/components/RecordTableNoRecordGroupAddNew';
|
||||
import { RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID } from '@/object-record/record-table/constants/RecordTableNoRecordGroupDroppableId';
|
||||
import { RECORD_TABLE_ROW_DND_TYPE } from '@/object-record/record-table/constants/RecordTableRowDndType';
|
||||
@@ -6,10 +13,9 @@ import { RecordTableVirtualizedBodyPlaceholder } from '@/object-record/record-ta
|
||||
import { RecordTableVirtualizedDebugHelper } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedDebugHelper';
|
||||
import { NUMBER_OF_VIRTUALIZED_ROWS } from '@/object-record/record-table/virtualization/constants/NumberOfVirtualizedRows';
|
||||
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
|
||||
import { DragDropItemEndDropZone } from '@/ui/utilities/drag-and-drop/components/DragDropItemEndDropZone';
|
||||
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
|
||||
import { DND_KIT_COLLISION_PRIORITY } from '@/ui/utilities/drag-and-drop/constants/DndKitCollisionPriority';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { getContiguousIncrementalValues } from 'twenty-shared/utils';
|
||||
|
||||
const StyledNoRecordGroupContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -17,12 +23,24 @@ const StyledNoRecordGroupContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEndDropZone = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableNoRecordGroupRows = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const totalNumberOfRecordsToVirtualize =
|
||||
useAtomComponentStateValue(
|
||||
totalNumberOfRecordsToVirtualizeComponentState,
|
||||
) ?? 0;
|
||||
|
||||
const isDraggingRecord = useAtomComponentStateValue(
|
||||
isDraggingRecordComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const numberOfRows = Math.min(
|
||||
totalNumberOfRecordsToVirtualize,
|
||||
NUMBER_OF_VIRTUALIZED_ROWS,
|
||||
@@ -30,6 +48,16 @@ export const RecordTableNoRecordGroupRows = () => {
|
||||
|
||||
const virtualRowIndices = getContiguousIncrementalValues(numberOfRows);
|
||||
|
||||
// Catches drops past the last row, where no row sortable is under the
|
||||
// pointer; the drop target inside only renders the insertion indicator.
|
||||
const { ref: endDropZoneRef } = useDroppable({
|
||||
id: RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID,
|
||||
accept: RECORD_TABLE_ROW_DND_TYPE,
|
||||
collisionPriority: DND_KIT_COLLISION_PRIORITY,
|
||||
collisionDetector: pointerIntersection,
|
||||
data: { droppableId: RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID },
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledNoRecordGroupContainer>
|
||||
<RecordTableVirtualizedBodyPlaceholder />
|
||||
@@ -41,16 +69,18 @@ export const RecordTableNoRecordGroupRows = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DragDropItemEndDropZone
|
||||
id="record-table-no-record-group-end-drop-zone"
|
||||
accept={RECORD_TABLE_ROW_DND_TYPE}
|
||||
data={{
|
||||
droppableId: RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID,
|
||||
index: totalNumberOfRecordsToVirtualize,
|
||||
}}
|
||||
>
|
||||
<StyledEndDropZone ref={endDropZoneRef}>
|
||||
{/* Zero footprint at rest; expands during a row drag so the zone
|
||||
stays droppable even when the add-new row is hidden. */}
|
||||
<DragDropItemDropTarget
|
||||
index={totalNumberOfRecordsToVirtualize}
|
||||
droppableId={RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID}
|
||||
orientation="horizontal"
|
||||
compact={!isDraggingRecord}
|
||||
seamAligned
|
||||
/>
|
||||
<RecordTableNoRecordGroupAddNew />
|
||||
</DragDropItemEndDropZone>
|
||||
</StyledEndDropZone>
|
||||
<RecordTableVirtualizedDebugHelper />
|
||||
</StyledNoRecordGroupContainer>
|
||||
);
|
||||
|
||||
-1
@@ -13,7 +13,6 @@ import { type TableCellPosition } from '@/object-record/record-table/types/Table
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type RecordTableRecordGroupBodyContextProviderProps = {
|
||||
recordGroupId: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
|
||||
+36
-11
@@ -1,5 +1,10 @@
|
||||
import { pointerIntersection } from '@dnd-kit/collision';
|
||||
import { useDroppable } from '@dnd-kit/react';
|
||||
|
||||
import { isDraggingRecordComponentState } from '@/object-record/record-drag/states/isDraggingRecordComponentState';
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { useShouldHideRecordGroup } from '@/object-record/record-group/hooks/useShouldHideRecordGroup';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RECORD_TABLE_ROW_DND_TYPE } from '@/object-record/record-table/constants/RecordTableRowDndType';
|
||||
@@ -8,22 +13,32 @@ import { RecordTableRow } from '@/object-record/record-table/record-table-row/co
|
||||
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
|
||||
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { DragDropItemEndDropZone } from '@/ui/utilities/drag-and-drop/components/DragDropItemEndDropZone';
|
||||
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
|
||||
import { DND_KIT_COLLISION_PRIORITY } from '@/ui/utilities/drag-and-drop/constants/DndKitCollisionPriority';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledRecordGroupEndDropZone = styled(DragDropItemEndDropZone)`
|
||||
const StyledRecordGroupDropTarget = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableRecordGroupRows = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||
|
||||
const shouldHide = useShouldHideRecordGroup(currentRecordGroupId);
|
||||
|
||||
const isDraggingRecord = useAtomComponentStateValue(
|
||||
isDraggingRecordComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const allRecordIds = useAtomComponentSelectorValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
@@ -43,6 +58,14 @@ export const RecordTableRecordGroupRows = () => {
|
||||
[allRecordIds],
|
||||
);
|
||||
|
||||
const { ref: endDropZoneRef } = useDroppable({
|
||||
id: currentRecordGroupId,
|
||||
accept: RECORD_TABLE_ROW_DND_TYPE,
|
||||
collisionPriority: DND_KIT_COLLISION_PRIORITY,
|
||||
collisionDetector: pointerIntersection,
|
||||
data: { droppableId: currentRecordGroupId },
|
||||
});
|
||||
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
@@ -69,17 +92,19 @@ export const RecordTableRecordGroupRows = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<StyledRecordGroupEndDropZone
|
||||
id={`record-group-end-drop-zone-${currentRecordGroupId}`}
|
||||
accept={RECORD_TABLE_ROW_DND_TYPE}
|
||||
data={{
|
||||
droppableId: currentRecordGroupId,
|
||||
index: recordIndexRecordIdsByGroup.length,
|
||||
}}
|
||||
>
|
||||
<StyledRecordGroupDropTarget ref={endDropZoneRef}>
|
||||
{/* Zero footprint at rest; expands during a row drag so the zone
|
||||
stays droppable even when neither action row below renders. */}
|
||||
<DragDropItemDropTarget
|
||||
index={recordIndexRecordIdsByGroup.length}
|
||||
droppableId={currentRecordGroupId}
|
||||
orientation="horizontal"
|
||||
compact={!isDraggingRecord}
|
||||
seamAligned
|
||||
/>
|
||||
<RecordTableRecordGroupSectionLoadMore />
|
||||
<RecordTableRecordGroupSectionAddNew />
|
||||
</StyledRecordGroupEndDropZone>
|
||||
</StyledRecordGroupDropTarget>
|
||||
<RecordTableAggregateFooter
|
||||
key={currentRecordGroupId}
|
||||
currentRecordGroupId={currentRecordGroupId}
|
||||
|
||||
+120
-72
@@ -1,6 +1,6 @@
|
||||
import { DragDropProvider, DragOverlay } from '@dnd-kit/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/hooks/useEndRecordDrag';
|
||||
@@ -11,13 +11,18 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableRowDragOverlayContent } from '@/object-record/record-table/record-table-row/components/RecordTableRowDragOverlayContent';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { type RecordTableRowDragData } from '@/object-record/record-table/types/RecordTableRowDragData';
|
||||
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
|
||||
import { DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION } from '@/ui/utilities/drag-and-drop/constants/DndKitProviderPluginsWithoutDropAnimation';
|
||||
import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
|
||||
import { DragDropItemDndContext } from '@/ui/utilities/drag-and-drop/context/DragDropItemDndContext';
|
||||
import { type DragDropItemData } from '@/ui/utilities/drag-and-drop/types/DragDropItemData';
|
||||
import { type DragDropProviderDragEndEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragEndEvent';
|
||||
import { type DragDropProviderDragMoveEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragMoveEvent';
|
||||
import { type DragDropProviderDragStartEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragStartEvent';
|
||||
import { getDestinationIndex } from '@/ui/utilities/drag-and-drop/utils/getDestinationIndex';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
|
||||
export const RecordTableBodyNoRecordGroupDragDropContextProvider = ({
|
||||
children,
|
||||
@@ -32,6 +37,11 @@ export const RecordTableBodyNoRecordGroupDragDropContextProvider = ({
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const totalNumberOfRecordsToVirtualize = useAtomComponentStateCallbackState(
|
||||
totalNumberOfRecordsToVirtualizeComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { startRecordDrag } = useStartRecordDrag(recordIndexId);
|
||||
@@ -39,84 +49,122 @@ export const RecordTableBodyNoRecordGroupDragDropContextProvider = ({
|
||||
const { processTableWithoutGroupRecordDrop } =
|
||||
useProcessTableWithoutGroupRecordDrop();
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragDropProviderDragStartEvent<DragDropItemData>) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
if (!isDefined(source) || !isDefined(sourceData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelectedRecordIds = store.get(selectedRowIds) as string[];
|
||||
|
||||
startRecordDrag(sourceData.recordId, currentSelectedRecordIds);
|
||||
},
|
||||
[selectedRowIds, startRecordDrag, store],
|
||||
const [activeDropTargetIndex, setActiveDropTargetIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeDroppableId, setActiveDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragDropProviderDragEndEvent<DragDropItemData>) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
const targetData = event.operation.target?.data as
|
||||
| DragDropItemData
|
||||
| undefined;
|
||||
const clearDragState = () => {
|
||||
endRecordDrag();
|
||||
setActiveDropTargetIndex(null);
|
||||
setActiveDroppableId(null);
|
||||
};
|
||||
|
||||
if (
|
||||
event.canceled ||
|
||||
!isDefined(source) ||
|
||||
!isDefined(sourceData) ||
|
||||
!isDefined(targetData)
|
||||
) {
|
||||
endRecordDrag();
|
||||
return;
|
||||
}
|
||||
const handleDragStart = (
|
||||
event: DragDropProviderDragStartEvent<DragDropItemData>,
|
||||
) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
// Row targets and end drop zones mark the gap before them; convert that
|
||||
// gap into the index the dragged row will occupy after the move.
|
||||
const destinationIndex = getDestinationIndex({
|
||||
dropTargetIndex: targetData.index,
|
||||
sourceIndex: sourceData.index,
|
||||
sourceDroppableId: sourceData.droppableId,
|
||||
destinationDroppableId: targetData.droppableId,
|
||||
if (!isDefined(source) || !isDefined(sourceData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelectedRecordIds = store.get(selectedRowIds) as string[];
|
||||
|
||||
startRecordDrag(sourceData.recordId, currentSelectedRecordIds);
|
||||
};
|
||||
|
||||
const handleDragMove = (
|
||||
event: DragDropProviderDragMoveEvent<DragDropItemData>,
|
||||
) => {
|
||||
const { target, position } = event.operation;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount: () =>
|
||||
store.get(totalNumberOfRecordsToVirtualize) ?? 0,
|
||||
});
|
||||
|
||||
setActiveDropTargetIndex(resolvedDrop?.dropTargetIndex ?? null);
|
||||
setActiveDroppableId(resolvedDrop?.droppableId ?? null);
|
||||
};
|
||||
|
||||
const handleDragEnd = (
|
||||
event: DragDropProviderDragEndEvent<DragDropItemData>,
|
||||
) => {
|
||||
const { source, target, position } = event.operation;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
if (event.canceled || !isDefined(source) || !isDefined(sourceData)) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount: () =>
|
||||
store.get(totalNumberOfRecordsToVirtualize) ?? 0,
|
||||
});
|
||||
|
||||
if (!isDefined(resolvedDrop)) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Row targets and end drop zones mark the gap before them; convert that
|
||||
// gap into the index the dragged row will occupy after the move.
|
||||
const destinationIndex = getDestinationIndex({
|
||||
dropTargetIndex: resolvedDrop.dropTargetIndex,
|
||||
sourceIndex: sourceData.index,
|
||||
sourceDroppableId: sourceData.droppableId,
|
||||
destinationDroppableId: resolvedDrop.droppableId,
|
||||
});
|
||||
|
||||
if (destinationIndex === sourceData.index) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processTableWithoutGroupRecordDrop({
|
||||
draggableId: sourceData.recordId,
|
||||
source: {
|
||||
droppableId: sourceData.droppableId,
|
||||
index: sourceData.index,
|
||||
},
|
||||
destination: {
|
||||
droppableId: resolvedDrop.droppableId,
|
||||
index: destinationIndex,
|
||||
},
|
||||
});
|
||||
|
||||
if (destinationIndex === sourceData.index) {
|
||||
endRecordDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processTableWithoutGroupRecordDrop({
|
||||
draggableId: sourceData.recordId,
|
||||
source: {
|
||||
droppableId: sourceData.droppableId,
|
||||
index: sourceData.index,
|
||||
},
|
||||
destination: {
|
||||
droppableId: targetData.droppableId,
|
||||
index: destinationIndex,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
endRecordDrag();
|
||||
}
|
||||
},
|
||||
[endRecordDrag, processTableWithoutGroupRecordDrop],
|
||||
);
|
||||
} finally {
|
||||
clearDragState();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropProvider<DragDropItemData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
plugins={DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
<DragDropItemDndContext.Provider
|
||||
value={{ activeDropTargetIndex, activeDroppableId }}
|
||||
>
|
||||
{children}
|
||||
<DragOverlay>
|
||||
{(source) => <RecordTableRowDragOverlayContent source={source} />}
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<DragDropProvider<DragDropItemData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
plugins={DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION}
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
<DragOverlay>
|
||||
{(source) => <RecordTableRowDragOverlayContent source={source} />}
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</DragDropItemDndContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+128
-75
@@ -1,22 +1,28 @@
|
||||
import { DragDropProvider, DragOverlay } from '@dnd-kit/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/hooks/useEndRecordDrag';
|
||||
import { useProcessTableWithGroupRecordDrop } from '@/object-record/record-drag/hooks/useProcessTableWithGroupRecordDrop';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/hooks/useStartRecordDrag';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowDragOverlayContent } from '@/object-record/record-table/record-table-row/components/RecordTableRowDragOverlayContent';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { type RecordTableRowDragData } from '@/object-record/record-table/types/RecordTableRowDragData';
|
||||
import { DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION } from '@/ui/utilities/drag-and-drop/constants/DndKitProviderPluginsWithoutDropAnimation';
|
||||
import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
|
||||
import { DragDropItemDndContext } from '@/ui/utilities/drag-and-drop/context/DragDropItemDndContext';
|
||||
import { type DragDropItemData } from '@/ui/utilities/drag-and-drop/types/DragDropItemData';
|
||||
import { type DragDropProviderDragEndEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragEndEvent';
|
||||
import { type DragDropProviderDragMoveEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragMoveEvent';
|
||||
import { type DragDropProviderDragStartEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragStartEvent';
|
||||
import { getDestinationIndex } from '@/ui/utilities/drag-and-drop/utils/getDestinationIndex';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
|
||||
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
|
||||
|
||||
export const RecordTableBodyRecordGroupDragDropContextProvider = ({
|
||||
@@ -32,6 +38,11 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const recordIdsByGroupCallbackState =
|
||||
useAtomComponentFamilyStateCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { startRecordDrag } = useStartRecordDrag(recordIndexId);
|
||||
@@ -40,87 +51,129 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
|
||||
const { processTableWithGroupRecordDrop } =
|
||||
useProcessTableWithGroupRecordDrop();
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragDropProviderDragStartEvent<DragDropItemData>) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
if (!isDefined(source) || !isDefined(sourceData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelectedRecordIds = store.get(selectedRowIds) as string[];
|
||||
|
||||
startRecordDrag(sourceData.recordId, currentSelectedRecordIds);
|
||||
},
|
||||
[selectedRowIds, startRecordDrag, store],
|
||||
const [activeDropTargetIndex, setActiveDropTargetIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeDroppableId, setActiveDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragDropProviderDragEndEvent<DragDropItemData>) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
const targetData = event.operation.target?.data as
|
||||
| DragDropItemData
|
||||
| undefined;
|
||||
const clearDragState = () => {
|
||||
endRecordDrag();
|
||||
setActiveDropTargetIndex(null);
|
||||
setActiveDroppableId(null);
|
||||
};
|
||||
|
||||
if (
|
||||
event.canceled ||
|
||||
!isDefined(source) ||
|
||||
!isDefined(sourceData) ||
|
||||
!isDefined(targetData)
|
||||
) {
|
||||
endRecordDrag();
|
||||
return;
|
||||
}
|
||||
const handleDragStart = (
|
||||
event: DragDropProviderDragStartEvent<DragDropItemData>,
|
||||
) => {
|
||||
const source = event.operation.source;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
// Row targets and end drop zones mark the gap before them; convert that
|
||||
// gap into the index the dragged row will occupy after the move.
|
||||
const destinationIndex = getDestinationIndex({
|
||||
dropTargetIndex: targetData.index,
|
||||
sourceIndex: sourceData.index,
|
||||
sourceDroppableId: sourceData.droppableId,
|
||||
destinationDroppableId: targetData.droppableId,
|
||||
if (!isDefined(source) || !isDefined(sourceData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelectedRecordIds = store.get(selectedRowIds) as string[];
|
||||
|
||||
startRecordDrag(sourceData.recordId, currentSelectedRecordIds);
|
||||
};
|
||||
|
||||
const handleDragMove = (
|
||||
event: DragDropProviderDragMoveEvent<DragDropItemData>,
|
||||
) => {
|
||||
const { target, position } = event.operation;
|
||||
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount: (droppableId) =>
|
||||
store.get(recordIdsByGroupCallbackState(droppableId)).length,
|
||||
});
|
||||
|
||||
setActiveDropTargetIndex(resolvedDrop?.dropTargetIndex ?? null);
|
||||
setActiveDroppableId(resolvedDrop?.droppableId ?? null);
|
||||
};
|
||||
|
||||
const handleDragEnd = (
|
||||
event: DragDropProviderDragEndEvent<DragDropItemData>,
|
||||
) => {
|
||||
const { source, target, position } = event.operation;
|
||||
const sourceData = source?.data as RecordTableRowDragData | undefined;
|
||||
|
||||
if (event.canceled || !isDefined(source) || !isDefined(sourceData)) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDrop = resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'horizontal',
|
||||
getDroppableItemCount: (droppableId) =>
|
||||
store.get(recordIdsByGroupCallbackState(droppableId)).length,
|
||||
});
|
||||
|
||||
if (!isDefined(resolvedDrop)) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Row targets and end drop zones mark the gap before them; convert that
|
||||
// gap into the index the dragged row will occupy after the move.
|
||||
const destinationIndex = getDestinationIndex({
|
||||
dropTargetIndex: resolvedDrop.dropTargetIndex,
|
||||
sourceIndex: sourceData.index,
|
||||
sourceDroppableId: sourceData.droppableId,
|
||||
destinationDroppableId: resolvedDrop.droppableId,
|
||||
});
|
||||
|
||||
const isSameRecordGroup =
|
||||
sourceData.droppableId === resolvedDrop.droppableId;
|
||||
|
||||
if (isSameRecordGroup && destinationIndex === sourceData.index) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processTableWithGroupRecordDrop({
|
||||
draggableId: sourceData.recordId,
|
||||
source: {
|
||||
droppableId: sourceData.droppableId,
|
||||
index: sourceData.index,
|
||||
},
|
||||
destination: {
|
||||
droppableId: resolvedDrop.droppableId,
|
||||
index: destinationIndex,
|
||||
},
|
||||
});
|
||||
|
||||
const isSameRecordGroup =
|
||||
sourceData.droppableId === targetData.droppableId;
|
||||
|
||||
if (isSameRecordGroup && destinationIndex === sourceData.index) {
|
||||
endRecordDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processTableWithGroupRecordDrop({
|
||||
draggableId: sourceData.recordId,
|
||||
source: {
|
||||
droppableId: sourceData.droppableId,
|
||||
index: sourceData.index,
|
||||
},
|
||||
destination: {
|
||||
droppableId: targetData.droppableId,
|
||||
index: destinationIndex,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
endRecordDrag();
|
||||
}
|
||||
},
|
||||
[endRecordDrag, processTableWithGroupRecordDrop],
|
||||
);
|
||||
} finally {
|
||||
clearDragState();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropProvider<DragDropItemData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
plugins={DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
<DragDropItemDndContext.Provider
|
||||
value={{ activeDropTargetIndex, activeDroppableId }}
|
||||
>
|
||||
{children}
|
||||
<DragOverlay>
|
||||
{(source) => <RecordTableRowDragOverlayContent source={source} />}
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<DragDropProvider<DragDropItemData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
plugins={DND_KIT_PROVIDER_PLUGINS_WITHOUT_DROP_ANIMATION}
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
<DragOverlay>
|
||||
{(source) => (
|
||||
<RecordTableRecordGroupBodyContextProvider>
|
||||
<RecordTableRowDragOverlayContent source={source} />
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</DragDropItemDndContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-4
@@ -38,10 +38,7 @@ export const RecordTableRecordGroupsBody = () => {
|
||||
<>
|
||||
<RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordTableRecordGroupBodyContextProvider
|
||||
key={recordGroupId}
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordTableRecordGroupBodyContextProvider key={recordGroupId}>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBody data-replay-ignore-mutations="true">
|
||||
<RecordTableRecordGroupSection />
|
||||
|
||||
+7
-19
@@ -9,8 +9,8 @@ import { RecordTableHeaderLastEmptyColumn } from '@/object-record/record-table/r
|
||||
import { RECORD_TABLE_HEADER_DROPPABLE_ID } from '@/object-record/record-table/record-table-header/dnd/constants/RecordTableHeaderDroppableId';
|
||||
import { RecordTableHeaderDndKitProvider } from '@/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider';
|
||||
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
||||
import { DragDropItemDroppableSlot } from '@/ui/utilities/drag-and-drop/components/DragDropItemDroppableSlot';
|
||||
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
|
||||
import { DragDropItemDropTargetSlot } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTargetSlot';
|
||||
import { DragDropItemSortableCell } from '@/ui/utilities/drag-and-drop/components/DragDropItemSortableCell';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -28,13 +28,9 @@ export const RecordTableHeaderDnd = () => {
|
||||
|
||||
return (
|
||||
<RecordTableHeaderDndKitProvider>
|
||||
<DragDropItemDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={0}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<DragDropItemDropTargetSlot>
|
||||
<DragDropItemDropTarget index={0} orientation="vertical" compact />
|
||||
</DragDropItemDroppableSlot>
|
||||
</DragDropItemDropTargetSlot>
|
||||
|
||||
{isDefined(firstScrollableRecordField) && (
|
||||
<DragDropItemSortableCell
|
||||
@@ -53,17 +49,13 @@ export const RecordTableHeaderDnd = () => {
|
||||
|
||||
{recordFieldsWithoutFirstTwo.map((recordField, index) => (
|
||||
<React.Fragment key={recordField.fieldMetadataItemId}>
|
||||
<DragDropItemDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={index + 1}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<DragDropItemDropTargetSlot>
|
||||
<DragDropItemDropTarget
|
||||
index={index + 1}
|
||||
orientation="vertical"
|
||||
compact
|
||||
/>
|
||||
</DragDropItemDroppableSlot>
|
||||
</DragDropItemDropTargetSlot>
|
||||
<DragDropItemSortableCell
|
||||
id={recordField.fieldMetadataItemId}
|
||||
index={index + 1}
|
||||
@@ -79,17 +71,13 @@ export const RecordTableHeaderDnd = () => {
|
||||
</DragDropItemSortableCell>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<DragDropItemDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={visibleRecordFields.length - 1}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<DragDropItemDropTargetSlot>
|
||||
<DragDropItemDropTarget
|
||||
index={visibleRecordFields.length - 1}
|
||||
orientation="vertical"
|
||||
compact
|
||||
/>
|
||||
</DragDropItemDroppableSlot>
|
||||
</DragDropItemDropTargetSlot>
|
||||
{isRecordTableColumnHeadersReadOnly ? (
|
||||
<RecordTableHeaderEmptyLastColumn />
|
||||
) : (
|
||||
|
||||
+51
-86
@@ -1,20 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { filterOutByProperty, isDefined } from 'twenty-shared/utils';
|
||||
import { useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
|
||||
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
|
||||
import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields';
|
||||
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
|
||||
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
|
||||
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
|
||||
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
|
||||
import { type DragDropItemData } from '@/ui/utilities/drag-and-drop/types/DragDropItemData';
|
||||
import { resolveDragDropItemDrop } from '@/ui/utilities/drag-and-drop/utils/resolveDragDropItemDrop';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { resolveDropFromPointer } from '@/ui/utilities/drag-and-drop/utils/resolveDropFromPointer';
|
||||
import { type DragDropProviderDragEndEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragEndEvent';
|
||||
import { type DragDropProviderDragMoveEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragMoveEvent';
|
||||
import { type DragDropProviderDragStartEvent } from '@/ui/utilities/drag-and-drop/types/DragDropProviderDragStartEvent';
|
||||
@@ -36,117 +29,89 @@ export const useRecordTableHeaderDndKit = (): {
|
||||
};
|
||||
} => {
|
||||
const { recordTableId, visibleRecordFields } = useRecordTableContextOrThrow();
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||
const { reorderVisibleRecordFields } =
|
||||
useReorderVisibleRecordFields(recordTableId);
|
||||
const { saveViewFields } = useSaveCurrentViewFields();
|
||||
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
|
||||
isRecordTableDragColumnHiddenComponentState,
|
||||
);
|
||||
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
|
||||
isRecordTableCheckboxColumnHiddenComponentState,
|
||||
);
|
||||
|
||||
const [activeDropTargetIndex, setActiveDropTargetIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const recordFieldsWithoutLabelIdentifier = visibleRecordFields.filter(
|
||||
filterOutByProperty(
|
||||
'fieldMetadataItemId',
|
||||
labelIdentifierFieldMetadataItem?.id,
|
||||
),
|
||||
);
|
||||
// The pointer can leave every sortable (sticky pinned column, table body,
|
||||
// trailing empty space); the last resolved boundary is kept so the drop
|
||||
// always lands where the insertion indicator was last shown. A ref because
|
||||
// it is gesture-scoped bookkeeping read back inside drag callbacks.
|
||||
// oxlint-disable-next-line twenty/no-state-useref
|
||||
const lastDropTargetIndexRef = useRef<number | null>(null);
|
||||
|
||||
const labelIdentifierRecordField = visibleRecordFields.find(
|
||||
(recordField) =>
|
||||
recordField.fieldMetadataItemId === labelIdentifierFieldMetadataItem?.id,
|
||||
);
|
||||
|
||||
const nonSortableColumnsWidth =
|
||||
(isRecordTableDragColumnHidden
|
||||
? 0
|
||||
: RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH) +
|
||||
(isRecordTableCheckboxColumnHidden
|
||||
? 0
|
||||
: RECORD_TABLE_COLUMN_CHECKBOX_WIDTH) +
|
||||
(labelIdentifierRecordField?.size ?? 0);
|
||||
|
||||
const resolveDropFromPointerX = ({
|
||||
pointerX,
|
||||
sourceIndex,
|
||||
}: {
|
||||
pointerX: number;
|
||||
sourceIndex: number;
|
||||
}) => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
if (!isDefined(scrollWrapperElement)) return null;
|
||||
|
||||
return resolveDragDropItemDrop({
|
||||
pointerX,
|
||||
sourceIndex,
|
||||
scrollWrapperElement,
|
||||
columnWidths: recordFieldsWithoutLabelIdentifier.map(
|
||||
(recordField) => recordField.size,
|
||||
),
|
||||
leadingOffset: nonSortableColumnsWidth,
|
||||
});
|
||||
};
|
||||
const lastIndex = visibleRecordFields.length - 1;
|
||||
|
||||
const handleDragStart = (_event: DragStartPayload) => {
|
||||
lastDropTargetIndexRef.current = null;
|
||||
setActiveDropTargetIndex(null);
|
||||
};
|
||||
|
||||
const handleDragMove = (event: DragMovePayload) => {
|
||||
const { operation } = event;
|
||||
const sourceIndex = operation.source?.data.index;
|
||||
const { target, position } = event.operation;
|
||||
|
||||
if (!isDefined(sourceIndex)) {
|
||||
setActiveDropTargetIndex(null);
|
||||
return;
|
||||
const resolvedDropTargetIndex =
|
||||
resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'vertical',
|
||||
getDroppableItemCount: () => lastIndex,
|
||||
})?.dropTargetIndex ?? null;
|
||||
|
||||
if (isDefined(resolvedDropTargetIndex)) {
|
||||
lastDropTargetIndexRef.current = resolvedDropTargetIndex;
|
||||
}
|
||||
|
||||
const resolvedDrop = resolveDropFromPointerX({
|
||||
pointerX: operation.position.current.x,
|
||||
sourceIndex,
|
||||
});
|
||||
const dropTargetIndex =
|
||||
resolvedDropTargetIndex ?? lastDropTargetIndexRef.current;
|
||||
|
||||
setActiveDropTargetIndex((currentActiveDropTargetIndex) => {
|
||||
const nextActiveDropTargetIndex = resolvedDrop?.dropTargetIndex ?? null;
|
||||
|
||||
return currentActiveDropTargetIndex === nextActiveDropTargetIndex
|
||||
setActiveDropTargetIndex((currentActiveDropTargetIndex) =>
|
||||
currentActiveDropTargetIndex === dropTargetIndex
|
||||
? currentActiveDropTargetIndex
|
||||
: nextActiveDropTargetIndex;
|
||||
});
|
||||
: dropTargetIndex,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const { source, target, position } = event.operation;
|
||||
|
||||
const lastDropTargetIndex = lastDropTargetIndexRef.current;
|
||||
lastDropTargetIndexRef.current = null;
|
||||
|
||||
setActiveDropTargetIndex(null);
|
||||
setDragSelectionStartEnabled(true);
|
||||
|
||||
if (event.canceled) return;
|
||||
if (!isDefined(source)) return;
|
||||
if (event.canceled || !isDefined(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIndex = source.data.index;
|
||||
const resolvedDrop = resolveDropFromPointerX({
|
||||
pointerX: operation.position.current.x,
|
||||
sourceIndex,
|
||||
});
|
||||
|
||||
if (!isDefined(resolvedDrop)) return;
|
||||
if (resolvedDrop.sourceIndex === resolvedDrop.destinationIndex) return;
|
||||
const dropTargetIndex =
|
||||
resolveDropFromPointer({
|
||||
target,
|
||||
pointer: position.current,
|
||||
defaultOrientation: 'vertical',
|
||||
getDroppableItemCount: () => lastIndex,
|
||||
})?.dropTargetIndex ?? lastDropTargetIndex;
|
||||
|
||||
if (!isDefined(dropTargetIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationIndex =
|
||||
dropTargetIndex <= sourceIndex ? dropTargetIndex + 1 : dropTargetIndex;
|
||||
|
||||
// Sortable indices exclude the pinned label-identifier column at visibleRecordFields[0],
|
||||
// so shift by one to address the full visible field list.
|
||||
const updatedRecordField = reorderVisibleRecordFields({
|
||||
fromIndex: resolvedDrop.sourceIndex + 1,
|
||||
toIndex: resolvedDrop.destinationIndex + 1,
|
||||
fromIndex: sourceIndex + 1,
|
||||
toIndex: destinationIndex,
|
||||
});
|
||||
|
||||
saveViewFields([mapRecordFieldToViewField(updatedRecordField)]);
|
||||
|
||||
+22
-9
@@ -9,19 +9,23 @@ import { RECORD_TABLE_NO_RECORD_GROUP_DROPPABLE_ID } from '@/object-record/recor
|
||||
import { RECORD_TABLE_ROW_DND_TYPE } from '@/object-record/record-table/constants/RecordTableRowDndType';
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { useIsTableRowSecondaryDragged } from '@/object-record/record-table/record-table-row/hooks/useIsRecordSecondaryDragged';
|
||||
import { type RecordTableRowDragData } from '@/object-record/record-table/types/RecordTableRowDragData';
|
||||
import { DragDropItemDropLine } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropLine';
|
||||
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
|
||||
import { DND_KIT_PLUGINS_WITHOUT_OPTIMISTIC } from '@/ui/utilities/drag-and-drop/constants/DndKitPluginsWithoutOptimistic';
|
||||
import { DRAG_SOURCE_OPACITY } from '@/ui/utilities/drag-and-drop/constants/DragSourceOpacity';
|
||||
import { DragDropItemSortableHandleRefContext } from '@/ui/utilities/drag-and-drop/context/DragDropItemSortableHandleRefContext';
|
||||
|
||||
// The grip, checkbox and first field cells are sticky at
|
||||
// TABLE_Z_INDEX.cell.sticky; without a higher z-index they would paint over
|
||||
// the insertion line and truncate it to the scrollable columns.
|
||||
const StyledRowDropLine = styled(DragDropItemDropLine)`
|
||||
// Overlays the row's leading edge without reflowing it. The grip, checkbox and
|
||||
// first field cells are sticky at TABLE_Z_INDEX.cell.sticky; without a higher
|
||||
// z-index they would paint over the insertion line and truncate it to the
|
||||
// scrollable columns.
|
||||
const StyledRowDropTargetSlot = styled.div`
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -1px;
|
||||
z-index: ${TABLE_Z_INDEX.rowDropLine};
|
||||
`;
|
||||
|
||||
@@ -66,7 +70,7 @@ export const RecordTableDraggableTr = ({
|
||||
// per-instance id avoids the collision; recordId travels in the drag data.
|
||||
const [sortableId] = useState(() => v4());
|
||||
|
||||
const { handleRef, ref, isDragSource, isDropTarget } = useSortable({
|
||||
const { handleRef, ref, isDragSource } = useSortable({
|
||||
id: sortableId,
|
||||
index: draggableIndex,
|
||||
group: droppableId,
|
||||
@@ -97,10 +101,19 @@ export const RecordTableDraggableTr = ({
|
||||
<DragDropItemSortableHandleRefContext.Provider value={handleRef}>
|
||||
<RecordTableRowDraggableContextProvider value={{ isDragging: false }}>
|
||||
{children}
|
||||
<RecordTableRowMultiDragPreview />
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</DragDropItemSortableHandleRefContext.Provider>
|
||||
{isDropTarget && !isDragSource && <StyledRowDropLine />}
|
||||
{!isDragSource && (
|
||||
<StyledRowDropTargetSlot>
|
||||
<DragDropItemDropTarget
|
||||
index={draggableIndex}
|
||||
droppableId={droppableId}
|
||||
orientation="horizontal"
|
||||
compact
|
||||
seamAligned
|
||||
/>
|
||||
</StyledRowDropTargetSlot>
|
||||
)}
|
||||
</RecordTableTr>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-
|
||||
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
|
||||
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
|
||||
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
|
||||
import { RecordTableRowMultiDragCounterChip } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragCounterChip';
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { type RecordTableRowDragData } from '@/object-record/record-table/types/RecordTableRowDragData';
|
||||
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
|
||||
@@ -194,7 +194,7 @@ export const RecordTableRowDragOverlayContent = ({
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableTr>
|
||||
</StyledRowClipContainer>
|
||||
<RecordTableRowMultiDragCounterChip />
|
||||
<RecordTableRowMultiDragPreview recordId={recordId} />
|
||||
</StyledRowDragOverlayCSSBridge>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -1,4 +1,5 @@
|
||||
import { originalDragSelectionComponentState } from '@/object-record/record-drag/states/originalDragSelectionComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { NotificationCounter } from 'twenty-ui/data-display';
|
||||
@@ -11,8 +12,11 @@ const StyledNotificationCounterContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const RecordTableRowMultiDragCounterChip = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const originalDragSelection = useAtomComponentStateValue(
|
||||
originalDragSelectionComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const selectedCount = originalDragSelection.length ?? 0;
|
||||
|
||||
+10
-3
@@ -1,14 +1,21 @@
|
||||
import { isRecordIdPrimaryDragMultipleComponentFamilyState } from '@/object-record/record-drag/states/isRecordIdPrimaryDragMultipleComponentFamilyState';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordTableRowMultiDragCounterChip } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragCounterChip';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
|
||||
export const RecordTableRowMultiDragPreview = () => {
|
||||
const { recordId } = useRecordTableRowContextOrThrow();
|
||||
type RecordTableRowMultiDragPreviewProps = {
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const RecordTableRowMultiDragPreview = ({
|
||||
recordId,
|
||||
}: RecordTableRowMultiDragPreviewProps) => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const isRecordIdPrimaryDragMultiple = useAtomComponentFamilyStateValue(
|
||||
isRecordIdPrimaryDragMultipleComponentFamilyState,
|
||||
{ recordId },
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
if (!isRecordIdPrimaryDragMultiple) {
|
||||
|
||||
+4
@@ -1,11 +1,15 @@
|
||||
import { isRecordIdSecondaryDragMultipleComponentFamilyState } from '@/object-record/record-drag/states/isRecordIdSecondaryDragMultipleComponentFamilyState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
|
||||
export const useIsTableRowSecondaryDragged = (recordId: Nullable<string>) => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const isRecordIdSecondaryDragMultiple = useAtomComponentFamilyStateValue(
|
||||
isRecordIdSecondaryDragMultipleComponentFamilyState,
|
||||
{ recordId: recordId ?? '' },
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user