@controleonline/ui-default 1.0.269 → 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 (91) 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/DefaultGoogleMap.styles.js +9 -2
  39. package/src/react/components/map/DefaultGoogleMap.web.js +49 -28
  40. package/src/react/components/map/DefaultMap.native.js +73 -73
  41. package/src/react/components/map/DefaultMap.shared.js +911 -762
  42. package/src/react/components/map/DefaultMap.web.js +124 -68
  43. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  44. package/src/react/components/table/DefaultDebug.js +45 -38
  45. package/src/react/components/table/DefaultSearchModal.js +9 -1
  46. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  47. package/src/react/components/table/DefaultTable.js +215 -109
  48. package/src/react/components/table/DefaultTable.styles.js +367 -314
  49. package/src/react/components/table/DefaultTableBody.js +13 -8
  50. package/src/react/components/table/DefaultTableCards.js +20 -1
  51. package/src/react/components/table/DefaultTableControls.js +180 -205
  52. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  53. package/src/react/components/table/DefaultTableInput.js +1 -0
  54. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  55. package/src/react/components/table/DefaultTableRows.js +224 -363
  56. package/src/react/components/table/DefaultTableSearch.js +28 -5
  57. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  58. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  59. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  60. package/src/react/components/upload/DefaultUpload.js +436 -0
  61. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  62. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  63. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  64. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  65. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  66. package/src/react/components/upload/fileUpload.js +93 -0
  67. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  68. package/src/react/index.js +6 -0
  69. package/src/react/services/addressFormUtils.js +196 -0
  70. package/src/react/services/addressGeo.js +227 -0
  71. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  72. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  73. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  74. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  75. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  76. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  77. package/src/tests/react/components/table/DefaultTable.test.js +54 -2
  78. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  79. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  80. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  81. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  82. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  83. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  84. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  85. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  86. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  87. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  88. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  89. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  90. package/src/tests/react/services/smoke-637/index.html +132 -0
  91. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -0,0 +1,227 @@
