MINOR: fix the widget placement when adding new one (#17329)

* fix the widget placement when adding new one

* fix the widget not adding in the right side add placeholders

* added unit test for the same

* fix sonar
This commit is contained in:
Ashish Gupta 2024-08-10 21:39:27 +05:30 committed by GitHub
parent a098c20c7c
commit 785e450e28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 496 additions and 11 deletions

View File

@ -0,0 +1,184 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { PersonaClass } from '../../support/persona/PersonaClass';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import { redirectToHomePage, toastNotification } from '../../utils/common';
import {
checkAllDefaultWidgets,
navigateToCustomizeLandingPage,
removeAndCheckWidget,
setUserDefaultPersona,
} from '../../utils/customizeLandingPage';
const adminUser = new UserClass();
const persona = new PersonaClass();
const persona2 = new PersonaClass();
const test = base.extend<{ adminPage: Page; userPage: Page }>({
adminPage: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
},
});
base.beforeAll('Setup pre-requests', async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await persona.create(apiContext, [adminUser.responseData.id]);
await persona2.create(apiContext);
await afterAction();
});
base.afterAll('Cleanup', async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
await adminUser.delete(apiContext);
await persona.delete(apiContext);
await persona2.delete(apiContext);
await afterAction();
});
test.describe('Customize Landing Page Flow', () => {
test('Check all default widget present', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await adminPage.getByTestId('welcome-screen-close-btn').click();
await checkAllDefaultWidgets(adminPage);
});
test('Remove and check widget', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await setUserDefaultPersona(adminPage, persona.responseData.displayName);
await navigateToCustomizeLandingPage(adminPage, {
personaName: persona.responseData.name,
customPageDataResponse: 404,
});
await removeAndCheckWidget(adminPage, {
widgetTestId: 'activity-feed-widget',
widgetKey: 'KnowledgePanel.ActivityFeed',
});
await removeAndCheckWidget(adminPage, {
widgetTestId: 'following-widget',
widgetKey: 'KnowledgePanel.Following',
});
await removeAndCheckWidget(adminPage, {
widgetTestId: 'kpi-widget',
widgetKey: 'KnowledgePanel.KPI',
});
const saveResponse = adminPage.waitForResponse('/api/v1/docStore');
await adminPage.click('[data-testid="save-button"]');
await saveResponse;
await toastNotification(adminPage, 'Page layout created successfully.');
await redirectToHomePage(adminPage);
// Check if removed widgets are not present on landing adminPage
await expect(
adminPage.locator('[data-testid="activity-feed-widget"]')
).not.toBeVisible();
await expect(
adminPage.locator('[data-testid="following-widget"]')
).not.toBeVisible();
await expect(
adminPage.locator('[data-testid="kpi-widget"]')
).not.toBeVisible();
});
test('Remove and add the widget in the same placeholder', async ({
adminPage,
}) => {
await redirectToHomePage(adminPage);
const feedResponse = adminPage.waitForResponse(
'/api/v1/feed?type=Conversation&*'
);
await navigateToCustomizeLandingPage(adminPage, {
personaName: persona2.responseData.name,
customPageDataResponse: 404,
});
await feedResponse;
await adminPage.waitForSelector('[data-testid="activity-feed-widget"]');
const followingElementStyle = await adminPage
.locator('[id="KnowledgePanel.Following"]')
.evaluate((node) => {
const computedStyle = window.getComputedStyle(node);
return {
transform: computedStyle.transform,
};
});
// Remove and check the placement of Following widget.
await adminPage.click(
'[data-testid="following-widget"] [data-testid="remove-widget-button"]'
);
await adminPage.waitForSelector('[data-testid="following-widget"]', {
state: 'detached',
});
await adminPage.waitForSelector(
'[data-testid*="KnowledgePanel.Following"][data-testid$="EmptyWidgetPlaceholder"]'
);
// Add KPI widget in the same placeholder
const getWidgetList = adminPage.waitForResponse(
'api/v1/docStore?fqnPrefix=KnowledgePanel&*'
);
await adminPage.click(
'[data-testid="KnowledgePanel.Following.EmptyWidgetPlaceholder"] [data-testid="add-widget-button"]'
);
await getWidgetList;
await adminPage.waitForSelector('[role="dialog"].ant-modal');
expect(adminPage.locator('[role="dialog"].ant-modal')).toBeVisible();
await adminPage.click('[data-testid="KPI-widget-tab-label"]');
await adminPage
.locator('.ant-tabs-tabpane-active [data-testid="add-widget-button"]')
.click();
await adminPage.waitForSelector('[role="dialog"].ant-modal', {
state: 'detached',
});
const kpiElement = adminPage.locator('[id^="KnowledgePanel.KPI-"]');
const kpiElementStyle = await kpiElement.evaluate((node) => {
const computedStyle = window.getComputedStyle(node);
return {
transform: computedStyle.transform,
};
});
// Check if the KPI widget is added in the same placeholder,by their transform property or placement.
expect(kpiElementStyle.transform).toEqual(followingElementStyle.transform);
const saveResponse = adminPage.waitForResponse('/api/v1/docStore');
await adminPage.click('[data-testid="save-button"]');
await saveResponse;
await toastNotification(adminPage, 'Page layout created successfully.');
});
});

