Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic components for search #935

Merged
merged 9 commits into from
Oct 31, 2024
35 changes: 34 additions & 1 deletion cypress/datasets/stopRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StopRegistryNameType,
} from '@hsl/jore4-test-db-manager';
import cloneDeep from 'lodash/cloneDeep';
import { DateTime } from 'luxon';
import { stopCoordinatesByLabel } from './base';

const coordinatesToStopRegistryGeoJSON = (
Expand Down Expand Up @@ -134,9 +135,41 @@ const stopPlaceData: Array<StopPlaceInput> = [
},
];

export const stopAreaX0003 = {
memberLabels: ['E2E001', 'E2E009'],
stopArea: {
name: { lang: 'fin', value: 'X0003' },
description: { lang: 'fin', value: 'Annankatu 15' },
validBetween: {
fromDate: DateTime.fromISO('2020-01-01T00:00:00.001'),
toDate: DateTime.fromISO('2050-01-01T00:00:00.001'),
},
geometry: {
coordinates: [24.938927, 60.165433],
type: StopRegistryGeoJsonType.Point,
},
},
};

export const stopAreaX0004 = {
memberLabels: ['E2E003', 'E2E006'],
stopArea: {
name: { lang: 'fin', value: 'X0004' },
description: { lang: 'fin', value: 'Kalevankatu 32' },
validBetween: {
fromDate: DateTime.fromISO('2020-01-01T00:00:00.001'),
toDate: DateTime.fromISO('2050-01-01T00:00:00.001'),
},
geometry: {
coordinates: [24.932914978884, 60.165538996581],
type: StopRegistryGeoJsonType.Point,
},
},
};

const baseStopRegistryData = {
organisations: [],
stopAreas: [],
stopAreas: [stopAreaX0003, stopAreaX0004],
stopPlaces: stopPlaceData,
};

