mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-02 11:49:23 +00:00
feat(ui) Add ability to add custom global widgets to templates (#14163)
This commit is contained in:
parent
e5056d9b55
commit
c9fe4ff1b4
@ -1,7 +1,5 @@
|
||||
import { IconNames } from '@components';
|
||||
|
||||
import { ModuleInfo } from '@app/homeV3/modules/types';
|
||||
|
||||
import { DataHubPageModuleType } from '@types';
|
||||
|
||||
// TODO: remove these description once descriptions in modules are implemented
|
||||
@ -25,59 +23,11 @@ export const MODULE_TYPE_TO_ICON: Map<DataHubPageModuleType, IconNames> = new Ma
|
||||
|
||||
export const DEFAULT_MODULE_ICON = 'Database';
|
||||
|
||||
export const DEFAULT_MODULE_YOUR_ASSETS: ModuleInfo = {
|
||||
type: DataHubPageModuleType.OwnedAssets,
|
||||
name: 'Your Assets',
|
||||
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.OwnedAssets),
|
||||
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.OwnedAssets) ?? DEFAULT_MODULE_ICON,
|
||||
key: 'default_module_your_assets',
|
||||
};
|
||||
|
||||
export const DEFAULT_MODULE_TOP_DOMAINS: ModuleInfo = {
|
||||
type: DataHubPageModuleType.Domains,
|
||||
name: 'Domains',
|
||||
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.Domains),
|
||||
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Domains) ?? DEFAULT_MODULE_ICON,
|
||||
key: 'default_module_top_domains',
|
||||
};
|
||||
|
||||
export const DEFAULT_MODULE_LINK: ModuleInfo = {
|
||||
type: DataHubPageModuleType.Link,
|
||||
name: 'Quick Link',
|
||||
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.Link),
|
||||
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Link) ?? DEFAULT_MODULE_ICON,
|
||||
key: 'default_module_quick_link',
|
||||
};
|
||||
|
||||
export const CUSTOM_LARGE_MODULE_ASSET_COLLECTION: ModuleInfo = {
|
||||
type: DataHubPageModuleType.AssetCollection,
|
||||
name: 'Asset Collection',
|
||||
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.AssetCollection),
|
||||
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.AssetCollection) ?? DEFAULT_MODULE_ICON,
|
||||
key: 'custom_large_module_asset_collection',
|
||||
};
|
||||
|
||||
export const DEFAULT_MODULE_HIERARCHY_VIEW: ModuleInfo = {
|
||||
type: DataHubPageModuleType.Hierarchy,
|
||||
name: 'Hierarchy View',
|
||||
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.Hierarchy),
|
||||
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Hierarchy) ?? DEFAULT_MODULE_ICON,
|
||||
key: 'default_module_hierarchy_view',
|
||||
};
|
||||
|
||||
export const DEFAULT_MODULES: ModuleInfo[] = [
|
||||
DEFAULT_MODULE_HIERARCHY_VIEW,
|
||||
DEFAULT_MODULE_YOUR_ASSETS,
|
||||
DEFAULT_MODULE_TOP_DOMAINS,
|
||||
CUSTOM_LARGE_MODULE_ASSET_COLLECTION,
|
||||
// Links isn't supported yet
|
||||
// DEFAULT_MODULE_LINK,
|
||||
];
|
||||
|
||||
export const ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||
export const CUSTOM_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||
DataHubPageModuleType.Link,
|
||||
DataHubPageModuleType.RichText,
|
||||
DataHubPageModuleType.Hierarchy,
|
||||
DataHubPageModuleType.AssetCollection,
|
||||
];
|
||||
|
||||
export const ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES,
|
||||
ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES,
|
||||
DEFAULT_MODULES,
|
||||
} from '@app/homeV3/modules/constants';
|
||||
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
|
||||
import { PageModuleFragment } from '@graphql/template.generated';
|
||||
import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
||||
|
||||
// TODO: Mocked default modules (should be replaced with the real calling of endpoint once it implemented)
|
||||
export const MOCKED_ADMIN_CREATED_MODULES: PageModuleFragment[] = [
|
||||
{
|
||||
urn: 'urn:li:dataHubPageModule:link_admin_1',
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: 'Link 1 (example)',
|
||||
type: DataHubPageModuleType.Link,
|
||||
visibility: {
|
||||
scope: PageModuleScope.Global,
|
||||
},
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
urn: 'urn:li:dataHubPageModule:link_admin_2',
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: 'Link 2 (example)',
|
||||
type: DataHubPageModuleType.Link,
|
||||
visibility: {
|
||||
scope: PageModuleScope.Global,
|
||||
},
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function useModulesAvailableToAdd(): ModulesAvailableToAdd {
|
||||
// TODO:: here we have to add logic with getting available modules
|
||||
return useMemo(() => {
|
||||
const customModules = DEFAULT_MODULES.filter((module) =>
|
||||
ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES.includes(module.type),
|
||||
);
|
||||
const customLargeModules = DEFAULT_MODULES.filter((module) =>
|
||||
ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES.includes(module.type),
|
||||
);
|
||||
const adminCreatedModules = MOCKED_ADMIN_CREATED_MODULES;
|
||||
|
||||
return {
|
||||
customModules,
|
||||
customLargeModules,
|
||||
adminCreatedModules,
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { IconNames } from '@components';
|
||||
|
||||
import { PageModuleFragment } from '@graphql/template.generated';
|
||||
import { DataHubPageModuleType } from '@types';
|
||||
|
||||
export type ModuleInfo = {
|
||||
@ -11,9 +10,3 @@ export type ModuleInfo = {
|
||||
description?: string;
|
||||
icon: IconNames;
|
||||
};
|
||||
|
||||
export type ModulesAvailableToAdd = {
|
||||
customModules: ModuleInfo[];
|
||||
customLargeModules: ModuleInfo[];
|
||||
adminCreatedModules: PageModuleFragment[]; // Full module fragments for admin-created modules
|
||||
};
|
||||
|
||||
@ -4,7 +4,6 @@ import styled from 'styled-components';
|
||||
|
||||
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||
import ModuleModalMapper from '@app/homeV3/moduleModals/ModuleModalMapper';
|
||||
import useModulesAvailableToAdd from '@app/homeV3/modules/hooks/useModulesAvailableToAdd';
|
||||
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
||||
import DragAndDropProvider from '@app/homeV3/template/components/DragAndDropProvider';
|
||||
import TemplateGrid from '@app/homeV3/template/components/TemplateGrid';
|
||||
@ -35,19 +34,14 @@ function Template({ className }: Props) {
|
||||
);
|
||||
const hasRows = useMemo(() => !!rows.length, [rows.length]);
|
||||
const wrappedRows = useMemo(() => wrapRows(rows), [rows]);
|
||||
const modulesAvailableToAdd = useModulesAvailableToAdd();
|
||||
|
||||
return (
|
||||
<Wrapper className={className}>
|
||||
<DragAndDropProvider>
|
||||
<TemplateGrid wrappedRows={wrappedRows} modulesAvailableToAdd={modulesAvailableToAdd} />
|
||||
<TemplateGrid wrappedRows={wrappedRows} />
|
||||
</DragAndDropProvider>
|
||||
|
||||
<StyledAddModulesButton
|
||||
orientation="horizontal"
|
||||
$hasRows={hasRows}
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
/>
|
||||
<StyledAddModulesButton orientation="horizontal" $hasRows={hasRows} />
|
||||
<ModuleModalMapper />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import { Button, Dropdown, Icon, colors } from '@components';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
import useAddModuleMenu from '@app/homeV3/template/components/addModuleMenu/useAddModuleMenu';
|
||||
import { ModulePositionInput, RowSide } from '@app/homeV3/template/types';
|
||||
|
||||
@ -44,13 +43,12 @@ const StyledVisibleOnHoverButton = styled(StyledButton)`
|
||||
|
||||
interface Props {
|
||||
orientation: AddModuleButtonOrientation;
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
className?: string;
|
||||
rowIndex?: number;
|
||||
rowSide?: RowSide;
|
||||
}
|
||||
|
||||
export default function AddModuleButton({ orientation, modulesAvailableToAdd, className, rowIndex, rowSide }: Props) {
|
||||
export default function AddModuleButton({ orientation, className, rowIndex, rowSide }: Props) {
|
||||
const [isOpened, setIsOpened] = useState<boolean>(false);
|
||||
|
||||
const ButtonComponent = useMemo(() => (isOpened ? StyledButton : StyledVisibleOnHoverButton), [isOpened]);
|
||||
@ -63,7 +61,7 @@ export default function AddModuleButton({ orientation, modulesAvailableToAdd, cl
|
||||
|
||||
const closeMenu = () => setIsOpened(false);
|
||||
|
||||
const menu = useAddModuleMenu(modulesAvailableToAdd, position, closeMenu);
|
||||
const menu = useAddModuleMenu(position, closeMenu);
|
||||
|
||||
const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
// FYI: Antd can open dropdown in the cursor's position only for contextMenu trigger
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
|
||||
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
import NewRowDropZone from '@app/homeV3/template/components/NewRowDropZone';
|
||||
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
|
||||
import { WrappedRow } from '@app/homeV3/templateRow/types';
|
||||
|
||||
interface Props {
|
||||
wrappedRows: WrappedRow[];
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
}
|
||||
|
||||
function TemplateGrid({ wrappedRows, modulesAvailableToAdd }: Props) {
|
||||
function TemplateGrid({ wrappedRows }: Props) {
|
||||
// Memoize the template rows with drop zones between them
|
||||
const templateRowsWithDropZones = useMemo(() => {
|
||||
const result: React.ReactElement[] = [];
|
||||
@ -24,9 +22,7 @@ function TemplateGrid({ wrappedRows, modulesAvailableToAdd }: Props) {
|
||||
|
||||
// Add the actual row
|
||||
const rowKey = `templateRow-${i}`;
|
||||
result.push(
|
||||
<TemplateRow key={rowKey} row={row} rowIndex={i} modulesAvailableToAdd={modulesAvailableToAdd} />,
|
||||
);
|
||||
result.push(<TemplateRow key={rowKey} row={row} rowIndex={i} />);
|
||||
|
||||
// Add drop zone after each row (for inserting between/after rows)
|
||||
const finalDropKey = `drop-zone-after-${i}`;
|
||||
@ -34,7 +30,7 @@ function TemplateGrid({ wrappedRows, modulesAvailableToAdd }: Props) {
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [wrappedRows, modulesAvailableToAdd]);
|
||||
}, [wrappedRows]);
|
||||
|
||||
return <>{templateRowsWithDropZones}</>;
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { ModuleInfo, ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
import useAddModuleMenu from '@app/homeV3/template/components/addModuleMenu/useAddModuleMenu';
|
||||
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||
|
||||
@ -12,16 +11,70 @@ import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
||||
// Mock the PageTemplateContext
|
||||
const mockAddModule = vi.fn();
|
||||
const mockOpenModal = vi.fn();
|
||||
|
||||
// Mock template and globalTemplate data - using any to avoid complex type issues
|
||||
const mockTemplate = {
|
||||
properties: {
|
||||
rows: [
|
||||
{
|
||||
modules: [
|
||||
{
|
||||
properties: {
|
||||
type: DataHubPageModuleType.Link,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mockGlobalTemplate = {
|
||||
properties: {
|
||||
rows: [
|
||||
{
|
||||
modules: [
|
||||
{
|
||||
urn: 'urn:li:dataHubPageModule:admin1',
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: 'Admin Widget 1',
|
||||
type: DataHubPageModuleType.Link,
|
||||
visibility: { scope: PageModuleScope.Global },
|
||||
params: {},
|
||||
},
|
||||
} as PageModuleFragment,
|
||||
{
|
||||
urn: 'urn:li:dataHubPageModule:admin2',
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: 'Admin Widget 2',
|
||||
type: DataHubPageModuleType.RichText,
|
||||
visibility: { scope: PageModuleScope.Global },
|
||||
params: {},
|
||||
},
|
||||
} as PageModuleFragment,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mockEmptyGlobalTemplate = {
|
||||
properties: {
|
||||
rows: [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
// Mock function for usePageTemplateContext
|
||||
const { mockUsePageTemplateContext } = vi.hoisted(() => {
|
||||
return {
|
||||
mockUsePageTemplateContext: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
|
||||
usePageTemplateContext: () => ({
|
||||
addModule: mockAddModule,
|
||||
moduleModalState: {
|
||||
open: mockOpenModal,
|
||||
close: vi.fn(),
|
||||
isOpen: false,
|
||||
isEditing: false,
|
||||
},
|
||||
}),
|
||||
usePageTemplateContext: mockUsePageTemplateContext,
|
||||
}));
|
||||
|
||||
// Mock components that are rendered inside the menu items
|
||||
@ -32,7 +85,7 @@ vi.mock('@app/homeV3/template/components/addModuleMenu/components/GroupItem', ()
|
||||
|
||||
vi.mock('@app/homeV3/template/components/addModuleMenu/components/ModuleMenuItem', () => ({
|
||||
__esModule: true,
|
||||
default: ({ module }: { module: ModuleInfo }) => <div data-testid={`module-${module.key}`}>{module.name}</div>,
|
||||
default: ({ module }: { module: any }) => <div data-testid={`module-${module.key}`}>{module.name}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@app/homeV3/template/components/addModuleMenu/components/MenuItem', () => ({
|
||||
@ -44,25 +97,20 @@ describe('useAddModuleMenu', () => {
|
||||
const mockCloseMenu = vi.fn();
|
||||
const mockPosition: ModulePositionInput = { rowIndex: 0, rowSide: 'left' };
|
||||
|
||||
const modulesAvailableToAdd: ModulesAvailableToAdd = {
|
||||
customModules: [], // Not used in new implementation
|
||||
customLargeModules: [], // Not used in new implementation
|
||||
adminCreatedModules: [
|
||||
{
|
||||
urn: 'urn:li:dataHubPageModule:admin1',
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: 'Admin Widget 1',
|
||||
type: DataHubPageModuleType.Link,
|
||||
visibility: { scope: PageModuleScope.Global },
|
||||
params: {},
|
||||
},
|
||||
} as PageModuleFragment,
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Set up default mock implementation
|
||||
mockUsePageTemplateContext.mockReturnValue({
|
||||
addModule: mockAddModule,
|
||||
moduleModalState: {
|
||||
open: mockOpenModal,
|
||||
close: vi.fn(),
|
||||
isOpen: false,
|
||||
isEditing: false,
|
||||
},
|
||||
template: mockTemplate,
|
||||
globalTemplate: mockEmptyGlobalTemplate,
|
||||
});
|
||||
});
|
||||
|
||||
function getChildren(item: any): any[] {
|
||||
@ -70,18 +118,8 @@ describe('useAddModuleMenu', () => {
|
||||
return [];
|
||||
}
|
||||
|
||||
it('should return menu items with hardcoded custom modules', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAddModuleMenu(
|
||||
{
|
||||
customModules: [],
|
||||
customLargeModules: [],
|
||||
adminCreatedModules: [],
|
||||
},
|
||||
mockPosition,
|
||||
mockCloseMenu,
|
||||
),
|
||||
);
|
||||
it('should return menu items with hardcoded custom modules when no global template custom modules exist', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
const { items } = result.current;
|
||||
expect(items).toHaveLength(2);
|
||||
@ -103,10 +141,25 @@ describe('useAddModuleMenu', () => {
|
||||
expect(items?.[1]?.children?.[0]).toHaveProperty('key', 'your-assets');
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
expect(items?.[1]?.children?.[1]).toHaveProperty('key', 'domains');
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
expect(items?.[1]?.children?.[2]).toHaveProperty('key', 'asset-collection');
|
||||
});
|
||||
|
||||
it('should include admin created modules when available', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
it('should include admin created modules when available in global template', () => {
|
||||
// Mock the context to return globalTemplate with custom modules
|
||||
mockUsePageTemplateContext.mockReturnValue({
|
||||
addModule: mockAddModule,
|
||||
moduleModalState: {
|
||||
open: mockOpenModal,
|
||||
close: vi.fn(),
|
||||
isOpen: false,
|
||||
isEditing: false,
|
||||
},
|
||||
template: mockTemplate,
|
||||
globalTemplate: mockGlobalTemplate,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
const { items } = result.current;
|
||||
expect(items).toHaveLength(3);
|
||||
@ -114,13 +167,15 @@ describe('useAddModuleMenu', () => {
|
||||
// Check Admin Created Widgets group
|
||||
expect(items?.[2]).toHaveProperty('key', 'adminCreatedModulesGroup');
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
expect(items?.[2]?.children).toHaveLength(1);
|
||||
expect(items?.[2]?.children).toHaveLength(2);
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
expect(items?.[2]?.children?.[0]).toHaveProperty('key', 'urn:li:dataHubPageModule:admin1');
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
expect(items?.[2]?.children?.[1]).toHaveProperty('key', 'urn:li:dataHubPageModule:admin2');
|
||||
});
|
||||
|
||||
it('should call addModule and closeMenu when Your Assets is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
const yourAssetsItem = result.current?.items?.[1]?.children?.[0];
|
||||
@ -140,7 +195,7 @@ describe('useAddModuleMenu', () => {
|
||||
});
|
||||
|
||||
it('should call addModule and closeMenu when Domains is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
const domainsItem = result.current?.items?.[1]?.children?.[1];
|
||||
@ -160,21 +215,47 @@ describe('useAddModuleMenu', () => {
|
||||
});
|
||||
|
||||
it('should call addModule and closeMenu when admin created module is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
// Mock the context to return globalTemplate with custom modules
|
||||
mockUsePageTemplateContext.mockReturnValue({
|
||||
addModule: mockAddModule,
|
||||
moduleModalState: {
|
||||
open: mockOpenModal,
|
||||
close: vi.fn(),
|
||||
isOpen: false,
|
||||
isEditing: false,
|
||||
},
|
||||
template: mockTemplate,
|
||||
globalTemplate: mockGlobalTemplate,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
// @ts-expect-error SubMenuItem should have children
|
||||
const adminModuleItem = result.current?.items?.[2]?.children?.[0];
|
||||
adminModuleItem.onClick?.({} as any); // simulate click
|
||||
|
||||
expect(mockAddModule).toHaveBeenCalledWith({
|
||||
module: modulesAvailableToAdd.adminCreatedModules[0],
|
||||
module: mockGlobalTemplate.properties.rows[0].modules[0],
|
||||
position: mockPosition,
|
||||
});
|
||||
expect(mockCloseMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set expandIcon and popupClassName for admin created modules group', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
// Mock the context to return globalTemplate with custom modules
|
||||
mockUsePageTemplateContext.mockReturnValue({
|
||||
addModule: mockAddModule,
|
||||
moduleModalState: {
|
||||
open: mockOpenModal,
|
||||
close: vi.fn(),
|
||||
isOpen: false,
|
||||
isEditing: false,
|
||||
},
|
||||
template: mockTemplate,
|
||||
globalTemplate: mockGlobalTemplate,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
|
||||
const adminGroup = result.current?.items?.[2];
|
||||
expect(adminGroup).toHaveProperty('expandIcon');
|
||||
@ -182,7 +263,7 @@ describe('useAddModuleMenu', () => {
|
||||
});
|
||||
|
||||
it('should open module modal when Asset Collection is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||
// Third child is Asset Collection
|
||||
const assetCollectionItem = customLargeChildren[2];
|
||||
@ -193,7 +274,7 @@ describe('useAddModuleMenu', () => {
|
||||
});
|
||||
|
||||
it('should not call addModule when Asset Collection is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||
const assetCollectionItem = customLargeChildren[2];
|
||||
assetCollectionItem.onClick?.({} as any);
|
||||
@ -209,11 +290,31 @@ describe('useAddModuleMenu', () => {
|
||||
});
|
||||
|
||||
it('should not open modal when Your Assets is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||
const yourAssetsItem = customLargeChildren[0];
|
||||
yourAssetsItem.onClick?.({} as any);
|
||||
|
||||
expect(mockOpenModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open module modal when Quick Link is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
const customChildren = getChildren(result.current.items?.[0]);
|
||||
const quickLinkItem = customChildren[0];
|
||||
quickLinkItem.onClick?.({} as any);
|
||||
|
||||
expect(mockOpenModal).toHaveBeenCalledWith(DataHubPageModuleType.Link, mockPosition);
|
||||
expect(mockCloseMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open module modal when Documentation is clicked', () => {
|
||||
const { result } = renderHook(() => useAddModuleMenu(mockPosition, mockCloseMenu));
|
||||
const customChildren = getChildren(result.current.items?.[0]);
|
||||
const documentationItem = customChildren[1];
|
||||
documentationItem.onClick?.({} as any);
|
||||
|
||||
expect(mockOpenModal).toHaveBeenCalledWith(DataHubPageModuleType.RichText, mockPosition);
|
||||
expect(mockCloseMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,208 @@
|
||||
import { getCustomGlobalModules } from '@app/homeV3/template/components/addModuleMenu/utils';
|
||||
|
||||
import { PageModuleFragment, PageTemplateFragment } from '@graphql/template.generated';
|
||||
import { DataHubPageModuleType, EntityType, PageModuleScope, PageTemplateScope, PageTemplateSurfaceType } from '@types';
|
||||
|
||||
// Helper function to create mock modules
|
||||
const createMockModule = (name: string, urn: string, type: DataHubPageModuleType): PageModuleFragment => ({
|
||||
urn,
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name,
|
||||
type,
|
||||
visibility: { scope: PageModuleScope.Personal },
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Helper function to create mock template
|
||||
const createMockTemplate = (modules: PageModuleFragment[][]): PageTemplateFragment => ({
|
||||
urn: 'urn:li:pageTemplate:test',
|
||||
type: EntityType.DatahubPageTemplate,
|
||||
properties: {
|
||||
rows: modules.map((rowModules) => ({
|
||||
modules: rowModules,
|
||||
})),
|
||||
surface: { surfaceType: PageTemplateSurfaceType.HomePage },
|
||||
visibility: { scope: PageTemplateScope.Global },
|
||||
},
|
||||
});
|
||||
|
||||
describe('getCustomGlobalModules', () => {
|
||||
describe('null and empty template handling', () => {
|
||||
it('should return empty array when template is null', () => {
|
||||
const result = getCustomGlobalModules(null);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when template has no rows', () => {
|
||||
const template = createMockTemplate([]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when template rows have no modules', () => {
|
||||
const template = createMockTemplate([[], []]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom module filtering', () => {
|
||||
it('should return only custom modules (Link, RichText, Hierarchy, AssetCollection)', () => {
|
||||
const customModules = [
|
||||
createMockModule('Link Module', 'urn:li:pageModule:link', DataHubPageModuleType.Link),
|
||||
createMockModule('RichText Module', 'urn:li:pageModule:richtext', DataHubPageModuleType.RichText),
|
||||
createMockModule('Hierarchy Module', 'urn:li:pageModule:hierarchy', DataHubPageModuleType.Hierarchy),
|
||||
createMockModule(
|
||||
'AssetCollection Module',
|
||||
'urn:li:pageModule:collection',
|
||||
DataHubPageModuleType.AssetCollection,
|
||||
),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([customModules]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toEqual(customModules);
|
||||
});
|
||||
|
||||
it('should exclude non-custom modules (OwnedAssets, Domains)', () => {
|
||||
const mixedModules = [
|
||||
createMockModule('Link Module', 'urn:li:pageModule:link', DataHubPageModuleType.Link),
|
||||
createMockModule('OwnedAssets Module', 'urn:li:pageModule:owned', DataHubPageModuleType.OwnedAssets),
|
||||
createMockModule('RichText Module', 'urn:li:pageModule:richtext', DataHubPageModuleType.RichText),
|
||||
createMockModule('Domains Module', 'urn:li:pageModule:domains', DataHubPageModuleType.Domains),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([mixedModules]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].properties.type).toBe(DataHubPageModuleType.Link);
|
||||
expect(result[1].properties.type).toBe(DataHubPageModuleType.RichText);
|
||||
});
|
||||
|
||||
it('should return empty array when template contains only non-custom modules', () => {
|
||||
const nonCustomModules = [
|
||||
createMockModule('OwnedAssets Module', 'urn:li:pageModule:owned', DataHubPageModuleType.OwnedAssets),
|
||||
createMockModule('Domains Module', 'urn:li:pageModule:domains', DataHubPageModuleType.Domains),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([nonCustomModules]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple rows handling', () => {
|
||||
it('should collect custom modules from multiple rows', () => {
|
||||
const row1Modules = [
|
||||
createMockModule('Link Module 1', 'urn:li:pageModule:link1', DataHubPageModuleType.Link),
|
||||
createMockModule('OwnedAssets Module', 'urn:li:pageModule:owned', DataHubPageModuleType.OwnedAssets),
|
||||
];
|
||||
|
||||
const row2Modules = [
|
||||
createMockModule('RichText Module', 'urn:li:pageModule:richtext', DataHubPageModuleType.RichText),
|
||||
createMockModule('Domains Module', 'urn:li:pageModule:domains', DataHubPageModuleType.Domains),
|
||||
];
|
||||
|
||||
const row3Modules = [
|
||||
createMockModule(
|
||||
'AssetCollection Module',
|
||||
'urn:li:pageModule:collection',
|
||||
DataHubPageModuleType.AssetCollection,
|
||||
),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([row1Modules, row2Modules, row3Modules]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].properties.name).toBe('Link Module 1');
|
||||
expect(result[1].properties.name).toBe('RichText Module');
|
||||
expect(result[2].properties.name).toBe('AssetCollection Module');
|
||||
});
|
||||
|
||||
it('should handle rows with mixed content correctly', () => {
|
||||
const mixedRows = [
|
||||
[], // Empty row
|
||||
[createMockModule('OwnedAssets Module', 'urn:li:pageModule:owned', DataHubPageModuleType.OwnedAssets)], // Non-custom only
|
||||
[createMockModule('Link Module', 'urn:li:pageModule:link', DataHubPageModuleType.Link)], // Custom only
|
||||
[
|
||||
createMockModule(
|
||||
'Hierarchy Module',
|
||||
'urn:li:pageModule:hierarchy',
|
||||
DataHubPageModuleType.Hierarchy,
|
||||
),
|
||||
createMockModule('Domains Module', 'urn:li:pageModule:domains', DataHubPageModuleType.Domains),
|
||||
], // Mixed
|
||||
];
|
||||
|
||||
const template = createMockTemplate(mixedRows);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].properties.type).toBe(DataHubPageModuleType.Link);
|
||||
expect(result[1].properties.type).toBe(DataHubPageModuleType.Hierarchy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should preserve module order when collecting from multiple rows', () => {
|
||||
const row1 = [
|
||||
createMockModule('Link Module A', 'urn:li:pageModule:linkA', DataHubPageModuleType.Link),
|
||||
createMockModule('RichText Module B', 'urn:li:pageModule:richtextB', DataHubPageModuleType.RichText),
|
||||
];
|
||||
|
||||
const row2 = [
|
||||
createMockModule('Hierarchy Module C', 'urn:li:pageModule:hierarchyC', DataHubPageModuleType.Hierarchy),
|
||||
createMockModule(
|
||||
'AssetCollection Module D',
|
||||
'urn:li:pageModule:collectionD',
|
||||
DataHubPageModuleType.AssetCollection,
|
||||
),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([row1, row2]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].properties.name).toBe('Link Module A');
|
||||
expect(result[1].properties.name).toBe('RichText Module B');
|
||||
expect(result[2].properties.name).toBe('Hierarchy Module C');
|
||||
expect(result[3].properties.name).toBe('AssetCollection Module D');
|
||||
});
|
||||
|
||||
it('should handle single row with single custom module', () => {
|
||||
const singleModule = [
|
||||
createMockModule('Single Link Module', 'urn:li:pageModule:single', DataHubPageModuleType.Link),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([singleModule]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].properties.name).toBe('Single Link Module');
|
||||
});
|
||||
|
||||
it('should return array with distinct modules (no duplicates expected from structure)', () => {
|
||||
const modules = [
|
||||
createMockModule('Link Module 1', 'urn:li:pageModule:link1', DataHubPageModuleType.Link),
|
||||
createMockModule('Link Module 2', 'urn:li:pageModule:link2', DataHubPageModuleType.Link),
|
||||
createMockModule('RichText Module 1', 'urn:li:pageModule:richtext1', DataHubPageModuleType.RichText),
|
||||
];
|
||||
|
||||
const template = createMockTemplate([modules]);
|
||||
const result = getCustomGlobalModules(template);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
// Verify each module is distinct
|
||||
const urns = result.map((module) => module.urn);
|
||||
expect(new Set(urns).size).toBe(urns.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -5,11 +5,11 @@ import { RESET_DROPDOWN_MENU_STYLES_CLASSNAME } from '@components/components/Dro
|
||||
|
||||
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||
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';
|
||||
import MenuItem from '@app/homeV3/template/components/addModuleMenu/components/MenuItem';
|
||||
import ModuleMenuItem from '@app/homeV3/template/components/addModuleMenu/components/ModuleMenuItem';
|
||||
import { getCustomGlobalModules } from '@app/homeV3/template/components/addModuleMenu/utils';
|
||||
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||
|
||||
import { PageModuleFragment } from '@graphql/template.generated';
|
||||
@ -37,15 +37,12 @@ const DOMAINS_MODULE: PageModuleFragment = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function useAddModuleMenu(
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd,
|
||||
position: ModulePositionInput,
|
||||
closeMenu: () => void,
|
||||
) {
|
||||
export default function useAddModuleMenu(position: ModulePositionInput, closeMenu: () => void) {
|
||||
const {
|
||||
addModule,
|
||||
moduleModalState: { open: openModal },
|
||||
template,
|
||||
globalTemplate,
|
||||
} = usePageTemplateContext();
|
||||
|
||||
const isLargeModuleRow =
|
||||
@ -162,9 +159,10 @@ export default function useAddModuleMenu(
|
||||
children: [yourAssets, domains, assetCollection, hierarchyView],
|
||||
});
|
||||
|
||||
// Add admin created modules if available
|
||||
if (modulesAvailableToAdd.adminCreatedModules.length) {
|
||||
const adminModuleItems = modulesAvailableToAdd.adminCreatedModules.map((module) => ({
|
||||
// Add global custom modules if available
|
||||
const customGlobalModules: PageModuleFragment[] = getCustomGlobalModules(globalTemplate);
|
||||
if (customGlobalModules.length > 0) {
|
||||
const adminModuleItems = customGlobalModules.map((module) => ({
|
||||
title: module.properties.name,
|
||||
key: module.urn,
|
||||
label: <ModuleMenuItem module={convertModuleToModuleInfo(module)} />,
|
||||
@ -189,13 +187,7 @@ export default function useAddModuleMenu(
|
||||
}
|
||||
|
||||
return { items };
|
||||
}, [
|
||||
isLargeModuleRow,
|
||||
isSmallModuleRow,
|
||||
modulesAvailableToAdd.adminCreatedModules,
|
||||
handleOpenCreateModuleModal,
|
||||
handleAddExistingModule,
|
||||
]);
|
||||
}, [isLargeModuleRow, isSmallModuleRow, globalTemplate, handleOpenCreateModuleModal, handleAddExistingModule]);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { CUSTOM_MODULE_TYPES } from '@app/homeV3/modules/constants';
|
||||
|
||||
import { PageModuleFragment, PageTemplateFragment } from '@graphql/template.generated';
|
||||
|
||||
export function getCustomGlobalModules(globalTemplate: PageTemplateFragment | null) {
|
||||
const customGlobalModules: PageModuleFragment[] = [];
|
||||
globalTemplate?.properties.rows.forEach((row) => {
|
||||
row.modules.forEach((module) => {
|
||||
if (CUSTOM_MODULE_TYPES.includes(module.properties.type)) {
|
||||
customGlobalModules.push(module);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return customGlobalModules;
|
||||
}
|
||||
@ -1,18 +1,16 @@
|
||||
import { useDndContext } from '@dnd-kit/core';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
import RowLayout from '@app/homeV3/templateRow/components/RowLayout';
|
||||
import { useTemplateRowLogic } from '@app/homeV3/templateRow/hooks/useTemplateRowLogic';
|
||||
import { WrappedRow } from '@app/homeV3/templateRow/types';
|
||||
|
||||
interface Props {
|
||||
row: WrappedRow;
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
function TemplateRow({ row, modulesAvailableToAdd, rowIndex }: Props) {
|
||||
function TemplateRow({ row, rowIndex }: Props) {
|
||||
const { modulePositions, isSmallRow } = useTemplateRowLogic(row, rowIndex);
|
||||
const { active } = useDndContext();
|
||||
const isActiveModuleSmall = active?.data?.current?.isSmall;
|
||||
@ -29,7 +27,6 @@ function TemplateRow({ row, modulesAvailableToAdd, rowIndex }: Props) {
|
||||
rowIndex={rowIndex}
|
||||
modulePositions={modulePositions}
|
||||
shouldDisableDropZones={isDropZoneDisabled}
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
isSmallRow={isSmallRow}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -3,7 +3,6 @@ import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Module from '@app/homeV3/module/Module';
|
||||
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
||||
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||
import ModuleDropZone from '@app/homeV3/templateRow/components/ModuleDropZone';
|
||||
@ -25,7 +24,6 @@ interface Props {
|
||||
rowIndex: number;
|
||||
modulePositions: ModulePosition[];
|
||||
shouldDisableDropZones: boolean;
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
isSmallRow: boolean | null;
|
||||
}
|
||||
|
||||
@ -39,15 +37,10 @@ const ModuleWrapper = memo(({ module, position }: ModuleWrapperProps) => (
|
||||
<Module module={module} position={position} />
|
||||
));
|
||||
|
||||
function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesAvailableToAdd, isSmallRow }: Props) {
|
||||
function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, isSmallRow }: Props) {
|
||||
return (
|
||||
<RowWrapper>
|
||||
<AddModuleButton
|
||||
orientation="vertical"
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
rowIndex={rowIndex}
|
||||
rowSide="left"
|
||||
/>
|
||||
<AddModuleButton orientation="vertical" rowIndex={rowIndex} rowSide="left" />
|
||||
|
||||
{/* Drop zone at the beginning of the row */}
|
||||
<ModuleDropZone
|
||||
@ -70,12 +63,7 @@ function RowLayout({ rowIndex, modulePositions, shouldDisableDropZones, modulesA
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<AddModuleButton
|
||||
orientation="vertical"
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
rowIndex={rowIndex}
|
||||
rowSide="right"
|
||||
/>
|
||||
<AddModuleButton orientation="vertical" rowIndex={rowIndex} rowSide="right" />
|
||||
</RowWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user