@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
|
2
1
|
import { useMultiStyleConfig } from 'chakra-ui-2--react';
|
|
3
2
|
import FocusLock from 'react-focus-lock';
|
|
4
3
|
import Tooltip from '../../Tooltip/Tooltip';
|
|
@@ -18,12 +17,10 @@ import RadioGroup from '../../Form/Radio/RadioGroup';
|
|
|
18
17
|
import SearchInput from '../../SearchInput/SearchInput';
|
|
19
18
|
import Text from '../../Text/Text';
|
|
20
19
|
import { FilterStyle } from '../Filter.theme';
|
|
21
|
-
import {
|
|
22
|
-
import { isEqual, useDebounce } from '../../../utils/utils';
|
|
20
|
+
import { FilterFormProps, FilterOptions, FilterValue } from '../Filter.types';
|
|
23
21
|
import { getOptionLabel } from '../Filter.utils';
|
|
24
22
|
import { useFilterContext } from '../Filter.context';
|
|
25
|
-
|
|
26
|
-
const MAX_ITEMS = 100;
|
|
23
|
+
import useFilterForm, { MAX_ITEMS } from '../hooks/useFilterForm';
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
26
|
* https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
|
|
@@ -88,128 +85,40 @@ const MatchingResults = (props: MatchingResultsProps) => {
|
|
|
88
85
|
);
|
|
89
86
|
};
|
|
90
87
|
|
|
91
|
-
export type FilterFormProps = {
|
|
92
|
-
category: string;
|
|
93
|
-
categoryName?: string;
|
|
94
|
-
categoryNamePlural?: string;
|
|
95
|
-
loadingText?: string;
|
|
96
|
-
onChange: (category: string, selected: FilterValue, previousValue: FilterValue) => void;
|
|
97
|
-
onCancel: () => void;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
88
|
const FilterForm = (props: FilterFormProps) => {
|
|
101
|
-
const { category, categoryName, categoryNamePlural, loadingText, onCancel
|
|
89
|
+
const { category, categoryName, categoryNamePlural, loadingText, onCancel } = props;
|
|
102
90
|
|
|
103
91
|
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
104
92
|
|
|
105
|
-
const { data
|
|
106
|
-
|
|
107
|
-
const {
|
|
108
|
-
isPatternEnabled,
|
|
109
|
-
iconsMap,
|
|
110
|
-
hasNotFilteredOption = true,
|
|
111
|
-
preserveOptionOrder,
|
|
112
|
-
isInitialLoading,
|
|
113
|
-
isMultiple,
|
|
114
|
-
onAsyncSearch,
|
|
115
|
-
options,
|
|
116
|
-
optionsMap,
|
|
117
|
-
} = data[category];
|
|
118
|
-
const value = state[category] || [];
|
|
119
|
-
|
|
120
|
-
const [selected, setSelected] = useState<FilterValue>(value);
|
|
121
|
-
|
|
122
|
-
const [mode, setMode] = useState<'manually' | 'pattern'>(
|
|
123
|
-
isPatternEnabled && value?.length && value[0].includes('*') ? 'pattern' : 'manually',
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const [searchValue, setSearchValue] = useState<string>('');
|
|
127
|
-
const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
|
|
128
|
-
|
|
129
|
-
const [isLoading, setLoading] = useState(Boolean(isInitialLoading));
|
|
130
|
-
const [foundOptions, setFoundOptions] = useState<FilterOptions>([]);
|
|
131
|
-
const [foundOptionsMap, setFoundOptionsMap] = useState<FilterOptionsMap>();
|
|
132
|
-
|
|
133
|
-
const isAsync = !!onAsyncSearch;
|
|
134
|
-
const withSearch = (options && options.length > 5) || isAsync;
|
|
135
|
-
|
|
136
|
-
const isSubmitDisabled =
|
|
137
|
-
isEqual(selected, value) || (mode === 'pattern' && !!selected[0] && !selected[0].includes('*'));
|
|
138
|
-
const filteredOptions = useMemo(() => {
|
|
139
|
-
if (options?.length) {
|
|
140
|
-
return options.filter((opt) => {
|
|
141
|
-
const optLabel = (optionsMap && optionsMap[opt]) || opt;
|
|
142
|
-
return optLabel.toLowerCase().includes(searchValue.toLowerCase());
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return [];
|
|
146
|
-
}, [searchValue, options]);
|
|
147
|
-
|
|
148
|
-
const onSearchChange = (q: string) => {
|
|
149
|
-
setSearchValue(q);
|
|
150
|
-
if (isAsync) {
|
|
151
|
-
if (q.length > 0) {
|
|
152
|
-
setLoading(true);
|
|
153
|
-
} else {
|
|
154
|
-
setFoundOptions([]);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
};
|
|
93
|
+
const { data } = useFilterContext();
|
|
158
94
|
|
|
159
|
-
const
|
|
160
|
-
e.preventDefault();
|
|
161
|
-
const newSelected = selected[0] === '' ? [] : selected;
|
|
162
|
-
onChange(category, newSelected, value);
|
|
163
|
-
};
|
|
95
|
+
const { isPatternEnabled, iconsMap, hasNotFilteredOption = true, isInitialLoading, isMultiple } = data[category];
|
|
164
96
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
97
|
+
const {
|
|
98
|
+
currentOptionMap,
|
|
99
|
+
getEmptyText,
|
|
100
|
+
isAsync,
|
|
101
|
+
isLoading,
|
|
102
|
+
isSubmitDisabled,
|
|
103
|
+
mode,
|
|
104
|
+
onClearClick,
|
|
105
|
+
onSearchChange,
|
|
106
|
+
onSubmit,
|
|
107
|
+
isEditMode,
|
|
108
|
+
items,
|
|
109
|
+
searchValue,
|
|
110
|
+
selected,
|
|
111
|
+
setMode,
|
|
112
|
+
setSelected,
|
|
113
|
+
value,
|
|
114
|
+
withSearch,
|
|
115
|
+
} = useFilterForm(props);
|
|
169
116
|
|
|
170
117
|
const onCancelClick = () => {
|
|
171
118
|
setSelected(value);
|
|
172
119
|
onCancel();
|
|
173
120
|
};
|
|
174
121
|
|
|
175
|
-
const getEmptyText = () => {
|
|
176
|
-
if (searchValue.length) {
|
|
177
|
-
return 'No result. Refine your search term.';
|
|
178
|
-
}
|
|
179
|
-
return '';
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const getAsyncList = async () => {
|
|
183
|
-
if (onAsyncSearch) {
|
|
184
|
-
const response = await onAsyncSearch(category, searchValue);
|
|
185
|
-
setLoading(false);
|
|
186
|
-
setFoundOptions(response.options);
|
|
187
|
-
setFoundOptionsMap(response.optionsMap || optionsMap);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
useEffect(() => {
|
|
192
|
-
setLoading(Boolean(isInitialLoading));
|
|
193
|
-
}, [isInitialLoading]);
|
|
194
|
-
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
if (debouncedSearchValue.length > 0) {
|
|
197
|
-
getAsyncList();
|
|
198
|
-
} else {
|
|
199
|
-
setLoading(Boolean(isInitialLoading));
|
|
200
|
-
}
|
|
201
|
-
}, [debouncedSearchValue]);
|
|
202
|
-
|
|
203
|
-
const isEditMode = value.length !== 0;
|
|
204
|
-
|
|
205
|
-
const isAsyncSearch = isAsync && !!searchValue;
|
|
206
|
-
|
|
207
|
-
const items = isAsyncSearch
|
|
208
|
-
? Array.from(new Set(preserveOptionOrder ? [...foundOptions] : [...value, ...foundOptions]))
|
|
209
|
-
: Array.from(new Set(preserveOptionOrder ? [...filteredOptions] : [...value, ...filteredOptions]));
|
|
210
|
-
|
|
211
|
-
const currentOptionMap = isAsyncSearch ? foundOptionsMap : optionsMap;
|
|
212
|
-
|
|
213
122
|
return (
|
|
214
123
|
<FocusLock>
|
|
215
124
|
<Box as="form" data-testid={`${category}-form`} onSubmit={onSubmit} sx={filterStyle.form}>
|
|
@@ -10,9 +10,9 @@ import Text from '../../Text/Text';
|
|
|
10
10
|
import Tooltip from '../../Tooltip/Tooltip';
|
|
11
11
|
import { FilterStyle } from '../Filter.theme';
|
|
12
12
|
import { FilterValue } from '../Filter.types';
|
|
13
|
-
import FilterForm from '../FilterForm/FilterForm';
|
|
14
13
|
import { useFilterContext } from '../Filter.context';
|
|
15
14
|
import { getOptionLabel } from '../Filter.utils';
|
|
15
|
+
import FilterForm from './FilterForm';
|
|
16
16
|
|
|
17
17
|
export type FilterItemProps = {
|
|
18
18
|
category: string;
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
} from 'chakra-ui-2--react';
|
|
10
10
|
import { omitThemingProps } from 'chakra-ui-2--styled-system';
|
|
11
11
|
import { createContext } from 'chakra-ui-2--react-utils';
|
|
12
|
-
import Divider from '
|
|
13
|
-
import Text from '
|
|
14
|
-
import Box from '
|
|
15
|
-
import Icon, { TypeIconName } from '
|
|
12
|
+
import Divider from '../../../Divider/Divider';
|
|
13
|
+
import Text from '../../../Text/Text';
|
|
14
|
+
import Box from '../../../Box/Box';
|
|
15
|
+
import Icon, { TypeIconName } from '../../../Icon/Icon';
|
|
16
16
|
|
|
17
17
|
type RadioInputProps = ChakraRadioProps['inputProps'] & {
|
|
18
18
|
'data-testid'?: string;
|
package/src/Components/Filter/{FilterSwitchAdapter → Desktop/FilterSwitch}/FilterSwitchAdapter.tsx
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import { useFilterContext } from '
|
|
3
|
-
import { getOptionLabel } from '
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
2
|
+
import { useFilterContext } from '../../Filter.context';
|
|
3
|
+
import { getOptionLabel } from '../../Filter.utils';
|
|
4
|
+
import { FilterCategoryProps } from '../../Filter.types';
|
|
5
|
+
import FilterSwitch from './FilterSwitch';
|
|
6
|
+
import FilterSwitchGroup from './FilterSwitchGroup';
|
|
7
7
|
|
|
8
8
|
type FilterSwitchAdapterProps = {
|
|
9
9
|
category: string;
|
|
@@ -49,6 +49,8 @@ export const FILTER_STORY_DATA: FilterData = {
|
|
|
49
49
|
type: 'tag',
|
|
50
50
|
},
|
|
51
51
|
cache_type: {
|
|
52
|
+
categoryName: 'Cache type',
|
|
53
|
+
categoryNamePlural: 'Cache types',
|
|
52
54
|
allOptionLabel: 'All items',
|
|
53
55
|
iconsMap: {
|
|
54
56
|
bazel: 'Bazel',
|
|
@@ -63,7 +65,7 @@ export const FILTER_STORY_DATA: FilterData = {
|
|
|
63
65
|
},
|
|
64
66
|
date_range: {
|
|
65
67
|
categoryName: 'Date',
|
|
66
|
-
categoryNamePlural: '
|
|
68
|
+
categoryNamePlural: 'Dates',
|
|
67
69
|
isMultiple: true,
|
|
68
70
|
dayTooltip: ({ date }) => (date > DateTime.now() ? 'Date must be in the past' : undefined),
|
|
69
71
|
selectable: new DateRange(DateTime.now().minus({ days: 15 }), DateTime.now()),
|
|
@@ -97,6 +99,7 @@ export const FILTER_STORY_DATA: FilterData = {
|
|
|
97
99
|
},
|
|
98
100
|
workflow: {
|
|
99
101
|
categoryName: 'Workflow',
|
|
102
|
+
categoryNamePlural: 'Workflows',
|
|
100
103
|
isMultiple: true,
|
|
101
104
|
options: FILTER_STORY_OPTIONS,
|
|
102
105
|
type: 'tag',
|
|
@@ -115,4 +118,14 @@ export const FILTER_STORY_CONTEXT: FilterContextType = {
|
|
|
115
118
|
onFilterClear: () => {},
|
|
116
119
|
setPopoverOpen: () => {},
|
|
117
120
|
state: FILTER_STORY_INIT_STATE,
|
|
121
|
+
filters: {
|
|
122
|
+
dateRange: {},
|
|
123
|
+
search: {},
|
|
124
|
+
select: {},
|
|
125
|
+
switch: {},
|
|
126
|
+
tag: {},
|
|
127
|
+
},
|
|
128
|
+
isPopoverOpen: false,
|
|
129
|
+
onClearFilters: () => {},
|
|
130
|
+
setSelectedCategory: () => {},
|
|
118
131
|
};
|
|
@@ -1,58 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Modal, ModalOverlay, useMultiStyleConfig } from 'chakra-ui-2--react';
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
3
2
|
import { isEqual } from '../../utils/utils';
|
|
4
|
-
import Box, { BoxProps } from '../Box/Box';
|
|
5
|
-
import Button from '../Button/Button';
|
|
6
|
-
import Divider from '../Divider/Divider';
|
|
7
|
-
import Icon from '../Icon/Icon';
|
|
8
3
|
import { FilterContext } from './Filter.context';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
FilterCategoryProps,
|
|
12
|
-
FilterContextType,
|
|
13
|
-
FilterData,
|
|
14
|
-
FilterState,
|
|
15
|
-
FilterType,
|
|
16
|
-
FilterValue,
|
|
17
|
-
} from './Filter.types';
|
|
4
|
+
import { FilterContextType, FilterProps, Filters, FilterState, FilterValue } from './Filter.types';
|
|
18
5
|
import { getDependents } from './Filter.utils';
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import FilterSearch from './FilterSearch/FilterSearch';
|
|
22
|
-
import FilterDate from './FilterDate/FilterDate';
|
|
23
|
-
import FilterSwitchAdapter from './FilterSwitchAdapter/FilterSwitchAdapter';
|
|
24
|
-
|
|
25
|
-
export interface FilterProps extends Omit<BoxProps, 'onChange'> {
|
|
26
|
-
data: FilterData;
|
|
27
|
-
defaultState?: FilterState;
|
|
28
|
-
filtersDependOn?: string[];
|
|
29
|
-
isLoading?: boolean;
|
|
30
|
-
onChange: (state: FilterState) => void;
|
|
31
|
-
searchTooltip?: ReactNode;
|
|
32
|
-
showAdd?: boolean;
|
|
33
|
-
showFilterIcon?: boolean;
|
|
34
|
-
showClearFilters?: boolean;
|
|
35
|
-
showSearch?: boolean;
|
|
36
|
-
state: FilterState;
|
|
37
|
-
}
|
|
6
|
+
import FilterMobile from './Mobile/Filter';
|
|
7
|
+
import FilterDesktop from './Desktop/Filter';
|
|
38
8
|
|
|
39
9
|
const Filter = (props: FilterProps) => {
|
|
40
|
-
const {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
filtersDependOn,
|
|
44
|
-
isLoading,
|
|
45
|
-
onChange,
|
|
46
|
-
searchTooltip,
|
|
47
|
-
showAdd = true,
|
|
48
|
-
showFilterIcon = true,
|
|
49
|
-
showClearFilters = true,
|
|
50
|
-
showSearch,
|
|
51
|
-
state,
|
|
52
|
-
...rest
|
|
53
|
-
} = props;
|
|
54
|
-
|
|
55
|
-
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
10
|
+
const { data, defaultState, filtersDependOn, isLoading, isMobile, onChange, showClearFilters = true, state } = props;
|
|
11
|
+
|
|
12
|
+
const [selectedCategory, setSelectedCategory] = useState<string | undefined>();
|
|
56
13
|
|
|
57
14
|
const cleanState: FilterState = {};
|
|
58
15
|
Object.entries(state).forEach(([category, values]) => {
|
|
@@ -106,13 +63,13 @@ const Filter = (props: FilterProps) => {
|
|
|
106
63
|
onChange(stateToRestore || {});
|
|
107
64
|
};
|
|
108
65
|
|
|
109
|
-
const filters = {
|
|
66
|
+
const filters: Filters = {
|
|
110
67
|
dateRange: {},
|
|
111
68
|
search: {},
|
|
112
69
|
select: {},
|
|
113
70
|
switch: {},
|
|
114
71
|
tag: {},
|
|
115
|
-
}
|
|
72
|
+
};
|
|
116
73
|
|
|
117
74
|
Object.entries(data).forEach(([category, value]) => {
|
|
118
75
|
filters[value.type || 'tag'][category] = value;
|
|
@@ -123,11 +80,17 @@ const Filter = (props: FilterProps) => {
|
|
|
123
80
|
const contextValue: FilterContextType = useMemo(
|
|
124
81
|
() => ({
|
|
125
82
|
data,
|
|
83
|
+
filters,
|
|
126
84
|
filtersDependOn,
|
|
85
|
+
isPopoverOpen,
|
|
127
86
|
isLoading,
|
|
87
|
+
onClearFilters,
|
|
128
88
|
onFilterChange,
|
|
129
89
|
onFilterClear,
|
|
90
|
+
selectedCategory,
|
|
91
|
+
showClearButton,
|
|
130
92
|
setPopoverOpen,
|
|
93
|
+
setSelectedCategory,
|
|
131
94
|
state: cleanState,
|
|
132
95
|
}),
|
|
133
96
|
[data, filtersDependOn, isLoading, onFilterChange, onFilterClear, setPopoverOpen, cleanState],
|
|
@@ -135,60 +98,7 @@ const Filter = (props: FilterProps) => {
|
|
|
135
98
|
|
|
136
99
|
return (
|
|
137
100
|
<FilterContext value={contextValue}>
|
|
138
|
-
<
|
|
139
|
-
<Modal isOpen={isPopoverOpen} onClose={() => {}}>
|
|
140
|
-
{isPopoverOpen && <ModalOverlay />}
|
|
141
|
-
</Modal>
|
|
142
|
-
<Box sx={filterStyle.content}>
|
|
143
|
-
{showFilterIcon && <Icon name="Filter" sx={filterStyle.icon} />}
|
|
144
|
-
|
|
145
|
-
{Object.keys(filters.switch).map((category) => (
|
|
146
|
-
<FilterSwitchAdapter key={category} category={category} />
|
|
147
|
-
))}
|
|
148
|
-
|
|
149
|
-
{Object.keys(filters.dateRange).map((category) => (
|
|
150
|
-
<FilterDate key={category} category={category} />
|
|
151
|
-
))}
|
|
152
|
-
|
|
153
|
-
{Object.keys(filters.select).map((category) => (
|
|
154
|
-
<FilterItem key={category} category={category} />
|
|
155
|
-
))}
|
|
156
|
-
{Object.keys(filters.tag).map((category) => {
|
|
157
|
-
if (!cleanState[category]) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
return <FilterItem key={category} category={category} />;
|
|
161
|
-
})}
|
|
162
|
-
|
|
163
|
-
{showAdd && <FilterAdd onChange={onFilterChange} />}
|
|
164
|
-
</Box>
|
|
165
|
-
{(showClearButton || showSearch) && (
|
|
166
|
-
<Box sx={filterStyle.rightContent}>
|
|
167
|
-
{showClearButton && (
|
|
168
|
-
<Button
|
|
169
|
-
isDisabled={isLoading}
|
|
170
|
-
leftIconName="Cross"
|
|
171
|
-
minWidth="7.5rem"
|
|
172
|
-
onClick={onClearFilters}
|
|
173
|
-
size="sm"
|
|
174
|
-
variant="tertiary"
|
|
175
|
-
>
|
|
176
|
-
Clear filters
|
|
177
|
-
</Button>
|
|
178
|
-
)}
|
|
179
|
-
{showSearch && (
|
|
180
|
-
<>
|
|
181
|
-
<Divider flexShrink="0" orientation="vertical" size="1" variant="solid" />
|
|
182
|
-
<FilterSearch
|
|
183
|
-
onChange={onFilterChange}
|
|
184
|
-
value={cleanState.search?.length ? cleanState.search[0] : ''}
|
|
185
|
-
searchTooltip={searchTooltip}
|
|
186
|
-
/>
|
|
187
|
-
</>
|
|
188
|
-
)}
|
|
189
|
-
</Box>
|
|
190
|
-
)}
|
|
191
|
-
</Box>
|
|
101
|
+
{isMobile ? <FilterMobile {...props} /> : <FilterDesktop {...props} />}
|
|
192
102
|
</FilterContext>
|
|
193
103
|
);
|
|
194
104
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Dispatch, SetStateAction } from 'react';
|
|
1
|
+
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
|
2
2
|
import { TypeIconName } from '../Icon/Icon';
|
|
3
3
|
import { DatePickerProps, DateRange } from '../DatePicker/DatePicker';
|
|
4
|
+
import { BoxProps } from '../Box/Box';
|
|
4
5
|
|
|
5
6
|
export type FilterType = 'dateRange' | 'search' | 'select' | 'switch' | 'tag';
|
|
6
7
|
|
|
@@ -58,10 +59,42 @@ export type FilterState = Record<string, FilterValue>;
|
|
|
58
59
|
|
|
59
60
|
export interface FilterContextType {
|
|
60
61
|
data: FilterData;
|
|
62
|
+
filters: Filters;
|
|
61
63
|
filtersDependOn?: string[];
|
|
62
64
|
isLoading?: boolean;
|
|
65
|
+
isPopoverOpen: boolean;
|
|
66
|
+
onClearFilters: () => void;
|
|
63
67
|
onFilterClear: (category: string) => void;
|
|
64
68
|
onFilterChange: (category: string, value: FilterValue) => void;
|
|
69
|
+
selectedCategory?: string;
|
|
65
70
|
setPopoverOpen: Dispatch<SetStateAction<boolean>>;
|
|
71
|
+
setSelectedCategory: Dispatch<SetStateAction<string | undefined>>;
|
|
72
|
+
showClearButton?: boolean;
|
|
66
73
|
state: FilterState;
|
|
67
74
|
}
|
|
75
|
+
|
|
76
|
+
export type Filters = Record<FilterType, Record<string, FilterCategoryProps>>;
|
|
77
|
+
|
|
78
|
+
export interface FilterProps extends Omit<BoxProps, 'onChange'> {
|
|
79
|
+
data: FilterData;
|
|
80
|
+
defaultState?: FilterState;
|
|
81
|
+
filtersDependOn?: string[];
|
|
82
|
+
isLoading?: boolean;
|
|
83
|
+
isMobile?: boolean;
|
|
84
|
+
onChange: (state: FilterState) => void;
|
|
85
|
+
searchTooltip?: ReactNode;
|
|
86
|
+
showAdd?: boolean;
|
|
87
|
+
showFilterIcon?: boolean;
|
|
88
|
+
showClearFilters?: boolean;
|
|
89
|
+
showSearch?: boolean;
|
|
90
|
+
state: FilterState;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type FilterFormProps = {
|
|
94
|
+
category: string;
|
|
95
|
+
categoryName?: string;
|
|
96
|
+
categoryNamePlural?: string;
|
|
97
|
+
loadingText?: string;
|
|
98
|
+
onChange: (category: string, selected: FilterValue, previousValue: FilterValue) => void;
|
|
99
|
+
onCancel: () => void;
|
|
100
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { DateRange } from '../DatePicker/DatePicker';
|
|
1
3
|
import { FilterData, FilterOptionsMap } from './Filter.types';
|
|
2
4
|
|
|
3
5
|
export const hasAllDependencies = (stateKeys: string[], dependsOn?: string[]): boolean => {
|
|
@@ -38,3 +40,14 @@ export const getOptionLabel = (option: string, optionsMap?: FilterOptionsMap) =>
|
|
|
38
40
|
}
|
|
39
41
|
return optionsMap[option] || option;
|
|
40
42
|
};
|
|
43
|
+
|
|
44
|
+
export const getDateRangeLabel = (value: string[] | undefined): string => {
|
|
45
|
+
const selectedRange: DateRange | undefined =
|
|
46
|
+
value && value[0]
|
|
47
|
+
? new DateRange(DateTime.fromMillis(Number(value[0])), DateTime.fromMillis(Number(value[1])))
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
return selectedRange
|
|
51
|
+
? `${selectedRange.from?.toFormat('LLL dd')} - ${selectedRange.to?.toFormat('LLL dd')}`
|
|
52
|
+
: 'All dates';
|
|
53
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import Box from '../../Box/Box';
|
|
3
|
+
import DatePicker, { DatePickerProps, DateRange, useDateRange } from '../../DatePicker/DatePicker';
|
|
4
|
+
import { useFilterContext } from '../Filter.context';
|
|
5
|
+
import { FilterCategoryProps } from '../Filter.types';
|
|
6
|
+
|
|
7
|
+
type DateSelectOptionProps = {
|
|
8
|
+
onClear: () => void;
|
|
9
|
+
onChange: (dateRange: string[]) => void;
|
|
10
|
+
selectedItems: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DateSelectOption = ({ onClear, onChange, selectedItems }: DateSelectOptionProps) => {
|
|
14
|
+
const { data } = useFilterContext();
|
|
15
|
+
|
|
16
|
+
const dateRangeData = data.date_range as FilterCategoryProps & {
|
|
17
|
+
dayTooltip?: DatePickerProps['dayTooltip'];
|
|
18
|
+
selectable?: DateRange;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleChange = (range: DateRange) => {
|
|
22
|
+
if (range.from && range.to) {
|
|
23
|
+
onChange([String(range.from.toMillis()), String(range.to.endOf('day').toMillis())]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const now = DateTime.local();
|
|
28
|
+
const twoYearsAgo = now.minus({ years: 2 });
|
|
29
|
+
|
|
30
|
+
const selectable = useDateRange(twoYearsAgo, now);
|
|
31
|
+
|
|
32
|
+
const selectedRange: DateRange | undefined =
|
|
33
|
+
selectedItems && selectedItems[0]
|
|
34
|
+
? new DateRange(DateTime.fromMillis(Number(selectedItems[0])), DateTime.fromMillis(Number(selectedItems[1])))
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Box display="flex" justifyContent="center">
|
|
39
|
+
<DatePicker
|
|
40
|
+
dayTooltip={dateRangeData.dayTooltip}
|
|
41
|
+
mode="range"
|
|
42
|
+
onApply={handleChange}
|
|
43
|
+
onClear={selectedItems?.length ? onClear : undefined}
|
|
44
|
+
selectable={dateRangeData.selectable || selectable}
|
|
45
|
+
selected={selectedRange}
|
|
46
|
+
variant="inline"
|
|
47
|
+
visible
|
|
48
|
+
/>
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default DateSelectOption;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Badge from '../../Badge/Badge';
|
|
2
|
+
import Text from '../../Text/Text';
|
|
3
|
+
import Button from '../../Button/Button';
|
|
4
|
+
import Box from '../../Box/Box';
|
|
5
|
+
import { useFilterContext } from '../Filter.context';
|
|
6
|
+
import { FilterProps } from '../Filter.types';
|
|
7
|
+
import FilterDrawer from './FilterDrawer';
|
|
8
|
+
import FilterForm from './FilterForm';
|
|
9
|
+
|
|
10
|
+
const Filter = ({ isLoading, showAdd = true }: FilterProps) => {
|
|
11
|
+
const {
|
|
12
|
+
isPopoverOpen,
|
|
13
|
+
onClearFilters,
|
|
14
|
+
selectedCategory,
|
|
15
|
+
setSelectedCategory,
|
|
16
|
+
setPopoverOpen,
|
|
17
|
+
showClearButton,
|
|
18
|
+
state,
|
|
19
|
+
} = useFilterContext();
|
|
20
|
+
|
|
21
|
+
const count = Object.values(state).filter((item) => item?.length > 0).length;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<Box alignItems="center" display="flex" justifyContent="space-between">
|
|
26
|
+
<Button leftIconName="Filter" onClick={() => setPopoverOpen(true)} size="sm" variant="secondary">
|
|
27
|
+
<Text alignItems="center" display="flex" gap="8">
|
|
28
|
+
Filters
|
|
29
|
+
<Badge colorScheme={count > 0 ? 'progress' : 'neutral'} variant="subtle">
|
|
30
|
+
{String(count)}
|
|
31
|
+
</Badge>
|
|
32
|
+
</Text>
|
|
33
|
+
</Button>
|
|
34
|
+
{showClearButton && (
|
|
35
|
+
<Button
|
|
36
|
+
isDisabled={isLoading}
|
|
37
|
+
leftIconName="Cross"
|
|
38
|
+
minWidth="7.5rem"
|
|
39
|
+
onClick={onClearFilters}
|
|
40
|
+
size="sm"
|
|
41
|
+
variant="tertiary"
|
|
42
|
+
>
|
|
43
|
+
Clear filters
|
|
44
|
+
</Button>
|
|
45
|
+
)}
|
|
46
|
+
</Box>
|
|
47
|
+
<FilterDrawer isOpen={isPopoverOpen} onClose={() => setPopoverOpen(false)} showAdd={showAdd} />
|
|
48
|
+
<FilterForm
|
|
49
|
+
isOpen={Boolean(selectedCategory)}
|
|
50
|
+
onClose={() => setSelectedCategory(undefined)}
|
|
51
|
+
selectedCategory={selectedCategory || ''}
|
|
52
|
+
/>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default Filter;
|