Skip to content

Commit bee6fe2

Browse files
authored
Merge pull request #752 from amitamrutiya/catalog-filter
Convert cloud catalog filter section to sistent
2 parents 3d35b16 + e08a79f commit bee6fe2

6 files changed

Lines changed: 453 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import FilterAltIcon from '@mui/icons-material/FilterAlt';
2+
import { useTheme } from '@mui/material/styles';
3+
import { useCallback, useState } from 'react';
4+
import { Box, Drawer, Typography } from '../../base';
5+
import { CloseIcon } from '../../icons';
6+
import { CloseBtn } from '../Modal';
7+
import CatalogFilterSidebarState from './CatalogFilterSidebarState';
8+
import {
9+
FilterButton,
10+
FilterDrawerDiv,
11+
FilterText,
12+
FiltersCardDiv,
13+
FiltersDrawerHeader
14+
} from './style';
15+
16+
export interface FilterOption {
17+
value: string;
18+
label: string;
19+
totalCount?: number;
20+
description?: string;
21+
Icon?: React.ComponentType<{
22+
width: string;
23+
height: string;
24+
}>;
25+
}
26+
27+
export interface FilterList {
28+
filterKey: string;
29+
sectionDisplayName?: string;
30+
options: FilterOption[];
31+
defaultOpen?: boolean;
32+
isMultiSelect?: boolean;
33+
}
34+
35+
export interface CatalogFilterSidebarProps {
36+
setData: (callback: (prevFilters: FilterValues) => FilterValues) => void;
37+
lists: FilterList[];
38+
value?: FilterValues;
39+
}
40+
41+
export type FilterValues = Record<string, string | string[]>;
42+
43+
export interface StyleProps {
44+
backgroundColor?: string;
45+
sectionTitleBackgroundColor?: string;
46+
}
47+
48+
/**
49+
* @function CatalogFilterSidebar
50+
* @description A functional component that renders the filter sidebar.
51+
* @param {Array} value - The data to be filtered.
52+
* @param {Function} setData - A function to set the filtered data.
53+
* @param {Array} lists - An array of filter sections and its options lists.
54+
*/
55+
const CatalogFilterSidebar: React.FC<CatalogFilterSidebarProps> = ({
56+
lists,
57+
setData,
58+
value = {}
59+
}) => {
60+
const theme = useTheme(); // Get the current theme
61+
const [openDrawer, setOpenDrawer] = useState<boolean>(false);
62+
63+
const handleDrawerOpen = useCallback(() => {
64+
setOpenDrawer(true);
65+
}, []);
66+
67+
const handleDrawerClose = useCallback(() => {
68+
setOpenDrawer(false);
69+
}, []);
70+
71+
const styleProps: StyleProps = {
72+
backgroundColor: theme.palette.background.default,
73+
sectionTitleBackgroundColor: theme.palette.background.surfaces
74+
};
75+
76+
return (
77+
<>
78+
<FiltersCardDiv styleProps={styleProps}>
79+
<CatalogFilterSidebarState
80+
lists={lists}
81+
onApplyFilters={setData}
82+
value={value}
83+
styleProps={styleProps}
84+
/>
85+
</FiltersCardDiv>
86+
<FilterDrawerDiv>
87+
<FilterButton variant="contained" onClick={handleDrawerOpen}>
88+
<FilterAltIcon height="20" width="20" fill={theme.palette.text.default} />
89+
<FilterText>Filters</FilterText>
90+
</FilterButton>
91+
92+
<Drawer anchor="bottom" open={openDrawer} variant="temporary" onClose={handleDrawerClose}>
93+
<Box sx={{ overflowY: 'hidden', height: '90vh' }}>
94+
<FiltersDrawerHeader>
95+
<Typography variant="h6" sx={{ color: theme.palette.text.default }} component="div">
96+
Filters
97+
</Typography>
98+
<CloseBtn onClick={handleDrawerClose}>
99+
<CloseIcon height={'32px'} width={'32px'} />
100+
</CloseBtn>
101+
</FiltersDrawerHeader>
102+
<Box
103+
style={{
104+
height: '75vh',
105+
overflowY: 'auto',
106+
background: theme.palette.background.default
107+
}}
108+
>
109+
<CatalogFilterSidebarState
110+
lists={lists}
111+
onApplyFilters={setData}
112+
value={value}
113+
styleProps={styleProps}
114+
/>
115+
</Box>
116+
<Box sx={{ backgroundColor: theme.palette.border.strong, height: '5vh' }} />
117+
{/* Use theme-aware color */}
118+
</Box>
119+
</Drawer>
120+
</FilterDrawerDiv>
121+
</>
122+
);
123+
};
124+
125+
export default CatalogFilterSidebar;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useCallback, useState } from 'react';
2+
import {
3+
CatalogFilterSidebarProps,
4+
FilterList,
5+
FilterValues,
6+
StyleProps
7+
} from './CatalogFilterSidebar';
8+
import FilterSection from './FilterSection';
9+
10+
/**
11+
* @component CatalogFilterSidebarState
12+
* @description A functional component that manages the filter state.
13+
* @param {Array} lists - An array of filter sections and its options lists.
14+
* @param {Function} onApplyFilters - A function to apply the filters.
15+
* @param {Object} value - The selected filters.
16+
* @param {Object} styleProps - The style properties for the component.
17+
*/
18+
const CatalogFilterSidebarState: React.FC<{
19+
lists: FilterList[];
20+
onApplyFilters: CatalogFilterSidebarProps['setData'];
21+
value: FilterValues;
22+
styleProps: StyleProps;
23+
}> = ({ lists, onApplyFilters, value, styleProps }) => {
24+
// Generate initial state with all sections open by default
25+
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => {
26+
const initialOpenSections: Record<string, boolean> = {};
27+
lists.forEach((list) => {
28+
initialOpenSections[list.filterKey] = !!list.defaultOpen;
29+
});
30+
return initialOpenSections;
31+
});
32+
33+
/**
34+
* @function handleSectionToggle
35+
* @description Handles the section toggle event.
36+
* @param {string} filterKey - The name of the filter section.
37+
*/
38+
const handleSectionToggle = useCallback((filterKey: string) => {
39+
setOpenSections((prevOpenSections) => ({
40+
...prevOpenSections,
41+
[filterKey]: !prevOpenSections[filterKey]
42+
}));
43+
}, []);
44+
45+
/**
46+
* @function handleCheckboxChange
47+
* @description Handles the checkbox change event.
48+
* @param {string} filterKey - The name of the filter section.
49+
* @param {string} value - The value of the checkbox.
50+
* @param {boolean} checked - The checked state of the checkbox.
51+
*/
52+
const handleCheckboxChange = useCallback(
53+
(filterKey: string, value: string, checked: boolean) => {
54+
onApplyFilters((prevFilters) => {
55+
const updatedFilters = { ...prevFilters };
56+
const filterList = lists.find((list) => list.filterKey === filterKey);
57+
58+
// default is multi select
59+
if (filterList?.isMultiSelect !== false) {
60+
let currentValues = updatedFilters[filterKey] as string[] | undefined;
61+
62+
if (!Array.isArray(currentValues)) {
63+
currentValues = currentValues ? [currentValues as string] : []; // convert to array;
64+
}
65+
66+
updatedFilters[filterKey] = checked
67+
? [...currentValues, value]
68+
: currentValues.filter((item) => item !== value);
69+
} else {
70+
updatedFilters[filterKey] = checked ? value : '';
71+
}
72+
73+
return updatedFilters;
74+
});
75+
},
76+
[lists, onApplyFilters]
77+
);
78+
79+
return (
80+
<>
81+
{lists.map((list) => (
82+
<FilterSection
83+
key={list.filterKey}
84+
filterKey={list.filterKey}
85+
sectionDisplayName={list.sectionDisplayName}
86+
options={list.options}
87+
filters={value}
88+
openSections={openSections}
89+
onCheckboxChange={handleCheckboxChange}
90+
onSectionToggle={handleSectionToggle}
91+
styleProps={styleProps}
92+
/>
93+
))}
94+
</>
95+
);
96+
};
97+
98+
export default CatalogFilterSidebarState;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
2+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
3+
import { useCallback, useState } from 'react';
4+
import {
5+
Box,
6+
Checkbox,
7+
Collapse,
8+
InputAdornment,
9+
List,
10+
OutlinedInput,
11+
Stack,
12+
Typography
13+
} from '../../base';
14+
import { SearchIcon } from '../../icons';
15+
import { InfoTooltip } from '../CustomTooltip';
16+
import { FilterOption, FilterValues, StyleProps } from './CatalogFilterSidebar';
17+
import { FilterTitleButton, InputAdornmentEnd } from './style';
18+
19+
interface FilterSectionProps {
20+
filterKey: string;
21+
sectionDisplayName?: string;
22+
options: FilterOption[];
23+
filters: FilterValues;
24+
openSections: Record<string, boolean>;
25+
onCheckboxChange: (filterKey: string, value: string, checked: boolean) => void;
26+
onSectionToggle: (filterKey: string) => void;
27+
styleProps: StyleProps;
28+
}
29+
30+
/**
31+
* @component FilterSection
32+
* @description A functional component that renders a filter section.
33+
* @param {string} filterKey - The key of the filter section.
34+
* @param {string} sectionDisplayName - The title of the filter section.
35+
* @param {Array} options - The available options for the filter section.
36+
* @param {Object} filters - The selected filters.
37+
* @param {Object} openSections - The open/closed state of the filter sections.
38+
* @param {Function} onCheckboxChange - A function to handle checkbox change event.
39+
* @param {Function} onSectionToggle - A function to handle section toggle event.
40+
* @param {Object} styleProps - The style properties for the component.
41+
*/
42+
const FilterSection: React.FC<FilterSectionProps> = ({
43+
filterKey,
44+
sectionDisplayName,
45+
options,
46+
filters,
47+
openSections,
48+
onCheckboxChange,
49+
onSectionToggle,
50+
styleProps
51+
}) => {
52+
const [searchQuery, setSearchQuery] = useState<string>('');
53+
54+
const handleTextFieldChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
55+
setSearchQuery(e.target.value);
56+
}, []);
57+
58+
const showSearch = options.length > 10;
59+
const searchedOptions = searchQuery
60+
? options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()))
61+
: options;
62+
63+
return (
64+
<>
65+
<FilterTitleButton
66+
onClick={() => onSectionToggle(filterKey)}
67+
style={{ backgroundColor: styleProps.sectionTitleBackgroundColor }}
68+
>
69+
<Typography variant="h6" fontWeight="bold">
70+
{(sectionDisplayName || filterKey).toUpperCase()}
71+
</Typography>
72+
{openSections[filterKey] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
73+
</FilterTitleButton>
74+
<Collapse in={openSections[filterKey]} timeout="auto" unmountOnExit>
75+
<List
76+
component="div"
77+
sx={{
78+
overflowY: 'auto',
79+
maxHeight: '25rem',
80+
backgroundColor: styleProps.backgroundColor
81+
}}
82+
>
83+
{showSearch && (
84+
<Box px={'0.5rem'}>
85+
<OutlinedInput
86+
type="search"
87+
fullWidth
88+
placeholder="Search "
89+
value={searchQuery}
90+
onChange={handleTextFieldChange}
91+
startAdornment={
92+
<InputAdornment position="start">
93+
<SearchIcon />
94+
</InputAdornment>
95+
}
96+
endAdornment={
97+
<InputAdornmentEnd position="end">
98+
Total: {searchedOptions.length}
99+
</InputAdornmentEnd>
100+
}
101+
/>
102+
</Box>
103+
)}
104+
{searchedOptions.map((option, index) => (
105+
<Stack
106+
key={`${option.value}-${index}`}
107+
direction="row"
108+
alignItems="center"
109+
px={'0.5rem'}
110+
justifyContent="space-between"
111+
>
112+
<Stack direction="row" alignItems="center" gap="0.35rem">
113+
<Checkbox
114+
id={`checkbox-${option.label}`}
115+
checked={
116+
Array.isArray(filters[filterKey])
117+
? (filters[filterKey] as string[]).includes(option.value)
118+
: filters[filterKey] === option.value
119+
}
120+
onChange={(e) => onCheckboxChange(filterKey, option.value, e.target.checked)}
121+
value={option.value}
122+
/>
123+
124+
{option.Icon && <option.Icon width="20px" height="20px" />}
125+
126+
<Typography>{option.label}</Typography>
127+
</Stack>
128+
<Stack direction="row" alignItems="center" gap="0.35rem">
129+
{option.totalCount !== undefined && `(${option.totalCount || 0})`}
130+
{option.description && (
131+
<InfoTooltip variant="standard" helpText={option.description} />
132+
)}
133+
</Stack>
134+
</Stack>
135+
))}
136+
</List>
137+
</Collapse>
138+
</>
139+
);
140+
};
141+
142+
export default FilterSection;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import CatalogFilterSidebar from './CatalogFilterSidebar';
2+
3+
export { CatalogFilterSidebar };

0 commit comments

Comments
 (0)