mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-26 17:37:33 +00:00
feat(ui/homepage): handle drag and drop with small modules (#14151)
This commit is contained in:
parent
5bcbc95d61
commit
62a9707c43
@ -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 &&
|
||||
|
||||
@ -64,6 +64,7 @@ function LargeModule({ children, module, position, loading, onClickViewAll }: Re
|
||||
data: {
|
||||
module,
|
||||
position,
|
||||
isSmall: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -89,3 +89,5 @@ export const LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||
DataHubPageModuleType.Hierarchy,
|
||||
DataHubPageModuleType.RichText,
|
||||
];
|
||||
|
||||
export const SMALL_MODULE_TYPES: DataHubPageModuleType[] = [DataHubPageModuleType.Link];
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user