From 78ba3b723cee91b5c9e6b046e362649ea76b2f4e Mon Sep 17 00:00:00 2001 From: Shreyas Devalapurkar Date: Fri, 26 Mar 2021 12:10:19 -0700 Subject: [PATCH 1/3] wip --- api/src/models/project-update.ts | 23 +++++++++- api/src/paths/project/{projectId}/update.ts | 43 ++++++++++++++++++- .../queries/project/project-delete-queries.ts | 38 ++++++++++++++++ .../features/projects/view/ProjectDetails.tsx | 2 +- .../view/components/IUCNClassification.tsx | 19 +++++--- 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 api/src/queries/project/project-delete-queries.ts diff --git a/api/src/models/project-update.ts b/api/src/models/project-update.ts index 7bbf2451f3..b8efb82c24 100644 --- a/api/src/models/project-update.ts +++ b/api/src/models/project-update.ts @@ -3,6 +3,25 @@ import { getLogger } from '../utils/logger'; const defaultLog = getLogger('models/project-update'); +export class PutIUCNData { + classificationDetails: IGetPutIUCN[]; + + constructor(obj?: any) { + defaultLog.debug({ label: 'PutIUCNData', message: 'params', obj }); + + this.classificationDetails = + (obj?.classificationDetails?.length && + obj.classificationDetails.map((item: any) => { + return { + classification: item.classification, + subClassification1: item.subClassification1, + subClassification2: item.subClassification2 + }; + })) || + []; + } +} + export class PutProjectData { name: string; type: number; @@ -114,7 +133,7 @@ export class GetPartnershipsData { } } -interface IGetIUCN { +interface IGetPutIUCN { classification: number; subClassification1: number; subClassification2: number; @@ -127,7 +146,7 @@ interface IGetIUCN { * @class GetIUCNClassificationData */ export class GetIUCNClassificationData { - classificationDetails: IGetIUCN[]; + classificationDetails: IGetPutIUCN[]; constructor(iucnClassificationData?: any[]) { defaultLog.debug({ diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index dfd67f6115..2731270a74 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -10,7 +10,8 @@ import { PutCoordinatorData, PutLocationData, PutObjectivesData, - PutProjectData + PutProjectData, + PutIUCNData } from '../../../models/project-update'; import { projectIdResponseObject, @@ -23,9 +24,11 @@ import { getIndigenousPartnershipsByProjectSQL, getIUCNActionClassificationByProjectSQL } from '../../../queries/project/project-update-queries'; +import { deleteIUCNSQL } from '../../../queries/project/project-delete-queries'; import { getStakeholderPartnershipsByProjectSQL } from '../../../queries/project/project-view-update-queries'; import { getLogger } from '../../../utils/logger'; import { logRequest } from '../../../utils/path-utils'; +import { postProjectIUCNSQL } from '../../../queries/project/project-create-queries'; const defaultLog = getLogger('paths/project/{projectId}'); @@ -345,6 +348,10 @@ function updateProject(): RequestHandler { promises.push(updateProjectData(projectId, entities, connection)); } + if (entities?.iucn) { + promises.push(updateProjectIUCNData(projectId, entities, connection)); + } + await Promise.all(promises); await connection.commit(); @@ -359,6 +366,40 @@ function updateProject(): RequestHandler { }; } +export const updateProjectIUCNData = async ( + projectId: number, + entities: IUpdateProject, + connection: IDBConnection +): Promise => { + const putIUCNData = (entities?.iucn && new PutIUCNData(entities.iucn)) || null; + + const sqlDeleteStatement = deleteIUCNSQL(projectId, putIUCNData); + + if (!sqlDeleteStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + const deleteResult = await connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); + + if (!deleteResult || !deleteResult.rowCount) { + throw new HTTP409('Failed to delete project IUCN data'); + } + + putIUCNData?.classificationDetails.forEach(async (iucnClassification) => { + const sqlInsertStatement = postProjectIUCNSQL(iucnClassification.subClassification2, projectId); + + if (!sqlInsertStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + const insertResult = await connection.query(sqlInsertStatement.text, sqlInsertStatement.values); + + if (!insertResult || !insertResult.rowCount) { + throw new HTTP409('Failed to insert project IUCN data'); + } + }); +}; + export const updateProjectData = async ( projectId: number, entities: IUpdateProject, diff --git a/api/src/queries/project/project-delete-queries.ts b/api/src/queries/project/project-delete-queries.ts new file mode 100644 index 0000000000..567b14bae7 --- /dev/null +++ b/api/src/queries/project/project-delete-queries.ts @@ -0,0 +1,38 @@ +import { SQL, SQLStatement } from 'sql-template-strings'; +import { PutIUCNData } from '../../models/project-update'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('queries/project/project-delete-queries'); + +/** + * SQL query to delete project IUCN rows. + * + * @param {(PutIUCNData)} iucn + * @returns {SQLStatement} sql query object + */ +export const deleteIUCNSQL = (projectId: number, iucn: PutIUCNData | null): SQLStatement | null => { + defaultLog.debug({ + label: 'deleteIUCNSQL', + message: 'params', + projectId, + iucn + }); + + const sqlStatement: SQLStatement = SQL` + DELETE + from project_iucn_action_classification + WHERE + p_id = ${projectId} + RETURNING + *; + `; + + defaultLog.debug({ + label: 'deleteProjectSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/app/src/features/projects/view/ProjectDetails.tsx b/app/src/features/projects/view/ProjectDetails.tsx index b819549bfb..f38a0b08df 100644 --- a/app/src/features/projects/view/ProjectDetails.tsx +++ b/app/src/features/projects/view/ProjectDetails.tsx @@ -71,7 +71,7 @@ const ProjectDetails: React.FC = (props) => { - + diff --git a/app/src/features/projects/view/components/IUCNClassification.tsx b/app/src/features/projects/view/components/IUCNClassification.tsx index 51f8172f55..cd791123eb 100644 --- a/app/src/features/projects/view/components/IUCNClassification.tsx +++ b/app/src/features/projects/view/components/IUCNClassification.tsx @@ -26,7 +26,6 @@ import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { Edit } from '@material-ui/icons'; import { APIError } from 'hooks/api/useAxios'; -import { useHistory } from 'react-router'; const useStyles = makeStyles({ table: { @@ -49,6 +48,7 @@ const useStyles = makeStyles({ export interface IIUCNClassificationProps { projectForViewData: IGetProjectForViewResponse; codes: IGetAllCodeSetsResponse; + refresh: () => void; } /** @@ -63,7 +63,6 @@ const IUCNClassification: React.FC = (props) => { } = props; const classes = useStyles(); - const history = useHistory(); const biohubApi = useBiohubApi(); const [errorDialogProps, setErrorDialogProps] = useState({ @@ -112,9 +111,19 @@ const IUCNClassification: React.FC = (props) => { }; const handleDialogEditSave = async (values: IProjectIUCNForm) => { - // make put request from here using values and projectId - setOpenEditDialog(false); - history.push(`/projects/${id}/details`); + const projectData = { iucn: values }; + + try { + await biohubApi.project.updateProject(id, projectData); + } catch (error) { + const apiError = new APIError(error); + showErrorDialog({ dialogText: apiError.message, open: true }); + return; + } finally { + setOpenEditDialog(false); + } + + props.refresh(); }; return ( From 68a7822b6f9b98f8046a42e0c1d3ddd215d3cc75 Mon Sep 17 00:00:00 2001 From: Shreyas Devalapurkar Date: Fri, 26 Mar 2021 12:34:06 -0700 Subject: [PATCH 2/3] editing of iucn --- api/src/paths/project/{projectId}/update.ts | 2 +- .../project/project-delete-queries.test.ts | 17 ++++++ .../queries/project/project-delete-queries.ts | 12 ++-- .../projects/view/ProjectDetails.test.tsx | 2 +- .../components/IUCNClassification.test.tsx | 56 ++++++++++++------- .../components/ProjectCoordinator.test.tsx | 24 ++++++++ 6 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 api/src/queries/project/project-delete-queries.test.ts diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index 2731270a74..57207c65ed 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -373,7 +373,7 @@ export const updateProjectIUCNData = async ( ): Promise => { const putIUCNData = (entities?.iucn && new PutIUCNData(entities.iucn)) || null; - const sqlDeleteStatement = deleteIUCNSQL(projectId, putIUCNData); + const sqlDeleteStatement = deleteIUCNSQL(projectId); if (!sqlDeleteStatement) { throw new HTTP400('Failed to build SQL statement'); diff --git a/api/src/queries/project/project-delete-queries.test.ts b/api/src/queries/project/project-delete-queries.test.ts new file mode 100644 index 0000000000..06eb98eac1 --- /dev/null +++ b/api/src/queries/project/project-delete-queries.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai'; +import { describe } from 'mocha'; +import { deleteIUCNSQL } from './project-delete-queries'; + +describe('deleteIUCNSQL', () => { + it('returns null response when null projectId provided', () => { + const response = deleteIUCNSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId provided', () => { + const response = deleteIUCNSQL(1); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/project/project-delete-queries.ts b/api/src/queries/project/project-delete-queries.ts index 567b14bae7..1d3ad38f48 100644 --- a/api/src/queries/project/project-delete-queries.ts +++ b/api/src/queries/project/project-delete-queries.ts @@ -1,5 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { PutIUCNData } from '../../models/project-update'; import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('queries/project/project-delete-queries'); @@ -7,17 +6,20 @@ const defaultLog = getLogger('queries/project/project-delete-queries'); /** * SQL query to delete project IUCN rows. * - * @param {(PutIUCNData)} iucn + * @param {projectId} projectId * @returns {SQLStatement} sql query object */ -export const deleteIUCNSQL = (projectId: number, iucn: PutIUCNData | null): SQLStatement | null => { +export const deleteIUCNSQL = (projectId: number): SQLStatement | null => { defaultLog.debug({ label: 'deleteIUCNSQL', message: 'params', - projectId, - iucn + projectId }); + if (!projectId) { + return null; + } + const sqlStatement: SQLStatement = SQL` DELETE from project_iucn_action_classification diff --git a/app/src/features/projects/view/ProjectDetails.test.tsx b/app/src/features/projects/view/ProjectDetails.test.tsx index 8af59f2cf8..eaff064ffd 100644 --- a/app/src/features/projects/view/ProjectDetails.test.tsx +++ b/app/src/features/projects/view/ProjectDetails.test.tsx @@ -24,7 +24,7 @@ describe('ProjectDetails', () => { const { asFragment } = render( - + ); diff --git a/app/src/features/projects/view/components/IUCNClassification.test.tsx b/app/src/features/projects/view/components/IUCNClassification.test.tsx index 788c0d289d..0eec7d606a 100644 --- a/app/src/features/projects/view/components/IUCNClassification.test.tsx +++ b/app/src/features/projects/view/components/IUCNClassification.test.tsx @@ -2,17 +2,15 @@ import { render, cleanup, waitFor, fireEvent } from '@testing-library/react'; import { getProjectForViewResponse } from 'test-helpers/project-helpers'; import React from 'react'; import IUCNClassification from './IUCNClassification'; -import { createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; import { codes } from 'test-helpers/code-helpers'; import { useBiohubApi } from 'hooks/useBioHubApi'; - -const history = createMemoryHistory(); +import { UPDATE_GET_ENTITIES } from 'interfaces/useProjectApi.interface'; jest.mock('../../../../hooks/useBioHubApi'); const mockUseBiohubApi = { project: { - getProjectForUpdate: jest.fn, []>() + getProjectForUpdate: jest.fn, []>(), + updateProject: jest.fn() } }; @@ -20,10 +18,19 @@ const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock { + return render( + + ); +}; + describe('IUCNClassification', () => { beforeEach(() => { // clear mocks before each test mockBiohubApi().project.getProjectForUpdate.mockClear(); + mockBiohubApi().project.updateProject.mockClear(); }); afterEach(() => { @@ -31,11 +38,7 @@ describe('IUCNClassification', () => { }); it('renders correctly', () => { - const { asFragment } = render( - - - - ); + const { asFragment } = renderContainer(); expect(asFragment()).toMatchSnapshot(); }); @@ -53,11 +56,7 @@ describe('IUCNClassification', () => { } }); - const { getByText } = render( - - - - ); + const { getByText } = renderContainer(); await waitFor(() => { expect(getByText('IUCN Classification')).toBeVisible(); @@ -65,6 +64,12 @@ describe('IUCNClassification', () => { fireEvent.click(getByText('EDIT')); + await waitFor(() => { + expect(mockBiohubApi().project.getProjectForUpdate).toBeCalledWith(getProjectForViewResponse.id, [ + UPDATE_GET_ENTITIES.iucn + ]); + }); + await waitFor(() => { expect(getByText('Edit IUCN Classification')).toBeVisible(); }); @@ -84,7 +89,20 @@ describe('IUCNClassification', () => { fireEvent.click(getByText('Save Changes')); await waitFor(() => { - expect(history.location.pathname).toEqual(`/projects/${getProjectForViewResponse.id}/details`); + expect(mockBiohubApi().project.updateProject).toHaveBeenCalledTimes(1); + expect(mockBiohubApi().project.updateProject).toBeCalledWith(getProjectForViewResponse.id, { + iucn: { + classificationDetails: [ + { + classification: 1, + subClassification1: 1, + subClassification2: 1 + } + ] + } + }); + + expect(mockRefresh).toBeCalledTimes(1); }); }); @@ -93,11 +111,7 @@ describe('IUCNClassification', () => { iucn: null }); - const { getByText } = render( - - - - ); + const { getByText } = renderContainer(); await waitFor(() => { expect(getByText('IUCN Classification')).toBeVisible(); diff --git a/app/src/features/projects/view/components/ProjectCoordinator.test.tsx b/app/src/features/projects/view/components/ProjectCoordinator.test.tsx index e1b0ea3678..69151cb504 100644 --- a/app/src/features/projects/view/components/ProjectCoordinator.test.tsx +++ b/app/src/features/projects/view/components/ProjectCoordinator.test.tsx @@ -103,4 +103,28 @@ describe('ProjectCoordinator', () => { expect(mockRefresh).toBeCalledTimes(1); }); }); + + it('displays an error dialog when fetching the update data fails', async () => { + mockBiohubApi().project.getProjectForUpdate.mockResolvedValue({ + coordinator: undefined + }); + + const { getByText } = renderContainer(); + + await waitFor(() => { + expect(getByText('Project Coordinator')).toBeVisible(); + }); + + fireEvent.click(getByText('EDIT')); + + await waitFor(() => { + expect(getByText('Error Editing Project Coordinator')).toBeVisible(); + }); + + fireEvent.click(getByText('Ok')); + + await waitFor(() => { + expect(getByText('Error Editing Project Coordinator')).not.toBeVisible(); + }); + }); }); From c6a8221142c077a18b39ff7ef08962d5b900628b Mon Sep 17 00:00:00 2001 From: Shreyas Devalapurkar Date: Fri, 26 Mar 2021 14:13:21 -0700 Subject: [PATCH 3/3] fix --- .../projects/components/ProjectIUCNForm.tsx | 13 ++++++------- app/src/themes/appTheme.ts | 3 +-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/features/projects/components/ProjectIUCNForm.tsx b/app/src/features/projects/components/ProjectIUCNForm.tsx index 3511121e88..6d5cc16343 100644 --- a/app/src/features/projects/components/ProjectIUCNForm.tsx +++ b/app/src/features/projects/components/ProjectIUCNForm.tsx @@ -23,9 +23,8 @@ const useStyles = makeStyles((theme) => ({ overflowX: 'hidden' }, iucnInput: { - flex: '1 0 200px', - width: '33.333%', - maxWidth: '200px' + flex: '0 0 auto', + width: '33.333%' } })); @@ -98,8 +97,8 @@ const ProjectIUCNForm: React.FC = (props) => { return ( - - + + Classification = (props) => { {subClassification1Meta.error} - + Sub-classification