diff --git a/source/frontend/src/features/account/Login.tsx b/source/frontend/src/features/account/Login.tsx index f2ffeef5e1..13752f74a2 100644 --- a/source/frontend/src/features/account/Login.tsx +++ b/source/frontend/src/features/account/Login.tsx @@ -19,7 +19,8 @@ import { LoginStyled } from './LoginStyled'; * @returns Login component. */ const Login = () => { - const { redirect } = useQuery(); + const query = useQuery(); + const redirect = query.get('redirect'); const keyCloakWrapper = useKeycloakWrapper(); const keycloak = keyCloakWrapper.obj; const isIE = usingIE(); diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx index e08c784a9d..60ece2779e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx @@ -1,6 +1,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Formik } from 'formik'; +import { createMemoryHistory } from 'history'; import { noop } from 'lodash'; import { FileTypes } from '@/constants/index'; @@ -24,8 +25,8 @@ import { import { SideBarContextProvider } from '../context/sidebarContext'; import { AcquisitionContainer, IAcquisitionContainerProps } from './AcquisitionContainer'; import { IAcquisitionViewProps } from './AcquisitionView'; -import { EditFormType } from './EditFormNames'; +const history = createMemoryHistory(); const mockAxios = new MockAdapter(axios); const generateFn = jest.fn(); @@ -84,6 +85,7 @@ describe('AcquisitionContainer component', () => { }, useMockAuthentication: true, claims: renderOptions?.claims ?? [], + history, ...renderOptions, }, ); @@ -136,11 +138,9 @@ describe('AcquisitionContainer component', () => { await waitForElementToBeRemoved(spinner); mockAxios.onGet(new RegExp('acquisitionfiles/1/properties')).timeout(); - await act(async () => { - viewProps.setContainerState({ activeEditForm: EditFormType.PROPERTY_SELECTOR }); - viewProps.canRemove(1); - expect(spinner).not.toBeVisible(); - }); + await act(async () => viewProps.onShowPropertySelector()); + await act(async () => viewProps.canRemove(1)); + expect(spinner).not.toBeVisible(); }); it('canRemove returns true if file property has no associated entities', async () => { @@ -158,10 +158,8 @@ describe('AcquisitionContainer component', () => { }, }, ]); - await act(async () => { - viewProps.setContainerState({ activeEditForm: EditFormType.PROPERTY_SELECTOR }); - }); - const canRemoveResponse = await viewProps.canRemove(1); + await act(async () => viewProps.onShowPropertySelector()); + const canRemoveResponse = await act(() => viewProps.canRemove(1)); expect(canRemoveResponse).toBe(true); }); @@ -181,10 +179,9 @@ describe('AcquisitionContainer component', () => { activityInstanceProperties: [{}], }, ]); - await act(async () => { - viewProps.setContainerState({ activeEditForm: EditFormType.PROPERTY_SELECTOR }); - }); - expect(await viewProps.canRemove(1)).toBe(false); + await act(async () => viewProps.onShowPropertySelector()); + const canRemoveResponse = await act(() => viewProps.canRemove(1)); + expect(canRemoveResponse).toBe(false); }); it('should change menu index when not editing', async () => { @@ -194,7 +191,7 @@ describe('AcquisitionContainer component', () => { await waitForElementToBeRemoved(spinner); await act(async () => viewProps.onMenuChange(1)); - await waitFor(async () => expect(viewProps.containerState.selectedMenuIndex).toBe(1)); + expect(history.location.pathname).toBe('/property/1'); }); it('displays a warning if form is dirty and menu index changes', async () => { @@ -204,13 +201,15 @@ describe('AcquisitionContainer component', () => { const spinner = getByTestId('filter-backdrop-loading'); await waitForElementToBeRemoved(spinner); - await act(async () => viewProps.setContainerState({ isEditing: true })); + await act(async () => viewProps.setIsEditing(true)); await act(async () => (viewProps.formikRef.current as any).setFieldValue('value', 1)); await screen.findByText('1'); await act(async () => viewProps.onMenuChange(1)); - await waitFor(async () => expect(viewProps.containerState.showConfirmModal).toBe(true)); - await waitFor(async () => expect(viewProps.containerState.isEditing).toBe(true)); + expect(viewProps.containerState.showConfirmModal).toBe(true); + expect(history.location.pathname).toBe('/property/1'); + const params = new URLSearchParams(history.location.search); + expect(params.has('edit')).toBe(false); }); it('cancels edit if form is not dirty and menu index changes', async () => { @@ -220,10 +219,11 @@ describe('AcquisitionContainer component', () => { const spinner = getByTestId('filter-backdrop-loading'); await waitForElementToBeRemoved(spinner); - await act(async () => viewProps.setContainerState({ isEditing: true })); + await act(async () => viewProps.setIsEditing(true)); await act(async () => viewProps.onMenuChange(1)); - await waitFor(async () => expect(viewProps.containerState.isEditing).toBe(false)); + const params = new URLSearchParams(history.location.search); + expect(params.has('edit')).toBe(false); }); it('on success function refetches acq file', async () => { @@ -245,10 +245,11 @@ describe('AcquisitionContainer component', () => { const spinner = getByTestId('filter-backdrop-loading'); await waitForElementToBeRemoved(spinner); - await act(async () => viewProps.setContainerState({ isEditing: true })); + await act(async () => viewProps.setIsEditing(true)); await act(async () => viewProps.onSuccess()); - expect(viewProps.containerState.isEditing).toBe(false); + const params = new URLSearchParams(history.location.search); + expect(params.has('edit')).toBe(false); }); it('on save function submits the form', async () => { diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx index c2ed3096f9..408ef3dd4e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx @@ -1,20 +1,22 @@ import { FormikProps } from 'formik'; -import React, { useCallback, useContext, useEffect, useReducer, useRef } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react'; +import { matchPath, useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapSearch } from '@/components/maps/hooks/useMapSearch'; import { FileTypes } from '@/constants/index'; import { InventoryTabNames } from '@/features/mapSideBar/property/InventoryTabs'; import { useAcquisitionProvider } from '@/hooks/repositories/useAcquisitionProvider'; +import { useQuery } from '@/hooks/use-query'; import useApiUserOverride from '@/hooks/useApiUserOverride'; import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; import { Api_File } from '@/models/api/File'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; +import { stripTrailingSlash } from '@/utils'; import { SideBarContext } from '../context/sidebarContext'; import { FileTabType } from '../shared/detail/FileTabs'; import { IAcquisitionViewProps } from './AcquisitionView'; -import { EditFormType } from './EditFormNames'; export interface IAcquisitionContainerProps { acquisitionFileId: number; @@ -25,7 +27,6 @@ export interface IAcquisitionContainerProps { // Interface for our internal state export interface AcquisitionContainerState { isEditing: boolean; - activeEditForm?: EditFormType; selectedMenuIndex: number; showConfirmModal: boolean; acquisitionFile: Api_AcquisitionFile | undefined; @@ -35,7 +36,6 @@ export interface AcquisitionContainerState { const initialState: AcquisitionContainerState = { isEditing: false, - activeEditForm: undefined, selectedMenuIndex: 0, showConfirmModal: false, acquisitionFile: undefined, @@ -63,6 +63,30 @@ export const AcquisitionContainer: React.FunctionComponent>(null); + const location = useLocation(); + const history = useHistory(); + const match = useRouteMatch(); + const query = useQuery(); + const isEditing = query.get('edit') === 'true'; + + const setIsEditing = (value: boolean) => { + if (value) { + query.set('edit', value.toString()); + } else { + query.delete('edit'); + } + history.push({ search: query.toString() }); + }; + + const isPropertySelector = useMemo( + () => + matchPath>( + location.pathname, + `${stripTrailingSlash(match.path)}/property/selector`, + ), + [location.pathname, match.path], + ); + /** See here that we are using `newState: Partial` in our reducer so we can provide only the properties that are updated on our state @@ -115,24 +139,33 @@ export const AcquisitionContainer: React.FunctionComponent onClose && onClose(), [onClose]); + const navigateToMenuRoute = (selectedIndex: number) => { + const route = selectedIndex === 0 ? '' : `/property/${selectedIndex}`; + history.push(`${stripTrailingSlash(match.url)}${route}`); + }; + const onMenuChange = (selectedIndex: number) => { - if (containerState.isEditing) { + if (isEditing) { if (formikRef?.current?.dirty) { if ( window.confirm('You have made changes on this form. Do you wish to leave without saving?') ) { handleCancelClick(); - setContainerState({ selectedMenuIndex: selectedIndex }); + navigateToMenuRoute(selectedIndex); } } else { handleCancelClick(); - setContainerState({ selectedMenuIndex: selectedIndex }); + navigateToMenuRoute(selectedIndex); } } else { - setContainerState({ selectedMenuIndex: selectedIndex }); + navigateToMenuRoute(selectedIndex); } }; + const onShowPropertySelector = () => { + history.push(`${stripTrailingSlash(match.url)}/property/selector`); + }; + const handleSaveClick = () => { if (formikRef !== undefined) { formikRef.current?.setSubmitting(true); @@ -156,17 +189,14 @@ export const AcquisitionContainer: React.FunctionComponent { fetchAcquisitionFile(); searchMany(); - setContainerState({ activeEditForm: undefined, isEditing: false }); + setIsEditing(false); }; const canRemove = async (propertyId: number) => { @@ -183,27 +213,29 @@ export const AcquisitionContainer: React.FunctionComponent { return updateAcquisitionProperties .execute({ ...file, productId: null, projectId: null }, userOverrideCodes) - .then(() => onSuccess()); + .then(response => { + onSuccess(); + return response; + }); }); }; // UI components - if ( - loadingAcquisitionFile || - (loadingAcquisitionFileProperties && - containerState.activeEditForm !== EditFormType.PROPERTY_SELECTOR) - ) { + if (loadingAcquisitionFile || (loadingAcquisitionFileProperties && !isPropertySelector)) { return ; } return ( { @@ -50,6 +56,9 @@ const DEFAULT_PROPS: IAcquisitionViewProps = { onCancelConfirm, onUpdateProperties, canRemove, + isEditing: false, + setIsEditing, + onShowPropertySelector: onEditFileProperties, setContainerState, containerState: { acquisitionFile: mockAcquisitionFileResponse(), @@ -62,9 +71,11 @@ const DEFAULT_PROPS: IAcquisitionViewProps = { formikRef: React.createRef(), }; +const history = createMemoryHistory(); + describe('AcquisitionView component', () => { // render component under test - const setup = ( + const setup = async ( props: IAcquisitionViewProps = { ...DEFAULT_PROPS }, renderOptions: RenderOptions = {}, ) => { @@ -75,7 +86,9 @@ describe('AcquisitionView component', () => { fileType: FileTypes.Acquisition, }} > - + + + , { store: { @@ -83,6 +96,7 @@ describe('AcquisitionView component', () => { }, useMockAuthentication: true, claims: renderOptions?.claims ?? [], + history, ...renderOptions, }, ); @@ -94,23 +108,34 @@ describe('AcquisitionView component', () => { }; beforeEach(() => { - mockAxios.onGet(new RegExp('users/info/*')).reply(200, {}); - mockAxios.onGet(new RegExp('notes/*')).reply(200, mockNotesResponse()); + history.replace(`/mapview/sidebar/acquisition/1`); + server.use( + rest.get('/api/users/info/*', (req, res, ctx) => + res(ctx.delay(500), ctx.status(200), ctx.json(getUserMock())), + ), + rest.get('/api/notes/*', (req, res, ctx) => + res(ctx.delay(500), ctx.status(200), ctx.json(mockNotesResponse())), + ), + rest.get('/api/acquisitionfiles/:id/owners', (req, res, ctx) => + res(ctx.delay(500), ctx.status(200), ctx.json(mockAcquisitionFileOwnersResponse())), + ), + rest.get('/api/acquisitionfiles/:id/interestholders', (req, res, ctx) => + res(ctx.delay(500), ctx.status(200), ctx.json(getMockApiInterestHolders())), + ), + ); }); afterEach(() => { - mockAxios.resetHistory(); jest.clearAllMocks(); }); it('renders as expected', async () => { - const { asFragment } = setup(); - + const { asFragment } = await setup(); expect(asFragment()).toMatchSnapshot(); }); it('renders the underlying form', async () => { - const { getByText } = setup(); + const { getByText } = await setup(); const testAcquisitionFile = mockAcquisitionFileResponse(); expect(getByText('Acquisition File')).toBeVisible(); @@ -121,7 +146,7 @@ describe('AcquisitionView component', () => { }); it('should close the form when Close button is clicked', async () => { - const { getCloseButton, getByText } = setup(); + const { getCloseButton, getByText } = await setup(); expect(getByText('Acquisition File')).toBeVisible(); await waitFor(() => userEvent.click(getCloseButton())); @@ -130,27 +155,71 @@ describe('AcquisitionView component', () => { }); it('should display the Edit Properties button if the user has permissions', async () => { - const { getByTitle } = setup(undefined, { claims: [Claims.ACQUISITION_EDIT] }); - + const { getByTitle } = await setup(undefined, { claims: [Claims.ACQUISITION_EDIT] }); expect(getByTitle(/Change properties/)).toBeVisible(); }); it('should not display the Edit Properties button if the user does not have permissions', async () => { - const { queryByTitle } = setup(undefined, { claims: [] }); - + const { queryByTitle } = await setup(undefined, { claims: [] }); expect(queryByTitle('Change properties')).toBeNull(); }); it('should display the notes tab if the user has permissions', async () => { - const { getAllByText } = setup(undefined, { claims: [Claims.NOTE_VIEW] }); - await act(async () => { - expect(getAllByText(/Notes/)[0]).toBeVisible(); - }); + const { getAllByText } = await setup(undefined, { claims: [Claims.NOTE_VIEW] }); + expect(getAllByText(/Notes/)[0]).toBeVisible(); }); it('should not display the notes tab if the user does not have permissions', async () => { - const { queryByText } = setup(undefined, { claims: [] }); - + const { queryByText } = await setup(undefined, { claims: [] }); expect(queryByText('Notes')).toBeNull(); }); + + it('should display the File Details tab by default', async () => { + const { getByRole } = await act(() => setup()); + const tab = getByRole('tab', { name: /File details/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + }); + + it(`should show a toast and redirect to the File Details page when accessing a non-existing property index`, async () => { + history.replace(`/mapview/sidebar/acquisition/1/property/99999`); + const { getByRole, findByText } = await setup(); + const tab = getByRole('tab', { name: /File details/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + // toast + expect(await findByText(/Could not find property in the file/)).toBeVisible(); + }); + + it('should display the Property Selector according to routing', async () => { + history.replace(`/mapview/sidebar/acquisition/1/property/selector`); + const { getByRole } = await act(() => setup()); + const tab = getByRole('tab', { name: /Locate on Map/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + }); + + it('should display the Property Details tab according to routing', async () => { + history.replace(`/mapview/sidebar/acquisition/1/property/1`); + const { getByRole } = await act(() => setup()); + const tab = getByRole('tab', { name: /Property Details/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + }); + + it(`should display the File Details tab when we are editing and the path doesn't match any route`, async () => { + history.replace(`/mapview/sidebar/acquisition/1/blahblahtab?edit=true`); + const { getByRole } = await act(() => setup()); + const tab = getByRole('tab', { name: /File details/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + }); + + it(`should display the Property Details tab when we are editing and the path doesn't match any route`, async () => { + history.replace(`/mapview/sidebar/acquisition/1/property/1/unknownTabWhatIsThis?edit=true`); + const { getByRole } = await act(() => setup()); + const tab = getByRole('tab', { name: /Property Details/i }); + expect(tab).toBeVisible(); + expect(tab).toHaveClass('active'); + }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx index dca08e57ee..c494a42139 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx @@ -1,5 +1,14 @@ import { FormikProps } from 'formik'; -import * as React from 'react'; +import React from 'react'; +import { + match, + matchPath, + Route, + Switch, + useHistory, + useLocation, + useRouteMatch, +} from 'react-router-dom'; import styled from 'styled-components'; import { ReactComponent as RealEstateAgent } from '@/assets/images/real-estate-agent.svg'; @@ -7,25 +16,31 @@ import GenericModal from '@/components/common/GenericModal'; import FileLayout from '@/features/mapSideBar/layout/FileLayout'; import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; import { Api_File } from '@/models/api/File'; +import { stripTrailingSlash } from '@/utils'; import { getFilePropertyName } from '@/utils/mapPropertyUtils'; +import { InventoryTabNames } from '../property/InventoryTabs'; +import { FileTabType } from '../shared/detail/FileTabs'; import SidebarFooter from '../shared/SidebarFooter'; import UpdateProperties from '../shared/update/properties/UpdateProperties'; import { AcquisitionContainerState } from './AcquisitionContainer'; import AcquisitionHeader from './common/AcquisitionHeader'; import AcquisitionMenu from './common/AcquisitionMenu'; -import { EditFormType } from './EditFormNames'; -import ViewSelector from './ViewSelector'; +import { AcquisitionRouter } from './router/AcquisitionRouter'; +import { FilePropertyRouter } from './router/FilePropertyRouter'; export interface IAcquisitionViewProps { onClose: (() => void) | undefined; onSave: () => void; onCancel: () => void; onMenuChange: (selectedIndex: number) => void; + onShowPropertySelector: () => void; onSuccess: () => void; onCancelConfirm: () => void; onUpdateProperties: (file: Api_File) => Promise; canRemove: (propertyId: number) => Promise; + isEditing: boolean; + setIsEditing: (value: boolean) => void; containerState: AcquisitionContainerState; setContainerState: React.Dispatch>; formikRef: React.RefObject>; @@ -36,134 +51,186 @@ export const AcquisitionView: React.FunctionComponent = ( onSave, onCancel, onMenuChange, + onShowPropertySelector, onSuccess, onCancelConfirm, onUpdateProperties, canRemove, + isEditing, + setIsEditing, containerState, setContainerState, formikRef, }) => { - const formTitle = - containerState.isEditing && containerState.activeEditForm - ? getEditTitle(containerState.activeEditForm) - : 'Acquisition File'; + // match for the current route + const location = useLocation(); + const history = useHistory(); + const match = useRouteMatch(); + + // match for property menu routes - eg /property/1/ltsa + const fileMatch = matchPath>(location.pathname, `${match.path}/:tab`); + const propertySelectorMatch = matchPath>( + location.pathname, + `${stripTrailingSlash(match.path)}/property/selector`, + ); + const propertiesMatch = matchPath>( + location.pathname, + `${stripTrailingSlash(match.path)}/property/:menuIndex/:tab`, + ); + + const selectedMenuIndex = propertiesMatch !== null ? Number(propertiesMatch.params.menuIndex) : 0; + + const formTitle = isEditing + ? getEditTitle(fileMatch, propertySelectorMatch, propertiesMatch) + : 'Acquisition File'; const menuItems = containerState.acquisitionFile?.fileProperties?.map(x => getFilePropertyName(x).value) || []; menuItems.unshift('File Summary'); - if ( - containerState.activeEditForm === EditFormType.PROPERTY_SELECTOR && - containerState.acquisitionFile - ) { - return ( - - setContainerState({ activeEditForm: undefined, isEditing: false }) - } - onSuccess={onSuccess} - updateFileProperties={onUpdateProperties} - canRemove={canRemove} - formikRef={formikRef} - /> - ); - } + const closePropertySelector = () => { + setIsEditing(false); + history.push(`${match.url}`); + }; return ( - - } - header={} - footer={ - containerState.isEditing && ( - + + {containerState.acquisitionFile && ( + - ) - } - > - - - - } - bodyComponent={ - - + + + } + header={} + footer={ + isEditing && ( + + ) + } + > + + + + } + bodyComponent={ + + + ( + + )} + /> - -
If you cancel now, this form will not be saved.
-
- Are you sure you want to Cancel? - - } - handleOk={onCancelConfirm} - handleCancel={() => setContainerState({ showConfirmModal: false })} - okButtonText="Ok" - cancelButtonText="Resume editing" - show - /> -
- } - >
-
+ +
If you cancel now, this form will not be saved.
+
+ Are you sure you want to Cancel? + + } + handleOk={onCancelConfirm} + handleCancel={() => setContainerState({ showConfirmModal: false })} + okButtonText="Ok" + cancelButtonText="Resume editing" + show + /> +
+ } + >
+
+ + ); }; -const getEditTitle = (editFormName: EditFormType) => { - switch (editFormName) { - case EditFormType.ACQUISITION_SUMMARY: - return 'Update Acquisition File'; - case EditFormType.PROPERTY_DETAILS: - return 'Update Property File Data'; - case EditFormType.TAKES: - return 'Update Takes'; - case EditFormType.ACQUISITION_CHECKLIST: - return 'Update Checklist'; - case EditFormType.PROPERTY_SELECTOR: - return 'Updating Acquisition Properties'; - case EditFormType.AGREEMENTS: - return 'Updating Agreements'; - case EditFormType.STAKEHOLDERS: - return 'Updating Stakeholders'; - default: - throw Error('Cannot edit this type of form'); +// Set header title based on current tab route +const getEditTitle = ( + fileMatch: match> | null, + propertySelectorMatch: match> | null, + propertiesMatch: match> | null, +) => { + if (fileMatch !== null) { + const fileTab = fileMatch.params.tab; + switch (fileTab) { + case FileTabType.FILE_DETAILS: + return 'Update Acquisition File'; + case FileTabType.CHECKLIST: + return 'Update Checklist'; + case FileTabType.AGREEMENTS: + return 'Update Agreements'; + case FileTabType.STAKEHOLDERS: + return 'Update Stakeholders'; + } + } + + if (propertySelectorMatch !== null) { + return 'Update Acquisition Properties'; } + + if (propertiesMatch !== null) { + const propertyTab = propertiesMatch.params.tab; + switch (propertyTab) { + case InventoryTabNames.property: + return 'Update Property File Data'; + case InventoryTabNames.takes: + return 'Update Takes'; + } + } + + return 'Acquisition File'; }; const StyledFormWrapper = styled.div` diff --git a/source/frontend/src/features/mapSideBar/acquisition/EditFormNames.ts b/source/frontend/src/features/mapSideBar/acquisition/EditFormNames.ts deleted file mode 100644 index 80d801f774..0000000000 --- a/source/frontend/src/features/mapSideBar/acquisition/EditFormNames.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum EditFormType { - ACQUISITION_SUMMARY = 'acquisitionSummary', - ACQUISITION_CHECKLIST = 'acquisitionChecklist', - PROPERTY_DETAILS = 'propertyDetails', - PROPERTY_SELECTOR = 'propertySelector', - TAKES = 'takes', - AGREEMENTS = 'agreements', - STAKEHOLDERS = 'stakeholders', -} diff --git a/source/frontend/src/features/mapSideBar/acquisition/ViewSelector.tsx b/source/frontend/src/features/mapSideBar/acquisition/ViewSelector.tsx deleted file mode 100644 index f067aa8ed1..0000000000 --- a/source/frontend/src/features/mapSideBar/acquisition/ViewSelector.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { FormikProps } from 'formik'; -import React from 'react'; - -import { FileTypes } from '@/constants'; -import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; - -import { InventoryTabNames, InventoryTabs } from '../property/InventoryTabs'; -import { UpdatePropertyDetailsContainer } from '../property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer'; -import TakesUpdateContainer from '../property/tabs/takes/update/TakesUpdateContainer'; -import TakesUpdateForm from '../property/tabs/takes/update/TakesUpdateForm'; -import { FileTabType } from '../shared/detail/FileTabs'; -import PropertyFileContainer from '../shared/detail/PropertyFileContainer'; -import { AcquisitionContainerState } from './AcquisitionContainer'; -import { EditFormType } from './EditFormNames'; -import AcquisitionFileTabs from './tabs/AcquisitionFileTabs'; -import { UpdateAgreementsContainer } from './tabs/agreement/update/UpdateAgreementsContainer'; -import { UpdateAgreementsForm } from './tabs/agreement/update/UpdateAgreementsForm'; -import { UpdateAcquisitionChecklistContainer } from './tabs/checklist/update/UpdateAcquisitionChecklistContainer'; -import { UpdateAcquisitionChecklistForm } from './tabs/checklist/update/UpdateAcquisitionChecklistForm'; -import UpdateAcquisitionContainer from './tabs/fileDetails/update/UpdateAcquisitionContainer'; -import UpdateAcquisitionForm from './tabs/fileDetails/update/UpdateAcquisitionForm'; -import UpdateStakeHolderContainer from './tabs/stakeholders/update/UpdateStakeHolderContainer'; -import UpdateStakeHolderForm from './tabs/stakeholders/update/UpdateStakeHolderForm'; - -export interface IViewSelectorProps { - acquisitionFile?: Api_AcquisitionFile; - isEditing: boolean; - activeEditForm?: EditFormType; - selectedMenuIndex: number; - defaultFileTab: FileTabType; - defaultPropertyTab: InventoryTabNames; - setContainerState: (value: Partial) => void; - onSuccess: () => void; - ref: React.RefObject>; -} - -export const ViewSelector = React.forwardRef, IViewSelectorProps>( - (props, formikRef) => { - // render edit forms - if (props.isEditing && !!props.acquisitionFile) { - // File-based tabs - if (props.selectedMenuIndex === 0) { - switch (props.activeEditForm) { - case EditFormType.ACQUISITION_CHECKLIST: - return ( - - ); - - case EditFormType.ACQUISITION_SUMMARY: - return ( - - ); - - case EditFormType.AGREEMENTS: - return ( - - props.setContainerState({ - isEditing: false, - activeEditForm: undefined, - }) - } - /> - ); - - case EditFormType.STAKEHOLDERS: - return ( - - props.setContainerState({ - isEditing: false, - activeEditForm: undefined, - }) - } - /> - ); - - default: - throw Error('Active edit form not defined'); - } - } else { - // Property-based tabs - const propertyFile = getAcquisitionFileProperty( - props.acquisitionFile, - props.selectedMenuIndex, - ); - switch (props.activeEditForm) { - case EditFormType.PROPERTY_DETAILS: - if (propertyFile?.property?.id === undefined) { - throw Error('Cannot edit property without a valid id'); - } - return ( - - ); - case EditFormType.TAKES: - return ( - - props.setContainerState({ - isEditing: false, - activeEditForm: undefined, - }) - } - /> - ); - - default: - throw Error('Active edit form not defined'); - } - } - } else { - // render read-only views - if (props.selectedMenuIndex === 0) { - return ( - - ); - } else { - if (!!props.acquisitionFile) { - return ( - - props.setContainerState({ - isEditing: true, - activeEditForm: EditFormType.PROPERTY_DETAILS, - defaultPropertyTab: InventoryTabNames.property, - }) - } - setEditTakes={() => - props.setContainerState({ - isEditing: true, - activeEditForm: EditFormType.TAKES, - defaultPropertyTab: InventoryTabNames.takes, - }) - } - fileProperty={getAcquisitionFileProperty( - props.acquisitionFile, - props.selectedMenuIndex, - )} - defaultTab={props.defaultPropertyTab} - customTabs={[]} - View={InventoryTabs} - fileContext={FileTypes.Acquisition} - /> - ); - } else { - return null; - } - } - } - }, -); - -const getAcquisitionFileProperty = ( - acquisitionFile: Api_AcquisitionFile, - selectedMenuIndex: number, -) => { - const properties = acquisitionFile?.fileProperties || []; - const selectedPropertyIndex = selectedMenuIndex - 1; - const acquisitionFileProperty = properties[selectedPropertyIndex]; - if (!!acquisitionFileProperty.file) { - acquisitionFileProperty.file = acquisitionFile; - } - return acquisitionFileProperty; -}; - -export default ViewSelector; diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.test.tsx index 67acb2c0ca..8f4d065c25 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.test.tsx @@ -1,27 +1,29 @@ import { Claims } from '@/constants/index'; import { render, RenderOptions, userEvent } from '@/utils/test-utils'; -import { EditFormType } from '../EditFormNames'; import AcquisitionMenu, { IAcquisitionMenuProps } from './AcquisitionMenu'; // mock auth library jest.mock('@react-keycloak/web'); const onChange = jest.fn(); -const setContainerState = jest.fn(); +const onShowPropertySelector = jest.fn(); const testData = ['one', 'two', 'three']; describe('AcquisitionMenu component', () => { // render component under test - const setup = (props: IAcquisitionMenuProps, renderOptions: RenderOptions = {}) => { + const setup = ( + props: Omit, + renderOptions: RenderOptions = {}, + ) => { const utils = render( , { useMockAuthentication: true, @@ -42,8 +44,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 0, - onChange, - setContainerState, }); expect(asFragment()).toMatchSnapshot(); }); @@ -53,8 +53,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 0, - onChange, - setContainerState, }); expect(getByText('one')).toBeVisible(); @@ -67,8 +65,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 1, - onChange, - setContainerState, }); expect(getByTestId('menu-item-row-0')).not.toHaveClass('selected'); @@ -81,8 +77,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 1, - onChange, - setContainerState, }); const lastItem = getByText('three'); @@ -97,8 +91,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 1, - onChange, - setContainerState, }, { claims: [Claims.ACQUISITION_EDIT] }, ); @@ -108,10 +100,7 @@ describe('AcquisitionMenu component', () => { userEvent.click(button); - expect(setContainerState).toHaveBeenCalledWith({ - isEditing: true, - activeEditForm: EditFormType.PROPERTY_SELECTOR, - }); + expect(onShowPropertySelector).toHaveBeenCalled(); }); it(`doesn't render the edit button for users without edit permissions`, () => { @@ -120,8 +109,6 @@ describe('AcquisitionMenu component', () => { acquisitionFileId: 1, items: testData, selectedIndex: 1, - onChange, - setContainerState, }, { claims: [Claims.ACQUISITION_VIEW] }, // no edit permissions, just view. ); diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx index 69524f3d4d..c951a8e648 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx @@ -8,8 +8,6 @@ import EditButton from '@/components/common/EditButton'; import { Claims } from '@/constants/index'; import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; -import { AcquisitionContainerState } from '../AcquisitionContainer'; -import { EditFormType } from '../EditFormNames'; import GenerateFormContainer from './GenerateForm/GenerateFormContainer'; import GenerateFormView from './GenerateForm/GenerateFormView'; @@ -18,7 +16,7 @@ export interface IAcquisitionMenuProps { items: string[]; selectedIndex: number; onChange: (index: number) => void; - setContainerState: (value: Partial) => void; + onShowPropertySelector: () => void; } const AcquisitionMenu: React.FunctionComponent< @@ -31,41 +29,49 @@ const AcquisitionMenu: React.FunctionComponent< return ( <> - {props.items.map((label: string, index: number) => ( - (props.selectedIndex !== index ? handleClick(index) : '')} - > - {props.selectedIndex === index && } - {index !== 0 && ( - - - {index} - - - )} - {label} - {index === 0 && ( - - Properties - {hasClaim(Claims.ACQUISITION_EDIT) && ( - } - onClick={() => { - props.setContainerState({ - isEditing: true, - activeEditForm: EditFormType.PROPERTY_SELECTOR, - }); - }} - /> - )} - - )} - - ))} + {props.items.map((label: string, index: number) => { + if (index === 0) { + return ( + + {props.selectedIndex === index && } + (props.selectedIndex !== index ? handleClick(index) : '')}> + {label} + + + Properties + {hasClaim(Claims.ACQUISITION_EDIT) && ( + } + onClick={props.onShowPropertySelector} + /> + )} + + + ); + } else { + return ( + (props.selectedIndex !== index ? handleClick(index) : '')} + > + {props.selectedIndex === index && } + + + {index} + + + {label} + + ); + } + })} diff --git a/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx b/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx new file mode 100644 index 0000000000..a6c5182397 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx @@ -0,0 +1,104 @@ +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_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { stripTrailingSlash } from '@/utils'; + +import { AcquisitionFileTabs } from '../tabs/AcquisitionFileTabs'; +import { UpdateAgreementsContainer } from '../tabs/agreement/update/UpdateAgreementsContainer'; +import { UpdateAgreementsForm } from '../tabs/agreement/update/UpdateAgreementsForm'; +import { UpdateAcquisitionChecklistContainer } from '../tabs/checklist/update/UpdateAcquisitionChecklistContainer'; +import { UpdateAcquisitionChecklistForm } from '../tabs/checklist/update/UpdateAcquisitionChecklistForm'; +import { UpdateAcquisitionContainer } from '../tabs/fileDetails/update/UpdateAcquisitionContainer'; +import { UpdateAcquisitionForm } from '../tabs/fileDetails/update/UpdateAcquisitionForm'; +import { UpdateStakeHolderContainer } from '../tabs/stakeholders/update/UpdateStakeHolderContainer'; +import { UpdateStakeHolderForm } from '../tabs/stakeholders/update/UpdateStakeHolderForm'; + +export interface IAcquisitionRouterProps { + formikRef: React.Ref>; + acquisitionFile?: Api_AcquisitionFile; + isEditing: boolean; + setIsEditing: (value: boolean) => void; + defaultFileTab: FileTabType; + defaultPropertyTab: InventoryTabNames; + onSuccess: () => void; +} + +export const AcquisitionRouter: React.FC = props => { + const { path, url } = useRouteMatch(); + + if (props.acquisitionFile === undefined || props.acquisitionFile === null) { + return null; + } + + // render edit forms + if (props.isEditing) { + return ( + + + + + + + + + props.setIsEditing(false)} + /> + + + props.setIsEditing(false)} + /> + + {/* 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 AcquisitionRouter; diff --git a/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx b/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx new file mode 100644 index 0000000000..1f60da67bb --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx @@ -0,0 +1,112 @@ +import { FormikProps } from 'formik'; +import React from 'react'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; +import { toast } from 'react-toastify'; + +import { FileTypes } from '@/constants'; +import { InventoryTabNames, InventoryTabs } from '@/features/mapSideBar/property/InventoryTabs'; +import { FileTabType } from '@/features/mapSideBar/shared/detail/FileTabs'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; + +import { UpdatePropertyDetailsContainer } from '../../property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer'; +import { TakesUpdateContainer } from '../../property/tabs/takes/update/TakesUpdateContainer'; +import { TakesUpdateForm } from '../../property/tabs/takes/update/TakesUpdateForm'; +import { PropertyFileContainer } from '../../shared/detail/PropertyFileContainer'; + +export interface IFilePropertyRouterProps { + formikRef: React.Ref>; + acquisitionFile?: Api_AcquisitionFile; + isEditing: boolean; + setIsEditing: (value: boolean) => void; + selectedMenuIndex: number; + defaultFileTab: FileTabType; + defaultPropertyTab: InventoryTabNames; + onSuccess: () => void; +} + +export const FilePropertyRouter: React.FC = props => { + const { path, url } = useRouteMatch(); + + if (props.acquisitionFile === undefined || props.acquisitionFile === null) { + return null; + } + + const fileProperty = getAcquisitionFileProperty(props.acquisitionFile, props.selectedMenuIndex); + + if (fileProperty == null) { + toast.warn('Could not find property in the file, showing file details instead', { + autoClose: 15000, + }); + return ; + } + + // render edit forms + if (props.isEditing) { + return ( + + + {(() => { + if (fileProperty?.property?.id === undefined) { + throw Error('Cannot edit property without a valid id'); + } + return ( + + ); + })()} + + + props.setIsEditing(false)} + /> + + + + ); + } else { + // render read-only views + return ( + + + props.setIsEditing(true)} + setEditTakes={() => props.setIsEditing(true)} + fileProperty={fileProperty} + defaultTab={props.defaultPropertyTab} + customTabs={[]} + View={InventoryTabs} + fileContext={FileTypes.Acquisition} + /> + + + + ); + } +}; + +const getAcquisitionFileProperty = ( + acquisitionFile: Api_AcquisitionFile, + selectedMenuIndex: number, +) => { + const properties = acquisitionFile?.fileProperties || []; + const selectedPropertyIndex = selectedMenuIndex - 1; + + if (selectedPropertyIndex < 0 || selectedPropertyIndex >= properties.length) { + return null; + } + + const acquisitionFileProperty = properties[selectedPropertyIndex]; + if (!!acquisitionFileProperty.file) { + acquisitionFileProperty.file = acquisitionFile; + } + return acquisitionFileProperty; +}; + +export default FilePropertyRouter; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.test.tsx index 7ec371469f..b3991a425e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.test.tsx @@ -1,28 +1,37 @@ +import { createMemoryHistory } from 'history'; +import { Route } from 'react-router-dom'; import { act } from 'react-test-renderer'; import Claims from '@/constants/claims'; +import { FileTabType } from '@/features/mapSideBar/shared/detail/FileTabs'; import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock'; -import { render, RenderOptions, userEvent, waitFor } from '@/utils/test-utils'; +import { render, RenderOptions, userEvent } from '@/utils/test-utils'; -import { FileTabType } from '../../shared/detail/FileTabs'; import AcquisitionFileTabs, { IAcquisitionFileTabsProps } from './AcquisitionFileTabs'; // mock auth library jest.mock('@react-keycloak/web'); -const setContainerState = jest.fn(); +const history = createMemoryHistory(); +const setIsEditing = jest.fn(); describe('AcquisitionFileTabs component', () => { // render component under test - const setup = (props: IAcquisitionFileTabsProps, renderOptions: RenderOptions = {}) => { + const setup = ( + props: Omit, + renderOptions: RenderOptions = {}, + ) => { const utils = render( - , + + + , { useMockAuthentication: true, + history, ...renderOptions, }, ); @@ -30,6 +39,10 @@ describe('AcquisitionFileTabs component', () => { return { ...utils }; }; + beforeEach(() => { + history.replace(`/blah/${FileTabType.FILE_DETAILS}`); + }); + afterEach(() => { jest.resetAllMocks(); }); @@ -39,7 +52,6 @@ describe('AcquisitionFileTabs component', () => { { acquisitionFile: mockAcquisitionFileResponse(), defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }, { claims: [Claims.DOCUMENT_VIEW] }, ); @@ -51,13 +63,12 @@ describe('AcquisitionFileTabs component', () => { { acquisitionFile: mockAcquisitionFileResponse(), defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }, { claims: [Claims.DOCUMENT_VIEW] }, ); - const editButton = getByText('Documents'); - expect(editButton).toBeVisible(); + const tab = getByText('Documents'); + expect(tab).toBeVisible(); }); it('documents tab can be changed to', async () => { @@ -65,25 +76,21 @@ describe('AcquisitionFileTabs component', () => { { acquisitionFile: mockAcquisitionFileResponse(), defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }, { claims: [Claims.DOCUMENT_VIEW] }, ); - const editButton = getByText('Documents'); - await act(async () => { - userEvent.click(editButton); - }); - await waitFor(() => { - expect(getByText('Documents')).toHaveClass('active'); - }); + const tab = getByText('Documents'); + await act(async () => userEvent.click(tab)); + + expect(getByText('Documents')).toHaveClass('active'); + expect(history.location.pathname).toBe(`/blah/${FileTabType.DOCUMENTS}`); }); it('hides the expropriation tab when the Acquisition file type is "Consensual Agreement"', () => { const { queryByText } = setup({ acquisitionFile: mockAcquisitionFileResponse(), defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }); const expropriationButton = queryByText('Expropriation'); @@ -101,7 +108,6 @@ describe('AcquisitionFileTabs component', () => { const { queryByText } = setup({ acquisitionFile: mockAcquisitionFile, defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }); const editButton = queryByText('Expropriation'); @@ -119,7 +125,6 @@ describe('AcquisitionFileTabs component', () => { const { queryByText } = setup({ acquisitionFile: mockAcquisitionFile, defaultTab: FileTabType.FILE_DETAILS, - setContainerState, }); const editButton = queryByText('Expropriation'); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx index 5371365304..11ced18708 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx @@ -1,17 +1,15 @@ -import React, { useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import React from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import { Claims } from '@/constants/claims'; import { FileTypes } from '@/constants/fileTypes'; import { NoteTypes } from '@/constants/noteTypes'; +import { FileTabs, FileTabType, TabFileView } from '@/features/mapSideBar/shared/detail/FileTabs'; import NoteListView from '@/features/notes/list/NoteListView'; import ActivityListView from '@/features/properties/map/activity/list/ActivityListView'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { Api_AcquisitionFile, EnumAcquisitionFileType } from '@/models/api/AcquisitionFile'; -import { FileTabs, FileTabType, TabFileView } from '../../shared/detail/FileTabs'; -import { AcquisitionContainerState } from '../AcquisitionContainer'; -import { EditFormType } from '../EditFormNames'; import AgreementContainer from './agreement/detail/AgreementContainer'; import AgreementView from './agreement/detail/AgreementView'; import { AcquisitionChecklistView } from './checklist/detail/AcquisitionChecklistView'; @@ -27,32 +25,31 @@ import StakeHolderView from './stakeholders/detail/StakeHolderView'; export interface IAcquisitionFileTabsProps { acquisitionFile?: Api_AcquisitionFile; defaultTab: FileTabType; - setContainerState: (value: Partial) => void; + setIsEditing: (value: boolean) => void; } export const AcquisitionFileTabs: React.FC = ({ acquisitionFile, defaultTab, - setContainerState, + setIsEditing, }) => { const tabViews: TabFileView[] = []; const { hasClaim } = useKeycloakWrapper(); - const [activeTab, setActiveTab] = useState(defaultTab); + const location = useLocation(); const history = useHistory(); + 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}`); + } + }; tabViews.push({ content: ( - - setContainerState({ - isEditing: true, - activeEditForm: EditFormType.ACQUISITION_SUMMARY, - defaultFileTab: FileTabType.FILE_DETAILS, - }) - } - /> + setIsEditing(true)} /> ), key: FileTabType.FILE_DETAILS, name: 'File details', @@ -62,13 +59,7 @@ export const AcquisitionFileTabs: React.FC = ({ content: ( - setContainerState({ - isEditing: true, - activeEditForm: EditFormType.ACQUISITION_CHECKLIST, - defaultFileTab: FileTabType.CHECKLIST, - }) - } + onEdit={() => setIsEditing(true)} /> ), key: FileTabType.CHECKLIST, @@ -110,13 +101,7 @@ export const AcquisitionFileTabs: React.FC = ({ - setContainerState({ - isEditing: true, - activeEditForm: EditFormType.AGREEMENTS, - defaultFileTab: FileTabType.AGREEMENTS, - }) - } + onEdit={() => setIsEditing(true)} > ), key: FileTabType.AGREEMENTS, @@ -129,13 +114,7 @@ export const AcquisitionFileTabs: React.FC = ({ content: ( - setContainerState({ - isEditing: true, - activeEditForm: EditFormType.STAKEHOLDERS, - defaultFileTab: FileTabType.STAKEHOLDERS, - }) - } + onEdit={() => setIsEditing(true)} acquisitionFile={acquisitionFile} > ), @@ -177,13 +156,11 @@ export const AcquisitionFileTabs: React.FC = ({ const onSetActiveTab = (tab: FileTabType) => { let previousTab = activeTab; - setActiveTab(tab); - setContainerState({ defaultFileTab: tab }); - if (previousTab === FileTabType.COMPENSATIONS) { - const backUrl = history.location.pathname.split('compensation-requisition')[0]; + const backUrl = location.pathname.split('/compensation-requisition')[0]; history.push(backUrl); } + setActiveTab(tab); }; return ( diff --git a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx index b70cb27cc5..4aca01e3c1 100644 --- a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx +++ b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx @@ -18,9 +18,9 @@ export interface IInventoryTabsProps { } export enum InventoryTabNames { - property = 'property', - title = 'title', - value = 'value', + property = 'details', + title = 'ltsa', + value = 'bcassessment', research = 'research', pims = 'pims', takes = 'takes', @@ -38,7 +38,9 @@ export const InventoryTabs: React.FunctionComponent< activeKey={activeTab} onSelect={(eventKey: string | null) => { const tab = Object.values(InventoryTabNames).find(value => value === eventKey); - tab && setActiveTab(tab); + if (tab && tab !== activeTab) { + setActiveTab(tab); + } }} > {tabViews.map((view: TabInventoryView, index: number) => ( diff --git a/source/frontend/src/features/mapSideBar/router/CompensationRequisitionRouter.tsx b/source/frontend/src/features/mapSideBar/router/CompensationRequisitionRouter.tsx index ea9d586471..e73185f582 100644 --- a/source/frontend/src/features/mapSideBar/router/CompensationRequisitionRouter.tsx +++ b/source/frontend/src/features/mapSideBar/router/CompensationRequisitionRouter.tsx @@ -32,7 +32,7 @@ export const CompensationRequisitionRouter: React.FunctionComponent< }, [matched, props]); const onClose = () => { - const backUrl = history.location.pathname.split('compensation-requisition')[0]; + const backUrl = location.pathname.split('/compensation-requisition')[0]; history.push(backUrl); }; diff --git a/source/frontend/src/features/mapSideBar/shared/detail/FileTabs.tsx b/source/frontend/src/features/mapSideBar/shared/detail/FileTabs.tsx index 7e53b90fae..a481e9a102 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/FileTabs.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/FileTabs.tsx @@ -44,7 +44,9 @@ export const FileTabs: React.FunctionComponent { const tab = Object.values(FileTabType).find(value => value === eventKey); - tab && setActiveTab(tab); + if (tab && tab !== activeTab) { + setActiveTab(tab); + } }} > {tabViews.map((view: TabFileView, index: number) => ( diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index 81954576ed..f985b9527f 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; import { FileTypes } from '@/constants/fileTypes'; import { usePropertyDetails } from '@/features/mapSideBar/hooks/usePropertyDetails'; @@ -25,6 +26,7 @@ export interface IPropertyFileContainerProps { customTabs: TabInventoryView[]; defaultTab: InventoryTabNames; fileContext?: FileTypes; + withRouter?: boolean; } export const PropertyFileContainer: React.FunctionComponent< @@ -120,8 +122,26 @@ export const PropertyFileContainer: React.FunctionComponent< }); } - const [activeTab, setActiveTab] = useState(props.defaultTab); const InventoryTabsView = props.View; + let activeTab: InventoryTabNames; + let setActiveTab: (tab: InventoryTabNames) => void; + + // Use state-based tabs OR route-based tabs (as passed in the 'withRouter' property) + const [activeTabState, setActiveTabState] = useState(props.defaultTab); + const history = useHistory(); + const params = useParams<{ tab?: string }>(); + + if (!!props.withRouter) { + activeTab = Object.values(InventoryTabNames).find(t => t === params.tab) ?? props.defaultTab; + setActiveTab = (tab: InventoryTabNames) => { + if (activeTab !== tab) { + history.push(`${tab}`); + } + }; + } else { + activeTab = activeTabState; + setActiveTab = setActiveTabState; + } return ( { const { search } = useLocation(); - return useMemo(() => queryString.parse(search), [search]); + return useMemo(() => new URLSearchParams(search), [search]); }; diff --git a/source/frontend/src/utils/utils.ts b/source/frontend/src/utils/utils.ts index 9c4516d415..cce7862534 100644 --- a/source/frontend/src/utils/utils.ts +++ b/source/frontend/src/utils/utils.ts @@ -8,6 +8,16 @@ import { SelectOption } from '@/components/common/form'; import { TableSort } from '@/components/Table/TableSort'; import { logError, logRequest, logSuccess } from '@/store/slices/network/networkSlice'; +/** + * Removes a trailing slash from a string. + * Useful when creating nested URLs or routes. + * @param value The input string + * @returns The string without trailing slash + */ +export function stripTrailingSlash(value: string) { + return value ? value.replace(/\/$/, '') : value; +} + /** * Rounds the supplied number to a certain number of decimal places * @param value The number to round