supported cypress for team and glossary drag and drop functionality (#15213)

* supported cypress for team drag and drop functionality

* added glossary drag and drop cypress and fix searchindex cypress failure

* changes as per comments

* fix the regression of import
This commit is contained in:
Ashish Gupta 2024-02-19 19:52:46 +05:30 committed by GitHub
parent 3748c792d0
commit 1c1dba7a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 615 additions and 121 deletions

View File

@ -0,0 +1,45 @@
/*
* 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.
*/
const dataTransfer = new DataTransfer();
export const dragAndDropElement = (
dragElement: string,
dropTarget: string,
isHeader?: boolean
) => {
cy.get(`[data-row-key="${Cypress.$.escapeSelector(dragElement)}"]`)
.invoke('attr', 'draggable')
.should('contain', 'true');
cy.get(`[data-row-key="${Cypress.$.escapeSelector(dragElement)}"]`).trigger(
'dragstart',
{
dataTransfer,
}
);
cy.get(
isHeader
? dropTarget
: `[data-row-key="${Cypress.$.escapeSelector(dropTarget)}"]`
)
.trigger('drop', { dataTransfer })
.trigger('dragend', { force: true });
};
export const openDragDropDropdown = (name: string) => {
cy.get(
`[data-row-key=${name}] > .whitespace-nowrap > [data-testid="expand-icon"] > svg`
).click();
};

View File

@ -96,3 +96,25 @@ export const removeGlossaryTerm = (
'[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="add-tag"]'
).should('be.visible');
};
export const confirmationDragAndDropGlossary = (
dragElement: string,
dropElement: string,
isHeader?: boolean
) => {
interceptURL('PATCH', `/api/v1/glossaryTerms/*`, 'patchGlossaryTerm');
// confirmation message before the transfer
cy.get('[data-testid="confirmation-modal"] .ant-modal-body').contains(
`Click on Confirm if youd like to move ${
isHeader
? `${dragElement} under ${dropElement} .`
: `${dragElement} term under ${dropElement} term.`
}`
);
// click on submit modal button to confirm the transfer
cy.get('.ant-modal-footer > .ant-btn-primary').click();
verifyResponseStatusCode('@patchGlossaryTerm', 200);
};

View File

