feat(customHomePage): adjust rows component and add wrapping (#14025)

This commit is contained in:
v-tarasevich-blitz-brain 2025-07-13 22:25:29 +03:00 committed by GitHub
parent 7c41e6d4a7
commit 58da8a8857
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 179 additions and 11 deletions

View File

@ -6,6 +6,7 @@ const ModuleContainer = styled.div<{ $height: string }>`
border: ${borders['1px']} ${colors.gray[100]};
border-radius: ${radius.lg};
flex: 1;
overflow-x: hidden;
height: ${(props) => props.$height};
box-shadow:

View File

@ -1,11 +1,12 @@
import { spacing } from '@components';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import useModulesAvailableToAdd from '@app/homeV3/modules/hooks/useModulesAvailableToAdd';
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
import { AddModuleHandlerInput } from '@app/homeV3/template/types';
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
import { wrapRows } from '@app/homeV3/templateRow/utils';
import { DataHubPageTemplate } from '@types';
@ -26,7 +27,10 @@ interface Props {
}
export default function Template({ template, className }: Props) {
const hasRows = !!template?.properties?.rows?.length;
const rows = useMemo(() => template?.properties?.rows ?? [], [template?.properties?.rows]);
const hasRows = useMemo(() => !!rows.length, [rows.length]);
const wrappedRows = useMemo(() => wrapRows(rows), [rows]);
const onAddModule = useCallback((input: AddModuleHandlerInput) => {
// TODO: implement the real handler
console.log('onAddModule handled with input', input);
@ -36,13 +40,14 @@ export default function Template({ template, className }: Props) {
return (
<Wrapper className={className}>
{template?.properties?.rows.map((row, i) => {
{wrappedRows.map((row, i) => {
const key = `templateRow-${i}`;
return (
<TemplateRow
key={key}
row={row}
rowIndex={i}
originRowIndex={row.originRowIndex}
modulesAvailableToAdd={modulesAvailableToAdd}
onAddModule={onAddModule}
/>

View File

@ -47,6 +47,7 @@ interface Props {
modulesAvailableToAdd: ModulesAvailableToAdd;
onAddModule?: (input: AddModuleHandlerInput) => void;
className?: string;
originRowIndex?: number;
rowIndex?: number;
rowSide?: RowSide;
}
@ -56,6 +57,7 @@ export default function AddModuleButton({
modulesAvailableToAdd,
onAddModule,
className,
originRowIndex,
rowIndex,
rowSide,
}: Props) {
@ -68,11 +70,12 @@ export default function AddModuleButton({
setIsOpened(false);
onAddModule?.({
module,
originRowIndex,
rowIndex,
rowSide,
});
},
[onAddModule, rowIndex, rowSide],
[onAddModule, originRowIndex, rowIndex, rowSide],
);
const menu = useAddModuleMenu(modulesAvailableToAdd, onAddModuleHandler);

View File

@ -4,7 +4,9 @@ export type RowSide = 'left' | 'right';
export interface AddModuleHandlerInput {
module: ModuleInfo;
// When these fields are empty it means adding a module to the new row
rowIndex?: number;
originRowIndex?: number; // Row index before wrapping
rowIndex?: number; // Row index after wrapping
rowSide?: RowSide;
}

View File

@ -20,15 +20,17 @@ interface Props {
onAddModule?: (input: AddModuleHandlerInput) => void;
modulesAvailableToAdd: ModulesAvailableToAdd;
rowIndex: number;
originRowIndex: number;
}
export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, rowIndex }: Props) {
export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, rowIndex, originRowIndex }: Props) {
return (
<RowWrapper>
<AddModuleButton
orientation="vertical"
modulesAvailableToAdd={modulesAvailableToAdd}
onAddModule={onAddModule}
originRowIndex={originRowIndex}
rowIndex={rowIndex}
rowSide="left"
/>
@ -41,6 +43,7 @@ export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, r
orientation="vertical"
modulesAvailableToAdd={modulesAvailableToAdd}
onAddModule={onAddModule}
originRowIndex={originRowIndex}
rowIndex={rowIndex}
rowSide="right"
/>

View File

@ -0,0 +1,108 @@
import { describe, expect, it } from 'vitest';
import { WrappedRow } from '@app/homeV3/templateRow/types';
import { wrapRows } from '@app/homeV3/templateRow/utils';
import { DataHubPageModuleType, DataHubPageTemplateRow, EntityType, PageModuleScope } from '@types';
const MOCKED_TIMESTAMP = 1752056099724;
describe('wrapRows', () => {
const makeRow = (moduleCount: number): DataHubPageTemplateRow => ({
modules: Array.from({ length: moduleCount }).map((_, i) => ({
urn: `urn:li:module:${i}`,
type: EntityType.DatahubPageModule,
properties: {
name: `Module ${i}`,
type: DataHubPageModuleType.OwnedAssets,
visibility: {
scope: PageModuleScope.Global,
},
created: {
time: MOCKED_TIMESTAMP,
},
lastModified: {
time: MOCKED_TIMESTAMP,
},
params: {},
},
})),
});
it('should split modules into chunks of default size (3)', () => {
const rows: DataHubPageTemplateRow[] = [
makeRow(7), // 7 modules → [3, 3, 1]
];
const result = wrapRows(rows);
expect(result).toHaveLength(3);
expect(result[0].modules).toHaveLength(3);
expect(result[1].modules).toHaveLength(3);
expect(result[2].modules).toHaveLength(1);
expect(result[0].originRowIndex).toBe(0);
expect(result[0].rowIndex).toBe(0);
expect(result[1].rowIndex).toBe(1);
expect(result[2].rowIndex).toBe(2);
});
it('should respect custom chunk size', () => {
const rows: DataHubPageTemplateRow[] = [makeRow(5)];
const result = wrapRows(rows, 2); // chunk size = 2 → [2, 2, 1]
expect(result).toHaveLength(3);
expect(result[0].modules).toHaveLength(2);
expect(result[1].modules).toHaveLength(2);
expect(result[2].modules).toHaveLength(1);
});
it('should handle multiple rows correctly', () => {
const rows: DataHubPageTemplateRow[] = [
makeRow(4), // → [3, 1]
makeRow(5), // → [3, 2]
];
const result = wrapRows(rows);
expect(result).toHaveLength(4);
// First row
expect(result[0].originRowIndex).toBe(0);
expect(result[0].rowIndex).toBe(0);
expect(result[1].originRowIndex).toBe(0);
expect(result[1].rowIndex).toBe(1);
// Second row
expect(result[2].originRowIndex).toBe(1);
expect(result[2].rowIndex).toBe(2);
expect(result[3].originRowIndex).toBe(1);
expect(result[3].rowIndex).toBe(3);
});
it('should return empty array if no rows provided', () => {
const result = wrapRows([]);
expect(result).toEqual<WrappedRow[]>([]);
});
it('should return empty array if all rows have no modules', () => {
const rows: DataHubPageTemplateRow[] = [{ modules: [] }, { modules: [] }];
const result = wrapRows(rows);
expect(result).toEqual<WrappedRow[]>([]);
});
it('should handle exact multiples of chunk size', () => {
const rows: DataHubPageTemplateRow[] = [
makeRow(6), // 6 modules → [3, 3]
];
const result = wrapRows(rows);
expect(result).toHaveLength(2);
expect(result[0].modules).toHaveLength(3);
expect(result[1].modules).toHaveLength(3);
});
});

View File

@ -0,0 +1,6 @@
import { DataHubPageTemplateRow } from '@types';
export interface WrappedRow extends DataHubPageTemplateRow {
originRowIndex: number;
rowIndex: number;
}

View File

@ -0,0 +1,25 @@
import { WrappedRow } from '@app/homeV3/templateRow/types';
import { DataHubPageTemplateRow } from '@types';
const DEFAULT_ROW_MAX_SIZE = 3;
export function wrapRows(rows: DataHubPageTemplateRow[], chunkSize = DEFAULT_ROW_MAX_SIZE): WrappedRow[] {
const result: WrappedRow[] = [];
let globalRowIndex = 0;
rows.forEach((row, originRowIndex) => {
const { modules } = row;
for (let i = 0; i < modules.length; i += chunkSize) {
const chunk = modules.slice(i, i + chunkSize);
result.push({
originRowIndex,
rowIndex: globalRowIndex++,
modules: chunk,
});
}
});
return result;
}

View File

@ -1,4 +1,4 @@
import { Tooltip } from '@components';
import { Tooltip, zIndices } from '@components';
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
@ -98,7 +98,7 @@ const EntityHeader: React.FC<EntityHeaderProps> = ({
<EntityTitleContainer>
<StyledLink to={`${url}/`} {...linkProps}>
{previewType === PreviewType.HOVER_CARD ? (
<Tooltip title={name}>
<Tooltip title={name} zIndex={zIndices.tooltip}>
<CardEntityTitle onClick={onClick} $titleSizePx={titleSizePx} data-testid="entity-title">
{name || urn}
</CardEntityTitle>

View File

@ -36,7 +36,7 @@ const DisplayNameWithHover = styled(DisplayName)<{ $decorationColor?: string }>`
`;
const DisplayNameWrapper = styled.div`
width: fit-content;
white-space: nowrap;
`;
const ContentContainer = styled.div`
@ -131,6 +131,7 @@ export default function AutoCompleteEntityItem({
color={variantProps?.nameColor}
colorLevel={variantProps?.nameColorLevel}
weight={variantProps?.nameWeight}
showNameTooltipIfTruncated
/>
)}
</DisplayNameWrapper>

View File

@ -25,15 +25,29 @@ interface Props {
weight?: FontWeightOptions;
fontSize?: FontSizeOptions;
className?: string;
showNameTooltipIfTruncated?: boolean;
}
export default function DisplayName({ displayName, highlight, color, colorLevel, weight, fontSize, className }: Props) {
export default function DisplayName({
displayName,
highlight,
color,
colorLevel,
weight,
fontSize,
className,
showNameTooltipIfTruncated,
}: Props) {
const { measuredRef, isHorizontallyTruncated } = useMeasureIfTrancated();
return (
<Popover
zIndex={zIndices.popover}
content={isHorizontallyTruncated ? <PopoverWrapper>{displayName}</PopoverWrapper> : undefined}
content={
showNameTooltipIfTruncated && isHorizontallyTruncated ? (
<PopoverWrapper>{displayName}</PopoverWrapper>
) : undefined
}
>
<EntityTitleContainer ref={measuredRef} className={className}>
<MatchText