Skip to content

Commit 31469b4

Browse files
Implement batch test case removal
1 parent ad78371 commit 31469b4

File tree

17 files changed

+405
-79
lines changed

17 files changed

+405
-79
lines changed

app/src/common/urls.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ export const URLS = {
376376

377377
testCaseDetails: (projectKey, testCaseId) =>
378378
`${urlBase}project/${projectKey}/tms/test-case/${testCaseId}`,
379-
bulkUpdateTestCases: (projectKey) => `${urlBase}project/${projectKey}/tms/test-case/batch`,
379+
testCasesBatch: (projectKey) => `${urlBase}project/${projectKey}/tms/test-case/batch`,
380380
testCaseBatchDuplicate: (projectKey) =>
381381
`${urlBase}project/${projectKey}/tms/test-case/batch/duplicate`,
382382
testCases: (projectKey, query = {}) =>

app/src/controllers/testCase/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
getFoldersAction,
1919
getAllTestCasesAction,
2020
getTestCaseByFolderIdAction,
21+
updateFolderCounterAction,
2122
} from './actionCreators';
2223
export { testCaseSagas } from './sagas';
2324
export * from './constants';

app/src/controllers/testCase/selectors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface TestCaseState {
3535
};
3636
testCases?: {
3737
isLoading?: boolean;
38-
list?: unknown[];
38+
list?: TestCase[];
3939
page: Page | null;
4040
};
4141
details?: {
@@ -69,7 +69,8 @@ export const isLoadingFolderSelector = (state: RootState): boolean =>
6969
export const isLoadingTestCasesSelector = (state: RootState) =>
7070
state.testCase?.testCases?.isLoading || false;
7171

72-
export const testCasesSelector = (state: RootState) => state.testCase?.testCases?.list || [];
72+
export const testCasesSelector = (state: RootState): TestCase[] =>
73+
state.testCase?.testCases?.list || [];
7374

7475
export const testCasesPageSelector = (state: RootState): Page | null =>
7576
state.testCase?.testCases?.page || null;

app/src/pages/inside/common/testCaseList/testCaseList.tsx

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { memo, SetStateAction, useState } from 'react';
17+
import { memo, useState } from 'react';
1818
import { useIntl } from 'react-intl';
19-
import { xor } from 'es-toolkit';
2019
import { BubblesLoader, FilterOutlineIcon, Table } from '@reportportal/ui-kit';
2120

2221
import { createClassnames } from 'common/utils';
@@ -25,6 +24,7 @@ import { ExtendedTestCase } from 'pages/inside/testCaseLibraryPage/types';
2524
import { INSTANCE_KEYS } from 'pages/inside/common/expandedOptions/folder/useFolderTooltipItems';
2625
import { TestCasePriority } from 'pages/inside/common/priorityIcon/types';
2726
import { useUserPermissions } from 'hooks/useUserPermissions';
27+
import { SelectedTestCaseRow } from 'pages/inside/testCaseLibraryPage/allTestCasesPage/allTestCasesPage';
2828

2929
import { TestCaseNameCell } from './testCaseNameCell';
3030
import { TestCaseExecutionCell } from './testCaseExecutionCell';
@@ -45,7 +45,8 @@ interface TestCaseListProps {
4545
folderTitle: string;
4646
searchValue?: string;
4747
selectedRowIds: (number | string)[];
48-
handleSelectedRowIds: (value: SetStateAction<(number | string)[]>) => void;
48+
selectedRows: SelectedTestCaseRow[];
49+
handleSelectedRows: (rows: SelectedTestCaseRow[]) => void;
4950
onSearchChange?: (value: string) => void;
5051
selectable?: boolean;
5152
instanceKey: INSTANCE_KEYS;
@@ -57,7 +58,8 @@ export const TestCaseList = memo(
5758
loading = false,
5859
currentPage = DEFAULT_CURRENT_PAGE,
5960
selectedRowIds,
60-
handleSelectedRowIds,
61+
selectedRows,
62+
handleSelectedRows,
6163
itemsPerPage,
6264
searchValue = '',
6365
onSearchChange,
@@ -86,25 +88,37 @@ export const TestCaseList = memo(
8688
};
8789

8890
const handleRowSelect = (id: number | string) => {
89-
handleSelectedRowIds((selectedRows) => xor(selectedRows, [id]));
91+
const testCase = testCases.find((testCase) => testCase.id === id);
92+
93+
if (!testCase) {
94+
return;
95+
}
96+
97+
const isCurrentlySelected = selectedRows.some((row) => row.id === id);
98+
99+
handleSelectedRows(
100+
isCurrentlySelected
101+
? selectedRows.filter((row) => row.id !== id)
102+
: [...selectedRows, { id: testCase.id, folderId: testCase.testFolder.id }],
103+
);
90104
};
91105

92-
const handleAllSelect = () => {
93-
handleSelectedRowIds((prevSelectedRowIds) => {
94-
const currentDataIds: (string | number)[] = currentData.map(
95-
({ id }: { id: string | number }) => id,
96-
);
97-
if (currentDataIds.every((rowId) => prevSelectedRowIds.includes(rowId))) {
98-
return prevSelectedRowIds.filter(
99-
(selectedRowId) => !currentDataIds.includes(selectedRowId),
100-
);
101-
}
102-
103-
return [
104-
...prevSelectedRowIds,
105-
...currentDataIds.filter((id) => !prevSelectedRowIds.includes(id)),
106-
];
107-
});
106+
const handleSelectAll = () => {
107+
const currentPageTestCaseIds = currentData.map(({ id }) => id);
108+
const isAllCurrentPageSelected = currentPageTestCaseIds.every((testCaseId) =>
109+
selectedRowIds.includes(testCaseId),
110+
);
111+
112+
const newSelectedRows = isAllCurrentPageSelected
113+
? selectedRows.filter((row) => !currentPageTestCaseIds.includes(row.id))
114+
: [
115+
...selectedRows,
116+
...currentData
117+
.filter((testCase) => !selectedRowIds.includes(testCase.id))
118+
.map((testCase) => ({ id: testCase.id, folderId: testCase.testFolder.id })),
119+
];
120+
121+
handleSelectedRows(newSelectedRows);
108122
};
109123

110124
const selectedTestCase = testCases.find((testCase) => testCase.id === selectedTestCaseId);
@@ -201,7 +215,7 @@ export const TestCaseList = memo(
201215
fixedColumns={fixedColumns}
202216
primaryColumn={primaryColumn}
203217
sortableColumns={[]}
204-
onToggleAllRowsSelection={handleAllSelect}
218+
onToggleAllRowsSelection={handleSelectAll}
205219
className={cx('test-case-table')}
206220
rowClassName={cx('test-case-table-row')}
207221
/>

app/src/pages/inside/testCaseLibraryPage/allTestCasesPage/allTestCasesPage.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { useState, useCallback, useMemo, useEffect } from 'react';
1818
import { useDispatch, useSelector } from 'react-redux';
1919
import { useIntl } from 'react-intl';
20-
import { isEmpty, noop, compact } from 'es-toolkit/compat';
20+
import { isEmpty, noop, compact, countBy } from 'es-toolkit/compat';
2121
import { Button, MeatballMenuIcon, Pagination, Selection } from '@reportportal/ui-kit';
2222

2323
import { createClassnames } from 'common/utils';
@@ -47,6 +47,7 @@ import { messages } from './messages';
4747
import { FolderEmptyState } from '../emptyState/folder/folderEmptyState';
4848
import { useAddTestCasesToTestPlanModal } from '../addTestCasesToTestPlanModal/useAddTestCasesToTestPlanModal';
4949
import { useBatchDuplicateToFolderModal } from './batchDuplicateToFolderModal';
50+
import { useBatchDeleteTestCasesModal } from './batchDeleteTestCasesModal';
5051

5152
import styles from './allTestCasesPage.scss';
5253

@@ -63,6 +64,11 @@ interface AllTestCasesPageProps {
6364

6465
const FIRST_PAGE_NUMBER = 1;
6566

67+
export interface SelectedTestCaseRow {
68+
id: number;
69+
folderId: number;
70+
}
71+
6672
export const AllTestCasesPage = ({
6773
testCases,
6874
loading,
@@ -77,13 +83,15 @@ export const AllTestCasesPage = ({
7783
totalItems: testCasesPageData?.totalElements,
7884
itemsPerPage: TestCasePageDefaultValues.limit,
7985
});
80-
const [selectedRowIds, setSelectedRowIds] = useState<number[]>([]);
86+
const [selectedRows, setSelectedRows] = useState<SelectedTestCaseRow[]>([]);
8187
const folderId = useSelector(urlFolderIdSelector);
8288
const folders = useSelector(foldersSelector);
8389
const dispatch = useDispatch();
84-
const isAnyRowSelected = !isEmpty(selectedRowIds);
90+
const isAnyRowSelected = !isEmpty(selectedRows);
91+
const selectedRowIds = useMemo(() => selectedRows.map((row) => row.id), [selectedRows]);
8592
const { openModal: openAddToTestPlanModal } = useAddTestCasesToTestPlanModal();
8693
const { openModal: openBatchDuplicateToFolderModal } = useBatchDuplicateToFolderModal();
94+
const { openModal: openBatchDeleteTestCasesModal } = useBatchDeleteTestCasesModal();
8795
const { canDeleteTestCase, canDuplicateTestCase, canEditTestCase } = useUserPermissions();
8896

8997
useEffect(() => {
@@ -127,7 +135,15 @@ export const AllTestCasesPage = ({
127135
canDeleteTestCase && {
128136
label: formatMessage(COMMON_LOCALE_KEYS.DELETE),
129137
variant: 'destructive',
130-
onClick: noop,
138+
onClick: () => {
139+
const folderDeltasMap = countBy(selectedRows, (row) => String(row.folderId));
140+
141+
openBatchDeleteTestCasesModal({
142+
selectedTestCaseIds: selectedRowIds,
143+
folderDeltasMap,
144+
onClearSelection,
145+
});
146+
},
131147
},
132148
]);
133149

@@ -147,7 +163,9 @@ export const AllTestCasesPage = ({
147163
return <FolderEmptyState folderTitle={folderTitle} />;
148164
}
149165

150-
const onClearSelection = () => setSelectedRowIds([]);
166+
const onClearSelection = () => setSelectedRows([]);
167+
168+
const handleSelectedRows = (rows: SelectedTestCaseRow[]) => setSelectedRows(rows);
151169

152170
const setTestCasesPage = (page: number): void => {
153171
const params = {
@@ -198,7 +216,8 @@ export const AllTestCasesPage = ({
198216
searchValue={searchValue}
199217
onSearchChange={handleSearchChange}
200218
selectedRowIds={selectedRowIds}
201-
handleSelectedRowIds={setSelectedRowIds}
219+
selectedRows={selectedRows}
220+
handleSelectedRows={handleSelectedRows}
202221
folderTitle={folderTitle}
203222
instanceKey={instanceKey}
204223
/>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
.batch-delete-modal {
18+
&__text {
19+
&--bold {
20+
font-weight: 600;
21+
}
22+
}
23+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2025 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useMemo, useCallback } from 'react';
18+
import { useIntl } from 'react-intl';
19+
import { useDispatch } from 'react-redux';
20+
import { noop } from 'es-toolkit';
21+
import { Modal } from '@reportportal/ui-kit';
22+
23+
import { UseModalData } from 'common/hooks';
24+
import { createClassnames } from 'common/utils';
25+
import { withModal, hideModalAction } from 'controllers/modal';
26+
import { COMMON_LOCALE_KEYS } from 'common/constants/localization';
27+
import { ModalLoadingOverlay } from 'components/modalLoadingOverlay';
28+
29+
import { useBatchDeleteTestCases } from './useBatchDeleteTestCases';
30+
import { messages } from './messages';
31+
32+
import styles from './batchDeleteTestCasesModal.scss';
33+
34+
const cx = createClassnames(styles);
35+
36+
export const BATCH_DELETE_TEST_CASES_MODAL_KEY = 'batchDeleteTestCasesModalKey';
37+
38+
export interface BatchDeleteTestCasesModalData {
39+
selectedTestCaseIds: number[];
40+
folderDeltasMap: Record<number, number>;
41+
onClearSelection?: () => void;
42+
}
43+
44+
type BatchDeleteTestCasesModalProps = UseModalData<BatchDeleteTestCasesModalData>;
45+
46+
const BatchDeleteTestCasesModal = ({
47+
data: {
48+
selectedTestCaseIds = [],
49+
folderDeltasMap = {},
50+
onClearSelection = noop,
51+
} = {} as BatchDeleteTestCasesModalData,
52+
}: BatchDeleteTestCasesModalProps) => {
53+
const { formatMessage } = useIntl();
54+
const dispatch = useDispatch();
55+
const { isLoading, batchDelete } = useBatchDeleteTestCases({
56+
onSuccess: () => {
57+
dispatch(hideModalAction());
58+
onClearSelection();
59+
},
60+
});
61+
62+
const hideModal = () => dispatch(hideModalAction());
63+
64+
const handleDelete = useCallback(() => {
65+
batchDelete(selectedTestCaseIds, folderDeltasMap).catch(noop);
66+
}, [batchDelete, selectedTestCaseIds, folderDeltasMap]);
67+
68+
const okButton = useMemo(
69+
() => ({
70+
children: formatMessage(COMMON_LOCALE_KEYS.DELETE),
71+
disabled: isLoading,
72+
variant: 'danger' as const,
73+
onClick: handleDelete,
74+
}),
75+
[formatMessage, isLoading, handleDelete],
76+
);
77+
78+
const cancelButton = useMemo(
79+
() => ({
80+
children: formatMessage(COMMON_LOCALE_KEYS.CANCEL),
81+
disabled: isLoading,
82+
}),
83+
[formatMessage, isLoading],
84+
);
85+
86+
return (
87+
<Modal
88+
title={formatMessage(messages.batchDeleteTestCasesTitle)}
89+
okButton={okButton}
90+
cancelButton={cancelButton}
91+
onClose={hideModal}
92+
>
93+
<div>
94+
{formatMessage(messages.batchDeleteDescription, {
95+
count: selectedTestCaseIds.length,
96+
b: (text) => <span className={cx('batch-delete-modal__text--bold')}>{text}</span>,
97+
})}
98+
<ModalLoadingOverlay isVisible={isLoading} />
99+
</div>
100+
</Modal>
101+
);
102+
};
103+
104+
export default withModal(BATCH_DELETE_TEST_CASES_MODAL_KEY)(BatchDeleteTestCasesModal);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2025 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export { useBatchDeleteTestCasesModal } from './useBatchDeleteTestCasesModal';
18+
export { BATCH_DELETE_TEST_CASES_MODAL_KEY } from './batchDeleteTestCasesModal';

0 commit comments

Comments
 (0)