@bitrise/bitkit 13.241.0 → 13.243.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 +1 -1
- package/src/Components/Dropdown/Dropdown.tsx +2 -0
- package/src/Components/Dropdown/DropdownProps.ts +1 -0
- package/src/Components/Filter/Filter.storyData.ts +4 -1
- package/src/Components/Filter/Filter.types.ts +1 -0
- package/src/Components/Filter/FilterAdd/FilterAdd.tsx +1 -0
- package/src/Components/Filter/FilterForm/FilterForm.tsx +133 -50
- package/src/Components/Filter/FilterItem/FilterItem.tsx +7 -1
- package/src/Components/Form/FormLabel.tsx +4 -1
package/package.json
CHANGED
|
@@ -386,6 +386,7 @@ const Dropdown = forwardRef<Element, DropdownProps<string | null>>(
|
|
|
386
386
|
isError,
|
|
387
387
|
isWarning,
|
|
388
388
|
label,
|
|
389
|
+
labelHelp,
|
|
389
390
|
name,
|
|
390
391
|
onBlur,
|
|
391
392
|
onChange,
|
|
@@ -498,6 +499,7 @@ const Dropdown = forwardRef<Element, DropdownProps<string | null>>(
|
|
|
498
499
|
badge={badge}
|
|
499
500
|
infoTooltipLabel={infoTooltipLabel}
|
|
500
501
|
infoTooltipProps={infoTooltipProps}
|
|
502
|
+
labelHelp={labelHelp}
|
|
501
503
|
htmlFor={buttonId}
|
|
502
504
|
>
|
|
503
505
|
{label}
|
|
@@ -39,7 +39,10 @@ export const FILTER_STORY_DATA: FilterData = {
|
|
|
39
39
|
type: 'select',
|
|
40
40
|
},
|
|
41
41
|
branch: {
|
|
42
|
-
|
|
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: {
|
|
@@ -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
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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
|
);
|
|
@@ -10,13 +10,14 @@ export interface FormLabelProps extends ChakraFormLabelProps {
|
|
|
10
10
|
children?: string;
|
|
11
11
|
infoTooltipLabel?: string;
|
|
12
12
|
infoTooltipProps?: TooltipProps;
|
|
13
|
+
labelHelp?: (icon: ReactNode) => ReactNode;
|
|
13
14
|
maxLength?: number;
|
|
14
15
|
valueLength?: number;
|
|
15
16
|
withCounter?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const FormLabel = forwardRef<FormLabelProps, 'label'>((props, ref) => {
|
|
19
|
-
const { badge, infoTooltipLabel, infoTooltipProps, maxLength, valueLength, withCounter, ...rest } = props;
|
|
20
|
+
const { badge, infoTooltipLabel, infoTooltipProps, labelHelp, maxLength, valueLength, withCounter, ...rest } = props;
|
|
20
21
|
|
|
21
22
|
const showLabel = rest.children || !!infoTooltipLabel || (withCounter && maxLength);
|
|
22
23
|
|
|
@@ -39,6 +40,8 @@ const FormLabel = forwardRef<FormLabelProps, 'label'>((props, ref) => {
|
|
|
39
40
|
ref={ref}
|
|
40
41
|
/>
|
|
41
42
|
|
|
43
|
+
{labelHelp?.(<Icon color="icon/tertiary" name="Info" size="16" />)}
|
|
44
|
+
|
|
42
45
|
{!!infoTooltipLabel && (
|
|
43
46
|
<Tooltip label={infoTooltipLabel} placement="right" {...infoTooltipProps}>
|
|
44
47
|
<Icon color="icon/tertiary" name="Info" size="16" />
|