@controleonline/ui-common 1.2.86 → 1.2.87
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/package.json
CHANGED
|
@@ -11,18 +11,29 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
|
11
11
|
import { useStore } from '@store';
|
|
12
12
|
import AnimatedModal from './AnimatedModal';
|
|
13
13
|
import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
|
|
14
|
-
import ImportsPage from '@controleonline/ui-common/src/react/pages/Imports';
|
|
15
14
|
import { useState } from 'react';
|
|
16
15
|
import { resolveSystemErrorMessage } from '@controleonline/ui-common/src/react/utils/systemErrorMessage';
|
|
17
16
|
import styles from './AddImportModal.styles';
|
|
18
17
|
|
|
18
|
+
const FORBIDDEN_EXTENSIONS = new Set(['*', '*.*', '', '.', '.*']);
|
|
19
|
+
|
|
20
|
+
const sanitizeAllowedExtensions = (extensions, importType) => {
|
|
21
|
+
const fallback =
|
|
22
|
+
importType === 'invoice_tax' || importType === 'xml' ? ['xml', 'zip'] : ['csv'];
|
|
23
|
+
const source = Array.isArray(extensions) && extensions.length > 0 ? extensions : fallback;
|
|
24
|
+
return [...new Set(
|
|
25
|
+
source
|
|
26
|
+
.map(item => String(item || '').trim().replace(/^\./, '').toLowerCase())
|
|
27
|
+
.filter(item => item && !FORBIDDEN_EXTENSIONS.has(item) && !item.includes('*')),
|
|
28
|
+
)];
|
|
29
|
+
};
|
|
30
|
+
|
|
19
31
|
const AddImportModal = ({
|
|
20
32
|
visible,
|
|
21
33
|
onClose,
|
|
22
34
|
onSuccess,
|
|
23
35
|
context = {},
|
|
24
|
-
allowedExtensions = [],
|
|
25
|
-
// acceptedTypes will be derived from allowedExtensions for security
|
|
36
|
+
allowedExtensions = [],
|
|
26
37
|
helperLabel,
|
|
27
38
|
modalTitle,
|
|
28
39
|
fileLabel,
|
|
@@ -46,12 +57,14 @@ const AddImportModal = ({
|
|
|
46
57
|
const _importSuccessLabel = importSuccessLabel ?? fallbackSuccessLabel;
|
|
47
58
|
const _importErrorLabel = importErrorLabel ?? fallbackErrorLabel;
|
|
48
59
|
const _helperLabel = helperLabel ?? fallbackInvalidFileLabel;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const resolvedImportType =
|
|
61
|
+
importType ?? context.context ?? (allowedExtensions.includes('xml') ? 'xml' : 'csv');
|
|
62
|
+
const _allowedExtensions = sanitizeAllowedExtensions(
|
|
63
|
+
allowedExtensions.length > 0 ? allowedExtensions : context.allowedExtensions,
|
|
64
|
+
resolvedImportType,
|
|
65
|
+
);
|
|
52
66
|
const _acceptedTypes = _allowedExtensions.map(ext => `.${ext}`).join(',');
|
|
53
|
-
|
|
54
|
-
const _importType = importType ?? (_allowedExtensions.includes('xml') ? 'xml' : (_allowedExtensions.includes('zip') ? 'zip' : 'csv'));
|
|
67
|
+
const _importType = resolvedImportType;
|
|
55
68
|
const { showError, showSuccess } = useMessage();
|
|
56
69
|
const peopleStore = useStore('people');
|
|
57
70
|
const importsStore = useStore('imports');
|
|
@@ -68,15 +81,36 @@ const AddImportModal = ({
|
|
|
68
81
|
};
|
|
69
82
|
|
|
70
83
|
const [showImportList, setShowImportList] = useState(false);
|
|
71
|
-
const [importResult, setImportResult] = useState(null);
|
|
84
|
+
const [importResult, setImportResult] = useState(null);
|
|
72
85
|
const [uploading, setUploading] = useState(false);
|
|
73
86
|
|
|
74
|
-
const handleUploadImportFile = async ({ file }) => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
if (!file?.name?.toLowerCase().match(extensionRegex)) {
|
|
87
|
+
const handleUploadImportFile = async ({ file } = {}) => {
|
|
88
|
+
if (!file) {
|
|
89
|
+
if (typeof document === 'undefined') {
|
|
78
90
|
throw new Error(_helperLabel);
|
|
79
91
|
}
|
|
92
|
+
|
|
93
|
+
file = await new Promise((resolve, reject) => {
|
|
94
|
+
const input = document.createElement('input');
|
|
95
|
+
input.type = 'file';
|
|
96
|
+
input.accept = _acceptedTypes;
|
|
97
|
+
input.onchange = () => resolve(input.files?.[0] || null);
|
|
98
|
+
input.onerror = () => reject(new Error(_helperLabel));
|
|
99
|
+
input.click();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!file) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (_allowedExtensions.length === 0) {
|
|
108
|
+
throw new Error('Importar *.* nao e permitido.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const extensionRegex = new RegExp(`\\.(${_allowedExtensions.join('|')})$`, 'i');
|
|
112
|
+
if (!file?.name?.toLowerCase().match(extensionRegex)) {
|
|
113
|
+
throw new Error(_helperLabel);
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
setUploading(true);
|
|
@@ -85,9 +119,9 @@ const AddImportModal = ({
|
|
|
85
119
|
file,
|
|
86
120
|
importType: _importType,
|
|
87
121
|
peopleId: currentCompany.id,
|
|
122
|
+
allowedExtensions: _allowedExtensions,
|
|
88
123
|
});
|
|
89
124
|
showSuccess(_importSuccessLabel);
|
|
90
|
-
// Indicate successful import and open import list
|
|
91
125
|
setImportResult(true);
|
|
92
126
|
setShowImportList(true);
|
|
93
127
|
if (onSuccess) onSuccess();
|
|
@@ -124,25 +158,19 @@ const AddImportModal = ({
|
|
|
124
158
|
</View>
|
|
125
159
|
|
|
126
160
|
<ScrollView style={styles.content} keyboardShouldPersistTaps="always">
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
style={[styles.filePicker, { marginTop: 10, backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
|
|
141
|
-
>
|
|
142
|
-
<Text style={[styles.fileName, { color: buttonPalette.buttonText }]}>{global.t?.t('imports', 'button', 'view_imports') || 'View Imports'}</Text>
|
|
143
|
-
</TouchableOpacity>
|
|
144
|
-
</ScrollView>
|
|
145
|
-
|
|
161
|
+
<Text style={styles.fileName}>{_fileLabel} ({_acceptedTypes})</Text>
|
|
162
|
+
<TouchableOpacity
|
|
163
|
+
onPress={() => handleUploadImportFile()}
|
|
164
|
+
style={[styles.filePicker, { backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
|
|
165
|
+
>
|
|
166
|
+
<Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>{_selectFileLabel}</Text>
|
|
167
|
+
{uploading ? (
|
|
168
|
+
<ActivityIndicator color={buttonPalette.buttonIcon} />
|
|
169
|
+
) : (
|
|
170
|
+
<Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
|
|
171
|
+
)}
|
|
172
|
+
</TouchableOpacity>
|
|
173
|
+
</ScrollView>
|
|
146
174
|
|
|
147
175
|
<View style={styles.footer}>
|
|
148
176
|
<TouchableOpacity onPress={handleClose} style={styles.secondaryButton}>
|
|
@@ -155,4 +183,3 @@ const AddImportModal = ({
|
|
|
155
183
|
};
|
|
156
184
|
|
|
157
185
|
export default AddImportModal;
|
|
158
|
-
// TODO(store-first): quando este arquivo for mexido, mover a leitura para stores e evitar chamadas HTTP diretas quando o store ja resolver isso.
|
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
|
|
28
28
|
const formatDate = (dateString) => {
|
|
29
29
|
const d = new Date(dateString);
|
|
30
|
-
return d.toLocaleString();
|
|
30
|
+
return d.toLocaleString();
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const Imports = ({ context = {}, onClose }) => {
|
|
@@ -57,6 +57,12 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
57
57
|
const importType = context.context;
|
|
58
58
|
const title = context.title;
|
|
59
59
|
const searchPlaceholder = context.searchPlaceholder;
|
|
60
|
+
const allowedExtensions = Array.isArray(context.allowedExtensions)
|
|
61
|
+
? context.allowedExtensions
|
|
62
|
+
: importType === 'invoice_tax' || importType === 'xml'
|
|
63
|
+
? ['xml', 'zip']
|
|
64
|
+
: ['csv'];
|
|
65
|
+
const showCsvTemplate = allowedExtensions.includes('csv');
|
|
60
66
|
const {
|
|
61
67
|
refreshLabel,
|
|
62
68
|
processingLabel,
|
|
@@ -255,11 +261,9 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
255
261
|
});
|
|
256
262
|
|
|
257
263
|
const csvText = typeof response === 'string' ? response : await response.text?.();
|
|
258
|
-
console.log('CSV recebido:', csvText.slice(0, 200));
|
|
259
264
|
|
|
260
265
|
if (!csvText) throw new Error('CSV vazio');
|
|
261
266
|
|
|
262
|
-
// mobile: verifica se o diretório está disponível
|
|
263
267
|
const dirUri = Directory?.cacheDocumentDirectory || Directory?.documentDirectory;
|
|
264
268
|
|
|
265
269
|
if (dirUri) {
|
|
@@ -272,21 +276,18 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
272
276
|
await Sharing.shareAsync(fileUri);
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
console.log(`Arquivo CSV criado em: ${fileUri}`);
|
|
276
279
|
return fileUri;
|
|
277
280
|
} else if (typeof window !== 'undefined') {
|
|
278
|
-
// fallback web
|
|
279
281
|
const blob = new Blob([csvText], { type: 'text/csv' });
|
|
280
282
|
const link = document.createElement('a');
|
|
281
283
|
link.href = URL.createObjectURL(blob);
|
|
282
284
|
link.download = `modelo_${importType}.csv`;
|
|
283
285
|
link.click();
|
|
284
286
|
URL.revokeObjectURL(link.href);
|
|
285
|
-
console.log('Download CSV disparado no web');
|
|
286
287
|
return 'web-download';
|
|
287
|
-
} else {
|
|
288
|
-
throw new Error('FileSystem não disponível');
|
|
289
288
|
}
|
|
289
|
+
|
|
290
|
+
throw new Error('FileSystem não disponível');
|
|
290
291
|
} catch (err) {
|
|
291
292
|
console.error('Erro detalhado ao baixar CSV:', err);
|
|
292
293
|
throw new Error('Falha ao baixar o modelo CSV');
|
|
@@ -306,6 +307,7 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
306
307
|
<View style={styles.subHeader}>
|
|
307
308
|
<View style={styles.topRow}>
|
|
308
309
|
<View style={styles.topRowActions}>
|
|
310
|
+
{showCsvTemplate ? (
|
|
309
311
|
<TouchableOpacity style={styles.downloadButton} onPress={downloadTemplate}>
|
|
310
312
|
<View
|
|
311
313
|
style={{
|
|
@@ -323,6 +325,7 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
323
325
|
<Text style={[styles.downloadText, { color: buttonPalette.buttonText }]}>Baixar Modelo</Text>
|
|
324
326
|
</View>
|
|
325
327
|
</TouchableOpacity>
|
|
328
|
+
) : null}
|
|
326
329
|
|
|
327
330
|
<TouchableOpacity
|
|
328
331
|
style={[
|
|
@@ -397,16 +400,15 @@ const Imports = ({ context = {}, onClose }) => {
|
|
|
397
400
|
visible={showAddImportModal}
|
|
398
401
|
onClose={() => setShowAddImportModal(false)}
|
|
399
402
|
context={context}
|
|
403
|
+
importType={importType}
|
|
404
|
+
allowedExtensions={allowedExtensions}
|
|
400
405
|
onSuccess={() => {
|
|
401
406
|
setCurrentPage(1);
|
|
402
407
|
fetchImports(searchQuery, 1);
|
|
403
|
-
if (onImportComplete) onImportComplete();
|
|
404
408
|
}}
|
|
405
|
-
onImportComplete={onImportComplete}
|
|
406
409
|
/>
|
|
407
410
|
</View>
|
|
408
411
|
);
|
|
409
412
|
};
|
|
410
413
|
|
|
411
414
|
export default Imports;
|
|
412
|
-
// TODO(store-first): quando este arquivo for mexido, mover a leitura para stores, remover api.fetch e evitar repassar dados em objetos quando o store ja resolver isso.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {Platform} from 'react-native';
|
|
2
2
|
import {api} from '@controleonline/ui-common/src/api';
|
|
3
3
|
|
|
4
|
+
const FORBIDDEN_EXTENSIONS = new Set(['*', '*.*', '', '.', '.*']);
|
|
5
|
+
|
|
4
6
|
const extractId = value => {
|
|
5
7
|
if (value === null || value === undefined || value === '') {
|
|
6
8
|
return '';
|
|
@@ -15,11 +17,42 @@ const extractId = value => {
|
|
|
15
17
|
return match ? match[1] : '';
|
|
16
18
|
};
|
|
17
19
|
|
|
20
|
+
const normalizeExtension = value =>
|
|
21
|
+
String(value || '')
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^\./, '')
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
|
|
26
|
+
const fileExtension = file => {
|
|
27
|
+
const name = String(file?.name || file?.uri || '');
|
|
28
|
+
const match = name.toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
29
|
+
return match ? match[1] : '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sanitizeAllowedExtensions = (extensions, importType) => {
|
|
33
|
+
const fallback =
|
|
34
|
+
importType === 'invoice_tax' || importType === 'xml' ? ['xml', 'zip'] : ['csv'];
|
|
35
|
+
const source = Array.isArray(extensions) && extensions.length > 0 ? extensions : fallback;
|
|
36
|
+
const cleaned = source
|
|
37
|
+
.map(normalizeExtension)
|
|
38
|
+
.filter(item => item && !FORBIDDEN_EXTENSIONS.has(item) && !item.includes('*'));
|
|
39
|
+
|
|
40
|
+
if (cleaned.length === 0) {
|
|
41
|
+
throw new Error('Tipo de importacao sem extensao permitida. Importar *.* nao e permitido.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...new Set(cleaned)];
|
|
45
|
+
};
|
|
46
|
+
|
|
18
47
|
export const uploadImportFile = (_context, payload = {}) => {
|
|
19
48
|
const normalizedPayload = payload && typeof payload === 'object' ? payload : {};
|
|
20
49
|
const file = normalizedPayload.file;
|
|
21
50
|
const importType = String(normalizedPayload.importType || '').trim();
|
|
22
51
|
const peopleId = extractId(normalizedPayload.peopleId || normalizedPayload.people);
|
|
52
|
+
const allowedExtensions = sanitizeAllowedExtensions(
|
|
53
|
+
normalizedPayload.allowedExtensions,
|
|
54
|
+
importType,
|
|
55
|
+
);
|
|
23
56
|
|
|
24
57
|
if (!file) {
|
|
25
58
|
throw new Error('Arquivo de importacao nao informado.');
|
|
@@ -33,6 +66,13 @@ export const uploadImportFile = (_context, payload = {}) => {
|
|
|
33
66
|
throw new Error('Empresa nao identificada para a importacao.');
|
|
34
67
|
}
|
|
35
68
|
|
|
69
|
+
const extension = fileExtension(file);
|
|
70
|
+
if (!allowedExtensions.includes(extension)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Extensao nao permitida. Aceitas: ${allowedExtensions.map(item => `.${item}`).join(', ')}.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
36
76
|
const formData = new FormData();
|
|
37
77
|
formData.append('importType', importType);
|
|
38
78
|
formData.append('people', peopleId);
|
|
@@ -42,8 +82,8 @@ export const uploadImportFile = (_context, payload = {}) => {
|
|
|
42
82
|
} else {
|
|
43
83
|
formData.append('file', {
|
|
44
84
|
uri: file.uri,
|
|
45
|
-
name: file.name ||
|
|
46
|
-
type: file.mimeType || '
|
|
85
|
+
name: file.name || `import.${extension || allowedExtensions[0]}`,
|
|
86
|
+
type: file.mimeType || 'application/octet-stream',
|
|
47
87
|
});
|
|
48
88
|
}
|
|
49
89
|
|