View File

@ -0,0 +1,76 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { APIRequestContext } from '@playwright/test';
import { uuid } from '../../utils/common';
type ResponseDataType = {
name: string;
displayName: string;
description: string;
id?: string;
fullyQualifiedName?: string;
users?: string[];
};
export class PersonaClass {
id = uuid();
data: ResponseDataType;
responseData: ResponseDataType;
constructor(data?: ResponseDataType) {
this.data = data ?? {
name: `PW%Persona-${this.id}`,
displayName: `PW Persona ${this.id}`,
description: 'playwright for persona description',
users: [],
};
}
get() {
return this.responseData;
}
async create(apiContext: APIRequestContext, users?: string[]) {
const response = await apiContext.post('/api/v1/personas', {
data: { ...this.data, users },
});
const data = await response.json();
this.responseData = data;
return data;
}
async delete(apiContext: APIRequestContext) {
const response = await apiContext.delete(
`/api/v1/personas/${this.responseData.id}?hardDelete=true&recursive=false`
);
return await response.json();
}
async patch(apiContext: APIRequestContext, data: Record<string, unknown>[]) {
const response = await apiContext.patch(
`/api/v1/personas/${this.responseData.id}`,
{
data,
headers: {
'Content-Type': 'application/json-patch+json',
},
}
);
this.responseData = await response.json();
return await response.json();
}
}

View File

