diff --git a/source/frontend/src/components/common/ExpandableTextList.tsx b/source/frontend/src/components/common/ExpandableTextList.tsx index 6ed2f05a70..644164889d 100644 --- a/source/frontend/src/components/common/ExpandableTextList.tsx +++ b/source/frontend/src/components/common/ExpandableTextList.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useState } from 'react'; +import { Fragment, ReactElement, useState } from 'react'; import { exists } from '@/utils/utils'; @@ -10,6 +10,9 @@ export interface IExpandableTextListProps { keyFunction: (item: T, index: number) => string; delimiter?: ReactElement | string; maxCollapsedLength?: number; + className?: string; + moreText?: string; + hideText?: string; } /** @@ -22,12 +25,14 @@ export function ExpandableTextList({ renderFunction, delimiter, maxCollapsedLength, + moreText, + hideText, }: IExpandableTextListProps) { const [isExpanded, setIsExpanded] = useState(false); const displayedItemsLength = !isExpanded ? maxCollapsedLength ?? items.length : items.length; const displayedItems = items.slice(0, displayedItemsLength); return ( -
+ {displayedItems.map((item: T, index: number) => ( {renderFunction(item, index)} @@ -36,10 +41,12 @@ export function ExpandableTextList({ ))} {exists(maxCollapsedLength) && maxCollapsedLength < items.length && ( setIsExpanded(collapse => !collapse)}> - {isExpanded ? 'hide' : `[+${items.length - displayedItemsLength} more...]`} + {isExpanded + ? hideText ?? 'hide' + : `[+${items.length - displayedItemsLength} ${moreText ?? 'more...'}]`} )} -
+ ); } diff --git a/source/frontend/src/features/disposition/list/DispositionSearchResults/__snapshots__/DispositionSearchResults.test.tsx.snap b/source/frontend/src/features/disposition/list/DispositionSearchResults/__snapshots__/DispositionSearchResults.test.tsx.snap index ee68e74cc4..6b7ffe8086 100644 --- a/source/frontend/src/features/disposition/list/DispositionSearchResults/__snapshots__/DispositionSearchResults.test.tsx.snap +++ b/source/frontend/src/features/disposition/list/DispositionSearchResults/__snapshots__/DispositionSearchResults.test.tsx.snap @@ -424,14 +424,12 @@ exports[`Disposition search results table > matches snapshot 1`] = ` style="box-sizing: border-box; flex: 40 0 auto; min-width: 30px; width: 40px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" title="" > -
- - Alejandro Sanchez (MoTI Solicitor), - - - Aman Monga (Negotiation agent) - -
+ + Alejandro Sanchez (MoTI Solicitor), + + + Aman Monga (Negotiation agent) +
matches snapshot 1`] = ` style="box-sizing: border-box; flex: 40 0 auto; min-width: 30px; width: 40px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" title="" > -
- - Alejandro Sanchez (MoTI Solicitor), - - - Aman Monga (Negotiation agent) - -
+ + Alejandro Sanchez (MoTI Solicitor), + + + Aman Monga (Negotiation agent) +
matches snapshot 1`] = ` role="cell" style="box-sizing: border-box; flex: 30 0 auto; min-width: 30px; width: 30px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" title="" - > -
-
+ />
matches snapshot 1`] = ` role="cell" style="box-sizing: border-box; flex: 30 0 auto; min-width: 30px; width: 30px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" title="" - > -
-
+ />
renders as expected 1`] = `
-
-
+ />
renders as expected when no data is provi
-
-
+ />
renders as expected 1`] = `
-
-
+ />
renders as expected when no data is provi
-
-
+ />
= ({ lease, lastUpdatedBy export default LeaseHeader; const HistoricalRow = styled(Row)` - max-height: 10rem; - overflow-y: auto; margin-right: 1rem; `; diff --git a/source/frontend/src/features/mapSideBar/lease/common/__snapshots__/LeaseHeader.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/common/__snapshots__/LeaseHeader.test.tsx.snap index e30884e654..0ccc122371 100644 --- a/source/frontend/src/features/mapSideBar/lease/common/__snapshots__/LeaseHeader.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/lease/common/__snapshots__/LeaseHeader.test.tsx.snap @@ -236,8 +236,6 @@ exports[`LeaseHeader component > renders as expected when no data is provided 1` } .c4 { - max-height: 10rem; - overflow-y: auto; margin-right: 1rem; } @@ -304,9 +302,7 @@ exports[`LeaseHeader component > renders as expected when no data is provided 1`
-
-
+ />
renders as expected when no data is provided 1`
-
- - French Mouse Property Management -
-
- -
+ [+2 more...] +
+
renders as expected when no data is provided 1`
-
-
+ />
diff --git a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap index 6679176ea2..2df954f805 100644 --- a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap @@ -761,9 +761,7 @@ exports[`ResearchContainer component > renders as expected 1`] = `
-
-
+ />
renders as expected when provided no researc
-
-
+ />
({ + getPropertyHistoricalNumbers: mockGetApi, + updatePropertyHistoricalNumbers: {} as any, +})); + +const onSuccess = vi.fn(); + +describe('HistoricalNumberContainer component', () => { + // render component under test + + let viewProps: IHistoricalNumbersViewProps; + const View = forwardRef, IHistoricalNumbersViewProps>((props, ref) => { + viewProps = props; + return <>; + }); + + const setup = ( + renderOptions: RenderOptions & { props?: Partial }, + ) => { + const utils = render( + , + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + afterEach(() => { + vi.clearAllMocks(); + }); + + it('calls hist file number api for every propertyId', async () => { + setup({ props: { propertyIds: [1, 2, 3] } }); + + await waitFor(async () => { + expect(mockGetApi.execute).toHaveBeenCalledWith(1); + expect(mockGetApi.execute).toHaveBeenCalledWith(2); + expect(mockGetApi.execute).toHaveBeenCalledWith(3); + }); + }); + + it('passes the list of historical file numbers to the view', async () => { + mockGetApi.execute.mockResolvedValue(['hist1', 'hist2']); + setup({ props: { propertyIds: [1] } }); + + await waitFor(() => expect(viewProps.historicalNumbers).toEqual(['hist1', 'hist2'])); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.test.tsx b/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.test.tsx new file mode 100644 index 0000000000..a6f924d3f6 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.test.tsx @@ -0,0 +1,185 @@ +import { createMemoryHistory } from 'history'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { getByTestId, getByText, render, RenderOptions } from '@/utils/test-utils'; + +import { vi } from 'vitest'; +import { + HistoricalNumberFieldView, + IHistoricalNumbersViewProps, +} from './HistoricalNumberFieldView'; +import { mockHistoricalFileNumber } from '@/mocks/historicalFileNumber.mock'; +import { ApiGen_CodeTypes_HistoricalFileNumberTypes } from '@/models/api/generated/ApiGen_CodeTypes_HistoricalFileNumberTypes'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockNumberOne = mockHistoricalFileNumber( + 1, + 1, + '123', + ApiGen_CodeTypes_HistoricalFileNumberTypes.LISNO, + 'LIS', +); + +describe('HistoricalNumberFieldView component', () => { + // render component under test + + const setup = ( + renderOptions: RenderOptions & { props?: Partial }, + ) => { + const utils = render( + , + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders nothing when passed an empty list of historical numbers', () => { + const { container } = setup({}); + expect(container.textContent).toBe(''); + }); + + it('renders the other description instead of other when historical number is of type other', () => { + const { container } = setup({ + props: { + historicalNumbers: [ + mockHistoricalFileNumber( + 1, + 1, + 'hist-1', + ApiGen_CodeTypes_HistoricalFileNumberTypes.OTHER, + 'Other', + 'oth-description', + ), + ], + }, + }); + expect(getByText(container, 'oth-description', { exact: false })).toBeVisible(); + expect(getByText(container, 'hist-1')).toBeVisible(); + }); + + it('groups multiple historical file numbers if in same category', () => { + const { container } = setup({ + props: { + historicalNumbers: [ + mockHistoricalFileNumber( + 1, + 1, + '123', + ApiGen_CodeTypes_HistoricalFileNumberTypes.LISNO, + 'LIS', + ), + mockHistoricalFileNumber( + 2, + 2, + '456', + ApiGen_CodeTypes_HistoricalFileNumberTypes.LISNO, + 'LIS', + ), + ], + }, + }); + expect(getByTestId(container, 'historical-group-LISNO-').textContent).toBe('LIS:123; 456'); + }); + + it('collapses duplicate numbers', () => { + const { container } = setup({ + props: { + historicalNumbers: [mockNumberOne], + }, + }); + expect(getByTestId(container, 'historical-group-LISNO-').textContent).toBe('LIS:123'); + }); + + it('displays +1 more when there are more then 3 entries', () => { + const { container } = setup({ + props: { + historicalNumbers: [ + { ...mockNumberOne, historicalFileNumber: '1', id: 1 }, + { ...mockNumberOne, historicalFileNumber: '2', id: 2 }, + { ...mockNumberOne, historicalFileNumber: '3', id: 3 }, + { ...mockNumberOne, historicalFileNumber: '4', id: 4 }, + ], + }, + }); + expect(getByTestId(container, 'historical-group-LISNO-').textContent).toBe( + 'LIS:1; 2; 3; [+1 more...]', + ); + }); + + it('displays one line per category', () => { + const { container } = setup({ + props: { + historicalNumbers: [ + mockHistoricalFileNumber( + 1, + 1, + '123', + ApiGen_CodeTypes_HistoricalFileNumberTypes.LISNO, + 'LIS', + ), + mockHistoricalFileNumber( + 2, + 2, + '456', + ApiGen_CodeTypes_HistoricalFileNumberTypes.PSNO, + 'PS', + ), + ], + }, + }); + expect(getByTestId(container, 'historical-group-LISNO-').textContent).toBe('LIS:123'); + expect(getByTestId(container, 'historical-group-PSNO-').textContent).toBe('PS:456'); + }); + + it('displays [+x more...] if there are more then 2 categories', () => { + const { container } = setup({ + props: { + historicalNumbers: [ + mockHistoricalFileNumber( + 1, + 1, + '123', + ApiGen_CodeTypes_HistoricalFileNumberTypes.LISNO, + 'LIS', + ), + mockHistoricalFileNumber( + 2, + 2, + '456', + ApiGen_CodeTypes_HistoricalFileNumberTypes.PSNO, + 'PS', + ), + mockHistoricalFileNumber( + 2, + 2, + '789', + ApiGen_CodeTypes_HistoricalFileNumberTypes.PROPNEG, + 'Property Negotiation (PN)', + ), + ], + }, + }); + expect(getByTestId(container, 'historical-group-LISNO-').textContent).toBe('LIS:123'); + expect(getByTestId(container, 'historical-group-PSNO-').textContent).toBe('PS:456'); + expect(getByText(container, '[+1 more categories...]')).toBeVisible(); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.tsx b/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.tsx index c7f3d363b4..859b0ff21e 100644 --- a/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/header/HistoricalNumberFieldView.tsx @@ -22,7 +22,7 @@ interface HistoricalGroup { export const HistoricalNumberFieldView: React.FC = ({ historicalNumbers, }) => { - const uniqueNumbers = useMemo(() => { + const historicalNumberGroup = useMemo(() => { const flatNumberDictionary: Dictionary = historicalNumbers .filter(exists) .reduce((accumulator, h) => { @@ -53,36 +53,48 @@ export const HistoricalNumberFieldView: React.FC = return ( - items={uniqueNumbers} - keyFunction={(p, index: number) => - `historical-group-${p.historicalType.id}-${p.propertyKey}-${index}` - } - renderFunction={p => ( - <> - - {p.historicalType.id === ApiGen_CodeTypes_HistoricalFileNumberTypes.OTHER - ? p.otherDescription - : p.historicalType.description} + moreText="more categories..." + hideText="hide categories" + items={Object.values(historicalNumberGroup)} + keyFunction={p => `historical-number-group-${p?.historicalType?.id}-${p.otherDescription}`} + renderFunction={group => ( + + + {group.historicalType.id === ApiGen_CodeTypes_HistoricalFileNumberTypes.OTHER + ? group.otherDescription + : group.historicalType.description} : - - {Object.values(p.historicalValues).map((historicalValue, index) => { - return ( - - {historicalValue.historicalFileNumber} - {index + 1 < Object.values(p.historicalValues).length && , } - - ); - })} - + + items={Object.values(group.historicalValues)} + keyFunction={p => `historical-number-${p.id}`} + renderFunction={p => <>{p.historicalFileNumber}} + delimiter={'; '} + maxCollapsedLength={3} + className="d-flex flex-wrap" + /> + )} - delimiter={'; '} maxCollapsedLength={2} - /> + className="d-flex flex-wrap" + > ); }; export const StyledLabel = styled.label` font-weight: bold; margin-bottom: 0; + min-width: fit-content; +`; + +export const GroupWrapper = styled.div` + display: flex; + gap: 0.5rem; + align-items: baseline; + flex-wrap: wrap; `;