@bitrise/bitkit 13.318.0 → 13.320.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +19 -19
- package/src/Components/DatePicker/DatePicker.tsx +59 -54
- package/src/Components/Dialog/DialogProps.ts +1 -2
- package/src/Components/Drawer/Drawer.tsx +23 -7
- package/src/Components/ExpandableCard/ExpandableCard.tsx +23 -24
- package/src/Components/Filter/Desktop/Filter.tsx +92 -0
- package/src/Components/Filter/Desktop/FilterAdd/FilterAdd.tsx +89 -0
- package/src/Components/Filter/{FilterDate → Desktop/FilterDate}/FilterDate.tsx +21 -10
- package/src/Components/Filter/{FilterForm → Desktop}/FilterForm.tsx +24 -115
- package/src/Components/Filter/{FilterItem → Desktop}/FilterItem.tsx +1 -1
- package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.theme.ts +1 -1
- package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.tsx +4 -4
- package/src/Components/Filter/{FilterSwitchAdapter → Desktop/FilterSwitch}/FilterSwitchAdapter.tsx +5 -5
- package/src/Components/Filter/Filter.storyData.ts +14 -1
- package/src/Components/Filter/Filter.tsx +16 -106
- package/src/Components/Filter/Filter.types.ts +34 -1
- package/src/Components/Filter/Filter.utils.ts +13 -0
- package/src/Components/Filter/Mobile/DateSelectOption.tsx +53 -0
- package/src/Components/Filter/Mobile/Filter.tsx +57 -0
- package/src/Components/Filter/Mobile/FilterAdd.tsx +97 -0
- package/src/Components/Filter/Mobile/FilterDrawer.tsx +96 -0
- package/src/Components/Filter/Mobile/FilterForm.tsx +236 -0
- package/src/Components/Filter/Mobile/FilterItem.tsx +95 -0
- package/src/Components/Filter/Mobile/MultiSelectOptions.tsx +69 -0
- package/src/Components/Filter/Mobile/SingleSelectOptions.tsx +136 -0
- package/src/Components/Filter/hooks/useFilterAdd.ts +68 -0
- package/src/Components/Filter/hooks/useFilterForm.ts +131 -0
- package/src/Components/Filter/hooks/useIsScrollable.ts +35 -0
- package/src/Components/Filter/hooks/useListBox.ts +66 -0
- package/src/Components/Form/Checkbox/Checkbox.tsx +4 -2
- package/src/Components/Form/Input/Input.theme.ts +27 -11
- package/src/Components/Form/Input/Input.tsx +4 -1
- package/src/Components/Form/Radio/Radio.tsx +4 -2
- package/src/Components/SearchInput/SearchInput.tsx +3 -2
- package/src/Components/components.theme.ts +1 -1
- package/src/index.ts +3 -4
- package/src/Components/Filter/FilterAdd/FilterAdd.tsx +0 -111
- /package/src/Components/Filter/{FilterSearch → Desktop}/FilterSearch.tsx +0 -0
- /package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitchGroup.tsx +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Modal, ModalOverlay, Portal } from 'chakra-ui-2--react';
|
|
2
|
+
import Menu from '../../Menu/Menu';
|
|
3
|
+
import MenuItem from '../../Menu/MenuItem';
|
|
4
|
+
import Button from '../../Button/Button';
|
|
5
|
+
import Box from '../../Box/Box';
|
|
6
|
+
import Text from '../../Text/Text';
|
|
7
|
+
import { getMissingDependencies } from '../Filter.utils';
|
|
8
|
+
import useFilterAdd from '../hooks/useFilterAdd';
|
|
9
|
+
import { useFilterContext } from '../Filter.context';
|
|
10
|
+
import FilterForm from './FilterForm';
|
|
11
|
+
|
|
12
|
+
const FilterAdd = () => {
|
|
13
|
+
const { data } = useFilterContext();
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
categoryList,
|
|
17
|
+
closeMenu,
|
|
18
|
+
isDisabled,
|
|
19
|
+
isMenuOpen,
|
|
20
|
+
onCategorySelect,
|
|
21
|
+
onOpen,
|
|
22
|
+
selectedCategory,
|
|
23
|
+
setSelectedCategory,
|
|
24
|
+
stateKeys,
|
|
25
|
+
} = useFilterAdd({ isMobile: true });
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Modal isOpen={isMenuOpen} onClose={() => closeMenu()} trapFocus={false} autoFocus={false}>
|
|
30
|
+
{isMenuOpen && <ModalOverlay onClick={() => closeMenu()} zIndex="popover" />}
|
|
31
|
+
</Modal>
|
|
32
|
+
<Button alignSelf="flex-start" isDisabled={isDisabled} variant="tertiary" leftIconName="Plus" onClick={onOpen}>
|
|
33
|
+
Add filter
|
|
34
|
+
</Button>
|
|
35
|
+
<FilterForm
|
|
36
|
+
isOpen={Boolean(selectedCategory)}
|
|
37
|
+
onClose={() => setSelectedCategory(undefined)}
|
|
38
|
+
selectedCategory={selectedCategory || ''}
|
|
39
|
+
/>
|
|
40
|
+
{isMenuOpen && (
|
|
41
|
+
<Portal>
|
|
42
|
+
<Box
|
|
43
|
+
bg="white"
|
|
44
|
+
borderRadius="8px"
|
|
45
|
+
bottom="12px"
|
|
46
|
+
boxShadow="0 4px 12px rgba(0, 0, 0, 0.15)"
|
|
47
|
+
left="12px"
|
|
48
|
+
maxHeight="75vh"
|
|
49
|
+
overflowY="auto"
|
|
50
|
+
paddingY="8"
|
|
51
|
+
position="fixed"
|
|
52
|
+
right="12px"
|
|
53
|
+
sx={{
|
|
54
|
+
'@keyframes slideUp': {
|
|
55
|
+
from: { transform: 'translateY(100%)', opacity: 0 },
|
|
56
|
+
to: { transform: 'translateY(0)', opacity: 1 },
|
|
57
|
+
},
|
|
58
|
+
animation: 'slideUp 0.3s ease-out',
|
|
59
|
+
}}
|
|
60
|
+
zIndex="tooltip"
|
|
61
|
+
>
|
|
62
|
+
<Menu>
|
|
63
|
+
<Text
|
|
64
|
+
color="text/tertiary"
|
|
65
|
+
paddingBlock="8"
|
|
66
|
+
paddingInline="16"
|
|
67
|
+
textStyle="heading/h6"
|
|
68
|
+
textTransform="uppercase"
|
|
69
|
+
>
|
|
70
|
+
Select filter
|
|
71
|
+
</Text>
|
|
72
|
+
{categoryList.map((category) => {
|
|
73
|
+
const { categoryName, dependsOn } = data[category];
|
|
74
|
+
const missingDependencies = getMissingDependencies(stateKeys, Object.keys(dependsOn || []));
|
|
75
|
+
const isCategoryDisabled = missingDependencies.length > 0 || !data[category]?.options?.length;
|
|
76
|
+
return (
|
|
77
|
+
<MenuItem
|
|
78
|
+
isDisabled={isCategoryDisabled}
|
|
79
|
+
key={category}
|
|
80
|
+
onClick={() => onCategorySelect(category)}
|
|
81
|
+
pointerEvents="all"
|
|
82
|
+
rightIconName="ChevronRight"
|
|
83
|
+
rightIconColor="neutral.60"
|
|
84
|
+
>
|
|
85
|
+
{categoryName || category}
|
|
86
|
+
</MenuItem>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</Menu>
|
|
90
|
+
</Box>
|
|
91
|
+
</Portal>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default FilterAdd;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import ButtonGroup from '../../ButtonGroup/ButtonGroup';
|
|
3
|
+
import type { DialogProps } from '../../Dialog/DialogProps';
|
|
4
|
+
import Box from '../../Box/Box';
|
|
5
|
+
import Button from '../../Button/Button';
|
|
6
|
+
import Drawer from '../../Drawer/Drawer';
|
|
7
|
+
import Divider from '../../Divider/Divider';
|
|
8
|
+
import Text from '../../Text/Text';
|
|
9
|
+
import { useFilterContext } from '../Filter.context';
|
|
10
|
+
import FilterItem from './FilterItem';
|
|
11
|
+
import FilterAdd from './FilterAdd';
|
|
12
|
+
|
|
13
|
+
const FilterDrawer = ({
|
|
14
|
+
isOpen,
|
|
15
|
+
onClose,
|
|
16
|
+
showAdd,
|
|
17
|
+
}: Pick<DialogProps, 'isOpen' | 'onClose'> & { showAdd?: boolean }) => {
|
|
18
|
+
const { filters, state, onClearFilters, showClearButton } = useFilterContext();
|
|
19
|
+
|
|
20
|
+
const initialFocusRef = useRef(null);
|
|
21
|
+
|
|
22
|
+
const onClear = () => {
|
|
23
|
+
onClearFilters();
|
|
24
|
+
onClose();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Drawer
|
|
29
|
+
blockScrollOnMount={false}
|
|
30
|
+
bodyPadding={16}
|
|
31
|
+
bodyProps={{ overflowY: 'auto' }}
|
|
32
|
+
contentProps={{ zIndex: 'fullDialog', top: '0' }}
|
|
33
|
+
headerPadding={0}
|
|
34
|
+
initialFocusRef={initialFocusRef}
|
|
35
|
+
isOpen={isOpen}
|
|
36
|
+
maxWidth="100%"
|
|
37
|
+
onClose={onClose}
|
|
38
|
+
overlayProps={{ zIndex: 'fullDialogOverlay' }}
|
|
39
|
+
padding={0}
|
|
40
|
+
title={
|
|
41
|
+
<>
|
|
42
|
+
<Text color="text/primary" padding="16" textStyle="heading/mobile/h2" textTransform="none">
|
|
43
|
+
Filters
|
|
44
|
+
</Text>
|
|
45
|
+
<Divider />
|
|
46
|
+
</>
|
|
47
|
+
}
|
|
48
|
+
footer={
|
|
49
|
+
<Box width="100%">
|
|
50
|
+
<Divider />
|
|
51
|
+
<ButtonGroup
|
|
52
|
+
display="flex"
|
|
53
|
+
justifyContent="space-between"
|
|
54
|
+
gap="12"
|
|
55
|
+
paddingBlock="12"
|
|
56
|
+
paddingInline="16"
|
|
57
|
+
width="100%"
|
|
58
|
+
ref={initialFocusRef}
|
|
59
|
+
>
|
|
60
|
+
<Button isDisabled={!showClearButton} variant="secondary" onClick={onClear} flex="1" size="md">
|
|
61
|
+
Clear filters
|
|
62
|
+
</Button>
|
|
63
|
+
<Button type="submit" flex="1" size="md" onClick={onClose}>
|
|
64
|
+
Show results
|
|
65
|
+
</Button>
|
|
66
|
+
</ButtonGroup>
|
|
67
|
+
</Box>
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
<Box display="flex" flexDirection="column" gap="16">
|
|
71
|
+
{Object.keys(filters.dateRange).map((category) => (
|
|
72
|
+
<FilterItem key={category} category={category} />
|
|
73
|
+
))}
|
|
74
|
+
|
|
75
|
+
{Object.keys(filters.switch).map((category) => (
|
|
76
|
+
<FilterItem key={category} category={category} />
|
|
77
|
+
))}
|
|
78
|
+
|
|
79
|
+
{Object.keys(filters.select).map((category) => (
|
|
80
|
+
<FilterItem key={category} category={category} />
|
|
81
|
+
))}
|
|
82
|
+
|
|
83
|
+
{Object.keys(filters.tag).map((category) => {
|
|
84
|
+
if (!state[category]) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
return <FilterItem key={category} category={category} />;
|
|
88
|
+
})}
|
|
89
|
+
|
|
90
|
+
{showAdd && <FilterAdd />}
|
|
91
|
+
</Box>
|
|
92
|
+
</Drawer>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default FilterDrawer;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import ProgressSpinner from '../../ProgressSpinner/ProgressSpinner';
|
|
3
|
+
import Divider from '../../Divider/Divider';
|
|
4
|
+
import SearchInput from '../../SearchInput/SearchInput';
|
|
5
|
+
import ControlButton from '../../ControlButton/ControlButton';
|
|
6
|
+
import Text from '../../Text/Text';
|
|
7
|
+
import type { DialogProps } from '../../Dialog/DialogProps';
|
|
8
|
+
import Box from '../../Box/Box';
|
|
9
|
+
import Button from '../../Button/Button';
|
|
10
|
+
import Drawer from '../../Drawer/Drawer';
|
|
11
|
+
import { useFilterContext } from '../Filter.context';
|
|
12
|
+
import useFilterForm from '../hooks/useFilterForm';
|
|
13
|
+
import { FilterValue } from '../Filter.types';
|
|
14
|
+
import ButtonGroup from '../../ButtonGroup/ButtonGroup';
|
|
15
|
+
import useIsScrollable from '../hooks/useIsScrollable';
|
|
16
|
+
import DateSelectOption from './DateSelectOption';
|
|
17
|
+
import MultiSelectOptions from './MultiSelectOptions';
|
|
18
|
+
import SingleSelectOptions from './SingleSelectOptions';
|
|
19
|
+
|
|
20
|
+
type FilterFormProps = Pick<DialogProps, 'isOpen' | 'onClose'> & { selectedCategory: string };
|
|
21
|
+
|
|
22
|
+
const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
|
|
23
|
+
const { data, onFilterChange } = useFilterContext();
|
|
24
|
+
|
|
25
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
const categoryData = data[selectedCategory || ''];
|
|
28
|
+
|
|
29
|
+
const categoryName = categoryData?.categoryName;
|
|
30
|
+
const categoryNamePlural = categoryData?.categoryNamePlural;
|
|
31
|
+
const hasNotFilteredOption =
|
|
32
|
+
categoryData?.hasNotFilteredOption === undefined ? true : categoryData?.hasNotFilteredOption;
|
|
33
|
+
const isMultiple = categoryData?.isMultiple;
|
|
34
|
+
const loadingText = categoryData?.loadingText;
|
|
35
|
+
const type = categoryData?.type;
|
|
36
|
+
const name = categoryData?.categoryName;
|
|
37
|
+
|
|
38
|
+
const onChange = (newCategory: string, newValue: FilterValue) => {
|
|
39
|
+
if (isMultiple) {
|
|
40
|
+
onFilterChange(newCategory, newValue);
|
|
41
|
+
} else {
|
|
42
|
+
onFilterChange(newCategory, newValue);
|
|
43
|
+
onClose();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
currentOptionMap,
|
|
49
|
+
getEmptyText,
|
|
50
|
+
isAsync,
|
|
51
|
+
isLoading,
|
|
52
|
+
isSubmitDisabled,
|
|
53
|
+
onClearClick,
|
|
54
|
+
onSearchChange,
|
|
55
|
+
onSubmit,
|
|
56
|
+
items,
|
|
57
|
+
searchValue,
|
|
58
|
+
selected,
|
|
59
|
+
setSelected,
|
|
60
|
+
withSearch,
|
|
61
|
+
} = useFilterForm({
|
|
62
|
+
category: selectedCategory,
|
|
63
|
+
categoryName,
|
|
64
|
+
categoryNamePlural,
|
|
65
|
+
loadingText,
|
|
66
|
+
onChange,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const { isScrollable } = useIsScrollable({
|
|
70
|
+
items,
|
|
71
|
+
hasNotFilteredOption,
|
|
72
|
+
ref: bodyRef,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const onClear = () => {
|
|
76
|
+
onClearClick();
|
|
77
|
+
onClose();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const count = selected.length || 0;
|
|
81
|
+
const showCount = isMultiple && type !== 'dateRange';
|
|
82
|
+
|
|
83
|
+
const clearText = type === 'dateRange' ? 'Reset' : `Clear ${isMultiple ? 'all' : ''}`;
|
|
84
|
+
|
|
85
|
+
const showFooter = selected.length > 0;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Drawer
|
|
89
|
+
bodyPadding="0"
|
|
90
|
+
bodyProps={{ overflowY: 'auto' }}
|
|
91
|
+
bodyRef={bodyRef}
|
|
92
|
+
contentProps={{ zIndex: 'dialog', top: '0' }}
|
|
93
|
+
headerPadding={16}
|
|
94
|
+
hideCloseButton
|
|
95
|
+
isOpen={isOpen}
|
|
96
|
+
maxWidth="100%"
|
|
97
|
+
onClose={onClose}
|
|
98
|
+
padding={0}
|
|
99
|
+
title={
|
|
100
|
+
<Box display="flex" alignItems="center" gap="12">
|
|
101
|
+
<ControlButton
|
|
102
|
+
aria-label="Back"
|
|
103
|
+
color="icon/secondary"
|
|
104
|
+
iconName="ArrowLeft"
|
|
105
|
+
isTooltipDisabled
|
|
106
|
+
onClick={onClose}
|
|
107
|
+
size="sm"
|
|
108
|
+
/>
|
|
109
|
+
<Box display="flex" flexDirection="column">
|
|
110
|
+
<Text as="h6" color="text/tertiary" textStyle="heading/h6">
|
|
111
|
+
Filter
|
|
112
|
+
</Text>
|
|
113
|
+
<Text as="h3" color="text/primary" textStyle="heading/mobile/h3">
|
|
114
|
+
{name}
|
|
115
|
+
</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
</Box>
|
|
118
|
+
}
|
|
119
|
+
footer={
|
|
120
|
+
showFooter && (hasNotFilteredOption || isMultiple) ? (
|
|
121
|
+
<Box width="100%">
|
|
122
|
+
<Divider />
|
|
123
|
+
<ButtonGroup
|
|
124
|
+
backgroundColor="background/primary"
|
|
125
|
+
display="flex"
|
|
126
|
+
gap="12"
|
|
127
|
+
justifyContent="space-between"
|
|
128
|
+
paddingBlock="12"
|
|
129
|
+
paddingInline="16"
|
|
130
|
+
width="100%"
|
|
131
|
+
>
|
|
132
|
+
{hasNotFilteredOption && (
|
|
133
|
+
<Button variant="secondary" onClick={onClear} flex="1" maxWidth="calc(50% - 0.5rem)" size="md">
|
|
134
|
+
{clearText}
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
137
|
+
{isMultiple && (
|
|
138
|
+
<Button
|
|
139
|
+
alignSelf="flex-end"
|
|
140
|
+
flex="1"
|
|
141
|
+
isDisabled={isSubmitDisabled}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
onSubmit();
|
|
144
|
+
onClose();
|
|
145
|
+
}}
|
|
146
|
+
size="md"
|
|
147
|
+
type="submit"
|
|
148
|
+
>
|
|
149
|
+
Apply {showCount && count ? `(${count})` : undefined}
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
152
|
+
</ButtonGroup>
|
|
153
|
+
</Box>
|
|
154
|
+
) : null
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
<Box display="flex" flexDirection="column" gap="16">
|
|
158
|
+
<Box
|
|
159
|
+
backgroundColor="background/primary"
|
|
160
|
+
display="flex"
|
|
161
|
+
flexDirection="column"
|
|
162
|
+
gap="8"
|
|
163
|
+
marginBottom="8"
|
|
164
|
+
position="sticky"
|
|
165
|
+
top="0"
|
|
166
|
+
>
|
|
167
|
+
{(withSearch || isAsync) && (
|
|
168
|
+
<>
|
|
169
|
+
<Divider />
|
|
170
|
+
<SearchInput
|
|
171
|
+
marginInline="12"
|
|
172
|
+
onChange={onSearchChange}
|
|
173
|
+
placeholder="Search for options"
|
|
174
|
+
value={searchValue}
|
|
175
|
+
variant="mobile"
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
<Divider />
|
|
180
|
+
</Box>
|
|
181
|
+
<Box display="flex" flexDirection="column" gap="8">
|
|
182
|
+
{isLoading && (
|
|
183
|
+
<Box display="flex" alignItems="center" padding="24">
|
|
184
|
+
<ProgressSpinner color="sys/primary/base" marginRight="12" size="16" />
|
|
185
|
+
<Text color="text/secondary">{loadingText || 'Loading...'}</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
)}
|
|
188
|
+
{type === 'dateRange' && (
|
|
189
|
+
<DateSelectOption
|
|
190
|
+
key={selectedCategory}
|
|
191
|
+
onChange={setSelected}
|
|
192
|
+
onClear={onClear}
|
|
193
|
+
selectedItems={selected}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
{type !== 'dateRange' && isMultiple && (
|
|
197
|
+
<MultiSelectOptions
|
|
198
|
+
currentOptionMap={currentOptionMap}
|
|
199
|
+
emptyText={getEmptyText()}
|
|
200
|
+
isLoading={isLoading}
|
|
201
|
+
items={items}
|
|
202
|
+
onChange={setSelected}
|
|
203
|
+
selectedItems={selected}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
{type !== 'dateRange' && !isMultiple && (
|
|
207
|
+
<SingleSelectOptions
|
|
208
|
+
currentOptionMap={currentOptionMap}
|
|
209
|
+
emptyText={getEmptyText()}
|
|
210
|
+
hasNotFilteredOption={hasNotFilteredOption}
|
|
211
|
+
isLoading={isLoading}
|
|
212
|
+
items={items}
|
|
213
|
+
onChange={(option: string) => {
|
|
214
|
+
onChange(selectedCategory, [option]);
|
|
215
|
+
}}
|
|
216
|
+
selectedItems={selected}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
{isScrollable && (
|
|
220
|
+
<Box
|
|
221
|
+
background="linear-gradient(0deg, white 0%, rgba(255, 255, 255, 0) 100%)"
|
|
222
|
+
bottom="0"
|
|
223
|
+
height="32"
|
|
224
|
+
left="0"
|
|
225
|
+
pointerEvents="none"
|
|
226
|
+
position="sticky"
|
|
227
|
+
right="0"
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</Box>
|
|
231
|
+
</Box>
|
|
232
|
+
</Drawer>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default FilterForm;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { FormControl } from 'chakra-ui-2--react';
|
|
3
|
+
import FormLabel from '../../Form/FormLabel';
|
|
4
|
+
import Button from '../../Button/Button';
|
|
5
|
+
import Box from '../../Box/Box';
|
|
6
|
+
import Icon from '../../Icon/Icon';
|
|
7
|
+
import IconButton from '../../IconButton/IconButton';
|
|
8
|
+
import { useFilterContext } from '../Filter.context';
|
|
9
|
+
import { getDateRangeLabel, getOptionLabel } from '../Filter.utils';
|
|
10
|
+
|
|
11
|
+
export type FilterItemProps = {
|
|
12
|
+
category: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FilterItem = ({ category }: FilterItemProps) => {
|
|
16
|
+
const { data, onFilterClear, setSelectedCategory, state } = useFilterContext();
|
|
17
|
+
|
|
18
|
+
const value = state[category];
|
|
19
|
+
|
|
20
|
+
const { categoryName, categoryNamePlural, isMultiple, optionsMap, unfilteredLabel, type } = data[category];
|
|
21
|
+
|
|
22
|
+
const pluralCategoryString = (categoryNamePlural || `${category}s`).toLowerCase();
|
|
23
|
+
|
|
24
|
+
const getText = () => {
|
|
25
|
+
if (!value || value.length === 0) {
|
|
26
|
+
return unfilteredLabel || `All ${pluralCategoryString}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value.length === 2 && type === 'dateRange') {
|
|
30
|
+
return getDateRangeLabel(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value.length > 1) {
|
|
34
|
+
return `${value.length} ${pluralCategoryString}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return getOptionLabel(value[0], optionsMap);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const buttonId = useId();
|
|
41
|
+
return (
|
|
42
|
+
<FormControl flex="1" isRequired>
|
|
43
|
+
<FormLabel htmlFor={buttonId}>{isMultiple && type !== 'dateRange' ? categoryNamePlural : categoryName}</FormLabel>
|
|
44
|
+
{type === 'tag' && (
|
|
45
|
+
<Box display="flex" alignItems="center">
|
|
46
|
+
<Button
|
|
47
|
+
borderRight="0"
|
|
48
|
+
borderRightRadius="0"
|
|
49
|
+
fontWeight="normal"
|
|
50
|
+
id={buttonId}
|
|
51
|
+
justifyContent="flex-start"
|
|
52
|
+
onClick={() => setSelectedCategory(category)}
|
|
53
|
+
rightIconName={type !== 'tag' ? 'ChevronRight' : undefined}
|
|
54
|
+
size="lg"
|
|
55
|
+
variant="secondary"
|
|
56
|
+
width="100%"
|
|
57
|
+
>
|
|
58
|
+
{getText()}
|
|
59
|
+
</Button>
|
|
60
|
+
<IconButton
|
|
61
|
+
aria-label="Clear"
|
|
62
|
+
borderLeft="0"
|
|
63
|
+
borderLeftRadius="0"
|
|
64
|
+
iconName="Cross"
|
|
65
|
+
onClick={() => onFilterClear(category)}
|
|
66
|
+
variant="secondary"
|
|
67
|
+
/>
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
{type !== 'tag' && (
|
|
71
|
+
<Button
|
|
72
|
+
fontWeight="normal"
|
|
73
|
+
id={buttonId}
|
|
74
|
+
justifyContent="space-between"
|
|
75
|
+
onClick={() => setSelectedCategory(category)}
|
|
76
|
+
rightIconName="ChevronRight"
|
|
77
|
+
size="lg"
|
|
78
|
+
variant="secondary"
|
|
79
|
+
width="100%"
|
|
80
|
+
>
|
|
81
|
+
{type === 'dateRange' ? (
|
|
82
|
+
<Box display="flex" alignItems="center" gap="8">
|
|
83
|
+
<Icon color="icon/tertiary" name="Calendar" />
|
|
84
|
+
{getText()}
|
|
85
|
+
</Box>
|
|
86
|
+
) : (
|
|
87
|
+
getText()
|
|
88
|
+
)}
|
|
89
|
+
</Button>
|
|
90
|
+
)}
|
|
91
|
+
</FormControl>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default FilterItem;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Checkbox, CheckboxGroup } from 'chakra-ui-2--react';
|
|
2
|
+
import Box from '../../Box/Box';
|
|
3
|
+
import Text from '../../Text/Text';
|
|
4
|
+
import { FilterOptionsMap } from '../Filter.types';
|
|
5
|
+
import { getOptionLabel } from '../Filter.utils';
|
|
6
|
+
import { MAX_ITEMS } from '../hooks/useFilterForm';
|
|
7
|
+
|
|
8
|
+
type MultiSelectOptionsProps = {
|
|
9
|
+
currentOptionMap?: FilterOptionsMap;
|
|
10
|
+
emptyText?: string;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
items: string[];
|
|
13
|
+
onChange: (option: string[]) => void;
|
|
14
|
+
selectedItems: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MultiSelectOptions = ({
|
|
18
|
+
currentOptionMap,
|
|
19
|
+
emptyText,
|
|
20
|
+
isLoading,
|
|
21
|
+
items,
|
|
22
|
+
onChange,
|
|
23
|
+
selectedItems,
|
|
24
|
+
}: MultiSelectOptionsProps) => {
|
|
25
|
+
if (isLoading) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<CheckboxGroup onChange={onChange} value={selectedItems}>
|
|
31
|
+
<Box>
|
|
32
|
+
{items.length ? (
|
|
33
|
+
items?.slice(0, MAX_ITEMS).map((option) => {
|
|
34
|
+
const id = `checkbox-${option}`;
|
|
35
|
+
const isSelected = selectedItems.includes(option);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Box
|
|
39
|
+
alignItems="center"
|
|
40
|
+
as="label"
|
|
41
|
+
backgroundColor={isSelected ? 'background/selected' : 'transparent'}
|
|
42
|
+
display="flex"
|
|
43
|
+
gap="16"
|
|
44
|
+
htmlFor={id}
|
|
45
|
+
paddingBlock="12"
|
|
46
|
+
paddingInlineStart="24"
|
|
47
|
+
paddingInlineEnd="24"
|
|
48
|
+
cursor="pointer"
|
|
49
|
+
role="option"
|
|
50
|
+
_focusWithin={{
|
|
51
|
+
backgroundColor: isSelected ? 'background/selected-hover' : 'background/hover',
|
|
52
|
+
}}
|
|
53
|
+
_hover={{ backgroundColor: isSelected ? 'background/selected-hover' : 'background/hover' }}
|
|
54
|
+
_active={{ backgroundColor: 'background/active' }}
|
|
55
|
+
>
|
|
56
|
+
<Checkbox id={id} key={option} value={option} position="static" />
|
|
57
|
+
{getOptionLabel(option, currentOptionMap)}
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
})
|
|
61
|
+
) : (
|
|
62
|
+
<Text paddingInline={16}>{emptyText}</Text>
|
|
63
|
+
)}
|
|
64
|
+
</Box>
|
|
65
|
+
</CheckboxGroup>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default MultiSelectOptions;
|