From 9508504dac02f39cdf7f08492cf19f4ec3c31acd Mon Sep 17 00:00:00 2001 From: "Carlos E. Feria Vila" Date: Tue, 2 Nov 2021 10:53:02 +0100 Subject: [PATCH] [TACKLE-192] - Bulk copy Assessment/Review (#261) * save changes * save * Save changes * Save changes * Add checbox confirmation * save changes * Add translations * Fix warning msg * Change CSV test files * Fix notification and form * remove unused import * Add select all * Add tests for useSelectionFromPageState * Add tests * change warning icon position * Change icon --- .../partially_valid_application_rows.csv | 6 +- .../valid_application_rows.csv | 4 +- public/locales/en/translation.json | 12 + public/locales/es/translation.json | 16 +- src/App.tsx | 2 + src/api/models.tsx | 14 + src/api/rest.tsx | 24 + .../application-list/application-list.tsx | 102 +++- .../application-list-expanded-area.tsx | 2 +- .../components/application-tags/index.ts | 2 +- .../bulk-copy-assessment-review-form.tsx | 502 ++++++++++++++++++ .../index.tsx | 1 + .../manage-imports-details.tsx | 2 +- .../manage-imports/manage-imports.tsx | 2 +- .../business-services/business-services.tsx | 2 +- .../controls/job-functions/job-functions.tsx | 2 +- .../stakeholder-groups/stakeholder-groups.tsx | 2 +- .../controls/stakeholders/stakeholders.tsx | 2 +- src/pages/controls/tags/tags.tsx | 2 +- .../adoption-candidate-table.tsx | 3 +- .../app-table-with-controls.tsx | 51 +- .../tests/app-table-with-controls.test.tsx | 2 +- .../toolbar-bulk-selector.tsx | 28 +- .../bulk-copy-notifications-container.tsx | 110 ++++ .../index.tsx | 1 + src/shared/containers/index.ts | 1 + src/shared/hooks/index.ts | 2 + src/shared/hooks/useFetch/useFetch.ts | 58 +- src/shared/hooks/useFetchPagination/index.ts | 1 + .../useFetchPagination.test.ts | 124 +++++ .../useFetchPagination/useFetchPagination.ts | 129 +++++ .../hooks/useSelectionFromPageState/index.ts | 1 + .../useSelectionFromPageState.test.ts | 158 ++++++ .../useSelectionFromPageState.ts | 66 +++ src/store/bulkCopy/actions.ts | 20 + src/store/bulkCopy/index.ts | 13 + src/store/bulkCopy/reducer.ts | 70 +++ src/store/bulkCopy/selectors.ts | 9 + src/store/rootReducer.tsx | 2 + 39 files changed, 1486 insertions(+), 64 deletions(-) create mode 100644 src/pages/application-inventory/application-list/components/bulk-copy-assessment-review-form/bulk-copy-assessment-review-form.tsx create mode 100644 src/pages/application-inventory/application-list/components/bulk-copy-assessment-review-form/index.tsx create mode 100644 src/shared/containers/bulk-copy-notifications-container/bulk-copy-notifications-container.tsx create mode 100644 src/shared/containers/bulk-copy-notifications-container/index.tsx create mode 100644 src/shared/hooks/useFetchPagination/index.ts create mode 100644 src/shared/hooks/useFetchPagination/useFetchPagination.test.ts create mode 100644 src/shared/hooks/useFetchPagination/useFetchPagination.ts create mode 100644 src/shared/hooks/useSelectionFromPageState/index.ts create mode 100644 src/shared/hooks/useSelectionFromPageState/useSelectionFromPageState.test.ts create mode 100644 src/shared/hooks/useSelectionFromPageState/useSelectionFromPageState.ts create mode 100644 src/store/bulkCopy/actions.ts create mode 100644 src/store/bulkCopy/index.ts create mode 100644 src/store/bulkCopy/reducer.ts create mode 100644 src/store/bulkCopy/selectors.ts diff --git a/cypress/fixtures/application-import/partially_valid_application_rows.csv b/cypress/fixtures/application-import/partially_valid_application_rows.csv index d47c2276..25d95689 100644 --- a/cypress/fixtures/application-import/partially_valid_application_rows.csv +++ b/cypress/fixtures/application-import/partially_valid_application_rows.csv @@ -1,4 +1,4 @@ Record Type 1,Application Name,Description,Comments,Business Service,Tag Type 1,Tag 1,Tag Type 2,Tag 2,Tag Type 3,Tag 3,Tag Type 4,Tag 4,Tag Type 5,Tag 5,Tag Type 6,Tag 6,Tag Type 7,Tag 7,Tag Type 8,Tag 8,Tag Type 9,Tag 9,Tag Type 10,Tag 10,Tag Type 11,Tag 11,Tag Type 12,Tag 12,Tag Type 13,Tag 13,Tag Type 14,Tag 14,Tag Type 15,Tag 15,Tag Type 16,Tag 16,Tag Type 17,Tag 17,Tag Type 18,Tag 18,Tag Type 19,Tag 19,Tag Type 20,Tag 20 -,application-a,description-a,comment-a,service-a,tagType-a,tag-a-a,tagType-a,tag-b-a,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -,application-b,description-b,comment-b,service-b,tagType-b,tag-a-b,tagType-b,tag-b-b,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -,application-c,description-c,comment-c,service-x,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +1,application-a,description-a,comment-a,service-a,tagType-a,tag-a-a,tagType-a,tag-b-a,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +1,application-b,description-b,comment-b,service-b,tagType-b,tag-a-b,tagType-b,tag-b-b,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +1,application-c,description-c,comment-c,service-x,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/cypress/fixtures/application-import/valid_application_rows.csv b/cypress/fixtures/application-import/valid_application_rows.csv index 20e641f4..f8c4a896 100644 --- a/cypress/fixtures/application-import/valid_application_rows.csv +++ b/cypress/fixtures/application-import/valid_application_rows.csv @@ -1,3 +1,3 @@ Record Type 1,Application Name,Description,Comments,Business Service,Tag Type 1,Tag 1,Tag Type 2,Tag 2,Tag Type 3,Tag 3,Tag Type 4,Tag 4,Tag Type 5,Tag 5,Tag Type 6,Tag 6,Tag Type 7,Tag 7,Tag Type 8,Tag 8,Tag Type 9,Tag 9,Tag Type 10,Tag 10,Tag Type 11,Tag 11,Tag Type 12,Tag 12,Tag Type 13,Tag 13,Tag Type 14,Tag 14,Tag Type 15,Tag 15,Tag Type 16,Tag 16,Tag Type 17,Tag 17,Tag Type 18,Tag 18,Tag Type 19,Tag 19,Tag Type 20,Tag 20 -,application-a,description-a,comment-a,service-a,tagType-a,tag-a-a,tagType-a,tag-b-a,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -,application-b,description-b,comment-b,service-b,tagType-b,tag-a-b,tagType-b,tag-b-b,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +1,application-a,description-a,comment-a,service-a,tagType-a,tag-a-a,tagType-a,tag-b-a,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +1,application-b,description-b,comment-b,service-b,tagType-b,tag-a-b,tagType-b,tag-b-b,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5b061766..aac5ff33 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -13,6 +13,9 @@ "clearAllFilters": "Clear all filters", "close": "Close", "continue": "Continue", + "copy": "Copy", + "copyAssessment": "Copy assessment", + "copyAssessmentAndReview": "Copy assessment and review", "create": "Create", "createNew": "Create new", "createTag": "Create tag", @@ -69,6 +72,8 @@ "leavePage": "Are you sure you want to leave this page? Be sure to save your changes, or they will be lost." }, "title": { + "copyApplicationAssessmentAndReviewFrom": "Copy {{what}} assessment and review", + "copyApplicationAssessmentFrom": "Copy {{what}} assessment", "delete": "Delete {{what}}?", "discard": "Discard {{what}}?", "importApplicationFile": "Import application file", @@ -101,6 +106,11 @@ "appNotAssesedTitle": "Assessment has not been completed", "appNotAssessedBody": "In order to review an application it must be assessed first. Please assess the application", "assessmentStakeholderHeader": "Select the stakeholder(s) or stakeholder group(s) associated with this assessment.", + "continueConfirmation": "Yes, continue", + "copyAssessmentAndReviewBody": "Some of the selected target applications have an in-progress or complete assessment/review. By continuing, the existing assessment(s)/review(s) will be replaced by the copied assessment/review. Do you wish to continue?", + "copyAssessmentAndReviewQuestion": "Copy assessment and review?", + "copyAssessmentBody": "Some of the selected target applications have an in-progress or complete assessment. By continuing, the existing assessment(s) will be replaced by the copied assessment. Do you wish to continue?", + "copyAssessmentQuestion": "Copy assessment?", "couldNotFetchBody": "Resource doesn't exists or you don't have access to it", "couldNotFetchTitle": "Not available", "importErrorCheckDocumentation": "For status Error imports please check the documentation to ensure your file is structured correctly.", @@ -243,6 +253,8 @@ "toastr": { "success": { "added": "Success! {{what}} was added as a {{type}}.", + "assessmentAndReviewCopied": "Success! Assessment and review copied to selected applications", + "assessmentCopied": "Success! Assessment copied to selected applications", "assessmentDiscarded": "Success! Assessment discarded for {{application}}.", "fileSavedToBeProcessed": "Success! file saved to be processed." } diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index b0f18187..d60834e9 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -13,6 +13,9 @@ "clearAllFilters": "Limpiar todos los filtros", "close": "Cerrar", "continue": "Continuar", + "copy": "Copiar", + "copyAssessment": "Copiar evaluación", + "copyAssessmentAndReview": "Copiar evaluación y revisión", "create": "Crear", "createNew": "Crear nuevo", "createTag": "Crear etiqueta", @@ -69,6 +72,8 @@ "leavePage": "¿Estás seguro de querer salir de esta página? Asegúrate de guardar tus cambios o estos se perderán." }, "title": { + "copyApplicationAssessmentAndReviewFrom": "Copiar evaluación y revisión de {{what}}", + "copyApplicationAssessmentFrom": "Copiar evaluación {{what}}", "delete": "¿Eliminar {{what}}?", "discard": "¿Descartar {{what}}?", "importApplicationFile": "Importar archivo con aplicaciones", @@ -98,9 +103,14 @@ "small": "Pequeño" }, "message": { - "appNotAssesedTitle": "", - "appNotAssessedBody": "", + "appNotAssesedTitle": "La evaluación no se ha completado", + "appNotAssessedBody": "Para revisar una aplicación, esta debe de ser evaluada primero. Por favor evalúe la aplicación", "assessmentStakeholderHeader": "Seleccione a los interesados o grupo de interesados asociados a esta evaluación.", + "continueConfirmation": "Si, continuar", + "copyAssessmentAndReviewBody": "Algunas de las aplicaciones de destino seleccionadas tienen una evaluación/revisión en curso o completa. Al continuar, las evaluaciones/revisiones existentes serán reemplazadas por la evaluación/revisión copiada. ¿Desea continuar?", + "copyAssessmentAndReviewQuestion": "¿Copiar evaluación y revisión?", + "copyAssessmentBody": "Algunas de las aplicaciones de destino seleccionadas tienen una evaluación en curso o completa. Al continuar, las evaluaciones existentes serán reemplazadas por la evaluación copiada. ¿Desea continuar?", + "copyAssessmentQuestion": "¿Copiar evaluación?", "couldNotFetchBody": "El recurso no existe o no tienes permiso para acceder al mismo", "couldNotFetchTitle": "No disponible", "importErrorCheckDocumentation": "Para estados de importación Error por favor verifica la documentación para asegurar la correcta estructura del archivo.", @@ -243,6 +253,8 @@ "toastr": { "success": { "added": "Éxito! {{what}} fue creado como un {{type}}.", + "assessmentAndReviewCopied": "Éxito! Evaluación y revisión copiada a las aplicaciones seleccionadas", + "assessmentCopied": "Éxito! Evaluación copiada a las aplicaciones seleccionadas", "assessmentDiscarded": "Éxito! Evaluación de {{application}} desechada.", "fileSavedToBeProcessed": "Éxito! El archivo fue guardado para ser procesado." } diff --git a/src/App.tsx b/src/App.tsx index f6bc229f..cb609ba6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import NotificationsPortal from "@redhat-cloud-services/frontend-components-noti import "@redhat-cloud-services/frontend-components-notifications/index.css"; import { ConfirmDialogContainer } from "./shared/containers/confirm-dialog-container"; +import { BulkCopyNotificationsContainer } from "./shared/containers/bulk-copy-notifications-container"; const App: React.FC = () => { return ( @@ -19,6 +20,7 @@ const App: React.FC = () => { + ); }; diff --git a/src/api/models.tsx b/src/api/models.tsx index b4f96370..393c225a 100644 --- a/src/api/models.tsx +++ b/src/api/models.tsx @@ -127,6 +127,13 @@ export interface ApplicationImport { isValid: boolean; } +export interface BulkCopyReview { + id?: number; + sourceReview: number; + targetApplications: number[]; + completed?: boolean; +} + // Pathfinder export type AssessmentStatus = "EMPTY" | "STARTED" | "COMPLETE"; @@ -188,6 +195,13 @@ export interface AssessmentConfidence { confidence: number; } +export interface BulkCopyAssessment { + bulkId?: number; + fromAssessmentId: number; + applications: { applicationId: number }[]; + completed?: boolean; +} + // Pagination export interface BusinessServicePage { diff --git a/src/api/rest.tsx b/src/api/rest.tsx index 763c38e0..acfb6b79 100644 --- a/src/api/rest.tsx +++ b/src/api/rest.tsx @@ -27,6 +27,8 @@ import { ApplicationImportSummaryPage, ApplicationImportPage, ApplicationImportSummary, + BulkCopyAssessment, + BulkCopyReview, } from "./models"; export const CONTROLS_BASE_URL = "controls"; @@ -666,6 +668,16 @@ export const getApplicationSummaryCSV = (id: string): AxiosPromise => { }); }; +export const createBulkCopyReview = ( + bulk: BulkCopyReview +): AxiosPromise => { + return APIClient.post(`${REVIEW}/bulk`, bulk); +}; + +export const getBulkCopyReview = (id: number): AxiosPromise => { + return APIClient.get(`${REVIEW}/bulk/${id}`); +}; + // export const getAssessments = (filters: { @@ -723,3 +735,15 @@ export const getAssessmentConfidence = ( applicationIds.map((f) => ({ applicationId: f })) ); }; + +export const createBulkCopyAssessment = ( + bulk: BulkCopyAssessment +): AxiosPromise => { + return APIClient.post(`${ASSESSMENTS}/bulk`, bulk); +}; + +export const getBulkCopyAssessment = ( + id: number +): AxiosPromise => { + return APIClient.get(`${ASSESSMENTS}/bulk/${id}`); +}; diff --git a/src/pages/application-inventory/application-list/application-list.tsx b/src/pages/application-inventory/application-list/application-list.tsx index 2265ed82..ea91a3e3 100644 --- a/src/pages/application-inventory/application-list/application-list.tsx +++ b/src/pages/application-inventory/application-list/application-list.tsx @@ -36,6 +36,7 @@ import { RootState } from "store/rootReducer"; import { alertActions } from "store/alert"; import { confirmDialogActions } from "store/confirmDialog"; import { unknownTagsSelectors } from "store/unknownTags"; +import { bulkCopySelectors } from "store/bulkCopy"; import { ApplicationToolbarToggleGroup, @@ -84,6 +85,7 @@ import { ApplicationAssessment } from "./components/application-assessment"; import { ApplicationBusinessService } from "./components/application-business-service"; import { ApplicationListExpandedArea } from "./components/application-list-expanded-area"; import { ImportApplicationsForm } from "./components/import-applications-form"; +import { BulkCopyAssessmentReviewForm } from "./components/bulk-copy-assessment-review-form"; const toSortByQuery = ( sortBy?: SortByQuery @@ -132,10 +134,15 @@ export const ApplicationList: React.FC = () => { // Redux const dispatch = useDispatch(); + const unknownTagIds = useSelector((state: RootState) => unknownTagsSelectors.unknownTagIds(state) ); + const isWatchingBulkCopy = useSelector((state: RootState) => + bulkCopySelectors.isWatching(state) + ); + // Router const history = useHistory(); @@ -191,7 +198,13 @@ export const ApplicationList: React.FC = () => { useEffect(() => { refreshTable(); - }, [filtersValue, paginationQuery, sortByQuery, refreshTable]); + }, [ + filtersValue, + paginationQuery, + sortByQuery, + isWatchingBulkCopy, + refreshTable, + ]); // Create and update modal const { @@ -224,6 +237,22 @@ export const ApplicationList: React.FC = () => { onDelete: (t: Application) => deleteApplication(t.id!), }); + // Copy assessment modal + const { + isOpen: isCopyAssessmentModalOpen, + data: applicationToCopyAssessmentFrom, + update: openCopyAssessmentModal, + close: closeCopyAssessmentModal, + } = useEntityModal(); + + // Copy assessment and review modal + const { + isOpen: isCopyAssessmentAndReviewModalOpen, + data: applicationToCopyAssessmentAndReviewFrom, + update: openCopyAssessmentAndReviewModal, + close: closeCopyAssessmentAndReviewModal, + } = useEntityModal(); + // Dependencies modal const { isOpen: isDependenciesModalOpen, @@ -388,7 +417,34 @@ export const ApplicationList: React.FC = () => { const actions: (IAction | ISeparator)[] = []; - if (getApplicationAssessment(row.id!)) { + const applicationAssessment = getApplicationAssessment(row.id!); + if (applicationAssessment?.status === "COMPLETE") { + actions.push({ + title: t("actions.copyAssessment"), + onClick: ( + event: React.MouseEvent, + rowIndex: number, + rowData: IRowData + ) => { + const row: Application = getRow(rowData); + openCopyAssessmentModal(row); + }, + }); + } + if (row.review) { + actions.push({ + title: t("actions.copyAssessmentAndReview"), + onClick: ( + event: React.MouseEvent, + rowIndex: number, + rowData: IRowData + ) => { + const row: Application = getRow(rowData); + openCopyAssessmentAndReviewModal(row); + }, + }); + } + if (applicationAssessment) { actions.push({ title: t("actions.discardAssessment"), onClick: ( @@ -650,7 +706,7 @@ export const ApplicationList: React.FC = () => { setFilter={setFilter} /> } - toolbar={ + toolbarActions={ <> @@ -754,6 +810,46 @@ export const ApplicationList: React.FC = () => { /> + + {applicationToCopyAssessmentFrom && ( + + )} + + + {applicationToCopyAssessmentAndReviewFrom && ( + + )} + + { + if (!sortBy) { + return undefined; + } + + let field: ApplicationSortBy; + switch (sortBy.index) { + case 1: + field = ApplicationSortBy.NAME; + break; + default: + return undefined; + } + + return { + field, + direction: sortBy.direction, + }; +}; + +const ENTITY_FIELD = "entity"; + +const getRow = (rowData: IRowData): Application => { + return rowData[ENTITY_FIELD]; +}; + +const searchAppAssessment = (id: number) => { + const result = getAssessments({ applicationId: id }).then(({ data }) => + data[0] ? data[0] : undefined + ); + return result; +}; + +interface BulkCopyAssessmentReviewFormProps { + application: Application; + assessment: Assessment; + review?: Review; + onSaved: () => void; +} + +export const BulkCopyAssessmentReviewForm: React.FC = ({ + application, + assessment, + review, + onSaved, +}) => { + // i18 + const { t } = useTranslation(); + + // Redux + const dispatch = useDispatch(); + + // Local state + const [requestConfirmation, setRequestConfirmation] = useState(false); + const [confirmationAccepted, setConfirmationAccepted] = useState(false); + + const [isSubmitting, setIsSubmitting] = useState(false); + + // Toolbar filters + const { + filters: filtersValue, + isPresent: areFiltersPresent, + addFilter, + setFilter, + clearAllFilters, + } = useToolbarFilter(); + + // Table data + const { + paginationQuery: pagination, + sortByQuery: sortBy, + handlePaginationChange, + handleSortChange, + } = useTableControls({ + sortByQuery: { direction: "asc", index: 1 }, + }); + + const fetchApplications = useCallback(() => { + const nameVal = filtersValue.get(ApplicationFilterKey.NAME); + const descriptionVal = filtersValue.get(ApplicationFilterKey.DESCRIPTION); + const serviceVal = filtersValue.get(ApplicationFilterKey.BUSINESS_SERVICE); + const tagVal = filtersValue.get(ApplicationFilterKey.TAG); + return getApplications( + { + name: nameVal?.map((f) => f.key), + description: descriptionVal?.map((f) => f.key), + businessService: serviceVal?.map((f) => f.key), + tag: tagVal?.map((f) => f.key), + }, + pagination, + toSortByQuery(sortBy) + ); + }, [filtersValue, pagination, sortBy]); + + const { + data: page, + isFetching, + fetchError, + requestFetch: refreshTable, + } = useFetch({ + defaultIsFetching: true, + onFetch: fetchApplications, + }); + + const applications = useMemo(() => { + return page ? applicationPageMapper(page) : undefined; + }, [page]); + + useEffect(() => { + refreshTable(); + }, [filtersValue, pagination, sortBy, refreshTable]); + + // Fetch all applications for SELECT ALL + const fetchAllApplications = useCallback( + (page: number, perPage: number) => { + const nameVal = filtersValue.get(ApplicationFilterKey.NAME); + const descriptionVal = filtersValue.get(ApplicationFilterKey.DESCRIPTION); + const serviceVal = filtersValue.get( + ApplicationFilterKey.BUSINESS_SERVICE + ); + const tagVal = filtersValue.get(ApplicationFilterKey.TAG); + return getApplications( + { + name: nameVal?.map((f) => f.key), + description: descriptionVal?.map((f) => f.key), + businessService: serviceVal?.map((f) => f.key), + tag: tagVal?.map((f) => f.key), + }, + { page, perPage } + ); + }, + [filtersValue] + ); + + const continueFetchingAllAppsIf = useCallback( + (page: ApplicationPage, currentPage: number, currentPageSize: number) => { + return page.total_count > currentPage * currentPageSize; + }, + [] + ); + + const allAppsResponseToArray = useCallback( + (page: ApplicationPage) => applicationPageMapper(page).data, + [] + ); + + const { + data: allApps, + isFetching: isFetchingAllApps, + fetchError: fetchErrorAllApps, + requestFetch: refreshAllApps, + } = useFetchPagination({ + requestFetch: fetchAllApplications, + continueIf: continueFetchingAllAppsIf, + toArray: allAppsResponseToArray, + }); + + useEffect(() => { + refreshAllApps(1, 1000); + }, [filtersValue, refreshAllApps]); + + // Table's assessments + const { + getData: getApplicationAssessment, + isFetching: isFetchingApplicationAssessment, + fetchError: fetchErrorApplicationAssessment, + fetchCount: fetchCountApplicationAssessment, + triggerFetch: fetchApplicationsAssessment, + } = useMultipleFetch({ + onFetchPromise: searchAppAssessment, + }); + + useEffect(() => { + if (applications) { + fetchApplicationsAssessment(applications.data.map((f) => f.id!)); + } + }, [applications, fetchApplicationsAssessment]); + + // Select rows + const { + selectedItems: selectedRows, + areAllSelected: areAllApplicationsSelected, + isItemSelected: isRowSelected, + toggleItemSelected: toggleRowSelected, + setSelectedItems: setSelectedRows, + } = useSelectionFromPageState({ + pageItems: applications?.data || [], + totalItems: applications?.meta.count || 0, + isEqual: (a, b) => a.id === b.id, + }); + + const filterInvalidRows = (rows?: Application[]) => { + return (rows ? rows : []).filter((f) => f.id !== application.id); + }; + + // Table + const columns: ICell[] = [ + { + title: t("terms.name"), + transforms: [sortable, cellWidth(40)], + cellTransforms: [truncate], + }, + { + title: t("terms.businessService"), + transforms: [cellWidth(30)], + cellTransforms: [truncate], + }, + { + title: t("terms.assessment"), + transforms: [cellWidth(15)], + cellTransforms: [truncate], + }, + { + title: t("terms.review"), + transforms: [cellWidth(15)], + cellTransforms: [truncate], + }, + ]; + + const rows: IRow[] = []; + applications?.data.forEach((item) => { + const isSelected = isRowSelected(item); + + rows.push({ + [ENTITY_FIELD]: item, + selected: isSelected, + disableSelection: item.id === application.id, + cells: [ + { + title: item.name, + }, + { + title: ( + <> + {item.businessService && ( + + )} + + ), + }, + { + title: ( + + ), + }, + { + title: item.review ? ( + + ) : ( + + ), + }, + ], + }); + }); + + // Row actions + const selectRow = ( + event: React.FormEvent, + isSelected: boolean, + rowIndex: number, + rowData: IRowData, + extraData: IExtraData + ) => { + const row = getRow(rowData); + toggleRowSelected(row); + }; + + // Confirmation checbox + useEffect(() => { + let selectedAnyAppWithAssessment = selectedRows.some((f) => + getApplicationAssessment(f.id!) + ); + + if (review) { + const selectedAnyAppWithReview = selectedRows.some((f) => f.review); + selectedAnyAppWithAssessment = + selectedAnyAppWithAssessment || selectedAnyAppWithReview; + } + + setRequestConfirmation(selectedAnyAppWithAssessment); + }, [review, selectedRows, getApplicationAssessment]); + + useEffect(() => { + setConfirmationAccepted(false); + }, [requestConfirmation]); + + // Copy + const onSubmit = () => { + if (requestConfirmation && !confirmationAccepted) { + console.log("Accept confirmation to continue"); + return; + } + + setIsSubmitting(true); + + createBulkCopyAssessment({ + fromAssessmentId: assessment.id!, + applications: selectedRows.map((f) => ({ applicationId: f.id! })), + }) + .then((bulkAssessment) => { + const bulkReview = review + ? createBulkCopyReview({ + sourceReview: review!.id!, + targetApplications: selectedRows.map((f) => f.id!), + }) + : undefined; + return Promise.all([bulkAssessment, bulkReview]); + }) + .then(([assessmentBulk, reviewBulk]) => { + setIsSubmitting(false); + + dispatch( + bulkCopyActions.scheduleWatchBulk({ + assessmentBulk: assessmentBulk.data.bulkId!, + reviewBulk: reviewBulk ? reviewBulk.data.id! : undefined, + }) + ); + onSaved(); + }) + .catch((error) => { + setIsSubmitting(false); + + dispatch(alertActions.addDanger(getAxiosErrorMessage(error))); + onSaved(); + }); + }; + + return ( +
+ + + + setSelectedRows([])} + onSelectCurrentPage={() => { + const rows = filterInvalidRows(applications?.data); + setSelectedRows(rows); + }} + onSelectAll={() => { + const rows = filterInvalidRows(allApps); + setSelectedRows(rows); + }} + /> + + } + toolbarToggle={ + } + addFilter={addFilter} + setFilter={setFilter} + /> + } + /> + + + {requestConfirmation && ( + + + + +    + {review + ? t("message.copyAssessmentAndReviewQuestion") + : t("message.copyAssessmentQuestion")} + + } + isStack + > + {review + ? t("message.copyAssessmentAndReviewBody") + : t("message.copyAssessmentBody")} + setConfirmationAccepted(isChecked)} + /> + + )} + + + +
+ ); +}; diff --git a/src/pages/application-inventory/application-list/components/bulk-copy-assessment-review-form/index.tsx b/src/pages/application-inventory/application-list/components/bulk-copy-assessment-review-form/index.tsx new file mode 100644 index 00000000..93a35862 --- /dev/null +++ b/src/pages/application-inventory/application-list/components/bulk-copy-assessment-review-form/index.tsx @@ -0,0 +1 @@ +export { BulkCopyAssessmentReviewForm } from "./bulk-copy-assessment-review-form"; diff --git a/src/pages/application-inventory/manage-imports-details/manage-imports-details.tsx b/src/pages/application-inventory/manage-imports-details/manage-imports-details.tsx index 291325f4..7fd587af 100644 --- a/src/pages/application-inventory/manage-imports-details/manage-imports-details.tsx +++ b/src/pages/application-inventory/manage-imports-details/manage-imports-details.tsx @@ -175,7 +175,7 @@ export const ManageImportsDetails: React.FC = () => { isLoading={isFetching} loadingVariant="skeleton" fetchError={fetchError} - toolbar={ + toolbarActions={ <> diff --git a/src/pages/application-inventory/manage-imports/manage-imports.tsx b/src/pages/application-inventory/manage-imports/manage-imports.tsx index 66a10bfc..592cf116 100644 --- a/src/pages/application-inventory/manage-imports/manage-imports.tsx +++ b/src/pages/application-inventory/manage-imports/manage-imports.tsx @@ -421,7 +421,7 @@ export const ManageImports: React.FC = () => { } - toolbar={ + toolbarActions={ <> diff --git a/src/pages/controls/business-services/business-services.tsx b/src/pages/controls/business-services/business-services.tsx index c314814d..99bee614 100644 --- a/src/pages/controls/business-services/business-services.tsx +++ b/src/pages/controls/business-services/business-services.tsx @@ -370,7 +370,7 @@ export const BusinessServices: React.FC = () => { /> } - toolbar={ + toolbarActions={