1
+ import {api} from '@controleonline/ui-common/src/api';
2
+
3
+ const onlyDigits = value => String(value || '').replace(/\D+/g, '');
4
+
5
+ const hasText = value => String(value || '').trim().length > 0;
6
+
7
+ export function isValidCoord(value) {
8
+ if (value === null || value === undefined || value === '') {
9
+ return false;
10
+ }
11
+ const n = Number(value);
12
+ return Number.isFinite(n) && Math.abs(n) > 0.000001;
13
+ }
14
+
15
+ export function isPostalLookupIncomplete(data) {
16
+ if (!data || typeof data !== 'object') {
17
+ return true;
18
+ }
19
+ const missingText = !hasText(data.street) || !hasText(data.city);
20
+ const missingCoords =
21
+ !isValidCoord(data.latitude ?? data?.map?.latitude) ||
22
+ !isValidCoord(data.longitude ?? data?.map?.longitude);
23
+ return missingText || missingCoords;
24
+ }
25
+
26
+ export function mergePostalLookupPayload(base, extra, providerSuffix) {
27
+ const src = extra && typeof extra === 'object' ? extra : {};
28
+ const next = {...(base && typeof base === 'object' ? base : {})};
29
+ const fill = (key, value) => {
30
+ if (!hasText(next[key]) && hasText(value)) {
31
+ next[key] = value;
32
+ }
33
+ };
34
+ fill('street', src.street);
35
+ fill('district', src.district);
36
+ fill('city', src.city);
37
+ fill('state', src.state || src.uf);
38
+ fill('uf', src.uf || src.state);
39
+ fill('country', src.country);
40
+ if (!isValidCoord(next.latitude) && isValidCoord(src.latitude)) {
41
+ next.latitude = Number(src.latitude);
42
+ }
43
+ if (!isValidCoord(next.longitude) && isValidCoord(src.longitude)) {
44
+ next.longitude = Number(src.longitude);
45
+ }
46
+ if (providerSuffix) {
47
+ const current = String(next.provider || '').trim();
48
+ next.provider = current
49
+ ? `${current}+${providerSuffix}`
50
+ : providerSuffix;
51
+ }
52
+ return next;
53
+ }
54
+
55
+ function mapViaCepPayload(json, digits) {
56
+ if (!json || json.erro) {
57
+ return null;
58
+ }
59
+ return {
60
+ cep: digits,
61
+ street: json.logradouro || '',
62
+ district: json.bairro || '',
63
+ city: json.localidade || '',
64
+ state: json.uf || '',
65
+ uf: json.uf || '',
66
+ country: 'Brasil',
67
+ provider: 'viacep-client',
68
+ };
69
+ }
70
+
71
+ export async function lookupViaCep(cep) {
72
+ const digits = onlyDigits(cep);
73
+ if (digits.length !== 8) {
74
+ throw new Error('CEP deve ter 8 dígitos');
75
+ }
76
+ const response = await fetch(`https://viacep.com.br/ws/${digits}/json/`);
77
+ if (!response.ok) {
78
+ throw new Error(`ViaCEP HTTP ${response.status}`);
79
+ }
80
+ const json = await response.json();
81
+ const mapped = mapViaCepPayload(json, digits);
82
+ if (!mapped) {
83
+ throw new Error('CEP não encontrado');
84
+ }
85
+ return mapped;
86
+ }
87
+
88
+ function buildNominatimQuery(data, digits) {
89
+ const cepFormatted =
90
+ digits.length === 8 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;
91
+ return [
92
+ data?.street,
93
+ data?.district,
94
+ data?.city,
95
+ data?.uf || data?.state,
96
+ cepFormatted,
97
+ 'Brasil',
98
+ ]
99
+ .map(part => String(part || '').trim())
100
+ .filter(Boolean)
101
+ .join(', ');
102
+ }
103
+
104
+ export async function lookupNominatimCoords(data, digits) {
105
+ const q = buildNominatimQuery(data, digits);
106
+ if (!q) {
107
+ return null;
108
+ }
109
+ const url = `https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(q)}`;
110
+ const response = await fetch(url, {
111
+ headers: {
112
+ Accept: 'application/json',
113
+ 'User-Agent': 'ControleOnline-ui-default/task-637',
114
+ },
115
+ });
116
+ if (!response.ok) {
117
+ return null;
118
+ }
119
+ const rows = await response.json();
120
+ if (!Array.isArray(rows) || !rows[0]?.lat || !rows[0]?.lon) {
121
+ return null;
122
+ }
123
+ return {
124
+ latitude: Number(rows[0].lat),
125
+ longitude: Number(rows[0].lon),
126
+ formatted: rows[0].display_name || null,
127
+ provider: 'nominatim-client',
128
+ };
129
+ }
130
+
131
+ export async function lookupPostalCode(cep) {
132
+ const digits = onlyDigits(cep);
133
+ if (digits.length !== 8) {
134
+ throw new Error('CEP deve ter 8 dígitos');
135
+ }
136
+
137
+ let payload = {cep: digits};
138
+ let apiError = null;
139
+
140
+ try {
141
+ const apiData = await api.fetch(`postal-codes/${digits}`, {method: 'GET'});
142
+ if (apiData && typeof apiData === 'object') {
143
+ payload = {...payload, ...apiData, cep: digits};
144
+ }
145
+ } catch (e) {
146
+ apiError = e;
147
+ }
148
+
149
+ if (!hasText(payload.street) || !hasText(payload.city)) {
150
+ try {
151
+ const viaCep = await lookupViaCep(digits);
152
+ payload = mergePostalLookupPayload(payload, viaCep, viaCep.provider);
153
+ } catch (e) {
154
+ if (apiError && isPostalLookupIncomplete(payload)) {
155
+ throw new Error(
156
+ e?.message ||
157
+ apiError?.message ||
158
+ apiError?.response?.data?.detail ||
159
+ 'CEP inválido ou serviço indisponível',
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ if (
166
+ !isValidCoord(payload.latitude ?? payload?.map?.latitude) ||
167
+ !isValidCoord(payload.longitude ?? payload?.map?.longitude)
168
+ ) {
169
+ try {
170
+ const geo = await lookupNominatimCoords(payload, digits);
171
+ if (geo) {
172
+ payload = mergePostalLookupPayload(payload, geo, geo.provider);
173
+ }
174
+ } catch {
175
+ // coords optional if text fields already filled
176
+ }
177
+ }
178
+
179
+ if (isPostalLookupIncomplete(payload) && apiError && !hasText(payload.street)) {
180
+ throw new Error(
181
+ apiError?.message ||
182
+ apiError?.response?.data?.detail ||
183
+ 'CEP inválido ou serviço indisponível',
184
+ );
185
+ }
186
+
187
+ return payload;
188
+ }
189
+
190
+ export async function listCountries(q = '') {
191
+ const params = q ? {q} : {};
192
+ const data = await api.fetch('address-geo/countries', {method: 'GET', params});
193
+ return Array.isArray(data?.member) ? data.member : [];
194
+ }
195
+
196
+ export async function listStates(country = 'BR') {
197
+ const data = await api.fetch('address-geo/states', {
198
+ method: 'GET',
199
+ params: {country},
200
+ });
201
+ return Array.isArray(data?.member) ? data.member : [];
202
+ }
203
+
204
+ export function buildAddressSavePayload(form, peopleIri) {
205
+ const payload = {
206
+ street: form.street || '',
207
+ city: form.city || '',
208
+ district: form.district || '',
209
+ state: form.uf || form.state || '',
210
+ country: form.countryCode || form.country || 'BR',
211
+ people: peopleIri,
212
+ number: form.number ?? '',
213
+ complement: form.complement || '',
214
+ nickname: form.nickname || '',
215
+ cep: onlyDigits(form.cep),
216
+ latitude: form.latitude ?? 0,
217
+ longitude: form.longitude ?? 0,
218
+ };
219
+
220
+ if (Array.isArray(form.categories)) {
221
+ payload.categories = form.categories;
222
+ } else if (Array.isArray(form.category)) {
223
+ payload.category = form.category;
224
+ }
225
+
226
+ return payload;
227
+ }
@@ -18,6 +18,7 @@ const normalizePreferenceSegment = value =>
18
18
  .replace(/-+/g, '-');
19
19
 
20
20
  export const resolveDefaultTablePreferenceScope = ({
21
+ companyId = '',
21
22
  preferenceKey = '',
22
23
  route = null,
23
24
  storeName = '',
@@ -30,11 +31,13 @@ export const resolveDefaultTablePreferenceScope = ({
30
31
  'unknown-route';
31
32
 
32
33
  /*
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.
34
+ * @agents DefaultTable preferences are saved as [company][store][route].
35
+ * The same store can back different screens, and the same user can switch
36
+ * companies, so filters, sort, view mode, and column visibility must be
37
+ * keyed by company, store and route together.
36
38
  */
37
39
  return {
40
+ companyKey: normalizePreferenceSegment(companyId),
38
41
  routeKey: routeName,
39
42
  storeKey: normalizePreferenceSegment(storeName) || 'unknown-store',
40
43
  };
@@ -45,10 +48,13 @@ const normalizePreferenceScope = scope => {
45
48
  return null;
46
49
  }
47
50
 
51
+ const companyKey = normalizePreferenceSegment(scope.companyKey);
48
52
  const storeKey = normalizePreferenceSegment(scope.storeKey);
49
53
  const routeKey = normalizePreferenceSegment(scope.routeKey);
50
54
 
51
- return storeKey && routeKey ? {storeKey, routeKey} : null;
55
+ return companyKey && storeKey && routeKey
56
+ ? {companyKey, storeKey, routeKey}
57
+ : null;
52
58
  };
53
59
 
54
60
  const readStoredPreferences = () => {
@@ -86,7 +92,10 @@ const readScopedPreference = scope => {
86
92
  }
87
93
 
88
94
  const storedPreferences = readStoredPreferences();
89
- const storePreferences = storedPreferences[resolvedScope.storeKey];
95
+ const companyPreferences = storedPreferences[resolvedScope.companyKey];
96
+ const storePreferences = isPlainObject(companyPreferences)
97
+ ? companyPreferences[resolvedScope.storeKey]
98
+ : null;
90
99
  const routePreferences = isPlainObject(storePreferences)
91
100
  ? storePreferences[resolvedScope.routeKey]
92
101
  : null;
@@ -101,8 +110,13 @@ const writeScopedPreference = (scope, nextValues = {}) => {
101
110
  }
102
111
 
103
112
  const storedPreferences = readStoredPreferences();
104
- const storePreferences = isPlainObject(storedPreferences[resolvedScope.storeKey])
105
- ? storedPreferences[resolvedScope.storeKey]
113
+ const companyPreferences = isPlainObject(
114
+ storedPreferences[resolvedScope.companyKey],
115
+ )
116
+ ? storedPreferences[resolvedScope.companyKey]
117
+ : {};
118
+ const storePreferences = isPlainObject(companyPreferences[resolvedScope.storeKey])
119
+ ? companyPreferences[resolvedScope.storeKey]
106
120
  : {};
107
121
  const routePreferences = isPlainObject(storePreferences[resolvedScope.routeKey])
108
122
  ? storePreferences[resolvedScope.routeKey]
@@ -110,11 +124,14 @@ const writeScopedPreference = (scope, nextValues = {}) => {
110
124
 
111
125
  writeStoredPreferences({
112
126
  ...storedPreferences,
113
- [resolvedScope.storeKey]: {
114
- ...storePreferences,
115
- [resolvedScope.routeKey]: {
116
- ...routePreferences,
117
- ...nextValues,
127
+ [resolvedScope.companyKey]: {
128
+ ...companyPreferences,
129
+ [resolvedScope.storeKey]: {
130
+ ...storePreferences,
131
+ [resolvedScope.routeKey]: {
132
+ ...routePreferences,
133
+ ...nextValues,
134
+ },
118
135
  },
119
136
  },
120
137
  });
@@ -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
+ });
@@ -35,7 +35,7 @@ describe('DefaultFile', () => {
35
35
  jest.clearAllTimers();
36
36
  });
37
37
 
38
- it('renders backend download files with app-domain query and headers', () => {
38
+ it('renders backend download files with path app-domain and headers', () => {
39
39
  let tree;
40
40
 
41
41
  renderer.act(() => {
@@ -58,11 +58,11 @@ describe('DefaultFile', () => {
58
58
 
59
59
  const image = tree.root.findByType('image');
60
60
 
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',
61
+ expect(image.props.source).toEqual({
62
+ uri: 'https://api.controleonline.com/maincompany.controleonline.com/files/3/download',
63
+ headers: {
64
+ Authorization: 'Bearer token',
65
+ 'app-domain': 'maincompany.controleonline.com',
66
66
  },
67
67
  });
68
68
  expect(image.props.resizeMode).toBe('contain');
@@ -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
  });