@controleonline/ui-default 1.0.270 → 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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -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 +203 -0
- package/docs/wiki.md +1 -0
- package/package.json +22 -21
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +435 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -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 +136 -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 +58 -41
- package/src/react/components/filters/DefaultExternalFilters.js +331 -265
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +59 -33
- package/src/react/components/help/DefaultTooltip.styles.js +23 -3
- 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 +69 -5
- 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 +69 -43
- 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 +109 -108
- 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 +213 -107
- package/src/react/components/table/DefaultTable.styles.js +367 -314
- 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 +224 -363
- 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 +436 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -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 +145 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +196 -0
- package/src/react/services/addressGeo.js +227 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +6 -6
- package/src/tests/react/components/form/DefaultForm.test.js +123 -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/DefaultTableControls.filters.test.mjs +21 -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 +82 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +176 -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 +84 -48
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {ScrollView, Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
+
import FeatherIcon from 'react-native-vector-icons/Feather';
|
|
4
|
+
import DefaultFile from '@controleonline/ui-default/src/react/components/files/DefaultFile';
|
|
5
|
+
import {extractFileId} from './fileUpload';
|
|
6
|
+
import {defaultUploadStyles as styles} from './DefaultUpload.styles';
|
|
7
|
+
|
|
8
|
+
/** Inline attachment cards for DefaultUpload (app-community#296 / #385). */
|
|
9
|
+
export default function DefaultUploadAttachmentsList({
|
|
10
|
+
title,
|
|
11
|
+
triggerContent,
|
|
12
|
+
status,
|
|
13
|
+
error,
|
|
14
|
+
sortedAttachments,
|
|
15
|
+
emptyAttachmentLabel,
|
|
16
|
+
coverId,
|
|
17
|
+
handleSetCover,
|
|
18
|
+
handleRemove,
|
|
19
|
+
buttonPalette,
|
|
20
|
+
managerModal,
|
|
21
|
+
company = null,
|
|
22
|
+
showAttachmentActions = true,
|
|
23
|
+
}) {
|
|
24
|
+
const iconDanger =
|
|
25
|
+
buttonPalette?.iconDanger ||
|
|
26
|
+
buttonPalette?.textDanger ||
|
|
27
|
+
'#B91C1C';
|
|
28
|
+
const iconActive =
|
|
29
|
+
buttonPalette?.iconActive ||
|
|
30
|
+
buttonPalette?.buttonBackground ||
|
|
31
|
+
'#0F172A';
|
|
32
|
+
const iconIdle =
|
|
33
|
+
buttonPalette?.buttonIcon ||
|
|
34
|
+
buttonPalette?.buttonTextSecondary ||
|
|
35
|
+
'#64748B';
|
|
36
|
+
const actionBg =
|
|
37
|
+
buttonPalette?.buttonBackgroundSecondary ||
|
|
38
|
+
buttonPalette?.cardBackground ||
|
|
39
|
+
'#F8FAFC';
|
|
40
|
+
const actionBorder =
|
|
41
|
+
buttonPalette?.buttonBorderSecondary ||
|
|
42
|
+
buttonPalette?.cardBorder ||
|
|
43
|
+
'#E2E8F0';
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={styles.attachmentsTitleRow}>
|
|
47
|
+
<View style={styles.attachmentsHeader}>
|
|
48
|
+
<Text style={styles.attachmentsTitle}>{title}</Text>
|
|
49
|
+
{triggerContent}
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
{!!status && <Text style={styles.attachmentsStatus}>{status}</Text>}
|
|
53
|
+
{!!error && <Text style={styles.attachmentsError}>{error}</Text>}
|
|
54
|
+
|
|
55
|
+
{sortedAttachments.length === 0 ? (
|
|
56
|
+
<View style={styles.attachmentsEmpty}>
|
|
57
|
+
<Text style={styles.attachmentsEmptyText}>{emptyAttachmentLabel}</Text>
|
|
58
|
+
</View>
|
|
59
|
+
) : (
|
|
60
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
61
|
+
<View style={styles.attachmentsList}>
|
|
62
|
+
{sortedAttachments.map((row, index) => {
|
|
63
|
+
// people_media rows must use nested file only — never the relation as file
|
|
64
|
+
// (relation id would be mistaken for file id and break the preview).
|
|
65
|
+
const rawFile = row?.file;
|
|
66
|
+
const fileId = extractFileId(rawFile);
|
|
67
|
+
const file =
|
|
68
|
+
rawFile && typeof rawFile === 'object' && !Array.isArray(rawFile)
|
|
69
|
+
? rawFile
|
|
70
|
+
: fileId
|
|
71
|
+
? {id: fileId, '@id': `/files/${fileId}`}
|
|
72
|
+
: null;
|
|
73
|
+
const isCover = String(coverId) === String(row.id);
|
|
74
|
+
return (
|
|
75
|
+
<View
|
|
76
|
+
key={row.id || fileId || index}
|
|
77
|
+
style={styles.attachmentCard}>
|
|
78
|
+
<View style={styles.attachmentThumb}>
|
|
79
|
+
{file ? (
|
|
80
|
+
<DefaultFile
|
|
81
|
+
file={file}
|
|
82
|
+
company={company}
|
|
83
|
+
resizeMode="cover"
|
|
84
|
+
style={styles.attachmentImage}
|
|
85
|
+
/>
|
|
86
|
+
) : null}
|
|
87
|
+
</View>
|
|
88
|
+
{showAttachmentActions ? (
|
|
89
|
+
<View style={styles.attachmentActionsRow}>
|
|
90
|
+
<TouchableOpacity
|
|
91
|
+
onPress={() => handleSetCover(row)}
|
|
92
|
+
accessibilityLabel={
|
|
93
|
+
isCover ? 'Capa selecionada' : 'Definir como capa'
|
|
94
|
+
}
|
|
95
|
+
style={[
|
|
96
|
+
styles.attachmentIconButton,
|
|
97
|
+
{
|
|
98
|
+
backgroundColor: actionBg,
|
|
99
|
+
borderColor: isCover ? iconActive : actionBorder,
|
|
100
|
+
borderWidth: 1,
|
|
101
|
+
},
|
|
102
|
+
]}>
|
|
103
|
+
<FeatherIcon
|
|
104
|
+
name="star"
|
|
105
|
+
size={16}
|
|
106
|
+
color={isCover ? iconActive : iconIdle}
|
|
107
|
+
/>
|
|
108
|
+
</TouchableOpacity>
|
|
109
|
+
<TouchableOpacity
|
|
110
|
+
onPress={() => handleRemove(row)}
|
|
111
|
+
accessibilityLabel="Remover"
|
|
112
|
+
style={[
|
|
113
|
+
styles.attachmentIconButton,
|
|
114
|
+
{
|
|
115
|
+
backgroundColor: actionBg,
|
|
116
|
+
borderColor: actionBorder,
|
|
117
|
+
borderWidth: 1,
|
|
118
|
+
},
|
|
119
|
+
]}>
|
|
120
|
+
<FeatherIcon
|
|
121
|
+
name="trash-2"
|
|
122
|
+
size={16}
|
|
123
|
+
color={iconDanger}
|
|
124
|
+
/>
|
|
125
|
+
</TouchableOpacity>
|
|
126
|
+
</View>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</View>
|
|
132
|
+
</ScrollView>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{managerModal}
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -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
|
+
};
|