@controleonline/ui-default 1.0.264 → 1.0.265

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 (46) hide show
  1. package/AGENTS.md +3 -2
  2. package/package.json +21 -18
  3. package/src/react/components/errors/DefaultErrors.js +64 -82
  4. package/src/react/components/filters/CompactFilterSelector.js +58 -23
  5. package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
  6. package/src/react/components/filters/DefaultColumnFilter.js +177 -45
  7. package/src/react/components/filters/DefaultExternalFilters.js +201 -143
  8. package/src/react/components/filters/DefaultSearch.js +27 -25
  9. package/src/react/components/form/DefaultForm.js +72 -53
  10. package/src/react/components/form/DefaultForm.styles.js +71 -66
  11. package/src/react/components/inputs/DefaultDateInput.js +150 -0
  12. package/src/react/components/inputs/DefaultInput.js +44 -11
  13. package/src/react/components/inputs/DefaultInput.styles.js +6 -0
  14. package/src/react/components/inputs/DefaultSelect.js +200 -43
  15. package/src/react/components/inputs/defaultInputUtils.js +40 -19
  16. package/src/react/components/table/DefaultColumnMenu.js +108 -0
  17. package/src/react/components/table/DefaultDebug.js +233 -0
  18. package/src/react/components/table/DefaultFiltersModal.js +142 -0
  19. package/src/react/components/table/DefaultModalButton.js +18 -0
  20. package/src/react/components/table/DefaultSearchModal.js +49 -0
  21. package/src/react/components/table/DefaultTable.js +350 -2025
  22. package/src/react/components/table/DefaultTable.styles.js +217 -97
  23. package/src/react/components/table/DefaultTable.utils.js +502 -0
  24. package/src/react/components/table/DefaultTableBody.js +26 -0
  25. package/src/react/components/table/DefaultTableCards.js +171 -0
  26. package/src/react/components/table/DefaultTableControls.js +208 -0
  27. package/src/react/components/table/DefaultTableEmptyState.js +22 -0
  28. package/src/react/components/table/DefaultTableFooter.js +172 -0
  29. package/src/react/components/table/DefaultTableInput.js +145 -0
  30. package/src/react/components/table/DefaultTableRows.js +340 -0
  31. package/src/react/components/table/DefaultTableSearch.js +100 -0
  32. package/src/react/components/table/DefaultTableToolbar.js +98 -0
  33. package/src/react/components/table/DefaultToolbarAction.js +56 -0
  34. package/src/react/components/table/useDefaultTablePagination.js +370 -0
  35. package/src/react/components/table/useDefaultTableSorting.js +126 -0
  36. package/src/react/components/table/useDefaultTableTheme.js +58 -0
  37. package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
  38. package/src/store/default/actions.js +51 -13
  39. package/src/store/default/getters.js +10 -8
  40. package/src/store/default/mutation_types.js +4 -2
  41. package/src/store/default/mutations.js +10 -1
  42. package/src/tests/react/components/form/DefaultForm.test.js +81 -0
  43. package/src/tests/react/components/inputs/DefaultSelect.test.js +106 -14
  44. package/src/tests/react/components/table/DefaultTable.test.js +945 -96
  45. package/src/tests/react/store/defaultActions.test.js +59 -0
  46. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,70 +1,75 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
 
