@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
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import {View, TextInput} from 'react-native';
3
+
4
+ /**
5
+ * Latitude / longitude fields.
6
+ * Read-only after a successful geocode; editable when Nominatim misses
7
+ * so the user can type coordinates and drop a pin.
8
+ */
9
+ export default function LatLonReadonlyFields({
10
+ form,
11
+ styles,
12
+ Field,
13
+ editable = false,
14
+ onChange,
15
+ }) {
16
+ const formatCoord = value =>
17
+ value == null || value === '' ? '' : String(value);
18
+
19
+ return (
20
+ <View style={styles.coordRow}>
21
+ <View style={styles.coordField}>
22
+ <Field label="Latitude">
23
+ <TextInput
24
+ testID="address-latitude-input"
25
+ style={[styles.input, editable ? null : styles.inputReadonly]}
26
+ value={formatCoord(form?.latitude)}
27
+ editable={editable}
28
+ selectTextOnFocus={editable}
29
+ onChangeText={v => onChange?.('latitude', v)}
30
+ placeholder="—"
31
+ keyboardType="decimal-pad"
32
+ />
33
+ </Field>
34
+ </View>
35
+ <View style={styles.coordField}>
36
+ <Field label="Longitude">
37
+ <TextInput
38
+ testID="address-longitude-input"
39
+ style={[styles.input, editable ? null : styles.inputReadonly]}
40
+ value={formatCoord(form?.longitude)}
41
+ editable={editable}
42
+ selectTextOnFocus={editable}
43
+ onChangeText={v => onChange?.('longitude', v)}
44
+ placeholder="—"
45
+ keyboardType="decimal-pad"
46
+ />
47
+ </Field>
48
+ </View>
49
+ </View>
50
+ );
51
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Pure helpers for DefaultAddress — keep the component under the 500-line limit.
3
+ * @see app-community#283 (franchise address lat/long + manual fields)
4
+ */
5
+
6
+ export const emptyForm = {
7
+ nickname: '',
8
+ cep: '',
9
+ street: '',
10
+ number: '',
11
+ complement: '',
12
+ district: '',
13
+ city: '',
14
+ uf: '',
15
+ stateName: '',
16
+ countryCode: 'BR',
17
+ countryName: 'Brazil',
18
+ latitude: null,
19
+ longitude: null,
20
+ mapStaticUrl: null,
21
+ facadeUrl: null,
22
+ provider: null,
23
+ categories: [],
24
+ };
25
+
26
+ export const onlyDigits = value => String(value || '').replace(/\D+/g, '');
27
+
28
+ export const hydrateFromRow = row => {
29
+ if (!row || typeof row !== 'object') {
30
+ return {...emptyForm};
31
+ }
32
+ const street = row?.street?.street || row?.street || '';
33
+ const district = row?.street?.district?.district || row?.district || '';
34
+ const city = row?.street?.district?.city?.city || row?.city || '';
35
+ const stateEntity = row?.street?.district?.city?.state || row?.state;
36
+ const countryEntity = stateEntity?.country || row?.country;
37
+ return {
38
+ ...emptyForm,
39
+ nickname: row?.nickname || '',
40
+ cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
41
+ street: typeof street === 'string' ? street : '',
42
+ number: row?.number != null ? String(row.number) : '',
43
+ complement: row?.complement || '',
44
+ district,
45
+ city,
46
+ uf: stateEntity?.uf || row?.uf || '',
47
+ stateName: stateEntity?.state || '',
48
+ countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
49
+ countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
50
+ latitude: row?.latitude ?? null,
51
+ longitude: row?.longitude ?? null,
52
+ categories: Array.isArray(row?.categories)
53
+ ? row.categories
54
+ : Array.isArray(row?.category)
55
+ ? row.category
56
+ : [],
57
+ };
58
+ };
59
+
60
+ export const hasAddressText = form =>
61
+ [form?.street, form?.number, form?.district, form?.city, form?.uf, form?.cep].some(
62
+ value => String(value || '').trim().length > 0,
63
+ );
64
+
65
+ export const hasCoordinates = form =>
66
+ Number.isFinite(Number(form?.latitude)) && Number.isFinite(Number(form?.longitude));
67
+
68
+ export const parseOptionalCoordinate = value => {
69
+ if (value === null || value === undefined || value === '') {
70
+ return null;
71
+ }
72
+ const n = Number(String(value).trim().replace(',', '.'));
73
+ return Number.isFinite(n) ? n : null;
74
+ };
75
+
76
+ export const mergePostalCodeData = (prev, data, {preserveFilledFields = false} = {}) => {
77
+ const keep = (key, nextValue) =>
78
+ preserveFilledFields && String(prev[key] || '').trim()
79
+ ? prev[key]
80
+ : nextValue || prev[key];
81
+
82
+ return {
83
+ ...prev,
84
+ cep: onlyDigits(data.cep || prev.cep),
85
+ street: keep('street', data.street),
86
+ district: keep('district', data.district),
87
+ city: keep('city', data.city),
88
+ uf: keep('uf', data.uf || data.state),
89
+ stateName: keep('stateName', data.state),
90
+ countryCode:
91
+ data.country === 'Brasil' || data.country === 'Brazil'
92
+ ? 'BR'
93
+ : data.country || prev.countryCode,
94
+ countryName:
95
+ data.country === 'Brasil' ? 'Brazil' : data.country || prev.countryName,
96
+ latitude: data.latitude ?? data.map?.latitude ?? prev.latitude,
97
+ longitude: data.longitude ?? data.map?.longitude ?? prev.longitude,
98
+ mapStaticUrl: data.map?.staticUrl || prev.mapStaticUrl || null,
99
+ facadeUrl: data.facade?.streetViewUrl || prev.facadeUrl || null,
100
+ provider: data.provider || prev.provider || null,
101
+ };
102
+ };
103
+
104
+ export const getCurrentCoordinates = () =>
105
+ new Promise(resolve => {
106
+ const geolocation =
107
+ typeof navigator !== 'undefined' ? navigator.geolocation : null;
108
+
109
+ if (!geolocation?.getCurrentPosition) {
110
+ resolve(null);
111
+ return;
112
+ }
113
+
114
+ geolocation.getCurrentPosition(
115
+ position => {
116
+ const latitude = Number(position?.coords?.latitude);
117
+ const longitude = Number(position?.coords?.longitude);
118
+
119
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
120
+ resolve(null);
121
+ return;
122
+ }
123
+
124
+ resolve({latitude, longitude});
125
+ },
126
+ () => resolve(null),
127
+ {enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
128
+ );
129
+ });
130
+
131
+ export const buildMapMarkerPayload = form => {
132
+ if (!hasAddressText(form) && !hasCoordinates(form)) {
133
+ return null;
134
+ }
135
+
136
+ const addressLine = [form.street, form.number].filter(Boolean).join(', ');
137
+ const addressExtra = [
138
+ form.district,
139
+ [form.city, form.uf].filter(Boolean).join(' - '),
140
+ form.countryName || form.countryCode,
141
+ form.cep,
142
+ ]
143
+ .filter(Boolean)
144
+ .join(' • ');
145
+
146
+ return {
147
+ id: 'default-address-preview',
148
+ title: form.nickname || 'Endereco',
149
+ addressLine: addressLine || addressExtra || 'Endereco',
150
+ addressExtra,
151
+ latitude: form.latitude,
152
+ longitude: form.longitude,
153
+ geocodeQuery: [
154
+ addressLine,
155
+ form.district,
156
+ [form.city, form.uf].filter(Boolean).join(' - '),
157
+ form.countryName || form.countryCode,
158
+ form.cep,
159
+ ]
160
+ .filter(Boolean)
161
+ .join(', '),
162
+ };
163
+ };
@@ -0,0 +1,148 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ /**
4
+ * Styles for DefaultAddress — extracted to keep the component under 500 lines.
5
+ */
6
+ export const styles = StyleSheet.create({
7
+ scroll: {flex: 1, backgroundColor: '#fff'},
8
+ container: {padding: 16, paddingBottom: 32},
9
+ containerDesktop: {
10
+ flexDirection: 'row',
11
+ alignItems: 'flex-start',
12
+ gap: 24,
13
+ maxWidth: 1200,
14
+ alignSelf: 'center',
15
+ width: '100%',
16
+ },
17
+ formPane: {width: '100%'},
18
+ formPaneDesktop: {flex: 1, maxWidth: 560},
19
+ field: {marginBottom: 10, position: 'relative', zIndex: 1},
20
+ fieldRaised: {zIndex: 30, elevation: 8},
21
+ label: {fontSize: 13, fontWeight: '600', marginBottom: 4, color: '#334155'},
22
+ input: {
23
+ borderWidth: 1,
24
+ borderColor: '#CBD5E1',
25
+ borderRadius: 8,
26
+ paddingHorizontal: 12,
27
+ paddingVertical: 10,
28
+ backgroundColor: '#fff',
29
+ },
30
+ select: {
31
+ borderWidth: 1,
32
+ borderColor: '#CBD5E1',
33
+ borderRadius: 8,
34
+ paddingHorizontal: 12,
35
+ paddingVertical: 12,
36
+ backgroundColor: '#F8FAFC',
37
+ },
38
+ dropdown: {
39
+ maxHeight: 220,
40
+ borderWidth: 1,
41
+ borderColor: '#E2E8F0',
42
+ borderRadius: 8,
43
+ marginTop: 4,
44
+ backgroundColor: '#fff',
45
+ zIndex: 40,
46
+ elevation: 8,
47
+ },
48
+ option: {
49
+ paddingHorizontal: 12,
50
+ paddingVertical: 10,
51
+ borderBottomWidth: 1,
52
+ borderBottomColor: '#F1F5F9',
53
+ },
54
+ row: {flexDirection: 'row', alignItems: 'center'},
55
+ flex: {flex: 1},
56
+ half: {flex: 1},
57
+ gap8: {gap: 8},
58
+ loader: {marginLeft: 8},
59
+ error: {color: '#B91C1C', marginBottom: 8},
60
+ hint: {color: '#64748B', marginBottom: 8, fontSize: 12},
61
+ mapPane: {
62
+ width: '100%',
63
+ borderWidth: 1,
64
+ borderColor: '#E2E8F0',
65
+ borderRadius: 12,
66
+ backgroundColor: '#F8FAFC',
67
+ padding: 16,
68
+ gap: 12,
69
+ },
70
+ mapPaneDesktop: {flex: 1, minHeight: 560},
71
+ mapPaneTitle: {fontWeight: '700', fontSize: 15, color: '#0F172A'},
72
+ mapImage: {
73
+ width: '100%',
74
+ height: 220,
75
+ borderRadius: 10,
76
+ backgroundColor: '#E2E8F0',
77
+ },
78
+ mapImageDesktop: {height: 360},
79
+ liveMap: {
80
+ width: '100%',
81
+ height: 220,
82
+ borderRadius: 10,
83
+ overflow: 'hidden',
84
+ backgroundColor: '#E2E8F0',
85
+ },
86
+ liveMapDesktop: {height: 360},
87
+ facadeImage: {
88
+ width: '100%',
89
+ height: 180,
90
+ borderRadius: 10,
91
+ backgroundColor: '#E2E8F0',
92
+ },
93
+ mapPlaceholder: {
94
+ height: 220,
95
+ borderRadius: 10,
96
+ backgroundColor: '#E2E8F0',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ padding: 18,
100
+ },
101
+ facadePlaceholder: {
102
+ minHeight: 120,
103
+ borderRadius: 10,
104
+ backgroundColor: '#E2E8F0',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ padding: 18,
108
+ },
109
+ mapPlaceholderTitle: {
110
+ fontWeight: '700',
111
+ color: '#334155',
112
+ marginBottom: 6,
113
+ },
114
+ mapPlaceholderText: {
115
+ color: '#64748B',
116
+ textAlign: 'center',
117
+ fontSize: 13,
118
+ lineHeight: 18,
119
+ },
120
+ actions: {
121
+ flexDirection: 'row',
122
+ justifyContent: 'flex-end',
123
+ gap: 12,
124
+ marginTop: 16,
125
+ },
126
+ btnSecondary: {paddingHorizontal: 16, paddingVertical: 12},
127
+ btnPrimary: {
128
+ backgroundColor: '#2563EB',
129
+ borderRadius: 8,
130
+ paddingHorizontal: 18,
131
+ paddingVertical: 12,
132
+ },
133
+ btnPrimaryText: {color: '#fff', fontWeight: '600'},
134
+ btnSecondaryText: {color: '#475569', fontWeight: '600'},
135
+ btnDisabled: {opacity: 0.6},
136
+
137
+ coordRow: {
138
+ flexDirection: 'row',
139
+ gap: 12,
140
+ },
141
+ coordField: {
142
+ flex: 1,
143
+ },
144
+ inputReadonly: {
145
+ backgroundColor: '#F1F5F9',
146
+ color: '#475569',
147
+ },
148
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Helpers for DefaultAddress form (hydration, CEP merge, geolocation).
3
+ */
4
+ const emptyForm = {
5
+ nickname: '',
6
+ cep: '',
7
+ street: '',
8
+ number: '',
9
+ complement: '',
10
+ district: '',
11
+ city: '',
12
+ uf: '',
13
+ stateName: '',
14
+ countryCode: 'BR',
15
+ countryName: 'Brazil',
16
+ latitude: null,
17
+ longitude: null,
18
+ mapStaticUrl: null,
19
+ facadeUrl: null,
20
+ provider: null,
21
+ };
22
+
23
+ const hydrateFromRow = row => {
24
+ if (!row || typeof row !== 'object') {
25
+ return {...emptyForm};
26
+ }
27
+ const street = row?.street?.street || row?.street || '';
28
+ const district = row?.street?.district?.district || row?.district || '';
29
+ const city = row?.street?.district?.city?.city || row?.city || '';
30
+ const stateEntity = row?.street?.district?.city?.state || row?.state;
31
+ const countryEntity = stateEntity?.country || row?.country;
32
+ return {
33
+ ...emptyForm,
34
+ nickname: row?.nickname || '',
35
+ cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
36
+ street: typeof street === 'string' ? street : '',
37
+ number: row?.number != null ? String(row.number) : '',
38
+ complement: row?.complement || '',
39
+ district,
40
+ city,
41
+ uf: stateEntity?.uf || row?.uf || '',
42
+ stateName: stateEntity?.state || '',
43
+ countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
44
+ countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
45
+ latitude: row?.latitude ?? null,
46
+ longitude: row?.longitude ?? null,
47
+ };
48
+ };
49
+
50
+ const onlyDigits = value => String(value || '').replace(/\D+/g, '');
51
+
52
+ const hasAddressText = form =>
53
+ [
54
+ form?.street,
55
+ form?.number,
56
+ form?.district,
57
+ form?.city,
58
+ form?.uf,
59
+ form?.cep,
60
+ ].some(value => String(value || '').trim().length > 0);
61
+
62
+ const hasCoordinates = form =>
63
+ Number.isFinite(Number(form?.latitude)) && Number.isFinite(Number(form?.longitude));
64
+
65
+ const mergePostalCodeData = (prev, data, {preserveFilledFields = false} = {}) => {
66
+ const keep = (key, nextValue) =>
67
+ preserveFilledFields && String(prev[key] || '').trim()
68
+ ? prev[key]
69
+ : nextValue || prev[key];
70
+
71
+ return {
72
+ ...prev,
73
+ cep: onlyDigits(data.cep || prev.cep),
74
+ street: keep('street', data.street),
75
+ district: keep('district', data.district),
76
+ city: keep('city', data.city),
77
+ uf: keep('uf', data.uf || data.state),
78
+ stateName: keep('stateName', data.state),
79
+ countryCode:
80
+ data.country === 'Brasil' || data.country === 'Brazil'
81
+ ? 'BR'
82
+ : data.country || prev.countryCode,
83
+ countryName:
84
+ data.country === 'Brasil' ? 'Brazil' : data.country || prev.countryName,
85
+ latitude: data.latitude ?? data.map?.latitude ?? prev.latitude,
86
+ longitude: data.longitude ?? data.map?.longitude ?? prev.longitude,
87
+ mapStaticUrl: data.map?.staticUrl || prev.mapStaticUrl || null,
88
+ facadeUrl: data.facade?.streetViewUrl || prev.facadeUrl || null,
89
+ provider: data.provider || prev.provider || null,
90
+ };
91
+ };
92
+
93
+ const getCurrentCoordinates = () =>
94
+ new Promise(resolve => {
95
+ const geolocation =
96
+ typeof navigator !== 'undefined' ? navigator.geolocation : null;
97
+
98
+ if (!geolocation?.getCurrentPosition) {
99
+ resolve(null);
100
+ return;
101
+ }
102
+
103
+ geolocation.getCurrentPosition(
104
+ position => {
105
+ const latitude = Number(position?.coords?.latitude);
106
+ const longitude = Number(position?.coords?.longitude);
107
+
108
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
109
+ resolve(null);
110
+ return;
111
+ }
112
+
113
+ resolve({latitude, longitude});
114
+ },
115
+ () => resolve(null),
116
+ {enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
117
+ );
118
+ });
119
+
120
+
121
+ export {
122
+ emptyForm,
123
+ hydrateFromRow,
124
+ onlyDigits,
125
+ hasAddressText,
126
+ hasCoordinates,
127
+ mergePostalCodeData,
128
+ getCurrentCoordinates,
129
+ };
@@ -0,0 +1,152 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Text, View } from 'react-native'
3
+
4
+ import styles from './DefaultExtraData.styles'
5
+
6
+ const MAX_CELL_ITEMS = 4
7
+
8
+ const formatKey = key =>
9
+ String(key || '')
10
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
11
+ .replace(/[_-]+/g, ' ')
12
+ .replace(/^\w/, letter => letter.toUpperCase())
13
+
14
+ const formatScalar = value => {
15
+ if (value === true) return 'true'
16
+ if (value === false) return 'false'
17
+ return String(value ?? '').trim()
18
+ }
19
+
20
+ const summarizeValue = value => {
21
+ if (Array.isArray(value)) return `${value.length}`
22
+ if (value && typeof value === 'object') {
23
+ return Object.keys(value).filter(Boolean).slice(0, 3).join(', ')
24
+ }
25
+
26
+ return formatScalar(value)
27
+ }
28
+
29
+ const resolveArrayKey = (item, index) =>
30
+ item?.name || item?.id || item?.code || item?.source || `${index + 1}`
31
+
32
+ const buildItem = (key, value, depth = 0) => {
33
+ if (Array.isArray(value)) {
34
+ return {
35
+ key,
36
+ value: summarizeValue(value),
37
+ children: value.map((item, index) =>
38
+ buildItem(resolveArrayKey(item, index), item, depth + 1),
39
+ ),
40
+ depth,
41
+ }
42
+ }
43
+
44
+ if (value && typeof value === 'object') {
45
+ return {
46
+ key,
47
+ value: summarizeValue(value),
48
+ children: Object.entries(value)
49
+ .filter(([, itemValue]) => itemValue !== null && itemValue !== undefined && itemValue !== '')
50
+ .map(([childKey, childValue]) => buildItem(childKey, childValue, depth + 1)),
51
+ depth,
52
+ }
53
+ }
54
+
55
+ return {
56
+ key,
57
+ value: formatScalar(value),
58
+ children: [],
59
+ depth,
60
+ }
61
+ }
62
+
63
+ const normalizeExtraData = value => {
64
+ if (!value) return []
65
+
66
+ const parsed = typeof value === 'string'
67
+ ? JSON.parse(value)
68
+ : value
69
+
70
+ if (Array.isArray(parsed)) {
71
+ return parsed
72
+ .map((item, index) => {
73
+ const key = item?.field || item?.key || item?.name || resolveArrayKey(item, index)
74
+ const itemValue = item?.value ?? item?.configValue ?? item?.data ?? item
75
+ return buildItem(key, itemValue)
76
+ })
77
+ .filter(item => item.value || item.children.length > 0)
78
+ }
79
+
80
+ if (!parsed || typeof parsed !== 'object') {
81
+ return []
82
+ }
83
+
84
+ return Object.entries(parsed)
85
+ .filter(([, itemValue]) => itemValue !== null && itemValue !== undefined && itemValue !== '')
86
+ .map(([key, itemValue]) => buildItem(key, itemValue))
87
+ }
88
+
89
+ const ExtraDataItem = ({ item, variant = 'cell' }) => {
90
+ const isCard = variant === 'card'
91
+ const children = Array.isArray(item.children) ? item.children : []
92
+
93
+ return (
94
+ <View style={[styles.item, isCard ? styles.cardItem : null]}>
95
+ <Text style={styles.key} numberOfLines={1}>
96
+ {formatKey(item.key)}
97
+ </Text>
98
+ {item.value ? (
99
+ <Text style={styles.value} numberOfLines={isCard ? 4 : 1}>
100
+ {item.value}
101
+ </Text>
102
+ ) : null}
103
+ {isCard && children.length > 0 ? (
104
+ <View style={styles.children}>
105
+ {children.map((child, index) => (
106
+ <ExtraDataItem
107
+ key={`${item.key}-${child.key}-${index}`}
108
+ item={child}
109
+ variant={variant}
110
+ />
111
+ ))}
112
+ </View>
113
+ ) : null}
114
+ </View>
115
+ )
116
+ }
117
+
118
+ const DefaultExtraData = ({ value, variant = 'cell' }) => {
119
+ const items = useMemo(() => {
120
+ try {
121
+ return normalizeExtraData(value)
122
+ } catch {
123
+ return []
124
+ }
125
+ }, [value])
126
+
127
+ if (items.length === 0) {
128
+ return <Text style={styles.empty}>-</Text>
129
+ }
130
+
131
+ const isCard = variant === 'card'
132
+ const visibleItems = isCard ? items : items.slice(0, MAX_CELL_ITEMS)
133
+
134
+ return (
135
+ <View style={[styles.wrap, isCard ? styles.cardWrap : null]}>
136
+ {visibleItems.map((item, index) => (
137
+ <ExtraDataItem
138
+ key={`${item.key}-${index}`}
139
+ item={item}
140
+ variant={variant}
141
+ />
142
+ ))}
143
+ {!isCard && items.length > MAX_CELL_ITEMS ? (
144
+ <Text style={styles.more} numberOfLines={1}>
145
+ +{items.length - MAX_CELL_ITEMS}
146
+ </Text>
147
+ ) : null}
148
+ </View>
149
+ )
150
+ }
151
+
152
+ export default DefaultExtraData
@@ -0,0 +1,61 @@
1
+ import { StyleSheet } from 'react-native'
2
+
3
+ const styles = StyleSheet.create({
4
+ wrap: {
5
+ flexDirection: 'row',
6
+ flexWrap: 'wrap',
7
+ gap: 6,
8
+ minWidth: 0,
9
+ },
10
+ cardWrap: {
11
+ flexDirection: 'column',
12
+ alignItems: 'stretch',
13
+ width: '100%',
14
+ },
15
+ item: {
16
+ maxWidth: 180,
17
+ minHeight: 24,
18
+ borderRadius: 8,
19
+ borderWidth: 1,
20
+ borderColor: '#E2E8F0',
21
+ backgroundColor: '#F8FAFC',
22
+ paddingHorizontal: 8,
23
+ paddingVertical: 4,
24
+ },
25
+ cardItem: {
26
+ maxWidth: '100%',
27
+ width: '100%',
28
+ },
29
+ children: {
30
+ gap: 4,
31
+ marginTop: 6,
32
+ paddingLeft: 8,
33
+ borderLeftWidth: 1,
34
+ borderLeftColor: '#CBD5E1',
35
+ },
36
+ key: {
37
+ color: '#64748B',
38
+ fontSize: 9,
39
+ fontWeight: '900',
40
+ textTransform: 'uppercase',
41
+ },
42
+ value: {
43
+ marginTop: 1,
44
+ color: '#0F172A',
45
+ fontSize: 11,
46
+ fontWeight: '700',
47
+ },
48
+ more: {
49
+ alignSelf: 'center',
50
+ color: '#64748B',
51
+ fontSize: 11,
52
+ fontWeight: '900',
53
+ },
54
+ empty: {
55
+ color: '#94A3B8',
56
+ fontSize: 12,
57
+ fontWeight: '700',
58
+ },
59
+ })
60
+
61
+ export default styles