From 86d4198c9b482f7623a7df5f7362e90209273d88 Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:32:18 -0800 Subject: [PATCH 1/6] psp-9510 hotfix for npe when acquisition subfile does not have an interest. (#4470) Co-authored-by: Smith --- source/backend/api/Pims.Api.csproj | 6 +++--- source/frontend/package.json | 2 +- .../acquisition/tabs/fileDetails/detail/models.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index bb56d99c2a..ca82fcf9d3 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,9 +2,9 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.6.0-92.37 - 5.6.0-92.37 - 5.6.0.92 + 5.6.1-92.37 + 5.6.1-92.37 + 5.6.1.92 true 16BC0468-78F6-4C91-87DA-7403C919E646 net8.0 diff --git a/source/frontend/package.json b/source/frontend/package.json index 543498b489..0be52c56eb 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.6.0-92.37", + "version": "5.6.1-92.37", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/models.ts b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/models.ts index c2395cc355..f895d05cb9 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/models.ts +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/models.ts @@ -34,7 +34,7 @@ export class DetailAcquisitionFile { detail.acquisitionTypeDescription = model?.acquisitionTypeCode?.description ?? undefined; if (detail.isSubFile) { - if (model?.subfileInterestTypeCode.id === ApiGen_CodeTypes_SubfileInterestTypes.OTHER) { + if (model?.subfileInterestTypeCode?.id === ApiGen_CodeTypes_SubfileInterestTypes.OTHER) { detail.subfileInterestTypeDescription = `Other-${model.otherSubfileInterestType ?? ''}`; } else { detail.subfileInterestTypeDescription = model?.subfileInterestTypeCode?.description ?? ''; From 5eca8974b02e94b1b22e1e77f31cbb417732827d Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:04:46 -0800 Subject: [PATCH 2/6] psp-9649 ensure that new map markers display immediately, and that map refresh also triggers the advanced filter refresh. (#4476) --- .../components/common/mapFSM/machineDefinition/mapMachine.ts | 3 +-- source/frontend/src/features/properties/map/MapContainer.tsx | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index 57033e12f7..4cd049e150 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -397,10 +397,9 @@ const advancedFilterSideBarStates = { actions: [ send({ type: 'REFRESH_PROPERTIES' }), assign({ - isFiltering: () => false, showDisposed: () => false, showRetired: () => false, - activePimsPropertyIds: () => [], + advancedSearchCriteria: () => new PropertyFilterFormModel(), }), ], }, diff --git a/source/frontend/src/features/properties/map/MapContainer.tsx b/source/frontend/src/features/properties/map/MapContainer.tsx index 9c5aec82e5..601fdbc96f 100644 --- a/source/frontend/src/features/properties/map/MapContainer.tsx +++ b/source/frontend/src/features/properties/map/MapContainer.tsx @@ -37,6 +37,7 @@ const MapContainer: React.FC> = () => setVisiblePimsProperties, advancedSearchCriteria, isMapVisible, + isLoading, } = useMapStateMachine(); const { getMatchingProperties } = usePimsPropertyRepository(); @@ -58,7 +59,7 @@ const MapContainer: React.FC> = () => useEffect(() => { filterProperties(advancedSearchCriteria?.toApi()); - }, [filterProperties, advancedSearchCriteria]); + }, [filterProperties, advancedSearchCriteria, isLoading]); const cursorClass = isSelecting ? MapCursors.DRAFT From c7c4cd6a87763977ae862113c2f704eb14cb2a50 Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:58:14 -0800 Subject: [PATCH 3/6] psp-9436 - remove restriction on take date such that db layer logic corresponds to api logic. (#4474) Co-authored-by: Alejandro Sanchez --- source/backend/dal/Repositories/PropertyRepository.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/source/backend/dal/Repositories/PropertyRepository.cs b/source/backend/dal/Repositories/PropertyRepository.cs index 364117144a..8e176ebe73 100644 --- a/source/backend/dal/Repositories/PropertyRepository.cs +++ b/source/backend/dal/Repositories/PropertyRepository.cs @@ -532,11 +532,10 @@ public HashSet GetMatchingIds(PropertyFilterCriteria filter) } if (filter.IsOtherInterest) { - var today = DateOnly.FromDateTime(DateTime.Now); - ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewLandAct && authorizationTypes.Contains(t.LandActTypeCode) && t.LandActEndDt >= today))); - ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewInterestInSrw && t.SrwEndDt >= today))); - ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewLicenseToConstruct && t.LtcEndDt >= today))); - ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsActiveLease && t.ActiveLeaseEndDt >= today))); + ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewLandAct && authorizationTypes.Contains(t.LandActTypeCode)))); + ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewInterestInSrw))); + ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsNewLicenseToConstruct))); + ownershipBuilder.Or(p => p.PimsPropertyAcquisitionFiles.Any(x => x.PimsTakes.Any(t => t.TakeStatusTypeCode == "COMPLETE" && t.IsActiveLease))); } if (filter.IsDisposed) { From fc4af60bb063e415b8231b4db7a809d6373255fe Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:27:53 -0800 Subject: [PATCH 4/6] HOTFIX: Psp 9505 - ensure document metadata reset when document type changed. (#4477) * psp-9436 - remove restriction on take date such that db layer logic corresponds to api logic. * psp-9505 reset the current documents metadata when the metadata type is changed. --------- Co-authored-by: Alejandro Sanchez --- .../src/features/documents/ComposedDocument.ts | 16 +++++++++------- .../documentDetail/DocumentDetailContainer.tsx | 1 + .../documentDetail/DocumentDetailForm.tsx | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/source/frontend/src/features/documents/ComposedDocument.ts b/source/frontend/src/features/documents/ComposedDocument.ts index df96e6eead..77b785179e 100644 --- a/source/frontend/src/features/documents/ComposedDocument.ts +++ b/source/frontend/src/features/documents/ComposedDocument.ts @@ -221,11 +221,13 @@ export class DocumentUpdateFormData { return model; } - public toRequestApi(): ApiGen_Requests_DocumentUpdateRequest { + public static toRequestApi( + formData: DocumentUpdateFormData, + ): ApiGen_Requests_DocumentUpdateRequest { const metadata: ApiGen_Concepts_DocumentMetadataUpdate[] = []; - for (const key in this.documentMetadata) { - const value = this.documentMetadata[key]; + for (const key in formData.documentMetadata) { + const value = formData.documentMetadata[key]; const metadataTypeId = Number(key); metadata.push({ value: value, @@ -235,10 +237,10 @@ export class DocumentUpdateFormData { } return { - documentId: this.documentId, - mayanDocumentId: this.mayanDocumentId, - documentTypeId: stringToNumber(this.documentTypeId), - documentStatusCode: this.documentStatusCode, + documentId: formData.documentId, + mayanDocumentId: formData.mayanDocumentId, + documentTypeId: stringToNumber(formData.documentTypeId), + documentStatusCode: formData.documentStatusCode, documentMetadata: metadata, }; } diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx index d94654b3cc..9003295a1d 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx @@ -160,6 +160,7 @@ export const DocumentDetailContainer: React.FunctionComponent< if (documentTypeId !== props.pimsDocument.documentType.id) { await updateDocumentType(documentTypes.find(x => x.id === documentTypeId)); setDocumentTypeUpdated(true); + formikRef?.current?.setValues({ ...formikRef?.current?.values, documentMetadata: {} }); } else { setDocumentTypeUpdated(false); } diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.tsx index 75ec9a04c0..7556bc93f9 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.tsx @@ -117,7 +117,7 @@ export const DocumentDetailForm: React.FunctionComponent< props.document?.pimsDocumentRelationship?.id && values.documentStatusCode !== undefined ) { - const request = values.toRequestApi(); + const request = DocumentUpdateFormData.toRequestApi(values); await props.onUpdate(request); setSubmitting(false); } else { From 80025010b9a0946837ab1c15520530ef9b8e56dd Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:31:10 -0800 Subject: [PATCH 5/6] HOTFIX: 9469 Set the map bounds correctly - on startup. (#4484) * Set the map bounds correctly - on startup. * add explicit check for isMapReady. * snapshot updates. * update logic to support unit tests. --- source/frontend/src/components/maps/MapLeafletView.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index 17b176d487..40bb1d3abd 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -1,4 +1,5 @@ import axios from 'axios'; +import { dequal } from 'dequal'; import { Feature, GeoJsonProperties, Geometry } from 'geojson'; import { geoJSON, @@ -87,6 +88,14 @@ const MapLeafletView: React.FC> = ( const requestedFlyTo = mapMachine.requestedFlyTo; const mapMachineProcessFlyTo = mapMachine.processFlyTo; + // Set the bounds when the map is ready. Not called from existing handleMapCreated as that function is called every time a state change occurs. + useEffect(() => { + const bounds = mapRef?.current?.getBounds(); + if (exists(bounds) && isMapReady && !dequal(bounds.getNorthEast(), bounds.getSouthWest())) { + setBounds(bounds); + } + }, [isMapReady, setBounds]); + useEffect(() => { if (isMapReady && mapMachinePendingRefresh && mapRef.current !== null) { // PSP-9347 it is possible that a fit bounds request will be made with an empty array of selected properties. In that case, we do not want to change the screen bounds, so cancel the request with no changes to the map. From d5019dbd6d2652955b5c18e8e128b3e64d94ae90 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 18 Nov 2024 14:29:19 -0800 Subject: [PATCH 6/6] HOTFIX: PSP-9516 Unexpected behaviors when changing document type of a digital document (#4485) * HOTFIX: PSP-9516 fix faulty logic when changing document type * Test updates * Increment hotfix version * Increase code coverage --- source/backend/api/Pims.Api.csproj | 7 +- source/frontend/package-lock.json | 4 +- source/frontend/package.json | 2 +- .../DocumentDetailContainer.test.tsx | 233 ++++++++ .../DocumentDetailContainer.tsx | 75 +-- .../DocumentDetailForm.test.tsx | 10 +- .../documentDetail/DocumentDetailView.tsx | 1 + .../DocumentDetailContainer.test.tsx.snap | 501 ++++++++++++++++++ .../DocumentDetailForm.test.tsx.snap | 2 +- .../DocumentDetailView.test.tsx.snap | 1 + .../DocumentResults.test.tsx.snap | 2 +- .../DocumentListView.test.tsx.snap | 2 +- source/frontend/src/mocks/documents.mock.ts | 163 +++++- 13 files changed, 931 insertions(+), 72 deletions(-) create mode 100644 source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.test.tsx create mode 100644 source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailContainer.test.tsx.snap diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index ca82fcf9d3..00a44e2464 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,9 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.6.1-92.37 - 5.6.1-92.37 - 5.6.1.92 + 5.6.2-92.37 + 5.6.2.92 true 16BC0468-78F6-4C91-87DA-7403C919E646 net8.0 @@ -87,4 +86,4 @@ - + \ No newline at end of file diff --git a/source/frontend/package-lock.json b/source/frontend/package-lock.json index db36bcc650..dada637f63 100644 --- a/source/frontend/package-lock.json +++ b/source/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "5.6.0-92.0", + "version": "5.6.2-92.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "5.6.0-92.0", + "version": "5.6.2-92.37", "dependencies": { "@bcgov/bc-sans": "1.0.1", "@bcgov/design-tokens": "3.0.0-rc1", diff --git a/source/frontend/package.json b/source/frontend/package.json index 0be52c56eb..1f0e814bb6 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.6.1-92.37", + "version": "5.6.2-92.37", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.test.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.test.tsx new file mode 100644 index 0000000000..6db36a5b65 --- /dev/null +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.test.tsx @@ -0,0 +1,233 @@ +import { createMemoryHistory } from 'history'; + +import { Claims } from '@/constants'; +import { useDocumentProvider } from '@/features/documents/hooks/useDocumentProvider'; +import { + mockDocumentResponse, + mockDocumentTypeMetadataBcAssessment, + mockDocumentTypesAcquisition, + mockDocumentTypesAll, +} from '@/mocks/documents.mock'; +import { mockLookups } from '@/mocks/lookups.mock'; +import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; +import { ApiGen_CodeTypes_ExternalResponseStatus } from '@/models/api/generated/ApiGen_CodeTypes_ExternalResponseStatus'; +import { ApiGen_System_HttpStatusCode } from '@/models/api/generated/ApiGen_System_HttpStatusCode'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { act, getByName, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; + +import { DocumentRow } from '../ComposedDocument'; +import { DocumentDetailContainer, IDocumentDetailContainerProps } from './DocumentDetailContainer'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const onUpdateSuccess = vi.fn(); + +const mockDocumentApi = { + retrieveDocumentMetadata: vi.fn(), + retrieveDocumentMetadataLoading: false, + downloadWrappedDocumentFile: vi.fn(), + downloadWrappedDocumentFileLoading: false, + downloadWrappedDocumentFileLatest: vi.fn(), + downloadWrappedDocumentFileLatestLoading: false, + downloadWrappedDocumentFileLatestResponse: null, + streamDocumentFile: vi.fn(), + streamDocumentFileLoading: false, + streamDocumentFileLatest: vi.fn(), + streamDocumentFileLatestLoading: false, + streamDocumentFileLatestResponse: null, + retrieveDocumentTypeMetadata: vi.fn(), + retrieveDocumentTypeMetadataLoading: false, + getDocumentTypes: vi.fn(), + getDocumentTypesLoading: false, + getDocumentRelationshipTypes: vi.fn(), + getDocumentRelationshipTypesLoading: false, + retrieveDocumentDetail: vi.fn(), + retrieveDocumentDetailLoading: false, + updateDocument: vi.fn(), + updateDocumentLoading: false, + downloadDocumentFilePageImage: vi.fn(), + downloadDocumentFilePageImageLoading: false, + getDocumentFilePageList: vi.fn(), + getDocumentFilePageListLoading: false, +}; + +vi.mock('@/features/documents/hooks/useDocumentProvider'); +vi.mocked(useDocumentProvider).mockReturnValue(mockDocumentApi); + +describe('DocumentDetailContainer component', () => { + // render component under test + const setup = ( + renderOptions: RenderOptions & { props?: Partial } = {}, + ) => { + const utils = render( + , + { + ...renderOptions, + store: storeState, + history, + claims: renderOptions.claims ?? [Claims.DOCUMENT_VIEW, Claims.DOCUMENT_EDIT], + }, + ); + + return { + ...utils, + getDocumentTypeDropdown: () => getByName('documentTypeId') as HTMLSelectElement, + }; + }; + + beforeEach(() => { + mockDocumentApi.getDocumentRelationshipTypes.mockResolvedValue(mockDocumentTypesAcquisition()); + mockDocumentApi.getDocumentTypes.mockResolvedValue(mockDocumentTypesAll()); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders the underlying form', async () => { + const { asFragment } = setup(); + + await act(async () => {}); + expect(screen.getByText(/Document Information/i)).toBeVisible(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('should call the api to fetch document types when in Edit mode', async () => { + setup(); + + await act(async () => {}); + await act(async () => { + userEvent.click(screen.getByTitle(/Edit document information/i)); + }); + expect(mockDocumentApi.getDocumentRelationshipTypes).toHaveBeenCalled(); + }); + + it('should call the api to fetch document types for CDOGS templates', async () => { + setup({ props: { relationshipType: ApiGen_CodeTypes_DocumentRelationType.Templates } }); + + await act(async () => {}); + await act(async () => { + userEvent.click(screen.getByTitle(/Edit document information/i)); + }); + expect(mockDocumentApi.getDocumentTypes).toHaveBeenCalled(); + }); + + it('should call the api to fetch mayan document type metadata', async () => { + mockDocumentApi.retrieveDocumentTypeMetadata.mockResolvedValue({ + status: ApiGen_CodeTypes_ExternalResponseStatus.Success, + message: null, + payload: { + results: mockDocumentTypeMetadataBcAssessment(), + }, + httpStatusCode: ApiGen_System_HttpStatusCode.OK, + }); + + setup(); + + await act(async () => {}); + expect(mockDocumentApi.retrieveDocumentTypeMetadata).toHaveBeenCalled(); + }); + + it('should fetch additional document child entities upon rendering this component', async () => { + mockDocumentApi.retrieveDocumentMetadata.mockResolvedValue({ + status: ApiGen_CodeTypes_ExternalResponseStatus.Success, + message: null, + payload: { + results: [], + }, + httpStatusCode: ApiGen_System_HttpStatusCode.OK, + }); + mockDocumentApi.retrieveDocumentDetail.mockResolvedValue({ + status: ApiGen_CodeTypes_ExternalResponseStatus.Success, + message: null, + payload: { + id: 1, + label: null, + datetime_created: new Date().toISOString(), + description: null, + uuid: null, + file_latest: { id: 1 }, + document_type: null, + }, + httpStatusCode: ApiGen_System_HttpStatusCode.OK, + }); + + setup(); + + await act(async () => {}); + expect(mockDocumentApi.retrieveDocumentMetadata).toHaveBeenCalled(); + expect(mockDocumentApi.retrieveDocumentDetail).toHaveBeenCalled(); + }); + + it('should refresh the document type metadata when document type is changed', async () => { + mockDocumentApi.retrieveDocumentTypeMetadata.mockResolvedValue({ + status: ApiGen_CodeTypes_ExternalResponseStatus.Success, + message: null, + payload: { + results: mockDocumentTypeMetadataBcAssessment(), + }, + httpStatusCode: ApiGen_System_HttpStatusCode.OK, + }); + vi.spyOn(console, 'error').mockImplementationOnce(() => {}); + + const { getDocumentTypeDropdown } = setup(); + + await act(async () => {}); + expect(mockDocumentApi.retrieveDocumentTypeMetadata).toHaveBeenCalled(); + await act(async () => { + userEvent.click(screen.getByTitle(/Edit document information/i)); + }); + + const documentType = getDocumentTypeDropdown(); + expect(documentType).toBeVisible(); + await act(async () => { + userEvent.selectOptions(documentType, '17'); + }); + + // document type metadata should be refreshed + expect(mockDocumentApi.retrieveDocumentTypeMetadata).toHaveBeenCalled(); + }); + + it('should submit the form as expected', async () => { + mockDocumentApi.retrieveDocumentTypeMetadata.mockResolvedValue({ + status: ApiGen_CodeTypes_ExternalResponseStatus.Success, + message: null, + payload: { + results: mockDocumentTypeMetadataBcAssessment(), + }, + httpStatusCode: ApiGen_System_HttpStatusCode.OK, + }); + mockDocumentApi.updateDocument.mockResolvedValue({ + metadataExternalResponse: [], + }); + + setup(); + + await act(async () => {}); + expect(mockDocumentApi.retrieveDocumentTypeMetadata).toHaveBeenCalled(); + await act(async () => { + userEvent.click(screen.getByTitle(/Edit document information/i)); + }); + + const yesButton = screen.getByText('Yes'); + expect(yesButton).toBeVisible(); + await act(async () => { + userEvent.click(yesButton); + }); + + // document update was called + expect(onUpdateSuccess).toHaveBeenCalled(); + }); +}); diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx index 9003295a1d..b52e9802b3 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailContainer.tsx @@ -4,7 +4,6 @@ import { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useSt import { ModalProps } from '@/components/common/GenericModal'; import { DocumentTypeName } from '@/constants/documentType'; import { ModalContext } from '@/contexts/modalContext'; -import { useApiDocuments } from '@/hooks/pims-api/useApiDocuments'; import { getCancelModalProps } from '@/hooks/useModalContext'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import useIsMounted from '@/hooks/util/useIsMounted'; @@ -13,7 +12,7 @@ import { ApiGen_CodeTypes_ExternalResponseStatus } from '@/models/api/generated/ import { ApiGen_Concepts_DocumentType } from '@/models/api/generated/ApiGen_Concepts_DocumentType'; import { ApiGen_Mayan_DocumentTypeMetadataType } from '@/models/api/generated/ApiGen_Mayan_DocumentTypeMetadataType'; import { ApiGen_Requests_DocumentUpdateRequest } from '@/models/api/generated/ApiGen_Requests_DocumentUpdateRequest'; -import { exists } from '@/utils/utils'; +import { exists, isValidId } from '@/utils/utils'; import { ComposedDocument, DocumentRow, DocumentUpdateFormData } from '../ComposedDocument'; import { useDocumentProvider } from '../hooks/useDocumentProvider'; @@ -50,7 +49,6 @@ export const DocumentDetailContainer: React.FunctionComponent< getDocumentRelationshipTypes, retrieveDocumentTypeMetadata, } = useDocumentProvider(); - const { getDocumentTypeMetadataApiCall } = useApiDocuments(); const formikRef = useRef>(null); @@ -94,6 +92,7 @@ export const DocumentDetailContainer: React.FunctionComponent< } }; + // callback to fetch available document types for a given context (i.e. Acquisition File, Research File, etc) const fetchDocumentTypes = useCallback(async () => { if (props.relationshipType === ApiGen_CodeTypes_DocumentRelationType.Templates) { const response = await getDocumentTypes(); @@ -108,57 +107,39 @@ export const DocumentDetailContainer: React.FunctionComponent< } }, [getDocumentRelationshipTypes, getDocumentTypes, isMounted, props.relationshipType]); + // callback to fetch document type metadata from mayan API endpoints const getDocumentMetadata = useCallback( async (documentType?: ApiGen_Concepts_DocumentType) => { if ( - documentType === undefined || + !exists(documentType) || props.relationshipType === ApiGen_CodeTypes_DocumentRelationType.Templates ) { return; } - if (documentTypeMetadataTypes[documentType.id.toString()] === undefined) { - if (documentType.mayanId) { - const results = await retrieveDocumentTypeMetadata(documentType.mayanId); - - if (results?.status === ApiGen_CodeTypes_ExternalResponseStatus.Success) { - setDocumentTypeMetadataTypes(prevState => ({ - ...prevState, - [documentType.id.toString()]: results.payload.results, - })); - return results.payload.results; - } - } else { - console.error('Document type does not have a mayan id type'); + if (isValidId(documentType.mayanId)) { + const retrievedMetadata = await retrieveDocumentTypeMetadata(documentType.mayanId); + if ( + retrievedMetadata?.status === ApiGen_CodeTypes_ExternalResponseStatus.Success && + isMounted() + ) { + setDocumentTypeMetadataTypes(retrievedMetadata?.payload?.results ?? []); } } else { - return documentTypeMetadataTypes[documentType.id.toString()]; - } - }, - [documentTypeMetadataTypes, props.relationshipType, retrieveDocumentTypeMetadata], - ); - - const updateDocumentType = useCallback( - async (documentType?: ApiGen_Concepts_DocumentType) => { - if (!exists(documentType)) { - return; - } - if (documentType.mayanId) { - const retrievedMetadata = await getDocumentMetadata(documentType); - if (exists(retrievedMetadata)) { - setDocumentTypeMetadataTypes(retrievedMetadata); - } + console.error('Document type does not have a mayan id type'); } }, - [getDocumentMetadata], + [isMounted, props.relationshipType, retrieveDocumentTypeMetadata], ); + // refresh document type metadata fields whenever a new document type is selected const onDocumentTypeChange = useCallback( async (changeEvent: ChangeEvent) => { - if (changeEvent.target.value) { + if (exists(changeEvent.target.value)) { const documentTypeId = Number(changeEvent.target.value); + await getDocumentMetadata(documentTypes.find(x => x.id === documentTypeId)); + if (documentTypeId !== props.pimsDocument.documentType.id) { - await updateDocumentType(documentTypes.find(x => x.id === documentTypeId)); setDocumentTypeUpdated(true); formikRef?.current?.setValues({ ...formikRef?.current?.values, documentMetadata: {} }); } else { @@ -169,9 +150,10 @@ export const DocumentDetailContainer: React.FunctionComponent< setDocumentTypeUpdated(false); } }, - [documentTypes, props.pimsDocument.documentType.id, updateDocumentType], + [documentTypes, props.pimsDocument.documentType.id, getDocumentMetadata], ); + // fetch additional document child entities (like metadata and document details) upon rendering this component useEffect(() => { const fetch = async () => { if (props.pimsDocument.mayanDocumentId !== undefined) { @@ -212,6 +194,7 @@ export const DocumentDetailContainer: React.FunctionComponent< retrieveDocumentDetail, ]); + // fetch document type metadata upon rendering this component useEffect(() => { const fetch = async () => { if (props.relationshipType === ApiGen_CodeTypes_DocumentRelationType.Templates) { @@ -219,28 +202,18 @@ export const DocumentDetailContainer: React.FunctionComponent< } if (props.pimsDocument.mayanDocumentId !== undefined) { - const mayanDocumentTypeId = props.pimsDocument.documentType?.mayanId; - if (mayanDocumentTypeId) { - const axiosResponse = await getDocumentTypeMetadataApiCall(mayanDocumentTypeId); - if ( - axiosResponse?.data.status === ApiGen_CodeTypes_ExternalResponseStatus.Success && - isMounted() - ) { - const results = axiosResponse?.data.payload?.results; - setDocumentTypeMetadataTypes(results || []); - } - } + await getDocumentMetadata(props.pimsDocument.documentType); } }; fetch(); }, [ + getDocumentMetadata, + props.pimsDocument.documentType, props.pimsDocument.mayanDocumentId, - props.pimsDocument.documentType?.mayanId, - getDocumentTypeMetadataApiCall, - isMounted, props.relationshipType, ]); + // fetch document types upon entering in Edit mode useEffect(() => { if (isEditable) { fetchDocumentTypes(); diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx index 204e0cad4a..6dfe6eff72 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx @@ -1,4 +1,6 @@ +import { FormikProps } from 'formik'; import { createMemoryHistory } from 'history'; +import { createRef } from 'react'; import { Claims } from '@/constants/claims'; import { mockDocumentTypesAcquisition } from '@/mocks/documents.mock'; @@ -14,8 +16,6 @@ import { mockKeycloak, render, RenderOptions } from '@/utils/test-utils'; import { ComposedDocument, DocumentUpdateFormData } from '../ComposedDocument'; import { DocumentDetailForm, IDocumentDetailFormProps } from './DocumentDetailForm'; -import { FormikProps } from 'formik'; -import { createRef } from 'react'; // mock auth library @@ -245,14 +245,14 @@ describe('DocumentDetailForm component', () => { expect(select).toBeDisabled(); }); - it('displays a warning leyend when the document type has changed.', async () => { + it('displays a warning legend when the document type has changed.', async () => { const { getByText } = await setup({ props: { documentTypeUpdated: true }, }); - const warningLeyend = getByText( + const warningLegend = getByText( 'Some associated metadata may be lost if the document type is changed.', ); - expect(warningLeyend).toBeInTheDocument(); + expect(warningLegend).toBeInTheDocument(); }); }); diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.tsx index cc5afbdb34..08d453f882 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.tsx @@ -58,6 +58,7 @@ export const DocumentDetailView: React.FunctionComponent< { props.setIsEditable(true); }} diff --git a/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailContainer.test.tsx.snap b/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailContainer.test.tsx.snap new file mode 100644 index 0000000000..5d2e14d368 --- /dev/null +++ b/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailContainer.test.tsx.snap @@ -0,0 +1,501 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DocumentDetailContainer component > renders the underlying form 1`] = ` + +
+
+ .c7.btn { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.4rem 1.2rem; + border: 0.2rem solid transparent; + border-radius: 0.4rem; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-size: 1.8rem; + font-family: 'BCSans','Noto Sans',Verdana,Arial,sans-serif; + font-weight: 700; + -webkit-letter-spacing: 0.1rem; + -moz-letter-spacing: 0.1rem; + -ms-letter-spacing: 0.1rem; + letter-spacing: 0.1rem; + cursor: pointer; +} + +.c7.btn .Button__value { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.c7.btn:hover { + -webkit-text-decoration: underline; + text-decoration: underline; + opacity: 0.8; +} + +.c7.btn:focus { + outline-width: 2px; + outline-style: solid; + outline-color: #2E5DD7; + outline-offset: 2px; + box-shadow: none; +} + +.c7.btn.btn-primary { + color: #FFFFFF; + background-color: #013366; +} + +.c7.btn.btn-primary:hover, +.c7.btn.btn-primary:active, +.c7.btn.btn-primary:focus { + background-color: #1E5189; +} + +.c7.btn.btn-secondary { + color: #013366; + background: none; + border-color: #013366; +} + +.c7.btn.btn-secondary:hover, +.c7.btn.btn-secondary:active, +.c7.btn.btn-secondary:focus { + color: #FFFFFF; + background-color: #013366; +} + +.c7.btn.btn-info { + color: #9F9D9C; + border: none; + background: none; + padding-left: 0.6rem; + padding-right: 0.6rem; +} + +.c7.btn.btn-info:hover, +.c7.btn.btn-info:active, +.c7.btn.btn-info:focus { + color: var(--surface-color-primary-button-hover); + background: none; +} + +.c7.btn.btn-light { + color: #FFFFFF; + background-color: #606060; + border: none; +} + +.c7.btn.btn-light:hover, +.c7.btn.btn-light:active, +.c7.btn.btn-light:focus { + color: #FFFFFF; + background-color: #606060; +} + +.c7.btn.btn-dark { + color: #FFFFFF; + background-color: #474543; + border: none; +} + +.c7.btn.btn-dark:hover, +.c7.btn.btn-dark:active, +.c7.btn.btn-dark:focus { + color: #FFFFFF; + background-color: #474543; +} + +.c7.btn.btn-danger { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c7.btn.btn-danger:hover, +.c7.btn.btn-danger:active, +.c7.btn.btn-danger:focus { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c7.btn.btn-warning { + color: #FFFFFF; + background-color: #FCBA19; + border-color: #FCBA19; +} + +.c7.btn.btn-warning:hover, +.c7.btn.btn-warning:active, +.c7.btn.btn-warning:focus { + color: #FFFFFF; + border-color: #FCBA19; + background-color: #FCBA19; +} + +.c7.btn.btn-link { + font-size: 1.6rem; + font-weight: 400; + color: var(--surface-color-primary-button-default); + background: none; + border: none; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 2.5rem; + line-height: 3rem; + -webkit-box-pack: left; + -webkit-justify-content: left; + -ms-flex-pack: left; + justify-content: left; + -webkit-letter-spacing: unset; + -moz-letter-spacing: unset; + -ms-letter-spacing: unset; + letter-spacing: unset; + text-align: left; + padding: 0; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c7.btn.btn-link:hover, +.c7.btn.btn-link:active, +.c7.btn.btn-link:focus { + color: var(--surface-color-primary-button-hover); + -webkit-text-decoration: underline; + text-decoration: underline; + border: none; + background: none; + box-shadow: none; + outline: none; +} + +.c7.btn.btn-link:disabled, +.c7.btn.btn-link.disabled { + color: #9F9D9C; + background: none; + pointer-events: none; +} + +.c7.btn:disabled, +.c7.btn:disabled:hover { + color: #9F9D9C; + background-color: #EDEBE9; + box-shadow: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + cursor: not-allowed; +} + +.c7.Button .Button__icon { + margin-right: 1.6rem; +} + +.c7.Button--icon-only:focus { + outline: none; +} + +.c7.Button--icon-only .Button__icon { + margin-right: 0; +} + +.c5 { + font-weight: bold; + color: var(--theme-blue-100); + border-bottom: 0.2rem var(--theme-blue-90) solid; + margin-bottom: 2rem; +} + +.c4 { + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c2.required::before { + content: '*'; + position: absolute; + top: 0.75rem; + left: 0rem; +} + +.c1 { + font-weight: bold; +} + +.c10 { + width: 100%; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + overflow-y: auto; +} + +.c9 { + font-weight: 700; + font-size: 1.7rem; + margin-bottom: 1rem; + text-align: left; + padding-top: 1rem; + color: var(--theme-blue-100); + border-bottom: solid 0.1rem var(--theme-blue-90); +} + +.c11 { + overflow-x: hidden; + max-height: 50rem; +} + +.c12 { + text-align: center; + font-style: italic; +} + +.c0 { + padding: 1rem; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + word-break: break-all; +} + +.c3 div:first-child { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c3 label { + margin-right: 1rem; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; +} + +.c8 { + color: black; + font-style: italic; +} + +
+
+
+
+ +
+
+
+
+ +
+
+ + + + + +
+
+
+
+
+
+

+
+
+ Document Information + + + + + + +
+
+

+
+
+ +
+
+
+ +
+
+ MOTI Plan +
+
+
+
+
+
+
+
+
+
+ +
+
+ Amended +
+
+

+ Details +

+
+
+ No additional data +
+
+
+
+
+ +`; diff --git a/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailForm.test.tsx.snap b/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailForm.test.tsx.snap index 527534036e..5112c20674 100644 --- a/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailForm.test.tsx.snap +++ b/source/frontend/src/features/documents/documentDetail/__snapshots__/DocumentDetailForm.test.tsx.snap @@ -595,7 +595,7 @@ exports[`DocumentDetailForm component > renders as expected 1`] = ` data-testid="select-option-4" value="4" > - MoTI plan + MOTI plan