@controleonline/ui-default 1.0.270 → 1.0.273

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 (100) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/.github/workflows/publish-npm.yml +91 -0
  4. package/AGENTS.md +46 -50
  5. package/docs/technical/Contrato-de-colunas.md +67 -0
  6. package/docs/technical/Default-Components.md +76 -0
  7. package/docs/technical/DefaultForm.md +80 -0
  8. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  9. package/docs/technical/DefaultTable.md +272 -0
  10. package/docs/wiki.md +1 -0
  11. package/package.json +4 -3
  12. package/scripts/validate-wiki-pointer.cjs +13 -0
  13. package/src/react/components/address/DefaultAddress.js +446 -0
  14. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  15. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  16. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  17. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  18. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  19. package/src/react/components/address/defaultAddressStyles.js +148 -0
  20. package/src/react/components/address/defaultAddressUtils.js +129 -0
  21. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  22. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  23. package/src/react/components/files/DefaultFile.js +231 -46
  24. package/src/react/components/files/DefaultFileColumn.js +123 -0
  25. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  26. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  27. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  28. package/src/react/components/filters/DefaultColumnFilter.js +60 -48
  29. package/src/react/components/filters/DefaultExternalFilters.js +324 -265
  30. package/src/react/components/filters/filterValue.js +65 -0
  31. package/src/react/components/form/DefaultForm.js +99 -79
  32. package/src/react/components/help/DefaultTooltip.js +48 -12
  33. package/src/react/components/help/DefaultTooltip.styles.js +20 -0
  34. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  35. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  36. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  37. package/src/react/components/inputs/DefaultInput.js +296 -141
  38. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  39. package/src/react/components/inputs/DefaultSelect.js +134 -123
  40. package/src/react/components/inputs/defaultInputUtils.js +262 -213
  41. package/src/react/components/map/DefaultMap.native.js +73 -73
  42. package/src/react/components/map/DefaultMap.shared.js +911 -762
  43. package/src/react/components/map/DefaultMap.web.js +124 -68
  44. package/src/react/components/table/DefaultColumnMenu.js +22 -3
  45. package/src/react/components/table/DefaultDebug.js +44 -37
  46. package/src/react/components/table/DefaultSearchModal.js +9 -1
  47. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  48. package/src/react/components/table/DefaultTable.js +217 -111
  49. package/src/react/components/table/DefaultTable.styles.js +369 -310
  50. package/src/react/components/table/DefaultTable.utils.js +38 -4
  51. package/src/react/components/table/DefaultTableBody.js +13 -8
  52. package/src/react/components/table/DefaultTableCards.js +20 -1
  53. package/src/react/components/table/DefaultTableControls.js +180 -205
  54. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  55. package/src/react/components/table/DefaultTableInput.js +1 -0
  56. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  57. package/src/react/components/table/DefaultTableRows.js +357 -325
  58. package/src/react/components/table/DefaultTableSearch.js +28 -5
  59. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  60. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  61. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  62. package/src/react/components/upload/DefaultUpload.js +406 -0
  63. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  64. package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
  65. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  66. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  67. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  68. package/src/react/components/upload/fileUpload.js +93 -0
  69. package/src/react/hooks/usePostalCodeLookup.js +159 -0
  70. package/src/react/index.js +6 -0
  71. package/src/react/services/addressFormUtils.js +262 -0
  72. package/src/react/services/addressGeo.js +230 -0
  73. package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
  74. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  75. package/src/tests/react/components/files/DefaultFile.test.js +22 -6
  76. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  77. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  78. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  79. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  80. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  81. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  82. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  83. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  84. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  85. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  86. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  87. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  88. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  89. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
  90. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  91. package/src/tests/react/filters/filterValue.test.js +46 -0
  92. package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
  93. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  94. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  95. package/src/tests/react/services/addressFormUtils.test.js +179 -0
  96. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  97. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  98. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  99. package/src/tests/react/services/smoke-637/index.html +132 -0
  100. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
@@ -3,12 +3,45 @@ export const TABLE_VIEW_MODE_PREFERENCES_KEY = 'viewMode';
3
3
  export const TABLE_SORT_PREFERENCES_KEY = 'sort';
4
4
  export const TABLE_FILTER_PREFERENCES_KEY = 'filters';
