@controleonline/ui-default 1.0.263 → 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 (61) hide show
  1. package/AGENTS.md +47 -45
  2. package/package.json +20 -20
  3. package/src/react/components/errors/DefaultErrors.js +307 -325
  4. package/src/react/components/files/DefaultFile.js +46 -46
  5. package/src/react/components/filters/CompactFilterSelector.js +78 -24
  6. package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
  7. package/src/react/components/filters/DefaultColumnFilter.js +191 -57
  8. package/src/react/components/filters/DefaultExternalFilters.js +245 -182
  9. package/src/react/components/filters/DefaultSearch.js +131 -92
  10. package/src/react/components/filters/DefaultSearch.styles.js +65 -70
  11. package/src/react/components/form/DefaultForm.js +172 -151
  12. package/src/react/components/form/DefaultForm.styles.js +71 -66
  13. package/src/react/components/help/DefaultTooltip.js +87 -87
  14. package/src/react/components/help/DefaultTooltip.styles.js +61 -61
  15. package/src/react/components/inputs/DefaultDateInput.js +150 -0
  16. package/src/react/components/inputs/DefaultInput.js +162 -127
  17. package/src/react/components/inputs/DefaultInput.styles.js +6 -0
  18. package/src/react/components/inputs/DefaultSelect.js +320 -152
  19. package/src/react/components/inputs/defaultInputUtils.js +40 -19
  20. package/src/react/components/map/DefaultGoogleMap.styles.js +115 -9
  21. package/src/react/components/map/DefaultGoogleMap.web.js +560 -446
  22. package/src/react/components/map/DefaultMap.native.js +73 -71
  23. package/src/react/components/map/DefaultMap.styles.js +16 -16
  24. package/src/react/components/map/DefaultMap.web.js +68 -66
  25. package/src/react/components/map/DefaultNativeMap.native.js +307 -183
  26. package/src/react/components/map/DefaultNativeMap.shared.js +439 -340
  27. package/src/react/components/map/DefaultNativeMap.styles.js +156 -122
  28. package/src/react/components/table/DefaultColumnMenu.js +108 -0
  29. package/src/react/components/table/DefaultDebug.js +233 -0
  30. package/src/react/components/table/DefaultFiltersModal.js +142 -0
  31. package/src/react/components/table/DefaultModalButton.js +18 -0
  32. package/src/react/components/table/DefaultSearchModal.js +49 -0
  33. package/src/react/components/table/DefaultTable.js +345 -1846
  34. package/src/react/components/table/DefaultTable.styles.js +247 -136
  35. package/src/react/components/table/DefaultTable.utils.js +502 -0
  36. package/src/react/components/table/DefaultTableBody.js +26 -0
  37. package/src/react/components/table/DefaultTableCards.js +171 -0
  38. package/src/react/components/table/DefaultTableControls.js +208 -0
  39. package/src/react/components/table/DefaultTableEmptyState.js +22 -0
  40. package/src/react/components/table/DefaultTableFooter.js +172 -0
  41. package/src/react/components/table/DefaultTableInput.js +145 -0
  42. package/src/react/components/table/DefaultTableRows.js +340 -0
  43. package/src/react/components/table/DefaultTableSearch.js +100 -0
  44. package/src/react/components/table/DefaultTableToolbar.js +98 -0
  45. package/src/react/components/table/DefaultToolbarAction.js +56 -0
  46. package/src/react/components/table/useDefaultTablePagination.js +370 -0
  47. package/src/react/components/table/useDefaultTableSorting.js +126 -0
  48. package/src/react/components/table/useDefaultTableTheme.js +58 -0
  49. package/src/react/index.js +10 -10
  50. package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
  51. package/src/store/default/actions.js +51 -13
  52. package/src/store/default/getters.js +10 -8
  53. package/src/store/default/mutation_types.js +4 -2
  54. package/src/store/default/mutations.js +10 -1
  55. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -162
  56. package/src/tests/react/components/files/DefaultFile.test.js +80 -80
  57. package/src/tests/react/components/form/DefaultForm.test.js +81 -0
  58. package/src/tests/react/components/inputs/DefaultSelect.test.js +185 -0
  59. package/src/tests/react/components/table/DefaultTable.test.js +1183 -0
  60. package/src/tests/react/store/defaultActions.test.js +59 -0
  61. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,46 +1,46 @@
