@controleonline/ui-common 1.2.85 → 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.85",
3
+ "version": "1.2.87",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
@@ -11,17 +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 DefaultUpload from '@controleonline/ui-default/src/react/components/upload/DefaultUpload';
14
+ import { useState } from 'react';
15
15
  import { resolveSystemErrorMessage } from '@controleonline/ui-common/src/react/utils/systemErrorMessage';
16
16
  import styles from './AddImportModal.styles';
17
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
+
18
31
  const AddImportModal = ({
19
32
  visible,
20
33
  onClose,
21
34
  onSuccess,
22
35
  context = {},
23
- allowedExtensions = [], // default will be set to ['csv'] if empty
24
- // acceptedTypes will be derived from allowedExtensions for security
36
+ allowedExtensions = [],
25
37
  helperLabel,
26
38
  modalTitle,
27
39
  fileLabel,
@@ -45,12 +57,14 @@ const AddImportModal = ({
45
57
  const _importSuccessLabel = importSuccessLabel ?? fallbackSuccessLabel;
46
58
  const _importErrorLabel = importErrorLabel ?? fallbackErrorLabel;
47
59
  const _helperLabel = helperLabel ?? fallbackInvalidFileLabel;
48
- // Resolve allowed extensions securely – default to CSV only
49
- const _allowedExtensions = (allowedExtensions && allowedExtensions.length > 0) ? allowedExtensions : ['csv'];
50
- // 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
+ );
51
66
  const _acceptedTypes = _allowedExtensions.map(ext => `.${ext}`).join(',');
52
- // Determine import type: prioritize prop, then infer from allowed extensions, default to 'csv'
53
- const _importType = importType ?? (_allowedExtensions.includes('xml') ? 'xml' : (_allowedExtensions.includes('zip') ? 'zip' : 'csv'));
67
+ const _importType = resolvedImportType;
54
68
  const { showError, showSuccess } = useMessage();
55
69
  const peopleStore = useStore('people');
56
70
  const importsStore = useStore('imports');
@@ -66,35 +80,66 @@ const AddImportModal = ({
66
80
  buttonIcon: themeColors.buttonIcon || themeColors.buttonText,
67
81
  };
68
82
 
83
+ const [showImportList, setShowImportList] = useState(false);
84
+ const [importResult, setImportResult] = useState(null);
85
+ const [uploading, setUploading] = useState(false);
69
86
 
70
-
71
- const handleUploadImportFile = async ({ file }) => {
72
- if (_allowedExtensions.length > 0) {
73
- const extensionRegex = new RegExp(`\\.(${_allowedExtensions.join('|')})$`, 'i');
74
- if (!file?.name?.toLowerCase().match(extensionRegex)) {
87
+ const handleUploadImportFile = async ({ file } = {}) => {
88
+ if (!file) {
89
+ if (typeof document === 'undefined') {
75
90
  throw new Error(_helperLabel);
76
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
+ });
77
101
  }
78
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);
114
+ }
115
+
116
+ setUploading(true);
79
117
  try {
80
118
  await importActions.uploadImportFile({
81
119
  file,
82
120
  importType: _importType,
83
121
  peopleId: currentCompany.id,
122
+ allowedExtensions: _allowedExtensions,
84
123
  });
85
124
  showSuccess(_importSuccessLabel);
125
+ setImportResult(true);
126
+ setShowImportList(true);
86
127
  if (onSuccess) onSuccess();
87
- handleClose();
88
128
  return file;
89
129
  } catch (error) {
90
130
  const importFeedback =
91
131
  resolveSystemErrorMessage(error) || _importErrorLabel;
92
132
  showError(importFeedback);
133
+ setImportResult(false);
93
134
  throw new Error(importFeedback);
135
+ } finally {
136
+ setUploading(false);
94
137
  }
95
138
  };
96
139
 
97
140
  const handleClose = () => {
141
+ setShowImportList(false);
142
+ setImportResult(null);
98
143
  onClose();
99
144
  };
100
145
 
@@ -112,52 +157,19 @@ const AddImportModal = ({
112
157
  </TouchableOpacity>
113
158
  </View>
114
159
 
115
- <ScrollView style={styles.content}>
116
- <Text style={styles.label}>{_fileLabel}</Text>
117
- <DefaultUpload
118
- relationField="import"
119
- relationResource="imports"
120
- entityId={context.context || 'import'}
121
- companyId={currentCompany.id}
122
- context={`imports-${context.context || 'default'}`}
123
- libraryContexts={[`imports-${context.context || 'default'}`]}
124
- acceptedTypes={_acceptedTypes}
125
- fileType=""
126
- title={_fileLabel}
127
- triggerLabel={_selectFileLabel}
128
- managerTitle={_modalTitle}
129
- searchPlaceholder={_selectFileLabel}
130
- uploadButtonLabel={_selectFileLabel}
131
- emptyAttachmentLabel=""
132
- emptyLibraryLabel={_selectFileLabel}
133
- showInlineContent={false}
134
- uploadResultAlreadyAttached
135
- requireEntity={false}
136
- onUploadFile={handleUploadImportFile}
137
- renderTrigger={({openManager, uploading}) => (
138
- <TouchableOpacity
139
- onPress={openManager}
140
- disabled={uploading}
141
- style={[
142
- styles.filePicker,
143
- {
144
- backgroundColor: buttonPalette.buttonBackground,
145
- borderColor: buttonPalette.buttonBorder,
146
- },
147
- ]}
148
- >
149
- <Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>
150
- {selectFileLabel}
151
- </Text>
152
- {uploading ? (
153
- <ActivityIndicator color={buttonPalette.buttonIcon} />
154
- ) : (
155
- <Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
156
- )}
157
- </TouchableOpacity>
160
+ <ScrollView style={styles.content} keyboardShouldPersistTaps="always">
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} />
158
171
  )}
159
- />
160
-
172
+ </TouchableOpacity>
161
173
  </ScrollView>
162
174
 
163
175
  <View style={styles.footer}>
@@ -171,4 +183,3 @@ const AddImportModal = ({
171
183
  };
172
184
 
173
185
  export default AddImportModal;
174
- // 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,6 +400,8 @@ 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);
@@ -407,4 +412,3 @@ const Imports = ({ context = {}, onClose }) => {
407
412
  };
408
413
 
409
414
  export default Imports;
410
- // 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