@bitrise/bitkit 12.70.0-alpha-filter-1.1 → 12.70.0-alpha-filter-2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "12.70.0-alpha-filter-1.1",
4
+ "version": "12.70.0-alpha-filter-2.2",
5
5
  "repository": "git@github.com:bitrise-io/bitkit.git",
6
6
  "main": "src/index.ts",
7
7
  "license": "UNLICENSED",
@@ -28,7 +28,7 @@
28
28
  "@emotion/react": "^11.11.1",
29
29
  "@emotion/styled": "^11.11.0",
30
30
  "@floating-ui/react-dom-interactions": "^0.8.1",
31
- "framer-motion": "^10.16.5",
31
+ "framer-motion": "^10.16.16",
32
32
  "luxon": "^3.4.4",
33
33
  "react": "^18.2.0",
34
34
  "react-dom": "^18.2.0",
@@ -40,38 +40,38 @@
40
40
  "react-dom": "^18.2.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@babel/core": "^7.23.3",
44
- "@babel/preset-env": "^7.23.3",
43
+ "@babel/core": "^7.23.6",
44
+ "@babel/preset-env": "^7.23.6",
45
45
  "@babel/preset-react": "^7.23.3",
46
46
  "@babel/preset-typescript": "^7.23.3",
47
- "@bitrise/eslint-plugin": "^2.3.2",
47
+ "@bitrise/eslint-plugin": "^2.3.3",
48
48
  "@commitlint/cli": "^17.8.0",
49
49
  "@commitlint/config-conventional": "^17.8.0",
50
- "@google-cloud/storage": "^7.6.0",
50
+ "@google-cloud/storage": "^7.7.0",
51
51
  "@semantic-release/commit-analyzer": "^11.1.0",
52
52
  "@semantic-release/git": "^10.0.1",
53
- "@storybook/addon-actions": "^7.5.3",
54
- "@storybook/addon-essentials": "^7.5.3",
55
- "@storybook/addon-interactions": "^7.5.3",
56
- "@storybook/addon-links": "^7.5.3",
57
- "@storybook/addons": "^7.5.3",
58
- "@storybook/blocks": "^7.5.3",
59
- "@storybook/react": "^7.5.3",
60
- "@storybook/react-webpack5": "^7.5.3",
53
+ "@storybook/addon-actions": "^7.6.4",
54
+ "@storybook/addon-essentials": "^7.6.4",
55
+ "@storybook/addon-interactions": "^7.6.4",
56
+ "@storybook/addon-links": "^7.6.4",
57
+ "@storybook/addons": "^7.6.4",
58
+ "@storybook/blocks": "^7.6.4",
59
+ "@storybook/react": "^7.6.4",
60
+ "@storybook/react-webpack5": "^7.6.4",
61
61
  "@storybook/testing-library": "^0.2.2",
62
- "@storybook/theming": "^7.5.3",
62
+ "@storybook/theming": "^7.6.4",
63
63
  "@testing-library/dom": "^9.3.3",
64
- "@testing-library/jest-dom": "^6.1.4",
65
- "@testing-library/react": "^14.1.0",
64
+ "@testing-library/jest-dom": "^6.1.5",
65
+ "@testing-library/react": "^14.1.2",
66
66
  "@testing-library/user-event": "^14.5.1",
67
- "@types/jest": "^29.5.8",
68
- "@types/luxon": "^3.3.4",
69
- "@types/react": "^18.2.37",
70
- "@types/react-dom": "^18.2.15",
71
- "@typescript-eslint/eslint-plugin": "^6.11.0",
72
- "@typescript-eslint/parser": "^6.11.0",
67
+ "@types/jest": "^29.5.11",
68
+ "@types/luxon": "^3.3.7",
69
+ "@types/react": "^18.2.43",
70
+ "@types/react-dom": "^18.2.17",
71
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
72
+ "@typescript-eslint/parser": "^6.14.0",
73
73
  "axios": "^1.6.2",
74
- "eslint": "^8.53.0",
74
+ "eslint": "^8.55.0",
75
75
  "eslint-plugin-import": "^2.29.0",
76
76
  "eslint-plugin-jest": "^27.6.0",
77
77
  "eslint-plugin-jsx-a11y": "^6.8.0",
@@ -79,15 +79,15 @@
79
79
  "eslint-plugin-react": "^7.33.2",
80
80
  "eslint-plugin-react-hooks": "^4.6.0",
81
81
  "eslint-plugin-storybook": "^0.6.15",
82
- "eslint-plugin-testing-library": "^6.1.2",
82
+ "eslint-plugin-testing-library": "^6.2.0",
83
83
  "glob": "^10.3.10",
84
84
  "jest": "^29.7.0",
85
85
  "jest-environment-jsdom": "^29.7.0",
86
- "jsdom": "^22.1.0",
87
- "prettier": "^3.1.0",
88
- "react-hook-form": "^7.48.2",
89
- "semantic-release": "^22.0.7",
90
- "storybook": "^7.5.3",
86
+ "jsdom": "^23.0.1",
87
+ "prettier": "^3.1.1",
88
+ "react-hook-form": "^7.49.0",
89
+ "semantic-release": "^22.0.12",
90
+ "storybook": "^7.6.4",
91
91
  "ts-jest": "^29.1.1",
