@controleonline/ui-default 1.0.272 → 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.
- package/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +1 -1
- package/docs/technical/DefaultTable.md +70 -1
- package/package.json +22 -22
- package/src/react/components/address/DefaultAddress.js +20 -9
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
- package/src/react/components/address/LatLonReadonlyFields.js +2 -2
- package/src/react/components/address/defaultAddressStyles.js +13 -1
- package/src/react/components/files/DefaultFile.js +2 -2
- package/src/react/components/files/DefaultFileColumn.js +1 -1
- package/src/react/components/filters/DefaultColumnFilter.js +2 -7
- package/src/react/components/filters/DefaultExternalFilters.js +4 -11
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/help/DefaultTooltip.js +73 -63
- package/src/react/components/help/DefaultTooltip.styles.js +23 -23
- package/src/react/components/inputs/DefaultInput.js +301 -210
- package/src/react/components/inputs/defaultInputUtils.js +193 -170
- package/src/react/components/table/DefaultColumnMenu.js +127 -109
- package/src/react/components/table/DefaultTable.js +4 -4
- package/src/react/components/table/DefaultTable.styles.js +10 -4
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- package/src/react/components/table/DefaultTableRows.js +324 -153
- package/src/react/components/upload/DefaultUpload.js +6 -36
- package/src/react/components/upload/DefaultUpload.styles.js +5 -5
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
- package/src/react/hooks/usePostalCodeLookup.js +15 -1
- package/src/react/services/addressFormUtils.js +104 -38
- package/src/react/services/addressGeo.js +6 -3
- package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
- package/src/tests/react/components/files/DefaultFile.test.js +16 -0
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/addressFormUtils.test.js +6 -3
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +12 -1
|
@@ -28,8 +28,6 @@ const DefaultUpload = ({
|
|
|
28
28
|
attachments = [],
|
|
29
29
|
entityId,
|
|
30
30
|
companyId,
|
|
31
|
-
company = null,
|
|
32
|
-
showAttachmentActions = true,
|
|
33
31
|
context = 'products',
|
|
34
32
|
libraryContexts = DEFAULT_LIBRARY_CONTEXTS,
|
|
35
33
|
fileStoreName = 'file',
|
|
@@ -162,17 +160,10 @@ const DefaultUpload = ({
|
|
|
162
160
|
setCoverId(coverRelationId || null);
|
|
163
161
|
}, [coverRelationId]);
|
|
164
162
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? relationActions
|
|
170
|
-
: null;
|
|
171
|
-
if (fromRelation) return fromRelation;
|
|
172
|
-
const fromPeople = peopleStoreForMedia?.actions;
|
|
173
|
-
if (typeof fromPeople?.getPeopleMedia === 'function') return fromPeople;
|
|
174
|
-
return null;
|
|
175
|
-
})();
|
|
163
|
+
const peopleActionsForLibrary =
|
|
164
|
+
relationStoreName === 'people' && typeof relationActions?.getPeopleMedia === 'function'
|
|
165
|
+
? relationActions
|
|
166
|
+
: null;
|
|
176
167
|
|
|
177
168
|
const loadLibrary = useCallback(async (extraFiles = []) => {
|
|
178
169
|
setLibraryLoading(true);
|
|
@@ -186,26 +177,7 @@ const DefaultUpload = ({
|
|
|
186
177
|
peopleActions: peopleActionsForLibrary,
|
|
187
178
|
knownFileIds: resolvedKnownIds,
|
|
188
179
|
});
|
|
189
|
-
|
|
190
|
-
const withMeta = (list) =>
|
|
191
|
-
(Array.isArray(list) ? list : []).map(item => {
|
|
192
|
-
if (!item || typeof item !== 'object') return item;
|
|
193
|
-
const id = extractFileId(item);
|
|
194
|
-
if (!id) return item;
|
|
195
|
-
return {
|
|
196
|
-
...item,
|
|
197
|
-
id: item.id || id,
|
|
198
|
-
'@id': item['@id'] || `/files/${id}`,
|
|
199
|
-
context: item.context || context || 'people_media',
|
|
200
|
-
fileType: item.fileType || item.mimeType || (preferImageMeta ? 'image' : item.fileType),
|
|
201
|
-
fileName: item.fileName || item.name || item.originalName || `Arquivo ${id}`,
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
setLibraryFiles(
|
|
205
|
-
dedupeFiles(
|
|
206
|
-
withMeta([...(Array.isArray(extraFiles) ? extraFiles : []), ...files]),
|
|
207
|
-
),
|
|
208
|
-
);
|
|
180
|
+
setLibraryFiles(dedupeFiles([...(Array.isArray(extraFiles) ? extraFiles : []), ...files]));
|
|
209
181
|
} catch (e) {
|
|
210
182
|
setLibraryError(e?.message || 'Falha ao carregar biblioteca de arquivos.');
|
|
211
183
|
setLibraryFiles(dedupeFiles(extraFiles));
|
|
@@ -400,8 +372,6 @@ const DefaultUpload = ({
|
|
|
400
372
|
emptyAttachmentsLabel={emptyAttachmentLabel}
|
|
401
373
|
status={status}
|
|
402
374
|
error={error}
|
|
403
|
-
fileType={fileType}
|
|
404
|
-
company={company || (companyId ? {id: companyId} : null)}
|
|
405
375
|
/>
|
|
406
376
|
);
|
|
407
377
|
|
|
@@ -427,7 +397,7 @@ const DefaultUpload = ({
|
|
|
427
397
|
handleRemove={handleRemove}
|
|
428
398
|
buttonPalette={buttonPalette}
|
|
429
399
|
managerModal={managerModal}
|
|
430
|
-
company={company
|
|
400
|
+
company={company}
|
|
431
401
|
showAttachmentActions={showAttachmentActions}
|
|
432
402
|
/>
|
|
433
403
|
);
|
|
@@ -63,7 +63,7 @@ export const defaultUploadStyles = {
|
|
|
63
63
|
modalList: {maxHeight: 520},
|
|
64
64
|
libraryGrid: {flexDirection: 'row', flexWrap: 'wrap', gap: 10, paddingHorizontal: 14, paddingBottom: 18},
|
|
65
65
|
fileCard: {
|
|
66
|
-
width:
|
|
66
|
+
width: 320,
|
|
67
67
|
borderWidth: 1,
|
|
68
68
|
borderColor: '#E2E8F0',
|
|
69
69
|
borderRadius: 8,
|
|
@@ -71,7 +71,7 @@ export const defaultUploadStyles = {
|
|
|
71
71
|
overflow: 'hidden',
|
|
72
72
|
},
|
|
73
73
|
fileCardAttached: {borderColor: '#86EFAC', backgroundColor: '#F0FDF4'},
|
|
74
|
-
fileThumb: {height:
|
|
74
|
+
fileThumb: {width: 320, height: 320, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center'},
|
|
75
75
|
fileThumbFallback: {alignItems: 'center', justifyContent: 'center', gap: 6, height: '100%', width: '100%', backgroundColor: '#EEF2FF'},
|
|
76
76
|
fileExtBadge: {
|
|
77
77
|
color: '#312E81',
|
|
@@ -130,9 +130,9 @@ export const defaultUploadStyles = {
|
|
|
130
130
|
attachmentsEmpty: {borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 10},
|
|
131
131
|
attachmentsEmptyText: {color: '#666'},
|
|
132
132
|
attachmentsList: {flexDirection: 'row', gap: 10},
|
|
133
|
-
attachmentCard: {width:
|
|
134
|
-
attachmentThumb: {height:
|
|
135
|
-
attachmentImage: {width:
|
|
133
|
+
attachmentCard: {width: 320, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 0, overflow: 'hidden', backgroundColor: '#fff'},
|
|
134
|
+
attachmentThumb: {width: 320, height: 320, backgroundColor: '#f5f5f5', overflow: 'hidden', alignItems: 'center', justifyContent: 'center'},
|
|
135
|
+
attachmentImage: {width: 320, height: 320},
|
|
136
136
|
attachmentActionsRow: {flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, marginTop: 2},
|
|
137
137
|
attachmentIconButton: {width: 36, height: 36, borderRadius: 8, alignItems: 'center', justifyContent: 'center'},
|
|
138
138
|
attachmentRemoveButton: {backgroundColor: '#b00020', paddingVertical: 6, borderRadius: 4},
|
|
@@ -74,14 +74,22 @@ export default function DefaultUploadAttachmentsList({
|
|
|
74
74
|
return (
|
|
75
75
|
<View
|
|
76
76
|
key={row.id || fileId || index}
|
|
77
|
-
style={
|
|
78
|
-
|
|
77
|
+
style={[
|
|
78
|
+
styles.attachmentCard,
|
|
79
|
+
// app-community#813: card principal 320x320
|
|
80
|
+
{width: 320, padding: 0, overflow: 'hidden'},
|
|
81
|
+
]}>
|
|
82
|
+
<View
|
|
83
|
+
style={[
|
|
84
|
+
styles.attachmentThumb,
|
|
85
|
+
{width: 320, height: 320, marginBottom: 0},
|
|
86
|
+
]}>
|
|
79
87
|
{file ? (
|
|
80
88
|
<DefaultFile
|
|
81
89
|
file={file}
|
|
82
90
|
company={company}
|
|
83
91
|
resizeMode="cover"
|
|
84
|
-
style={
|
|
92
|
+
style={{width: 320, height: 320}}
|
|
85
93
|
/>
|
|
86
94
|
) : null}
|
|
87
95
|
</View>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
2
|
import {lookupPostalCode, listStates} from '../services/addressGeo';
|
|
3
3
|
import {
|
|
4
|
+
clearPostalCodeDerivedFields,
|
|
5
|
+
isGeocodeMiss,
|
|
4
6
|
mergePostalCodeData,
|
|
5
7
|
onlyDigits,
|
|
6
8
|
} from '../services/addressFormUtils';
|
|
@@ -9,7 +11,8 @@ const DEBOUNCE_MS = 450;
|
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* CEP lookup with debounce (8 digits), race-token protection and loading/error state.
|
|
12
|
-
*
|
|
14
|
+
* Clears number on lookup; clears derived address/coords on failure (#746).
|
|
15
|
+
* Preserves complement/nickname.
|
|
13
16
|
*/
|
|
14
17
|
export default function usePostalCodeLookup({
|
|
15
18
|
formCep,
|
|
@@ -20,6 +23,7 @@ export default function usePostalCodeLookup({
|
|
|
20
23
|
}) {
|
|
21
24
|
const [loadingCep, setLoadingCep] = useState(false);
|
|
22
25
|
const [cepError, setCepError] = useState(null);
|
|
26
|
+
const [geocodeMiss, setGeocodeMiss] = useState(false);
|
|
23
27
|
const requestIdRef = useRef(0);
|
|
24
28
|
const debounceRef = useRef(null);
|
|
25
29
|
const lastRequestedRef = useRef('');
|
|
@@ -35,12 +39,14 @@ export default function usePostalCodeLookup({
|
|
|
35
39
|
lastRequestedRef.current = digits;
|
|
36
40
|
setLoadingCep(true);
|
|
37
41
|
setCepError(null);
|
|
42
|
+
setGeocodeMiss(false);
|
|
38
43
|
|
|
39
44
|
try {
|
|
40
45
|
const data = await lookupPostalCode(digits);
|
|
41
46
|
if (requestId !== requestIdRef.current) {
|
|
42
47
|
return null; // stale response
|
|
43
48
|
}
|
|
49
|
+
setGeocodeMiss(isGeocodeMiss(data));
|
|
44
50
|
setForm(prev => {
|
|
45
51
|
const next = mergePostalCodeData(
|
|
46
52
|
{...prev, cep: digits},
|
|
@@ -76,6 +82,12 @@ export default function usePostalCodeLookup({
|
|
|
76
82
|
e?.response?.data?.detail ||
|
|
77
83
|
'CEP inválido ou serviço indisponível';
|
|
78
84
|
setCepError(message);
|
|
85
|
+
// CEP não encontrado: zera residual de endereço/coords/número (#746)
|
|
86
|
+
setForm(prev => {
|
|
87
|
+
const next = clearPostalCodeDerivedFields(prev);
|
|
88
|
+
onFormChange?.(next);
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
79
91
|
return null;
|
|
80
92
|
} finally {
|
|
81
93
|
if (requestId === requestIdRef.current) {
|
|
@@ -138,6 +150,8 @@ export default function usePostalCodeLookup({
|
|
|
138
150
|
loadingCep,
|
|
139
151
|
cepError,
|
|
140
152
|
setCepError,
|
|
153
|
+
geocodeMiss,
|
|
154
|
+
setGeocodeMiss,
|
|
141
155
|
onCepBlur,
|
|
142
156
|
runLookup,
|
|
143
157
|
cancelPending,
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
const onlyDigits = value => String(value || '').replace(/\D+/g, '');
|
|
2
2
|
|
|
3
|
-
/** Keep at most 8 CEP digits (Brazilian postal code). */
|
|
4
|
-
export const normalizeCepDigits = value => onlyDigits(value).slice(0, 8);
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Display mask #####-### (hyphen is visual only; max 8 digits).
|
|
8
|
-
* @example formatCepMask('01310100') => '01310-100'
|
|
9
|
-
*/
|
|
10
|
-
export const formatCepMask = value => {
|
|
11
|
-
const digits = normalizeCepDigits(value);
|
|
12
|
-
if (digits.length <= 5) {
|
|
13
|
-
return digits;
|
|
14
|
-
}
|
|
15
|
-
return `${digits.slice(0, 5)}-${digits.slice(5)}`;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
3
|
export const emptyAddressForm = {
|
|
19
4
|
nickname: '',
|
|
20
5
|
cep: '',
|
|
@@ -73,15 +58,21 @@ export function hasAddressText(form) {
|
|
|
73
58
|
}
|
|
74
59
|
|
|
75
60
|
export function hasCoordinates(form) {
|
|
61
|
+
const lat = Number(form?.latitude);
|
|
62
|
+
const lng = Number(form?.longitude);
|
|
63
|
+
// 0,0 is the API/entity default for "no coordinates"
|
|
76
64
|
return (
|
|
77
|
-
Number.isFinite(
|
|
78
|
-
Number.isFinite(
|
|
65
|
+
Number.isFinite(lat) &&
|
|
66
|
+
Number.isFinite(lng) &&
|
|
67
|
+
!(Math.abs(lat) < 0.000001 && Math.abs(lng) < 0.000001)
|
|
79
68
|
);
|
|
80
69
|
}
|
|
81
70
|
|
|
82
71
|
/**
|
|
83
72
|
* Merge postal-code API payload into form state.
|
|
84
|
-
*
|
|
73
|
+
* Preserves complement and nickname (manual).
|
|
74
|
+
* On CEP lookup (preserveFilledFields=false): clears number and replaces
|
|
75
|
+
* auto-filled address/coords — no residual from previous CEP (#746).
|
|
85
76
|
* When preserveFilledFields is true, also keeps non-empty street/city/etc.
|
|
86
77
|
*/
|
|
87
78
|
export function mergePostalCodeData(
|
|
@@ -89,36 +80,88 @@ export function mergePostalCodeData(
|
|
|
89
80
|
data,
|
|
90
81
|
{preserveFilledFields = false} = {},
|
|
91
82
|
) {
|
|
92
|
-
const
|
|
93
|
-
preserveFilledFields && String(prev[key] || '').trim()
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
const pick = (key, nextValue) => {
|
|
84
|
+
if (preserveFilledFields && String(prev[key] || '').trim()) {
|
|
85
|
+
return prev[key];
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
nextValue !== undefined &&
|
|
89
|
+
nextValue !== null &&
|
|
90
|
+
String(nextValue).trim() !== ''
|
|
91
|
+
) {
|
|
92
|
+
return nextValue;
|
|
93
|
+
}
|
|
94
|
+
// Fresh CEP lookup must not keep residual street/city/etc.
|
|
95
|
+
if (!preserveFilledFields) {
|
|
96
|
+
return nextValue == null ? '' : String(nextValue);
|
|
97
|
+
}
|
|
98
|
+
return prev[key] || '';
|
|
99
|
+
};
|
|
96
100
|
|
|
97
101
|
const countryRaw = data?.country;
|
|
98
102
|
const isBrazil =
|
|
99
103
|
countryRaw === 'Brasil' || countryRaw === 'Brazil' || countryRaw === 'BR';
|
|
100
104
|
|
|
105
|
+
const latitude =
|
|
106
|
+
data?.latitude !== undefined
|
|
107
|
+
? data.latitude
|
|
108
|
+
: data?.map?.latitude !== undefined
|
|
109
|
+
? data.map.latitude
|
|
110
|
+
: preserveFilledFields
|
|
111
|
+
? prev.latitude
|
|
112
|
+
: null;
|
|
113
|
+
const longitude =
|
|
114
|
+
data?.longitude !== undefined
|
|
115
|
+
? data.longitude
|
|
116
|
+
: data?.map?.longitude !== undefined
|
|
117
|
+
? data.map.longitude
|
|
118
|
+
: preserveFilledFields
|
|
119
|
+
? prev.longitude
|
|
120
|
+
: null;
|
|
121
|
+
|
|
101
122
|
return {
|
|
102
123
|
...prev,
|
|
103
|
-
//
|
|
104
|
-
number: prev.number,
|
|
124
|
+
// Número do imóvel anterior não se aplica ao novo CEP (#746)
|
|
125
|
+
number: preserveFilledFields ? prev.number : '',
|
|
105
126
|
complement: prev.complement,
|
|
106
127
|
nickname: prev.nickname,
|
|
107
|
-
cep:
|
|
108
|
-
street:
|
|
109
|
-
district:
|
|
110
|
-
city:
|
|
111
|
-
uf:
|
|
112
|
-
stateName:
|
|
113
|
-
countryCode: isBrazil ? 'BR' : countryRaw || prev.countryCode,
|
|
128
|
+
cep: onlyDigits(data?.cep || prev.cep),
|
|
129
|
+
street: pick('street', data?.street),
|
|
130
|
+
district: pick('district', data?.district),
|
|
131
|
+
city: pick('city', data?.city),
|
|
132
|
+
uf: pick('uf', data?.uf || data?.state),
|
|
133
|
+
stateName: pick('stateName', data?.state),
|
|
134
|
+
countryCode: isBrazil ? 'BR' : countryRaw || prev.countryCode || 'BR',
|
|
114
135
|
countryName: isBrazil
|
|
115
136
|
? 'Brazil'
|
|
116
|
-
: countryRaw || prev.countryName,
|
|
117
|
-
latitude
|
|
118
|
-
longitude
|
|
119
|
-
mapStaticUrl:
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
: countryRaw || prev.countryName || 'Brazil',
|
|
138
|
+
latitude,
|
|
139
|
+
longitude,
|
|
140
|
+
mapStaticUrl: preserveFilledFields
|
|
141
|
+
? data?.map?.staticUrl || prev.mapStaticUrl || null
|
|
142
|
+
: data?.map?.staticUrl || null,
|
|
143
|
+
facadeUrl: preserveFilledFields
|
|
144
|
+
? data?.facade?.streetViewUrl || prev.facadeUrl || null
|
|
145
|
+
: data?.facade?.streetViewUrl || null,
|
|
146
|
+
provider: data?.provider || (preserveFilledFields ? prev.provider : null) || null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Clear fields derived from a previous CEP when lookup fails (#746). */
|
|
151
|
+
export function clearPostalCodeDerivedFields(prev) {
|
|
152
|
+
return {
|
|
153
|
+
...prev,
|
|
154
|
+
number: '',
|
|
155
|
+
street: '',
|
|
156
|
+
district: '',
|
|
157
|
+
city: '',
|
|
158
|
+
uf: '',
|
|
159
|
+
stateName: '',
|
|
160
|
+
latitude: null,
|
|
161
|
+
longitude: null,
|
|
162
|
+
mapStaticUrl: null,
|
|
163
|
+
facadeUrl: null,
|
|
164
|
+
provider: null,
|
|
122
165
|
};
|
|
123
166
|
}
|
|
124
167
|
|
|
@@ -157,16 +200,39 @@ export function buildMapMarkerPayload(form) {
|
|
|
157
200
|
|
|
158
201
|
export {onlyDigits};
|
|
159
202
|
|
|
203
|
+
export const GEOCODE_MISS_MESSAGE =
|
|
204
|
+
'Não foi possível obter a localização no mapa para este endereço. Você pode ajustar latitude e longitude manualmente.';
|
|
205
|
+
|
|
160
206
|
export function parseOptionalCoordinate(value) {
|
|
161
207
|
if (value === null || value === undefined || value === '') {
|
|
162
208
|
return null;
|
|
163
209
|
}
|
|
164
210
|
|
|
165
|
-
const parsed = Number(String(value).trim());
|
|
211
|
+
const parsed = Number(String(value).trim().replace(',', '.'));
|
|
166
212
|
|
|
167
213
|
return Number.isFinite(parsed) ? parsed : null;
|
|
168
214
|
}
|
|
169
215
|
|
|
216
|
+
/** True when CEP/address text arrived but Nominatim (or API) left coords empty. */
|
|
217
|
+
export function isGeocodeMiss(data) {
|
|
218
|
+
if (!data || typeof data !== 'object') {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const hasText = [
|
|
222
|
+
data.street,
|
|
223
|
+
data.district,
|
|
224
|
+
data.city,
|
|
225
|
+
data.uf,
|
|
226
|
+
data.state,
|
|
227
|
+
].some(value => String(value || '').trim().length > 0);
|
|
228
|
+
if (!hasText) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const lat = data.latitude ?? data.map?.latitude;
|
|
232
|
+
const lon = data.longitude ?? data.map?.longitude;
|
|
233
|
+
return !Number.isFinite(Number(lat)) || !Number.isFinite(Number(lon));
|
|
234
|
+
}
|
|
235
|
+
|
|
170
236
|
export function getCurrentCoordinates() {
|
|
171
237
|
return new Promise(resolve => {
|
|
172
238
|
const geolocation =
|
|
@@ -151,7 +151,9 @@ export async function lookupPostalCode(cep) {
|
|
|
151
151
|
const viaCep = await lookupViaCep(digits);
|
|
152
152
|
payload = mergePostalLookupPayload(payload, viaCep, viaCep.provider);
|
|
153
153
|
} catch (e) {
|
|
154
|
-
|
|
154
|
+
// ViaCEP "CEP não encontrado" (or HTTP error) must surface so the form
|
|
155
|
+
// clears residual address/coords/number (#746) — do not swallow.
|
|
156
|
+
if (isPostalLookupIncomplete(payload)) {
|
|
155
157
|
throw new Error(
|
|
156
158
|
e?.message ||
|
|
157
159
|
apiError?.message ||
|
|
@@ -176,11 +178,12 @@ export async function lookupPostalCode(cep) {
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
// No usable address text after all providers → treat as not found (#746)
|
|
182
|
+
if (!hasText(payload.street) && !hasText(payload.city)) {
|
|
180
183
|
throw new Error(
|
|
181
184
|
apiError?.message ||
|
|
182
185
|
apiError?.response?.data?.detail ||
|
|
183
|
-
'CEP
|
|
186
|
+
'CEP não encontrado',
|
|
184
187
|
);
|
|
185
188
|
}
|
|
186
189
|
|
|
@@ -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()
|
|
@@ -141,11 +174,41 @@ export const buildDefaultVisibleColumns = columns =>
|
|
|
141
174
|
(Array.isArray(columns) ? columns : []).reduce((accumulator, column) => {
|
|
142
175
|
const key = getColumnKey(column);
|
|
143
176
|
if (key) {
|
|
144
|
-
accumulator[key] = column
|
|
177
|
+
accumulator[key] = isRequiredVisibleColumn(column)
|
|
178
|
+
? true
|
|
179
|
+
: column?.visible !== false;
|
|
145
180
|
}
|
|
146
181
|
return accumulator;
|
|
147
182
|
}, {});
|
|
148
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
|
+
|
|
149
212
|
export const sanitizeVisibleColumnsPreference = ({
|
|
150
213
|
columns = [],
|
|
151
214
|
visibleColumns = null,
|
|
@@ -153,16 +216,34 @@ export const sanitizeVisibleColumnsPreference = ({
|
|
|
153
216
|
const defaultVisibleColumns = buildDefaultVisibleColumns(columns);
|
|
154
217
|
|
|
155
218
|
if (!isPlainObject(visibleColumns)) {
|
|
156
|
-
return defaultVisibleColumns;
|
|
219
|
+
return enforceRequiredVisibleColumns(columns, defaultVisibleColumns);
|
|
157
220
|
}
|
|
158
221
|
|
|
159
|
-
|
|
222
|
+
const sanitized = Object.keys(defaultVisibleColumns).reduce((accumulator, key) => {
|
|
160
223
|
accumulator[key] =
|
|
161
224
|
typeof visibleColumns[key] === 'boolean'
|
|
162
225
|
? visibleColumns[key]
|
|
163
226
|
: defaultVisibleColumns[key];
|
|
164
227
|
return accumulator;
|
|
165
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;
|
|
166
247
|
};
|
|
167
248
|
|
|
168
249
|
export const sanitizeTableFiltersPreference = ({
|
|
@@ -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
|
|
|
@@ -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
|
+
});
|