@bitrise/bitkit 12.70.0-alpha.1 → 12.70.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.
Files changed (28) hide show
  1. package/package.json +31 -31
  2. package/src/Components/Avatar/Avatar.tsx +4 -1
  3. package/src/Components/DatePicker/DatePickerFooter.tsx +1 -1
  4. package/src/Components/DatePicker/DatePickerHeader.tsx +2 -0
  5. package/src/Components/DatePicker/DatePickerMonth.tsx +8 -1
  6. package/src/Components/ExpandableCard/ExpandableCard.theme.ts +0 -3
  7. package/src/Components/Filter/Filter.storyData.ts +29 -4
  8. package/src/Components/Filter/Filter.tsx +57 -40
  9. package/src/Components/Filter/Filter.types.ts +13 -3
  10. package/src/Components/Filter/Filter.utils.ts +9 -2
  11. package/src/Components/Filter/FilterAdd/FilterAdd.tsx +23 -14
  12. package/src/Components/Filter/FilterDate/FilterDate.tsx +7 -8
  13. package/src/Components/Filter/FilterForm/FilterForm.tsx +7 -2
  14. package/src/Components/Filter/FilterItem/FilterItem.tsx +14 -17
  15. package/src/Components/Filter/FilterSearch/FilterSearch.tsx +7 -6
  16. package/src/Components/Filter/FilterSwitch/FilterSwitch.theme.ts +16 -1
  17. package/src/Components/Filter/FilterSwitch/FilterSwitch.tsx +2 -2
  18. package/src/Components/Filter/FilterSwitch/FilterSwitchGroup.tsx +4 -18
  19. package/src/Components/Filter/FilterSwitchAdapter/FilterSwitchAdapter.tsx +34 -0
  20. package/src/Components/Form/Checkbox/Checkbox.theme.ts +3 -0
  21. package/src/Components/Form/Input/Input.tsx +1 -0
  22. package/src/Components/Form/Radio/Radio.theme.ts +3 -0
  23. package/src/Components/Form/Textarea/Textarea.tsx +10 -1
  24. package/src/Components/Icons/16x16/Filter.tsx +3 -4
  25. package/src/Components/Icons/24x24/Filter.tsx +3 -4
  26. package/src/Components/OverflowMenu/OverflowMenu.tsx +9 -2
  27. package/src/Components/Select/Select.tsx +1 -0
  28. package/src/index.ts +4 -0
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.1",
4
+ "version": "12.70.0",
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,54 +40,54 @@
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.45",
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",
75
- "eslint-plugin-import": "^2.29.0",
74
+ "eslint": "^8.55.0",
75
+ "eslint-plugin-import": "^2.29.1",
76
76
  "eslint-plugin-jest": "^27.6.0",
77
77
  "eslint-plugin-jsx-a11y": "^6.8.0",
