@bitrise/bitkit 13.240.0 → 13.242.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "13.240.0",
4
+ "version": "13.242.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -1,4 +1,4 @@
1
- import React, { Children } from 'react';
1
+ import { Children } from 'react';
2
2
  import {
3
3
  Breadcrumb as ChakraBreadcrumb,
4
4
  BreadcrumbItem,
@@ -22,12 +22,9 @@ const Breadcrumb = forwardRef<BreadcrumbProps, 'nav'>((props, ref) => {
22
22
  const { children, hasSeparatorAfterLast, hasSeparatorBeforeFirst, separatorIconName, ...rest } = props;
23
23
  const { isMobile } = useResponsive();
24
24
 
25
- const childrenCount = Children.count(children);
26
- const items = Children.map(children, (child, index) => {
27
- if (!React.isValidElement(child)) {
28
- return null;
29
- }
30
-
25
+ const childArray = Children.toArray(children);
26
+ const childrenCount = childArray.length;
27
+ const items = childArray.map((child, index) => {
31
28
  return (
32
29
  <BreadcrumbItem>
33
30
  {hasSeparatorBeforeFirst && index === 0 && <BreadcrumbSeparator marginLeft="0" />}
@@ -39,7 +39,10 @@ export const FILTER_STORY_DATA: FilterData = {
39
39
  type: 'select',
40
40
  },
41
41
  branch: {
42
- options: [],
42
+ categoryName: 'Branch',
43
+ categoryNamePlural: 'Branches',
44
+ isPatternEnabled: true,
45
+ options: ['default 1', 'default 2', 'default 3', 'default 13'],
43
46
  type: 'tag',
44
47
  },
45
48
  cache_type: {
@@ -27,6 +27,7 @@ export type FilterCategoryProps = {
27
27
  onAsyncSearch?: FilterSearchCallback;
28
28
  options?: FilterOptions;
29
29
  optionsMap?: FilterOptionsMap;
30
+ isPatternEnabled?: boolean;
30
31
  } & (
31
32
  | {
32
33
  type: 'dateRange';
@@ -73,6 +73,7 @@ const FilterAdd = (props: FilterAddProps) => {
73
73
  <FilterForm
74
74
  category={selectedCategory}
75
75
  categoryName={data[selectedCategory].categoryName}
76
+ categoryNamePlural={data[selectedCategory].categoryNamePlural}
76
77
  onCancel={onClose}
77
78
  onChange={onFilterChange}
78
79
  />
@@ -8,6 +8,9 @@ import ButtonGroup from '../../ButtonGroup/ButtonGroup';
8
8
  import Checkbox from '../../Form/Checkbox/Checkbox';
9
9
  import CheckboxGroup from '../../Form/Checkbox/CheckboxGroup';
10
10
  import Icon from '../../Icon/Icon';
11
+ import Input from '../../Form/Input/Input';
12
+ import List from '../../List/List';
13
+ import ListItem from '../../List/ListItem';
11
14
  import Radio from '../../Form/Radio/Radio';
12
15
  import RadioGroup from '../../Form/Radio/RadioGroup';
13
16
  import SearchInput from '../../SearchInput/SearchInput';
@@ -18,20 +21,64 @@ import { isEqual, useDebounce } from '../../../utils/utils';
18
21
  import { getOptionLabel } from '../Filter.utils';
19
22
  import { useFilterContext } from '../Filter.context';
20
23
 
24
+ /**
25
+ * https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
26
+ * RegExp-escapes all characters in the given string.
27
+ */
28
+ function regExpEscape(s: string) {
29
+ return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
30
+ }
31
+
32
+ const getMatchingResults = (categoryNamePlural: string, selected: FilterValue, items: FilterOptions) => {
33
+ if (!selected.length || selected[0] === '') {
34
+ return (
35
+ <Text textStyle="body/md/regular" color="text/secondary">
36
+ Enter pattern to view {categoryNamePlural} with matching names.
37
+ </Text>
38
+ );
39
+ }
40
+
41
+ const filterRegexp = new RegExp(`^${selected[0].split(/\*+/).map(regExpEscape).join('.*')}$`);
42
+
43
+ const matchingResults = items.filter((item) => {
44
+ return filterRegexp.test(item) && item !== selected[0];
45
+ });
46
+ if (!matchingResults.length) {
47
+ return (
48
+ <Text textStyle="body/md/regular" color="text/secondary">
49
+ (no matching {categoryNamePlural})
50
+ </Text>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <Box maxHeight="11rem" overflow="scroll">
56
+ <List textStyle="body/md/regular" variant="unordered">
57
+ {matchingResults.map((r) => (
58
+ <ListItem key={r}>{r}</ListItem>
59
+ ))}
60
+ </List>
61
+ </Box>
62
+ );
63
+ };
64
+
21
65
  export type FilterFormProps = {
22
66
  category: string;
23
67
  categoryName?: string;
68
+ categoryNamePlural?: string;
24
69
  onChange: (category: string, selected: FilterValue, previousValue: FilterValue) => void;
25
70
  onCancel: () => void;
26
71
  };
27
72
 
28
73
  const FilterForm = (props: FilterFormProps) => {
29
- const { category, categoryName, onCancel, onChange } = props;
74
+ const { category, categoryName, categoryNamePlural, onCancel, onChange } = props;
30
75
 
31
76
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
32
77
 
33
78
  const { data, state } = useFilterContext();
79
+
34
80
  const {
81
+ isPatternEnabled,
35
82
  iconsMap,
36
83
  hasNotFilteredOption = true,
37
84
  preserveOptionOrder,
@@ -45,6 +92,10 @@ const FilterForm = (props: FilterFormProps) => {
45
92
 
46
93
  const [selected, setSelected] = useState<FilterValue>(value);
47
94
 
95
+ const [mode, setMode] = useState<'manually' | 'pattern'>(
96
+ isPatternEnabled && value?.length && value[0].includes('*') ? 'pattern' : 'manually',
97
+ );
98
+
48
99
  const [searchValue, setSearchValue] = useState<string>('');
49
100
  const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
50
101
 
@@ -145,58 +196,90 @@ const FilterForm = (props: FilterFormProps) => {
145
196
  </Badge>
146
197
  )}
147
198
  </Box>
148
- {(withSearch || isAsync) && (
149
- <SearchInput
150
- isDisabled={isInitialLoading}
151
- onChange={onSearchChange}
152
- placeholder={isAsync ? 'Start typing to search options' : 'Start typing to find options'}
153
- sx={filterStyle.formSearch}
154
- value={searchValue}
155
- />
156
- )}
157
- {isLoading && 'Loading...'}
158
- {!isLoading && isMultiple && (
159
- <CheckboxGroup onChange={setSelected} sx={filterStyle.formInputGroup} value={selected}>
160
- {items.length
161
- ? items.map((opt) => (
162
- <Checkbox key={opt} value={opt}>
163
- {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
164
- {getOptionLabel(opt, currentOptionMap)}
165
- </Checkbox>
166
- ))
167
- : getEmptyText()}
168
- </CheckboxGroup>
169
- )}
170
- {!isLoading && !isMultiple && (
171
- <RadioGroup onChange={(v) => setSelected([v])} sx={filterStyle.formInputGroup} value={selected[0] || ''}>
172
- {hasNotFilteredOption && (
173
- <Radio value="">
174
- <Text as="span" color="neutral.40" fontStyle="italic">
175
- Not filtered
176
- </Text>
177
- </Radio>
199
+ {mode === 'manually' ? (
200
+ <>
201
+ {(withSearch || isAsync) && (
202
+ <SearchInput
203
+ isDisabled={isInitialLoading}
204
+ onChange={onSearchChange}
205
+ placeholder="Search by name"
206
+ size="md"
207
+ sx={filterStyle.formSearch}
208
+ value={searchValue}
209
+ />
178
210
  )}
179
- {items.length
180
- ? items.map((opt) => {
181
- const hasIcon = iconsMap && iconsMap[opt];
182
- const label = getOptionLabel(opt, currentOptionMap);
183
- return (
184
- <Radio key={opt} value={opt}>
185
- {hasIcon ? (
186
- <Box as="span" display="flex" gap="4">
187
- <Icon name={iconsMap[opt]} />
188
- {label}
189
- </Box>
190
- ) : (
191
- label
192
- )}
193
- </Radio>
194
- );
195
- })
196
- : getEmptyText()}
197
- </RadioGroup>
211
+ {isLoading && 'Loading...'}
212
+ {!isLoading && isMultiple && (
213
+ <CheckboxGroup onChange={setSelected} sx={filterStyle.formInputGroup} value={selected}>
214
+ {items.length
215
+ ? items.map((opt) => (
216
+ <Checkbox key={opt} value={opt}>
217
+ {iconsMap && iconsMap[opt] && <Icon name={iconsMap[opt]} />}
218
+ {getOptionLabel(opt, currentOptionMap)}
219
+ </Checkbox>
220
+ ))
221
+ : getEmptyText()}
222
+ </CheckboxGroup>
223
+ )}
224
+ {!isLoading && !isMultiple && (
225
+ <RadioGroup onChange={(v) => setSelected([v])} sx={filterStyle.formInputGroup} value={selected[0] || ''}>
226
+ {hasNotFilteredOption && (
227
+ <Radio value="">
228
+ <Text as="span" color="neutral.40" fontStyle="italic">
229
+ Not filtered
230
+ </Text>
231
+ </Radio>
232
+ )}
233
+ {items.length
234
+ ? items.map((opt) => {
235
+ const hasIcon = iconsMap && iconsMap[opt];
236
+ const label = getOptionLabel(opt, currentOptionMap);
237
+ return (
238
+ <Radio key={opt} value={opt}>
239
+ {hasIcon ? (
240
+ <Box as="span" display="flex" gap="4">
241
+ <Icon name={iconsMap[opt]} />
242
+ {label}
243
+ </Box>
244
+ ) : (
245
+ label
246
+ )}
247
+ </Radio>
248
+ );
249
+ })
250
+ : getEmptyText()}
251
+ </RadioGroup>
252
+ )}
253
+ </>
254
+ ) : (
255
+ <>
256
+ <Input
257
+ isRequired
258
+ helperText="Allowed wildcard character: *"
259
+ label="Wildcard pattern"
260
+ marginBlockEnd="24"
261
+ placeholder="(e.g. *main*, release-*)"
262
+ size="md"
263
+ onChange={(e) => setSelected([e.target.value])}
264
+ value={selected[0] || ''}
265
+ />
266
+ <Text as="label" textStyle="comp/input/label" color="text/primary" display="block" marginBlockEnd="8">
267
+ Matching results
268
+ </Text>
269
+ {getMatchingResults((categoryNamePlural || category).toLowerCase(), selected, items)}
270
+ </>
198
271
  )}
199
272
  <ButtonGroup spacing="12" sx={filterStyle.formButtonGroup}>
273
+ {isPatternEnabled && !isEditMode && (
274
+ <Button
275
+ marginInlineEnd="auto"
276
+ onClick={() => setMode(mode === 'manually' ? 'pattern' : 'manually')}
277
+ size="sm"
278
+ variant="tertiary"
279
+ >
280
+ {mode === 'manually' ? 'Use wildcard pattern' : 'Select manually'}
281
+ </Button>
282
+ )}
200
283
  {isEditMode && hasNotFilteredOption && (
201
284
  <Button marginInlineEnd="auto" onClick={onClearClick} size="sm" variant="tertiary">
202
285
  Clear
@@ -98,7 +98,13 @@ const FilterItem = (props: FilterItemProps) => {
98
98
  </Box>
99
99
  </PopoverTrigger>
100
100
  <PopoverContent>
101
- <FilterForm category={category} categoryName={categoryName} onCancel={onClose} onChange={onChange} />
101
+ <FilterForm
102
+ category={category}
103
+ categoryName={categoryName}
104
+ categoryNamePlural={pluralCategoryString}
105
+ onCancel={onClose}
106
+ onChange={onChange}
107
+ />
102
108
  </PopoverContent>
103
109
  </Popover>
104
110
  );