@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.86",
3
+ "version": "1.2.87",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
@@ -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 = [], // default will be set to ['csv'] if empty
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
- // Resolve allowed extensions securely – default to CSV only
50
- const _allowedExtensions = (allowedExtensions && allowedExtensions.length > 0) ? allowedExtensions : ['csv'];
51
- // Build acceptedTypes string for file input (e.g., ".csv,.xml,.zip")
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
- // Determine import type: prioritize prop, then infer from allowed extensions, default to 'csv'
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); // true = success, false = error
84
+ const [importResult, setImportResult] = useState(null);
72
85
  const [uploading, setUploading] = useState(false);
73
86
 
74
- const handleUploadImportFile = async ({ file }) => {
75
- if (_allowedExtensions.length > 0) {
76
- const extensionRegex = new RegExp(`\\\\.(${_allowedExtensions.join('|')})$`, 'i');
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
- <TouchableOpacity
128
- onPress={handleUploadImportFile}
129
- style={[styles.filePicker, { backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
130
- >
131
- <Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>{_selectFileLabel}</Text>
132
- {uploading ? (
133
- <ActivityIndicator color={buttonPalette.buttonIcon} />
134
- ) : (
135
- <Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
136
- )}
137
- </TouchableOpacity>
138
- <TouchableOpacity
139
- onPress={() => setShowImportList(true)}
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(); // Formato local legível
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 || 'import.csv',
46
- type: file.mimeType || 'text/csv',
85
+ name: file.name || `import.${extension || allowedExtensions[0]}`,
86
+ type: file.mimeType || 'application/octet-stream',
47
87
  });
48
88
  }
49
89