Skip to content

Commit

Permalink
Merge pull request #5784 from gooddata/pdo-description-auto-expand
Browse files Browse the repository at this point in the history
fix: update parent containers height when widget height is updated
  • Loading branch information
xMort authored Jan 13, 2025
2 parents 09a8a93 + 8e095f6 commit 73450f1
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 21 deletions.
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

0 comments on commit 73450f1

Please sign in to comment.