feat(ui/homepage): handle drag and drop with small modules (#14151)

This commit is contained in:
purnimagarg1 2025-07-21 22:05:09 +05:30 committed by GitHub
parent 5bcbc95d61
commit 62a9707c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 106 additions and 17 deletions

View File

@ -9,17 +9,20 @@ import { PageModuleFragment } from '@graphql/template.generated';
interface DraggedModuleData {
module: PageModuleFragment;
position: ModulePositionInput;
isSmall: boolean;
}
export interface DroppableData {
rowIndex: number;
moduleIndex?: number; // If undefined, drop at the end of the row
insertNewRow?: boolean; // If true, create a new row at this position
isSmall?: boolean; // If undefined, accept any module size
}
export interface ActiveDragModule {
module: PageModuleFragment;
position: ModulePositionInput;
isSmall: boolean;
}
export function useDragAndDrop() {
@ -33,6 +36,7 @@ export function useDragAndDrop() {
| {
module?: PageModuleFragment;
position?: ModulePositionInput;
isSmall: boolean;
}
| undefined;
@ -40,6 +44,7 @@ export function useDragAndDrop() {
setActiveModule({
module: draggedData.module,
position: draggedData.position,
isSmall: draggedData.isSmall,
});
}
}, []);
@ -55,6 +60,14 @@ export function useDragAndDrop() {
const draggedData = active.data.current as DraggedModuleData;
const droppableData = over.data.current as DroppableData;
const isDragSmall = draggedData.isSmall;
const isDropSmall = droppableData.isSmall;
// Check if we're dropping in mis-matched sized row
if (isDropSmall !== undefined && isDragSmall !== undefined && isDragSmall !== isDropSmall) {
return;
}
// Check if we're dropping in the same position
if (
draggedData.position.rowIndex === droppableData.rowIndex &&

View File

@ -64,6 +64,7 @@ function LargeModule({ children, module, position, loading, onClickViewAll }: Re
data: {
module,
position,
isSmall: false,
},
});

View File

@ -1,3 +1,5 @@
import { Icon } from '@components';
import { useDraggable } from '@dnd-kit/core';
import React from 'react';
import styled from 'styled-components';
@ -6,12 +8,29 @@ import ModuleMenu from '@app/homeV3/module/components/ModuleMenu';
import { ModuleProps } from '@app/homeV3/module/types';
import { FloatingRightHeaderSection } from '@app/homeV3/styledComponents';
const Container = styled.div`
const DragIcon = styled(Icon)<{ isDragging: boolean }>`
cursor: ${(props) => (props.isDragging ? 'grabbing' : 'grab')};
display: none;
position: absolute;
left: 4px;
top: 50%;
transform: translateY(-50%);
`;
const ContainerWithHover = styled.div`
display: flex;
flex-direction: column;
position: relative;
height: 100%;
justify-content: center;
:hover {
background: linear-gradient(180deg, #fff 0%, #fafafb 100%);
}
:hover ${DragIcon} {
display: block;
}
`;
const Content = styled.div`
@ -20,20 +39,37 @@ const Content = styled.div`
`;
const StyledModuleContainer = styled(ModuleContainer)<{ clickable?: boolean }>`
max-height: 72px;
max-height: 64px;
${({ clickable }) => clickable && `cursor: pointer;`}
`;
export default function SmallModule({ children, module, position, onClick }: React.PropsWithChildren<ModuleProps>) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `module-${module.urn}-${position.rowIndex}-${position.moduleIndex}`,
data: {
module,
position,
isSmall: true,
},
});
return (
<StyledModuleContainer clickable={!!onClick} onClick={onClick}>
<Container>
<StyledModuleContainer clickable={!!onClick} onClick={onClick} ref={setNodeRef} {...attributes}>
<ContainerWithHover>
<DragIcon
{...listeners}
size="lg"
color="gray"
icon="DotsSixVertical"
source="phosphor"
isDragging={isDragging}
/>
<Content>{children}</Content>
<FloatingRightHeaderSection>
<ModuleMenu module={module} position={position} />
</FloatingRightHeaderSection>
</Container>
</ContainerWithHover>
</StyledModuleContainer>
);
}

