@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,248 @@
1
+ /**
2
+ * Library fetch helpers for DefaultUpload (app-community#296 / #433 / #670).
3
+ *
4
+ * Always send itemsPerPage so the API does not default to a tiny page size.
5
+ * Also hydrate known file ids (certificates are public=false and may not
6
+ * appear in the collection filter).
7
+ */
8
+ const DEFAULT_LIBRARY_CONTEXTS = ['products', 'products-category'];
9
+
10
+ const normalizeCollection = response => {
11
+ if (Array.isArray(response)) return response;
12
+ if (Array.isArray(response?.member)) return response.member;
13
+ if (Array.isArray(response?.['hydra:member'])) return response['hydra:member'];
14
+ return [];
15
+ };
16
+
17
+ const getEntityId = relation => {
18
+ const value = relation?.id || relation?.['@id'] || relation;
19
+ const match = String(value || '').match(/(\d+)$/);
20
+ return match ? match[1] : null;
21
+ };
22
+
23
+ const extractFileIdLocal = value => {
24
+ if (!value && value !== 0) return null;
25
+ if (typeof value === 'number') return value;
26
+ if (typeof value === 'object') {
27
+ return extractFileIdLocal(value.id || value['@id'] || value.fileId);
28
+ }
29
+ const match = String(value).match(/(\d+)$/);
30
+ return match ? parseInt(match[1], 10) : null;
31
+ };
32
+
33
+ const dedupeFiles = files => {
34
+ const seen = new Set();
35
+ return files.filter(file => {
36
+ const id = extractFileIdLocal(file);
37
+ if (!id || seen.has(String(id))) return false;
38
+ seen.add(String(id));
39
+ return true;
40
+ });
41
+ };
42
+
43
+ function resolvePeopleMediaTypeLabel(relation) {
44
+ const mediaType = relation?.mediaType;
45
+ if (!mediaType) return '';
46
+ if (typeof mediaType === 'string') {
47
+ // Prefer last path segment only when it looks like a label, not a numeric id
48
+ const segment = String(mediaType).split('/').filter(Boolean).pop() || '';
49
+ if (segment && !/^\d+$/.test(segment)) return segment.trim();
50
+ return '';
51
+ }
52
+ return String(
53
+ mediaType.type || mediaType.name || mediaType.label || mediaType.code || '',
54
+ ).trim();
55
+ }
56
+
57
+ function filesFromPeopleMediaRelations(relations) {
58
+ if (!Array.isArray(relations)) return [];
59
+ // Group by file id and collect media_types where the image is used (#814)
60
+ const byFileId = new Map();
61
+ for (const relation of relations) {
62
+ const file = relation?.file;
63
+ if (!file) continue;
64
+ const id = extractFileIdLocal(file);
65
+ if (!id) continue;
66
+ const typeLabel = resolvePeopleMediaTypeLabel(relation);
67
+ const key = String(id);
68
+ const existing = byFileId.get(key);
69
+ if (existing) {
70
+ if (typeLabel && !existing.mediaTypesUsed.includes(typeLabel)) {
71
+ existing.mediaTypesUsed.push(typeLabel);
72
+ }
73
+ continue;
74
+ }
75
+ // Always tag people_media library entries so the manager can preview as image
76
+ // even when API returns a bare IRI / minimal File payload.
77
+ const base =
78
+ typeof file === 'object'
79
+ ? {
80
+ ...file,
81
+ id: file.id || id,
82
+ '@id': file['@id'] || `/files/${id}`,
83
+ context: file.context || 'people_media',
84
+ fileType: file.fileType || file.mimeType || 'image',
85
+ fileName: file.fileName || file.name || file.originalName || `Arquivo ${id}`,
86
+ }
87
+ : {
88
+ id,
89
+ '@id': `/files/${id}`,
90
+ context: 'people_media',
91
+ fileType: 'image',
92
+ fileName: `Arquivo ${id}`,
93
+ };
94
+ byFileId.set(key, {
95
+ ...base,
96
+ mediaTypesUsed: typeLabel ? [typeLabel] : [],
97
+ });
98
+ }
99
+ return Array.from(byFileId.values()).map(file => ({
100
+ ...file,
101
+ mediaTypesUsed: Array.isArray(file.mediaTypesUsed)
102
+ ? [...file.mediaTypesUsed].sort((a, b) =>
103
+ String(a).localeCompare(String(b), 'pt-BR', {sensitivity: 'base'}),
104
+ )
105
+ : [],
106
+ }));
107
+ }
108
+
109
+ async function fetchPeopleMediaFiles({peopleActions, peopleIri}) {
110
+ if (!peopleIri || typeof peopleActions?.getPeopleMedia !== 'function') {
111
+ return [];
112
+ }
113
+ const pageSize = 100;
114
+ const maxPages = 10;
115
+ const collected = [];
116
+ for (let page = 1; page <= maxPages; page += 1) {
117
+ const response = await peopleActions.getPeopleMedia({
118
+ people: peopleIri,
119
+ page,
120
+ itemsPerPage: pageSize,
121
+ 'order[id]': 'DESC',
122
+ });
123
+ const pageItems = normalizeCollection(response);
124
+ collected.push(...pageItems);
125
+ if (pageItems.length < pageSize) break;
126
+ }
127
+ return filesFromPeopleMediaRelations(collected);
128
+ }
129
+
130
+ async function fetchKnownFiles({fileActions, knownFileIds = []}) {
131
+ if (typeof fileActions?.get !== 'function') return [];
132
+ const ids = [...new Set(knownFileIds.map(extractFileIdLocal).filter(Boolean))];
133
+ const files = [];
134
+ for (const id of ids) {
135
+ try {
136
+ const item = await fileActions.get(id);
137
+ if (item && typeof item === 'object') files.push(item);
138
+ } catch (_) {
139
+ // item GET is best-effort so a missing id does not empty the library
140
+ }
141
+ }
142
+ return files;
143
+ }
144
+
145
+ function synthesizeKnownImageFiles(knownFileIds = [], fileType = 'image', context = 'people_media') {
146
+ const preferImage = String(fileType || '').toLowerCase() === 'image';
147
+ return knownFileIds
148
+ .map(extractFileIdLocal)
149
+ .filter(Boolean)
150
+ .map(id => ({
151
+ id,
152
+ '@id': `/files/${id}`,
153
+ context,
154
+ fileType: preferImage ? 'image' : fileType || undefined,
155
+ fileName: `Arquivo ${id}`,
156
+ }));
157
+ }
158
+
159
+ async function fetchLibraryFiles({
160
+ fileActions,
161
+ companyId,
162
+ fileType,
163
+ libraryContexts,
164
+ peopleActions = null,
165
+ knownFileIds = [],
166
+ }) {
167
+ const peopleIri = getEntityId(companyId) ? `/people/${getEntityId(companyId)}` : null;
168
+ const contexts = Array.isArray(libraryContexts) && libraryContexts.length
169
+ ? libraryContexts
170
+ : DEFAULT_LIBRARY_CONTEXTS;
171
+ const includesPeopleMedia = contexts.some(
172
+ c => String(c || '').trim().toLowerCase() === 'people_media',
173
+ );
174
+
175
+ // people_media is company-scoped via /people_media — never GET /files collection/item
176
+ // (those endpoints 404 for private company media while /download still works).
177
+ if (includesPeopleMedia) {
178
+ let files = [];
179
+ if (peopleIri) {
180
+ try {
181
+ files = await fetchPeopleMediaFiles({peopleActions, peopleIri});
182
+ } catch (_) {
183
+ files = [];
184
+ }
185
+ }
186
+ const existing = new Set(
187
+ files.map(file => String(extractFileIdLocal(file) || '')).filter(Boolean),
188
+ );
189
+ const stubs = synthesizeKnownImageFiles(knownFileIds, fileType, 'people_media').filter(
190
+ file => !existing.has(String(file.id)),
191
+ );
192
+ return dedupeFiles(files.concat(stubs));
193
+ }
194
+
195
+ if (typeof fileActions?.getItems !== 'function') {
196
+ return fetchKnownFiles({fileActions, knownFileIds});
197
+ }
198
+
199
+ const pageSize = 500;
200
+ const maxPages = 10;
201
+
202
+ const fetchContextFiles = async fileContext => {
203
+ const contextFiles = [];
204
+ for (let page = 1; page <= maxPages; page += 1) {
205
+ const params = {
206
+ context: fileContext,
207
+ page,
208
+ itemsPerPage: pageSize,
209
+ };
210
+ if (fileType) params.fileType = fileType;
211
+ if (peopleIri) params.people = peopleIri;
212
+ try {
213
+ const response = await fileActions.getItems(params);
214
+ const pageItems = normalizeCollection(response);
215
+ contextFiles.push(...pageItems);
216
+ if (pageItems.length < pageSize) break;
217
+ } catch (_) {
218
+ // Collection filter may 404 for some contexts — skip page
219
+ break;
220
+ }
221
+ }
222
+ return contextFiles;
223
+ };
224
+
225
+ const batches = await Promise.all(contexts.map(fetchContextFiles));
226
+ let files = batches.flat();
227
+
228
+ if (knownFileIds.length) {
229
+ // Prefer stubs over GET /files/{id} when item endpoint 404s for private files
230
+ const knownFiles = await fetchKnownFiles({fileActions, knownFileIds});
231
+ if (knownFiles.length) {
232
+ files = knownFiles.concat(files);
233
+ } else {
234
+ files = synthesizeKnownImageFiles(knownFileIds, fileType, contexts[0]).concat(files);
235
+ }
236
+ }
237
+
238
+ return dedupeFiles(files);
239
+ }
240
+
241
+ module.exports = {
242
+ fetchLibraryFiles,
243
+ filesFromPeopleMediaRelations,
244
+ resolvePeopleMediaTypeLabel,
245
+ fetchPeopleMediaFiles,
246
+ fetchKnownFiles,
247
+ synthesizeKnownImageFiles,
248
+ };
@@ -0,0 +1,93 @@
1
+ import {Platform} from 'react-native';
2
+ import * as DocumentPicker from 'expo-document-picker';
3
+ import {env as APP_ENV} from '@env';
4
+
5
+ const extractId = value => {
6
+ if (!value && value !== 0) return null;
7
+ if (typeof value === 'number') return value;
8
+
9
+ const raw = typeof value === 'string' ? value : value?.id || value?.['@id'];
10
+ if (!raw) return null;
11
+
12
+ const match = String(raw).match(/(\d+)$/);
13
+ return match ? parseInt(match[1], 10) : null;
14
+ };
15
+
16
+ export const uploadFileToApi = async ({file, context = 'products', peopleId, entityId}) => {
17
+ const session = JSON.parse(localStorage.getItem('session') || '{}');
18
+ const token = session?.api_key || session?.token;
19
+ if (!token) throw new Error('Sessao invalida para upload.');
20
+
21
+ const apiEntryPoint = String(APP_ENV?.API_ENTRYPOINT || '').replace(/\/$/, '');
22
+ const host = APP_ENV?.DOMAIN || (typeof location !== 'undefined' ? location.host : '');
23
+ if (!apiEntryPoint) throw new Error('API_ENTRYPOINT nao configurado.');
24
+
25
+ const formData = new FormData();
26
+
27
+ if (Platform.OS === 'web') {
28
+ formData.append('file', file);
29
+ } else {
30
+ formData.append('file', {
31
+ uri: file.uri,
32
+ name: file.name || 'imagem.jpg',
33
+ type: file.mimeType || file.type || 'image/jpeg',
34
+ });
35
+ }
36
+
37
+ formData.append('context', context);
38
+ if (peopleId) formData.append('people', String(extractId(peopleId)));
39
+ if (entityId) formData.append('id', String(extractId(entityId)));
40
+
41
+ const response = await fetch(`${apiEntryPoint}/files/upload`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'API-TOKEN': token,
45
+ 'App-Domain': host,
46
+ Accept: 'application/json',
47
+ },
48
+ body: formData,
49
+ });
50
+
51
+ const result = await response.json().catch(() => ({}));
52
+ if (!response.ok || result?.['@type'] === 'Error') {
53
+ throw new Error(result?.description || result?.message || 'Falha no upload do arquivo.');
54
+ }
55
+
56
+ return result;
57
+ };
58
+
59
+ export const selectFile = async (acceptedTypes = '*/*') => {
60
+ if (Platform.OS === 'web' && typeof document !== 'undefined') {
61
+ return new Promise(resolve => {
62
+ const input = document.createElement('input');
63
+ input.type = 'file';
64
+ if (acceptedTypes) {
65
+ input.accept = acceptedTypes;
66
+ }
67
+ input.onchange = event => resolve(event?.target?.files?.[0] || null);
68
+ input.click();
69
+ });
70
+ }
71
+
72
+ const result = await DocumentPicker.getDocumentAsync({
73
+ type: acceptedTypes || '*/*',
74
+ copyToCacheDirectory: true,
75
+ multiple: false,
76
+ });
77
+
78
+ if (result?.canceled) {
79
+ return null;
80
+ }
81
+
82
+ return result?.assets?.[0] || null;
83
+ };
84
+
85
+ export const toFileIri = fileObj => {
86
+ const iri = fileObj?.['@id'];
87
+ if (iri) return iri;
88
+
89
+ const id = extractId(fileObj?.id || fileObj);
90
+ return id ? `/files/${id}` : null;
91
+ };
92
+
93
+ export const extractFileId = extractId;
@@ -0,0 +1,145 @@
1
+ import {useCallback, useEffect, useRef, useState} from 'react';
2
+ import {lookupPostalCode, listStates} from '../services/addressGeo';
3
+ import {
4
+ mergePostalCodeData,
5
+ onlyDigits,
6
+ } from '../services/addressFormUtils';
7
+
8
+ const DEBOUNCE_MS = 450;
9
+
10
+ /**
11
+ * CEP lookup with debounce (8 digits), race-token protection and loading/error state.
12
+ * Never overwrites number/complement/nickname (via mergePostalCodeData).
13
+ */
14
+ export default function usePostalCodeLookup({
15
+ formCep,
16
+ setForm,
17
+ onFormChange,
18
+ setStates,
19
+ enabled = true,
20
+ }) {
21
+ const [loadingCep, setLoadingCep] = useState(false);
22
+ const [cepError, setCepError] = useState(null);
23
+ const requestIdRef = useRef(0);
24
+ const debounceRef = useRef(null);
25
+ const lastRequestedRef = useRef('');
26
+
27
+ const runLookup = useCallback(
28
+ async (rawCep, {preserveFilledFields = false} = {}) => {
29
+ const digits = onlyDigits(rawCep).slice(0, 8);
30
+ if (digits.length !== 8) {
31
+ return null;
32
+ }
33
+
34
+ const requestId = ++requestIdRef.current;
35
+ lastRequestedRef.current = digits;
36
+ setLoadingCep(true);
37
+ setCepError(null);
38
+
39
+ try {
40
+ const data = await lookupPostalCode(digits);
41
+ if (requestId !== requestIdRef.current) {
42
+ return null; // stale response
43
+ }
44
+ setForm(prev => {
45
+ const next = mergePostalCodeData(
46
+ {...prev, cep: digits},
47
+ data,
48
+ {preserveFilledFields},
49
+ );
50
+ onFormChange?.(next);
51
+ return next;
52
+ });
53
+ if (data?.uf || data?.state) {
54
+ try {
55
+ const country =
56
+ data.country === 'Brasil' ||
57
+ data.country === 'Brazil' ||
58
+ data.country === 'BR'
59
+ ? 'BR'
60
+ : data.country || 'BR';
61
+ const s = await listStates(country);
62
+ if (requestId === requestIdRef.current) {
63
+ setStates?.(s);
64
+ }
65
+ } catch {
66
+ // states optional
67
+ }
68
+ }
69
+ return data;
70
+ } catch (e) {
71
+ if (requestId !== requestIdRef.current) {
72
+ return null;
73
+ }
74
+ const message =
75
+ e?.message ||
76
+ e?.response?.data?.detail ||
77
+ 'CEP inválido ou serviço indisponível';
78
+ setCepError(message);
79
+ return null;
80
+ } finally {
81
+ if (requestId === requestIdRef.current) {
82
+ setLoadingCep(false);
83
+ }
84
+ }
85
+ },
86
+ [setForm, onFormChange, setStates],
87
+ );
88
+
89
+ // Debounced auto-lookup ONLY when CEP has exactly 8 digits (never partial).
90
+ useEffect(() => {
91
+ if (!enabled) {
92
+ return undefined;
93
+ }
94
+ const digits = onlyDigits(formCep).slice(0, 8);
95
+ if (digits.length !== 8) {
96
+ if (debounceRef.current) {
97
+ clearTimeout(debounceRef.current);
98
+ debounceRef.current = null;
99
+ }
100
+ return undefined;
101
+ }
102
+ if (digits === lastRequestedRef.current) {
103
+ return undefined;
104
+ }
105
+ if (debounceRef.current) {
106
+ clearTimeout(debounceRef.current);
107
+ }
108
+ debounceRef.current = setTimeout(() => {
109
+ runLookup(digits);
110
+ }, DEBOUNCE_MS);
111
+ return () => {
112
+ if (debounceRef.current) {
113
+ clearTimeout(debounceRef.current);
114
+ }
115
+ };
116
+ }, [formCep, enabled, runLookup]);
117
+
118
+ const onCepBlur = useCallback(() => {
119
+ const digits = onlyDigits(formCep);
120
+ if (digits.length !== 8) {
121
+ return;
122
+ }
123
+ if (debounceRef.current) {
124
+ clearTimeout(debounceRef.current);
125
+ }
126
+ runLookup(digits);
127
+ }, [formCep, runLookup]);
128
+
129
+ const cancelPending = useCallback(() => {
130
+ requestIdRef.current += 1;
131
+ if (debounceRef.current) {
132
+ clearTimeout(debounceRef.current);
133
+ }
134
+ setLoadingCep(false);
135
+ }, []);
136
+
137
+ return {
138
+ loadingCep,
139
+ cepError,
140
+ setCepError,
141
+ onCepBlur,
142
+ runLookup,
143
+ cancelPending,
144
+ };
145
+ }
@@ -1,10 +1,16 @@
1
1
  // React entrypoint kept after Vue removal.