@ -0,0 +1,87 @@
/*
* 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 {
interceptURL,
toastNotification,
verifyResponseStatusCode,
} from '../common';
export const commonTeamDetails = {
username: 'Aaron Johnson',
userId: 'aaron_johnson0',
assetname: 'dim_address',
email: 'team1@gmail.com',
updatedEmail: 'updatedemail@gmail.com',
};
export const confirmationDragAndDropTeam = (
dragTeam: string,
dropTeam: string
) => {
interceptURL('PUT', `/api/v1/teams`, 'putTeam');
// confirmation message before the transfer
cy.get('[data-testid="confirmation-modal"] .ant-modal-body')
.contains(
`Click on Confirm if youd like to move ${dragTeam} team under ${dropTeam} team.`
)
.should('be.visible');
// click on submit modal button to confirm the transfer
cy.get('.ant-modal-footer > .ant-btn-primary').click();
verifyResponseStatusCode('@putTeam', 200);
toastNotification('Team moved successfully!');
};
export const deleteTeamPermanently = (teamName: string) => {
interceptURL('GET', `/api/v1/teams/name/${teamName}*`, 'getSelectedTeam');
// Click on created team
cy.get(`[data-row-key="${teamName}"]`).contains(teamName).click();
verifyResponseStatusCode('@getSelectedTeam', 200);
cy.get(
'[data-testid="team-detail-header"] [data-testid="manage-button"]'
).click();
cy.get('[data-menu-id*="delete-button"]').should('be.visible');
cy.get('[data-testid="delete-button-title"]').click();
cy.get('[data-testid="confirm-button"]')
.should('exist')
.should('be.disabled');
// Check if soft delete option is present
cy.get('[data-testid="soft-delete-option"]').should('contain', teamName);
// Click on permanent delete option
cy.get('[data-testid="hard-delete-option"]')
.should('contain', teamName)
.click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
interceptURL('DELETE', '/api/v1/teams/*', 'deleteTeam');
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@deleteTeam', 200);
// Verify the toast message
toastNotification(`"${teamName}" deleted successfully!`);
// Validating the deleted team
cy.get('table').should('not.contain', teamName);
};

View File

@ -0,0 +1,168 @@
/*
* 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 { addTeam, interceptURL, toastNotification } from '../../common/common';
import {
dragAndDropElement,
openDragDropDropdown,
} from '../../common/Utils/DragAndDrop';
import {
commonTeamDetails,
confirmationDragAndDropTeam,
deleteTeamPermanently,
} from '../../common/Utils/Teams';
import { uuid } from '../../constants/constants';
import { SidebarItem } from '../../constants/Entity.interface';
import { GlobalSettingOptions } from '../../constants/settings.constant';
const teamNameGroup = `team-ct-test-${uuid()}`;
const teamNameBusiness = `team-ct-test-${uuid()}`;
const teamNameDivision = `team-ct-test-${uuid()}`;
const teamNameDepartment = `team-ct-test-${uuid()}`;
const TEAM_TYPE_BY_NAME = {
[teamNameBusiness]: 'BusinessUnit',
[teamNameDivision]: 'Division',
[teamNameDepartment]: 'Department',
[teamNameGroup]: 'Group',
};
const DRAG_AND_DROP_TEAM_DETAILS = [
{
name: teamNameBusiness,
updatedName: `${teamNameBusiness}-updated`,
teamType: 'BusinessUnit',
description: `This is ${teamNameBusiness} description`,
...commonTeamDetails,
},
{
name: teamNameDivision,
updatedName: `${teamNameDivision}-updated`,
teamType: 'Division',
description: `This is ${teamNameDivision} description`,
...commonTeamDetails,
},
{
name: teamNameDepartment,
updatedName: `${teamNameDepartment}-updated`,
teamType: 'Department',
description: `This is ${teamNameDepartment} description`,
...commonTeamDetails,
},
{
name: teamNameGroup,
updatedName: `${teamNameGroup}-updated`,
teamType: 'Group',
description: `This is ${teamNameGroup} description`,
...commonTeamDetails,
},
];
describe('Teams drag and drop should work properly', () => {
beforeEach(() => {
interceptURL('GET', `/api/v1/users?fields=*`, 'getUserDetails');
interceptURL('GET', `/api/v1/permissions/team/name/*`, 'permissions');
cy.login();
cy.sidebarClick(SidebarItem.SETTINGS);
// Clicking on teams
cy.settingClick(GlobalSettingOptions.TEAMS);
});
it('Add new team for drag and drop', () => {
DRAG_AND_DROP_TEAM_DETAILS.map((team) => {
addTeam(team);
cy.reload();
// asserting the added values
cy.get(`[data-row-key="${team.name}"]`)
.scrollIntoView()
.should('be.visible');
cy.get(`[data-row-key="${team.name}"]`).should(
'contain',
team.description
);
});
});
it('Should fail when drop team type is Group', () => {
[teamNameBusiness, teamNameDepartment, teamNameDivision].map((team) => {
dragAndDropElement(team, teamNameGroup);
toastNotification(
`You cannot move to this team as Team Type ${TEAM_TYPE_BY_NAME[team]} can't be Group children`
);
cy.get('.Toastify__toast-body', { timeout: 10000 }).should('not.exist');
});
});
it('Should fail when droppable team type is Department', () => {
[teamNameBusiness, teamNameDivision].map((team) => {
dragAndDropElement(team, teamNameDepartment);
toastNotification(
`You cannot move to this team as Team Type ${TEAM_TYPE_BY_NAME[team]} can't be Department children`
);
cy.get('.Toastify__toast-body', { timeout: 10000 }).should('not.exist');
});
});
it('Should fail when draggable team type is BusinessUnit and droppable team type is Division', () => {
dragAndDropElement(teamNameBusiness, teamNameDivision);
toastNotification(
`You cannot move to this team as Team Type BusinessUnit can't be Division children`
);
});
[teamNameBusiness, teamNameDivision, teamNameDepartment].map(
(droppableTeamName, index) => {
it(`Should drag and drop on ${TEAM_TYPE_BY_NAME[droppableTeamName]} team type`, () => {
// nested team will be shown once anything is moved under it
if (index !== 0) {
openDragDropDropdown(
[teamNameBusiness, teamNameDivision, teamNameDepartment][index - 1]
);
}
dragAndDropElement(teamNameGroup, droppableTeamName);
confirmationDragAndDropTeam(teamNameGroup, droppableTeamName);
// verify the team is moved under the business team
openDragDropDropdown(droppableTeamName);
cy.get(
`.ant-table-row-level-1[data-row-key="${teamNameGroup}"]`
).should('be.visible');
});
}
);
it(`Should drag and drop team on table level`, () => {
// open department team dropdown as it is moved under it from last test
openDragDropDropdown(teamNameDepartment);
dragAndDropElement(teamNameGroup, '.ant-table-thead > tr', true);
confirmationDragAndDropTeam(teamNameGroup, 'Organization');
// verify the team is moved under the table level
cy.get(`.ant-table-row-level-0[data-row-key="${teamNameGroup}"]`).should(
'be.visible'
);
});
it('Permanently deleting a team for drag and drop', () => {
[teamNameBusiness, teamNameDivision, teamNameDepartment, teamNameGroup].map(
(teamName) => {
deleteTeamPermanently(teamName);
}
);
});
});

View File

@ -28,26 +28,26 @@ describe(
verifyResponseStatusCode('@getApplications', 200);
});
it('Edit data insight application', () => {
interceptURL(
'GET',
'/api/v1/apps/name/DataInsightsApplication?fields=*',
'getDataInsightDetails'
);
interceptURL('PATCH', '/api/v1/apps/*', 'updateApplication');
cy.get(
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@getDataInsightDetails', 200);
cy.get('[data-testid="edit-button"]').click();
cy.get('[data-testid="cron-type"]').click();
cy.get('.rc-virtual-list [title="Day"]').click();
cy.get('[data-testid="hour-options"]').click();
cy.get('[title="01"]').click();
cy.get('.ant-modal-body [data-testid="deploy-button"]').click();
verifyResponseStatusCode('@updateApplication', 200);
cy.get('[data-testid="cron-string"]').should('contain', 'At 01:00 AM');
});
it('Edit data insight application', () => {
interceptURL(
'GET',
'/api/v1/apps/name/DataInsightsApplication?fields=*',
'getDataInsightDetails'
);
interceptURL('PATCH', '/api/v1/apps/*', 'updateApplication');
cy.get(
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
verifyResponseStatusCode('@getDataInsightDetails', 200);
cy.get('[data-testid="edit-button"]').click();
cy.get('[data-testid="cron-type"]').click();
cy.get('.rc-virtual-list [title="Day"]').click();
cy.get('[data-testid="hour-options"]').click();
cy.get('[title="01"]').click();
cy.get('.ant-modal-body [data-testid="deploy-button"]').click();
verifyResponseStatusCode('@updateApplication', 200);
cy.get('[data-testid="cron-string"]').should('contain', 'At 01:00 AM');
});
it('Uninstall application', () => {
interceptURL(
@ -75,27 +75,26 @@ describe(
);
});
it('Install application', () => {
interceptURL('GET', '/api/v1/apps/marketplace?limit=*', 'getMarketPlace');
interceptURL('POST', '/api/v1/apps', 'installApplication');
cy.get('[data-testid="add-application"]').click();
verifyResponseStatusCode('@getMarketPlace', 200);
cy.get(
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
cy.get('[data-testid="install-application"]').click();
cy.get('[data-testid="save-button"]').click();
cy.get('[data-testid="cron-type"]').click();
cy.get('.rc-virtual-list [title="Day"]').click();
cy.get('[data-testid="cron-type"]').should('contain', 'Day');
cy.get('[data-testid="deploy-button"]').click();
verifyResponseStatusCode('@installApplication', 201);
verifyResponseStatusCode('@getApplications', 200);
cy.get('[data-testid="data-insights-application-card"]').should(
'be.visible'
);
});
it('Install application', () => {
interceptURL('GET', '/api/v1/apps/marketplace?limit=*', 'getMarketPlace');
interceptURL('POST', '/api/v1/apps', 'installApplication');
cy.get('[data-testid="add-application"]').click();
verifyResponseStatusCode('@getMarketPlace', 200);
cy.get(
'[data-testid="data-insights-application-card"] [data-testid="config-btn"]'
).click();
cy.get('[data-testid="install-application"]').click();
cy.get('[data-testid="save-button"]').click();
cy.get('[data-testid="cron-type"]').click();
cy.get('.rc-virtual-list [title="Day"]').click();
cy.get('[data-testid="cron-type"]').should('contain', 'Day');
cy.get('[data-testid="deploy-button"]').click();
verifyResponseStatusCode('@installApplication', 201);
verifyResponseStatusCode('@getApplications', 200);
cy.get('[data-testid="data-insights-application-card"]').should(
'be.visible'
);
});
it('Deploy & run application', () => {
interceptURL(

View File

@ -25,7 +25,9 @@ import {
verifyResponseStatusCode,
} from '../../common/common';
import { deleteGlossary } from '../../common/GlossaryUtils';
import { dragAndDropElement } from '../../common/Utils/DragAndDrop';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { confirmationDragAndDropGlossary } from '../../common/Utils/Glossary';
import { addOwner, removeOwner } from '../../common/Utils/Owner';
import {
COLUMN_NAME_FOR_APPLY_GLOSSARY_TERM,
@ -1223,6 +1225,50 @@ describe('Glossary page should work properly', { tags: 'Glossary' }, () => {
});
});
it('Drag and Drop should work properly for glossary term', () => {
selectActiveGlossary(NEW_GLOSSARY.name);
dragAndDropElement(
NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName,
NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName
);
confirmationDragAndDropGlossary(
NEW_GLOSSARY_TERMS.term_2.name,
NEW_GLOSSARY_TERMS.term_1.name
);
// verify the term is moved under the parent term
cy.get(
`.ant-table-row-level-1[data-row-key="${Cypress.$.escapeSelector(
NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName
)}.${NEW_GLOSSARY_TERMS.term_2.name}"]`
).should('be.visible');
});
it('Drag and Drop should work properly for glossary term at table level', () => {
selectActiveGlossary(NEW_GLOSSARY.name);
dragAndDropElement(
`${NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName}.${NEW_GLOSSARY_TERMS.term_2.name}`,
'.ant-table-thead > tr',
true
);
confirmationDragAndDropGlossary(
NEW_GLOSSARY_TERMS.term_2.name,
NEW_GLOSSARY.name,
true
);
// verify the term is moved under the parent term
cy.get(
`.ant-table-row-level-0[data-row-key="${Cypress.$.escapeSelector(
NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName
)}"]`
).should('be.visible');
});
it('Delete glossary term should work properly', () => {
const terms = Object.values(NEW_GLOSSARY_TERMS);
selectActiveGlossary(NEW_GLOSSARY.name);

View File

@ -23,6 +23,7 @@ import {
uuid,
verifyResponseStatusCode,
} from '../../common/common';
import { deleteTeamPermanently } from '../../common/Utils/Teams';
import { SidebarItem } from '../../constants/Entity.interface';
import { GlobalSettingOptions } from '../../constants/settings.constant';
@ -394,55 +395,6 @@ describe('Teams flow should work properly', () => {
it('Permanently deleting a team without soft deleting should work properly', () => {
// Add a new team
addTeam(HARD_DELETE_TEAM_DETAILS);
interceptURL(
'GET',
`/api/v1/teams/name/${HARD_DELETE_TEAM_DETAILS.name}*`,
'getSelectedTeam'
);
// Click on created team
cy.get(`[data-row-key="${HARD_DELETE_TEAM_DETAILS.name}"]`)
.contains(HARD_DELETE_TEAM_DETAILS.name)
.click();
verifyResponseStatusCode('@getSelectedTeam', 200);
cy.get(
'[data-testid="team-detail-header"] [data-testid="manage-button"]'
).click();
cy.get('[data-menu-id*="delete-button"]').should('be.visible');
cy.get('[data-testid="delete-button-title"]').click();
cy.get('[data-testid="confirm-button"]')
.should('exist')
.should('be.disabled');
// Check if soft delete option is present
cy.get('[data-testid="soft-delete-option"]').should(
'contain',
HARD_DELETE_TEAM_DETAILS.name
);
// Click on permanent delete option
cy.get('[data-testid="hard-delete-option"]')
.should('contain', HARD_DELETE_TEAM_DETAILS.name)
.click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
interceptURL('DELETE', '/api/v1/teams/*', 'deleteTeam');
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@deleteTeam', 200);
// Verify the toast message
toastNotification(
`"${HARD_DELETE_TEAM_DETAILS.name}" deleted successfully!`
);
// Validating the deleted team
cy.get('table').should('not.contain', HARD_DELETE_TEAM_DETAILS.name);
deleteTeamPermanently(HARD_DELETE_TEAM_DETAILS.name);
});
});

View File

@ -21,6 +21,7 @@ import RedshiftWithDBTIngestionClass from '../../common/Services/RedshiftWithDBT
import S3IngestionClass from '../../common/Services/S3IngestionClass';
import SnowflakeIngestionClass from '../../common/Services/SnowflakeIngestionClass';
import SupersetIngestionClass from '../../common/Services/SupersetIngestionClass';
import { goToServiceListingPage } from '../../common/Utils/Services';
const services = [
new S3IngestionClass(),

View File

@ -39,6 +39,7 @@ jest.mock('react-router-dom', () => ({
jest.mock('../../../../utils/TeamUtils', () => ({
getMovedTeamData: jest.fn().mockReturnValue([]),
isDropRestricted: jest.fn().mockReturnValue(false),
}));
jest.mock('../../../../rest/teamsAPI', () => ({

View File

@ -12,10 +12,11 @@
*/
import { Modal, Skeleton, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ColumnsType, TableProps } from 'antd/lib/table';
import { ExpandableConfig } from 'antd/lib/table/interface';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import classNames from 'classnames';
import { isEmpty, isUndefined } from 'lodash';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -26,7 +27,6 @@ import {
NO_DATA_PLACEHOLDER,
} from '../../../../constants/constants';
import { TABLE_CONSTANTS } from '../../../../constants/Teams.constants';
import { TeamType } from '../../../../generated/api/teams/createTeam';
import { Team } from '../../../../generated/entity/teams/team';
import { Include } from '../../../../generated/type/include';
import { getTeamByName, updateTeam } from '../../../../rest/teamsAPI';
@ -34,7 +34,10 @@ import { Transi18next } from '../../../../utils/CommonUtils';
import { getEntityName } from '../../../../utils/EntityUtils';
import { getTeamsWithFqnPath } from '../../../../utils/RouterUtils';
import { getTableExpandableConfig } from '../../../../utils/TableUtils';
import { getMovedTeamData } from '../../../../utils/TeamUtils';
import {
getMovedTeamData,
isDropRestricted,
} from '../../../../utils/TeamUtils';
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
import { DraggableBodyRowProps } from '../../../common/Draggable/DraggableBodyRowProps.interface';
import FilterTablePlaceHolder from '../../../common/ErrorWithPlaceholder/FilterTablePlaceHolder';
@ -53,6 +56,7 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isTableLoading, setIsTableLoading] = useState<boolean>(false);
const [movedTeam, setMovedTeam] = useState<MovedTeamProps>();
const [isTableHovered, setIsTableHovered] = useState(false);
const columns: ColumnsType<Team> = useMemo(() => {
return [
@ -143,14 +147,27 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
];
}, [data, isFetchingAllTeamAdvancedDetails, onTeamExpand]);
const handleTableHover = useCallback(
(value: boolean) => setIsTableHovered(value),
[]
);
const handleMoveRow = useCallback(
async (dragRecord: Team, dropRecord: Team) => {
if (dragRecord.id === dropRecord.id) {
async (dragRecord: Team, dropRecord?: Team) => {
if (dragRecord.id === dropRecord?.id) {
return;
}
if (dropRecord.teamType === TeamType.Group) {
showErrorToast(t('message.error-team-transfer-message'));
if (
!isUndefined(dropRecord) &&
isDropRestricted(dragRecord.teamType, dropRecord?.teamType)
) {
showErrorToast(
t('message.error-team-transfer-message', {
dragTeam: dragRecord.teamType,
dropTeam: dropRecord.teamType,
})
);
return;
}
@ -167,11 +184,14 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
if (movedTeam) {
setIsTableLoading(true);
try {
const dropTeam = movedTeam.to?.id;
const data = await getTeamByName(movedTeam.from.name, {
fields: 'users, defaultRoles, policies, owner, parents, children',
include: Include.All,
});
await updateTeam(getMovedTeamData(data, [movedTeam.to.id]));
await updateTeam(
getMovedTeamData(data, dropTeam ? [dropTeam] : undefined)
);
onTeamExpand(true, currentTeam?.name);
showSuccessToast(t('message.team-moved-success'));
} catch (error) {
@ -179,12 +199,29 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
} finally {
setIsTableLoading(false);
setIsModalOpen(false);
setIsTableHovered(false);
}
}
};
const onTableRow = (record: Team, index?: number) =>
({ index, handleMoveRow, record } as DraggableBodyRowProps<Team>);
({
index,
handleMoveRow,
handleTableHover,
record,
} as DraggableBodyRowProps<Team>);
const onTableHeader: TableProps<Team>['onHeaderRow'] = () =>
({
handleMoveRow,
handleTableHover,
} as DraggableBodyRowProps<Team>);
const onDragConfirmationModalClose = useCallback(() => {
setIsModalOpen(false);
setIsTableHovered(false);
}, []);
const expandableConfig: ExpandableConfig<Team> = useMemo(
() => ({
@ -203,7 +240,9 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
<DndProvider backend={HTML5Backend}>
<Table
bordered
className="teams-list-table"
className={classNames('teams-list-table drop-over-background', {
'drop-over-table': isTableHovered,
})}
columns={columns}
components={TABLE_CONSTANTS}
data-testid="team-hierarchy-table"
@ -216,6 +255,7 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
pagination={false}
rowKey="name"
size="small"
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
</DndProvider>
@ -230,14 +270,14 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
okText={t('label.confirm')}
open={isModalOpen}
title={t('label.move-the-entity', { entity: t('label.team') })}
onCancel={() => setIsModalOpen(false)}
onCancel={onDragConfirmationModalClose}
onOk={handleChangeTeam}>
<Transi18next
i18nKey="message.entity-transfer-message"
renderElement={<strong />}
values={{
from: movedTeam?.from?.name,
to: movedTeam?.to?.name,
from: movedTeam?.from.name,
to: movedTeam?.to?.name ?? getEntityName(currentTeam),
entity: t('label.team-lowercase'),
}}
/>

View File

@ -30,7 +30,7 @@ export interface TeamHierarchyProps {
export interface MovedTeamProps {
from: Team;
to: Team;
to?: Team;
}
export interface TableExpandableDataProps {

View File

@ -20,7 +20,7 @@ export interface DraggableBodyRowProps<T>
extends React.HTMLAttributes<HTMLTableRowElement> {
index?: number;
record?: T;
handleMoveRow: (dragRecord: T, dropRecord?: T) => void;
handleMoveRow: (dragRecord: T, dropRecord?: T) => Promise<void>;
handleTableHover?: (value: boolean) => void;
}

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}} Größe muss zwischen {{min}} und {{max}} liegen.",
"entity-size-must-be-between-2-and-64": "{{entity}} Größe muss zwischen 2 und 64 liegen.",
"entity-transfer-message": "Klicken Sie auf Bestätigen, wenn Sie {{entity}} von <0>{{from}}</0> unter <0>{{to}}</0> {{entity}} verschieben möchten.",
"error-team-transfer-message": "Sie können nicht zu diesem Team wechseln, da Teamtyp 'Gruppe' keine untergeordneten Teams haben kann.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Fehler beim Abrufen des Zugriffstokens.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Laden Sie alle Ihre {{entity}} als CSV-Datei herunter und teilen Sie sie mit Ihrem Team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}} size must be between {{min}} and {{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} size must be between 2 and 64",
"entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "You cannot move to this team as Team Type Group cannot have children",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Error while fetching access token.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Download all your {{entity}} as a CSV file, and share with your team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "El tamaño de {{entity}} debe estar entre {{min}} y {{max}}",
"entity-size-must-be-between-2-and-64": "El tamaño de {{entity}} debe estar entre 2 y 64",
"entity-transfer-message": "Haga clic en Confirmar si desea mover <0>{{from}}</0> {{entity}} debajo de <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "No puede mover a este equipo ya que el tipo de equipo \"Grupo\" no puede tener hijos",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Error al obtener el token de acceso.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Download all your {{entity}} as a CSV file, and share with your team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}} taille doit être de {{min}} et {{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} taille doit être comprise entre 2 et 64",
"entity-transfer-message": "Cliquer sur Confirmer si vous souhaitez déplacer <0>{{from}}</0> {{entity}} sous <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "Vous ne pouvez pas déplacer cette équipe car le type d'équipe Groupe ne peut pas eêtre changé. Merci de créer une nouvelle équipe avec le type préférentiel.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Erreur pendant la récupération du jeton d'accès.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Download all your {{entity}} as a CSV file, and share with your team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}} יכול להיות בגודל בין {{min}} ל-{{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} יכול להיות בגודל בין 2 ל-64",
"entity-transfer-message": "לחץ על אישור אם ברצונך להעביר <0>{{from}}</0> {{entity}} מתחת ל-<0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "אין אפשרות להעביר לצוות זה כיוון שצוות מסוג קבוצה לא יכולים לכלול ילדים",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "הורד את כל הישויות שלך בפורמט קובץ CSV ושתף עם הצוות שלך.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}}のサイズは{{min}}以上{{max}}以下にしてください",
"entity-size-must-be-between-2-and-64": "{{entity}}のサイズは2以上64以下",
"entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "You cannot move to this team as Team Type Group cannot have children",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "アクセストークンの取得中にエラーが発生しました。",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Download all your {{entity}} as a CSV file, and share with your team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}} grootte moet tussen {{min}} en {{max}} liggen",
"entity-size-must-be-between-2-and-64": "{{entity}} grootte moet tussen 2 en 64 liggen",
"entity-transfer-message": "Klik op Bevestigen als je <0>{{from}}</0> {{entity}} wilt verplaatsen naar <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "Je kunt niet naar dit team verplaatsen, omdat een groep van het teamtype geen kinderen kan hebben",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Fout bij het ophalen van toegangstoken.",
"explore-our-guide-here": "bekijk onze handleiding hier.",
"export-entity-help": "Download al je {{entity}} als een CSV-bestand en deel het met je team.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "O tamanho de {{entity}} deve ser entre {{min}} e {{max}}",
"entity-size-must-be-between-2-and-64": "O tamanho de {{entity}} deve ser entre 2 e 64",
"entity-transfer-message": "Clique em Confirmar se deseja mover <0>{{from}}</0> {{entity}} para <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "Você não pode mover para esta equipe, pois o Tipo de Equipe Grupo não pode ter filhos",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Erro ao buscar token de acesso.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Baixe todos os seus {{entity}} como um arquivo CSV e compartilhe com sua equipe.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "Размер {{entity}} должен быть между {{min}} и {{max}}",
"entity-size-must-be-between-2-and-64": "Размер {{entity}} должен быть от 2 до 64",
"entity-transfer-message": "Нажмите «Подтвердить», если вы хотите переместить <0>{{from}}</0> {{entity}} в <0>{{to}}</0> {{entity}}.",
"error-team-transfer-message": "Вы не можете перейти в эту команду, так как в группе данного типа не может быть дочерних элементов",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Ошибка при получении токена доступа.",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "Загрузите все свои {{entity}} в виде CSV-файла и поделитесь ими со своей командой.",

View File

@ -1403,7 +1403,7 @@
"entity-size-in-between": "{{entity}}大小须介于{{min}}和{{max}}之间",
"entity-size-must-be-between-2-and-64": "{{entity}}大小必须介于2和64之间",
"entity-transfer-message": "如果您想将<0>{{from}}</0> {{entity}} 移动到<0>{{to}}</0> {{entity}}中,请单击确认",
"error-team-transfer-message": "由于团队类型的组不能有子级,因此您无法移动到此团队",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "获取访问令牌时出现错误",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "以 CSV 文件格式导出下载所有{{entity}},并可与团队共享。",

View File

@ -91,7 +91,7 @@
.drop-over-background {
.drop-over-child {
td {
background: @active-color;
background: @active-color !important;
border: 2px dashed @primary-color;
border-left: transparent;
@ -113,7 +113,7 @@
border: 2px dashed @primary-color;
td {
background: @active-color;
background: @active-color !important;
}
}
}

View File

@ -0,0 +1,112 @@
/*
* 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 { TeamType } from '../generated/entity/teams/team';
import { isDropRestricted } from './TeamUtils';
describe('isDropRestricted', () => {
it('should be droppable if on drop team is BusinessUnit', () => {
const groupDragResult = isDropRestricted(
TeamType.Group,
TeamType.BusinessUnit
);
expect(groupDragResult).toBe(false);
const departmentDragResult = isDropRestricted(
TeamType.Department,
TeamType.BusinessUnit
);
expect(departmentDragResult).toBe(false);
const divisionDragResult = isDropRestricted(
TeamType.Division,
TeamType.BusinessUnit
);
expect(divisionDragResult).toBe(false);
});
it('should not be droppable if on drop team team is Group', () => {
const businessUnitDragResult = isDropRestricted(
TeamType.BusinessUnit,
TeamType.Group
);
expect(businessUnitDragResult).toBe(true);
const departmentDragResult = isDropRestricted(
TeamType.Department,
TeamType.Group
);
expect(departmentDragResult).toBe(true);
const divisionDragResult = isDropRestricted(
TeamType.Division,
TeamType.Group
);
expect(divisionDragResult).toBe(true);
});
// For Division TeamType
it('should not be droppable if on drop team is Division', () => {
const businessUnitDragResult = isDropRestricted(
TeamType.BusinessUnit,
TeamType.Division
);
expect(businessUnitDragResult).toBe(true);
});
it('should be droppable if on drop team is Division', () => {
const departmentDragResult = isDropRestricted(
TeamType.Department,
TeamType.Division
);
expect(departmentDragResult).toBe(false);
const groupDragResult = isDropRestricted(TeamType.Group, TeamType.Division);
expect(groupDragResult).toBe(false);
});
// For Department TeamType
it('should not be droppable if on drop team is Department', () => {
const businessUnitDragResult = isDropRestricted(
TeamType.BusinessUnit,
TeamType.Department
);
expect(businessUnitDragResult).toBe(true);
const divisionDragResult = isDropRestricted(
TeamType.Division,
TeamType.Department
);
expect(divisionDragResult).toBe(true);
});
it('should be droppable if on drop team is Department', () => {
const groupDragResult = isDropRestricted(
TeamType.Group,
TeamType.Department
);
expect(groupDragResult).toBe(false);
});
});

View File

@ -57,7 +57,10 @@ const getEntityValue = (value: EntityReference[] | undefined) => {
return undefined;
};
export const getMovedTeamData = (team: Team, parents: string[]): CreateTeam => {
export const getMovedTeamData = (
team: Team,
parents?: string[]
): CreateTeam => {
const userDetails = omit(cloneDeep(team), [
'id',
'fullyQualifiedName',
@ -84,7 +87,7 @@ export const getMovedTeamData = (team: Team, parents: string[]): CreateTeam => {
userDetails.teamType === TeamType.Group
? undefined
: getEntityValue(children),
parents: parents,
parents,
policies: getEntityValue(policies),
users: getEntityValue(users),
} as CreateTeam;
@ -126,3 +129,21 @@ export const getTeamOptionsFromType = (parentType: TeamType) => {
return [TeamType.Group];
}
};
/**
* Restricting the drop of team based on the team type
* Group: Can't have any child team
* Division: Can have only Department and Group
* Department: Can have only Group
*/
export const isDropRestricted = (
dragTeamType?: TeamType,
dropTeamType?: TeamType
) =>
dropTeamType === TeamType.Group ||
(dropTeamType === TeamType.Division &&
dragTeamType === TeamType.BusinessUnit) ||
(dropTeamType === TeamType.Department &&
dragTeamType === TeamType.BusinessUnit) ||
(dropTeamType === TeamType.Department && dragTeamType === TeamType.Division);