78
78
  "eslint-plugin-prettier": "^5.0.1",
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.2",
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
@@ -4,9 +4,6 @@ const { defineMultiStyleConfig } = createMultiStyleConfigHelpers(['button', 'che
4
4
 
5
5
  const ExpandableCardTheme = defineMultiStyleConfig({
6
6
  baseStyle: ({ isOpen }) => ({
7
- box: {
8
- zIndex: 1,
9
- },
10
7
  button: {
11
8
  borderTopStartRadius: '8',
12
9
  borderTopEndRadius: '8',
@@ -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',
@@ -51,21 +60,37 @@ export const FILTER_STORY_DATA: FilterData = {
51
60
  console.log('onAsyncSearch', { category, q });
52
61
  return new Promise((resolve) => {
53
62
  setTimeout(() => {
54
- resolve(['found 1', 'found 2']);
63
+ resolve({ options: ['found 1', 'found 2'] });
55
64
  }, 2000);
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,28 @@
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
- export type FilterSearchCallback = (category: string, q: string) => Promise<FilterValue>;
11
+ export type FilterSearchCallback = (
12
+ category: string,
13
+ q: string,
14
+ ) => Promise<{ iconsMap?: FilterIconsMap; options: FilterOptions; optionsMap?: FilterOptionsMap }>;
8
15
 
9
16
  export type FilterCategoryProps = {
10
17
  categoryName?: string;
11
18
  categoryNamePlural?: string;
12
- dependsOn?: string[];
19
+ dependsOn?: Record<string, string>;
20
+ iconsMap?: FilterIconsMap;
13
21
  isMultiple?: boolean;
14
- isPermanent?: boolean;
15
22
  onAsyncSearch?: FilterSearchCallback;
16
23
  options?: FilterOptions;
17
24
  optionsMap?: FilterOptionsMap;
25
+ type?: FilterType;
18
26
  };
19
27
 
20
28
  export type FilterData = Record<string, FilterCategoryProps>;
@@ -24,6 +32,8 @@ export interface FilterContextType {
24
32
  data: FilterData;
25
33
  filtersDependOn?: string[];
26
34
  isLoading?: boolean;
35
+ onFilterClear: (category: string) => void;
36
+ onFilterChange: (category: string, value: FilterValue) => void;
27
37
  setPopoverOpen: Dispatch<SetStateAction<boolean>>;
28
38
  state: FilterState;
29
39
  }
@@ -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);
@@ -90,13 +91,15 @@ const FilterForm = (props: FilterFormProps) => {
90
91
  if (onAsyncSearch) {
91
92
  const response = await onAsyncSearch(category, searchValue);
92
93
  setLoading(false);
93
- setFoundOptions(response);
94
+ setFoundOptions(response.options);
94
95
  }
95
96
  };
96
97
 
97
98
  useEffect(() => {
98
99
  if (debouncedSearchValue.length > 0) {
99
100
  getAsyncList();
101
+ } else {
102
+ setLoading(false);
100
103
  }
101
104
  }, [debouncedSearchValue]);
102
105
 
@@ -129,6 +132,7 @@ const FilterForm = (props: FilterFormProps) => {
129
132
  {items.length
130
133
  ? items.map((opt) => (
131
134
  <Checkbox key={opt} value={opt}>
135
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
132
136
  {getOptionLabel(opt, optionsMap)}
133
137
  </Checkbox>
134
138
  ))
@@ -145,6 +149,7 @@ const FilterForm = (props: FilterFormProps) => {
145
149
  {items.length
146
150
  ? items.map((opt) => (
147
151
  <Radio key={opt} value={opt}>
152
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
148
153
  {getOptionLabel(opt, optionsMap)}
149
154
  </Radio>
150
155
  ))
@@ -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
 
@@ -1,3 +1,5 @@
1
+ import { Fragment } from 'react';
2
+ import { Portal } from '@chakra-ui/react';
1
3
  import IconButton, { IconButtonProps } from '../IconButton/IconButton';
2
4
  import Menu, { MenuProps } from '../Menu/Menu';
3
5
  import MenuButton from '../Menu/MenuButton';
@@ -7,9 +9,11 @@ export interface OverflowMenuProps extends MenuProps {
7
9
  children: MenuListProps['children'];
8
10
  buttonSize?: IconButtonProps['size'];
9
11
  triggerLabel?: string;
12
+ withPortal?: boolean;
10
13
  }
11
14
 
12
- const OverflowMenu = ({ buttonSize = 'small', children, triggerLabel }: OverflowMenuProps) => {
15
+ const OverflowMenu = ({ buttonSize = 'small', children, triggerLabel, withPortal }: OverflowMenuProps) => {
16
+ const Wrapper = withPortal ? Portal : Fragment;
13
17
  return (
14
18
  <Menu isLazy>
15
19
  <MenuButton
@@ -20,13 +24,16 @@ const OverflowMenu = ({ buttonSize = 'small', children, triggerLabel }: Overflow
20
24
  size={buttonSize}
21
25
  variant="tertiary"
22
26
  />
23
- <MenuList>{children}</MenuList>
27
+ <Wrapper>
28
+ <MenuList>{children}</MenuList>
29
+ </Wrapper>
24
30
  </Menu>
25
31
  );
26
32
  };
27
33
 
28
34
  OverflowMenu.defaultProps = {
29
35
  triggerLabel: 'Open menu',
36
+ withPortal: false,
30
37
  };
31
38
 
32
39
  export default OverflowMenu;
@@ -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';
package/src/index.ts CHANGED
@@ -323,3 +323,7 @@ export { default as TablePagination } from './Components/Table/TablePagination';
323
323
 
324
324
  export type { ProgressIndicatorProps } from './Components/ProgressIndicator/ProgressIndicator';
325
325
  export { default as ProgressIndicator } from './Components/ProgressIndicator/ProgressIndicator';
326
+
327
+ export type { FilterProps } from './Components/Filter/Filter';
328
+ export { default as Filter } from './Components/Filter/Filter';
329
+ export * from './Components/Filter/Filter.types';