2
2
  import DefaultFile from './components/files/DefaultFile';
3
+ import DefaultFeatherIconPicker from './components/inputs/DefaultFeatherIconPicker';
4
+ import DefaultUpload from './components/upload/DefaultUpload';
3
5
 
4
6
  const moduleEntry = {
5
7
  DefaultFile,
8
+ DefaultFeatherIconPicker,
9
+ DefaultUpload,
6
10
  };
7
11
 
8
12
  export {DefaultFile};
13
+ export {DefaultFeatherIconPicker};
14
+ export {DefaultUpload};
9
15
 
10
16
  export default moduleEntry;
@@ -0,0 +1,196 @@
1
+ const onlyDigits = value => String(value || '').replace(/\D+/g, '');
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
+ export const emptyAddressForm = {
19
+ nickname: '',
20
+ cep: '',
21
+ street: '',
22
+ number: '',
23
+ complement: '',
24
+ district: '',
25
+ city: '',
26
+ uf: '',
27
+ stateName: '',
28
+ countryCode: 'BR',
29
+ countryName: 'Brazil',
30
+ latitude: null,
31
+ longitude: null,
32
+ mapStaticUrl: null,
33
+ facadeUrl: null,
34
+ provider: null,
35
+ };
36
+
37
+ export function hydrateAddressFromRow(row) {
38
+ if (!row || typeof row !== 'object') {
39
+ return {...emptyAddressForm};
40
+ }
41
+ const street = row?.street?.street || row?.street || '';
42
+ const district = row?.street?.district?.district || row?.district || '';
43
+ const city = row?.street?.district?.city?.city || row?.city || '';
44
+ const stateEntity = row?.street?.district?.city?.state || row?.state;
45
+ const countryEntity = stateEntity?.country || row?.country;
46
+ return {
47
+ ...emptyAddressForm,
48
+ nickname: row?.nickname || '',
49
+ cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
50
+ street: typeof street === 'string' ? street : '',
51
+ number: row?.number != null ? String(row.number) : '',
52
+ complement: row?.complement || '',
53
+ district,
54
+ city,
55
+ uf: stateEntity?.uf || row?.uf || '',
56
+ stateName: stateEntity?.state || '',
57
+ countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
58
+ countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
59
+ latitude: row?.latitude ?? null,
60
+ longitude: row?.longitude ?? null,
61
+ };
62
+ }
63
+
64
+ export function hasAddressText(form) {
65
+ return [
66
+ form?.street,
67
+ form?.number,
68
+ form?.district,
69
+ form?.city,
70
+ form?.uf,
71
+ form?.cep,
72
+ ].some(value => String(value || '').trim().length > 0);
73
+ }
74
+
75
+ export function hasCoordinates(form) {
76
+ return (
77
+ Number.isFinite(Number(form?.latitude)) &&
78
+ Number.isFinite(Number(form?.longitude))
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Merge postal-code API payload into form state.
84
+ * Always preserves number, complement and nickname (manual fields).
85
+ * When preserveFilledFields is true, also keeps non-empty street/city/etc.
86
+ */
87
+ export function mergePostalCodeData(
88
+ prev,
89
+ data,
90
+ {preserveFilledFields = false} = {},
91
+ ) {
92
+ const keep = (key, nextValue) =>
93
+ preserveFilledFields && String(prev[key] || '').trim()
94
+ ? prev[key]
95
+ : nextValue || prev[key] || '';
96
+
97
+ const countryRaw = data?.country;
98
+ const isBrazil =
99
+ countryRaw === 'Brasil' || countryRaw === 'Brazil' || countryRaw === 'BR';
100
+
101
+ return {
102
+ ...prev,
103
+ // Manual fields never overwritten by lookup
104
+ number: prev.number,
105
+ complement: prev.complement,
106
+ nickname: prev.nickname,
107
+ cep: normalizeCepDigits(data?.cep || prev.cep),
108
+ street: keep('street', data?.street),
109
+ district: keep('district', data?.district),
110
+ city: keep('city', data?.city),
111
+ uf: keep('uf', data?.uf || data?.state),
112
+ stateName: keep('stateName', data?.state),
113
+ countryCode: isBrazil ? 'BR' : countryRaw || prev.countryCode,
114
+ countryName: isBrazil
115
+ ? 'Brazil'
116
+ : countryRaw || prev.countryName,
117
+ latitude: data?.latitude ?? data?.map?.latitude ?? prev.latitude,
118
+ longitude: data?.longitude ?? data?.map?.longitude ?? prev.longitude,
119
+ mapStaticUrl: data?.map?.staticUrl || prev.mapStaticUrl || null,
120
+ facadeUrl: data?.facade?.streetViewUrl || prev.facadeUrl || null,
121
+ provider: data?.provider || prev.provider || null,
122
+ };
123
+ }
124
+
125
+ export function buildMapMarkerPayload(form) {
126
+ if (!hasAddressText(form) && !hasCoordinates(form)) {
127
+ return null;
128
+ }
129
+ const addressLine = [form.street, form.number].filter(Boolean).join(', ');
130
+ const addressExtra = [
131
+ form.district,
132
+ [form.city, form.uf].filter(Boolean).join(' - '),
133
+ form.countryName || form.countryCode,
134
+ form.cep,
135
+ ]
136
+ .filter(Boolean)
137
+ .join(' • ');
138
+
139
+ return {
140
+ id: 'default-address-preview',
141
+ title: form.nickname || 'Endereco',
142
+ addressLine: addressLine || addressExtra || 'Endereco',
143
+ addressExtra,
144
+ latitude: form.latitude,
145
+ longitude: form.longitude,
146
+ geocodeQuery: [
147
+ addressLine,
148
+ form.district,
149
+ [form.city, form.uf].filter(Boolean).join(' - '),
150
+ form.countryName || form.countryCode,
151
+ form.cep,
152
+ ]
153
+ .filter(Boolean)
154
+ .join(', '),
155
+ };
156
+ }
157
+
158
+ export {onlyDigits};
159
+
160
+ export function parseOptionalCoordinate(value) {
161
+ if (value === null || value === undefined || value === '') {
162
+ return null;
163
+ }
164
+
165
+ const parsed = Number(String(value).trim());
166
+
167
+ return Number.isFinite(parsed) ? parsed : null;
168
+ }
169
+
170
+ export function getCurrentCoordinates() {
171
+ return new Promise(resolve => {
172
+ const geolocation =
173
+ typeof navigator !== 'undefined' ? navigator.geolocation : null;
174
+
175
+ if (!geolocation?.getCurrentPosition) {
176
+ resolve(null);
177
+ return;
178
+ }
179
+
180
+ geolocation.getCurrentPosition(
181
+ position => {
182
+ const latitude = Number(position?.coords?.latitude);
183
+ const longitude = Number(position?.coords?.longitude);
184
+
185
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
186
+ resolve(null);
187
+ return;
188
+ }
189
+
190
+ resolve({latitude, longitude});
191
+ },
192
+ () => resolve(null),
193
+ {enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
194
+ );
195
+ });
196
+ }