Skip to content

Commit 868662b

Browse files
feat: Feature search UI (#841)
* Feature search UI
1 parent a18227e commit 868662b

File tree

6 files changed

+489
-88
lines changed

6 files changed

+489
-88
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
Checkbox,
3+
Collapse,
4+
IconButton,
5+
List,
6+
ListItem,
7+
ListItemButton,
8+
ListItemText,
9+
} from '@mui/material';
10+
import * as React from 'react';
11+
import { ExpandLess, ExpandMore } from '@mui/icons-material';
12+
import { theme } from '../Theme';
13+
14+
interface NestedCheckboxListProps {
15+
checkboxData: CheckboxStructure[];
16+
onCheckboxChange: (checkboxData: CheckboxStructure[]) => void;
17+
onExpandGroupChange?: (checkboxData: CheckboxStructure[]) => void;
18+
}
19+
20+
// NOTE: Although the data structure allows for multiple levels of nesting, the current implementation only supports two levels.
21+
// TODO: Implement support for multiple levels of nesting
22+
export interface CheckboxStructure {
23+
title: string;
24+
type: 'label' | 'checkbox';
25+
checked: boolean;
26+
seeChildren?: boolean;
27+
children?: CheckboxStructure[];
28+
}
29+
30+
export default function NestedCheckboxList({
31+
checkboxData,
32+
onCheckboxChange,
33+
onExpandGroupChange,
34+
}: NestedCheckboxListProps): JSX.Element {
35+
const [checkboxStructure, setCheckboxStructure] =
36+
React.useState<CheckboxStructure[]>(checkboxData);
37+
const [hasChange, setHasChange] = React.useState<boolean>(false);
38+
39+
React.useEffect(() => {
40+
if (hasChange) {
41+
setHasChange(false);
42+
onCheckboxChange(checkboxStructure);
43+
}
44+
}, [checkboxStructure]);
45+
46+
React.useEffect(() => {
47+
setCheckboxStructure(checkboxData);
48+
}, [checkboxData]);
49+
50+
return (
51+
<List sx={{ width: '100%' }} dense>
52+
{checkboxStructure.map((checkboxData, index) => {
53+
const labelId = `checkbox-list-label-${checkboxData.title}`;
54+
return (
55+
<ListItem
56+
key={checkboxData.title}
57+
disablePadding
58+
sx={{
59+
display: 'block',
60+
borderBottom:
61+
checkboxData.children !== undefined
62+
? `1px solid ${theme.palette.text.primary}`
63+
: 'none',
64+
'.MuiListItemSecondaryAction-root': {
65+
top: checkboxData.type === 'checkbox' ? '22px' : '11px',
66+
},
67+
}}
68+
secondaryAction={
69+
<>
70+
{checkboxData.children !== undefined &&
71+
checkboxData.children?.length > 0 && (
72+
<IconButton
73+
edge={'end'}
74+
aria-label='expand'
75+
onClick={() => {
76+
checkboxData.seeChildren =
77+
checkboxData.seeChildren === undefined
78+
? true
79+
: !checkboxData.seeChildren;
80+
checkboxStructure[index] = checkboxData;
81+
setCheckboxStructure([...checkboxStructure]);
82+
if (onExpandGroupChange !== undefined) {
83+
onExpandGroupChange([...checkboxStructure]);
84+
}
85+
}}
86+
>
87+
{checkboxData.seeChildren !== undefined &&
88+
checkboxData.seeChildren ? (
89+
<ExpandLess />
90+
) : (
91+
<ExpandMore />
92+
)}
93+
</IconButton>
94+
)}
95+
</>
96+
}
97+
>
98+
{checkboxData.type === 'checkbox' && (
99+
<ListItemButton
100+
role={undefined}
101+
dense={true}
102+
sx={{ p: 0 }}
103+
onClick={() => {
104+
setCheckboxStructure((prev) => {
105+
checkboxData.checked = !checkboxData.checked;
106+
checkboxData.children?.forEach((child) => {
107+
child.checked = checkboxData.checked;
108+
});
109+
prev[index] = checkboxData;
110+
return [...prev];
111+
});
112+
setHasChange(true);
113+
}}
114+
>
115+
<Checkbox
116+
edge='start'
117+
tabIndex={-1}
118+
disableRipple
119+
inputProps={{ 'aria-labelledby': labelId }}
120+
checked={
121+
checkboxData.checked ||
122+
(checkboxData.children !== undefined &&
123+
checkboxData.children.length > 0 &&
124+
checkboxData.children.every((child) => child.checked))
125+
}
126+
indeterminate={
127+
checkboxData.children !== undefined
128+
? checkboxData.children.some((child) => child.checked) &&
129+
!checkboxData.children.every((child) => child.checked)
130+
: false
131+
}
132+
/>
133+
<ListItemText
134+
id={labelId}
135+
primary={<b>{checkboxData.title}</b>}
136+
primaryTypographyProps={{
137+
variant: 'body1',
138+
}}
139+
/>
140+
</ListItemButton>
141+
)}
142+
{checkboxData.type === 'label' && (
143+
<ListItemText
144+
id={labelId}
145+
primary={<b>{checkboxData.title}</b>}
146+
primaryTypographyProps={{
147+
variant: 'body1',
148+
}}
149+
/>
150+
)}
151+
{checkboxData.children !== undefined && (
152+
<Collapse
153+
in={checkboxData.seeChildren}
154+
timeout='auto'
155+
unmountOnExit
156+
>
157+
<List
158+
sx={{
159+
ml: 1,
160+
display: { xs: 'flex', md: 'block' },
161+
flexWrap: 'wrap',
162+
}}
163+
dense
164+
>
165+
{checkboxData.children.map((value) => {
166+
const labelId = `checkbox-list-label-${value.title}`;
167+
168+
return (
169+
<ListItem
170+
key={value.title}
171+
disablePadding
172+
sx={{ width: { xs: '50%', sm: '33%', md: '100%' } }}
173+
onClick={() => {
174+
setCheckboxStructure((prev) => {
175+
value.checked = !value.checked;
176+
if (!value.checked) {
177+
checkboxData.checked = false;
178+
}
179+
return [...prev];
180+
});
181+
setHasChange(true);
182+
}}
183+
>
184+
<ListItemButton
185+
role={undefined}
186+
dense={true}
187+
sx={{ p: 0, pl: 1 }}
188+
>
189+
<Checkbox
190+
edge='start'
191+
tabIndex={-1}
192+
disableRipple
193+
checked={value.checked || checkboxData.checked}
194+
inputProps={{ 'aria-labelledby': labelId }}
195+
/>
196+
197+
<ListItemText
198+
id={labelId}
199+
primary={`${value.title}`}
200+
primaryTypographyProps={{
201+
variant: 'body1',
202+
}}
203+
/>
204+
</ListItemButton>
205+
</ListItem>
206+
);
207+
})}
208+
</List>
209+
</Collapse>
210+
)}
211+
</ListItem>
212+
);
213+
})}
214+
</List>
215+
);
216+
}

web-app/src/app/interface/RemoteConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface RemoteConfigValues extends FirebaseDefaultConfig {
2828
/** GBFS metrics' bucket endpoint */
2929
gbfsMetricsBucketEndpoint: string;
3030
featureFlagBypass: string;
31+
enableFeatureFilterSearch: boolean;
3132
}
3233

3334
const featureByPassDefault: BypassConfig = {
@@ -46,6 +47,7 @@ export const defaultRemoteConfigValues: RemoteConfigValues = {
4647
gbfsMetricsBucketEndpoint:
4748
'https://storage.googleapis.com/mobilitydata-gbfs-analytics-dev',
4849
featureFlagBypass: JSON.stringify(featureByPassDefault),
50+
enableFeatureFilterSearch: false,
4951
};
5052

5153
remoteConfig.defaultConfig = defaultRemoteConfigValues;

0 commit comments

Comments
 (0)