Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update parent containers height when widget height is updated #5784

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/sdk-ui-dashboard/.dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ options = {
"src/widgets",
"src/types.ts",
"src/presentation/dragAndDrop/types.ts",
"src/presentation/flexibleLayout/DefaultDashboardLayoutRenderer/utils/sizing.ts"
]),
depCruiser.moduleWithDependencies("presentation", "src/presentation", [
"src/_staging/*",
Expand Down
9 changes: 9 additions & 0 deletions libs/sdk-ui-dashboard/src/_staging/layout/coordinates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,12 @@ export const isFirstInContainer = (parentLayoutPath: ILayoutItemPath | undefined
}
return false;
};

export const getParentPath = (
path: ILayoutItemPath | ILayoutSectionPath | undefined,
): ILayoutItemPath | undefined => {
if (path === undefined) {
return undefined;
}
return isLayoutItemPath(path) ? (path.length > 1 ? path.slice(0, -1) : undefined) : path.parent;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// (C) 2024 GoodData Corporation
// (C) 2024-2025 GoodData Corporation

import { describe, it, expect } from "vitest";

Expand All @@ -20,6 +20,7 @@ import {
findItem,
findSection,
findSections,
getParentPath,
} from "../coordinates";
import { NESTED_LAYOUT } from "./coordinates.mock";

Expand Down Expand Up @@ -943,4 +944,65 @@ describe("coordinates", () => {
});
});
});