5
5
  export const DEFAULT_TABLE_PREFERENCES_STORAGE_KEY = 'default-table';
6
+ export const REQUIRED_VISIBLE_COLUMN_KEYS = ['id'];
6
7
 
7
8
  const isPlainObject = value =>
8
9
  !!value && typeof value === 'object' && !Array.isArray(value);
9
10
 
10
11
  const getColumnKey = column => column?.key || column?.name || '';
11
12
 
13
+ const normalizeColumnKey = value => String(value || '').trim().toLowerCase();
14
+
15
+ export const isRequiredVisibleColumn = column => {
16
+ const key = normalizeColumnKey(getColumnKey(column));
17
+ if (!key) {
18
+ return false;
19
+ }
20
+
21
+ if (column?.required === true || column?.hideable === false) {
22
+ return true;
23
+ }
24
+
25
+ return REQUIRED_VISIBLE_COLUMN_KEYS.includes(key);
26
+ };
27
+
28
+ export const isRequiredVisibleColumnKey = (key, columns = []) => {
29
+ const normalizedKey = normalizeColumnKey(key);
30
+ if (!normalizedKey) {
31
+ return false;
32
+ }
33
+
34
+ const column = (Array.isArray(columns) ? columns : []).find(
35
+ item => normalizeColumnKey(getColumnKey(item)) === normalizedKey,
36
+ );
37
+
38
+ if (column) {
39
+ return isRequiredVisibleColumn(column);
40
+ }
41
+
42
+ return REQUIRED_VISIBLE_COLUMN_KEYS.includes(normalizedKey);
43
+ };
44
+
12
45
  const normalizePreferenceSegment = value =>
13
46
  String(value || '')
14
47
  .trim()
@@ -18,6 +51,7 @@ const normalizePreferenceSegment = value =>
18
51
  .replace(/-+/g, '-');
19
52
 