3
- const styles = StyleSheet.create({
4
- body: {
5
- maxHeight: 520,
6
- },
7
- formGrid: {
8
- padding: 14,
9
- flexDirection: 'row',
10
- flexWrap: 'wrap',
11
- gap: 10,
12
- },
13
- formField: {
14
- flexGrow: 1,
15
- flexBasis: 220,
16
- minWidth: 220,
17
- },
18
- emptyBox: {
19
- padding: 18,
20
- alignItems: 'center',
21
- justifyContent: 'center',
22
- },
23
- emptyText: {
24
- color: '#64748B',
25
- fontSize: 12,
26
- fontWeight: '700',
27
- },
28
- actions: {
29
- minHeight: 52,
30
- paddingHorizontal: 14,
31
- borderTopWidth: 1,
32
- borderTopColor: '#E2E8F0',
33
- flexDirection: 'row',
34
- alignItems: 'center',
35
- justifyContent: 'flex-end',
36
- gap: 8,
37
- },
38
- secondaryButton: {
39
- height: 34,
40
- borderRadius: 8,
41
- borderWidth: 1,
42
- borderColor: '#CBD5E1',
43
- paddingHorizontal: 14,
44
- alignItems: 'center',
45
- justifyContent: 'center',
46
- backgroundColor: '#FFFFFF',
47
- },
48
- secondaryButtonText: {
49
- color: '#334155',
50
- fontSize: 12,
51
- fontWeight: '800',
52
- },
53
- primaryButton: {
54
- height: 34,
55
- borderRadius: 8,
56
- paddingHorizontal: 16,
57
- alignItems: 'center',
58
- justifyContent: 'center',
59
- },
60
- primaryButtonDisabled: {
61
- opacity: 0.62,
62
- },
63
- primaryButtonText: {
64
- color: '#FFFFFF',
65
- fontSize: 12,
66
- fontWeight: '900',
67
- },
68
- });
3
+ export const createStyles = palette =>
4
+ StyleSheet.create({
5
+ body: {
6
+ maxHeight: 520,
7
+ },
8
+ formGrid: {
9
+ padding: 14,
10
+ flexDirection: 'row',
11
+ flexWrap: 'wrap',
12
+ gap: 10,
13
+ },
14
+ formField: {
15
+ flexGrow: 1,
16
+ flexBasis: 220,
17
+ minWidth: 220,
18
+ },
19
+ emptyBox: {
20
+ padding: 18,
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ },
24
+ emptyText: {
25
+ color: palette.textSecondary,
26
+ fontSize: 12,
27
+ fontWeight: '700',
28
+ },
29
+ actions: {
30
+ minHeight: 52,
31
+ paddingHorizontal: 14,
32
+ borderTopWidth: 1,
33
+ borderTopColor: palette.dividerBorder,
34
+ flexDirection: 'row',
35
+ alignItems: 'center',
36
+ justifyContent: 'flex-end',
37
+ gap: 8,
38
+ },
39
+ secondaryButton: {
40
+ height: 34,
41
+ borderRadius: 8,
42
+ borderWidth: 1,
43
+ borderColor: palette.buttonBorderSecondary,
44
+ paddingHorizontal: 14,
45
+ alignItems: 'center',
46
+ justifyContent: 'center',
47
+ backgroundColor: palette.buttonBackgroundSecondary,
48
+ },
49
+ secondaryButtonText: {
50
+ color: palette.buttonTextSecondary,
51
+ fontSize: 12,
52
+ fontWeight: '800',
53
+ },
54
+ primaryButton: {
55
+ height: 34,
56
+ borderRadius: 8,
57
+ paddingHorizontal: 16,
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ },
61
+ primaryButtonDisabled: {
62
+ backgroundColor: palette.buttonDisabledBackground,
63
+ },
64
+ primaryButtonText: {
65
+ color: palette.buttonText,
66
+ fontSize: 12,
67
+ fontWeight: '900',
68
+ },
69
+ primaryButtonTextDisabled: {
70
+ color: palette.buttonDisabledText,
71
+ },
72
+ });
69
73
 
74
+ const styles = createStyles({});
70
75
  export default styles;
