Skip to content

Commit 96ec10d

Browse files
Merge pull request #11 from MobilityData/feat/1604-license-filtering
Feat: License filtering in search
2 parents 0d8dcf3 + 40476fd commit 96ec10d

File tree

5 files changed

+713
-283
lines changed

5 files changed

+713
-283
lines changed

external_types/DatabaseCatalogAPI.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ paths:
338338
- $ref: "#/components/parameters/version_query_param"
339339
- $ref: "#/components/parameters/search_text_query_param"
340340
- $ref: "#/components/parameters/feature"
341+
- $ref: "#/components/parameters/license_ids_query_param"
341342
security:
342343
- Authentication: []
343344
responses:
@@ -1793,6 +1794,15 @@ components:
17931794
type: string
17941795
example: 2.3
17951796

1797+
license_ids_query_param:
1798+
name: license_ids
1799+
in: query
1800+
description: Comma separated list of license IDs to filter by.
1801+
required: False
1802+
schema:
1803+
type: string
1804+
example: CC-BY-4.0,CC0-1.0
1805+
17961806
securitySchemes:
17971807
Authentication:
17981808
$ref: "./BearerTokenSchema.yaml#/components/securitySchemes/Authentication"

src/app/screens/Feeds/AdvancedSearchTable.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,43 @@ export interface AdvancedSearchTableProps {
3131
selectedGbfsVersions: string[] | undefined;
3232
}
3333

34+
interface DetailsContainerProps {
35+
children: React.ReactNode;
36+
feedSearchItem: SearchFeedItem;
37+
}
38+
39+
const DetailsContainer = ({
40+
children,
41+
feedSearchItem,
42+
}: DetailsContainerProps): React.ReactElement => {
43+
return (
44+
<Box
45+
sx={{
46+
display: 'flex',
47+
justifyContent: 'space-between',
48+
alignItems: 'end',
49+
flexWrap: { xs: 'wrap', lg: 'nowrap' },
50+
}}
51+
>
52+
<Box sx={{ width: 'calc(100% - 100px' }}>{children}</Box>
53+
<Tooltip title={'Feed License'} placement={'top-end'}>
54+
<Typography
55+
variant='caption'
56+
sx={{
57+
opacity: 0.7,
58+
ml: { xs: 0, lg: 2 },
59+
minWidth: { xs: '100%', lg: '100px' },
60+
textAlign: 'right',
61+
pt: { xs: 1, lg: 0 },
62+
}}
63+
>
64+
{feedSearchItem.source_info?.license_id}
65+
</Typography>
66+
</Tooltip>
67+
</Box>
68+
);
69+
};
70+
3471
const renderGTFSDetails = (
3572
gtfsFeed: SearchFeedItem,
3673
selectedFeatures: string[],
@@ -39,7 +76,7 @@ const renderGTFSDetails = (
3976
const feedFeatures =
4077
gtfsFeed?.latest_dataset?.validation_report?.features ?? [];
4178
return (
42-
<>
79+
<DetailsContainer feedSearchItem={gtfsFeed}>
4380
{gtfsFeed?.feed_name != null && (
4481
<Typography
4582
variant='body1'
@@ -75,18 +112,20 @@ const renderGTFSDetails = (
75112
},
76113
)}
77114
</Box>
78-
</>
115+
</DetailsContainer>
79116
);
80117
};
81118

82119
const renderGTFSRTDetails = (
83120
gtfsRtFeed: SearchFeedItem,
84121
): React.ReactElement => {
85122
return (
86-
<GtfsRtEntities
87-
entities={gtfsRtFeed?.entity_types}
88-
includeName={true}
89-
></GtfsRtEntities>
123+
<DetailsContainer feedSearchItem={gtfsRtFeed}>
124+
<GtfsRtEntities
125+
entities={gtfsRtFeed?.entity_types}
126+
includeName={true}
127+
></GtfsRtEntities>
128+
</DetailsContainer>
90129
);
91130
};
92131

@@ -96,7 +135,7 @@ const renderGBFSDetails = (
96135
): React.ReactElement => {
97136
const theme = useTheme();
98137
return (
99-
<Box>
138+
<DetailsContainer feedSearchItem={gbfsFeedSearchElement}>
100139
{gbfsFeedSearchElement.versions?.map((version: string, index: number) => (
101140
<Chip
102141
label={'v' + version}
@@ -112,7 +151,7 @@ const renderGBFSDetails = (
112151
}}
113152
/>
114153
))}
115-
</Box>
154+
</DetailsContainer>
116155
);
117156
};
118157