20
53
  export const resolveDefaultTablePreferenceScope = ({
54
+ companyId = '',
21
55
  preferenceKey = '',
22
56
  route = null,
23
57
  storeName = '',
@@ -30,11 +64,13 @@ export const resolveDefaultTablePreferenceScope = ({
30
64
  'unknown-route';
31
65
 
32
66
  /*
33
- * @agents DefaultTable preferences are saved as [store][route].
34
- * The same store can back different screens, so filters, sort, view mode,
35
- * and column visibility must never be keyed only by store or only by route.
67
+ * @agents DefaultTable preferences are saved as [company][store][route].
68
+ * The same store can back different screens, and the same user can switch
69
+ * companies, so filters, sort, view mode, and column visibility must be
70
+ * keyed by company, store and route together.
36
71
  */
37
72
  return {
73
+ companyKey: normalizePreferenceSegment(companyId),
38
74
  routeKey: routeName,
39
75
  storeKey: normalizePreferenceSegment(storeName) || 'unknown-store',
40
76
  };
@@ -45,10 +81,13 @@ const normalizePreferenceScope = scope => {
45
81
  return null;
46
82
  }
47
83
 
84
+ const companyKey = normalizePreferenceSegment(scope.companyKey);
48
85
  const storeKey = normalizePreferenceSegment(scope.storeKey);
49
86
  const routeKey = normalizePreferenceSegment(scope.routeKey);
50
87
 
51
- return storeKey && routeKey ? {storeKey, routeKey} : null;
88
+ return companyKey && storeKey && routeKey
89
+ ? {companyKey, storeKey, routeKey}
90
+ : null;
52
91
  };
53
92
 
54
93
  const readStoredPreferences = () => {
@@ -86,7 +125,10 @@ const readScopedPreference = scope => {
86
125
  }
87
126
 
88
127
  const storedPreferences = readStoredPreferences();
89
- const storePreferences = storedPreferences[resolvedScope.storeKey];
128
+ const companyPreferences = storedPreferences[resolvedScope.companyKey];
129
+ const storePreferences = isPlainObject(companyPreferences)
130
+ ? companyPreferences[resolvedScope.storeKey]
131
+ : null;
90
132
  const routePreferences = isPlainObject(storePreferences)
91
133
  ? storePreferences[resolvedScope.routeKey]
92
134
  : null;
@@ -101,8 +143,13 @@ const writeScopedPreference = (scope, nextValues = {}) => {
101
143
  }
102
144
 
103
145
  const storedPreferences = readStoredPreferences();
104
- const storePreferences = isPlainObject(storedPreferences[resolvedScope.storeKey])
105
- ? storedPreferences[resolvedScope.storeKey]
146
+ const companyPreferences = isPlainObject(
147
+ storedPreferences[resolvedScope.companyKey],
148
+ )
149
+ ? storedPreferences[resolvedScope.companyKey]
150
+ : {};
151
+ const storePreferences = isPlainObject(companyPreferences[resolvedScope.storeKey])
152
+ ? companyPreferences[resolvedScope.storeKey]
106
153
  : {};
107
154
  const routePreferences = isPlainObject(storePreferences[resolvedScope.routeKey])
108
155
  ? storePreferences[resolvedScope.routeKey]
@@ -110,11 +157,14 @@ const writeScopedPreference = (scope, nextValues = {}) => {
110
157
 
111
158
  writeStoredPreferences({
112
159
  ...storedPreferences,
113
- [resolvedScope.storeKey]: {
114
- ...storePreferences,
115
- [resolvedScope.routeKey]: {
116
- ...routePreferences,
117
- ...nextValues,
160
+ [resolvedScope.companyKey]: {
161
+ ...companyPreferences,
162
+ [resolvedScope.storeKey]: {
163
+ ...storePreferences,
164
+ [resolvedScope.routeKey]: {
165
+ ...routePreferences,
166
+ ...nextValues,
167
+ },
118
168
  },
119
169
  },
120
170
  });
@@ -124,11 +174,41 @@ export const buildDefaultVisibleColumns = columns =>
124
174
  (Array.isArray(columns) ? columns : []).reduce((accumulator, column) => {
125
175
  const key = getColumnKey(column);
126
176
  if (key) {
127
- accumulator[key] = column?.visible !== false;
177
+ accumulator[key] = isRequiredVisibleColumn(column)
178
+ ? true
179
+ : column?.visible !== false;
128
180
  }
129
181
  return accumulator;
130
182
  }, {});
131
183
 
184
+ const enforceRequiredVisibleColumns = (columns = [], visibleColumns = {}) => {
185
+ const nextVisibleColumns = {...visibleColumns};
186
+ const columnList = Array.isArray(columns) ? columns : [];
187
+
188
+ columnList.forEach(column => {
189
+ const key = getColumnKey(column);
190
+ if (key && isRequiredVisibleColumn(column)) {
191
+ nextVisibleColumns[key] = true;
192
+ }
193
+ });
194
+
195
+ const hasVisibleColumn = Object.keys(nextVisibleColumns).some(
196
+ key => nextVisibleColumns[key] !== false,
197
+ );
198
+
199
+ if (!hasVisibleColumn) {
200
+ const fallbackColumn =
201
+ columnList.find(column => isRequiredVisibleColumn(column)) ||
202
+ columnList.find(column => getColumnKey(column));
203
+ const fallbackKey = getColumnKey(fallbackColumn);
204
+ if (fallbackKey) {
205
+ nextVisibleColumns[fallbackKey] = true;
206
+ }
207
+ }
208
+
209
+ return nextVisibleColumns;
210
+ };
211
+
132
212
  export const sanitizeVisibleColumnsPreference = ({
133
213
  columns = [],
134
214
  visibleColumns = null,
@@ -136,16 +216,34 @@ export const sanitizeVisibleColumnsPreference = ({
136
216
  const defaultVisibleColumns = buildDefaultVisibleColumns(columns);
137
217
 
138
218
  if (!isPlainObject(visibleColumns)) {
139
- return defaultVisibleColumns;
219
+ return enforceRequiredVisibleColumns(columns, defaultVisibleColumns);
140
220
  }
141
221
 
142
- return Object.keys(defaultVisibleColumns).reduce((accumulator, key) => {
222
+ const sanitized = Object.keys(defaultVisibleColumns).reduce((accumulator, key) => {
143
223
  accumulator[key] =
144
224
  typeof visibleColumns[key] === 'boolean'
145
225
  ? visibleColumns[key]
146
226
  : defaultVisibleColumns[key];
147
227
  return accumulator;
148
228
  }, {});
229
+
230
+ return enforceRequiredVisibleColumns(columns, sanitized);
231
+ };
232
+
233
+ export const canHideVisibleColumn = ({
234
+ columns = [],
235
+ fieldName = '',
236
+ visibleColumns = {},
237
+ } = {}) => {
238
+ if (isRequiredVisibleColumnKey(fieldName, columns)) {
239
+ return false;
240
+ }
241
+
242
+ const visibleCount = Object.keys(visibleColumns || {}).filter(
243
+ key => visibleColumns[key] !== false,
244
+ ).length;
245
+
246
+ return visibleCount > 1;
149
247
  };
150
248
 
151
249
  export const sanitizeTableFiltersPreference = ({
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Unit tests for defaultAddressHelpers (app-community#283).
3
+ */
4
+ import {
5
+ emptyForm,
6
+ hasCoordinates,
7
+ hydrateFromRow,
8
+ mergePostalCodeData,
9
+ onlyDigits,
10
+ parseOptionalCoordinate,
11
+ buildMapMarkerPayload,
12
+ } from '../../../../react/components/address/defaultAddressHelpers';
13
+
14
+ describe('defaultAddressHelpers', () => {
15
+ test('onlyDigits strips non-digits', () => {
16
+ expect(onlyDigits('12.345-678')).toBe('12345678');
17
+ expect(onlyDigits(null)).toBe('');
18
+ });
19
+
20
+ test('parseOptionalCoordinate accepts number and comma decimal', () => {
21
+ expect(parseOptionalCoordinate('-23.5505')).toBe(-23.5505);
22
+ expect(parseOptionalCoordinate('-46,6333')).toBeCloseTo(-46.6333);
23
+ expect(parseOptionalCoordinate('')).toBeNull();
24
+ expect(parseOptionalCoordinate('abc')).toBeNull();
25
+ });
26
+
27
+ test('hasCoordinates requires both finite values', () => {
28
+ expect(hasCoordinates({latitude: -23.5, longitude: -46.6})).toBe(true);
29
+ expect(hasCoordinates({latitude: null, longitude: -46.6})).toBe(false);
30
+ expect(hasCoordinates(emptyForm)).toBe(false);
31
+ });
32
+
33
+ test('hydrateFromRow maps nested street/district/city/state', () => {
34
+ const row = {
35
+ nickname: 'Matriz',
36
+ number: 100,
37
+ latitude: -23.5,
38
+ longitude: -46.6,
39
+ street: {
40
+ street: 'Av Paulista',
41
+ cep: {cep: '01310100'},
42
+ district: {
43
+ district: 'Bela Vista',
44
+ city: {
45
+ city: 'São Paulo',
46
+ state: {
47
+ uf: 'SP',
48
+ state: 'São Paulo',
49
+ country: {countrycode: 'BR', countryname: 'Brazil'},
50
+ },
51
+ },
52
+ },
53
+ },
54
+ };
55
+ const form = hydrateFromRow(row);
56
+ expect(form.street).toBe('Av Paulista');
57
+ expect(form.district).toBe('Bela Vista');
58
+ expect(form.city).toBe('São Paulo');
59
+ expect(form.uf).toBe('SP');
60
+ expect(form.cep).toBe('01310100');
61
+ expect(form.latitude).toBe(-23.5);
62
+ expect(form.longitude).toBe(-46.6);
63
+ });
64
+
65
+ test('mergePostalCodeData fills lat/long from provider payload', () => {
66
+ const prev = {...emptyForm, street: 'Rua A'};
67
+ const data = {
68
+ cep: '01310100',
69
+ street: 'Av Paulista',
70
+ city: 'São Paulo',
71
+ uf: 'SP',
72
+ country: 'Brasil',
73
+ latitude: -23.561,
74
+ longitude: -46.656,
75
+ map: {staticUrl: 'https://example.com/map'},
76
+ provider: 'googlemaps',
77
+ };
78
+ const next = mergePostalCodeData(prev, data);
79
+ expect(next.latitude).toBe(-23.561);
80
+ expect(next.longitude).toBe(-46.656);
81
+ expect(next.provider).toBe('googlemaps');
82
+ expect(next.mapStaticUrl).toBe('https://example.com/map');
83
+ expect(next.countryCode).toBe('BR');
84
+ });
85
+
86
+ test('buildMapMarkerPayload returns null without address or coords', () => {
87
+ expect(buildMapMarkerPayload(emptyForm)).toBeNull();
88
+ });
89
+
90
+ test('buildMapMarkerPayload includes coords when present', () => {
91
+ const payload = buildMapMarkerPayload({
92
+ ...emptyForm,
93
+ street: 'Av Paulista',
94
+ number: '1000',
95
+ city: 'São Paulo',
96
+ uf: 'SP',
97
+ latitude: -23.5,
98
+ longitude: -46.6,
99
+ });
100
+ expect(payload).not.toBeNull();
101
+ expect(payload.latitude).toBe(-23.5);
102
+ expect(payload.longitude).toBe(-46.6);
103
+ });
104
+ });
@@ -20,6 +20,7 @@ jest.mock('@store', () => ({
20
20
  }));
21
21
 
22
22
  jest.mock('react-native', () => ({
23
+ Platform: {OS: 'android'},
23
24
  Image: props => React.createElement('image', props, props.children),
24
25
  }));
25
26
 
@@ -35,7 +36,7 @@ describe('DefaultFile', () => {
35
36
  jest.clearAllTimers();
36
37
  });
37
38
 
38
- it('renders backend download files with app-domain query and headers', () => {
39
+ it('renders backend download files with path app-domain and headers', () => {
39
40
  let tree;
40
41
 
41
42
  renderer.act(() => {
@@ -58,11 +59,11 @@ describe('DefaultFile', () => {
58
59
 
59
60
  const image = tree.root.findByType('image');
60
61
 
61
- expect(image.props.source).toEqual({
62
- uri: 'https://api.controleonline.com/files/3/download?app-domain=maincompany.controleonline.com',
63
- headers: {
64
- Authorization: 'Bearer token',
65
- 'app-domain': 'maincompany.controleonline.com',
62
+ expect(image.props.source).toEqual({
63
+ uri: 'https://api.controleonline.com/maincompany.controleonline.com/files/3/download',
64
+ headers: {
65
+ Authorization: 'Bearer token',
66
+ 'app-domain': 'maincompany.controleonline.com',
66
67
  },
67
68
  });
68
69
  expect(image.props.resizeMode).toBe('contain');
@@ -78,3 +79,18 @@ describe('DefaultFile', () => {
78
79
  expect(tree.toJSON()).toBeNull();
79
80
  });
80
81
  });
82
+
83
+ it('uses the domain company from the store when another company is selected', () => {
84
+ require('@store').useStore.mockReturnValue({getters: {
85
+ mainCompany: {id: 1, domain: 'domain.example'},
86
+ currentCompany: {id: 2, domain: 'selected.example'},
87
+ }});
88
+ let tree;
89
+ renderer.act(() => {
90
+ tree = renderer.create(React.createElement(DefaultFile, {file: {id: 7, url: '/files/7/download'}}));
91
+ });
92
+ const source = tree.root.findByType('image').props.source;
93
+ expect(source.uri).toBe('https://api.controleonline.com/domain.example/files/7/download');
94
+ expect(source.headers['app-domain']).toBe('domain.example');
95
+ renderer.act(() => tree.unmount());
96
+ });
@@ -0,0 +1,24 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {expect, it, jest} = require('@jest/globals');
4
+ global.IS_REACT_ACT_ENVIRONMENT = true;
5
+ jest.mock('react-native', () => ({View: 'View', Text: 'Text', TouchableOpacity: 'TouchableOpacity',
6
+ Linking: {openURL: jest.fn()}, StyleSheet: {create: x => x}}));
7
+ jest.mock('react-native-vector-icons/Feather', () => 'Icon');
8
+ jest.mock('@store', () => ({useStore: jest.fn()}));
9
+ jest.mock('../../../../react/components/files/DefaultFile', () => 'Preview');
10
+ jest.mock('../../../../react/components/files/DefaultFileColumn.utils', () => ({
11
+ normalizeDefaultFileColumnItems: jest.fn(() => []), normalizeFileColumnText: value => value,
12
+ }));
13
+ const {useStore} = require('@store');
14
+ const {normalizeDefaultFileColumnItems} = require('../../../../react/components/files/DefaultFileColumn.utils');
15
+ const Column = require('../../../../react/components/files/DefaultFileColumn').default;
16
+ it('passes the domain company to file URL resolution when B is selected', () => {
17
+ const mainCompany = {id: 1, domain: 'domain.example'};
18
+ useStore.mockImplementation(name => name === 'people'
19
+ ? {getters: {mainCompany, currentCompany: {id: 2}}} : {getters: {colors: {}}});
20
+ let tree;
21
+ renderer.act(() => { tree = renderer.create(React.createElement(Column, {})); });
22
+ expect(normalizeDefaultFileColumnItems.mock.calls.at(-1)[0].company).toEqual(mainCompany);
23
+ renderer.act(() => tree.unmount());
24
+ });
@@ -78,4 +78,127 @@ describe('DefaultForm', () => {
78
78
  const inputAfterTyping = capturedInputs[capturedInputs.length - 1];
79
79
  expect(inputAfterTyping.value).toBe('Cliente desistiu');
80
80
  });
81
+
82
+ it('saves hidden create payload columns with row and default values', async () => {
83
+ const save = jest.fn(() => Promise.resolve({id: 10}));
84
+ const columns = [
85
+ {
86
+ name: 'price',
87
+ label: 'value',
88
+ },
89
+ {
90
+ name: 'receiver',
91
+ createPayload: true,
92
+ visibleForm: false,
93
+ saveFormat: value => `/people/${value}`,
94
+ },
95
+ {
96
+ name: 'invoiceType',
97
+ createPayload: true,
98
+ defaultValue: 'invoice',
99
+ visibleForm: false,
100
+ },
101
+ ];
102
+ let tree;
103
+
104
+ await renderer.act(async () => {
105
+ tree = renderer.create(
106
+ React.createElement(DefaultForm, {
107
+ actions: {save},
108
+ columns,
109
+ mode: 'create',
110
+ row: {receiver: 21},
111
+ storeName: 'invoice',
112
+ }),
113
+ );
114
+ });
115
+
116
+ await renderer.act(async () => {
117
+ capturedInputs[capturedInputs.length - 1].onChangeValue('123.45');
118
+ });
119
+
120
+ const saveButton = tree.root.findAllByType('TouchableOpacity')[1];
121
+
122
+ await renderer.act(async () => {
123
+ await saveButton.props.onPress();
124
+ });
125
+
126
+ expect(save).toHaveBeenCalledWith({
127
+ price: 123.45,
128
+ receiver: '/people/21',
129
+ invoiceType: 'invoice',
130
+ });
131
+ });
132
+
133
+ it('preserves primitive ids for hidden create payload list columns', async () => {
134
+ const save = jest.fn(() => Promise.resolve({id: 11}));
135
+ const columns = [
136
+ {
137
+ name: 'price',
138
+ },
139
+ {
140
+ name: 'receiver',
141
+ createPayload: true,
142
+ list: 'people/getItems',
143
+ visibleForm: false,
144
+ formatList: value => {
145
+ if (!value || typeof value !== 'object') {
146
+ return {value, label: String(value || '')};
147
+ }
148
+
149
+ return {value: value.id, label: value.name};
150
+ },
151
+ saveFormat: value => `/people/${value?.value || value}`,
152
+ },
153
+ ];
154
+ let tree;
155
+
156
+ await renderer.act(async () => {
157
+ tree = renderer.create(
158
+ React.createElement(DefaultForm, {
159
+ actions: {save},
160
+ columns,
161
+ mode: 'create',
162
+ row: {receiver: 21},
163
+ storeName: 'invoice',
164
+ }),
165
+ );
166
+ });
167
+
168
+ await renderer.act(async () => {
169
+ capturedInputs[capturedInputs.length - 1].onChangeValue('1.23');
170
+ });
171
+
172
+ await renderer.act(async () => {
173
+ await tree.root.findAllByType('TouchableOpacity')[1].props.onPress();
174
+ });
175
+
176
+ expect(save).toHaveBeenCalledWith({
177
+ price: 1.23,
178
+ receiver: '/people/21',
179
+ });
180
+ });
181
+
182
+ it('uses explicit form labels before translated column fallbacks', async () => {
183
+ const columns = [
184
+ {
185
+ name: 'paymentType',
186
+ label: 'paymentType.paymentType',
187
+ formLabel: 'Tipo de pagamento',
188
+ },
189
+ ];
190
+
191
+ await renderer.act(async () => {
192
+ renderer.create(
193
+ React.createElement(DefaultForm, {
194
+ actions: {save: jest.fn()},
195
+ columns,
196
+ mode: 'create',
197
+ storeName: 'invoice',
198
+ }),
199
+ );
200
+ });
201
+
202
+ expect(capturedInputs[capturedInputs.length - 1].label).toBe('Tipo de pagamento');
203
+ });
81
204
  });
@@ -0,0 +1,93 @@
1
+ /* global jest */
2
+
3
+ const React = require('react');
4
+ const TestRenderer = require('react-test-renderer');
5
+ const {act} = TestRenderer;
6
+ const {describe, expect, it} = global;
7
+
8
+ jest.mock('react-native', () => {
9
+ const React = require('react');
10
+ const component = name => props => React.createElement(name, props, props.children);
11
+
12
+ return {
13
+ Modal: props =>
14
+ React.createElement('modal', props, props.visible ? props.children : null),
15
+ Pressable: component('pressable'),
16
+ StyleSheet: {create: styles => styles},
17
+ Text: component('text'),
18
+ TouchableOpacity: component('touchable-opacity'),
19
+ View: component('view'),
20
+ };
21
+ });
22
+
23
+ const DefaultTooltip = require('../../../../react/components/help/DefaultTooltip').default;
24
+
25
+ describe('DefaultTooltip', () => {
26
+ it('keeps the existing text message contract', () => {
27
+ let renderer;
28
+ act(() => {
29
+ renderer = TestRenderer.create(
30
+ React.createElement(DefaultTooltip, {
31
+ message: 'Ajuda já existente',
32
+ testID: 'message-trigger',
33
+ }),
34
+ );
35
+ });
36
+
37
+ const trigger = renderer.root
38
+ .findAllByType('touchable-opacity')
39
+ .find(node => node.props.testID === 'message-trigger');
40
+
41
+ act(() => trigger.props.onPress());
42
+
43
+ const renderedText = renderer.root
44
+ .findAllByType('text')
45
+ .flatMap(node => node.children)
46
+ .join(' ');
47
+ expect(renderedText).toContain('Ajuda já existente');
48
+ });
49
+
50
+ it('renders structured rows and closes from the action or backdrop', () => {
51
+ let renderer;
52
+ act(() => {
53
+ renderer = TestRenderer.create(
54
+ React.createElement(DefaultTooltip, {
55
+ backdropTestID: 'tooltip-backdrop',
56
+ closeAccessibilityLabel: 'Fechar detalhes',
57
+ dialogTestID: 'tooltip-dialog',
58
+ label: 'i',
59
+ rows: [
60
+ {key: 'company', label: 'Empresa', value: 'GYROS'},
61
+ {key: 'device', label: 'Device', value: null},
62
+ ],
63
+ testID: 'tooltip-trigger',
64
+ title: 'Detalhes',
65
+ }),
66
+ );
67
+ });
68
+
69
+ expect(renderer.root.findAllByProps({testID: 'tooltip-dialog'})).toHaveLength(0);
70
+
71
+ const trigger = renderer.root
72
+ .findAllByType('touchable-opacity')
73
+ .find(node => node.props.testID === 'tooltip-trigger');
74
+
75
+ act(() => trigger.props.onPress());
76
+
77
+ const renderedText = renderer.root
78
+ .findAllByType('text')
79
+ .flatMap(node => node.children)
80
+ .join(' ');
81
+ expect(renderedText).toContain('GYROS');
82
+ expect(renderedText).toContain('Não configurado');
83
+
84
+ act(() =>
85
+ renderer.root.findByProps({accessibilityLabel: 'Fechar detalhes'}).props.onPress(),
86
+ );
87
+ expect(renderer.root.findAllByProps({testID: 'tooltip-dialog'})).toHaveLength(0);
88
+
89
+ act(() => trigger.props.onPress());
90
+ act(() => renderer.root.findByProps({testID: 'tooltip-backdrop'}).props.onPress());
91
+ expect(renderer.root.findAllByProps({testID: 'tooltip-dialog'})).toHaveLength(0);
92
+ });
93
+ });