@controleonline/ui-default 1.0.270 → 1.0.272

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 (89) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultMap.native.js +73 -73
  39. package/src/react/components/map/DefaultMap.shared.js +911 -762
  40. package/src/react/components/map/DefaultMap.web.js +124 -68
  41. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  42. package/src/react/components/table/DefaultDebug.js +44 -37
  43. package/src/react/components/table/DefaultSearchModal.js +9 -1
  44. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  45. package/src/react/components/table/DefaultTable.js +213 -107
  46. package/src/react/components/table/DefaultTable.styles.js +367 -314
  47. package/src/react/components/table/DefaultTableBody.js +13 -8
  48. package/src/react/components/table/DefaultTableCards.js +20 -1
  49. package/src/react/components/table/DefaultTableControls.js +180 -205
  50. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  51. package/src/react/components/table/DefaultTableInput.js +1 -0
  52. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  53. package/src/react/components/table/DefaultTableRows.js +224 -363
  54. package/src/react/components/table/DefaultTableSearch.js +28 -5
  55. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  56. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  57. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  58. package/src/react/components/upload/DefaultUpload.js +436 -0
  59. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  60. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  61. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  62. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  63. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  64. package/src/react/components/upload/fileUpload.js +93 -0
  65. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  66. package/src/react/index.js +6 -0
  67. package/src/react/services/addressFormUtils.js +196 -0
  68. package/src/react/services/addressGeo.js +227 -0
  69. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  70. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  71. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  72. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  73. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  74. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  75. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  76. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  77. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  78. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  79. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  80. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  81. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  82. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  83. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  84. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  85. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  86. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  87. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  88. package/src/tests/react/services/smoke-637/index.html +132 -0
  89. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -0,0 +1,104 @@