src/app/screens/Feeds/SearchFilters.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ interface SearchFiltersProps {
2727
isOfficialFeedSearch: boolean;
2828
selectedFeatures: string[];
2929
selectedGbfsVersions: string[];
30+
selectedLicenses: string[];
3031
setSelectedFeedTypes: (selectedFeedTypes: Record<string, boolean>) => void;
3132
setIsOfficialFeedSearch: (isOfficialFeedSearch: boolean) => void;
3233
setSelectedFeatures: (selectedFeatures: string[]) => void;
3334
setSelectedGbfsVerions: (selectedVersions: string[]) => void;
35+
setSelectedLicenses: (selectedLicenses: string[]) => void;
3436
isOfficialTagFilterEnabled: boolean;
3537
areFeatureFiltersEnabled: boolean;
3638
areGBFSFiltersEnabled: boolean;
@@ -41,10 +43,12 @@ export function SearchFilters({
4143
isOfficialFeedSearch,
4244
selectedFeatures,
4345
selectedGbfsVersions,
46+
selectedLicenses,
4447
setSelectedFeedTypes,
4548
setIsOfficialFeedSearch,
4649
setSelectedFeatures,
4750
setSelectedGbfsVerions,
51+
setSelectedLicenses,
4852
isOfficialTagFilterEnabled,
4953
areFeatureFiltersEnabled,
5054
areGBFSFiltersEnabled,
@@ -61,6 +65,7 @@ export function SearchFilters({
6165
features: areFeatureFiltersEnabled,
6266
tags: isOfficialTagFilterEnabled,
6367
gbfsVersions: true,
68+
licenses: true,
6469
});
6570
const [featureCheckboxData, setFeatureCheckboxData] = useState<
6671
CheckboxStructure[]
@@ -279,6 +284,75 @@ export function SearchFilters({
279284
></NestedCheckboxList>
280285
</AccordionDetails>
281286
</Accordion>
287+
288+
<Accordion
289+
disableGutters
290+
variant={'outlined'}
291+
sx={{
292+
border: 0,
293+
'&::before': {
294+
display: 'none',
295+
},
296+
}}
297+
expanded={expandedCategories.licenses}
298+
onChange={() => {
299+
setExpandedCategories({
300+
...expandedCategories,
301+
licenses: !expandedCategories.licenses,
302+
});
303+
}}
304+
>
305+
<AccordionSummary
306+
expandIcon={<ExpandMoreIcon />}
307+
aria-controls='panel-licenses-content'
308+
sx={{
309+
px: 0,
310+
}}
311+
>
312+
<SearchHeader variant='h6'>Licenses</SearchHeader>
313+
</AccordionSummary>
314+
<AccordionDetails sx={{ p: 0, m: 0, border: 0 }}>
315+
<NestedCheckboxList
316+
debounceTime={500}
317+
checkboxData={[
318+
{
319+
title: 'CC-BY-4.0',
320+
checked: selectedLicenses.includes('CC-BY-4.0'),
321+
type: 'checkbox',
322+
},
323+
{
324+
title: 'etalab-2.0',
325+
checked: selectedLicenses.includes('etalab-2.0'),
326+
type: 'checkbox',
327+
},
328+
{
329+
title: 'CC0-1.0',
330+
checked: selectedLicenses.includes('CC0-1.0'),
331+
type: 'checkbox',
332+
},
333+
{
334+
title: 'ODbL-1.0',
335+
checked: selectedLicenses.includes('ODbL-1.0'),
336+
type: 'checkbox',
337+
},
338+
{
339+
title: 'OGL-UK-3.0',
340+
checked: selectedLicenses.includes('OGL-UK-3.0'),
341+
type: 'checkbox',
342+
},
343+
]}
344+
onCheckboxChange={(checkboxData) => {
345+
const selectedLicenseIds: string[] = [];
346+
checkboxData.forEach((checkbox) => {
347+
if (checkbox.checked) {
348+
selectedLicenseIds.push(checkbox.title);
349+
}
350+
});
351+
setSelectedLicenses([...selectedLicenseIds]);
352+
}}
353+
></NestedCheckboxList>
354+
</AccordionDetails>
355+
</Accordion>
282356
</>
283357
);
284358
}

src/app/screens/Feeds/index.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export default function Feed(): React.ReactElement {
6969
const [selectGbfsVersions, setSelectGbfsVersions] = useState<string[]>(
7070
searchParams.get('gbfs_versions')?.split(',') ?? [],
7171
);
72+
const [selectedLicenses, setSelectedLicenses] = useState<string[]>(
73+
searchParams.get('licenses')?.split(',') ?? [],
74+
);
7275
const [activePagination, setActivePagination] = useState(
7376
searchParams.get('o') !== null ? Number(searchParams.get('o')) : 1,
7477
);
@@ -130,6 +133,10 @@ export default function Feed(): React.ReactElement {
130133
version: areGBFSFiltersEnabled
131134
? selectGbfsVersions.join(',').replaceAll('v', '')
132135
: undefined,
136+
license_ids:
137+
selectedLicenses.length > 0
138+
? selectedLicenses.join(',')
139+
: undefined,
133140
},
134141
},
135142
}),
@@ -143,6 +150,7 @@ export default function Feed(): React.ReactElement {
143150
isOfficialFeedSearch,
144151
selectedFeatures,
145152
selectGbfsVersions,
153+
selectedLicenses,
146154
]);
147155

