diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx index 95b632a8ad..99491944d6 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx @@ -183,7 +183,8 @@ export const AcquisitionView: React.FunctionComponent = ( >; - acquisitionFile?: Api_AcquisitionFile; + file?: Api_File; + fileType: FileTypes; isEditing: boolean; setIsEditing: (value: boolean) => void; selectedMenuIndex: number; @@ -30,22 +32,21 @@ export const FilePropertyRouter: React.FC = props => { const { setStaleLastUpdatedBy } = useContext(SideBarContext); - const onChildSucess = () => { + const onChildSuccess = () => { props.setIsEditing(false); setStaleLastUpdatedBy(true); }; - if (props.acquisitionFile === undefined || props.acquisitionFile === null) { + if (props.file === undefined || props.file === null) { return null; } - const fileProperty = getAcquisitionFileProperty(props.acquisitionFile, props.selectedMenuIndex); - + const fileProperty = getFileProperty(props.file, props.selectedMenuIndex); if (fileProperty == null) { toast.warn('Could not find property in the file, showing file details instead', { autoClose: 15000, }); - return ; + return ; } // render edit forms @@ -71,9 +72,17 @@ export const FilePropertyRouter: React.FC = props => { fileProperty={fileProperty} View={TakesUpdateForm} ref={props.formikRef} - onSuccess={onChildSucess} + onSuccess={onChildSuccess} /> + + + + ); @@ -83,37 +92,36 @@ export const FilePropertyRouter: React.FC = props => { props.setIsEditing(true)} - setEditTakes={() => props.setIsEditing(true)} + setEditing={() => props.setIsEditing(true)} fileProperty={fileProperty} defaultTab={props.defaultPropertyTab} customTabs={[]} View={InventoryTabs} - fileContext={FileTypes.Acquisition} + fileContext={props.fileType} /> - + ); } }; -const getAcquisitionFileProperty = ( - acquisitionFile: Api_AcquisitionFile, - selectedMenuIndex: number, -) => { - const properties = acquisitionFile?.fileProperties || []; +const getFileProperty = (file: Api_File, selectedMenuIndex: number) => { + const properties = file?.fileProperties || []; const selectedPropertyIndex = selectedMenuIndex - 1; if (selectedPropertyIndex < 0 || selectedPropertyIndex >= properties.length) { return null; } - const acquisitionFileProperty = properties[selectedPropertyIndex]; - if (!!acquisitionFileProperty.file) { - acquisitionFileProperty.file = acquisitionFile; + const fileProperty = properties[selectedPropertyIndex]; + if (!!fileProperty) { + fileProperty.file = file; } - return acquisitionFileProperty; + return fileProperty; }; export default FilePropertyRouter; diff --git a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx index 26bcb13537..6b19903791 100644 --- a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx +++ b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx @@ -34,15 +34,39 @@ export const InventoryTabs: React.FunctionComponent< React.PropsWithChildren > = ({ defaultTabKey, tabViews, activeTab }) => { const history = useHistory(); - const match = useRouteMatch<{ propertyId: string }>(); + const match = useRouteMatch<{ + propertyId: string; + menuIndex: string; + id: string; + researchId: string; + }>(); return ( { const tab = Object.values(InventoryTabNames).find(value => value === eventKey); - const path = generatePath(match.path, { propertyId: match.params.propertyId, tab }); - history.push(path); + if (match.path.includes('acquisition')) { + const path = generatePath(match.path, { + menuIndex: match.params.menuIndex, + id: match.params.id, + tab, + }); + history.push(path); + } else if (match.path.includes('research')) { + const path = generatePath(match.path, { + menuIndex: match.params.menuIndex, + researchId: match.params.researchId, + tab, + }); + history.push(path); + } else { + const path = generatePath(match.path, { + propertyId: match.params.propertyId, + tab, + }); + history.push(path); + } }} > {tabViews.map((view: TabInventoryView, index: number) => ( diff --git a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx index 6fb927339c..3f9fafc076 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx @@ -2,6 +2,7 @@ import { FormikProps } from 'formik'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { MdTopic } from 'react-icons/md'; +import { matchPath, useHistory, useRouteMatch } from 'react-router-dom'; import styled from 'styled-components'; import GenericModal from '@/components/common/GenericModal'; @@ -11,10 +12,12 @@ import { FileTypes } from '@/constants/fileTypes'; import FileLayout from '@/features/mapSideBar/layout/FileLayout'; import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; import { useResearchRepository } from '@/hooks/repositories/useResearchRepository'; +import { useQuery } from '@/hooks/use-query'; import useApiUserOverride from '@/hooks/useApiUserOverride'; import { Api_File } from '@/models/api/File'; import { Api_ResearchFile } from '@/models/api/ResearchFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; +import { stripTrailingSlash } from '@/utils'; import { getFilePropertyName } from '@/utils/mapPropertyUtils'; import { SideBarContext } from '../context/sidebarContext'; @@ -22,10 +25,9 @@ import SidebarFooter from '../shared/SidebarFooter'; import { UpdateProperties } from '../shared/update/properties/UpdateProperties'; import ResearchHeader from './common/ResearchHeader'; import ResearchMenu from './common/ResearchMenu'; -import { FormKeys } from './FormKeys'; import { useGetResearch } from './hooks/useGetResearch'; import { useUpdateResearchProperties } from './hooks/useUpdateResearchProperties'; -import ViewSelector from './ViewSelector'; +import ResearchView from './ResearchView'; export interface IResearchContainerProps { researchFileId: number; @@ -61,9 +63,6 @@ export const ResearchContainer: React.FunctionComponent< setStaleLastUpdatedBy, } = React.useContext(SideBarContext); - const [selectedMenuIndex, setSelectedMenuIndex] = useState(0); - const [isEditing, setIsEditing] = useState(false); - const [editKey, setEditKey] = useState(FormKeys.none); const [isValid, setIsValid] = useState(true); const [isShowingPropertySelector, setIsShowingPropertySelector] = useState(false); @@ -72,6 +71,9 @@ export const ResearchContainer: React.FunctionComponent< const [showConfirmModal, setShowConfirmModal] = useState(false); + const history = useHistory(); + const match = useRouteMatch(); + const menuItems = researchFile?.fileProperties?.map(x => getFilePropertyName(x).value) || []; menuItems.unshift('File Summary'); @@ -104,13 +106,27 @@ export const ResearchContainer: React.FunctionComponent< } }, [props.researchFileId, getLastUpdatedBy, setLastUpdatedBy]); + const push = history.push; + const query = useQuery(); + const setIsEditing = React.useCallback( + (editing: boolean) => { + if (editing) { + query.set('edit', 'true'); + } else { + query.delete('edit'); + } + + push({ search: query.toString() }); + }, + [push, query], + ); + const onSuccess = React.useCallback(() => { setStaleFile(true); setStaleLastUpdatedBy(true); mapMachine.refreshMapProperties(); setIsEditing(false); - setEditKey(FormKeys.none); - }, [mapMachine, setStaleFile, setStaleLastUpdatedBy]); + }, [mapMachine, setIsEditing, setStaleFile, setStaleLastUpdatedBy]); React.useEffect(() => { if (researchFile === undefined || researchFileId !== researchFile?.id || staleFile) { @@ -128,13 +144,17 @@ export const ResearchContainer: React.FunctionComponent< } }, [fetchLastUpdatedBy, lastUpdatedBy, researchFileId, staleLastUpdatedBy]); - if (researchFile === undefined && (loadingResearchFile || loadingResearchFileProperties)) { - return ( - <> - - - ); - } + const isEditing = query.get('edit') === 'true'; + + const navigateToMenuRoute = (selectedIndex: number) => { + const route = selectedIndex === 0 ? '' : `/property/${selectedIndex}`; + history.push(`${stripTrailingSlash(match.url)}${route}`); + }; + const propertiesMatch = matchPath>( + history.location.pathname, + `${stripTrailingSlash(match.path)}/property/:menuIndex/:tab?`, + ); + const selectedMenuIndex = propertiesMatch !== null ? Number(propertiesMatch.params.menuIndex) : 0; const onMenuChange = (selectedIndex: number) => { if (isEditing) { @@ -143,14 +163,14 @@ export const ResearchContainer: React.FunctionComponent< window.confirm('You have made changes on this form. Do you wish to leave without saving?') ) { handleCancelClick(); - setSelectedMenuIndex(selectedIndex); + navigateToMenuRoute(selectedIndex); } } else { handleCancelClick(); - setSelectedMenuIndex(selectedIndex); + navigateToMenuRoute(selectedIndex); } } else { - setSelectedMenuIndex(selectedIndex); + navigateToMenuRoute(selectedIndex); } }; @@ -180,13 +200,20 @@ export const ResearchContainer: React.FunctionComponent< } setShowConfirmModal(false); setIsEditing(false); - setEditKey(FormKeys.none); }; const showPropertiesSelector = () => { setIsShowingPropertySelector(true); }; + if (researchFile === undefined && (loadingResearchFile || loadingResearchFileProperties)) { + return ( + <> + + + ); + } + if (isShowingPropertySelector && researchFile) { return ( - diff --git a/source/frontend/src/features/mapSideBar/research/ResearchRouter.tsx b/source/frontend/src/features/mapSideBar/research/ResearchRouter.tsx new file mode 100644 index 0000000000..891ec5917f --- /dev/null +++ b/source/frontend/src/features/mapSideBar/research/ResearchRouter.tsx @@ -0,0 +1,71 @@ +import { FormikProps } from 'formik'; +import React from 'react'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; + +import { InventoryTabNames } from '@/features/mapSideBar/property/InventoryTabs'; +import { FileTabType } from '@/features/mapSideBar/shared/detail/FileTabs'; +import { Api_ResearchFile } from '@/models/api/ResearchFile'; +import { stripTrailingSlash } from '@/utils'; + +import UpdateResearchContainer from './tabs/fileDetails/update/UpdateSummaryContainer'; +import ResearchTabsContainer from './tabs/ResearchTabsContainer'; + +export interface IResearchRouterProps { + formikRef: React.Ref>; + researchFile?: Api_ResearchFile; + isEditing: boolean; + setIsEditing: (value: boolean) => void; + defaultFileTab: FileTabType; + defaultPropertyTab: InventoryTabNames; + onSuccess: () => void; +} + +export const ResearchRouter: React.FC = props => { + const { path, url } = useRouteMatch(); + + if (props.researchFile === undefined || props.researchFile === null) { + return null; + } + + // render edit forms + if (props.isEditing) { + return ( + + + + + {/* Ignore property-related routes (which are handled in separate FilePropertyRouter) */} + + <> + + + + ); + } else { + // render read-only views + return ( + + {/* Ignore property-related routes (which are handled in separate FilePropertyRouter) */} + + <> + + + + + + + ); + } +}; + +export default ResearchRouter; diff --git a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx new file mode 100644 index 0000000000..3a72fbc60f --- /dev/null +++ b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx @@ -0,0 +1,63 @@ +import { FormikProps } from 'formik'; +import * as React from 'react'; +import { matchPath, Route, useHistory, useRouteMatch } from 'react-router-dom'; + +import { FileTypes } from '@/constants'; +import { InventoryTabNames } from '@/features/mapSideBar/property/InventoryTabs'; +import { Api_ResearchFile } from '@/models/api/ResearchFile'; +import { stripTrailingSlash } from '@/utils'; + +import FilePropertyRouter from '../acquisition/router/FilePropertyRouter'; +import { FileTabType } from '../shared/detail/FileTabs'; +import ResearchRouter from './ResearchRouter'; + +export interface IViewSelectorProps { + researchFile?: Api_ResearchFile; + setEditMode: (isEditing: boolean) => void; + isEditing: boolean; + onSuccess: () => void; +} + +const ResearchView = React.forwardRef, IViewSelectorProps>((props, formikRef) => { + const match = useRouteMatch(); + + const { location } = useHistory(); + const propertiesMatch = matchPath>( + location.pathname, + `${stripTrailingSlash(match.path)}/property/:menuIndex/:tab?`, + ); + + const selectedMenuIndex = propertiesMatch !== null ? Number(propertiesMatch.params.menuIndex) : 0; + + return ( + <> + + ( + + )} + /> + + ); +}); + +export default ResearchView; diff --git a/source/frontend/src/features/mapSideBar/research/ViewSelector.tsx b/source/frontend/src/features/mapSideBar/research/ViewSelector.tsx deleted file mode 100644 index bb2c20dd48..0000000000 --- a/source/frontend/src/features/mapSideBar/research/ViewSelector.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { FormikProps } from 'formik'; -import noop from 'lodash/noop'; -import * as React from 'react'; - -import { - InventoryTabNames, - InventoryTabs, - TabInventoryView, -} from '@/features/mapSideBar/property/InventoryTabs'; -import { UpdatePropertyDetailsContainer } from '@/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer'; -import { Api_ResearchFile } from '@/models/api/ResearchFile'; - -import { PropertyResearchTabView } from '../property/tabs/propertyResearch/detail/PropertyResearchTabView'; -import { UpdatePropertyResearchContainer } from '../property/tabs/propertyResearch/update/UpdatePropertyResearchContainer'; -import { PropertyFileContainer } from '../shared/detail/PropertyFileContainer'; -import { FormKeys } from './FormKeys'; -import UpdateResearchContainer from './tabs/fileDetails/update/UpdateSummaryContainer'; -import ResearchTabsContainer from './tabs/ResearchTabsContainer'; - -export interface IViewSelectorProps { - researchFile?: Api_ResearchFile; - selectedIndex: number; - - isEditMode: boolean; - setEditMode: (isEditing: boolean) => void; - // The "edit key" identifies which form is currently being edited: e.g. - // - property details info, - // - research summary, - // - research property info - // - 'none' means no form is being edited. - editKey: FormKeys; - setEditKey: (editKey: FormKeys) => void; - onSuccess: () => void; -} - -const ViewSelector = React.forwardRef, IViewSelectorProps>((props, formikRef) => { - if (props.selectedIndex === 0) { - if (props.isEditMode && !!props.researchFile) { - return ( - - ); - } else { - return ( - - ); - } - } else { - const properties = props.researchFile?.fileProperties || []; - const selectedPropertyIndex = props.selectedIndex - 1; - const researchFileProperty = properties[selectedPropertyIndex]; - researchFileProperty.file = props.researchFile; - - if (props.isEditMode) { - if (props.editKey === FormKeys.propertyDetails) { - return ( - - ); - } else { - return ( - - ); - } - } else { - const researchPropertyTab: TabInventoryView = { - content: ( - { - props.setEditMode(editable); - props.setEditKey(FormKeys.propertyResearch); - }} - /> - ), - key: InventoryTabNames.research, - name: 'Property Research', - }; - - return ( - { - props.setEditKey(FormKeys.propertyDetails); - props.setEditMode(true); - }} - setEditTakes={noop} - defaultTab={InventoryTabNames.research} - customTabs={[researchPropertyTab]} - View={InventoryTabs} - /> - ); - } - } -}); - -export default ViewSelector; diff --git a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.test.tsx b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.test.tsx index fd3ce2e509..ef9b2c70f9 100644 --- a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.test.tsx @@ -1,3 +1,4 @@ +import { createMemoryHistory } from 'history'; import { act } from 'react-test-renderer'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; @@ -14,8 +15,8 @@ jest.mock('@react-keycloak/web'); jest.mock('@/components/common/mapFSM/MapStateMachineContext'); -const setEditKey = jest.fn(); -const setEditMode = jest.fn(); +const setIsEditing = jest.fn(); +const history = createMemoryHistory(); describe('ResearchFileTabs component', () => { // render component under test @@ -24,12 +25,12 @@ describe('ResearchFileTabs component', () => { , { useMockAuthentication: true, + history, ...renderOptions, }, ); @@ -49,8 +50,7 @@ describe('ResearchFileTabs component', () => { const { asFragment } = setup( { researchFile: getMockResearchFile(), - setEditKey, - setEditMode, + setIsEditing, }, { claims: [Claims.DOCUMENT_VIEW] }, ); @@ -61,8 +61,7 @@ describe('ResearchFileTabs component', () => { const { getByText } = setup( { researchFile: getMockResearchFile(), - setEditKey, - setEditMode, + setIsEditing, }, { claims: [Claims.DOCUMENT_VIEW] }, ); @@ -75,8 +74,7 @@ describe('ResearchFileTabs component', () => { const { getByText } = setup( { researchFile: getMockResearchFile(), - setEditKey, - setEditMode, + setIsEditing, }, { claims: [Claims.DOCUMENT_VIEW] }, ); @@ -86,7 +84,7 @@ describe('ResearchFileTabs component', () => { userEvent.click(editButton); }); await waitFor(() => { - expect(getByText('Documents')).toHaveClass('active'); + expect(history.location.pathname).toBe('/documents'); }); }); @@ -94,8 +92,7 @@ describe('ResearchFileTabs component', () => { const { getAllByText } = setup( { researchFile: getMockResearchFile(), - setEditKey, - setEditMode, + setIsEditing, }, { claims: [Claims.NOTE_VIEW] }, ); @@ -105,7 +102,7 @@ describe('ResearchFileTabs component', () => { userEvent.click(editButton); }); await waitFor(() => { - expect(getAllByText('Notes')[0]).toHaveClass('active'); + expect(history.location.pathname).toBe('/notes'); }); }); }); diff --git a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx index dfe805eac8..85b944884c 100644 --- a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; import { Claims } from '@/constants/claims'; import { DocumentRelationshipType } from '@/constants/documentRelationshipType'; @@ -10,18 +11,11 @@ import { Api_ResearchFile } from '@/models/api/ResearchFile'; import { SideBarContext } from '../../context/sidebarContext'; import { FileTabs, FileTabType, TabFileView } from '../../shared/detail/FileTabs'; import DocumentsTab from '../../shared/tabs/DocumentsTab'; -import { FormKeys } from '../FormKeys'; import ResearchSummaryView from './fileDetails/details/ResearchSummaryView'; export interface IResearchTabsContainerProps { researchFile?: Api_ResearchFile; - // The "edit key" identifies which form is currently being edited: e.g. - // - property details info, - // - research summary, - // - research property info - // - 'none' means no form is being edited. - setEditKey: (editKey: FormKeys) => void; - setEditMode: (isEditing: boolean) => void; + setIsEditing: (value: boolean) => void; } /** @@ -29,25 +23,28 @@ export interface IResearchTabsContainerProps { */ export const ResearchTabsContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ researchFile, setEditMode, setEditKey }) => { +> = ({ researchFile, setIsEditing }) => { const tabViews: TabFileView[] = []; const { hasClaim } = useKeycloakWrapper(); const { setStaleLastUpdatedBy } = React.useContext(SideBarContext); + const history = useHistory(); + const defaultTab = FileTabType.FILE_DETAILS; + const { tab } = useParams<{ tab?: string }>(); + const activeTab = Object.values(FileTabType).find(value => value === tab) ?? defaultTab; + + const setActiveTab = (tab: FileTabType) => { + if (activeTab !== tab) { + history.push(`${tab}`); + } + }; + const onChildEntityUpdate = () => { setStaleLastUpdatedBy(true); }; tabViews.push({ - content: ( - { - setEditMode(editable); - setEditKey(FormKeys.researchSummary); - }} - /> - ), + content: , key: FileTabType.FILE_DETAILS, name: 'File Details', }); @@ -80,10 +77,6 @@ export const ResearchTabsContainer: React.FunctionComponent< }); } - var defaultTab = FileTabType.FILE_DETAILS; - - const [activeTab, setActiveTab] = useState(defaultTab); - return ( { const DEFAULT_PROPS: IPropertyFileContainerProps = { View: ActivityView, fileProperty: (getMockResearchFile().fileProperties ?? [])[0], - setEditFileProperty: noop, + setEditing: noop, customTabs: [], defaultTab: InventoryTabNames.property, - setEditTakes: noop, }; describe('PropertyFileContainer component', () => { diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index f372d9eb2f..921f530fb6 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -17,10 +17,11 @@ import TakesDetailView from '@/features/mapSideBar/property/tabs/takes/detail/Ta import { PROPERTY_TYPES, useComposedProperties } from '@/hooks/repositories/useComposedProperties'; import { Api_PropertyFile } from '@/models/api/PropertyFile'; +import PropertyResearchTabView from '../../property/tabs/propertyResearch/detail/PropertyResearchTabView'; + export interface IPropertyFileContainerProps { fileProperty: Api_PropertyFile; - setEditFileProperty: () => void; - setEditTakes: () => void; + setEditing: () => void; View: React.FunctionComponent>; customTabs: TabInventoryView[]; defaultTab: InventoryTabNames; @@ -76,6 +77,16 @@ export const PropertyFileContainer: React.FunctionComponent< name: 'Value', }); + if (props.fileContext === FileTypes.Research) { + tabViews.push({ + content: ( + + ), + key: InventoryTabNames.research, + name: 'Property Research', + }); + } + tabViews.push(...props.customTabs); if (!!id) { @@ -108,7 +119,7 @@ export const PropertyFileContainer: React.FunctionComponent< content: ( ),