View File

@ -89,3 +89,5 @@ export const LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
DataHubPageModuleType.Hierarchy,
DataHubPageModuleType.RichText,
];
export const SMALL_MODULE_TYPES: DataHubPageModuleType[] = [DataHubPageModuleType.Link];

View File

@ -4,7 +4,7 @@ import React, { useCallback, useMemo } from 'react';
import { RESET_DROPDOWN_MENU_STYLES_CLASSNAME } from '@components/components/Dropdown/constants';
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
import { LARGE_MODULE_TYPES } from '@app/homeV3/modules/constants';
import { LARGE_MODULE_TYPES, SMALL_MODULE_TYPES } from '@app/homeV3/modules/constants';
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
import { convertModuleToModuleInfo } from '@app/homeV3/modules/utils';
import GroupItem from '@app/homeV3/template/components/addModuleMenu/components/GroupItem';
@ -48,11 +48,16 @@ export default function useAddModuleMenu(
template,
} = usePageTemplateContext();
const isRowWithLargeModule =
const isLargeModuleRow =
position.rowIndex !== undefined &&
template?.properties.rows[position.rowIndex]?.modules?.some((module) =>
LARGE_MODULE_TYPES.includes(module.properties.type),
);
const isSmallModuleRow =
position.rowIndex !== undefined &&
template?.properties.rows[position.rowIndex]?.modules?.some((module) =>
SMALL_MODULE_TYPES.includes(module.properties.type),
);
const handleAddExistingModule = useCallback(
(module: PageModuleFragment) => {
@ -83,7 +88,7 @@ export default function useAddModuleMenu(
onClick: () => {
handleOpenCreateModuleModal(DataHubPageModuleType.Link);
},
disabled: isRowWithLargeModule,
disabled: isLargeModuleRow,
};
const documentation = {
@ -93,6 +98,7 @@ export default function useAddModuleMenu(
onClick: () => {
handleOpenCreateModuleModal(DataHubPageModuleType.RichText);
},
disabled: isSmallModuleRow,
};
items.push({
@ -109,6 +115,7 @@ export default function useAddModuleMenu(
onClick: () => {
handleAddExistingModule(YOUR_ASSETS_MODULE);
},
disabled: isSmallModuleRow,
};
const domains = {
@ -118,6 +125,7 @@ export default function useAddModuleMenu(
onClick: () => {
handleAddExistingModule(DOMAINS_MODULE);
},
disabled: isSmallModuleRow,
};
const assetCollection = {
@ -133,6 +141,7 @@ export default function useAddModuleMenu(
onClick: () => {
handleOpenCreateModuleModal(DataHubPageModuleType.AssetCollection);
},
disabled: isSmallModuleRow,
};
items.push({
@ -170,7 +179,8 @@ export default function useAddModuleMenu(
return { items };
}, [
isRowWithLargeModule,
isLargeModuleRow,
isSmallModuleRow,
modulesAvailableToAdd.adminCreatedModules,
handleOpenCreateModuleModal,
handleAddExistingModule,

View File

@ -1,3 +1,4 @@
import { useDndContext } from '@dnd-kit/core';
import React, { memo } from 'react';
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
@ -12,14 +13,24 @@ interface Props {
}
function TemplateRow({ row, modulesAvailableToAdd, rowIndex }: Props) {
const { modulePositions, shouldDisableDropZones } = useTemplateRowLogic(row, rowIndex);
const { modulePositions, shouldDisableDropZones, isSmallRow } = useTemplateRowLogic(row, rowIndex);
const { active } = useDndContext();
const isActiveModuleSmall = active?.data?.current?.isSmall;
const isDropAllowed =
!active ||
isSmallRow == null || // empty row
isActiveModuleSmall === isSmallRow;
const isDropZoneDisabled = shouldDisableDropZones || !isDropAllowed;
return (
<RowLayout
rowIndex={rowIndex}
modulePositions={modulePositions}
shouldDisableDropZones={shouldDisableDropZones}
shouldDisableDropZones={isDropZoneDisabled}
modulesAvailableToAdd={modulesAvailableToAdd}
isSmallRow={isSmallRow}
/>
);
}

View File

@ -2,8 +2,8 @@ import { useDroppable } from '@dnd-kit/core';
import React, { memo } from 'react';
import styled from 'styled-components';
const DropZone = styled.div<{ $isOver?: boolean; $canDrop?: boolean }>`
height: 316px;
const DropZone = styled.div<{ $isOver?: boolean; $canDrop?: boolean; $isSmall?: boolean | null }>`
height: ${(props) => (props.$isSmall ? '64px' : '316px')};
transition: all 0.2s ease;
${({ $isOver, $canDrop }) => {
@ -24,19 +24,21 @@ interface Props {
rowIndex: number;
moduleIndex?: number;
disabled?: boolean;
isSmall: boolean | null;
}
function ModuleDropZone({ rowIndex, moduleIndex, disabled }: Props) {
function ModuleDropZone({ rowIndex, moduleIndex, disabled, isSmall }: Props) {
const { isOver, setNodeRef } = useDroppable({
id: `drop-zone-${rowIndex}-${moduleIndex ?? 'end'}`,
disabled,
data: {
rowIndex,
moduleIndex,
isSmall,
},
});
return <DropZone ref={setNodeRef} $isOver={isOver} $canDrop={!disabled} />;
return <DropZone ref={setNodeRef} $isOver={isOver} $canDrop={!disabled} $isSmall={isSmall} />;
}
export default memo(ModuleDropZone);

View File

@ -26,6 +26,7 @@ interface Props {
modulePositions: ModulePosition[];
shouldDisableDropZones: boolean;
modulesAvailableToAdd: ModulesAvailableToAdd;
isSmallRow: boolean | null;
}
interface ModuleWrapperProps {
@ -38,7 +39,7 @@ const ModuleWrapper = memo(({ module, position }: ModuleWrapperProps) => (
<Module module={module} position={position} />
));
function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesAvailableToAdd }: Props) {
function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesAvailableToAdd, isSmallRow }: Props) {
return (
<RowWrapper>
<AddModuleButton
@ -49,7 +50,12 @@ function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesA
/>
{/* Drop zone at the beginning of the row */}
<ModuleDropZone rowIndex={rowIndex} moduleIndex={0} disabled={shouldDisableDropZones} />
<ModuleDropZone
rowIndex={rowIndex}
moduleIndex={0}
disabled={shouldDisableDropZones}
isSmall={isSmallRow}
/>
{modulePositions.map(({ module, position, key }, moduleIndex) => (
<React.Fragment key={key}>
@ -59,6 +65,7 @@ function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesA
rowIndex={rowIndex}
moduleIndex={moduleIndex + 1}
disabled={shouldDisableDropZones}
isSmall={isSmallRow}
/>
</React.Fragment>
))}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { SMALL_MODULE_TYPES } from '@app/homeV3/modules/constants';
import { ModulePositionInput } from '@app/homeV3/template/types';
import { useDragRowContext } from '@app/homeV3/templateRow/hooks/useDragRowContext';
import { WrappedRow } from '@app/homeV3/templateRow/types';
@ -39,11 +40,17 @@ export function useTemplateRowLogic(row: WrappedRow, rowIndex: number) {
[row.modules, rowIndex],
);
const isSmallRow = useMemo(
() => (row.modules.length > 0 ? SMALL_MODULE_TYPES.includes(row.modules[0].properties.type) : null),
[row.modules],
);
return {
// Row state
isRowFull,
currentModuleCount,
maxModulesPerRow,
isSmallRow,
// Drag state
isDraggingFromSameRow,