@controleonline/ui-default 1.0.270 → 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/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -0
- package/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +46 -50
- package/docs/technical/Contrato-de-colunas.md +67 -0
- package/docs/technical/Default-Components.md +76 -0
- package/docs/technical/DefaultForm.md +80 -0
- package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
- package/docs/technical/DefaultTable.md +272 -0
- package/docs/wiki.md +1 -0
- package/package.json +4 -3
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +446 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
- package/src/react/components/address/DefaultAddressMapPane.js +55 -0
- package/src/react/components/address/LatLonReadonlyFields.js +51 -0
- package/src/react/components/address/defaultAddressHelpers.js +163 -0
- package/src/react/components/address/defaultAddressStyles.js +148 -0
- package/src/react/components/address/defaultAddressUtils.js +129 -0
- package/src/react/components/extra-data/DefaultExtraData.js +152 -0
- package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
- package/src/react/components/files/DefaultFile.js +231 -46
- package/src/react/components/files/DefaultFileColumn.js +123 -0
- package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
- package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
- package/src/react/components/filters/CompactFilterSelector.js +51 -35
- package/src/react/components/filters/DefaultColumnFilter.js +60 -48
- package/src/react/components/filters/DefaultExternalFilters.js +324 -265
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +48 -12
- package/src/react/components/help/DefaultTooltip.styles.js +20 -0
- package/src/react/components/inputs/DefaultColorInput.js +153 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
- package/src/react/components/inputs/DefaultInput.js +296 -141
- package/src/react/components/inputs/DefaultInput.styles.js +26 -0
- package/src/react/components/inputs/DefaultSelect.js +134 -123
- package/src/react/components/inputs/defaultInputUtils.js +262 -213
- package/src/react/components/map/DefaultMap.native.js +73 -73
- package/src/react/components/map/DefaultMap.shared.js +911 -762
- package/src/react/components/map/DefaultMap.web.js +124 -68
- package/src/react/components/table/DefaultColumnMenu.js +22 -3
- package/src/react/components/table/DefaultDebug.js +44 -37
- package/src/react/components/table/DefaultSearchModal.js +9 -1
- package/src/react/components/table/DefaultTable.grouping.js +98 -0
- package/src/react/components/table/DefaultTable.js +217 -111
- package/src/react/components/table/DefaultTable.styles.js +369 -310
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- package/src/react/components/table/DefaultTableBody.js +13 -8
- package/src/react/components/table/DefaultTableCards.js +20 -1
- package/src/react/components/table/DefaultTableControls.js +180 -205
- package/src/react/components/table/DefaultTableImportModal.js +108 -0
- package/src/react/components/table/DefaultTableInput.js +1 -0
- package/src/react/components/table/DefaultTableRowActions.js +1 -1
- package/src/react/components/table/DefaultTableRows.js +357 -325
- package/src/react/components/table/DefaultTableSearch.js +28 -5
- package/src/react/components/table/DefaultTableToolbar.js +92 -3
- package/src/react/components/table/useDefaultTablePagination.js +56 -7
- package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
- package/src/react/components/upload/DefaultUpload.js +406 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
- package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
- package/src/react/components/upload/defaultUploadHelpers.js +108 -0
- package/src/react/components/upload/defaultUploadLibrary.js +248 -0
- package/src/react/components/upload/fileUpload.js +93 -0
- package/src/react/hooks/usePostalCodeLookup.js +159 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +262 -0
- package/src/react/services/addressGeo.js +230 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +22 -6
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
- package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
- package/src/tests/react/components/table/DefaultTable.test.js +53 -2
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -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/table/resolveRowBackgroundColor.test.mjs +45 -0
- package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
- package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +179 -0
- package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
- package/src/tests/react/services/addressGeoLookup.test.js +105 -0
- package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
- package/src/tests/react/services/smoke-637/index.html +132 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
ScrollView,
|
|
5
|
+
Text,
|
|
6
|
+
TextInput,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import {MaterialCommunityIcons} from '@expo/vector-icons';
|
|
11
|
+
import AnimatedModal from '@controleonline/ui-common/src/react/components/AnimatedModal';
|
|
12
|
+
import DefaultFile from '@controleonline/ui-default/src/react/components/files/DefaultFile';
|
|
13
|
+
import {extractFileId} from './fileUpload';
|
|
14
|
+
import {
|
|
15
|
+
getFileName,
|
|
16
|
+
getFileExtension,
|
|
17
|
+
getContextLabel,
|
|
18
|
+
isPreviewableImage,
|
|
19
|
+
getGenericFileIcon,
|
|
20
|
+
} from './defaultUploadHelpers';
|
|
21
|
+
import {defaultUploadStyles as styles} from './DefaultUpload.styles';
|
|
22
|
+
|
|
23
|
+
function FileThumb({file, preferImage = false, company = null}) {
|
|
24
|
+
const fileId = extractFileId(file);
|
|
25
|
+
// people_media / image uploads often arrive as {id} without extension metadata
|
|
26
|
+
if (fileId && (preferImage || isPreviewableImage(file))) {
|
|
27
|
+
return (
|
|
28
|
+
<DefaultFile
|
|
29
|
+
file={file}
|
|
30
|
+
company={company}
|
|
31
|
+
resizeMode="cover"
|
|
32
|
+
style={styles.fileImage}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const ext = getFileExtension(file);
|
|
37
|
+
return (
|
|
38
|
+
<View style={styles.fileThumbFallback}>
|
|
39
|
+
<MaterialCommunityIcons name={getGenericFileIcon(file)} size={36} color="#475569" />
|
|
40
|
+
<Text style={styles.fileExtBadge}>{ext ? ext.toUpperCase() : 'FILE'}</Text>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function DefaultUploadManagerModal(props) {
|
|
46
|
+
const {
|
|
47
|
+
visible,
|
|
48
|
+
onClose,
|
|
49
|
+
managerTitle,
|
|
50
|
+
context,
|
|
51
|
+
buttonPalette,
|
|
52
|
+
librarySearch,
|
|
53
|
+
setLibrarySearch,
|
|
54
|
+
searchPlaceholder,
|
|
55
|
+
handleUpload,
|
|
56
|
+
uploading,
|
|
57
|
+
uploadButtonLabel,
|
|
58
|
+
libraryLoading,
|
|
59
|
+
loadLibrary,
|
|
60
|
+
attachLabel,
|
|
61
|
+
savingFileId,
|
|
62
|
+
loadingText,
|
|
63
|
+
libraryError,
|
|
64
|
+
filteredLibraryFiles,
|
|
65
|
+
attachedFileIds,
|
|
66
|
+
handleAttachExisting,
|
|
67
|
+
attachmentRows = [],
|
|
68
|
+
emptyLibraryLabel,
|
|
69
|
+
emptyAttachmentsLabel,
|
|
70
|
+
status,
|
|
71
|
+
error,
|
|
72
|
+
fileType = 'image',
|
|
73
|
+
company = null,
|
|
74
|
+
coverId = null,
|
|
75
|
+
handleSetCover = null,
|
|
76
|
+
handleRemove = null,
|
|
77
|
+
} = props;
|
|
78
|
+
const preferImage =
|
|
79
|
+
String(fileType || '').toLowerCase() === 'image' ||
|
|
80
|
+
String(context || '').toLowerCase() === 'people_media';
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<AnimatedModal visible={visible} onRequestClose={onClose}>
|
|
84
|
+
<View style={styles.modalContainer}>
|
|
85
|
+
<View style={styles.modalHeader}>
|
|
86
|
+
<View>
|
|
87
|
+
<Text style={styles.modalTitle}>{managerTitle}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
<TouchableOpacity onPress={onClose} style={styles.iconButton}>
|
|
90
|
+
<MaterialCommunityIcons name="close" size={22} color={buttonPalette.buttonIconSecondary} />
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
</View>
|
|
93
|
+
|
|
94
|
+
<View style={styles.toolbar}>
|
|
95
|
+
<View style={styles.searchBox}>
|
|
96
|
+
<MaterialCommunityIcons name="magnify" size={18} color="#94A3B8" />
|
|
97
|
+
<TextInput
|
|
98
|
+
value={librarySearch}
|
|
99
|
+
onChangeText={setLibrarySearch}
|
|
100
|
+
placeholder={searchPlaceholder}
|
|
101
|
+
placeholderTextColor="#94A3B8"
|
|
102
|
+
style={styles.searchInput}
|
|
103
|
+
/>
|
|
104
|
+
{!!librarySearch && (
|
|
105
|
+
<TouchableOpacity onPress={() => setLibrarySearch('')}>
|
|
106
|
+
<MaterialCommunityIcons name="close-circle" size={18} color="#94A3B8" />
|
|
107
|
+
</TouchableOpacity>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
<TouchableOpacity
|
|
111
|
+
onPress={handleUpload}
|
|
112
|
+
disabled={uploading}
|
|
113
|
+
style={[
|
|
114
|
+
styles.uploadButton,
|
|
115
|
+
{backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder, borderWidth: 1},
|
|
116
|
+
uploading && styles.disabledButton,
|
|
117
|
+
]}>
|
|
118
|
+
{uploading ? (
|
|
119
|
+
<ActivityIndicator size="small" color={buttonPalette.buttonIcon} />
|
|
120
|
+
) : (
|
|
121
|
+
<MaterialCommunityIcons name="cloud-upload-outline" size={18} color={buttonPalette.buttonIcon} />
|
|
122
|
+
)}
|
|
123
|
+
<Text style={[styles.uploadButtonText, {color: buttonPalette.buttonText}]}>
|
|
124
|
+
{uploading ? 'Enviando' : uploadButtonLabel}
|
|
125
|
+
</Text>
|
|
126
|
+
</TouchableOpacity>
|
|
127
|
+
<TouchableOpacity
|
|
128
|
+
onPress={loadLibrary}
|
|
129
|
+
disabled={libraryLoading}
|
|
130
|
+
style={[
|
|
131
|
+
styles.refreshButton,
|
|
132
|
+
{backgroundColor: buttonPalette.buttonBackgroundSecondary, borderColor: buttonPalette.buttonBorderSecondary},
|
|
133
|
+
]}>
|
|
134
|
+
<MaterialCommunityIcons name="refresh" size={19} color={buttonPalette.buttonIconSecondary} />
|
|
135
|
+
</TouchableOpacity>
|
|
136
|
+
</View>
|
|
137
|
+
|
|
138
|
+
{!!libraryError && <Text style={styles.modalError}>{libraryError}</Text>}
|
|
139
|
+
|
|
140
|
+
{libraryLoading ? (
|
|
141
|
+
<View style={styles.loadingState}>
|
|
142
|
+
<ActivityIndicator size="small" color="#0F172A" />
|
|
143
|
+
<Text style={styles.loadingText}>{loadingText}</Text>
|
|
144
|
+
</View>
|
|
145
|
+
) : filteredLibraryFiles.length === 0 ? (
|
|
146
|
+
<View style={styles.emptyState}>
|
|
147
|
+
<MaterialCommunityIcons name="file-search-outline" size={34} color="#CBD5E1" />
|
|
148
|
+
<Text style={styles.emptyText}>{emptyLibraryLabel}</Text>
|
|
149
|
+
</View>
|
|
150
|
+
) : (
|
|
151
|
+
<ScrollView style={styles.modalList} showsVerticalScrollIndicator={false}>
|
|
152
|
+
<View style={styles.libraryGrid}>
|
|
153
|
+
{filteredLibraryFiles.map(file => {
|
|
154
|
+
const fileId = extractFileId(file);
|
|
155
|
+
const isAttached = fileId && attachedFileIds.has(String(fileId));
|
|
156
|
+
const isSaving = String(savingFileId || '') === String(fileId || getFileName(file));
|
|
157
|
+
return (
|
|
158
|
+
<View key={fileId || getFileName(file)} style={[styles.fileCard, isAttached && styles.fileCardAttached]}>
|
|
159
|
+
<View style={styles.fileThumb}>
|
|
160
|
+
<FileThumb file={file} preferImage={preferImage} company={company} />
|
|
161
|
+
</View>
|
|
162
|
+
<View style={{flexDirection: 'row', gap: 6, position: 'absolute', top: 8, right: 8, zIndex: 2}}>
|
|
163
|
+
{isAttached ? (
|
|
164
|
+
<>
|
|
165
|
+
<TouchableOpacity
|
|
166
|
+
onPress={() => {
|
|
167
|
+
const row = (attachmentRows || []).find(
|
|
168
|
+
item => String(extractFileId(item?.file)) === String(fileId),
|
|
169
|
+
);
|
|
170
|
+
if (row) handleSetCover?.(row);
|
|
171
|
+
}}
|
|
172
|
+
style={styles.fileAction}
|
|
173
|
+
accessibilityLabel="Definir como principal">
|
|
174
|
+
<MaterialCommunityIcons
|
|
175
|
+
name={
|
|
176
|
+
(attachmentRows || []).some(
|
|
177
|
+
item =>
|
|
178
|
+
String(item?.id) === String(coverId) &&
|
|
179
|
+
String(extractFileId(item?.file)) === String(fileId),
|
|
180
|
+
)
|
|
181
|
+
? 'star'
|
|
182
|
+
: 'star-outline'
|
|
183
|
+
}
|
|
184
|
+
size={18}
|
|
185
|
+
color="#0F172A"
|
|
186
|
+
/>
|
|
187
|
+
</TouchableOpacity>
|
|
188
|
+
<TouchableOpacity
|
|
189
|
+
onPress={() => {
|
|
190
|
+
const row = (attachmentRows || []).find(
|
|
191
|
+
item => String(extractFileId(item?.file)) === String(fileId),
|
|
192
|
+
);
|
|
193
|
+
if (row) handleRemove?.(row);
|
|
194
|
+
}}
|
|
195
|
+
style={styles.fileAction}
|
|
196
|
+
accessibilityLabel="Remover">
|
|
197
|
+
<MaterialCommunityIcons name="trash-can-outline" size={18} color="#B91C1C" />
|
|
198
|
+
</TouchableOpacity>
|
|
199
|
+
</>
|
|
200
|
+
) : (
|
|
201
|
+
<TouchableOpacity
|
|
202
|
+
onPress={() => handleAttachExisting(file)}
|
|
203
|
+
disabled={isSaving}
|
|
204
|
+
style={styles.fileAction}>
|
|
205
|
+
{isSaving ? (
|
|
206
|
+
<ActivityIndicator size="small" color="#0F172A" />
|
|
207
|
+
) : (
|
|
208
|
+
<MaterialCommunityIcons name="plus" size={18} color="#0F172A" />
|
|
209
|
+
)}
|
|
210
|
+
</TouchableOpacity>
|
|
211
|
+
)}
|
|
212
|
+
</View>
|
|
213
|
+
<View style={styles.fileInfo}>
|
|
214
|
+
<Text numberOfLines={2} style={styles.fileName}>{getFileName(file)}</Text>
|
|
215
|
+
{Array.isArray(file?.mediaTypesUsed) && file.mediaTypesUsed.length > 0 ? (
|
|
216
|
+
<Text numberOfLines={2} style={styles.mediaTypesLine}>
|
|
217
|
+
{file.mediaTypesUsed.join(', ')}
|
|
218
|
+
</Text>
|
|
219
|
+
) : null}
|
|
220
|
+
<View style={styles.fileMetaRow}>
|
|
221
|
+
{(() => {
|
|
222
|
+
const label = getContextLabel(file?.context ?? file);
|
|
223
|
+
// Hide internal store contexts from end users
|
|
224
|
+
if (!label || label === 'sem contexto' || /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(label)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return <Text style={styles.contextBadge}>{label}</Text>;
|
|
228
|
+
})()}
|
|
229
|
+
{isAttached && <Text style={styles.attachedBadge}>{attachLabel}</Text>}
|
|
230
|
+
</View>
|
|
231
|
+
</View>
|
|
232
|
+
</View>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</View>
|
|
236
|
+
</ScrollView>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{attachmentRows.length === 0 && (
|
|
240
|
+
<Text style={styles.emptyText}>{emptyAttachmentsLabel}</Text>
|
|
241
|
+
)}
|
|
242
|
+
{!!status && <Text style={styles.attachmentsStatus}>{status}</Text>}
|
|
243
|
+
{!!error && <Text style={styles.attachmentsError}>{error}</Text>}
|
|
244
|
+
</View>
|
|
245
|
+
</AnimatedModal>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for DefaultUpload (app-community#296 modularization).
|
|
3
|
+
* task-432: getContextLabel accepts file object or context string.
|
|
4
|
+
*/
|
|
5
|
+
const {extractFileId} = require('./fileUpload');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIBRARY_CONTEXTS = ['products', 'products-category'];
|
|
8
|
+
const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)$/i;
|
|
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 =
|
|
19
|
+
relation?.id ||
|
|
20
|
+
relation?.['@id'] ||
|
|
21
|
+
relation?.mediaId ||
|
|
22
|
+
relation?.media?.id ||
|
|
23
|
+
relation?.media?.['@id'] ||
|
|
24
|
+
relation?.peopleMedia?.id ||
|
|
25
|
+
relation?.peopleMedia?.['@id'] ||
|
|
26
|
+
relation;
|
|
27
|
+
const match = String(value || '').match(/(\d+)$/);
|
|
28
|
+
return match ? match[1] : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const normalizeAttachmentRelation = relation => {
|
|
32
|
+
const relationId = getEntityId(relation);
|
|
33
|
+
if (!relation || typeof relation !== 'object' || relation?.id || !relationId) {
|
|
34
|
+
return relation;
|
|
35
|
+
}
|
|
36
|
+
return {...relation, id: relationId};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getRelationFileId = relation => extractFileId(relation?.file);
|
|
40
|
+
|
|
41
|
+
const getFileName = file => {
|
|
42
|
+
const id = extractFileId(file);
|
|
43
|
+
return file?.fileName || file?.name || file?.originalName || (id ? `Arquivo ${id}` : 'Arquivo');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getFileExtension = file => {
|
|
47
|
+
const fromField = String(file?.extension || '').replace(/^\./, '').trim().toLowerCase();
|
|
48
|
+
if (fromField) return fromField;
|
|
49
|
+
const name = getFileName(file);
|
|
50
|
+
const match = String(name).match(/\.([a-z0-9]+)$/i);
|
|
51
|
+
return match ? match[1].toLowerCase() : '';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isPreviewableImage = file => {
|
|
55
|
+
const type = String(file?.fileType || file?.mimeType || file?.type || '').toLowerCase();
|
|
56
|
+
if (type.startsWith('image/')) return true;
|
|
57
|
+
if (type === 'image') return true;
|
|
58
|
+
const ext = getFileExtension(file);
|
|
59
|
+
return IMAGE_EXT.test(`.${ext}`) || IMAGE_EXT.test(getFileName(file));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getGenericFileIcon = file => {
|
|
63
|
+
const ext = getFileExtension(file);
|
|
64
|
+
if (['pfx', 'p12', 'cer', 'crt'].includes(ext)) return 'file-certificate-outline';
|
|
65
|
+
if (['pdf'].includes(ext)) return 'file-pdf-box';
|
|
66
|
+
if (['xml', 'zip'].includes(ext)) return 'folder-zip-outline';
|
|
67
|
+
return 'file-outline';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getContextLabel = contextOrFile => {
|
|
71
|
+
if (contextOrFile == null || contextOrFile === '') return 'sem contexto';
|
|
72
|
+
if (typeof contextOrFile === 'object' && !Array.isArray(contextOrFile)) {
|
|
73
|
+
const fromFile =
|
|
74
|
+
contextOrFile.context ??
|
|
75
|
+
contextOrFile.fileContext ??
|
|
76
|
+
contextOrFile.file_context ??
|
|
77
|
+
'';
|
|
78
|
+
if (fromFile != null && fromFile !== '' && typeof fromFile !== 'object') {
|
|
79
|
+
return String(fromFile).trim() || 'sem contexto';
|
|
80
|
+
}
|
|
81
|
+
return 'sem contexto';
|
|
82
|
+
}
|
|
83
|
+
return String(contextOrFile).trim() || 'sem contexto';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const dedupeFiles = files => {
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
return files.filter(file => {
|
|
89
|
+
const id = extractFileId(file);
|
|
90
|
+
if (!id || seen.has(String(id))) return false;
|
|
91
|
+
seen.add(String(id));
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
DEFAULT_LIBRARY_CONTEXTS,
|
|
98
|
+
normalizeCollection,
|
|
99
|
+
getEntityId,
|
|
100
|
+
normalizeAttachmentRelation,
|
|
101
|
+
getRelationFileId,
|
|
102
|
+
getFileName,
|
|
103
|
+
getFileExtension,
|
|
104
|
+
isPreviewableImage,
|
|
105
|
+
getGenericFileIcon,
|
|
106
|
+
getContextLabel,
|
|
107
|
+
dedupeFiles,
|
|
108
|
+
};
|
|
@@ -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;
|