describe("getParentPath", () => {
describe("item path", () => {
it("should return undefined when section or item path is undefined", () => {
expect(getParentPath(undefined)).toBeUndefined();
});

it("should return undefined when item has no parent", () => {
expect(getParentPath([{ itemIndex: 0, sectionIndex: 1 }])).toBeUndefined();
});

it("should return path to the parent when item has just one parent", () => {
expect(
getParentPath([
{ itemIndex: 0, sectionIndex: 1 },
{ itemIndex: 2, sectionIndex: 8 },
]),
).toStrictEqual([{ itemIndex: 0, sectionIndex: 1 }]);
});

it("should return path to the immediate parent when item is nested in multiple parents", () => {
expect(
getParentPath([
{ itemIndex: 4, sectionIndex: 5 },
{ itemIndex: 0, sectionIndex: 1 },
{ itemIndex: 2, sectionIndex: 8 },
]),
).toStrictEqual([
{ itemIndex: 4, sectionIndex: 5 },
{ itemIndex: 0, sectionIndex: 1 },
]);
});
});

describe("section path", () => {
it("should return undefined when section has no parent", () => {
expect(getParentPath({ parent: undefined, sectionIndex: 2 })).toBeUndefined();
});

it("should return path to the parent when section has just one parent", () => {
expect(
getParentPath({ parent: [{ itemIndex: 0, sectionIndex: 1 }], sectionIndex: 2 }),
).toStrictEqual([{ itemIndex: 0, sectionIndex: 1 }]);
});

it("should return path to the immediate parent when section is nested in multiple parents", () => {
expect(
getParentPath({
parent: [
{ itemIndex: 4, sectionIndex: 5 },
{ itemIndex: 0, sectionIndex: 1 },
],
sectionIndex: 2,
}),
).toStrictEqual([
{ itemIndex: 4, sectionIndex: 5 },
{ itemIndex: 0, sectionIndex: 1 },
]);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// (C) 2021-2024 GoodData Corporation
// (C) 2021-2025 GoodData Corporation

import { SagaIterator } from "redux-saga";
import { DashboardContext } from "../../types/commonTypes.js";
Expand All @@ -22,9 +22,15 @@ import {
} from "./validation/itemValidation.js";
import { addTemporaryIdentityToWidgets } from "../../utils/dashboardItemUtils.js";
import { sanitizeHeader } from "./utils.js";
import { updateSectionIndex, findSections, asLayoutItemPath } from "../../../_staging/layout/coordinates.js";
import {
updateSectionIndex,
findSections,
asLayoutItemPath,
getParentPath,
} from "../../../_staging/layout/coordinates.js";
import { selectSettings } from "../../store/config/configSelectors.js";
import { normalizeItemSizeToParent } from "../../../_staging/layout/sizing.js";
import { resizeParentContainers } from "./containerHeightSanitization.js";

type AddLayoutSectionContext = {
readonly ctx: DashboardContext;
Expand Down Expand Up @@ -143,6 +149,10 @@ export function* addLayoutSectionHandler(
]),
);

if (!isLegacyCommand) {
yield call(resizeParentContainers, getParentPath(index));
}

const relevantSections = isLegacyCommand
? commandCtx.layout.sections
: findSections(commandCtx.layout, index);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// (C) 2021-2024 GoodData Corporation
// (C) 2021-2025 GoodData Corporation
import { SagaIterator } from "redux-saga";
import { call, put, SagaReturnType, select } from "redux-saga/effects";
import isEmpty from "lodash/isEmpty.js";
Expand All @@ -21,6 +21,7 @@ import {
updateItemIndex,
findSection,
getSectionIndex,
getParentPath,
} from "../../../_staging/layout/coordinates.js";

import { validateItemPlacement, validateSectionExists } from "./validation/layoutValidation.js";
Expand All @@ -31,6 +32,7 @@ import {
} from "./validation/itemValidation.js";
import { selectSettings } from "../../store/config/configSelectors.js";
import { normalizeItemSizeToParent } from "../../../_staging/layout/sizing.js";
import { resizeParentContainers } from "./containerHeightSanitization.js";

type AddSectionItemsContext = {
readonly ctx: DashboardContext;
Expand Down Expand Up @@ -194,6 +196,8 @@ export function* addSectionItemsHandler(
]),
);

yield call(resizeParentContainers, getParentPath(layoutPath));

const originalItemIndex = itemPath === undefined ? itemIndex : getItemIndex(itemPath);
const newItemIndex = resolveIndexOfNewItem(section.items, originalItemIndex);
const updatedLayoutPath =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// (C) 2025 GoodData Corporation

import { invariant } from "ts-invariant";
import { SagaReturnType, select, put } from "redux-saga/effects";
import {
isDashboardLayout,
IDashboardLayout,
IDashboardLayoutItem,
ScreenSize,
IDashboardLayoutSection,
} from "@gooddata/sdk-model";

import { ILayoutItemPath } from "../../../types.js";
import { selectLayout, selectScreen } from "../../store/layout/layoutSelectors.js";
import { IItemWithHeight, ExtendedDashboardWidget } from "../../types/layoutTypes.js";
import { findItem, areLayoutPathsEqual } from "../../../_staging/layout/coordinates.js";
import { implicitLayoutItemSizeFromXlSize } from "../../../_staging/layout/sizing.js";
import { splitDashboardLayoutItemsAsRenderedGridRows } from "../../../presentation/flexibleLayout/DefaultDashboardLayoutRenderer/utils/sizing.js";
import { layoutActions } from "../../store/layout/index.js";

const getParentPathsFromDeepestToShallowest = (parentPath: ILayoutItemPath) =>
parentPath.map((_, index) => {
return parentPath.slice(0, parentPath.length - index);
});

const getUpdatedSizesOnly = (
layout: IDashboardLayout<ExtendedDashboardWidget>,
itemsWithSizes: IItemWithHeight[],
) => {
return itemsWithSizes.filter(({ itemPath, height }) => {
const container = findItem(layout, itemPath);
return container.size.xl.gridHeight !== height;
});
};

class ContainerHeightCalculator {
constructor(
private readonly screen: ScreenSize,
private readonly getPreviouslyComputedHeight: (
itemPath: ILayoutItemPath,
) => IItemWithHeight | undefined,
) {}

public computeContainerHeight(
layout: IDashboardLayout<ExtendedDashboardWidget>,
itemPath: ILayoutItemPath,
) {
const container = findItem(layout, itemPath);
invariant(isDashboardLayout(container.widget));

return container.widget.sections.reduce((totalHeight, section, sectionIndex) => {
const sectionHeight = this.getSectionHeight(container, section, sectionIndex, itemPath);
return totalHeight + sectionHeight;
}, 0);
}

private getSectionHeight = (
container: IDashboardLayoutItem<ExtendedDashboardWidget>,
section: IDashboardLayoutSection<ExtendedDashboardWidget>,
sectionIndex: number,
itemPath: ILayoutItemPath,
): number => {
const allScreenSizes = implicitLayoutItemSizeFromXlSize(container.size.xl);
const rows = splitDashboardLayoutItemsAsRenderedGridRows(section.items, allScreenSizes, this.screen);

return rows.reduce((sectionHeight, row) => {
const rowHeight = this.getRowHeight(section, sectionIndex, row, itemPath);
return sectionHeight + rowHeight;
}, 0);
};

private getRowHeight = (
section: IDashboardLayoutSection<ExtendedDashboardWidget>,
sectionIndex: number,
row: IDashboardLayoutItem<ExtendedDashboardWidget>[],
itemPath: ILayoutItemPath,
) => {
return row.reduce((maxItemHeight, item) => {
const itemIndex = section.items.findIndex((currentItem) => currentItem === item);
const currentItemPath: ILayoutItemPath = [...itemPath, { sectionIndex, itemIndex }];
const previouslyComputedHeight = this.getPreviouslyComputedHeight(currentItemPath);
const currentItemHeight = previouslyComputedHeight?.height ?? item.size.xl.gridHeight ?? 0;
return Math.max(maxItemHeight, currentItemHeight);
}, 0);
};
}

// the handler does not support heightRatio, only gridHeight
export function* resizeParentContainers(parentPath: ILayoutItemPath | undefined) {
if (parentPath === undefined) {
return;
}
const layout: SagaReturnType<typeof selectLayout> = yield select(selectLayout);
const screen: SagaReturnType<typeof selectScreen> = yield select(selectScreen);
invariant(screen);

const containers = getParentPathsFromDeepestToShallowest(parentPath);

// go through each container from the deepest one to the shallowest one, get new height for each of them
const itemsWithSizes = containers.reduce<IItemWithHeight[]>((aggregator, itemPath) => {
const getPreviouslyComputedHeight = (currentItemPath: ILayoutItemPath) =>
aggregator.find(({ itemPath }) => areLayoutPathsEqual(itemPath, currentItemPath));

const calculator = new ContainerHeightCalculator(screen, getPreviouslyComputedHeight);
const height = calculator.computeContainerHeight(layout, itemPath);
return [
...aggregator,
{
itemPath,
height,
},
];
}, []);

const updatedItemsWithSizes = getUpdatedSizesOnly(layout, itemsWithSizes);

if (updatedItemsWithSizes.length > 0) {
yield put(
layoutActions.updateHeightOfMultipleItems({
itemsWithSizes: updatedItemsWithSizes,
}),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// (C) 2021-2024 GoodData Corporation
// (C) 2021-2025 GoodData Corporation
import { SagaIterator } from "redux-saga";
import { put, select } from "redux-saga/effects";
import { put, select, call } from "redux-saga/effects";

import { DashboardContext } from "../../types/commonTypes.js";
import { MoveLayoutSection } from "../../commands/index.js";
Expand All @@ -16,9 +16,11 @@ import {
areLayoutPathsEqual,
updateSectionIndex,
findSection,
getParentPath,
} from "../../../_staging/layout/coordinates.js";

import { validateSectionExists, validateSectionPlacement } from "./validation/layoutValidation.js";
import { resizeParentContainers } from "./containerHeightSanitization.js";

type MoveLayoutSectionContext = {
readonly ctx: DashboardContext;
Expand Down Expand Up @@ -147,6 +149,9 @@ export function* moveLayoutSectionHandler(
}),
);

yield call(resizeParentContainers, getParentPath(sourceSection));
yield call(resizeParentContainers, getParentPath(toSectionPath));

return layoutSectionMoved(
ctx,
movedSection,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// (C) 2021-2024 GoodData Corporation
// (C) 2021-2025 GoodData Corporation
import { SagaIterator } from "redux-saga";
import { batchActions } from "redux-batched-actions";
import { SagaReturnType, put, select } from "redux-saga/effects";
import { SagaReturnType, put, select, call } from "redux-saga/effects";

import { DashboardContext } from "../../types/commonTypes.js";
import { MoveSectionItem } from "../../commands/index.js";
Expand All @@ -20,6 +20,7 @@ import {
getSectionIndex,
areItemsInSameSection,
asSectionPath,
getParentPath,
} from "../../../_staging/layout/coordinates.js";

import {
Expand All @@ -31,6 +32,7 @@ import {
import { selectSettings } from "../../store/config/configSelectors.js";
import { selectInsightsMap } from "../../store/insights/insightsSelectors.js";
import { normalizeItemSizeToParent } from "../../../_staging/layout/sizing.js";
import { resizeParentContainers } from "./containerHeightSanitization.js";

type MoveSectionItemContext = {
readonly ctx: DashboardContext;
Expand Down Expand Up @@ -263,6 +265,9 @@ export function* moveSectionItemHandler(
]),
);

yield call(resizeParentContainers, getParentPath(fromPath));
yield call(resizeParentContainers, getParentPath(targetIndex));

const targetSectionIndexUpdated =
targetSectionIndex === undefined
? getSectionIndex(targetIndex)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// (C) 2021-2024 GoodData Corporation
// (C) 2021-2025 GoodData Corporation
import { batchActions } from "redux-batched-actions";
import { SagaIterator } from "redux-saga";
import { SagaReturnType, put, select } from "redux-saga/effects";
import { SagaReturnType, put, select, call } from "redux-saga/effects";
import { MoveSectionItemToNewSection } from "../../commands/layout.js";
import { invalidArgumentsProvided } from "../../events/general.js";
import {
Expand All @@ -26,11 +26,13 @@ import {
asLayoutItemPath,
getSectionIndex,
getItemIndex,
getParentPath,
} from "../../../_staging/layout/coordinates.js";
import { ILayoutItemPath } from "../../../types.js";
import { selectSettings } from "../../store/config/configSelectors.js";
import { selectInsightsMap } from "../../store/insights/insightsSelectors.js";
import { normalizeItemSizeToParent } from "../../../_staging/layout/sizing.js";
import { resizeParentContainers } from "./containerHeightSanitization.js";

type MoveSectionItemToNewSectionContext = {
readonly ctx: DashboardContext;
Expand Down Expand Up @@ -265,6 +267,9 @@ export function* moveSectionItemToNewSectionHandler(
]),
);

yield call(resizeParentContainers, getParentPath(itemPath));
yield call(resizeParentContainers, getParentPath(toItemIndex));

return layoutSectionItemMovedToNewSection(
ctx,
itemWithNormalizedSize,
Expand Down
Loading
Loading