1
- import React, {memo, useMemo} from 'react';
2
- import {Image} from 'react-native';
3
- import {useStore} from '@store';
4
- import {resolveDefaultFileSource} from '@controleonline/ui-common/src/react/utils/fileUrl';
5
-
6
- const resolveCompanyFromStore = peopleGetters => {
7
- if (peopleGetters?.defaultCompany?.id) {
8
- return peopleGetters.defaultCompany;
9
- }
10
-
11
- if (peopleGetters?.currentCompany?.id) {
12
- return peopleGetters.currentCompany;
13
- }
14
-
15
- return null;
16
- };
17
-
18
- const DefaultFile = ({
19
- file,
20
- source,
21
- company,
22
- appDomain = '',
23
- headers = {},
24
- ...imageProps
25
- }) => {
26
- const peopleStore = useStore('people');
27
- const peopleGetters = peopleStore?.getters || {};
28
- const resolvedCompany = company || resolveCompanyFromStore(peopleGetters);
29
- const resolvedSource = useMemo(
30
- () =>
31
- resolveDefaultFileSource(source ?? file, {
32
- company: resolvedCompany,
33
- appDomain,
34
- headers,
35
- }),
36
- [appDomain, company, file, headers, resolvedCompany, source],
37
- );
38
-
39
- if (!resolvedSource) {
40
- return null;
41
- }
42
-
43
- return <Image {...imageProps} source={resolvedSource} />;
44
- };
45
-
46
- export default memo(DefaultFile);
1
+ import React, {memo, useMemo} from 'react';
2
+ import {Image} from 'react-native';
3
+ import {useStore} from '@store';
4
+ import {resolveDefaultFileSource} from '@controleonline/ui-common/src/react/utils/fileUrl';
5
+
6
+ const resolveCompanyFromStore = peopleGetters => {
7
+ if (peopleGetters?.defaultCompany?.id) {
8
+ return peopleGetters.defaultCompany;
9
+ }
10
+
11
+ if (peopleGetters?.currentCompany?.id) {
12
+ return peopleGetters.currentCompany;
13
+ }
14
+
15
+ return null;
16
+ };
17
+
18
+ const DefaultFile = ({
19
+ file,
20
+ source,
21
+ company,
22
+ appDomain = '',
23
+ headers = {},
24
+ ...imageProps
25
+ }) => {
26
+ const peopleStore = useStore('people');
27
+ const peopleGetters = peopleStore?.getters || {};
28
+ const resolvedCompany = company || resolveCompanyFromStore(peopleGetters);
29
+ const resolvedSource = useMemo(
30
+ () =>
31
+ resolveDefaultFileSource(source ?? file, {
32
+ company: resolvedCompany,
33
+ appDomain,
34
+ headers,
35
+ }),
36
+ [appDomain, company, file, headers, resolvedCompany, source],
37
+ );
38
+
39
+ if (!resolvedSource) {
40
+ return null;
41
+ }
42
+
43
+ return <Image {...imageProps} source={resolvedSource} />;
44
+ };
45
+
46
+ export default memo(DefaultFile);
@@ -1,10 +1,11 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react';
2
2
  import {
3
- Modal,
4
- ScrollView,
5
- Text,
6
- TouchableOpacity,
7
- TouchableWithoutFeedback,
3
+ Modal,
4
+ ScrollView,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ TouchableWithoutFeedback,
8
9
  useWindowDimensions,
9
10
  View,
10
11
  } from 'react-native';