@@ -0,0 +1,150 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Text, TextInput, TouchableOpacity, View } from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import Formatter from '@controleonline/ui-common/src/utils/formatter.js';
5
+ import {
6
+ getColumnKey,
7
+ isEditableColumn,
8
+ normalizeText,
9
+ resolveCellText,
10
+ resolveEditValue,
11
+ } from './defaultInputUtils';
12
+ import styles from './DefaultInput.styles';
13
+
14
+ const normalizeDateDraft = value => {
15
+ const text = normalizeText(value);
16
+ if (!text) return '';
17
+
18
+ return Formatter.formatDateToBR(text);
19
+ };
20
+
21
+ const DefaultDateInput = ({
22
+ accentColor = '#2563EB',
23
+ autoFocus = true,
24
+ autoSave = true,
25
+ column,
26
+ columns = [],
27
+ containerStyle = null,
28
+ displayValue,
29
+ editing = false,
30
+ inputStyle = null,
31
+ label = '',
32
+ numberOfLines = 1,
33
+ onCancelEditing = null,
34
+ onChangeValue = null,
35
+ onSave = null,
36
+ onStartEditing = null,
37
+ readTextStyle = null,
38
+ row = {},
39
+ saving = false,
40
+ showLabel = false,
41
+ storeName = '',
42
+ value,
43
+ variant = 'cell',
44
+ }) => {
45
+ const fieldName = getColumnKey(column);
46
+ const isForm = variant === 'form';
47
+ const canEdit = isEditableColumn(column) && typeof onStartEditing === 'function';
48
+ const editValue = useMemo(
49
+ () => normalizeDateDraft(value ?? resolveEditValue(column, row)),
50
+ [column, row, value],
51
+ );
52
+ const [draftValue, setDraftValue] = useState(editValue);
53
+ const [validationMessage, setValidationMessage] = useState('');
54
+ const resolvedLabel =
55
+ displayValue ?? resolveCellText({ column, columns, row, storeName, value });
56
+
57
+ useEffect(() => {
58
+ setDraftValue(editValue);
59
+ setValidationMessage('');
60
+ }, [editValue, editing]);
61
+
62
+ const saveDraft = () => {
63
+ const validationResult = Formatter.validateBRDate(draftValue);
64
+
65
+ if (validationResult !== true) {
66
+ setValidationMessage(validationResult);
67
+ return;
68
+ }
69
+
70
+ setValidationMessage('');
71
+
72
+ if (autoSave) {
73
+ onSave?.(draftValue);
74
+ return;
75
+ }
76
+
77
+ onChangeValue?.(draftValue);
78
+ };
79
+
80
+ const updateDraft = nextValue => {
81
+ setDraftValue(nextValue);
82
+ if (validationMessage) setValidationMessage('');
83
+ if (!autoSave) onChangeValue?.(nextValue);
84
+ };
85
+
86
+ if (!editing && !isForm) {
87
+ return (
88
+ <View style={[styles.wrap, containerStyle]}>
89
+ {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
90
+ <TouchableOpacity
91
+ style={styles.readButton}
92
+ activeOpacity={canEdit ? 0.78 : 1}
93
+ disabled={!canEdit}
94
+ onPress={() => onStartEditing?.()}
95
+ >
96
+ <Text
97
+ style={[
98
+ styles.readText,
99
+ resolvedLabel === '-' ? styles.mutedText : null,
100
+ readTextStyle,
101
+ ]}
102
+ numberOfLines={numberOfLines}
103
+ >
104
+ {resolvedLabel || '-'}
105
+ </Text>
106
+ {saving ? (
107
+ <Text style={[styles.savingText, { color: accentColor }]}>Salvando</Text>
108
+ ) : canEdit ? (
109
+ <Icon style={styles.editIcon} name="calendar" size={13} color="#64748B" />
110
+ ) : null}
111
+ </TouchableOpacity>
112
+ </View>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <View style={[styles.wrap, containerStyle]}>
118
+ {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
119
+ <View style={styles.editingWrap}>
120
+ <View style={styles.editingRow}>
121
+ <TextInput
122
+ style={[
123
+ styles.input,
124
+ isForm ? styles.formInput : null,
125
+ inputStyle,
126
+ ]}
127
+ value={draftValue}
128
+ keyboardType="numeric"
129
+ placeholder="DD/MM/YYYY"
130
+ onBlur={autoSave ? saveDraft : undefined}
131
+ onChangeText={updateDraft}
132
+ onSubmitEditing={saveDraft}
133
+ autoFocus={autoFocus && !isForm}
134
+ selectTextOnFocus
135
+ />
136
+ <Icon name="calendar" size={15} color="#64748B" />
137
+ {!isForm ? (
138
+ <TouchableOpacity style={styles.cancelButton} activeOpacity={0.8} onPress={onCancelEditing}>
139
+ <Icon name="x" size={14} color="#64748B" />
140
+ </TouchableOpacity>
141
+ ) : null}
142
+ </View>
143
+ {!!validationMessage && <Text style={styles.validationText}>{validationMessage}</Text>}
144
+ {saving ? <Text style={styles.savingText}>Salvando</Text> : null}
145
+ </View>
146
+ </View>
147
+ );
148
+ };
149
+
150
+ export default DefaultDateInput;
@@ -2,13 +2,15 @@ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import { Text, TextInput, TouchableOpacity, View } from 'react-native';
3
3
  import Icon from 'react-native-vector-icons/Feather';
4
4
  import {
5
- getColumnKey,
6
- isEditableColumn,
5
+ getColumnKey,
6
+ isDateLikeColumn,
7
+ isEditableColumn,
7
8
  normalizeText,
8
9
  resolveCellText,
9
10
  resolveEditValue,
10
- } from './defaultInputUtils';
11
- import DefaultSelect from './DefaultSelect';
11
+ } from './defaultInputUtils';
12
+ import DefaultDateInput from './DefaultDateInput';
13
+ import DefaultSelect from './DefaultSelect';
12
14
  import styles from './DefaultInput.styles';
13
15
 
14
16
  const DefaultInput = ({
@@ -24,11 +26,12 @@ const DefaultInput = ({
24
26
  inputStyle = null,
25
27
  label = '',
26
28
  numberOfLines = 1,
27
- onBeforeOpen = null,
28
- onCancelEditing = null,
29
- onChangeValue = null,
30
- onSave = null,
31
- onStartEditing = null,
29
+ onBeforeOpen = null,
30
+ onCancelEditing = null,
31
+ onChangeValue = null,
32
+ onSave = null,
33
+ onSearchChange = null,
34
+ onStartEditing = null,
32
35
  readTextStyle = null,
33
36
  row = {},
34
37
  saving = false,
@@ -52,7 +55,7 @@ const DefaultInput = ({
52
55
  setDraftValue(editValue);
53
56
  }, [editValue, editing]);
54
57
 
55
- if (column?.list) {
58
+ if (column?.list) {
56
59
  return (
57
60
  <DefaultSelect
58
61
  accentColor={accentColor}
@@ -79,7 +82,37 @@ const DefaultInput = ({
79
82
  variant={variant}
80
83
  />
81
84
  );
82
- }
85
+ }
86
+
87
+ if (isDateLikeColumn(column)) {
88
+ return (
89
+ <DefaultDateInput
90
+ accentColor={accentColor}
91
+ autoFocus={autoFocus}
92
+ autoSave={autoSave}
93
+ column={column}
94
+ columns={columns}
95
+ containerStyle={containerStyle}
96
+ displayValue={displayValue}
97
+ editing={editing}
98
+ inputStyle={inputStyle}
99
+ label={label}
100
+ numberOfLines={numberOfLines}
101
+ onCancelEditing={onCancelEditing}
102
+ onChangeValue={onChangeValue}
103
+ onSave={onSave}
104
+ onSearchChange={onSearchChange}
105
+ onStartEditing={onStartEditing}
106
+ readTextStyle={readTextStyle}
107
+ row={row}
108
+ saving={saving}
109
+ showLabel={showLabel}
110
+ storeName={storeName}
111
+ value={value}
112
+ variant={variant}
113
+ />
114
+ );
115
+ }
83
116
 
84
117
  const saveDraft = () => {
85
118
  if (autoSave) {
@@ -88,6 +88,12 @@ const styles = StyleSheet.create({
88
88
  fontWeight: '800',
89
89
  textTransform: 'uppercase',
90
90
  },
91
+ validationText: {
92
+ marginTop: 3,
93
+ color: '#DC2626',
94
+ fontSize: 10,
95
+ fontWeight: '800',
96
+ },
91
97
  });
92
98
 
93
99
  export default styles;
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  Modal,
4
4
  ScrollView,
@@ -7,18 +7,87 @@ import {
7
7
  TouchableOpacity,
8
8
  View,
9
9
  } from 'react-native';
10
- import Icon from 'react-native-vector-icons/Feather';
11
- import {
12
- buildOptionsFromColumn,
13
- getColumnKey,
14
- isEditableColumn,
15
- normalizeOptionKey,
16
- normalizeText,
17
- resolveCellText,
18
- resolveEditValue,
19
- } from './defaultInputUtils';
20
- import inputStyles from './DefaultInput.styles';
21
- import styles from './DefaultSelect.styles';
10
+ import Icon from 'react-native-vector-icons/Feather';
11
+ import { getAllStores, useStore } from '@store';
12
+ import {
13
+ buildOptionsFromColumn,
14
+ getColumnKey,
15
+ isEditableColumn,
16
+ mapOptions,
17
+ normalizeOptionKey,
18
+ normalizeText,
19
+ resolveCellText,
20
+ resolveEditValue,
21
+ resolveStoreNameFromList,
22
+ } from './defaultInputUtils';
23
+ import inputStyles from './DefaultInput.styles';
24
+ import styles from './DefaultSelect.styles';
25
+
26
+ const COMPANY_SCOPED_LIST_STORES = new Set([
27
+ 'categories',
28
+ 'paymentType',
29
+ 'wallet',
30
+ ]);
31
+
32
+ const normalizeCollectionItems = response => {
33
+ if (Array.isArray(response)) return response;
34
+ if (Array.isArray(response?.member)) return response.member;
35
+ if (Array.isArray(response?.['hydra:member'])) return response['hydra:member'];
36
+ return [];
37
+ };
38
+
39
+ const stableSerialize = value => {
40
+ if (value === null || value === undefined) return 'null';
41
+ if (Array.isArray(value)) return `[${value.map(item => stableSerialize(item)).join(',')}]`;
42
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
43
+ if (value && typeof value === 'object') {
44
+ return `{${Object.keys(value)
45
+ .sort()
46
+ .map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
47
+ .join(',')}}`;
48
+ }
49
+
50
+ return JSON.stringify(value);
51
+ };
52
+
53
+ const resolveListActionName = list =>
54
+ normalizeText(list).split('/')[1] || 'getItems';
55
+
56
+ const resolveCompanyScopedParams = ({ currentCompanyId, listStoreName }) => {
57
+ if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
58
+ return {};
59
+ }
60
+
61
+ return listStoreName === 'categories'
62
+ ? { company: currentCompanyId }
63
+ : { people: currentCompanyId };
64
+ };
65
+
66
+ const resolveColumnListLoadParams = ({
67
+ column,
68
+ currentCompanyId,
69
+ requestParams = {},
70
+ searchValue = '',
71
+ }) => {
72
+ const listStoreName = resolveStoreNameFromList(column?.list);
73
+ const resolvedCustomParams = typeof column?.listRequestParams === 'function'
74
+ ? column.listRequestParams({ currentCompanyId, requestParams })
75
+ : column?.listRequestParams;
76
+ const customParams =
77
+ resolvedCustomParams &&
78
+ typeof resolvedCustomParams === 'object' &&
79
+ !Array.isArray(resolvedCustomParams)
80
+ ? resolvedCustomParams
81
+ : {};
82
+
83
+ return {
84
+ ...resolveCompanyScopedParams({ currentCompanyId, listStoreName }),
85
+ ...customParams,
86
+ ...(normalizeText(searchValue)
87
+ ? { [column?.listSearchParam || column?.searchParam || 'search']: normalizeText(searchValue) }
88
+ : {}),
89
+ };
90
+ };
22
91
 
23
92
  const DefaultSelect = ({
24
93
  accentColor = '#2563EB',
@@ -32,10 +101,11 @@ const DefaultSelect = ({
32
101
  label = '',
33
102
  numberOfLines = 1,
34
103
  onBeforeOpen = null,
35
- onCancelEditing = null,
36
- onChangeValue = null,
37
- onSave = null,
38
- onStartEditing = null,
104
+ onCancelEditing = null,
105
+ onChangeValue = null,
106
+ onSave = null,
107
+ onSearchChange = null,
108
+ onStartEditing = null,
39
109
  readTextStyle = null,
40
110
  row = {},
41
111
  saving = false,
@@ -43,16 +113,32 @@ const DefaultSelect = ({
43
113
  storeName = '',
44
114
  value,
45
115
  variant = 'cell',
46
- }) => {
47
- const [isOpen, setIsOpen] = useState(false);
48
- const [searchText, setSearchText] = useState('');
116
+ }) => {
117
+ const peopleStore = useStore('people');
118
+ const ownerStore = useStore(storeName);
119
+ const loadedListKeysRef = useRef(new Set());
120
+ const [isOpen, setIsOpen] = useState(false);
121
+ const [loadedItems, setLoadedItems] = useState([]);
122
+ const [searchText, setSearchText] = useState('');
123
+ const currentCompanyId = peopleStore?.getters?.currentCompany?.id;
124
+ const requestParams = ownerStore?.getters?.configs?.requestParams || {};
49
125
 
50
126
  const fieldName = getColumnKey(column);
51
127
  const selectedKey = normalizeOptionKey(value ?? resolveEditValue(column, row));
52
- const options = useMemo(
53
- () => buildOptionsFromColumn(column, getOptionsForColumn, storeName),
54
- [column, getOptionsForColumn, storeName],
55
- );
128
+ const options = useMemo(() => {
129
+ const localOptions = buildOptionsFromColumn(column, getOptionsForColumn, storeName);
130
+ const remoteOptions = mapOptions(column, loadedItems, storeName);
131
+
132
+ if (remoteOptions.length === 0) {
133
+ return localOptions;
134
+ }
135
+
136
+ const seenKeys = new Set(remoteOptions.map(option => option.key));
137
+ return [
138
+ ...remoteOptions,
139
+ ...localOptions.filter(option => !seenKeys.has(option.key)),
140
+ ];
141
+ }, [column, getOptionsForColumn, loadedItems, storeName]);
56
142
  const selected = options.find(option => option.key === selectedKey);
57
143
  const resolvedLabel =
58
144
  displayValue ??
@@ -60,14 +146,82 @@ const DefaultSelect = ({
60
146
  resolveCellText({ column, columns, row, storeName, value });
61
147
  const canEdit = isEditableColumn(column) && typeof onStartEditing === 'function';
62
148
  const isForm = variant === 'form';
63
- const filteredOptions = useMemo(() => {
64
- const query = normalizeText(searchText).toLowerCase();
65
- if (!query) return options;
66
- return options.filter(option =>
149
+ const filteredOptions = useMemo(() => {
150
+ const query = normalizeText(searchText).toLowerCase();
151
+ if (!query || onSearchChange) return options;
152
+ return options.filter(option =>
67
153
  normalizeText(option.label).toLowerCase().includes(query) ||
68
154
  normalizeText(option.key).toLowerCase().includes(query),
69
155
  );
70
- }, [options, searchText]);
156
+ }, [options, searchText]);
157
+
158
+ const applyLoadedItems = useCallback(value => {
159
+ const nextItems = normalizeCollectionItems(value);
160
+ if (nextItems.length > 0 || Array.isArray(value)) {
161
+ setLoadedItems(nextItems);
162
+ }
163
+ }, []);
164
+
165
+ const loadRemoteOptions = useCallback((searchValue = '') => {
166
+ const listStoreName = resolveStoreNameFromList(column?.list);
167
+ const actionName = resolveListActionName(column?.list);
168
+ const stores = getAllStores?.() || {};
169
+ const listStore = stores?.[listStoreName];
170
+ const listAction = listStore?.actions?.[actionName];
171
+
172
+ if (!listStoreName || typeof listAction !== 'function') {
173
+ return Promise.resolve([]);
174
+ }
175
+
176
+ const listLoadParams = resolveColumnListLoadParams({
177
+ column,
178
+ currentCompanyId,
179
+ requestParams,
180
+ searchValue,
181
+ });
182
+ const loadKey = `${getColumnKey(column)}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
183
+
184
+ if (
185
+ !normalizeText(searchValue) &&
186
+ loadedListKeysRef.current.has(loadKey) &&
187
+ loadedItems.length > 0
188
+ ) {
189
+ return Promise.resolve(loadedItems);
190
+ }
191
+
192
+ if (!normalizeText(searchValue)) {
193
+ loadedListKeysRef.current.add(loadKey);
194
+ }
195
+
196
+ return Promise.resolve(
197
+ listAction({
198
+ ...listLoadParams,
199
+ __storeMeta: {
200
+ dedupeKey: `default-select-list-options:${loadKey}`,
201
+ skipSystemError: true,
202
+ },
203
+ }),
204
+ )
205
+ .then(response => {
206
+ applyLoadedItems(response);
207
+ return response;
208
+ })
209
+ .catch(error => {
210
+ loadedListKeysRef.current.delete(loadKey);
211
+ throw error;
212
+ });
213
+ }, [applyLoadedItems, column, currentCompanyId, loadedItems, requestParams]);
214
+
215
+ const handleSearchChange = useCallback(value => {
216
+ setSearchText(value);
217
+ const maybeLoad = onSearchChange ? onSearchChange(value) : loadRemoteOptions(value);
218
+ if (maybeLoad && typeof maybeLoad.then === 'function') {
219
+ maybeLoad.then(applyLoadedItems).catch(() => {});
220
+ return;
221
+ }
222
+
223
+ applyLoadedItems(maybeLoad);
224
+ }, [applyLoadedItems, loadRemoteOptions, onSearchChange]);
71
225
 
72
226
  const close = () => {
73
227
  setIsOpen(false);
@@ -105,15 +259,18 @@ const DefaultSelect = ({
105
259
  }
106
260
  }
107
261
 
108
- const maybeLoad = onBeforeOpen?.();
109
- if (maybeLoad && typeof maybeLoad.then === 'function') {
110
- await maybeLoad;
111
- }
112
- } catch (error) {
113
- return;
114
- }
115
- setIsOpen(true);
116
- };
262
+ const maybeLoad = onBeforeOpen?.();
263
+ if (maybeLoad && typeof maybeLoad.then === 'function') {
264
+ applyLoadedItems(await maybeLoad);
265
+ } else {
266
+ applyLoadedItems(maybeLoad);
267
+ }
268
+ } catch (error) {
269
+ return;
270
+ }
271
+ setIsOpen(true);
272
+ loadRemoteOptions().catch(() => {});
273
+ };
117
274
 
118
275
  return (
119
276
  <View style={[inputStyles.wrap, containerStyle]}>
@@ -156,11 +313,11 @@ const DefaultSelect = ({
156
313
 
157
314
  <View style={styles.searchBox}>
158
315
  <TextInput
159
- style={[inputStyles.input, inputStyles.formInput]}
160
- value={searchText}
161
- placeholder={global.t?.t(storeName, 'input', column?.searchParam || 'search')}
162
- onChangeText={setSearchText}
163
- />
316
+ style={[inputStyles.input, inputStyles.formInput]}
317
+ value={searchText}
318
+ placeholder={global.t?.t(storeName, 'input', column?.searchParam || 'search')}
319
+ onChangeText={handleSearchChange}
320
+ />
164
321
  </View>
165
322
 
166
323
  <ScrollView keyboardShouldPersistTaps="handled">