@bitrise/bitkit 13.340.0 → 13.342.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.340.0",
4
+ "version": "13.342.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -92,7 +92,14 @@ const FilterForm = (props: FilterFormProps) => {
92
92
 
93
93
  const { data } = useFilterContext();
94
94
 
95
- const { isPatternEnabled, iconsMap, hasNotFilteredOption = true, isInitialLoading, isMultiple } = data[category];
95
+ const {
96
+ isPatternEnabled,
97
+ iconsMap,
98
+ hasNotFilteredOption = true,
99
+ isInitialLoading,
100
+ isMultiple,
101
+ searchPlaceholder,
102
+ } = data[category];
96
103
 
97
104
  const {
98
105
  currentOptionMap,
@@ -138,7 +145,7 @@ const FilterForm = (props: FilterFormProps) => {
138
145
  <SearchInput
139
146
  isDisabled={isInitialLoading}
140
147
  onChange={onSearchChange}
141
- placeholder="Search by name"
148
+ placeholder={searchPlaceholder || 'Search by name'}
142
149
  size="md"
143
150
  sx={filterStyle.formSearch}
144
151
  value={searchValue}
@@ -39,6 +39,7 @@ export const FILTER_STORY_DATA: FilterData = {
39
39
  hasNotFilteredOption: false,
40
40
  preserveOptionOrder: true,
41
41
  type: 'select',
42
+ searchPlaceholder: 'Search apps',
42
43
  },
43
44
  branch: {
44
45
  categoryName: 'Branch',
@@ -32,6 +32,7 @@ export type FilterCategoryProps = {
32
32
  optionsMap?: FilterOptionsMap;
33
33
  isPatternEnabled?: boolean;
34
34
  loadingText?: string;
35
+ searchPlaceholder?: string;
35
36
  } & (
36
37
  | {
37
38
  type: 'dateRange';
@@ -1,4 +1,4 @@
1
- import { FormEvent, useEffect, useMemo, useState } from 'react';
1
+ import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { isEqual, useDebounce } from '../../../utils/utils';
3
3
  import { useFilterContext } from '../Filter.context';
4
4
  import { FilterFormProps, FilterOptions, FilterOptionsMap, FilterValue } from '../Filter.types';
@@ -26,52 +26,56 @@ const useFilterForm = (props: Omit<FilterFormProps, 'onCancel'>) => {
26
26
  );
27
27
 
28
28
  const [searchValue, setSearchValue] = useState<string>('');
29
- const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
29
+ const debouncedSearchValue = useDebounce<string>(searchValue, 500);
30
30
 
31
31
  const [isLoading, setLoading] = useState(Boolean(isInitialLoading));
32
32
  const [foundOptions, setFoundOptions] = useState<FilterOptions>([]);
33
33
  const [foundOptionsMap, setFoundOptionsMap] = useState<FilterOptionsMap>();
34
34
 
35
+ const latestRequestRef = useRef(0);
36
+
35
37
  const isAsync = !!onAsyncSearch;
36
38
  const withSearch = (options && options.length > 5) || isAsync;
37
39
 
38
40
  const isSubmitDisabled =
39
41
  isEqual(selected, value) || (mode === 'pattern' && !!selected[0] && !selected[0].includes('*'));
42
+
40
43
  const filteredOptions = useMemo(() => {
41
- if (options?.length) {
42
- return options.filter((opt) => {
43
- const optLabel = (optionsMap && optionsMap[opt]) || opt;
44
+ return (
45
+ options?.filter((opt) => {
46
+ const optLabel = optionsMap?.[opt] || opt;
44
47
  return optLabel.toLowerCase().includes(searchValue.toLowerCase());
45
- });
46
- }
47
- return [];
48
- }, [searchValue, options]);
49
-
50
- const onSearchChange = (q: string) => {
51
- setSearchValue(q);
52
- if (isAsync) {
53
- if (q.length > 0) {
54
- setLoading(true);
55
- } else {
48
+ }) || []
49
+ );
50
+ }, [searchValue, options, optionsMap]);
51
+
52
+ const onSearchChange = useCallback(
53
+ (q: string) => {
54
+ setSearchValue(q);
55
+ if (isAsync && q.length === 0) {
56
56
  setFoundOptions([]);
57
57
  }
58
- }
59
- };
58
+ },
59
+ [isAsync],
60
+ );
60
61
 
61
- const onSubmit = (e?: FormEvent<HTMLDivElement>) => {
62
- e?.preventDefault();
63
- const newSelected = selected[0] === '' ? [] : selected;
64
- onChange(category, newSelected, value);
65
- };
62
+ const onSubmit = useCallback(
63
+ (e?: FormEvent<HTMLDivElement>) => {
64
+ e?.preventDefault();
65
+ const newSelected = selected[0] === '' ? [] : selected;
66
+ onChange(category, newSelected, value);
67
+ },
68
+ [selected, onChange, category, value],
69
+ );
66
70
 
67
- const onClearClick = () => {
71
+ const onClearClick = useCallback(() => {
68
72
  let newValue: FilterValue = [];
69
73
  if (data[category]?.type === 'switch') {
70
74
  newValue = ['all'];
71
75
  }
72
76
  setSelected(newValue);
73
77
  onChange(category, newValue, value);
74
- };
78
+ }, [data, category, onChange, value]);
75
79
 
76
80
  const getEmptyText = () => {
77
81
  if (searchValue.length) {
@@ -80,36 +84,60 @@ const useFilterForm = (props: Omit<FilterFormProps, 'onCancel'>) => {
80
84
  return '';
81
85
  };
82
86
 
83
- const getAsyncList = async () => {
84
- if (onAsyncSearch) {
85
- const response = await onAsyncSearch(category, searchValue);
86
- setLoading(false);
87
- setFoundOptions(response.options);
88
- setFoundOptionsMap(response.optionsMap || optionsMap);
87
+ const getAsyncList = useCallback(async () => {
88
+ if (!onAsyncSearch) return;
89
+
90
+ latestRequestRef.current += 1;
91
+ const requestId = latestRequestRef.current;
92
+ setLoading(true);
93
+
94
+ try {
95
+ const response = await onAsyncSearch(category, debouncedSearchValue);
96
+
97
+ if (requestId === latestRequestRef.current) {
98
+ setFoundOptions(response.options);
99
+ setFoundOptionsMap(response.optionsMap || optionsMap);
100
+ setLoading(false);
101
+ }
102
+ } catch (error) {
103
+ if (requestId === latestRequestRef.current) {
104
+ setLoading(false);
105
+ }
89
106
  }
90
- };
107
+ }, [onAsyncSearch, category, debouncedSearchValue, optionsMap]);
91
108
 
92
109
  useEffect(() => {
93
- setLoading(Boolean(isInitialLoading));
110
+ setLoading(!!isInitialLoading);
94
111
  }, [isInitialLoading]);
95
112
 
96
113
  useEffect(() => {
97
- if (debouncedSearchValue.length > 0) {
114
+ if (isAsync && debouncedSearchValue) {
98
115
  getAsyncList();
99
- } else {
100
- setLoading(Boolean(isInitialLoading));
116
+ } else if (isAsync && !debouncedSearchValue) {
117
+ setFoundOptions([]);
118
+ setLoading(!!isInitialLoading);
101
119
  }
102
- }, [debouncedSearchValue]);
120
+ }, [debouncedSearchValue, isAsync, getAsyncList, isInitialLoading]);
121
+
122
+ const isEditMode = !!value.length;
123
+ const isAsyncSearch = isAsync && !!debouncedSearchValue && !!searchValue;
124
+ const currentOptionMap = isAsyncSearch ? foundOptionsMap : optionsMap;
103
125
 
104
- const isEditMode = value.length !== 0;
126
+ const items = useMemo(() => {
127
+ let baseItems: string[] = [];
105
128
 
106
- const isAsyncSearch = isAsync && !!searchValue;
129
+ if (isAsync) {
130
+ baseItems = debouncedSearchValue ? foundOptions : options || [];
131
+ } else {
132
+ baseItems = searchValue ? filteredOptions : options || [];
133
+ }
107
134
 
108
- const items = isAsyncSearch
109
- ? Array.from(new Set(preserveOptionOrder ? [...foundOptions] : [...value, ...foundOptions]))
110
- : Array.from(new Set(preserveOptionOrder ? [...filteredOptions] : [...value, ...filteredOptions]));
135
+ if (!searchValue || (isAsync && !debouncedSearchValue)) {
136
+ return baseItems;
137
+ }
111
138
 
112
- const currentOptionMap = isAsyncSearch ? foundOptionsMap : optionsMap;
139
+ return Array.from(new Set(preserveOptionOrder ? baseItems : [...value, ...baseItems]));
140
+ }, [isAsync, debouncedSearchValue, searchValue, foundOptions, filteredOptions, options, preserveOptionOrder, value]);
113
141
 
114
142
  return {
115
143
  currentOptionMap,