@@ -88,16 +89,20 @@ const CompactFilterSelector = ({
88
89
  icon = 'sliders',
89
90
  label = '',
90
91
  labelCaption = '',
91
- onClose = null,
92
- onSelect = null,
93
- options = [],
94
- selectedKey = '',
92
+ onClose = null,
93
+ onBeforeOpen = null,
94
+ onSearchChange = null,
95
+ onSelect = null,
96
+ options = [],
97
+ searchable = false,
98
+ selectedKey = '',
95
99
  field = '',
96
100
  store = '',
97
101
  themeColors = {},
98
102
  title = '',
99
103
  }) => {
100
- const [visible, setVisible] = useState(false);
104
+ const [visible, setVisible] = useState(false);
105
+ const [searchText, setSearchText] = useState('');
101
106
  const { width: windowWidth } = useWindowDimensions();
102
107
  const resolvedTheme = useMemo(
103
108
  () => buildTheme({ accentColor, themeColors }),
@@ -115,16 +120,56 @@ const CompactFilterSelector = ({
115
120
  const resolvedLabelCaption = labelCaption || storeFieldLabel;
116
121
  const resolvedTitle = title || storeFieldLabel;
117
122
 
118
- const closeModal = useCallback(() => {
119
- setVisible(false);
120
- onClose?.();
121
- }, [onClose]);
123
+ const closeModal = useCallback(() => {
124
+ setVisible(false);
125
+ setSearchText('');
126
+ onClose?.();
127
+ }, [onClose]);
122
128
 
123
- const openModal = useCallback(() => {
124
- if (!disabled) {
125
- setVisible(true);
129
+ const openModal = useCallback(() => {
130
+ if (disabled) {
131
+ return;
126
132
  }
127
- }, [disabled]);
133
+
134
+ try {
135
+ const maybePromise = onBeforeOpen?.();
136
+ if (maybePromise && typeof maybePromise.then === 'function') {
137
+ maybePromise
138
+ .then(() => {
139
+ setVisible(true);
140
+ })
141
+ .catch(() => {
142
+ // Keep the modal closed if the lazy load fails.
143
+ });
144
+ return;
145
+ }
146
+ } catch (error) {
147
+ return;
148
+ }
149
+
150
+ setVisible(true);
151
+ }, [disabled, onBeforeOpen]);
152
+
153
+ /*
154
+ * @agents
155
+ * List filters must keep the column/store contract in the default component:
156
+ * open loads the initial page, typing delegates autocomplete to the caller
157
+ * that knows the column list store/action.
158
+ */
159
+ const handleSearchChange = useCallback(value => {
160
+ setSearchText(value);
161
+ onSearchChange?.(value);
162
+ }, [onSearchChange]);
163
+
164
+ const filteredOptions = useMemo(() => {
165
+ const normalizedSearch = normalizeText(searchText).toLowerCase();
166
+ if (!searchable || onSearchChange || !normalizedSearch) return options;
167
+
168
+ return options.filter(option =>
169
+ normalizeText(option?.label).toLowerCase().includes(normalizedSearch) ||
170
+ normalizeText(option?.key).toLowerCase().includes(normalizedSearch),
171
+ );
172
+ }, [onSearchChange, options, searchable, searchText]);
128
173
 
129
174
  const handleSelect = useCallback(optionKey => {
130
175
  const shouldClose = onSelect?.(optionKey, {
@@ -210,12 +255,21 @@ const CompactFilterSelector = ({
210
255
  </TouchableOpacity>
211
256
  </View>
212
257
 
213
- <ScrollView
214
- style={styles.modalScroll}
215
- contentContainerStyle={styles.modalContent}
216
- keyboardShouldPersistTaps="handled"
217
- >
218
- {options.map(option => {
258
+ <ScrollView
259
+ style={styles.modalScroll}
260
+ contentContainerStyle={styles.modalContent}
261
+ keyboardShouldPersistTaps="handled"
262
+ >
263
+ {searchable ? (
264
+ <TextInput
265
+ style={styles.searchInput}
266
+ value={searchText}
267
+ placeholder={global.t?.t(resolveStoreName(store), 'input', 'search')}
268
+ onChangeText={handleSearchChange}
269
+ />
270
+ ) : null}
271
+
272
+ {filteredOptions.map(option => {
219
273
  const isSelected = option.key === selectedKey;
220
274
 
221
275
  return (
@@ -88,11 +88,23 @@ const createStyles = (theme, dense = false, windowWidth = 0) => {
88
88
  modalScroll: {
89
89
  flexGrow: 0,
90
90
  },
91
- modalContent: {
92
- paddingBottom: 10,
93
- gap: 10,
94
- },
95
- modalOption: {
91
+ modalContent: {
92
+ paddingBottom: 10,
93
+ gap: 10,
94
+ },
95
+ searchInput: {
96
+ minHeight: 42,
97
+ borderWidth: 1,
98
+ borderColor: theme.optionBorderColor,
99
+ borderRadius: 14,
100
+ paddingHorizontal: 12,
101
+ paddingVertical: 8,
102
+ backgroundColor: theme.backgroundColor,
103
+ color: theme.textColor,
104
+ fontSize: 13,
105
+ fontWeight: '700',
106
+ },
107
+ modalOption: {
96
108
  minHeight: 50,
97
109
  borderWidth: 1,
98
110
  borderColor: theme.optionBorderColor,
@@ -1,61 +1,192 @@
1
- import React from 'react';
2
- import { TextInput, View } from 'react-native';
3
- import {
4
- buildOptionsFromColumn,
5
- getColumnKey,
6
- normalizeOptionKey,
7
- normalizeText,
8
- } from '../inputs/defaultInputUtils';
9
- import CompactFilterSelector from './CompactFilterSelector';
10
- import DateShortcutFilter from './DateShortcutFilter';
11
- import { resolveNextDateFilterValue } from './dateFilterSelection';
12
- import styles from './DefaultColumnFilter.styles';
1
+ import React, { useCallback, useState } from 'react';
2
+ import { TextInput, View } from 'react-native';
3
+ import { getAllStores, useStore } from '@store';
4
+ import {
5
+ buildOptionsFromColumn,
6
+ getColumnKey,
7
+ isDateLikeColumn,
8
+ mapOptions,
9
+ normalizeOptionKey,
10
+ normalizeText,
11
+ resolveStoreNameFromList,
12
+ } from '../inputs/defaultInputUtils';
13
+ import CompactFilterSelector from './CompactFilterSelector';
14
+ import DateShortcutFilter from './DateShortcutFilter';
15
+ import { resolveNextDateFilterValue } from './dateFilterSelection';
16
+ import {
17
+ normalizeCollectionItems,
18
+ resolveColumnListLoadParams,
19
+ resolveListActionName,
20
+ stableSerialize,
21
+ } from '../table/DefaultTable.utils';
22
+ import {
23
+ persistTableFiltersPreference,
24
+ resolveDefaultTablePreferenceScope,
25
+ sanitizeTableFiltersPreference,
26
+ } from '../../utils/tableVisibleColumnsPreferences';
27
+ import styles from './DefaultColumnFilter.styles';
13
28
 
14
29
  const DefaultColumnFilter = ({
15
30
  accentColor = '#2563EB',
16
- column,
17
- filters = {},
18
- getOptionsForColumn = null,
19
- onChange = null,
20
- storeName = '',
31
+ column,
32
+ filters = {},
33
+ onBeforeOpen = null,
34
+ onSearchChange = null,
35
+ getOptionsForColumn = null,
36
+ onChange = null,
37
+ storeName = '',
21
38
  style = null,
22
- }) => {
23
- const fieldName = getColumnKey(column);
39
+ }) => {
40
+ const store = useStore(storeName);
41
+ const peopleStore = useStore('people');
42
+ const configs = store?.getters?.configs || {};
43
+ const [loadedItems, setLoadedItems] = useState([]);
44
+ const [loadedKey, setLoadedKey] = useState('');
45
+ const fieldName = getColumnKey(column);
46
+ const currentCompanyId = peopleStore?.getters?.currentCompany?.id;
47
+ const requestChange = useCallback(
48
+ (nextFieldName, value) => {
49
+ if (typeof onChange === 'function') {
50
+ onChange(nextFieldName, value);
51
+ return;
52
+ }
53
+
54
+ const currentFilters = store?.getters?.filters || {};
55
+ const nextFilters = { ...(currentFilters || {}) };
56
+ const isEmpty =
57
+ value === null ||
58
+ value === undefined ||
59
+ normalizeText(value) === '' ||
60
+ (Array.isArray(value) && value.length === 0);
61
+
62
+ if (isEmpty) delete nextFilters[nextFieldName];
63
+ else nextFilters[nextFieldName] = value;
64
+
65
+ const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
66
+ const tablePreferenceScope =
67
+ configs.tablePreferenceScope ||
68
+ resolveDefaultTablePreferenceScope({ storeName });
69
+ const persistedFilters = sanitizeTableFiltersPreference({
70
+ columns,
71
+ filters: nextFilters,
72
+ });
73
+
74
+ persistTableFiltersPreference(tablePreferenceScope, persistedFilters);
75
+
76
+ if (typeof store?.actions?.setFilters === 'function') {
77
+ store.actions.setFilters(nextFilters);
78
+ } else if (store?.getters) {
79
+ store.getters.filters = nextFilters;
80
+ }
81
+
82
+ configs.onFilterChange?.(nextFilters);
83
+ },
84
+ [configs, onChange, store, storeName],
85
+ );
86
+ const resolveOptions = useCallback(
87
+ targetColumn => {
88
+ const configOptions = configs.getOptionsForColumn?.(targetColumn);
89
+ const propOptions = getOptionsForColumn?.(targetColumn);
90
+
91
+ if (Array.isArray(configOptions)) return configOptions;
92
+ if (Array.isArray(propOptions)) return propOptions;
93
+ if (targetColumn === column && loadedItems.length > 0) return loadedItems;
94
+
95
+ return undefined;
96
+ },
97
+ [column, configs, getOptionsForColumn, loadedItems],
98
+ );
99
+ const loadListOptions = useCallback(
100
+ (searchValue = '') => {
101
+ const listStoreName = resolveStoreNameFromList(column?.list);
102
+ const actionName = resolveListActionName(column?.list);
103
+ const listStore = getAllStores?.()?.[listStoreName];
104
+ const listAction = listStore?.actions?.[actionName];
105
+
106
+ if (!listStoreName || typeof listAction !== 'function') {
107
+ return Promise.resolve([]);
108
+ }
109
+
110
+ const listLoadParams = resolveColumnListLoadParams({
111
+ column,
112
+ currentCompanyId,
113
+ requestParams: configs.requestParams || {},
114
+ searchValue,
115
+ });
116
+ const nextLoadedKey = stableSerialize({
117
+ actionName,
118
+ listLoadParams,
119
+ listStoreName,
120
+ });
121
+
122
+ if (!normalizeText(searchValue) && loadedKey === nextLoadedKey && loadedItems.length > 0) {
123
+ return Promise.resolve(loadedItems);
124
+ }
125
+
126
+ return Promise.resolve(
127
+ listAction({
128
+ ...listLoadParams,
129
+ __storeMeta: {
130
+ dedupeKey: `default-column-filter-list:${fieldName}:${nextLoadedKey}`,
131
+ skipSystemError: true,
132
+ },
133
+ }),
134
+ )
135
+ .then(response => {
136
+ const items = normalizeCollectionItems(response);
137
+ setLoadedItems(items);
138
+ setLoadedKey(nextLoadedKey);
139
+ return items;
140
+ })
141
+ .catch(() => []);
142
+ },
143
+ [
144
+ column,
145
+ configs.requestParams,
146
+ currentCompanyId,
147
+ fieldName,
148
+ loadedItems,
149
+ loadedKey,
150
+ ],
151
+ );
24
152
 
25
153
  if (column?.filter === false || column?.filters === false) {
26
154
  return <View style={style} />;
27
155
  }
28
156
 
29
- if (column?.inputType === 'date-range' || column?.type === 'range-date') {
157
+ if (isDateLikeColumn(column)) {
30
158
  const filterValue = filters?.[fieldName] || {};
31
159
  return (
32
160
  <View style={[style, styles.filterCell]}>
33
- <DateShortcutFilter
34
- dense
35
- store={storeName}
36
- field={fieldName}
37
- value={filterValue.shortcut || 'all'}
161
+ <DateShortcutFilter
162
+ dense
163
+ store={storeName}
164
+ field={fieldName}
165
+ labelCaption={global.t?.t(storeName, 'label', column?.label || fieldName)}
166
+ value={filterValue.shortcut || 'all'}
38
167
  customRange={filterValue.customRange || { from: '', to: '' }}
39
- onChange={optionKey =>
40
- onChange?.(
41
- fieldName,
42
- resolveNextDateFilterValue(filterValue, optionKey),
43
- )
44
- }
45
- onCustomRangeChange={range => onChange?.(fieldName, {
46
- ...(filterValue || {}),
47
- shortcut: 'custom',
48
- customRange: range,
168
+ onChange={optionKey =>
169
+ requestChange(
170
+ fieldName,
171
+ resolveNextDateFilterValue(filterValue, optionKey),
172
+ )
173
+ }
174
+ onCustomRangeChange={range => requestChange(fieldName, {
175
+ ...(filterValue || {}),
176
+ shortcut: 'custom',
177
+ customRange: range,
49
178
  })}
50
179
  />
51
180
  </View>
52
181
  );
53
182
  }
54
183
 
55
- if (column?.list) {
56
- const rawOptions = buildOptionsFromColumn(column, getOptionsForColumn, storeName);
184
+ if (column?.list) {
185
+ const rawOptions = loadedItems.length > 0
186
+ ? mapOptions(column, loadedItems, storeName)
187
+ : buildOptionsFromColumn(column, resolveOptions, storeName);
57
188
  const options = [
58
- { key: '', label: global.t?.t(storeName, 'label', 'select') || 'Selecionar' },
189
+ { key: '', label: global.t?.t(storeName, 'label', 'select') },
59
190
  ...rawOptions,
60
191
  ];
61
192
  const selectedKey = normalizeOptionKey(filters?.[fieldName]);
@@ -63,20 +194,23 @@ const DefaultColumnFilter = ({
63
194
 
64
195
  return (
65
196
  <View style={[style, styles.filterCell]}>
66
- <CompactFilterSelector
67
- dense
68
- store={storeName}
69
- field={fieldName}
70
- icon="filter"
71
- accentColor={accentColor}
72
- active={Boolean(selectedKey)}
73
- label={selected?.label || options[0]?.label || ''}
74
- options={options}
75
- selectedKey={selectedKey}
76
- onSelect={optionKey => {
77
- onChange?.(fieldName, optionKey);
78
- return true;
79
- }}
197
+ <CompactFilterSelector
198
+ dense
199
+ store={storeName}
200
+ field={fieldName}
201
+ icon="filter"
202
+ onBeforeOpen={onBeforeOpen || (() => loadListOptions())}
203
+ onSearchChange={onSearchChange || (value => loadListOptions(value))}
204
+ accentColor={accentColor}
205
+ active={Boolean(selectedKey)}
206
+ label={selected?.label || options[0]?.label || ''}
207
+ options={options}
208
+ searchable
209
+ selectedKey={selectedKey}
210
+ onSelect={optionKey => {
211
+ requestChange(fieldName, optionKey);
212
+ return true;
213
+ }}
80
214
  />
81
215
  </View>
82
216
  );
@@ -85,11 +219,11 @@ const DefaultColumnFilter = ({
85
219
  return (
86
220
  <View style={[style, styles.filterCell]}>
87
221
  <TextInput
88
- style={styles.filterInput}
89
- value={normalizeText(filters?.[fieldName])}
90
- placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
91
- onChangeText={value => onChange?.(fieldName, value)}
92
- />
222
+ style={styles.filterInput}
223
+ value={normalizeText(filters?.[fieldName])}
224
+ placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
225
+ onChangeText={value => requestChange(fieldName, value)}
226
+ />
93
227
  </View>
94
228
  );
95
229
  };