17ff17bdec
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>
119 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
};
|