@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.
Files changed (89) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultMap.native.js +73 -73
  39. package/src/react/components/map/DefaultMap.shared.js +911 -762
  40. package/src/react/components/map/DefaultMap.web.js +124 -68
  41. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  42. package/src/react/components/table/DefaultDebug.js +44 -37
  43. package/src/react/components/table/DefaultSearchModal.js +9 -1
  44. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  45. package/src/react/components/table/DefaultTable.js +213 -107
  46. package/src/react/components/table/DefaultTable.styles.js +367 -314
  47. package/src/react/components/table/DefaultTableBody.js +13 -8
  48. package/src/react/components/table/DefaultTableCards.js +20 -1
  49. package/src/react/components/table/DefaultTableControls.js +180 -205
  50. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  51. package/src/react/components/table/DefaultTableInput.js +1 -0
  52. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  53. package/src/react/components/table/DefaultTableRows.js +224 -363
  54. package/src/react/components/table/DefaultTableSearch.js +28 -5
  55. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  56. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  57. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  58. package/src/react/components/upload/DefaultUpload.js +436 -0
  59. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  60. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  61. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  62. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  63. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  64. package/src/react/components/upload/fileUpload.js +93 -0
  65. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  66. package/src/react/index.js +6 -0
  67. package/src/react/services/addressFormUtils.js +196 -0
  68. package/src/react/services/addressGeo.js +227 -0
  69. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  70. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  71. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  72. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  73. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  74. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  75. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  76. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  77. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  78. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  79. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  80. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  81. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  82. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  83. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  84. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  85. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  86. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  87. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  88. package/src/tests/react/services/smoke-637/index.html +132 -0
  89. 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
+ };