1
+ import {extractFileId, resolveDefaultFileUrl} from '@controleonline/ui-common/src/react/utils/fileUrl';
2
+
3
+ const FILE_INPUT_TYPES = new Set([
4
+ 'document',
5
+ 'documents',
6
+ 'file',
7
+ 'files',
8
+ 'image',
9
+ 'images',
10
+ 'pdf',
11
+ ]);
12
+
13
+ const IMAGE_EXTENSIONS = new Set(['avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp']);
14
+ const PDF_EXTENSIONS = new Set(['pdf']);
15
+ const DOCUMENT_EXTENSIONS = new Set(['doc', 'docx', 'odt', 'rtf', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp']);
16
+
17
+ export const normalizeFileColumnText = value => String(value ?? '').trim();
18
+
19
+ export const normalizeFileColumnType = value =>
20
+ normalizeFileColumnText(value).replace(/[^a-z0-9]/gi, '').toLowerCase();
21
+
22
+ export const isFileInputColumn = column =>
23
+ FILE_INPUT_TYPES.has(normalizeFileColumnType(column?.inputType));
24
+
25
+ const asArray = value => {
26
+ if (Array.isArray(value)) return value;
27
+ if (value === undefined || value === null || value === '') return [];
28
+ if (typeof value === 'string' && value.includes(',')) {
29
+ return value
30
+ .split(',')
31
+ .map(item => item.trim())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ return [value];
36
+ };
37
+
38
+ const pickFilePayload = item =>
39
+ item && typeof item === 'object' && !Array.isArray(item) && item.file
40
+ ? item.file
41
+ : item;
42
+
43
+ const resolveForcedFileType = column => {
44
+ const inputType = normalizeFileColumnType(column?.inputType);
45
+ if (inputType === 'image' || inputType === 'images') return 'image';
46
+ if (inputType === 'pdf') return 'application';
47
+ if (inputType === 'document' || inputType === 'documents') return 'document';
48
+ return normalizeFileColumnText(column?.fileType);
49
+ };
50
+
51
+ const resolveForcedExtension = column => {
52
+ const inputType = normalizeFileColumnType(column?.inputType);
53
+ if (inputType === 'pdf') return 'pdf';
54
+ return normalizeFileColumnText(column?.extension).toLowerCase();
55
+ };
56
+
57
+ const resolveKind = ({fileType, extension}) => {
58
+ const normalizedType = normalizeFileColumnType(fileType);
59
+ const normalizedExtension = normalizeFileColumnText(extension).replace(/^\./, '').toLowerCase();
60
+
61
+ if (normalizedType === 'image' || IMAGE_EXTENSIONS.has(normalizedExtension)) return 'image';
62
+ if (PDF_EXTENSIONS.has(normalizedExtension)) return 'pdf';
63
+ if (DOCUMENT_EXTENSIONS.has(normalizedExtension) || normalizedType === 'document') return 'document';
64
+ if (normalizedType === 'text') return 'text';
65
+ if (normalizedType === 'audio') return 'audio';
66
+ if (normalizedType === 'video') return 'video';
67
+ return 'file';
68
+ };
69
+
70
+ export const normalizeDefaultFileColumnItems = ({
71
+ column = {},
72
+ company = null,
73
+ row = {},
74
+ value,
75
+ } = {}) =>
76
+ asArray(value ?? row?.[column?.key || column?.name]).map((item, index) => {
77
+ const payload = pickFilePayload(item);
78
+ const isObject = payload && typeof payload === 'object' && !Array.isArray(payload);
79
+ const id = extractFileId(payload);
80
+ const fileType = isObject
81
+ ? normalizeFileColumnText(payload.fileType) || resolveForcedFileType(column)
82
+ : resolveForcedFileType(column);
83
+ const extension = isObject
84
+ ? normalizeFileColumnText(payload.extension).toLowerCase() || resolveForcedExtension(column)
85
+ : resolveForcedExtension(column);
86
+ const url = isObject && typeof payload.url === 'string'
87
+ ? payload.url
88
+ : resolveDefaultFileUrl(payload, {company});
89
+ const driveUrl = isObject && typeof payload.driveUrl === 'string'
90
+ ? payload.driveUrl
91
+ : '';
92
+
93
+ return {
94
+ extension,
95
+ fileName: isObject ? normalizeFileColumnText(payload.fileName) : '',
96
+ fileType,
97
+ id,
98
+ key: `${id || normalizeFileColumnText(payload) || index}`,
99
+ kind: resolveKind({fileType, extension}),
100
+ openUrl: driveUrl || url,
101
+ relationId: item && typeof item === 'object' && !Array.isArray(item) ? extractFileId(item) : null,
102
+ source: payload,
103
+ };
104
+ });
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
2
  import {
3
3
  Image,
4
4
  Modal,
@@ -7,10 +7,11 @@ import {
7
7
  TextInput,
8
8
  TouchableOpacity,
9
9
  TouchableWithoutFeedback,
10
- useWindowDimensions,
11
- View,
12
- } from 'react-native';
13
- import Icon from 'react-native-vector-icons/Feather';
10
+ useWindowDimensions,
11
+ View,
12
+ } from 'react-native';
13
+ import { useStore } from '@store';
14
+ import Icon from 'react-native-vector-icons/Feather';
14
15
  import {
15
16
  formatStoreColumnLabel,
16
17
  resolveStoreColumn,
@@ -19,16 +20,16 @@ import {
19
20
  import { isValidFeatherIcon } from '../inputs/defaultInputUtils';
20
21
  import createStyles from './CompactFilterSelector.styles';
21
22
 
22
- const buildTheme = ({ accentColor, themeColors = {} }) => ({
23
- accentColor: accentColor,
24
- activeBackgroundColor:
25
- themeColors.activeBackgroundColor || `${accentColor}14`,
26
- activeIconBackgroundColor:
27
- themeColors.activeIconBackgroundColor || `${accentColor}24`,
28
- activeChevronColor:
29
- themeColors.activeChevronColor ||
30
- themeColors.activeIconColor ||
31
- accentColor,
23
+ const buildTheme = ({ accentColor, themeColors = {} }) => ({
24
+ accentColor: accentColor,
25
+ activeBackgroundColor:
26
+ themeColors.activeBackgroundColor || (accentColor ? `${accentColor}14` : themeColors.backgroundColor),
27
+ activeIconBackgroundColor:
28
+ themeColors.activeIconBackgroundColor || (accentColor ? `${accentColor}24` : themeColors.iconBackgroundColor),
29
+ activeChevronColor:
30
+ themeColors.activeChevronColor ||
31
+ themeColors.activeIconColor ||
32
+ accentColor,
32
33
  activeIconColor: themeColors.activeIconColor || accentColor,
33
34
  activeTextColor: themeColors.activeTextColor || accentColor,
34
35
  backgroundColor: themeColors.backgroundColor || '#F8FAFC',
@@ -44,10 +45,10 @@ const buildTheme = ({ accentColor, themeColors = {} }) => ({
44
45
  modalTitleColor: themeColors.modalTitleColor || '#0F172A',
45
46
  optionBackgroundColor: themeColors.optionBackgroundColor || '#F8FAFC',
46
47
  optionBorderColor: themeColors.optionBorderColor || '#E2E8F0',
47
- optionSelectedTextColor:
48
- themeColors.optionSelectedTextColor || accentColor,
49
- textColor: themeColors.textColor || '#0F172A',
50
- });
48
+ optionSelectedTextColor:
49
+ themeColors.optionSelectedTextColor || accentColor,
50
+ textColor: themeColors.textColor || '#0F172A',
51
+ });
51
52
 
52
53
  const noop = () => {};
53
54
 
@@ -82,12 +83,12 @@ const resolveStoreFieldLabel = ({ fallbackLabel = '', field = '', store = '' })
82
83
  );
83
84
  };
84
85
 
85
- const CompactFilterSelector = ({
86
- accentColor = '#2563EB',
87
- active = false,
88
- children = null,
89
- dense = false,
90
- disabled = false,
86
+ const CompactFilterSelector = ({
87
+ accentColor = null,
88
+ active = false,
89
+ children = null,
90
+ dense = false,
91
+ disabled = false,
91
92
  icon = 'sliders',
92
93
  label = '',
93
94
  labelCaption = '',
@@ -98,18 +99,33 @@ const CompactFilterSelector = ({
98
99
  options = [],
99
100
  searchable = false,
100
101
  selectedKey = '',
101
- field = '',
102
- store = '',
103
- themeColors = {},
104
- title = '',
105
- }) => {
102
+ field = '',
103
+ store = '',
104
+ themeColors = {},
105
+ title = '',
106
+ }) => {
107
+ const themeStore = useStore('theme');
106
108
  const [visible, setVisible] = useState(false);
107
109
  const [searchText, setSearchText] = useState('');
108
- const { width: windowWidth } = useWindowDimensions();
109
- const resolvedTheme = useMemo(
110
- () => buildTheme({ accentColor, themeColors }),
111
- [accentColor, themeColors],
112
- );
110
+ const { width: windowWidth } = useWindowDimensions();
111
+ const themeStoreColors = themeStore?.getters?.colors || {};
112
+ const resolvedAccentColor =
113
+ accentColor ||
114
+ themeColors.primary ||
115
+ themeColors.accent ||
116
+ themeStoreColors.primary ||
117
+ themeStoreColors.accent ||
118
+ '';
119
+ const resolvedTheme = useMemo(
120
+ () => buildTheme({
121
+ accentColor: resolvedAccentColor,
122
+ themeColors: {
123
+ ...themeStoreColors,
124
+ ...themeColors,
125
+ },
126
+ }),
127
+ [resolvedAccentColor, themeColors, themeStoreColors],
128
+ );
113
129
  const styles = useMemo(
114
130
  () => createStyles(resolvedTheme, dense, windowWidth),
115
131
  [dense, resolvedTheme, windowWidth],
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useState } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { TextInput, View } from 'react-native';
3
3
  import { getAllStores, useStore } from '@store';
4
4
  import {
@@ -13,6 +13,7 @@ import {
13
13
  import CompactFilterSelector from './CompactFilterSelector';
14
14
  import DateShortcutFilter from './DateShortcutFilter';
15
15
  import { resolveNextDateFilterValue } from './dateFilterSelection';
16
+ import useDefaultTableTheme from '@controleonline/ui-default/src/react/components/table/useDefaultTableTheme';
16
17
  import {
17
18
  normalizeCollectionItems,
18
19
  resolveColumnListLoadParams,
@@ -25,9 +26,9 @@ import {
25
26
  sanitizeTableFiltersPreference,
26
27
  } from '../../utils/tableVisibleColumnsPreferences';
27
28
  import styles from './DefaultColumnFilter.styles';
28
-
29
- const DefaultColumnFilter = ({
30
- accentColor = '#2563EB',
29
+
30
+ const DefaultColumnFilter = ({
31
+ accentColor = null,
31
32
  column,
32
33
  filters = {},
33
34
  onBeforeOpen = null,
@@ -35,15 +36,22 @@ const DefaultColumnFilter = ({
35
36
  getOptionsForColumn = null,
36
37
  onChange = null,
37
38
  storeName = '',
38
- style = null,
39
+ style = null,
39
40
  }) => {
40
41
  const store = useStore(storeName);
41
42
  const peopleStore = useStore('people');
42
43
  const configs = store?.getters?.configs || {};
44
+ const { resolvedAccentColor } = useDefaultTableTheme(accentColor);
43
45
  const [loadedItems, setLoadedItems] = useState([]);
44
46
  const [loadedKey, setLoadedKey] = useState('');
45
47
  const fieldName = getColumnKey(column);
46
48
  const currentCompanyId = peopleStore?.getters?.currentCompany?.id;
49
+
50
+ useEffect(() => {
51
+ setLoadedItems([]);
52
+ setLoadedKey('');
53
+ }, [currentCompanyId]);
54
+
47
55
  const requestChange = useCallback(
48
56
  (nextFieldName, value) => {
49
57
  if (typeof onChange === 'function') {
@@ -65,7 +73,10 @@ const DefaultColumnFilter = ({
65
73
  const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
66
74
  const tablePreferenceScope =
67
75
  configs.tablePreferenceScope ||
68
- resolveDefaultTablePreferenceScope({ storeName });
76
+ resolveDefaultTablePreferenceScope({
77
+ companyId: currentCompanyId,
78
+ storeName,
79
+ });
69
80
  const persistedFilters = sanitizeTableFiltersPreference({
70
81
  columns,
71
82
  filters: nextFilters,
@@ -81,7 +92,7 @@ const DefaultColumnFilter = ({
81
92
 
82
93
  configs.onFilterChange?.(nextFilters);
83
94
  },
84
- [configs, onChange, store, storeName],
95
+ [configs, currentCompanyId, onChange, store, storeName],
85
96
  );
86
97
  const resolveOptions = useCallback(
87
98
  targetColumn => {
@@ -149,11 +160,11 @@ const DefaultColumnFilter = ({
149
160
  loadedKey,
150
161
  ],
151
162
  );
152
-
153
- if (column?.filter === false || column?.filters === false) {
154
- return <View style={style} />;
155
- }
156
-
163
+
164
+ if (column?.filter === false || column?.filters === false) {
165
+ return <View style={style} />;
166
+ }
167
+
157
168
  if (isDateLikeColumn(column)) {
158
169
  const filterValue = filters?.[fieldName] || {};
159
170
  return (
@@ -164,7 +175,7 @@ const DefaultColumnFilter = ({
164
175
  field={fieldName}
165
176
  labelCaption={global.t?.t(storeName, 'label', column?.label || fieldName)}
166
177
  value={filterValue.shortcut || 'all'}
167
- customRange={filterValue.customRange || { from: '', to: '' }}
178
+ customRange={filterValue.customRange || { from: '', to: '' }}
168
179
  onChange={optionKey =>
169
180
  requestChange(
170
181
  fieldName,
@@ -175,25 +186,31 @@ const DefaultColumnFilter = ({
175
186
  ...(filterValue || {}),
176
187
  shortcut: 'custom',
177
188
  customRange: range,
178
- })}
179
- />
180
- </View>
181
- );
182
- }
183
-
189
+ })}
190
+ />
191
+ </View>
192
+ );
193
+ }
194
+
184
195
  if (column?.list) {
185
- const rawOptions = loadedItems.length > 0
186
- ? mapOptions(column, loadedItems, storeName)
187
- : buildOptionsFromColumn(column, resolveOptions, storeName);
196
+ /*
197
+ * @agents List options for filters must load only when the modal opens
198
+ * (onBeforeOpen). Falling back to the shared store items would show
199
+ * another company's payment types / wallets / categories.
200
+ */
201
+ const explicitOptions = resolveOptions(column);
202
+ const rawOptions = Array.isArray(explicitOptions)
203
+ ? mapOptions(column, explicitOptions, storeName)
204
+ : mapOptions(column, loadedItems, storeName);
188
205
  const options = [
189
206
  { key: '', label: global.t?.t(storeName, 'label', 'select') },
190
207
  ...rawOptions,
191
208
  ];
192
- const selectedKey = normalizeOptionKey(filters?.[fieldName]);
193
- const selected = options.find(option => option.key === selectedKey);
194
-
195
- return (
196
- <View style={[style, styles.filterCell]}>
209
+ const selectedKey = normalizeOptionKey(filters?.[fieldName]);
210
+ const selected = options.find(option => option.key === selectedKey);
211
+
212
+ return (
213
+ <View style={[style, styles.filterCell]}>
197
214
  <CompactFilterSelector
198
215
  dense
199
216
  store={storeName}
@@ -201,7 +218,7 @@ const DefaultColumnFilter = ({
201
218
  icon="filter"
202
219
  onBeforeOpen={onBeforeOpen || (() => loadListOptions())}
203
220
  onSearchChange={onSearchChange || (value => loadListOptions(value))}
204
- accentColor={accentColor}
221
+ accentColor={resolvedAccentColor}
205
222
  active={Boolean(selectedKey)}
206
223
  label={selected?.label || options[0]?.label || ''}
207
224
  options={options}
@@ -211,21 +228,21 @@ const DefaultColumnFilter = ({
211
228
  requestChange(fieldName, optionKey);
212
229
  return true;
213
230
  }}
214
- />
215
- </View>
216
- );
217
- }
218
-
219
- return (
220
- <View style={[style, styles.filterCell]}>
221
- <TextInput
231
+ />
232
+ </View>
233
+ );
234
+ }
235
+
236
+ return (
237
+ <View style={[style, styles.filterCell]}>
238
+ <TextInput
222
239
  style={styles.filterInput}
223
240
  value={normalizeText(filters?.[fieldName])}
224
241
  placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
225
242
  onChangeText={value => requestChange(fieldName, value)}
226
243
  />
227
- </View>
228
- );
229
- };
230
-
231
- export default DefaultColumnFilter;
244
+ </View>
245
+ );
246
+ };
247
+
248
+ export default DefaultColumnFilter;