92
92
  "typescript": "^4.8.4"
93
93
  },
@@ -12,7 +12,10 @@ const getInitials = (name: string) => {
12
12
  if (name.length < 3) {
13
13
  return name;
14
14
  }
15
- return `${name.charAt(0)}${name.charAt(name.length - 1)}`;
15
+
16
+ const nameArray = Array.from(name);
17
+
18
+ return `${nameArray[0]}${nameArray[nameArray.length - 1]}`;
16
19
  };
17
20
 
18
21
  const Avatar = forwardRef<AvatarProps, 'span'>((props, ref) => (
@@ -23,7 +23,7 @@ const DatePickerFooter = ({
23
23
  gap="24"
24
24
  >
25
25
  {!!onClear && (
26
- <Button size="small" variant="tertiary" width="fit-content" onClick={() => onClear()}>
26
+ <Button size="small" variant="tertiary" color="purple.10" width="fit-content" onClick={() => onClear()}>
27
27
  Clear
28
28
  </Button>
29
29
  )}
@@ -21,6 +21,7 @@ export const DatePickerHeaderPrevious = ({ label }: { label: string }) => {
21
21
  visibility={controls === 'right' ? 'hidden' : undefined}
22
22
  aria-label={label}
23
23
  variant="tertiary"
24
+ color="purple.10"
24
25
  as="button"
25
26
  onClick={onPrevious}
26
27
  iconName="ChevronLeft"
@@ -37,6 +38,7 @@ export const DatePickerHeaderNext = ({ label }: { label: string }) => {
37
38
  size="small"
38
39
  aria-label={label}
39
40
  variant="tertiary"
41
+ color="purple.10"
40
42
  onClick={onNext}
41
43
  iconName="ChevronRight"
42
44
  isTooltipDisabled
@@ -50,7 +50,14 @@ const DatePickerMonth = ({ controls, onViewDateChange, viewDate, onMonthClick }:
50
50
  <DatePickerHeader onPrevious={onPreviousMonth} onNext={onNextMonth} controls={controls}>
51
51
  <DatePickerHeaderPrevious label="previous month" />
52
52
  <DatePickerHeaderContent id={monthLabelId}>
53
- <Button onClick={onMonthClick} size="small" variant="tertiary" flexShrink={0} rightIconName="ChevronDown">
53
+ <Button
54
+ onClick={onMonthClick}
55
+ size="small"
56
+ variant="tertiary"
57
+ color="purple.10"
58
+ flexShrink={0}
59
+ rightIconName="ChevronDown"
60
+ >
54
61
  {viewDate.monthLong}
55
62
  </Button>
56
63
  <NumberInput
@@ -9,23 +9,26 @@ export const FILTER_STORY_DATA: FilterData = {
9
9
  date_range: {
10
10
  categoryName: 'Date',
11
11
  categoryNamePlural: 'dates',
12
- isPermanent: true,
12
+ type: 'dateRange',
13
13
  },
14
14
  pipeline: {
15
15
  categoryName: 'Pipeline',
16
16
  categoryNamePlural: 'Pipelines',
17
17
  options: FILTER_STORY_OPTIONS,
18
+ type: 'tag',
18
19
  },
19
20
  stage: {
20
21
  categoryName: 'Stage',
21
22
  categoryNamePlural: 'Stages',
22
23
  options: FILTER_STORY_OPTIONS,
23
- dependsOn: ['pipeline'],
24
+ dependsOn: { pipeline: 'Select a pipeline first' },
25
+ type: 'tag',
24
26
  },
25
27
  workflow: {
26
28
  categoryName: 'Workflow',
27
29
  isMultiple: true,
28
30
  options: FILTER_STORY_OPTIONS,
31
+ type: 'tag',
29
32
  },
30
33
  branch: {
31
34
  options: [
@@ -34,16 +37,22 @@ export const FILTER_STORY_DATA: FilterData = {
34
37
  'master',
35
38
  'CI-2264-consolidate-other-provider-type-to-custom',
36
39
  ],
40
+ type: 'tag',
37
41
  },
38
42
  app: {
39
43
  categoryName: 'App',
40
- isPermanent: true,
44
+ iconsMap: {
45
+ '46b6b9a78a418ee8': 'AppleFill',
46
+ '32b14416be4b7b24': 'Android',
47
+ '0a248b278e135ea7': 'Other',
48
+ },
41
49
  options: ['46b6b9a78a418ee8', '32b14416be4b7b24', '0a248b278e135ea7'],
42
50
  optionsMap: {
43
51
  '46b6b9a78a418ee8': 'bitrise-website',
44
52
  '32b14416be4b7b24': 'bitkit',
45
53
  '0a248b278e135ea7': 'pipeline-service',
46
54
  },
55
+ type: 'select',
47
56
  },
48
57
  test_case: {
49
58
  categoryName: 'Test case',
@@ -56,16 +65,32 @@ export const FILTER_STORY_DATA: FilterData = {
56
65
  });
57
66
  },
58
67
  options: ['default 1', 'default 2', 'default 3'],
68
+ type: 'tag',
69
+ },
70
+ cache_type: {
71
+ iconsMap: {
72
+ bazel: 'Bazel',
73
+ gradle: 'Gradle',
74
+ },
75
+ options: ['gradle', 'bazel'],
76
+ optionsMap: {
77
+ bazel: 'Bazel',
78
+ gradle: 'Gradle',
79
+ },
80
+ type: 'switch',
59
81
  },
60
82
  };
61
83
 
62
84
  export const FILTER_STORY_INIT_STATE: FilterState = {
63
85
  pipeline: ['ipsum'],
64
86
  app: ['46b6b9a78a418ee8'],
87
+ cache_type: ['gradle'],
65
88
  };
66
89
 
67
90
  export const FILTER_STORY_CONTEXT: FilterContextType = {
68
91
  data: FILTER_STORY_DATA,
69
92
  setPopoverOpen: () => {},
70
93
  state: FILTER_STORY_INIT_STATE,
94
+ onFilterChange: () => {},
95
+ onFilterClear: () => {},
71
96
  };
@@ -6,29 +6,47 @@ import Divider from '../Divider/Divider';
6
6
  import Icon from '../Icon/Icon';
7
7
  import { FilterContext } from './Filter.context';
8
8
  import { FilterStyle } from './Filter.theme';
9
- import { FilterContextType, FilterData, FilterState, FilterValue } from './Filter.types';
10
- import { getDependents, hasAllDependencies } from './Filter.utils';
9
+ import {
10
+ FilterCategoryProps,
11
+ FilterContextType,
12
+ FilterData,
13
+ FilterState,
14
+ FilterType,
15
+ FilterValue,
16
+ } from './Filter.types';
17
+ import { getDependents } from './Filter.utils';
11
18
  import FilterAdd from './FilterAdd/FilterAdd';
12
19
  import FilterItem from './FilterItem/FilterItem';
13
20
  import FilterSearch from './FilterSearch/FilterSearch';
14
21
  import FilterDate from './FilterDate/FilterDate';
22
+ import FilterSwitchAdapter from './FilterSwitchAdapter/FilterSwitchAdapter';
15
23
 
16
24
  export interface FilterProps extends Omit<BoxProps, 'onChange'> {
17
25
  filtersDependOn?: string[];
18
26
  initialData: FilterData;
27
+ initialState: FilterState;
19
28
  isLoading?: boolean;
20
29
  onChange: (state: FilterState) => void;
21
30
  showSearch?: boolean;
22
- state: FilterState;
23
31
  }
24
32
 
25
33
  const Filter = (props: FilterProps) => {
26
- const { filtersDependOn, initialData, isLoading, onChange, showSearch, state, ...rest } = props;
34
+ const { filtersDependOn, initialData, initialState, isLoading, onChange, showSearch, ...rest } = props;
27
35
 
28
36
  const isInited = useRef<boolean>(false);
29
37
 
30
38
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
31
39
 
40
+ const state: FilterState = {};
41
+ Object.entries(initialState).forEach(([category, values]) => {
42
+ if (values?.length) {
43
+ const cleanValues = values.filter((v) => v !== null && v !== '' && v !== undefined);
44
+ if (cleanValues.length) {
45
+ state[category] = cleanValues;
46
+ }
47
+ }
48
+ });
49
+
32
50
  const [data] = useState<FilterData>(initialData);
33
51
  const [isPopoverOpen, setPopoverOpen] = useState<boolean>(false);
34
52
 
@@ -58,7 +76,7 @@ const Filter = (props: FilterProps) => {
58
76
  onChange(newState);
59
77
  };
60
78
 
61
- const onClear = (category: string) => {
79
+ const onFilterClear = (category: string) => {
62
80
  onChange(deleteFromState(category, state));
63
81
  };
64
82
 
@@ -66,14 +84,19 @@ const Filter = (props: FilterProps) => {
66
84
  onChange({});
67
85
  };
68
86
 
69
- const permanentCategories = Object.keys(data).filter(
70
- (category) => data[category].isPermanent && category !== 'date_range',
71
- );
87
+ const filters = {
88
+ search: {},
89
+ select: {},
90
+ switch: {},
91
+ dateRange: {},
92
+ tag: {},
93
+ } as Record<FilterType, Record<string, FilterCategoryProps>>;
72
94
 
73
- const stateCategories = Object.keys(state).filter((c) => !['date_range', 'search'].includes(c));
74
- const filteredStateCategories = stateCategories.filter((category) => !data[category].isPermanent);
95
+ Object.entries(data).forEach(([category, value]) => {
96
+ filters[value.type || 'tag'][category] = value;
97
+ });
75
98
 
76
- const filterCategories = [...permanentCategories, ...filteredStateCategories];
99
+ const stateCategories = Object.keys(state).filter((c) => !['date_range', 'search'].includes(c));
77
100
 
78
101
  const showClearFilters = stateCategories.length > 0 || (state.search && state.search.length > 0);
79
102
 
@@ -82,10 +105,12 @@ const Filter = (props: FilterProps) => {
82
105
  data: initialData,
83
106
  filtersDependOn,
84
107
  isLoading,
108
+ onFilterChange,
109
+ onFilterClear,
85
110
  setPopoverOpen,
86
111
  state,
87
112
  }),
88
- [filtersDependOn, isLoading, initialData, setPopoverOpen, state],
113
+ [filtersDependOn, isLoading, initialData, onFilterChange, onFilterClear, setPopoverOpen, state],
89
114
  );
90
115
 
91
116
  useEffect(() => {
@@ -102,27 +127,25 @@ const Filter = (props: FilterProps) => {
102
127
  </Modal>
103
128
  <Box sx={filterStyle.content}>
104
129
  <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
- })}
130
+
131
+ {Object.keys(filters.switch).map((category) => (
132
+ <FilterSwitchAdapter category={category} key={category} />
133
+ ))}
134
+
135
+ {Object.keys(filters.dateRange).map((category) => (
136
+ <FilterDate category={category} key={category} />
137
+ ))}
138
+
139
+ {Object.keys(filters.select).map((category) => (
140
+ <FilterItem category={category} key={category} />
141
+ ))}
142
+ {Object.keys(filters.tag).map((category) => {
143
+ if (!state[category]) {
144
+ return;
145
+ }
146
+ return <FilterItem category={category} key={category} />;
147
+ })}
148
+
126
149
  <FilterAdd onChange={onFilterChange} />
127
150
  </Box>
128
151
  {(showClearFilters || showSearch) && (
@@ -142,13 +165,7 @@ const Filter = (props: FilterProps) => {
142
165
  {showClearFilters && showSearch && (
143
166
  <Divider orientation="vertical" size="1" variant="solid" flexShrink="0" />
144
167
  )}
145
- {showSearch && (
146
- <FilterSearch
147
- onChange={onFilterChange}
148
- onClear={onClear}
149
- value={(state.Search && state.Search[0]) || ''}
150
- />
151
- )}
168
+ {showSearch && <FilterSearch onChange={onFilterChange} value={(state.Search && state.Search[0]) || ''} />}
152
169
  </Box>
153
170
  )}
154
171
  </Box>
@@ -1,20 +1,25 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
+ import { TypeIconName } from '../Icon/Icon';
3
+
4
+ export type FilterType = 'dateRange' | 'search' | 'select' | 'switch' | 'tag';
2
5
 
3
6
  export type FilterOptions = string[];
4
7
  export type FilterValue = string[];
5
8
  export type FilterOptionsMap = Record<string, string>;
9
+ export type FilterIconsMap = Record<string, TypeIconName>;
6
10
 
7
11
  export type FilterSearchCallback = (category: string, q: string) => Promise<FilterValue>;
8
12
 
9
13
  export type FilterCategoryProps = {
10
14
  categoryName?: string;
11
15
  categoryNamePlural?: string;
12
- dependsOn?: string[];
16
+ dependsOn?: Record<string, string>;
17
+ iconsMap?: FilterIconsMap;
13
18
  isMultiple?: boolean;
14
- isPermanent?: boolean;
15
19
  onAsyncSearch?: FilterSearchCallback;
16
20
  options?: FilterOptions;
17
21
  optionsMap?: FilterOptionsMap;
22
+ type?: FilterType;
18
23
  };
19
24
 
20
25
  export type FilterData = Record<string, FilterCategoryProps>;
@@ -24,6 +29,8 @@ export interface FilterContextType {
24
29
  data: FilterData;
25
30
  filtersDependOn?: string[];
26
31
  isLoading?: boolean;
32
+ onFilterClear: (category: string) => void;
33
+ onFilterChange: (category: string, value: FilterValue) => void;
27
34
  setPopoverOpen: Dispatch<SetStateAction<boolean>>;
28
35
  state: FilterState;
29
36
  }
@@ -7,17 +7,24 @@ export const hasAllDependencies = (stateKeys: string[], dependsOn?: string[]): b
7
7
  return dependsOn.every((key) => stateKeys.includes(key));
8
8
  };
9
9
 
10
+ export const getMissingDependencies = (stateKeys: string[], dependsOn?: string[]): string[] => {
11
+ if (!dependsOn || dependsOn.length === 0) {
12
+ return [];
13
+ }
14
+ return dependsOn.filter((category) => !stateKeys.includes(category));
15
+ };
16
+
10
17
  export const getDependents = (data: FilterData, categoryKey: string, filtersDependOn?: string[]): string[] => {
11
18
  const dependents: string[] = [];
12
19
  if (filtersDependOn && filtersDependOn.includes(categoryKey)) {
13
20
  Object.keys(data).forEach((category) => {
14
- if (!data[category].isPermanent) {
21
+ if (data[category].type !== 'select') {
15
22
  dependents.push(category);
16
23
  }
17
24
  });
18
25
  } else {
19
26
  Object.keys(data).forEach((category) => {
20
- if (data[category].dependsOn?.includes(categoryKey)) {
27
+ if (Object.keys(data[category].dependsOn || {}).includes(categoryKey)) {
21
28
  dependents.push(category);
22
29
  }
23
30
  });
@@ -2,10 +2,11 @@ import { useState } from 'react';
2
2
  import { Menu, MenuButton, MenuList, useDisclosure } from '@chakra-ui/react';
3
3
  import Button from '../../Button/Button';
4
4
  import MenuItem from '../../Menu/MenuItem';
5
+ import Tooltip from '../../Tooltip/Tooltip';
5
6
  import { useFilterContext } from '../Filter.context';
6
7
  import FilterForm from '../FilterForm/FilterForm';
7
8
  import { FilterValue } from '../Filter.types';
8
- import { hasAllDependencies } from '../Filter.utils';
9
+ import { getMissingDependencies, hasAllDependencies } from '../Filter.utils';
9
10
 
10
11
  export interface FilterAddProps {
11
12
  onChange: (category: string, selected: FilterValue) => void;
@@ -42,7 +43,11 @@ const FilterAdd = (props: FilterAddProps) => {
42
43
 
43
44
  const stateKeys = Object.keys(state);
44
45
 
45
- const isDisabled = !hasAllDependencies(stateKeys, filtersDependOn);
46
+ const categoryList = Object.keys(data).filter((category) => {
47
+ return !stateKeys.includes(category) && data[category].type === 'tag';
48
+ });
49
+
50
+ const isDisabled = !hasAllDependencies(stateKeys, filtersDependOn) || categoryList.length === 0;
46
51
 
47
52
  return (
48
53
  <Menu closeOnSelect={false} isOpen={isOpen} onClose={onClose} onOpen={onOpen}>
@@ -66,21 +71,25 @@ const FilterAdd = (props: FilterAddProps) => {
66
71
  {selectedCategory ? (
67
72
  <FilterForm category={selectedCategory} onChange={onFilterChange} onCancel={onClose} />
68
73
  ) : (
69
- Object.keys(data).map((category) => {
70
- const { categoryName, dependsOn, isPermanent } = data[category];
71
- if (isPermanent) {
72
- return null;
73
- }
74
+ categoryList.map((category) => {
75
+ const { categoryName, dependsOn } = data[category];
76
+ const missingDependencies = getMissingDependencies(stateKeys, Object.keys(dependsOn || []));
74
77
  return (
75
- <MenuItem
76
- isDisabled={!hasAllDependencies(stateKeys, dependsOn)}
78
+ <Tooltip
79
+ isDisabled={missingDependencies.length === 0}
80
+ label={dependsOn?.[missingDependencies[0]]}
81
+ placement="right"
77
82
  key={category}
78
- onClick={() => onCategorySelect(category)}
79
- pointerEvents="all"
80
- rightIconName="ChevronRight"
81
83
  >
82
- {categoryName || category}
83
- </MenuItem>
84
+ <MenuItem
85
+ isDisabled={missingDependencies.length > 0}
86
+ onClick={() => onCategorySelect(category)}
87
+ pointerEvents="all"
88
+ rightIconName="ChevronRight"
89
+ >
90
+ {categoryName || category}
91
+ </MenuItem>
92
+ </Tooltip>
84
93
  );
85
94
  })
86
95
  )}
@@ -9,30 +9,29 @@ import Text from '../../Text/Text';
9
9
  import Tooltip from '../../Tooltip/Tooltip';
10
10
  import { useFilterContext } from '../Filter.context';
11
11
  import { FilterStyle } from '../Filter.theme';
12
- import { FilterValue } from '../Filter.types';
13
12
 
14
13
  export type FilterDateProps = {
15
- onChange: (category: string, value: FilterValue) => void;
16
- onClear: (category: string) => void;
17
- value: FilterValue;
14
+ category: string;
18
15
  };
19
16
 
20
17
  const FilterDate = (props: FilterDateProps) => {
21
- const { onChange, onClear, value } = props;
18
+ const { category } = props;
22
19
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
23
20
 
24
- const { isLoading, setPopoverOpen } = useFilterContext();
21
+ const { isLoading, onFilterChange, onFilterClear, setPopoverOpen, state } = useFilterContext();
25
22
 
26
23
  const { isOpen, onClose, onToggle } = useDisclosure();
27
24
 
25
+ const value = state[category];
26
+
28
27
  const onDateRangeApply = (range: DateRange) => {
29
28
  if (range.from && range.to) {
30
- onChange('date_range', [String(range.from.toMillis()), String(range.to.toMillis())]);
29
+ onFilterChange('date_range', [String(range.from.toMillis()), String(range.to.toMillis())]);
31
30
  }
32
31
  };
33
32
 
34
33
  const onClearClick = () => {
35
- onClear('date_range');
34
+ onFilterClear('date_range');
36
35
  onClose();
37
36
  };
38
37
 
@@ -6,6 +6,7 @@ import Button from '../../Button/Button';
6
6
  import ButtonGroup from '../../ButtonGroup/ButtonGroup';
7
7
  import Checkbox from '../../Form/Checkbox/Checkbox';
8
8
  import CheckboxGroup from '../../Form/Checkbox/CheckboxGroup';
9
+ import Icon from '../../Icon/Icon';
9
10
  import Radio from '../../Form/Radio/Radio';
10
11
  import RadioGroup from '../../Form/Radio/RadioGroup';
11
12
  import SearchInput from '../../SearchInput/SearchInput';
@@ -29,7 +30,7 @@ const FilterForm = (props: FilterFormProps) => {
29
30
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
30
31
 
31
32
  const { data, state } = useFilterContext();
32
- const { isMultiple, onAsyncSearch, options, optionsMap } = data[category];
33
+ const { iconsMap, isMultiple, onAsyncSearch, options, optionsMap } = data[category];
33
34
  const value = state[category] || [];
34
35
 
35
36
  const [selected, setSelected] = useState<FilterValue>(value);
@@ -129,6 +130,7 @@ const FilterForm = (props: FilterFormProps) => {
129
130
  {items.length
130
131
  ? items.map((opt) => (
131
132
  <Checkbox key={opt} value={opt}>
133
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
132
134
  {getOptionLabel(opt, optionsMap)}
133
135
  </Checkbox>
134
136
  ))
@@ -145,6 +147,7 @@ const FilterForm = (props: FilterFormProps) => {
145
147
  {items.length
146
148
  ? items.map((opt) => (
147
149
  <Radio key={opt} value={opt}>
150
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
148
151
  {getOptionLabel(opt, optionsMap)}
149
152
  </Radio>
150
153
  ))
@@ -9,24 +9,23 @@ import PopoverTrigger from '../../Popover/PopoverTrigger';
9
9
  import Text from '../../Text/Text';
10
10
  import Tooltip from '../../Tooltip/Tooltip';
11
11
  import { FilterStyle } from '../Filter.theme';
12
- import { FilterOptionsMap, FilterValue } from '../Filter.types';
12
+ import { FilterValue } from '../Filter.types';
13
13
  import FilterForm from '../FilterForm/FilterForm';
14
14
  import { useFilterContext } from '../Filter.context';
15
15
  import { getOptionLabel } from '../Filter.utils';
16
16
 
17
17
  export type FilterItemProps = {
18
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
19
  };
27
20
 
28
21
  const FilterItem = (props: FilterItemProps) => {
29
- const { category, categoryName, categoryNamePlural, isPermanent, onChange, onClear, optionsMap, value } = props;
22
+ const { category } = props;
23
+
24
+ const { data, isLoading, onFilterChange, onFilterClear, setPopoverOpen, state } = useFilterContext();
25
+
26
+ const { categoryName, categoryNamePlural, optionsMap, type } = data[category];
27
+
28
+ const value = state[category];
30
29
 
31
30
  const pluralCategoryString = (categoryNamePlural || `${category}s`).toLowerCase();
32
31
 
@@ -34,8 +33,6 @@ const FilterItem = (props: FilterItemProps) => {
34
33
 
35
34
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
36
35
 
37
- const { isLoading, setPopoverOpen } = useFilterContext();
38
-
39
36
  const onToggle = () => {
40
37
  togglePopover();
41
38
  };
@@ -45,9 +42,9 @@ const FilterItem = (props: FilterItemProps) => {
45
42
  setPopoverOpen(false);
46
43
  };
47
44
 
48
- const onFilterChange = (newCategory: string, newValue: FilterValue) => {
45
+ const onChange = (newCategory: string, newValue: FilterValue) => {
49
46
  onClose();
50
- onChange(newCategory, newValue);
47
+ onFilterChange(newCategory, newValue);
51
48
  };
52
49
 
53
50
  const getText = () => {
@@ -73,15 +70,15 @@ const FilterItem = (props: FilterItemProps) => {
73
70
  <Tooltip isDisabled={isLoading} label="Edit">
74
71
  <Text as="button" disabled={isLoading} onClick={onToggle} size="2" sx={filterStyle.tagEdit}>
75
72
  {getText()}
76
- {isPermanent && <Icon name="ChevronDown" size="16" />}
73
+ {type === 'select' && <Icon name="ChevronDown" size="16" />}
77
74
  </Text>
78
75
  </Tooltip>
79
- {!isPermanent && (
76
+ {type !== 'select' && (
80
77
  <IconButton
81
78
  aria-label={isLoading ? '' : 'Clear'}
82
79
  iconName="CloseSmall"
83
80
  isDisabled={isLoading}
84
- onClick={() => onClear(category)}
81
+ onClick={() => onFilterClear(category)}
85
82
  size="small"
86
83
  variant="tertiary"
87
84
  sx={filterStyle.tagClear}
@@ -91,7 +88,7 @@ const FilterItem = (props: FilterItemProps) => {
91
88
  </Box>
92
89
  </PopoverTrigger>
93
90
  <PopoverContent>
94
- <FilterForm category={category} categoryName={categoryName} onChange={onFilterChange} onCancel={onClose} />
91
+ <FilterForm category={category} categoryName={categoryName} onChange={onChange} onCancel={onClose} />
95
92
  </PopoverContent>
96
93
  </Popover>
97
94
  );
@@ -15,12 +15,11 @@ import { FilterValue } from '../Filter.types';
15
15
 
16
16
  export interface FilterSearchProps extends Omit<InputProps, 'onChange' | 'value'> {
17
17
  onChange: (category: string, selected: FilterValue) => void;
18
- onClear: (category: string) => void;
19
18
  value: string;
20
19
  }
21
20
 
22
21
  const FilterSearch = (props: FilterSearchProps) => {
23
- const { onChange, onClear, value, ...rest } = props;
22
+ const { onChange, value, ...rest } = props;
24
23
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
25
24
 
26
25
  const [searchValue, setSearchValue] = useState(value);
@@ -31,7 +30,7 @@ const FilterSearch = (props: FilterSearchProps) => {
31
30
  };
32
31
 
33
32
  const onClearClick = () => {
34
- onClear('search');
33
+ setSearchValue('');
35
34
  };
36
35
 
37
36
  useEffect(() => {
@@ -55,9 +54,11 @@ const FilterSearch = (props: FilterSearchProps) => {
55
54
  <Icon color="neutral.60" name="Magnifier" size="16" />
56
55
  </InputLeftElement>
57
56
  <Input {...inputProps} />
58
- <InputRightElement>
59
- <IconButton aria-label="Clear" iconName="CloseSmall" onClick={onClearClick} size="small" variant="tertiary" />
60
- </InputRightElement>
57
+ {!!searchValue && (
58
+ <InputRightElement>
59
+ <IconButton aria-label="Clear" iconName="CloseSmall" onClick={onClearClick} size="small" variant="tertiary" />
60
+ </InputRightElement>
61
+ )}
61
62
  </InputGroup>
62
63
  );
63
64
  };
@@ -6,9 +6,12 @@ const FilterSwitch = {
6
6
  container: {
7
7
  color: 'neutral.40',
8
8
  background: 'neutral.95',
9
- borderRadius: '4',
10
9
  border: '1px solid',
11
10
  borderColor: 'neutral.80',
11
+ display: 'flex',
12
+ borderRadius: '4',
13
+ },
14
+ item: {
12
15
  cursor: 'pointer',
13
16
  position: 'relative',
14
17
  textOverflow: 'ellipsis',
@@ -19,6 +22,18 @@ const FilterSwitch = {
19
22
  zIndex: 0,
20
23
  fontSize: rem(14),
21
24
  lineHeight: rem(20),
25
+ display: 'flex',
26
+ alignItems: 'center',
27
+ gap: '4',
28
+ borderLeft: '1px solid',
29
+ borderLeftColor: 'neutral.80',
30
+ _first: {
31
+ borderLeftRadius: '4',
32
+ borderLeft: 'none',
33
+ },
34
+ _last: {
35
+ borderRightRadius: '4',
36
+ },
22
37
  _focusVisible: {
23
38
  boxShadow: 'outline',
24
39
  zIndex: 1,
@@ -20,7 +20,7 @@ const FilterSwitch = forwardRef<FilterSwitchProps, 'input'>((props, ref) => {
20
20
  const group = useRadioGroupContext();
21
21
  const { value: valueProp } = props;
22
22
 
23
- const styles = useMultiStyleConfig('FilterSwitch', { ...group, ...props });
23
+ const styles = useMultiStyleConfig('FilterSwitch');
24
24
 
25
25
  const ownProps = omitThemingProps(props);
26
26
 
@@ -46,7 +46,7 @@ const FilterSwitch = forwardRef<FilterSwitchProps, 'input'>((props, ref) => {
46
46
  });
47
47
 
48
48
  return (
49
- <chakra.label {...getLabelProps()} {...getCheckboxProps()} __css={styles.container}>
49
+ <chakra.label {...getLabelProps()} {...getCheckboxProps()} __css={styles.item}>
50
50
  <chakra.input {...getInputProps(htmlInputProps, ref)} />
51
51
  {children}
52
52
  </chakra.label>
@@ -1,25 +1,11 @@
1
- import { RadioGroup as ChakraRadioGroup, RadioGroupProps } from '@chakra-ui/react';
1
+ import { RadioGroup as ChakraRadioGroup, RadioGroupProps, useMultiStyleConfig } from '@chakra-ui/react';
2
2
 
3
3
  export type { RadioGroupProps };
4
4
 
5
5
  const FilterSwitchGroup = (props: RadioGroupProps) => {
6
- return (
7
- <ChakraRadioGroup
8
- display="flex"
9
- {...props}
10
- sx={{
11
- label: {
12
- _notFirst: {
13
- borderLeftRadius: 0,
14
- borderLeft: 'none',
15
- },
16
- _notLast: {
17
- borderRightRadius: 0,
18
- },
19
- },
20
- }}
21
- />
22
- );
6
+ const { container } = useMultiStyleConfig('FilterSwitch');
7
+
8
+ return <ChakraRadioGroup sx={container} {...props} />;
23
9
  };
24
10
 
25
11
  export default FilterSwitchGroup;
@@ -0,0 +1,34 @@
1
+ import { useFilterContext } from '../Filter.context';
2
+ import { getOptionLabel } from '../Filter.utils';
3
+ import Icon from '../../Icon/Icon';
4
+ import FilterSwitch from '../FilterSwitch/FilterSwitch';
5
+ import FilterSwitchGroup from '../FilterSwitch/FilterSwitchGroup';
6
+
7
+ type FilterSwitchAdapterProps = {
8
+ category: string;
9
+ };
10
+
11
+ const FilterSwitchAdapter = (props: FilterSwitchAdapterProps) => {
12
+ const { category } = props;
13
+ const { data, onFilterChange, state } = useFilterContext();
14
+ const { iconsMap, options, optionsMap } = data[category];
15
+
16
+ if (!options?.length) {
17
+ return null;
18
+ }
19
+
20
+ const value = state[category]?.[0] || '';
21
+
22
+ return (
23
+ <FilterSwitchGroup onChange={(newValue) => onFilterChange(category, [newValue])} value={value}>
24
+ {options.map((opt) => (
25
+ <FilterSwitch key={opt} value={opt}>
26
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} size="16" />}
27
+ {getOptionLabel(opt, optionsMap)}
28
+ </FilterSwitch>
29
+ ))}
30
+ </FilterSwitchGroup>
31
+ );
32
+ };
33
+
34
+ export default FilterSwitchAdapter;
@@ -41,6 +41,9 @@ const CheckboxTheme = {
41
41
  },
42
42
  label: {
43
43
  userSelect: 'none',
44
+ display: 'flex',
45
+ alignItems: 'center',
46
+ gap: '4',
44
47
  _disabled: { color: 'neutral.60' },
45
48
  },
46
49
  },
@@ -36,6 +36,7 @@ type UsedChakraInputProps = Pick<
36
36
  | 'min'
37
37
  | 'minLength'
38
38
  | 'pattern'
39
+ | 'placeholder'
39
40
  | 'step'
40
41
  >;
41
42
  export interface InputProps extends UsedFormControlProps, UsedChakraInputProps {
@@ -45,6 +45,9 @@ const RadioTheme = {
45
45
  },
46
46
  label: {
47
47
  userSelect: 'none',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ gap: '4',
48
51
  _disabled: { color: 'neutral.60' },
49
52
  },
50
53
  },
@@ -17,7 +17,16 @@ import Tooltip, { TooltipProps } from '../../Tooltip/Tooltip';
17
17
  type UsedFormControlProps = Omit<FormControlProps, 'label' | 'onBlur' | 'onChange'>;
18
18
  type UsedChakraTextProps = Pick<
19
19
  ChakraTextareaProps,
20
- 'onBlur' | 'onChange' | 'role' | 'name' | 'value' | 'autoComplete' | 'autoFocus' | 'maxLength' | 'minLength'
20
+ | 'onBlur'
21
+ | 'onChange'
22
+ | 'role'
23
+ | 'name'
24
+ | 'value'
25
+ | 'autoComplete'
26
+ | 'autoFocus'
27
+ | 'maxLength'
28
+ | 'minLength'
29
+ | 'placeholder'
21
30
  >;
22
31
 
23
32
  export interface TextareaProps extends UsedFormControlProps, UsedChakraTextProps {
@@ -2,10 +2,9 @@ import { Icon, IconProps, forwardRef } from '@chakra-ui/react';
2
2
 
3
3
  const Filter = forwardRef<IconProps, 'svg'>((props, ref) => (
4
4
  <Icon ref={ref} viewBox="0 0 16 16" {...props}>
5
- <path
6
- fill="currentColor"
7
- d="M7.148 3.333A1.327 1.327 0 0 0 6 2.667c-.492 0-.918.269-1.148.666H2v1.334h2.852c.23.397.656.666 1.148.666.492 0 .917-.269 1.148-.666H14V3.333H7.148ZM10 6.667a1.33 1.33 0 0 0-1.149.666H2v1.334h6.851c.232.397.657.666 1.149.666s.917-.269 1.149-.666H14V7.333h-2.851A1.328 1.328 0 0 0 10 6.667Zm-5.101 4.666a1.33 1.33 0 0 1 2.202 0H14v1.334H7.193c-.216.442-.667.75-1.193.75s-.977-.308-1.193-.75H2v-1.334h2.899Z"
8
- />
5
+ <path d="M2 4.5H14V6H2V4.5Z" fill="currentColor" />
6
+ <path d="M6 10.5H10V12H6V10.5Z" fill="currentColor" />
7
+ <path d="M12 7.5H4V9H12V7.5Z" fill="currentColor" />
9
8
  </Icon>
10
9
  ));
11
10
 
@@ -2,10 +2,9 @@ import { Icon, IconProps, forwardRef } from '@chakra-ui/react';
2
2
 
3
3
  const Filter = forwardRef<IconProps, 'svg'>((props, ref) => (
4
4
  <Icon ref={ref} viewBox="0 0 24 24" {...props}>
5
- <path
6
- d="M10.723 5A1.99 1.99 0 0 0 9 4c-.738 0-1.376.404-1.722 1H3v2h4.278c.346.596.984 1 1.722 1a1.99 1.99 0 0 0 1.723-1H21V5H10.723ZM15 10c-.738 0-1.376.404-1.723 1H3v2h10.277c.347.596.985 1 1.723 1 .738 0 1.376-.404 1.723-1H21v-2h-4.277c-.347-.596-.985-1-1.723-1ZM7.348 17a1.994 1.994 0 0 1 3.304 0H21v2H10.79A1.993 1.993 0 0 1 9 20.125 1.993 1.993 0 0 1 7.21 19H3v-2h4.348Z"
7
- fill="currentColor"
8
- />
5
+ <path d="m3 7h18v2h-18z" fill="currentColor" />
6
+ <path d="m9 15h6v2h-6z" fill="currentColor" />
7
+ <path d="m18 11h-12v2h12z" fill="currentColor" />
9
8
  </Icon>
10
9
  ));
11
10
 
@@ -20,6 +20,7 @@ export interface SelectProps extends Omit<FormControlProps, 'label' | 'onBlur' |
20
20
  isLoading?: boolean;
21
21
  label?: ReactNode;
22
22
  name?: string;
23
+ placeholder?: ChakraSelectProps['placeholder'];
23
24
  onBlur?: ChakraSelectProps['onBlur'];
24
25
  onChange?: ChakraSelectProps['onChange'];
25
26
  size?: 'small' | 'medium';