@ -61,6 +61,19 @@ export class UserClass {
};
}
async setAdminRole(apiContext: APIRequestContext) {
return this.patch({
apiContext,
patchData: [
{
op: 'replace',
path: '/isAdmin',
value: true,
},
],
});
}
async delete(apiContext: APIRequestContext) {
const response = await apiContext.delete(
`/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=true`

View File

@ -0,0 +1,118 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { GlobalSettingOptions } from '../constant/settings';
import { visitUserProfilePage } from './common';
import { settingClick } from './sidebar';
export const navigateToCustomizeLandingPage = async (
page: Page,
{ personaName, customPageDataResponse }
) => {
const getPersonas = page.waitForResponse('/api/v1/personas*');
await settingClick(page, GlobalSettingOptions.CUSTOMIZE_LANDING_PAGE);
await getPersonas;
// Navigate to the customize landing page
await page.click(
`[data-testid="persona-details-card-${personaName}"] [data-testid="customize-page-button"]`
);
const getCustomPageDataResponse = await page.waitForResponse(
`/api/v1/docStore/name/persona.${encodeURIComponent(
personaName
)}.Page.LandingPage`
);
// await getCustomPageDataResponse;
expect(getCustomPageDataResponse.status()).toBe(customPageDataResponse);
};
export const removeAndCheckWidget = async (
page: Page,
{ widgetTestId, widgetKey }
) => {
// Click on remove widget button
await page.click(
`[data-testid="${widgetTestId}"] [data-testid="remove-widget-button"]`
);
// Check if widget does not exist
await page.waitForSelector(`[data-testid="${widgetTestId}"]`, {
state: 'detached',
});
// Check if empty widget placeholder is displayed in place of removed widget
await page.waitForSelector(
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]`
);
// Remove empty widget placeholder
await page.click(
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"] [data-testid="remove-widget-button"]`
);
// Check if empty widget placeholder does not exist
await page.waitForSelector(
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]`,
{ state: 'detached' }
);
};
export const checkAllDefaultWidgets = async (
page: Page,
checkEmptyWidgetPlaceholder = false
) => {
await expect(page.getByTestId('activity-feed-widget')).toBeVisible();
await expect(page.getByTestId('following-widget')).toBeVisible();
await expect(page.getByTestId('recently-viewed-widget')).toBeVisible();
await expect(page.getByTestId('data-assets-widget')).toBeVisible();
await expect(page.getByTestId('my-data-widget')).toBeVisible();
await expect(page.getByTestId('kpi-widget')).toBeVisible();
await expect(page.getByTestId('total-assets-widget')).toBeVisible();
if (checkEmptyWidgetPlaceholder) {
await expect(
page.getByTestId('ExtraWidget.EmptyWidgetPlaceholder')
).toBeVisible();
}
};
export const setUserDefaultPersona = async (
page: Page,
personaName: string
) => {
await visitUserProfilePage(page);
await page
.locator(
'[data-testid="user-profile-details"] [data-testid="edit-persona"]'
)
.click();
await page.waitForSelector(
'[role="tooltip"] [data-testid="selectable-list"]'
);
const setDefaultPersona = page.waitForResponse('/api/v1/users/*');
await page.getByTitle(personaName).click();
await setDefaultPersona;
await expect(
page.locator('[data-testid="user-profile-details"]')
).toContainText(personaName);
};

View File

@ -90,7 +90,6 @@ function AddWidgetTabContent({
placement="bottom"
title={widgetAddable ? '' : t('message.can-not-add-widget')}>
<Button
ghost
className="p-x-lg m-t-md"
data-testid="add-widget-button"
disabled={!widgetAddable}

View File

@ -208,5 +208,42 @@ export const mockAddWidgetReturnValues = [
x: 0,
y: 100,
},
{ h: 3, i: 'KnowledgePanel.Following-1', static: false, w: 1, x: 0, y: 6 },
{ h: 3, i: 'KnowledgePanel.Following-1', static: false, w: 1, x: 0, y: 4 },
];
export const mockAddWidgetReturnValues2 = [
{
h: 6,
i: 'KnowledgePanel.ActivityFeed',
static: false,
w: 3,
x: 0,
y: 0,
},
{
h: 3,
i: 'KnowledgePanel.RecentlyViewed',
static: false,
w: 1,
x: 3,
y: 3,
},
{
h: 2,
i: 'ExtraWidget.EmptyWidgetPlaceholder',
isDraggable: false,
static: false,
w: 4,
x: 0,
y: 100,
},
{
h: 3,
i: 'KnowledgePanel.dataAsset',
w: 1,
x: 2,
y: 4,
static: false,
},
{ h: 3, i: 'KnowledgePanel.Following-2', static: false, w: 1, x: 3, y: 4 },
];

View File

@ -13,19 +13,41 @@
import { mockWidget } from '../mocks/AddWidgetTabContent.mock';
import {
mockAddWidgetReturnValues,
mockAddWidgetReturnValues2,
mockCurrentAddWidget,
} from '../mocks/CustomizablePage.mock';
import { getAddWidgetHandler } from './CustomizableLandingPageUtils';
describe('getAddWidgetHandler function', () => {
it('should add new widget at EmptyWidgetPlaceholder place to be in the bottom', () => {
it('should add new widget at the bottom if not fit in the grid row', () => {
const result = getAddWidgetHandler(
mockWidget,
'ExtraWidget.EmptyWidgetPlaceholder',
1,
3
4
)(mockCurrentAddWidget);
expect(result).toEqual(mockAddWidgetReturnValues);
});
it('should add new widget at the same line if new widget can fit', () => {
const result = getAddWidgetHandler(
mockWidget,
'ExtraWidget.EmptyWidgetPlaceholder',
1,
4
)([
...mockCurrentAddWidget,
{
h: 3,
i: 'KnowledgePanel.dataAsset',
w: 1,
x: 2,
y: 4,
static: false,
},
]);
expect(result).toEqual(mockAddWidgetReturnValues2);
});
});

View File

@ -34,6 +34,47 @@ import { EntityReference } from '../generated/entity/type';
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
import customizePageClassBase from './CustomizePageClassBase';
const getNewWidgetPlacement = (
currentLayout: WidgetConfig[],
widgetWidth: number
) => {
const lowestWidgetLayout = currentLayout.reduce(
(acc, widget) => {
if (
widget.y >= acc.y &&
widget.i !== LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
) {
if (widget.y === acc.y && widget.x < acc.x) {
return acc;
}
return widget;
}
return acc;
},
{ y: 0, x: 0, w: 0 }
);
// Check if there's enough space to place the new widget on the same row
if (
customizePageClassBase.landingPageMaxGridSize -
(lowestWidgetLayout.x + lowestWidgetLayout.w) >=
widgetWidth
) {
return {
x: lowestWidgetLayout.x + lowestWidgetLayout.w,
y: lowestWidgetLayout.y,
};
}
// Otherwise, move to the next row
return {
x: 0,
y: lowestWidgetLayout.y + 1,
};
};
export const getAddWidgetHandler =
(
newWidgetData: Document,
@ -55,19 +96,14 @@ export const getAddWidgetHandler =
if (
placeholderWidgetKey === LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
) {
const emptyWidgetPlaceholder = currentLayout.find(
(item) => item.i === LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
) ?? { x: 0, y: 99 };
return [
...moveEmptyWidgetToTheEnd(currentLayout),
{
w: widgetWidth,
h: widgetHeight,
x: emptyWidgetPlaceholder.x,
y: emptyWidgetPlaceholder.y,
i: widgetFQN,
static: false,
...getNewWidgetPlacement(currentLayout, widgetWidth),
},
];
} else {

View File

@ -47,7 +47,7 @@ class CustomizePageClassBase {
defaultWidgetHeight = 3;
landingPageWidgetMargin = 16;
landingPageRowHeight = 100;
landingPageMaxGridSize = 3;
landingPageMaxGridSize = 4;
landingPageWidgetDefaultHeights: Record<string, number> = {
activityFeed: 6,