feat(page-layout): drag widgets across tabs on record pages (#23023)

## What

When editing a record page layout, you can now drag a widget out of one
tab and into another. The most common case works: drag from the left
column (the pinned first tab, in full mode) into the tab you're
currently viewing.

Ways to move a widget across tabs:
- **Into the visible tab's content** — drop it into another
vertical-list tab's list to place it at a precise index. A blue line
shows exactly where it will land.
- **Onto a tab button** — drop a widget onto another tab's button to
move it to that tab; the button highlights while hovered.
- **Into an empty tab / the end of a tab** — an end-of-list drop zone
(wrapping the add-widget area) accepts the widget, so an empty tab is a
valid drop target and widgets can be appended to the end of a populated
one.

Within-tab reordering keeps working as before, now with the same blue
drop-line indicator.

## How

The record-page widget list is migrated from `@hello-pangea/dnd` to
`@dnd-kit/react` (already used elsewhere in the app, e.g.
navigation-menu-item and record-board). A single `DragDropProvider`
spans the left column, the tab bar, and the active tab content, which is
what makes cross-list drag possible — Pangea scopes each list to its own
context, so cross-tab drag wasn't expressible there.

- Widgets are dnd-kit sortables grouped by `tabId`; dropping into a
different group is a cross-tab move.
- The drop line uses `useSortable().isDropTarget` on the targeted
widget, plus an end-of-list droppable for append/empty-tab.
- Each record-page tab button is a `useDroppable` target for widgets,
opt-in per vertical-list tab so canvas/grid tabs keep their native
placement.
- The drag lifecycle lives in one router hook
(`usePageLayoutWidgetDragAndDrop`) that routes to two pure, unit-tested
draft utils (`moveWidgetWithinTabInDraft`, `moveWidgetToTabInDraft`).
The side-panel "Move to tab" action shares the same
`moveWidgetToTabInDraft` util, so drag and menu paths converge on one
mutation.
- Drop resolution reuses the shared module #23071 landed:
`getDestinationIndex` compensates same-tab downward moves for the
source-removal shift so the drop line and the landing slot agree, and
the shared `preventNativeDragStart` guard stops links/images inside
widget content from starting a native URL drag.

## Scope

Deliberately staged to the record-page widget list. Not included
(follow-ups):
- Tab reordering and the field-config editors still use
`@hello-pangea/dnd`; finishing the full page-layout removal of Pangea is
separate.
- Grid/dashboard cross-tab drag (the source there is
`react-grid-layout`, which needs a cross-system bridge).
- #23071 has merged and this branch sits on it; the remaining
convergence is a follow-up: fold
`PageLayoutWidgetSortableItem`/`PageLayoutWidgetDropLine` into the
shared `DragDropItem*` cells, export a generic drag-event-type helper to
delete the 7 copied `Parameters<...>` extractions, replace
`useMovePageLayoutWidgetUp/Down` with `moveWidgetWithinTabInDraft`, and
migrate the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Testing

- Unit tests for `moveWidgetToTabInDraft` and
`moveWidgetWithinTabInDraft` (incl. the non-vertical destination guard);
the three suites now share one fixture module
(`page-layout/testing/pageLayoutDraftFixtures`).
- The downward off-by-one is covered by the shared `getDestinationIndex`
unit tests from #23071.
- Full `page-layout` suite green (161 files / 1055 tests), plus
typecheck, lint, and format.
- The drag interaction itself (drop precision incl. downward same-tab
drops, line/highlight, clone feedback) still needs a manual pass in the
running app.
This commit is contained in:
Félix Malfait
2026-07-23 15:06:20 +02:00
committed by GitHub
parent 96a2456367
commit 65d399c70d
24 changed files with 939 additions and 332 deletions
@@ -6,7 +6,6 @@ import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutCo
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
import { RecordPageAddWidgetSection } from '@/page-layout/widgets/components/RecordPageAddWidgetSection';
import { styled } from '@linaria/react';
@@ -25,8 +24,6 @@ export const PageLayoutContent = () => {
const { tabId } = usePageLayoutContentContext();
const { reorderWidgets } = useReorderPageLayoutWidgets(tabId);
const activeTab = usePageLayoutTabWithVisibleWidgetsOrThrow(tabId);
const { layoutMode } = usePageLayoutContentContext();
@@ -60,8 +57,6 @@ export const PageLayoutContent = () => {
return (
<PageLayoutVerticalListEditor
widgets={activeTab.widgets}
onReorder={reorderWidgets}
isReorderEnabled={true}
trailingElement={<RecordPageAddWidgetSection />}
/>
);
@@ -1,4 +1,5 @@
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
import { PageLayoutWidgetDndProvider } from '@/page-layout/components/dnd/PageLayoutWidgetDndProvider';
import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
@@ -65,7 +66,9 @@ const PageLayoutSingleTabRendererInner = () => {
layoutMode,
}}
>
<PageLayoutContent />
<PageLayoutWidgetDndProvider>
<PageLayoutContent />
</PageLayoutWidgetDndProvider>
</PageLayoutContentProvider>
</>
);
@@ -49,7 +49,10 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type PageLayoutType } from '~/generated-metadata/graphql';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -320,6 +323,18 @@ export const PageLayoutTabList = ({
const shouldRenderStaticDropdown = hasHiddenTabs && !canReorderTabs;
// Widgets can only be dropped onto record-page vertical-list tabs; other tab
// types keep their native (canvas/grid) widget placement.
const widgetDropTargetTabIds = new Set(
pageLayoutType === PageLayoutType.RECORD_PAGE
? tabs
.filter(
(tab) => tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST,
)
.map((tab) => tab.id)
: [],
);
return (
<TabListComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
@@ -371,6 +386,7 @@ export const PageLayoutTabList = ({
onChangeTab={onChangeTab}
onSelectTab={handleSelectTab}
canReorder={canReorderTabs}
widgetDropTargetTabIds={widgetDropTargetTabIds}
/>
{shouldRenderReorderableDropdown && (
@@ -436,6 +452,7 @@ export const PageLayoutTabList = ({
onChangeTab={onChangeTab}
onSelectTab={handleSelectTab}
canReorder={canReorderTabs}
widgetDropTargetTabIds={widgetDropTargetTabIds}
/>
{shouldRenderStaticDropdown && (
<StyledDropdownContainer>
@@ -1,5 +1,6 @@
import { Draggable } from '@hello-pangea/dnd';
import { PageLayoutTabWidgetDropTarget } from '@/page-layout/components/dnd/PageLayoutTabWidgetDropTarget';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -12,6 +13,7 @@ type PageLayoutTabListReorderableTabProps = {
index: number;
isActive: boolean;
disabled?: boolean;
isWidgetDropTarget?: boolean;
onSelect: () => void;
};
@@ -27,6 +29,7 @@ export const PageLayoutTabListReorderableTab = ({
index,
isActive,
disabled,
isWidgetDropTarget = false,
onSelect,
}: PageLayoutTabListReorderableTabProps) => {
const pageLayoutTabSettingsOpenTabId = useAtomComponentStateValue(
@@ -34,7 +37,8 @@ export const PageLayoutTabListReorderableTab = ({
);
const isSettingsOpenForThisTab = pageLayoutTabSettingsOpenTabId === tab.id;
return (
const draggableTab = (
<Draggable draggableId={tab.id} index={index} isDragDisabled={disabled}>
{(draggableProvided, draggableSnapshot) => (
<StyledTabContainer
@@ -66,4 +70,14 @@ export const PageLayoutTabListReorderableTab = ({
)}
</Draggable>
);
if (!isWidgetDropTarget) {
return draggableTab;
}
return (
<PageLayoutTabWidgetDropTarget tabId={tab.id}>
{draggableTab}
</PageLayoutTabWidgetDropTarget>
);
};
@@ -23,6 +23,7 @@ type PageLayoutTabListVisibleTabsProps = {
onChangeTab?: (tabId: string) => void;
onSelectTab: (tabId: string) => void;
canReorder: boolean;
widgetDropTargetTabIds: Set<string>;
};
const StyledTabContainer = styled.div`
@@ -45,6 +46,7 @@ export const PageLayoutTabListVisibleTabs = ({
onChangeTab,
onSelectTab,
canReorder,
widgetDropTargetTabIds,
}: PageLayoutTabListVisibleTabsProps) => {
if (canReorder) {
return (
@@ -80,6 +82,7 @@ export const PageLayoutTabListVisibleTabs = ({
index={index}
isActive={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
isWidgetDropTarget={widgetDropTargetTabIds.has(tab.id)}
onSelect={() => onSelectTab(tab.id)}
/>
))}
@@ -1,6 +1,7 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { PageLayoutWidgetDndProvider } from '@/page-layout/components/dnd/PageLayoutWidgetDndProvider';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
@@ -192,57 +193,59 @@ export const PageLayoutTabsRenderer = () => {
);
return (
<StyledContainer hasPinnedTab={isDefined(pinnedLeftTab)}>
{isDefined(pinnedLeftTab) && (
<PageLayoutLeftPanel pinnedLeftTabId={pinnedLeftTab.id} />
)}
<StyledTabsAndDashboardContainer>
<PageLayoutTabListEffect
tabs={sortedActiveTabs}
componentInstanceId={tabListInstanceId}
defaultTabToFocusOnMobileAndSidePanelId={
currentPageLayout.defaultTabToFocusOnMobileAndSidePanelId ??
undefined
}
/>
{(sortedActiveTabs.length > 1 || isPageLayoutInEditMode) && (
<PageLayoutTabList
className="page-layout-tab-list-print-hidden"
tabs={sortedActiveTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
isInSidePanel={isInSidePanel}
componentInstanceId={tabListInstanceId}
addTabStrategy={addTabStrategy}
isReorderEnabled={canEnableTabEditing}
onReorder={
canEnableTabEditing
? (result, provided) =>
reorderRecordPageTabs(
result,
provided,
isDefined(pinnedLeftTab),
)
: undefined
}
pageLayoutType={currentPageLayout.type}
/>
<PageLayoutWidgetDndProvider>
<StyledContainer hasPinnedTab={isDefined(pinnedLeftTab)}>
{isDefined(pinnedLeftTab) && (
<PageLayoutLeftPanel pinnedLeftTabId={pinnedLeftTab.id} />
)}
<StyledScrollWrapperContainer>
<ScrollWrapper
className="page-layout-scroll-wrapper"
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
currentPageLayout.id,
)}
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && activeTabExistsInCurrentPageLayout && (
<PageLayoutMainContent tabId={activeTabId} />
)}
</ScrollWrapper>
</StyledScrollWrapperContainer>
</StyledTabsAndDashboardContainer>
</StyledContainer>
<StyledTabsAndDashboardContainer>
<PageLayoutTabListEffect
tabs={sortedActiveTabs}
componentInstanceId={tabListInstanceId}
defaultTabToFocusOnMobileAndSidePanelId={
currentPageLayout.defaultTabToFocusOnMobileAndSidePanelId ??
undefined
}
/>
{(sortedActiveTabs.length > 1 || isPageLayoutInEditMode) && (
<PageLayoutTabList
className="page-layout-tab-list-print-hidden"
tabs={sortedActiveTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
isInSidePanel={isInSidePanel}
componentInstanceId={tabListInstanceId}
addTabStrategy={addTabStrategy}
isReorderEnabled={canEnableTabEditing}
onReorder={
canEnableTabEditing
? (result, provided) =>
reorderRecordPageTabs(
result,
provided,
isDefined(pinnedLeftTab),
)
: undefined
}
pageLayoutType={currentPageLayout.type}
/>
)}
<StyledScrollWrapperContainer>
<ScrollWrapper
className="page-layout-scroll-wrapper"
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
currentPageLayout.id,
)}
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && activeTabExistsInCurrentPageLayout && (
<PageLayoutMainContent tabId={activeTabId} />
)}
</ScrollWrapper>
</StyledScrollWrapperContainer>
</StyledTabsAndDashboardContainer>
</StyledContainer>
</PageLayoutWidgetDndProvider>
);
};
@@ -1,24 +1,20 @@
import { pointerIntersection } from '@dnd-kit/collision';
import { useDroppable } from '@dnd-kit/react';
import { PageLayoutWidgetDropLine } from '@/page-layout/components/dnd/PageLayoutWidgetDropLine';
import { PageLayoutWidgetSortableItem } from '@/page-layout/components/dnd/PageLayoutWidgetSortableItem';
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { type PageLayoutVerticalListViewerVariant } from '@/page-layout/types/PageLayoutVerticalListViewerVariant';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { type PageLayoutWidgetListDropData } from '@/page-layout/types/PageLayoutWidgetDndData';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import {
DragDropContext,
Draggable,
Droppable,
type DropResult,
} from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { type ReactNode, useId } from 'react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
const StyledVerticalListContainer = styled.div<{
variant: PageLayoutVerticalListViewerVariant;
shouldUseWhiteBackground: boolean;
@@ -36,29 +32,27 @@ const StyledVerticalListContainer = styled.div<{
: themeCssVariables.spacing[2]};
`;
const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>`
background: ${({ isDragging }) =>
isDragging
? themeCssVariables.background.transparent.light
: 'transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
transition: background 0.1s ease;
// Catches drops below the last widget (append) and drops into an empty tab,
// where there is no sortable item to target.
const StyledEndDropZone = styled.div`
display: flex;
flex: 1;
flex-direction: column;
gap: ${themeCssVariables.spacing[4]};
min-height: ${themeCssVariables.spacing[6]};
position: relative;
`;
type PageLayoutVerticalListEditorProps = {
widgets: PageLayoutWidget[];
onReorder: (result: DropResult) => void;
isReorderEnabled?: boolean;
trailingElement?: ReactNode;
};
export const PageLayoutVerticalListEditor = ({
widgets,
onReorder,
isReorderEnabled = true,
trailingElement,
}: PageLayoutVerticalListEditorProps) => {
const droppableId = `page-layout-vertical-list-${useId()}`;
const { tabId } = usePageLayoutContentContext();
const { isInSidePanel } = useLayoutRenderingContext();
const isMobile = useIsMobile();
@@ -70,56 +64,36 @@ export const PageLayoutVerticalListEditor = ({
isInSidePanel,
});
const setPageLayoutDraggingWidgetId = useSetAtomComponentState(
pageLayoutDraggingWidgetIdComponentState,
);
const endDropData: PageLayoutWidgetListDropData = {
type: 'widget-list',
tabId,
};
const { ref: endDropRef, isDropTarget: isEndDropTarget } = useDroppable({
id: `page-layout-widget-list-${tabId}`,
collisionDetector: pointerIntersection,
data: endDropData,
});
return (
<DragDropContext
onDragStart={(result) => {
setPageLayoutDraggingWidgetId(result.draggableId);
}}
onDragEnd={(result) => {
setPageLayoutDraggingWidgetId(null);
onReorder(result);
}}
<StyledVerticalListContainer
variant={variant}
shouldUseWhiteBackground={!isInPinnedTab || isMobile}
>
<Droppable droppableId={droppableId}>
{(provided) => (
<StyledVerticalListContainer
ref={provided.innerRef}
variant={variant}
shouldUseWhiteBackground={!isInPinnedTab || isMobile}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{widgets.map((widget, index) => (
<Draggable
key={widget.id}
draggableId={widget.id}
index={index}
isDragDisabled={!isReorderEnabled}
>
{(provided, snapshot) => (
<StyledDraggableWrapper
ref={provided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...getCssCompatibleDraggableProps(provided.draggableProps)}
isDragging={snapshot.isDragging}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<div {...provided.dragHandleProps}>
<WidgetRenderer widget={widget} />
</div>
</StyledDraggableWrapper>
)}
</Draggable>
))}
{provided.placeholder}
{trailingElement}
</StyledVerticalListContainer>
)}
</Droppable>
</DragDropContext>
{widgets.map((widget, index) => (
<PageLayoutWidgetSortableItem
key={widget.id}
widgetId={widget.id}
tabId={tabId}
index={index}
>
<WidgetRenderer widget={widget} />
</PageLayoutWidgetSortableItem>
))}
<StyledEndDropZone ref={endDropRef}>
{isEndDropTarget && <PageLayoutWidgetDropLine />}
{trailingElement}
</StyledEndDropZone>
</StyledVerticalListContainer>
);
};
@@ -0,0 +1,42 @@
import { pointerIntersection } from '@dnd-kit/collision';
import { useDroppable } from '@dnd-kit/react';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type PageLayoutTabWidgetDropData } from '@/page-layout/types/PageLayoutWidgetDndData';
const StyledDropTarget = styled.div<{ isActive: boolean }>`
border-radius: ${themeCssVariables.border.radius.sm};
display: flex;
outline: ${({ isActive }) =>
isActive ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
outline-offset: -1px;
`;
type PageLayoutTabWidgetDropTargetProps = {
tabId: string;
children: ReactNode;
};
export const PageLayoutTabWidgetDropTarget = ({
tabId,
children,
}: PageLayoutTabWidgetDropTargetProps) => {
const data: PageLayoutTabWidgetDropData = {
type: 'tab-widget-drop',
tabId,
};
const { ref, isDropTarget } = useDroppable({
id: `page-layout-tab-widget-drop-${tabId}`,
collisionDetector: pointerIntersection,
data,
});
return (
<StyledDropTarget ref={ref} isActive={isDropTarget}>
{children}
</StyledDropTarget>
);
};
@@ -0,0 +1,30 @@
import { DragDropProvider } from '@dnd-kit/react';
import { type ReactNode } from 'react';
import { usePageLayoutWidgetDragAndDrop } from '@/page-layout/hooks/usePageLayoutWidgetDragAndDrop';
import { type PageLayoutWidgetDndData } from '@/page-layout/types/PageLayoutWidgetDndData';
import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
type PageLayoutWidgetDndProviderProps = {
children: ReactNode;
};
// Mounted in both view and edit mode so toggling edit mode does not remount the
// layout subtree (which would reset scroll position and widget-local state).
// Widget sortables and drop targets only render while editing, so the provider
// is inert in view mode.
export const PageLayoutWidgetDndProvider = ({
children,
}: PageLayoutWidgetDndProviderProps) => {
const { handlers } = usePageLayoutWidgetDragAndDrop();
return (
<DragDropProvider<PageLayoutWidgetDndData>
sensors={DND_KIT_SENSORS}
onDragStart={handlers.onDragStart}
onDragEnd={handlers.onDragEnd}
>
{children}
</DragDropProvider>
);
};
@@ -0,0 +1,24 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
// Absolutely positioned in the gap above its (position: relative) parent so
// activating the drop target does not reflow the list.
const StyledDropLineContainer = styled.div`
left: 0;
position: absolute;
right: 0;
top: calc(-1 * ${themeCssVariables.spacing[2]});
`;
const StyledDropLine = styled.div`
background-color: ${themeCssVariables.color.blue};
border-radius: ${themeCssVariables.border.radius.sm};
height: 2px;
width: 100%;
`;
export const PageLayoutWidgetDropLine = () => (
<StyledDropLineContainer>
<StyledDropLine />
</StyledDropLineContainer>
);
@@ -0,0 +1,64 @@
import { SortableKeyboardPlugin } from '@dnd-kit/dom/sortable';
import { useSortable } from '@dnd-kit/react/sortable';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { PageLayoutWidgetDropLine } from '@/page-layout/components/dnd/PageLayoutWidgetDropLine';
import { type PageLayoutWidgetDragData } from '@/page-layout/types/PageLayoutWidgetDndData';
import { preventNativeDragStart } from '@/ui/utilities/drag-and-drop/utils/preventNativeDragStart';
const PLUGINS_WITHOUT_OPTIMISTIC = [SortableKeyboardPlugin];
const StyledSortableRoot = styled.div<{ isDragging: boolean }>`
background: ${({ isDragging }) =>
isDragging
? themeCssVariables.background.transparent.light
: 'transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
min-height: 0;
position: relative;
transition: background 0.1s ease;
`;
type PageLayoutWidgetSortableItemProps = {
widgetId: string;
tabId: string;
index: number;
children: ReactNode;
};
export const PageLayoutWidgetSortableItem = ({
widgetId,
tabId,
index,
children,
}: PageLayoutWidgetSortableItemProps) => {
const data: PageLayoutWidgetDragData = {
type: 'widget',
widgetId,
tabId,
index,
};
const { ref, isDragging, isDropTarget } = useSortable({
id: widgetId,
index,
group: tabId,
data,
transition: null,
plugins: PLUGINS_WITHOUT_OPTIMISTIC,
feedback: 'clone',
});
return (
<StyledSortableRoot
ref={ref}
isDragging={isDragging}
onDragStart={preventNativeDragStart}
>
{isDropTarget && <PageLayoutWidgetDropLine />}
{children}
</StyledSortableRoot>
);
};