148156
useEffect(() => {
@@ -169,6 +177,9 @@ export default function Feed(): React.ReactElement {
169177
if (selectGbfsVersions.length > 0) {
170178
newSearchParams.set('gbfs_versions', selectGbfsVersions.join(','));
171179
}
180+
if (selectedLicenses.length > 0) {
181+
newSearchParams.set('licenses', selectedLicenses.join(','));
182+
}
172183
if (isOfficialFeedSearch) {
173184
newSearchParams.set('official', 'true');
174185
}
@@ -187,6 +198,7 @@ export default function Feed(): React.ReactElement {
187198
selectedFeedTypes,
188199
selectedFeatures,
189200
selectGbfsVersions,
201+
selectedLicenses,
190202
isOfficialFeedSearch,
191203
]);
192204

@@ -214,6 +226,11 @@ export default function Feed(): React.ReactElement {
214226
setSelectGbfsVersions([...newGbfsVersions]);
215227
}
216228

229+
const newLicenses = searchParams.get('licenses')?.split(',') ?? [];
230+
if (newLicenses.join(',') !== selectedLicenses.join(',')) {
231+
setSelectedLicenses([...newLicenses]);
232+
}
233+
217234
const newSearchOfficial = Boolean(searchParams.get('official')) ?? false;
218235
if (newSearchOfficial !== isOfficialFeedSearch) {
219236
setIsOfficialFeedSearch(newSearchOfficial);
@@ -265,6 +282,7 @@ export default function Feed(): React.ReactElement {
265282
});
266283
setSelectedFeatures([]);
267284
setSelectGbfsVersions([]);
285+
setSelectedLicenses([]);
268286
setIsOfficialFeedSearch(false);
269287
}
270288

@@ -400,6 +418,7 @@ export default function Feed(): React.ReactElement {
400418
isOfficialFeedSearch={isOfficialFeedSearch}
401419
selectedFeatures={selectedFeatures}
402420
selectedGbfsVersions={selectGbfsVersions}
421+
selectedLicenses={selectedLicenses}
403422
setSelectedFeedTypes={(feedTypes) => {
404423
setActivePagination(1);
405424
setSelectedFeedTypes({ ...feedTypes });
@@ -416,6 +435,10 @@ export default function Feed(): React.ReactElement {
416435
setSelectGbfsVersions(versions);
417436
setActivePagination(1);
418437
}}
438+
setSelectedLicenses={(licenses) => {
439+
setActivePagination(1);
440+
setSelectedLicenses(licenses);
441+
}}
419442
isOfficialTagFilterEnabled={isOfficialTagFilterEnabled}
420443
areFeatureFiltersEnabled={areFeatureFiltersEnabled}
421444
areGBFSFiltersEnabled={areGBFSFiltersEnabled}
@@ -515,8 +538,24 @@ export default function Feed(): React.ReactElement {
515538
/>
516539
))}
517540

541+
{selectedLicenses.map((license) => (
542+
<Chip
543+
color='primary'
544+
variant='outlined'
545+
size='small'
546+
label={license}
547+
key={license}
548+
onDelete={() => {
549+
setSelectedLicenses([
550+
...selectedLicenses.filter((sl) => sl !== license),
551+
]);
552+
}}
553+
/>
554+
))}
555+
518556
{(selectedFeatures.length > 0 ||
519557
selectGbfsVersions.length > 0 ||
558+
selectedLicenses.length > 0 ||
520559
isOfficialFeedSearch ||
521560
selectedFeedTypes.gtfs_rt ||
522561
selectedFeedTypes.gtfs ||

0 commit comments

Comments
 (0)