Files
twenty/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListVisibleTabs.tsx
T
Priyanshu Bartwal 17ff17bdec 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>
2026-08-05 14:51:51 +00:00

119 lines
3.4 KiB
TypeScript

import { styled } from '@linaria/react';
import { TabButton } from 'twenty-ui/input';
import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListReorderableTab } from '@/page-layout/components/PageLayoutTabListReorderableTab';
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
type PageLayoutTabListVisibleTabsProps = {
visibleTabs: SingleTabProps[];
visibleTabCount: number;
activeTabId: string | null;
behaveAsLinks: boolean;
loading?: boolean;
onChangeTab?: (tabId: string) => void;
onSelectTab: (tabId: string) => void;
canReorder: boolean;
widgetDropTargetTabIds: Set<string>;
firstHiddenTabId: string | null;
};
const StyledTabContainer = styled.div`
display: flex;
max-width: 100%;
overflow: hidden;
position: relative;
> *:not(:last-child) {
margin-right: ${TAB_LIST_GAP}px;
}
`;
const StyledTabSlot = styled.div`
display: flex;
`;
const StyledLeadingDropTarget = styled.div`
flex: 0 0 2px;
margin-left: -1px;
margin-right: -1px;
`;
export const PageLayoutTabListVisibleTabs = ({
visibleTabs,
visibleTabCount,
activeTabId,
behaveAsLinks,
loading,
onChangeTab,
onSelectTab,
canReorder,
widgetDropTargetTabIds,
firstHiddenTabId,
}: PageLayoutTabListVisibleTabsProps) => {
if (canReorder) {
const shownTabs = visibleTabs.slice(0, visibleTabCount);
return (
<StyledTabContainer>
{shownTabs.map((tab, index) => (
<StyledTabSlot key={tab.id}>
<StyledLeadingDropTarget>
<DragDropItemDropTarget
index={index}
droppableId={PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS.VISIBLE_TABS}
orientation="vertical"
compact
/>
</StyledLeadingDropTarget>
<PageLayoutTabListReorderableTab
tab={tab}
index={index}
group={PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS.VISIBLE_TABS}
nextTabId={shownTabs[index + 1]?.id ?? firstHiddenTabId}
isActive={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
isWidgetDropTarget={widgetDropTargetTabIds.has(tab.id)}
onSelect={() => onSelectTab(tab.id)}
/>
</StyledTabSlot>
))}
<StyledLeadingDropTarget>
<DragDropItemDropTarget
index={visibleTabCount}
droppableId={PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS.VISIBLE_TABS}
orientation="vertical"
compact
/>
</StyledLeadingDropTarget>
</StyledTabContainer>
);
}
return (
<StyledTabContainer>
{visibleTabs.slice(0, visibleTabCount).map((tab) => (
<TabButton
key={tab.id}
id={tab.id}
title={tab.title}
LeftIcon={tab.Icon}
logo={tab.logo}
active={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
pill={tab.pill}
to={behaveAsLinks ? `#${tab.id}` : undefined}
onClick={
behaveAsLinks
? () => onChangeTab?.(tab.id)
: () => onSelectTab(tab.id)
}
/>
))}
</StyledTabContainer>
);
};