Expand Down
35 changes: 9 additions & 26 deletions cypress/e2e/stop-registry/stopAreaDetails.cy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import {
StopAreaInput,
StopRegistryGeoJsonType,
} from '@hsl/jore4-test-db-manager';
import { DateTime } from 'luxon';
import {
buildInfraLinksAlongRoute,
buildStopsOnInfraLinks,
getClonedBaseDbResources,
testInfraLinkExternalIds,
} from '../../datasets/base';
import { getClonedBaseStopRegistryData } from '../../datasets/stopRegistry';
import {
getClonedBaseStopRegistryData,
stopAreaX0003,
} from '../../datasets/stopRegistry';
import {
SelectMemberStopsDropdown,
StopAreaDetailsPage,
Expand Down Expand Up @@ -49,23 +48,7 @@ describe('Stop area details', () => {
const baseDbResources = getClonedBaseDbResources();
const baseStopRegistryData = getClonedBaseStopRegistryData();

const testStopArea = {
memberLabels: ['E2E001', 'E2E009'],
stopArea: {
name: { lang: 'fin', value: 'X0003' },
description: { lang: 'fin', value: 'Annankatu 15' },
validBetween: {
fromDate: DateTime.fromISO('2020-01-01T00:00:00.001'),
toDate: DateTime.fromISO('2050-01-01T00:00:00.001'),
},
geometry: {
coordinates: [24.938927, 60.165433],
type: StopRegistryGeoJsonType.Point,
},
},
};

const stopAreaData: Array<StopAreaInput> = [testStopArea];
const testStopArea = { ...stopAreaX0003 };

const testAreaExpectedBasicDetails: ExpectedBasicDetails = {
name: testStopArea.stopArea.name.value,
Expand Down Expand Up @@ -100,10 +83,10 @@ describe('Stop area details', () => {

insertToDbHelper(dbResources);

cy.task<InsertedStopRegistryIds>('insertStopRegistryData', {
...baseStopRegistryData,
stopAreas: stopAreaData,
}).then((data) => {
cy.task<InsertedStopRegistryIds>(
'insertStopRegistryData',
baseStopRegistryData,
).then((data) => {
const id = data.stopAreaIdsByName.X0003;

cy.setupTests();
Expand Down
45 changes: 20 additions & 25 deletions cypress/e2e/stop-registry/stopSearch.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { getClonedBaseStopRegistryData } from '../../datasets/stopRegistry';
import { Tag } from '../../enums';
import {
StopGroupSelector,
StopSearchBar,
StopSearchByLine,
StopSearchResultsPage,
Expand All @@ -26,6 +27,8 @@ describe('Stop search', () => {
const stopSearchBar = new StopSearchBar();
const stopSearchResultsPage = new StopSearchResultsPage();
const stopSearchByLine = new StopSearchByLine();
const stopGroupSelector = new StopGroupSelector();

let dbResources: SupportedResources;

before(() => {
Expand Down Expand Up @@ -442,25 +445,16 @@ describe('Stop search', () => {
});
}

function shouldHaveLines(lines: ReadonlyArray<string>) {
const shouldHaveLength = stopSearchByLine
.getLineSelectors()
.should('have.length', lines.length);

lines.reduce(
(should, line) => should.and('contain', line),
shouldHaveLength,
);
}

function assertShowsAllResultsByDefault() {
stopSearchBar.getSearchInput().clearAndType(`LE*{enter}`);
expectGraphQLCallToSucceed('@gqlfindLinesByStopSearch');

// Should contain and show all LE -lines
shouldHaveLines(range(SHOW_ALL_BY_DEFAULT_MAX).map((i) => `LE${i}`));
stopSearchByLine.getShowAllLinesButton().should('not.exist');
stopSearchByLine.getShowLessLinesButton().should('not.exist');
stopGroupSelector.shouldHaveGroups(
range(SHOW_ALL_BY_DEFAULT_MAX).map((i) => `LE${i}`),
);
stopGroupSelector.getShowAllGroupsButton().should('not.exist');
stopGroupSelector.getShowLessGroupsButton().should('not.exist');
}

function assertShowAllAndShowLessWork(
Expand All @@ -473,26 +467,27 @@ describe('Stop search', () => {
// the list of shown labels.
cy.viewport(1000, 1080);
// prettier-ignore
shouldHaveLines(['L20', 'L21', 'L22', 'L23', 'L24', 'L25', 'L26', 'L27', 'L28', 'L29']);
stopGroupSelector
.shouldHaveGroups(['L20', 'L21', 'L22', 'L23', 'L24', 'L25', 'L26', 'L27', 'L28', 'L29']);

cy.viewport(500, 1080);
// prettier-ignore
const minimalResult = ['L20', 'L21', 'L22', 'L23', 'L24'];
shouldHaveLines(minimalResult);
stopGroupSelector.shouldHaveGroups(minimalResult);

stopSearchByLine.getShowLessLinesButton().should('not.exist');
stopSearchByLine
.getShowAllLinesButton()
stopGroupSelector.getShowLessGroupsButton().should('not.exist');
stopGroupSelector
.getShowAllGroupsButton()
.should('contain', `Näytä kaikki (${SHOW_ALL_BY_DEFAULT_MAX * 2})`)
.click();
stopSearchByLine.getShowLessLinesButton().shouldBeVisible();
stopSearchByLine.getShowAllLinesButton().should('not.exist');
shouldHaveLines(allExtraLines);
stopGroupSelector.getShowLessGroupsButton().shouldBeVisible();
stopGroupSelector.getShowAllGroupsButton().should('not.exist');
stopGroupSelector.shouldHaveGroups(allExtraLines);

stopSearchByLine.getShowLessLinesButton().click();
stopGroupSelector.getShowLessGroupsButton().click();

shouldHaveLines(minimalResult);
stopSearchByLine.getShowAllLinesButton().shouldBeVisible();
stopGroupSelector.shouldHaveGroups(minimalResult);
stopGroupSelector.getShowAllGroupsButton().shouldBeVisible();
}

it('should have a working asterisk search and line selector', () => {
Expand Down
25 changes: 25 additions & 0 deletions cypress/pageObjects/stop-registry/StopGroupSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class StopGroupSelector {
getGroupSelectors() {
return cy.get('[data-group-id][data-visible="true"]');
}

getShowAllGroupsButton() {
return cy.getByTestId('StopGroupSelector::showAllButton');
}

getShowLessGroupsButton() {
return cy.getByTestId('StopGroupSelector::showLessButton');
}

shouldHaveGroups(groups: ReadonlyArray<string>) {
const shouldHaveLength = this.getGroupSelectors().should(
'have.length',
groups.length,
);

groups.reduce(
(should, group) => should.and('contain', group),
shouldHaveLength,
);
}
}
12 changes: 0 additions & 12 deletions cypress/pageObjects/stop-registry/StopSearchByLine.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
export class StopSearchByLine {
getLineSelectors() {
return cy.get('[data-line-id][data-visible="true"]');
}

getShowAllLinesButton() {
return cy.getByTestId('StopSearchByLine::line::showAllButton');
}

getShowLessLinesButton() {
return cy.getByTestId('StopSearchByLine::line::showLessButton');
}

getActiveLineName() {
return cy.getByTestId('StopSearchByLine::line::name');
}
Expand Down
1 change: 1 addition & 0 deletions cypress/pageObjects/stop-registry/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './StopAreaDetailsPage';
export * from './stop-details';
export * from './StopDetailsPage';
export * from './StopGroupSelector';
export * from './StopSearchBar';
export * from './StopSearchByLine';
export * from './StopSearchResultsPage';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { resetSelectedRowsAction } from '../../../redux';
import { Path } from '../../../router/routeDetails';
import { StopsByLineSearchResults } from './by-line';
import { StopSearchByStopResults } from './by-stop';
import { StopSearchBar } from './StopSearchBar';
import { StopSearchBar } from './components';
import { SearchBy, StopSearchFilters } from './types';
import { useStopSearchUrlState } from './utils';

Expand Down
114 changes: 17 additions & 97 deletions ui/src/components/stop-registry/search/by-line/LineSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import { RadioGroup } from '@headlessui/react';
import { FC, useEffect, useRef, useState } from 'react';
import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { twJoin, twMerge } from 'tailwind-merge';
import { Visible } from '../../../../layoutComponents';
import { StopGroupSelector } from '../components';
import { FindStopByLineInfo } from './useFindLinesByStopSearch';
import { useVisibilityMap } from './useVisibilityMap';

const testIds = {
lineSelector: (lineId: UUID) =>
`StopSearchByLine::line::lineSelector::${lineId}`,
showAll: `StopSearchByLine::line::showAllButton`,
showLess: `StopSearchByLine::line::showLessButton`,
};

type LineSelectorProps = {
readonly activeLineId: UUID | null;
Expand All @@ -20,10 +10,6 @@ type LineSelectorProps = {
readonly setActiveLineId: (activeLineId: UUID | null) => void;
};

const SHOW_ALL_BY_DEFAULT_MAX = 20;
const MAX_PADDING = 5;
const NO_BREAK_SPACE = '\xa0';

export const LineSelector: FC<LineSelectorProps> = ({
activeLineId,
className,
Expand All @@ -32,89 +18,23 @@ export const LineSelector: FC<LineSelectorProps> = ({
}) => {
const { t } = useTranslation();

const showAllByDefault = lines.length <= SHOW_ALL_BY_DEFAULT_MAX;
const [showAll, setShowAll] = useState(showAllByDefault);
useEffect(() => setShowAll(showAllByDefault), [showAllByDefault]);

const lineListRef = useRef<HTMLDivElement | null>(null);
const visibilityMap = useVisibilityMap(showAll, lines, lineListRef);

const someLineIsHidden = Object.values(visibilityMap).some(
(visible) => !visible,
);

const longestLabel = Math.min(
Math.max(...lines.map((line) => line.label.length)),
MAX_PADDING,
const groups = useMemo(
() =>
lines.map(({ line_id: id, label, name_i18n: { fi_FI: title = '' } }) => ({
id,
label,
title,
})),
[lines],
);

return (
<RadioGroup
className={twMerge('flex gap-2', className)}
onChange={setActiveLineId}
value={activeLineId}
>
<RadioGroup.Label className="mt-2">
{t('stopRegistrySearch.lines')}
</RadioGroup.Label>

<div
className={twJoin(
'flex items-center gap-2 overflow-hidden',
showAll ? 'flex-wrap' : '',
)}
ref={lineListRef}
>
{lines.map(
({ line_id: lineId, label, name_i18n: { fi_FI: title } }) => (
<RadioGroup.Option
className={twJoin(
'cursor-pointer rounded border px-2 py-1 text-center font-bold text-dark-grey',
'ui-checked:border-brand ui-checked:bg-brand ui-checked:text-white',
'hover:border-black hover:bg-background-hsl-blue hover:text-black',
'font-mono', // Helps to align the items into a grid
showAll || visibilityMap[lineId] ? '' : 'invisible',
)}
data-line-id={lineId}
data-visible={visibilityMap[lineId]}
data-testid={testIds.lineSelector(lineId)}
id={`select-line-${lineId}`}
key={lineId}
title={title}
value={lineId}
>
{label.padEnd(
showAll && !showAllByDefault ? longestLabel : 0,
NO_BREAK_SPACE,
)}
</RadioGroup.Option>
),
)}

{/* Hide button is nested in here to render the button as a last element in the list. */}
<Visible visible={!showAllByDefault && showAll}>
<button
className="text-nowrap text-base font-bold text-brand"
data-testid={testIds.showLess}
onClick={() => setShowAll(false)}
type="button"
>
{t('stopRegistrySearch.showLessLines')}
</button>
</Visible>
</div>

{/* Show more button is outside the list as to exclude from the overflow calculations. */}
<Visible visible={!showAll && someLineIsHidden}>
<button
className="text-nowrap text-base font-bold text-brand"
data-testid={testIds.showAll}
onClick={() => setShowAll(true)}
type="button"
>
{t('stopRegistrySearch.showAllLines', { count: lines.length })}
</button>
</Visible>
</RadioGroup>
<StopGroupSelector
className={className}
groups={groups}
label={t('stopRegistrySearch.lines')}
onSelect={setActiveLineId}
selected={activeLineId}
/>
);
};
Loading
Loading