@bitrise/bitkit 12.69.0 → 12.70.0-alpha.1
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 +1 -1
- package/src/Components/Button/Button.theme.ts +1 -1
- package/src/Components/DatePicker/DatePicker.tsx +4 -3
- package/src/Components/DatePicker/DatePickerFooter.tsx +7 -0
- package/src/Components/Filter/Filter.context.tsx +6 -0
- package/src/Components/Filter/Filter.storyData.ts +71 -0
- package/src/Components/Filter/Filter.theme.ts +161 -0
- package/src/Components/Filter/Filter.tsx +159 -0
- package/src/Components/Filter/Filter.types.ts +29 -0
- package/src/Components/Filter/Filter.utils.ts +33 -0
- package/src/Components/Filter/FilterAdd/FilterAdd.tsx +92 -0
- package/src/Components/Filter/FilterDate/FilterDate.tsx +78 -0
- package/src/Components/Filter/FilterForm/FilterForm.tsx +171 -0
- package/src/Components/Filter/FilterItem/FilterItem.tsx +100 -0
- package/src/Components/Filter/FilterSearch/FilterSearch.tsx +65 -0
- package/src/Components/{Form → Filter}/FilterSwitch/FilterSwitch.theme.ts +11 -9
- package/src/Components/{Form → Filter}/FilterSwitch/FilterSwitch.tsx +3 -5
- package/src/Components/Form/Checkbox/CheckboxGroup.tsx +4 -1
- package/src/Components/Icons/Icons.tsx +2 -2
- package/src/Components/Text/Text.tsx +0 -39
- package/src/index.ts +3 -3
- package/src/theme.ts +3 -1
- package/src/utils/utils.ts +23 -1
- /package/src/Components/{Form → Filter}/FilterSwitch/FilterSwitchGroup.tsx +0 -0
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ export interface DatePickerProps {
|
|
|
22
22
|
selected?: DateRange;
|
|
23
23
|
onApply?: (range: DateRange) => void;
|
|
24
24
|
onClose: () => void;
|
|
25
|
+
onClear?: () => void;
|
|
25
26
|
visible: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -30,7 +31,7 @@ export interface DatePickerProps {
|
|
|
30
31
|
* range selection.
|
|
31
32
|
*/
|
|
32
33
|
const DatePicker = (props: DatePickerProps) => {
|
|
33
|
-
const { children, onApply, onClose, visible, selectable, selected } = props;
|
|
34
|
+
const { children, onApply, onClose, onClear, visible, selectable, selected } = props;
|
|
34
35
|
|
|
35
36
|
const { isMobile } = useResponsive();
|
|
36
37
|
const today = DateTime.now().startOf('day');
|
|
@@ -63,7 +64,7 @@ const DatePicker = (props: DatePickerProps) => {
|
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
const { leftViewDate, rightViewDate, updateLeftViewDate, updateRightViewDate } = useViewDate({
|
|
66
|
-
initalView: dateFrom || selectable?.
|
|
67
|
+
initalView: dateFrom || selectable?.to,
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
const [preview, setPreview] = useState<'from' | 'to' | undefined>(undefined);
|
|
@@ -164,7 +165,7 @@ const DatePicker = (props: DatePickerProps) => {
|
|
|
164
165
|
/>
|
|
165
166
|
)}
|
|
166
167
|
</Box>
|
|
167
|
-
<DatePickerFooter onApply={handleApply} onClose={handleClose} selected={selected} />
|
|
168
|
+
<DatePickerFooter onApply={handleApply} onClose={handleClose} onClear={onClear} selected={selected} />
|
|
168
169
|
</>
|
|
169
170
|
)}
|
|
170
171
|
</Box>
|
|
@@ -8,10 +8,12 @@ const DatePickerFooter = ({
|
|
|
8
8
|
selected,
|
|
9
9
|
onClose,
|
|
10
10
|
onApply,
|
|
11
|
+
onClear,
|
|
11
12
|
}: {
|
|
12
13
|
selected?: DateRange;
|
|
13
14
|
onClose: () => void;
|
|
14
15
|
onApply: () => void;
|
|
16
|
+
onClear?: () => void;
|
|
15
17
|
}) => {
|
|
16
18
|
return (
|
|
17
19
|
<Box
|
|
@@ -20,6 +22,11 @@ const DatePickerFooter = ({
|
|
|
20
22
|
gridTemplateRows={['1.25rem 2rem', 'unset']}
|
|
21
23
|
gap="24"
|
|
22
24
|
>
|
|
25
|
+
{!!onClear && (
|
|
26
|
+
<Button size="small" variant="tertiary" width="fit-content" onClick={() => onClear()}>
|
|
27
|
+
Clear
|
|
28
|
+
</Button>
|
|
29
|
+
)}
|
|
23
30
|
<Text alignSelf="center" justifySelf="center" size="2" color="text.secondary" gridColumn={['1', '2']}>
|
|
24
31
|
{selected?.from?.toFormat('DD', { locale: 'en-US' })}
|
|
25
32
|
{selected?.from || selected?.to ? ' - ' : undefined}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { FilterContextType, FilterData, FilterOptions, FilterState } from './Filter.types';
|
|
2
|
+
|
|
3
|
+
export const FILTER_STORY_OPTIONS: FilterOptions =
|
|
4
|
+
'Lorem ipsum dolor sit amet consectetur adipisicing elit Reiciendis reprehenderit laudantium laborum excepturi nam quae quod sunt expedita vel repellat cum cupiditate esse similique est ducimus provident eos numquam voluptas'.split(
|
|
5
|
+
' ',
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export const FILTER_STORY_DATA: FilterData = {
|
|
9
|
+
date_range: {
|
|
10
|
+
categoryName: 'Date',
|
|
11
|
+
categoryNamePlural: 'dates',
|
|
12
|
+
isPermanent: true,
|
|
13
|
+
},
|
|
14
|
+
pipeline: {
|
|
15
|
+
categoryName: 'Pipeline',
|
|
16
|
+
categoryNamePlural: 'Pipelines',
|
|
17
|
+
options: FILTER_STORY_OPTIONS,
|
|
18
|
+
},
|
|
19
|
+
stage: {
|
|
20
|
+
categoryName: 'Stage',
|
|
21
|
+
categoryNamePlural: 'Stages',
|
|
22
|
+
options: FILTER_STORY_OPTIONS,
|
|
23
|
+
dependsOn: ['pipeline'],
|
|
24
|
+
},
|
|
25
|
+
workflow: {
|
|
26
|
+
categoryName: 'Workflow',
|
|
27
|
+
isMultiple: true,
|
|
28
|
+
options: FILTER_STORY_OPTIONS,
|
|
29
|
+
},
|
|
30
|
+
branch: {
|
|
31
|
+
options: [
|
|
32
|
+
'BIVS-2231-create-rename-to-utilisation-views',
|
|
33
|
+
'revert-11937-revert-11884-RA-2060-release-manager-role',
|
|
34
|
+
'master',
|
|
35
|
+
'CI-2264-consolidate-other-provider-type-to-custom',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
app: {
|
|
39
|
+
categoryName: 'App',
|
|
40
|
+
isPermanent: true,
|
|
41
|
+
options: ['46b6b9a78a418ee8', '32b14416be4b7b24', '0a248b278e135ea7'],
|
|
42
|
+
optionsMap: {
|
|
43
|
+
'46b6b9a78a418ee8': 'bitrise-website',
|
|
44
|
+
'32b14416be4b7b24': 'bitkit',
|
|
45
|
+
'0a248b278e135ea7': 'pipeline-service',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
test_case: {
|
|
49
|
+
categoryName: 'Test case',
|
|
50
|
+
onAsyncSearch: (category, q) => {
|
|
51
|
+
console.log('onAsyncSearch', { category, q });
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
resolve(['found 1', 'found 2']);
|
|
55
|
+
}, 2000);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
options: ['default 1', 'default 2', 'default 3'],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const FILTER_STORY_INIT_STATE: FilterState = {
|
|
63
|
+
pipeline: ['ipsum'],
|
|
64
|
+
app: ['46b6b9a78a418ee8'],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const FILTER_STORY_CONTEXT: FilterContextType = {
|
|
68
|
+
data: FILTER_STORY_DATA,
|
|
69
|
+
setPopoverOpen: () => {},
|
|
70
|
+
state: FILTER_STORY_INIT_STATE,
|
|
71
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createMultiStyleConfigHelpers, SystemStyleObject } from '@chakra-ui/styled-system';
|
|
2
|
+
import { rem } from '../../utils/utils';
|
|
3
|
+
import { disabledStates } from '../Button/Button.theme';
|
|
4
|
+
|
|
5
|
+
export const parts = [
|
|
6
|
+
'container',
|
|
7
|
+
'icon',
|
|
8
|
+
'content',
|
|
9
|
+
'rightContent',
|
|
10
|
+
'item',
|
|
11
|
+
'tagEdit',
|
|
12
|
+
'tagClear',
|
|
13
|
+
'form',
|
|
14
|
+
'formHeader',
|
|
15
|
+
'formTitle',
|
|
16
|
+
'formBadge',
|
|
17
|
+
'formSearch',
|
|
18
|
+
'formInputGroup',
|
|
19
|
+
'formButtonGroup',
|
|
20
|
+
'searchInput',
|
|
21
|
+
] as const;
|
|
22
|
+
export type FilterStyle = Record<(typeof parts)[number], SystemStyleObject>;
|
|
23
|
+
|
|
24
|
+
const { defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts);
|
|
25
|
+
|
|
26
|
+
const FilterTheme = defineMultiStyleConfig({
|
|
27
|
+
baseStyle: {
|
|
28
|
+
container: {
|
|
29
|
+
background: 'neutral.100',
|
|
30
|
+
display: 'flex',
|
|
31
|
+
flexDirection: ['column', 'row'],
|
|
32
|
+
gap: '12',
|
|
33
|
+
},
|
|
34
|
+
icon: {
|
|
35
|
+
margin: '4',
|
|
36
|
+
color: 'neutral.60',
|
|
37
|
+
},
|
|
38
|
+
content: {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
gap: '12',
|
|
41
|
+
flexWrap: 'wrap',
|
|
42
|
+
},
|
|
43
|
+
rightContent: {
|
|
44
|
+
display: 'flex',
|
|
45
|
+
gap: '16',
|
|
46
|
+
marginInlineStart: 'auto',
|
|
47
|
+
},
|
|
48
|
+
item: {
|
|
49
|
+
border: '1px solid',
|
|
50
|
+
borderColor: 'neutral.80',
|
|
51
|
+
borderRadius: '4',
|
|
52
|
+
display: 'flex',
|
|
53
|
+
width: 'fit-content',
|
|
54
|
+
background: 'neutral.100',
|
|
55
|
+
wordBreak: 'break-word',
|
|
56
|
+
},
|
|
57
|
+
tagEdit: {
|
|
58
|
+
color: 'purple.10',
|
|
59
|
+
display: 'flex',
|
|
60
|
+
gap: '8',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
paddingBlock: '4',
|
|
63
|
+
paddingInlineStart: rem(11),
|
|
64
|
+
paddingInlineEnd: rem(7),
|
|
65
|
+
borderRadius: '4',
|
|
66
|
+
_hover: {
|
|
67
|
+
background: 'neutral.93',
|
|
68
|
+
},
|
|
69
|
+
_active: {
|
|
70
|
+
background: 'neutral.90',
|
|
71
|
+
},
|
|
72
|
+
_disabled: {
|
|
73
|
+
cursor: 'not-allowed',
|
|
74
|
+
...disabledStates.tertiary,
|
|
75
|
+
_hover: disabledStates.tertiary,
|
|
76
|
+
_active: disabledStates.tertiary,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
tagClear: {
|
|
80
|
+
color: 'purple.10',
|
|
81
|
+
borderRadius: '4',
|
|
82
|
+
border: 0,
|
|
83
|
+
minWidth: rem(27),
|
|
84
|
+
height: rem(30),
|
|
85
|
+
justifyContent: 'flex-start',
|
|
86
|
+
svg: {
|
|
87
|
+
marginInlineStart: '4',
|
|
88
|
+
},
|
|
89
|
+
_hover: {
|
|
90
|
+
background: 'neutral.93',
|
|
91
|
+
},
|
|
92
|
+
_active: {
|
|
93
|
+
background: 'neutral.90',
|
|
94
|
+
},
|
|
95
|
+
_disabled: {
|
|
96
|
+
cursor: 'not-allowed',
|
|
97
|
+
...disabledStates.tertiary,
|
|
98
|
+
_hover: disabledStates.tertiary,
|
|
99
|
+
_active: disabledStates.tertiary,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
form: {
|
|
103
|
+
background: 'neutral.100',
|
|
104
|
+
borderRadius: '8',
|
|
105
|
+
padding: '16',
|
|
106
|
+
minWidth: rem(352),
|
|
107
|
+
maxWidth: rem(480),
|
|
108
|
+
},
|
|
109
|
+
formHeader: {
|
|
110
|
+
display: 'flex',
|
|
111
|
+
gap: '8',
|
|
112
|
+
justifyContent: 'space-between',
|
|
113
|
+
marginBlockEnd: '8',
|
|
114
|
+
},
|
|
115
|
+
formTitle: {
|
|
116
|
+
color: 'purpe.10',
|
|
117
|
+
fontWeight: 'demiBold',
|
|
118
|
+
},
|
|
119
|
+
formBadge: {
|
|
120
|
+
backgroundColor: 'neutral.93',
|
|
121
|
+
color: 'neutral.40',
|
|
122
|
+
fontWeight: 'bold',
|
|
123
|
+
fontVariantNumeric: 'tabular-nums',
|
|
124
|
+
},
|
|
125
|
+
formSearch: {
|
|
126
|
+
marginBlockEnd: '16',
|
|
127
|
+
},
|
|
128
|
+
formInputGroup: {
|
|
129
|
+
display: 'flex',
|
|
130
|
+
flexDirection: 'column',
|
|
131
|
+
gap: '12',
|
|
132
|
+
maxHeight: rem(196),
|
|
133
|
+
overflowY: 'scroll',
|
|
134
|
+
paddingInline: '12',
|
|
135
|
+
paddingBlock: rem(3),
|
|
136
|
+
},
|
|
137
|
+
formButtonGroup: {
|
|
138
|
+
display: 'flex',
|
|
139
|
+
justifyContent: 'flex-end',
|
|
140
|
+
marginBlockStart: '24',
|
|
141
|
+
},
|
|
142
|
+
searchInput: {
|
|
143
|
+
paddingLeft: '32',
|
|
144
|
+
paddingY: '4',
|
|
145
|
+
paddingRight: '8',
|
|
146
|
+
border: undefined,
|
|
147
|
+
borderRadius: '4',
|
|
148
|
+
borderColor: undefined,
|
|
149
|
+
boxShadow: undefined,
|
|
150
|
+
fontSize: '2',
|
|
151
|
+
_focusVisible: {
|
|
152
|
+
backgroundColor: 'neutral.95',
|
|
153
|
+
},
|
|
154
|
+
_hover: {
|
|
155
|
+
backgroundColor: 'neutral.95',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export default FilterTheme;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Modal, ModalOverlay, useMultiStyleConfig } from '@chakra-ui/react';
|
|
3
|
+
import Box, { BoxProps } from '../Box/Box';
|
|
4
|
+
import Button from '../Button/Button';
|
|
5
|
+
import Divider from '../Divider/Divider';
|
|
6
|
+
import Icon from '../Icon/Icon';
|
|
7
|
+
import { FilterContext } from './Filter.context';
|
|
8
|
+
import { FilterStyle } from './Filter.theme';
|
|
9
|
+
import { FilterContextType, FilterData, FilterState, FilterValue } from './Filter.types';
|
|
10
|
+
import { getDependents, hasAllDependencies } from './Filter.utils';
|
|
11
|
+
import FilterAdd from './FilterAdd/FilterAdd';
|
|
12
|
+
import FilterItem from './FilterItem/FilterItem';
|
|
13
|
+
import FilterSearch from './FilterSearch/FilterSearch';
|
|
14
|
+
import FilterDate from './FilterDate/FilterDate';
|
|
15
|
+
|
|
16
|
+
export interface FilterProps extends Omit<BoxProps, 'onChange'> {
|
|
17
|
+
filtersDependOn?: string[];
|
|
18
|
+
initialData: FilterData;
|
|
19
|
+
isLoading?: boolean;
|
|
20
|
+
onChange: (state: FilterState) => void;
|
|
21
|
+
showSearch?: boolean;
|
|
22
|
+
state: FilterState;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Filter = (props: FilterProps) => {
|
|
26
|
+
const { filtersDependOn, initialData, isLoading, onChange, showSearch, state, ...rest } = props;
|
|
27
|
+
|
|
28
|
+
const isInited = useRef<boolean>(false);
|
|
29
|
+
|
|
30
|
+
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
31
|
+
|
|
32
|
+
const [data] = useState<FilterData>(initialData);
|
|
33
|
+
const [isPopoverOpen, setPopoverOpen] = useState<boolean>(false);
|
|
34
|
+
|
|
35
|
+
const deleteFromState = (category: string, stateProp: FilterState): FilterState => {
|
|
36
|
+
const filteredState = { ...stateProp };
|
|
37
|
+
delete filteredState[category];
|
|
38
|
+
|
|
39
|
+
const dependents = getDependents(data, category, filtersDependOn);
|
|
40
|
+
|
|
41
|
+
dependents.forEach((dependent) => {
|
|
42
|
+
delete filteredState[dependent];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return filteredState;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const onFilterChange = (category: string, value: FilterValue) => {
|
|
49
|
+
if (isInited.current === false) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let newState = { ...state };
|
|
53
|
+
if (value && value.length > 0) {
|
|
54
|
+
newState[category] = value;
|
|
55
|
+
} else if (newState[category]) {
|
|
56
|
+
newState = deleteFromState(category, newState);
|
|
57
|
+
}
|
|
58
|
+
onChange(newState);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onClear = (category: string) => {
|
|
62
|
+
onChange(deleteFromState(category, state));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onClearFilters = () => {
|
|
66
|
+
onChange({});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const permanentCategories = Object.keys(data).filter(
|
|
70
|
+
(category) => data[category].isPermanent && category !== 'date_range',
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const stateCategories = Object.keys(state).filter((c) => !['date_range', 'search'].includes(c));
|
|
74
|
+
const filteredStateCategories = stateCategories.filter((category) => !data[category].isPermanent);
|
|
75
|
+
|
|
76
|
+
const filterCategories = [...permanentCategories, ...filteredStateCategories];
|
|
77
|
+
|
|
78
|
+
const showClearFilters = stateCategories.length > 0 || (state.search && state.search.length > 0);
|
|
79
|
+
|
|
80
|
+
const contextValue: FilterContextType = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
data: initialData,
|
|
83
|
+
filtersDependOn,
|
|
84
|
+
isLoading,
|
|
85
|
+
setPopoverOpen,
|
|
86
|
+
state,
|
|
87
|
+
}),
|
|
88
|
+
[filtersDependOn, isLoading, initialData, setPopoverOpen, state],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (isInited.current === false) {
|
|
93
|
+
isInited.current = true;
|
|
94
|
+
}
|
|
95
|
+
}, [isInited.current]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<FilterContext value={contextValue}>
|
|
99
|
+
<Box sx={filterStyle.container} {...rest}>
|
|
100
|
+
<Modal isOpen={isPopoverOpen} onClose={() => {}}>
|
|
101
|
+
{isPopoverOpen && <ModalOverlay />}
|
|
102
|
+
</Modal>
|
|
103
|
+
<Box sx={filterStyle.content}>
|
|
104
|
+
<Icon name="Filter" sx={filterStyle.icon} />
|
|
105
|
+
{!!data.date_range && <FilterDate onChange={onFilterChange} onClear={onClear} value={state.date_range} />}
|
|
106
|
+
{filterCategories.length > 0 &&
|
|
107
|
+
filterCategories.map((category) => {
|
|
108
|
+
const { categoryName, categoryNamePlural, dependsOn, isPermanent, optionsMap } = data[category];
|
|
109
|
+
if (hasAllDependencies(filterCategories, dependsOn)) {
|
|
110
|
+
return (
|
|
111
|
+
<FilterItem
|
|
112
|
+
category={category}
|
|
113
|
+
categoryName={categoryName}
|
|
114
|
+
categoryNamePlural={categoryNamePlural}
|
|
115
|
+
isPermanent={isPermanent}
|
|
116
|
+
key={category}
|
|
117
|
+
onChange={onFilterChange}
|
|
118
|
+
onClear={onClear}
|
|
119
|
+
optionsMap={optionsMap}
|
|
120
|
+
value={state[category]}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
})}
|
|
126
|
+
<FilterAdd onChange={onFilterChange} />
|
|
127
|
+
</Box>
|
|
128
|
+
{(showClearFilters || showSearch) && (
|
|
129
|
+
<Box sx={filterStyle.rightContent}>
|
|
130
|
+
{showClearFilters && (
|
|
131
|
+
<Button
|
|
132
|
+
isDisabled={isLoading}
|
|
133
|
+
leftIconName="CloseSmall"
|
|
134
|
+
minWidth="7.5rem"
|
|
135
|
+
onClick={onClearFilters}
|
|
136
|
+
size="small"
|
|
137
|
+
variant="tertiary"
|
|
138
|
+
>
|
|
139
|
+
Clear filters
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
142
|
+
{showClearFilters && showSearch && (
|
|
143
|
+
<Divider orientation="vertical" size="1" variant="solid" flexShrink="0" />
|
|
144
|
+
)}
|
|
145
|
+
{showSearch && (
|
|
146
|
+
<FilterSearch
|
|
147
|
+
onChange={onFilterChange}
|
|
148
|
+
onClear={onClear}
|
|
149
|
+
value={(state.Search && state.Search[0]) || ''}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
</Box>
|
|
153
|
+
)}
|
|
154
|
+
</Box>
|
|
155
|
+
</FilterContext>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export default Filter;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
|
|
3
|
+
export type FilterOptions = string[];
|
|
4
|
+
export type FilterValue = string[];
|
|
5
|
+
export type FilterOptionsMap = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
export type FilterSearchCallback = (category: string, q: string) => Promise<FilterValue>;
|
|
8
|
+
|
|
9
|
+
export type FilterCategoryProps = {
|
|
10
|
+
categoryName?: string;
|
|
11
|
+
categoryNamePlural?: string;
|
|
12
|
+
dependsOn?: string[];
|
|
13
|
+
isMultiple?: boolean;
|
|
14
|
+
isPermanent?: boolean;
|
|
15
|
+
onAsyncSearch?: FilterSearchCallback;
|
|
16
|
+
options?: FilterOptions;
|
|
17
|
+
optionsMap?: FilterOptionsMap;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FilterData = Record<string, FilterCategoryProps>;
|
|
21
|
+
export type FilterState = Record<string, FilterValue>;
|
|
22
|
+
|
|
23
|
+
export interface FilterContextType {
|
|
24
|
+
data: FilterData;
|
|
25
|
+
filtersDependOn?: string[];
|
|
26
|
+
isLoading?: boolean;
|
|
27
|
+
setPopoverOpen: Dispatch<SetStateAction<boolean>>;
|
|
28
|
+
state: FilterState;
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FilterData, FilterOptionsMap } from './Filter.types';
|
|
2
|
+
|
|
3
|
+
export const hasAllDependencies = (stateKeys: string[], dependsOn?: string[]): boolean => {
|
|
4
|
+
if (!dependsOn || dependsOn.length === 0) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
return dependsOn.every((key) => stateKeys.includes(key));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getDependents = (data: FilterData, categoryKey: string, filtersDependOn?: string[]): string[] => {
|
|
11
|
+
const dependents: string[] = [];
|
|
12
|
+
if (filtersDependOn && filtersDependOn.includes(categoryKey)) {
|
|
13
|
+
Object.keys(data).forEach((category) => {
|
|
14
|
+
if (!data[category].isPermanent) {
|
|
15
|
+
dependents.push(category);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
19
|
+
Object.keys(data).forEach((category) => {
|
|
20
|
+
if (data[category].dependsOn?.includes(categoryKey)) {
|
|
21
|
+
dependents.push(category);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return dependents;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getOptionLabel = (option: string, optionsMap?: FilterOptionsMap) => {
|
|
29
|
+
if (!optionsMap) {
|
|
30
|
+
return option;
|
|
31
|
+
}
|
|
32
|
+
return optionsMap[option] || option;
|
|
33
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Menu, MenuButton, MenuList, useDisclosure } from '@chakra-ui/react';
|
|
3
|
+
import Button from '../../Button/Button';
|
|
4
|
+
import MenuItem from '../../Menu/MenuItem';
|
|
5
|
+
import { useFilterContext } from '../Filter.context';
|
|
6
|
+
import FilterForm from '../FilterForm/FilterForm';
|
|
7
|
+
import { FilterValue } from '../Filter.types';
|
|
8
|
+
import { hasAllDependencies } from '../Filter.utils';
|
|
9
|
+
|
|
10
|
+
export interface FilterAddProps {
|
|
11
|
+
onChange: (category: string, selected: FilterValue) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FilterAdd = (props: FilterAddProps) => {
|
|
15
|
+
const { onChange } = props;
|
|
16
|
+
const [selectedCategory, setSelectedCategory] = useState<string | undefined>();
|
|
17
|
+
|
|
18
|
+
const { isOpen, onClose: closeMenu, onOpen: openMenu } = useDisclosure();
|
|
19
|
+
|
|
20
|
+
const { data, filtersDependOn, isLoading, setPopoverOpen, state } = useFilterContext();
|
|
21
|
+
|
|
22
|
+
const onCategorySelect = (category: string) => {
|
|
23
|
+
setSelectedCategory(category);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const onOpen = () => {
|
|
27
|
+
openMenu();
|
|
28
|
+
setPopoverOpen(true);
|
|
29
|
+
setSelectedCategory(undefined);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const onClose = () => {
|
|
33
|
+
setPopoverOpen(false);
|
|
34
|
+
closeMenu();
|
|
35
|
+
setSelectedCategory(undefined);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const onFilterChange = (category: string, value: FilterValue) => {
|
|
39
|
+
onClose();
|
|
40
|
+
onChange(category, value);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const stateKeys = Object.keys(state);
|
|
44
|
+
|
|
45
|
+
const isDisabled = !hasAllDependencies(stateKeys, filtersDependOn);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Menu closeOnSelect={false} isOpen={isOpen} onClose={onClose} onOpen={onOpen}>
|
|
49
|
+
<MenuButton
|
|
50
|
+
as={Button}
|
|
51
|
+
isDisabled={isDisabled}
|
|
52
|
+
isLoading={isLoading}
|
|
53
|
+
leftIconName="PlusOpen"
|
|
54
|
+
size="small"
|
|
55
|
+
variant="tertiary"
|
|
56
|
+
position={isOpen ? 'relative' : undefined}
|
|
57
|
+
zIndex={isOpen ? 'dialog' : undefined}
|
|
58
|
+
>
|
|
59
|
+
Add filter {isOpen}
|
|
60
|
+
</MenuButton>
|
|
61
|
+
<MenuList
|
|
62
|
+
paddingY={selectedCategory ? 0 : '12'}
|
|
63
|
+
position={isOpen ? 'relative' : undefined}
|
|
64
|
+
zIndex={isOpen ? 'dialog' : undefined}
|
|
65
|
+
>
|
|
66
|
+
{selectedCategory ? (
|
|
67
|
+
<FilterForm category={selectedCategory} onChange={onFilterChange} onCancel={onClose} />
|
|
68
|
+
) : (
|
|
69
|
+
Object.keys(data).map((category) => {
|
|
70
|
+
const { categoryName, dependsOn, isPermanent } = data[category];
|
|
71
|
+
if (isPermanent) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return (
|
|
75
|
+
<MenuItem
|
|
76
|
+
isDisabled={!hasAllDependencies(stateKeys, dependsOn)}
|
|
77
|
+
key={category}
|
|
78
|
+
onClick={() => onCategorySelect(category)}
|
|
79
|
+
pointerEvents="all"
|
|
80
|
+
rightIconName="ChevronRight"
|
|
81
|
+
>
|
|
82
|
+
{categoryName || category}
|
|
83
|
+
</MenuItem>
|
|
84
|
+
);
|
|
85
|
+
})
|
|
86
|
+
)}
|
|
87
|
+
</MenuList>
|
|
88
|
+
</Menu>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default FilterAdd;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// import { useEffect } from 'react';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useDisclosure, useMultiStyleConfig } from '@chakra-ui/react';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
5
|
+
import Box from '../../Box/Box';
|
|
6
|
+
import DatePicker, { DateRange, useDateRange } from '../../DatePicker/DatePicker';
|
|
7
|
+
import Icon from '../../Icon/Icon';
|
|
8
|
+
import Text from '../../Text/Text';
|
|
9
|
+
import Tooltip from '../../Tooltip/Tooltip';
|
|
10
|
+
import { useFilterContext } from '../Filter.context';
|
|
11
|
+
import { FilterStyle } from '../Filter.theme';
|
|
12
|
+
import { FilterValue } from '../Filter.types';
|
|
13
|
+
|
|
14
|
+
export type FilterDateProps = {
|
|
15
|
+
onChange: (category: string, value: FilterValue) => void;
|
|
16
|
+
onClear: (category: string) => void;
|
|
17
|
+
value: FilterValue;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const FilterDate = (props: FilterDateProps) => {
|
|
21
|
+
const { onChange, onClear, value } = props;
|
|
22
|
+
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
23
|
+
|
|
24
|
+
const { isLoading, setPopoverOpen } = useFilterContext();
|
|
25
|
+
|
|
26
|
+
const { isOpen, onClose, onToggle } = useDisclosure();
|
|
27
|
+
|
|
28
|
+
const onDateRangeApply = (range: DateRange) => {
|
|
29
|
+
if (range.from && range.to) {
|
|
30
|
+
onChange('date_range', [String(range.from.toMillis()), String(range.to.toMillis())]);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const onClearClick = () => {
|
|
35
|
+
onClear('date_range');
|
|
36
|
+
onClose();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setPopoverOpen(isOpen);
|
|
41
|
+
}, [isOpen]);
|
|
42
|
+
|
|
43
|
+
const now = DateTime.local();
|
|
44
|
+
const twoYearsAgo = now.minus({ years: 2 });
|
|
45
|
+
|
|
46
|
+
const selectable = useDateRange(twoYearsAgo, now);
|
|
47
|
+
|
|
48
|
+
const selectedRange: DateRange | undefined =
|
|
49
|
+
value && value[0]
|
|
50
|
+
? new DateRange(DateTime.fromMillis(Number(value[0])), DateTime.fromMillis(Number(value[1])))
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const label = selectedRange
|
|
54
|
+
? `${selectedRange.from?.toFormat('LLL dd')} - ${selectedRange.to?.toFormat('LLL dd')}`
|
|
55
|
+
: 'All dates';
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<DatePicker
|
|
59
|
+
selectable={selectable}
|
|
60
|
+
selected={selectedRange}
|
|
61
|
+
onApply={onDateRangeApply}
|
|
62
|
+
onClose={onClose}
|
|
63
|
+
onClear={value?.length ? onClearClick : undefined}
|
|
64
|
+
visible={isOpen}
|
|
65
|
+
>
|
|
66
|
+
<Box sx={filterStyle.item} position={isOpen ? 'relative' : undefined} zIndex={isOpen ? 'dialog' : undefined}>
|
|
67
|
+
<Tooltip isDisabled={isLoading} label="Edit">
|
|
68
|
+
<Text as="button" disabled={isLoading} onClick={onToggle} size="2" sx={filterStyle.tagEdit}>
|
|
69
|
+
<Icon color="neutral.60" name="Calendar" size="16" /> {label}
|
|
70
|
+
<Icon name="ChevronDown" size="16" />
|
|
71
|
+
</Text>
|
|
72
|
+
</Tooltip>
|
|
73
|
+
</Box>
|
|
74
|
+
</DatePicker>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default FilterDate;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useMultiStyleConfig } from '@chakra-ui/react';
|
|
3
|
+
import Badge from '../../Badge/Badge';
|
|
4
|
+
import Box from '../../Box/Box';
|
|
5
|
+
import Button from '../../Button/Button';
|
|
6
|
+
import ButtonGroup from '../../ButtonGroup/ButtonGroup';
|
|
7
|
+
import Checkbox from '../../Form/Checkbox/Checkbox';
|
|
8
|
+
import CheckboxGroup from '../../Form/Checkbox/CheckboxGroup';
|
|
9
|
+
import Radio from '../../Form/Radio/Radio';
|
|
10
|
+
import RadioGroup from '../../Form/Radio/RadioGroup';
|
|
11
|
+
import SearchInput from '../../SearchInput/SearchInput';
|
|
12
|
+
import Text from '../../Text/Text';
|
|
13
|
+
import { FilterStyle } from '../Filter.theme';
|
|
14
|
+
import { FilterOptions, FilterValue } from '../Filter.types';
|
|
15
|
+
import { isEqual, useDebounce } from '../../../utils/utils';
|
|
16
|
+
import { getOptionLabel } from '../Filter.utils';
|
|
17
|
+
import { useFilterContext } from '../Filter.context';
|
|
18
|
+
|
|
19
|
+
export type FilterFormProps = {
|
|
20
|
+
category: string;
|
|
21
|
+
categoryName?: string;
|
|
22
|
+
onChange: (category: string, selected: FilterValue, previousValue: FilterValue) => void;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const FilterForm = (props: FilterFormProps) => {
|
|
27
|
+
const { category, categoryName, onChange, onCancel } = props;
|
|
28
|
+
|
|
29
|
+
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
30
|
+
|
|
31
|
+
const { data, state } = useFilterContext();
|
|
32
|
+
const { isMultiple, onAsyncSearch, options, optionsMap } = data[category];
|
|
33
|
+
const value = state[category] || [];
|
|
34
|
+
|
|
35
|
+
const [selected, setSelected] = useState<FilterValue>(value);
|
|
36
|
+
|
|
37
|
+
const [searchValue, setSearchValue] = useState<string>('');
|
|
38
|
+
const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
|
|
39
|
+
|
|
40
|
+
const [isLoading, setLoading] = useState(false);
|
|
41
|
+
const [foundOptions, setFoundOptions] = useState<FilterOptions>([]);
|
|
42
|
+
|
|
43
|
+
const isAsync = !!onAsyncSearch;
|
|
44
|
+
const withSearch = (options && options.length > 5) || isAsync;
|
|
45
|
+
|
|
46
|
+
const isDisabled = isEqual(selected, value);
|
|
47
|
+
|
|
48
|
+
const filteredOptions = useMemo(() => {
|
|
49
|
+
if (options?.length) {
|
|
50
|
+
return options.filter((o) => o.toLowerCase().includes(searchValue.toLowerCase()));
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}, [searchValue]);
|
|
54
|
+
|
|
55
|
+
const onSearchChange = (q: string) => {
|
|
56
|
+
setSearchValue(q);
|
|
57
|
+
if (isAsync) {
|
|
58
|
+
if (q.length > 0) {
|
|
59
|
+
setLoading(true);
|
|
60
|
+
} else {
|
|
61
|
+
setFoundOptions([]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onSubmit = (e: FormEvent<HTMLDivElement>) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const newSelected = selected[0] === '' ? [] : selected;
|
|
69
|
+
onChange(category, newSelected, value);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const onClearClick = () => {
|
|
73
|
+
setSelected([]);
|
|
74
|
+
onChange(category, [], value);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const onCancelClick = () => {
|
|
78
|
+
setSelected(value);
|
|
79
|
+
onCancel();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getEmptyText = () => {
|
|
83
|
+
if (searchValue.length) {
|
|
84
|
+
return 'No result. Refine your search term.';
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getAsyncList = async () => {
|
|
90
|
+
if (onAsyncSearch) {
|
|
91
|
+
const response = await onAsyncSearch(category, searchValue);
|
|
92
|
+
setLoading(false);
|
|
93
|
+
setFoundOptions(response);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (debouncedSearchValue.length > 0) {
|
|
99
|
+
getAsyncList();
|
|
100
|
+
}
|
|
101
|
+
}, [debouncedSearchValue]);
|
|
102
|
+
|
|
103
|
+
const isEditMode = value.length !== 0;
|
|
104
|
+
|
|
105
|
+
const items =
|
|
106
|
+
isAsync && !!searchValue ? [...value, ...foundOptions] : Array.from(new Set([...value, ...filteredOptions]));
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Box as="form" onSubmit={onSubmit} sx={filterStyle.form}>
|
|
110
|
+
<Box sx={filterStyle.formHeader}>
|
|
111
|
+
<Text as="h5" sx={filterStyle.formTitle}>
|
|
112
|
+
{categoryName || category}
|
|
113
|
+
</Text>
|
|
114
|
+
{isMultiple && <Badge sx={filterStyle.formBadge}>{selected[0] === '' ? '0' : selected.length}</Badge>}
|
|
115
|
+
</Box>
|
|
116
|
+
{(withSearch || isAsync) && (
|
|
117
|
+
<SearchInput
|
|
118
|
+
autoFocus
|
|
119
|
+
// isLoading={isLoading}
|
|
120
|
+
placeholder={isAsync ? 'Start typing to search options' : 'Start typing to find options'}
|
|
121
|
+
onChange={onSearchChange}
|
|
122
|
+
sx={filterStyle.formSearch}
|
|
123
|
+
value={searchValue}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
{isLoading && 'Loading...'}
|
|
127
|
+
{!isLoading && isMultiple && (
|
|
128
|
+
<CheckboxGroup onChange={setSelected} value={selected} sx={filterStyle.formInputGroup}>
|
|
129
|
+
{items.length
|
|
130
|
+
? items.map((opt) => (
|
|
131
|
+
<Checkbox key={opt} value={opt}>
|
|
132
|
+
{getOptionLabel(opt, optionsMap)}
|
|
133
|
+
</Checkbox>
|
|
134
|
+
))
|
|
135
|
+
: getEmptyText()}
|
|
136
|
+
</CheckboxGroup>
|
|
137
|
+
)}
|
|
138
|
+
{!isLoading && !isMultiple && (
|
|
139
|
+
<RadioGroup onChange={(v) => setSelected([v])} value={selected[0] || ''} sx={filterStyle.formInputGroup}>
|
|
140
|
+
<Radio value="">
|
|
141
|
+
<Text as="span" color="neutral.40" fontStyle="italic">
|
|
142
|
+
Not filtered
|
|
143
|
+
</Text>
|
|
144
|
+
</Radio>
|
|
145
|
+
{items.length
|
|
146
|
+
? items.map((opt) => (
|
|
147
|
+
<Radio key={opt} value={opt}>
|
|
148
|
+
{getOptionLabel(opt, optionsMap)}
|
|
149
|
+
</Radio>
|
|
150
|
+
))
|
|
151
|
+
: getEmptyText()}
|
|
152
|
+
</RadioGroup>
|
|
153
|
+
)}
|
|
154
|
+
<ButtonGroup spacing="12" sx={filterStyle.formButtonGroup}>
|
|
155
|
+
{isEditMode && (
|
|
156
|
+
<Button marginInlineEnd="auto" onClick={onClearClick} size="small" variant="tertiary">
|
|
157
|
+
Clear
|
|
158
|
+
</Button>
|
|
159
|
+
)}
|
|
160
|
+
<Button onClick={onCancelClick} size="small" type="reset" variant="secondary">
|
|
161
|
+
Cancel
|
|
162
|
+
</Button>
|
|
163
|
+
<Button isDisabled={isDisabled} size="small" type="submit">
|
|
164
|
+
{(selected.length === 0 || selected[0] === '') && isEditMode ? 'Remove' : 'Apply'}
|
|
165
|
+
</Button>
|
|
166
|
+
</ButtonGroup>
|
|
167
|
+
</Box>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default FilterForm;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDisclosure, useMultiStyleConfig } from '@chakra-ui/react';
|
|
3
|
+
import Box from '../../Box/Box';
|
|
4
|
+
import Icon from '../../Icon/Icon';
|
|
5
|
+
import IconButton from '../../IconButton/IconButton';
|
|
6
|
+
import Popover from '../../Popover/Popover';
|
|
7
|
+
import PopoverContent from '../../Popover/PopoverContent';
|
|
8
|
+
import PopoverTrigger from '../../Popover/PopoverTrigger';
|
|
9
|
+
import Text from '../../Text/Text';
|
|
10
|
+
import Tooltip from '../../Tooltip/Tooltip';
|
|
11
|
+
import { FilterStyle } from '../Filter.theme';
|
|
12
|
+
import { FilterOptionsMap, FilterValue } from '../Filter.types';
|
|
13
|
+
import FilterForm from '../FilterForm/FilterForm';
|
|
14
|
+
import { useFilterContext } from '../Filter.context';
|
|
15
|
+
import { getOptionLabel } from '../Filter.utils';
|
|
16
|
+
|
|
17
|
+
export type FilterItemProps = {
|
|
18
|
+
category: string;
|
|
19
|
+
categoryName?: string;
|
|
20
|
+
categoryNamePlural?: string;
|
|
21
|
+
isPermanent?: boolean;
|
|
22
|
+
onChange: (category: string, value: FilterValue) => void;
|
|
23
|
+
onClear: (category: string) => void;
|
|
24
|
+
optionsMap?: FilterOptionsMap;
|
|
25
|
+
value: FilterValue;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FilterItem = (props: FilterItemProps) => {
|
|
29
|
+
const { category, categoryName, categoryNamePlural, isPermanent, onChange, onClear, optionsMap, value } = props;
|
|
30
|
+
|
|
31
|
+
const pluralCategoryString = (categoryNamePlural || `${category}s`).toLowerCase();
|
|
32
|
+
|
|
33
|
+
const { isOpen, onToggle: togglePopover, onClose: closePopover } = useDisclosure();
|
|
34
|
+
|
|
35
|
+
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
36
|
+
|
|
37
|
+
const { isLoading, setPopoverOpen } = useFilterContext();
|
|
38
|
+
|
|
39
|
+
const onToggle = () => {
|
|
40
|
+
togglePopover();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onClose = () => {
|
|
44
|
+
closePopover();
|
|
45
|
+
setPopoverOpen(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const onFilterChange = (newCategory: string, newValue: FilterValue) => {
|
|
49
|
+
onClose();
|
|
50
|
+
onChange(newCategory, newValue);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getText = () => {
|
|
54
|
+
if (!value || value.length === 0) {
|
|
55
|
+
return `All ${pluralCategoryString}`;
|
|
56
|
+
}
|
|
57
|
+
if (value.length > 1) {
|
|
58
|
+
return `${value.length} ${pluralCategoryString}`;
|
|
59
|
+
}
|
|
60
|
+
return `${categoryName || category}: ${getOptionLabel(value[0], optionsMap)}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (isOpen) {
|
|
65
|
+
setPopoverOpen(true);
|
|
66
|
+
}
|
|
67
|
+
}, [isOpen]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Popover isLazy isOpen={isOpen} onClose={onClose}>
|
|
71
|
+
<PopoverTrigger>
|
|
72
|
+
<Box sx={filterStyle.item} position={isOpen ? 'relative' : undefined} zIndex={isOpen ? 'dialog' : undefined}>
|
|
73
|
+
<Tooltip isDisabled={isLoading} label="Edit">
|
|
74
|
+
<Text as="button" disabled={isLoading} onClick={onToggle} size="2" sx={filterStyle.tagEdit}>
|
|
75
|
+
{getText()}
|
|
76
|
+
{isPermanent && <Icon name="ChevronDown" size="16" />}
|
|
77
|
+
</Text>
|
|
78
|
+
</Tooltip>
|
|
79
|
+
{!isPermanent && (
|
|
80
|
+
<IconButton
|
|
81
|
+
aria-label={isLoading ? '' : 'Clear'}
|
|
82
|
+
iconName="CloseSmall"
|
|
83
|
+
isDisabled={isLoading}
|
|
84
|
+
onClick={() => onClear(category)}
|
|
85
|
+
size="small"
|
|
86
|
+
variant="tertiary"
|
|
87
|
+
sx={filterStyle.tagClear}
|
|
88
|
+
tooltipProps={{ shouldWrapChildren: false }}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
</Box>
|
|
92
|
+
</PopoverTrigger>
|
|
93
|
+
<PopoverContent>
|
|
94
|
+
<FilterForm category={category} categoryName={categoryName} onChange={onFilterChange} onCancel={onClose} />
|
|
95
|
+
</PopoverContent>
|
|
96
|
+
</Popover>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default FilterItem;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ChangeEvent, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Input,
|
|
4
|
+
InputGroup,
|
|
5
|
+
InputLeftElement,
|
|
6
|
+
InputProps,
|
|
7
|
+
InputRightElement,
|
|
8
|
+
useMultiStyleConfig,
|
|
9
|
+
} from '@chakra-ui/react';
|
|
10
|
+
import { useDebounce } from '../../../utils/utils';
|
|
11
|
+
import Icon from '../../Icon/Icon';
|
|
12
|
+
import IconButton from '../../IconButton/IconButton';
|
|
13
|
+
import { FilterStyle } from '../Filter.theme';
|
|
14
|
+
import { FilterValue } from '../Filter.types';
|
|
15
|
+
|
|
16
|
+
export interface FilterSearchProps extends Omit<InputProps, 'onChange' | 'value'> {
|
|
17
|
+
onChange: (category: string, selected: FilterValue) => void;
|
|
18
|
+
onClear: (category: string) => void;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FilterSearch = (props: FilterSearchProps) => {
|
|
23
|
+
const { onChange, onClear, value, ...rest } = props;
|
|
24
|
+
const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
|
|
25
|
+
|
|
26
|
+
const [searchValue, setSearchValue] = useState(value);
|
|
27
|
+
const debouncedSearchValue = useDebounce<string>(searchValue, 500);
|
|
28
|
+
|
|
29
|
+
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
30
|
+
setSearchValue(event.currentTarget.value);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const onClearClick = () => {
|
|
34
|
+
onClear('search');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
onChange('search', debouncedSearchValue.length ? [debouncedSearchValue] : []);
|
|
39
|
+
}, [debouncedSearchValue]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setSearchValue(value);
|
|
43
|
+
}, [value]);
|
|
44
|
+
|
|
45
|
+
const inputProps: InputProps = {
|
|
46
|
+
placeholder: 'Search...',
|
|
47
|
+
sx: filterStyle.searchInput,
|
|
48
|
+
onChange: onInputChange,
|
|
49
|
+
value: searchValue,
|
|
50
|
+
...rest,
|
|
51
|
+
};
|
|
52
|
+
return (
|
|
53
|
+
<InputGroup maxWidth="9.25rem" height="fit-content">
|
|
54
|
+
<InputLeftElement margin="8">
|
|
55
|
+
<Icon color="neutral.60" name="Magnifier" size="16" />
|
|
56
|
+
</InputLeftElement>
|
|
57
|
+
<Input {...inputProps} />
|
|
58
|
+
<InputRightElement>
|
|
59
|
+
<IconButton aria-label="Clear" iconName="CloseSmall" onClick={onClearClick} size="small" variant="tertiary" />
|
|
60
|
+
</InputRightElement>
|
|
61
|
+
</InputGroup>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default FilterSearch;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rem } from '../../../utils/utils';
|
|
2
|
+
|
|
1
3
|
const FilterSwitch = {
|
|
2
4
|
baseStyle: () => {
|
|
3
5
|
return {
|
|
@@ -7,13 +9,20 @@ const FilterSwitch = {
|
|
|
7
9
|
borderRadius: '4',
|
|
8
10
|
border: '1px solid',
|
|
9
11
|
borderColor: 'neutral.80',
|
|
10
|
-
paddingBlock: '6',
|
|
11
|
-
paddingInline: '12',
|
|
12
12
|
cursor: 'pointer',
|
|
13
13
|
position: 'relative',
|
|
14
14
|
textOverflow: 'ellipsis',
|
|
15
15
|
whiteSpace: 'nowrap',
|
|
16
16
|
overflow: 'hidden',
|
|
17
|
+
paddingBlock: rem(5),
|
|
18
|
+
paddingInline: rem(11),
|
|
19
|
+
zIndex: 0,
|
|
20
|
+
fontSize: rem(14),
|
|
21
|
+
lineHeight: rem(20),
|
|
22
|
+
_focusVisible: {
|
|
23
|
+
boxShadow: 'outline',
|
|
24
|
+
zIndex: 1,
|
|
25
|
+
},
|
|
17
26
|
_hover: {
|
|
18
27
|
background: 'neutral.100',
|
|
19
28
|
},
|
|
@@ -27,13 +36,6 @@ const FilterSwitch = {
|
|
|
27
36
|
cursor: 'default',
|
|
28
37
|
},
|
|
29
38
|
},
|
|
30
|
-
label: {
|
|
31
|
-
margin: 0,
|
|
32
|
-
userSelect: 'none',
|
|
33
|
-
_focusVisible: {
|
|
34
|
-
boxShadow: 'outline',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
39
|
};
|
|
38
40
|
},
|
|
39
41
|
};
|
|
@@ -36,7 +36,7 @@ const FilterSwitch = forwardRef<FilterSwitchProps, 'input'>((props, ref) => {
|
|
|
36
36
|
|
|
37
37
|
const { name } = group;
|
|
38
38
|
|
|
39
|
-
const { getInputProps, getLabelProps,
|
|
39
|
+
const { getInputProps, getLabelProps, getCheckboxProps } = useRadio({
|
|
40
40
|
...rest,
|
|
41
41
|
isChecked,
|
|
42
42
|
isFocusable,
|
|
@@ -46,11 +46,9 @@ const FilterSwitch = forwardRef<FilterSwitchProps, 'input'>((props, ref) => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
|
-
<chakra.label {...
|
|
49
|
+
<chakra.label {...getLabelProps()} {...getCheckboxProps()} __css={styles.container}>
|
|
50
50
|
<chakra.input {...getInputProps(htmlInputProps, ref)} />
|
|
51
|
-
|
|
52
|
-
{children}
|
|
53
|
-
</chakra.span>
|
|
51
|
+
{children}
|
|
54
52
|
</chakra.label>
|
|
55
53
|
);
|
|
56
54
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { CheckboxGroup as ChakraCheckboxGroup, CheckboxGroupProps as ChakraCheckboxGroupProps } from '@chakra-ui/react';
|
|
2
2
|
import Box, { BoxProps } from '../../Box/Box';
|
|
3
3
|
|
|
4
|
-
export type CheckboxGroupProps =
|
|
4
|
+
export type CheckboxGroupProps = Omit<ChakraCheckboxGroupProps, 'onChange'> &
|
|
5
|
+
Omit<BoxProps, 'onChange'> & {
|
|
6
|
+
onChange?(value: Array<string>): void;
|
|
7
|
+
};
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* CheckboxGroup component to help manage the checked state of its children Checkbox components and conveniently pass a few shared style props to each.
|
|
@@ -14,8 +14,8 @@ import Th from '../Table/Th';
|
|
|
14
14
|
import Thead from '../Table/Thead';
|
|
15
15
|
import Provider from '../Provider/Provider';
|
|
16
16
|
import Input from '../Form/Input/Input';
|
|
17
|
-
import FilterSwitchGroup from '../
|
|
18
|
-
import FilterSwitch from '../
|
|
17
|
+
import FilterSwitchGroup from '../Filter/FilterSwitch/FilterSwitchGroup';
|
|
18
|
+
import FilterSwitch from '../Filter/FilterSwitch/FilterSwitch';
|
|
19
19
|
import * as bigIcons from './24x24';
|
|
20
20
|
import { FigmaIcon, figmaIcons } from './figmaIcons';
|
|
21
21
|
|
|
@@ -1,46 +1,7 @@
|
|
|
1
1
|
import { Text as ChakraText, TextProps as ChakraTextProps, forwardRef, ResponsiveValue } from '@chakra-ui/react';
|
|
2
2
|
import { TextSizes } from '../../Foundations/Typography/Typography';
|
|
3
3
|
|
|
4
|
-
type TextTags =
|
|
5
|
-
| 'a'
|
|
6
|
-
| 'abbr'
|
|
7
|
-
| 'bdi'
|
|
8
|
-
| 'bdo'
|
|
9
|
-
| 'blockquote'
|
|
10
|
-
| 'cite'
|
|
11
|
-
| 'data'
|
|
12
|
-
| 'dd'
|
|
13
|
-
| 'dfn'
|
|
14
|
-
| 'dt'
|
|
15
|
-
| 'em'
|
|
16
|
-
| 'figcaption'
|
|
17
|
-
| 'h1'
|
|
18
|
-
| 'h2'
|
|
19
|
-
| 'h3'
|
|
20
|
-
| 'h4'
|
|
21
|
-
| 'h5'
|
|
22
|
-
| 'h6'
|
|
23
|
-
| 'kbd'
|
|
24
|
-
| 'li'
|
|
25
|
-
| 'label'
|
|
26
|
-
| 'mark'
|
|
27
|
-
| 'p'
|
|
28
|
-
| 'pre'
|
|
29
|
-
| 'q'
|
|
30
|
-
| 'samp'
|
|
31
|
-
| 'small'
|
|
32
|
-
| 'span'
|
|
33
|
-
| 'strong'
|
|
34
|
-
| 'sub'
|
|
35
|
-
| 'sup'
|
|
36
|
-
| 'time'
|
|
37
|
-
| 'var';
|
|
38
|
-
|
|
39
4
|
export interface TextProps extends ChakraTextProps {
|
|
40
|
-
/**
|
|
41
|
-
* Any valid HTML text tag
|
|
42
|
-
*/
|
|
43
|
-
as?: TextTags;
|
|
44
5
|
/**
|
|
45
6
|
* Font weight
|
|
46
7
|
*/
|
package/src/index.ts
CHANGED
|
@@ -313,10 +313,10 @@ export { default as FileInput } from './Components/Form/FileInput/FileInput';
|
|
|
313
313
|
export type { ToggletipProps as ToggleTooltipProps } from './Components/Toggletip/Toggletip';
|
|
314
314
|
export { default as Toggletip } from './Components/Toggletip/Toggletip';
|
|
315
315
|
|
|
316
|
-
export type { FilterSwitchProps } from './Components/
|
|
317
|
-
export { default as FilterSwitch } from './Components/
|
|
316
|
+
export type { FilterSwitchProps } from './Components/Filter/FilterSwitch/FilterSwitch';
|
|
317
|
+
export { default as FilterSwitch } from './Components/Filter/FilterSwitch/FilterSwitch';
|
|
318
318
|
|
|
319
|
-
export { default as FilterSwitchGroup } from './Components/
|
|
319
|
+
export { default as FilterSwitchGroup } from './Components/Filter/FilterSwitch/FilterSwitchGroup';
|
|
320
320
|
|
|
321
321
|
export type { TablePaginationProps } from './Components/Table/TablePagination';
|
|
322
322
|
export { default as TablePagination } from './Components/Table/TablePagination';
|
package/src/theme.ts
CHANGED
|
@@ -43,6 +43,7 @@ import CodeSnippet from './Components/CodeSnippet/CodeSnippet.theme';
|
|
|
43
43
|
import DefinitionTooltip from './Components/DefinitionTooltip/DefinitionTooltip.theme';
|
|
44
44
|
import ExpandableCard from './Components/ExpandableCard/ExpandableCard.theme';
|
|
45
45
|
import FileInput from './Components/Form/FileInput/FileInput.theme';
|
|
46
|
+
import Filter from './Components/Filter/Filter.theme';
|
|
46
47
|
|
|
47
48
|
import breakpoints from './Foundations/Breakpoints/Breakpoints';
|
|
48
49
|
import colors from './Foundations/Colors/Colors';
|
|
@@ -52,7 +53,7 @@ import sizes from './Foundations/Sizes/Sizes';
|
|
|
52
53
|
import typography from './Foundations/Typography/Typography';
|
|
53
54
|
import zIndices from './Foundations/Zindex/Zindex';
|
|
54
55
|
import Toggletip from './Components/Toggletip/Toggletip.theme';
|
|
55
|
-
import FilterSwitch from './Components/
|
|
56
|
+
import FilterSwitch from './Components/Filter/FilterSwitch/FilterSwitch.theme';
|
|
56
57
|
|
|
57
58
|
const theme = {
|
|
58
59
|
config: {
|
|
@@ -137,6 +138,7 @@ const theme = {
|
|
|
137
138
|
Toggletip,
|
|
138
139
|
ExpandableCard,
|
|
139
140
|
FileInput,
|
|
141
|
+
Filter,
|
|
140
142
|
},
|
|
141
143
|
};
|
|
142
144
|
|
package/src/utils/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
export const rem = (input: number | string): number | string => {
|
|
4
4
|
const value = typeof input === 'string' ? parseInt(input, 10) : input;
|
|
@@ -14,3 +14,25 @@ export function useObjectMemo<T extends object>(obj: T): T {
|
|
|
14
14
|
return obj;
|
|
15
15
|
}, Object.values(obj));
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export function isEqual(a: number | string | number[] | string[], b: number | string | number[] | string[]) {
|
|
19
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
20
|
+
return a.length === b.length && [...a].every((val) => [...b].includes(val));
|
|
21
|
+
}
|
|
22
|
+
return a === b;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// https://usehooks-ts.com/react-hook/use-debounce
|
|
26
|
+
export function useDebounce<T>(value: T, delay?: number): T {
|
|
27
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
};
|
|
35
|
+
}, [value, delay]);
|
|
36
|
+
|
|
37
|
+
return debouncedValue;
|
|
38
|
+
}
|
|
File without changes
|