@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.
Files changed (100) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/.github/workflows/publish-npm.yml +91 -0
  4. package/AGENTS.md +46 -50
  5. package/docs/technical/Contrato-de-colunas.md +67 -0
  6. package/docs/technical/Default-Components.md +76 -0
  7. package/docs/technical/DefaultForm.md +80 -0
  8. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  9. package/docs/technical/DefaultTable.md +272 -0
  10. package/docs/wiki.md +1 -0
  11. package/package.json +4 -3
  12. package/scripts/validate-wiki-pointer.cjs +13 -0
  13. package/src/react/components/address/DefaultAddress.js +446 -0
  14. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  15. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  16. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  17. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  18. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  19. package/src/react/components/address/defaultAddressStyles.js +148 -0
  20. package/src/react/components/address/defaultAddressUtils.js +129 -0
  21. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  22. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  23. package/src/react/components/files/DefaultFile.js +231 -46
  24. package/src/react/components/files/DefaultFileColumn.js +123 -0
  25. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  26. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  27. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  28. package/src/react/components/filters/DefaultColumnFilter.js +60 -48
  29. package/src/react/components/filters/DefaultExternalFilters.js +324 -265
  30. package/src/react/components/filters/filterValue.js +65 -0
  31. package/src/react/components/form/DefaultForm.js +99 -79
  32. package/src/react/components/help/DefaultTooltip.js +48 -12
  33. package/src/react/components/help/DefaultTooltip.styles.js +20 -0
  34. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  35. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  36. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  37. package/src/react/components/inputs/DefaultInput.js +296 -141
  38. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  39. package/src/react/components/inputs/DefaultSelect.js +134 -123
  40. package/src/react/components/inputs/defaultInputUtils.js +262 -213
  41. package/src/react/components/map/DefaultMap.native.js +73 -73
  42. package/src/react/components/map/DefaultMap.shared.js +911 -762
  43. package/src/react/components/map/DefaultMap.web.js +124 -68
  44. package/src/react/components/table/DefaultColumnMenu.js +22 -3
  45. package/src/react/components/table/DefaultDebug.js +44 -37
  46. package/src/react/components/table/DefaultSearchModal.js +9 -1
  47. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  48. package/src/react/components/table/DefaultTable.js +217 -111
  49. package/src/react/components/table/DefaultTable.styles.js +369 -310
  50. package/src/react/components/table/DefaultTable.utils.js +38 -4
  51. package/src/react/components/table/DefaultTableBody.js +13 -8
  52. package/src/react/components/table/DefaultTableCards.js +20 -1
  53. package/src/react/components/table/DefaultTableControls.js +180 -205
  54. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  55. package/src/react/components/table/DefaultTableInput.js +1 -0
  56. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  57. package/src/react/components/table/DefaultTableRows.js +357 -325
  58. package/src/react/components/table/DefaultTableSearch.js +28 -5
  59. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  60. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  61. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  62. package/src/react/components/upload/DefaultUpload.js +406 -0
  63. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  64. package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
  65. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  66. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  67. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  68. package/src/react/components/upload/fileUpload.js +93 -0
  69. package/src/react/hooks/usePostalCodeLookup.js +159 -0
  70. package/src/react/index.js +6 -0
  71. package/src/react/services/addressFormUtils.js +262 -0
  72. package/src/react/services/addressGeo.js +230 -0
  73. package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
  74. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  75. package/src/tests/react/components/files/DefaultFile.test.js +22 -6
  76. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  77. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  78. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  79. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  80. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  81. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  82. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  83. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  84. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  85. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  86. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  87. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  88. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  89. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
  90. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  91. package/src/tests/react/filters/filterValue.test.js +46 -0
  92. package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
  93. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  94. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  95. package/src/tests/react/services/addressFormUtils.test.js +179 -0
  96. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  97. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  98. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  99. package/src/tests/react/services/smoke-637/index.html +132 -0
  100. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
@@ -0,0 +1,406 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Text,
5
+ TouchableOpacity,
6
+ View,
7
+ } from 'react-native';
8
+ import {MaterialCommunityIcons} from '@expo/vector-icons';
9
+ import {useStore} from '@store';
10
+ import DefaultUploadAttachmentsList from './DefaultUploadAttachmentsList';
11
+ import DefaultUploadManagerModal from './DefaultUploadManagerModal';
12
+ import {defaultUploadStyles as styles} from './DefaultUpload.styles';
13
+ import {selectFile, uploadFileToApi, toFileIri, extractFileId} from './fileUpload';
14
+ import {
15
+ DEFAULT_LIBRARY_CONTEXTS,
16
+ getEntityId,
17
+ normalizeAttachmentRelation,
18
+ getRelationFileId,
19
+ getFileName,
20
+ dedupeFiles,
21
+ } from './defaultUploadHelpers';
22
+ import {fetchLibraryFiles} from './defaultUploadLibrary';
23
+
24
+ const DefaultUpload = ({
25
+ relationStoreName,
26
+ relationField,
27
+ relationResource,
28
+ attachments = [],
29
+ entityId,
30
+ companyId,
31
+ context = 'products',
32
+ libraryContexts = DEFAULT_LIBRARY_CONTEXTS,
33
+ fileStoreName = 'file',
34
+ onChanged,
35
+ coverRelationId,
36
+ onCoverChanged,
37
+ title = 'Imagens anexas',
38
+ triggerLabel = 'Gerenciar imagens',
39
+ managerTitle = 'Gerenciador de imagens',
40
+ searchPlaceholder = 'Buscar imagem',
41
+ uploadButtonLabel = 'Enviar nova',
42
+ loadingText = 'Carregando arquivos...',
43
+ emptyAttachmentLabel = 'Nenhum arquivo anexado.',
44
+ emptyLibraryLabel = 'Nenhum arquivo encontrado.',
45
+ saveBeforeLabel = 'Salve o registro antes de anexar arquivos.',
46
+ uploadSuccessMessage = 'Arquivo enviado e anexado.',
47
+ attachSuccessMessage = 'Arquivo anexado com sucesso.',
48
+ removeSuccessMessage = 'Arquivo removido.',
49
+ attachedLabel = 'Anexado',
50
+ fileType = 'image',
51
+ acceptedTypes = 'image/*',
52
+ fileTypeLabel = 'arquivo',
53
+ onBeforeOpen = null,
54
+ onUploadFile = null,
55
+ onAttachFile = null,
56
+ onRemoveAttachment = null,
57
+ renderTrigger = null,
58
+ requireEntity = true,
59
+ showInlineContent = true,
60
+ uploadResultAlreadyAttached = false,
61
+ knownFileIds = [],
62
+ }) => {
63
+ const relationStore = useStore(relationStoreName);
64
+ const fileStore = useStore(fileStoreName);
65
+ const themeStore = useStore('theme');
66
+ const relationActions = relationStore?.actions || {};
67
+ const fileActions = fileStore?.actions || {};
68
+ const themeColors = themeStore?.getters?.colors || {};
69
+
70
+ const buttonPalette = useMemo(
71
+ () => ({
72
+ buttonBackground: themeColors.buttonBackground,
73
+ buttonBorder: themeColors.buttonBorder,
74
+ buttonText: themeColors.buttonText,
75
+ buttonIcon: themeColors.buttonIcon || themeColors.buttonText,
76
+ buttonBackgroundSecondary: themeColors.buttonBackgroundSecondary,
77
+ buttonBorderSecondary: themeColors.buttonBorderSecondary,
78
+ buttonTextSecondary: themeColors.buttonTextSecondary,
79
+ buttonIconSecondary: themeColors.buttonIconSecondary || themeColors.buttonTextSecondary,
80
+ iconSuccess: themeColors.iconSuccess,
81
+ iconDanger: themeColors.iconDanger,
82
+ iconActive: themeColors.iconActive,
83
+ textDanger: themeColors.textDanger,
84
+ cardBackground: themeColors.cardBackground,
85
+ cardBorder: themeColors.cardBorder,
86
+ }),
87
+ [
88
+ themeColors.buttonBackground,
89
+ themeColors.buttonBackgroundSecondary,
90
+ themeColors.buttonBorder,
91
+ themeColors.buttonBorderSecondary,
92
+ themeColors.buttonIcon,
93
+ themeColors.buttonIconSecondary,
94
+ themeColors.buttonText,
95
+ themeColors.buttonTextSecondary,
96
+ themeColors.iconActive,
97
+ themeColors.iconDanger,
98
+ themeColors.iconSuccess,
99
+ themeColors.textDanger,
100
+ themeColors.cardBackground,
101
+ themeColors.cardBorder,
102
+ ],
103
+ );
104
+
105
+ const attachmentRows = Array.isArray(attachments) ? attachments : [];
106
+ const resolvedKnownIds = useMemo(() => {
107
+ const fromProp = Array.isArray(knownFileIds) ? knownFileIds : [knownFileIds];
108
+ const fromAttachments = attachmentRows.map(getRelationFileId);
109
+ return [...fromProp, ...fromAttachments].map(extractFileId).filter(Boolean);
110
+ }, [attachmentRows, knownFileIds]);
111
+ const resolvedKnownIdsKey = resolvedKnownIds.join('|');
112
+ const libraryContextsKey = Array.isArray(libraryContexts)
113
+ ? libraryContexts.join('|')
114
+ : String(libraryContexts || '');
115
+
116
+ const [uploading, setUploading] = useState(false);
117
+ const [error, setError] = useState('');
118
+ const [status, setStatus] = useState('');
119
+ const [coverId, setCoverId] = useState(coverRelationId || null);
120
+ const [managerOpen, setManagerOpen] = useState(false);
121
+ const [libraryFiles, setLibraryFiles] = useState([]);
122
+ const [libraryLoading, setLibraryLoading] = useState(false);
123
+ const [libraryError, setLibraryError] = useState('');
124
+ const [librarySearch, setLibrarySearch] = useState('');
125
+ const [savingFileId, setSavingFileId] = useState(null);
126
+
127
+ const sortedAttachments = useMemo(() => {
128
+ if (!coverId) return attachmentRows;
129
+ const index = attachmentRows.findIndex(item => String(item.id) === String(coverId));
130
+ if (index < 0) return attachmentRows;
131
+ const copy = [...attachmentRows];
132
+ const [selected] = copy.splice(index, 1);
133
+ copy.unshift(selected);
134
+ return copy;
135
+ }, [attachmentRows, coverId]);
136
+
137
+ const attachedFileIds = useMemo(
138
+ () =>
139
+ new Set(
140
+ [...attachmentRows.map(getRelationFileId), ...resolvedKnownIds]
141
+ .filter(Boolean)
142
+ .map(String),
143
+ ),
144
+ [attachmentRows, resolvedKnownIds],
145
+ );
146
+
147
+ const filteredLibraryFiles = useMemo(() => {
148
+ const query = String(librarySearch || '').trim().toLowerCase();
149
+ if (!query) return libraryFiles;
150
+ return libraryFiles.filter(file => {
151
+ const name = getFileName(file).toLowerCase();
152
+ const fileContext = String(file?.context || '').toLowerCase();
153
+ const type = String(file?.fileType || '').toLowerCase();
154
+ const label = String(fileTypeLabel || '').toLowerCase();
155
+ return name.includes(query) || fileContext.includes(query) || type.includes(query) || label.includes(query);
156
+ });
157
+ }, [fileTypeLabel, libraryFiles, librarySearch]);
158
+
159
+ useEffect(() => {
160
+ setCoverId(coverRelationId || null);
161
+ }, [coverRelationId]);
162
+
163
+ const peopleActionsForLibrary =
164
+ relationStoreName === 'people' && typeof relationActions?.getPeopleMedia === 'function'
165
+ ? relationActions
166
+ : null;
167
+
168
+ const loadLibrary = useCallback(async (extraFiles = []) => {
169
+ setLibraryLoading(true);
170
+ setLibraryError('');
171
+ try {
172
+ const files = await fetchLibraryFiles({
173
+ fileActions,
174
+ companyId,
175
+ fileType,
176
+ libraryContexts: libraryContexts || DEFAULT_LIBRARY_CONTEXTS,
177
+ peopleActions: peopleActionsForLibrary,
178
+ knownFileIds: resolvedKnownIds,
179
+ });
180
+ setLibraryFiles(dedupeFiles([...(Array.isArray(extraFiles) ? extraFiles : []), ...files]));
181
+ } catch (e) {
182
+ setLibraryError(e?.message || 'Falha ao carregar biblioteca de arquivos.');
183
+ setLibraryFiles(dedupeFiles(extraFiles));
184
+ } finally {
185
+ setLibraryLoading(false);
186
+ }
187
+ }, [companyId, fileActions, fileType, libraryContextsKey, peopleActionsForLibrary, resolvedKnownIdsKey]);
188
+
189
+ useEffect(() => {
190
+ if (managerOpen) {
191
+ if (typeof onBeforeOpen === 'function') onBeforeOpen();
192
+ void loadLibrary();
193
+ }
194
+ }, [loadLibrary, managerOpen, onBeforeOpen]);
195
+
196
+ const attachFileToEntity = useCallback(
197
+ async (fileObj, options = {}) => {
198
+ const {successMessage = attachSuccessMessage, closeManager = false} = options;
199
+ setError('');
200
+ setStatus('');
201
+ if (requireEntity && !entityId) {
202
+ setError(saveBeforeLabel);
203
+ return null;
204
+ }
205
+ const fileIri = toFileIri(fileObj);
206
+ if (!fileIri) throw new Error('Arquivo sem identificador.');
207
+ const fileId = extractFileId(fileObj);
208
+ if (fileId && attachedFileIds.has(String(fileId))) {
209
+ setStatus('Arquivo ja anexado.');
210
+ return null;
211
+ }
212
+ if (typeof onAttachFile === 'function') {
213
+ const savedRelation = await onAttachFile(fileObj);
214
+ setStatus(successMessage);
215
+ if (onChanged) await onChanged();
216
+ if (closeManager) setManagerOpen(false);
217
+ return savedRelation;
218
+ }
219
+ if (typeof relationActions.save !== 'function') {
220
+ throw new Error('Fluxo de vinculo indisponivel.');
221
+ }
222
+ try {
223
+ const savedRelation = await relationActions.save({
224
+ [relationField]: `/${relationResource}/${entityId}`,
225
+ file: fileIri,
226
+ });
227
+ setStatus(successMessage);
228
+ if (onChanged) await onChanged();
229
+ if (closeManager) setManagerOpen(false);
230
+ return savedRelation;
231
+ } catch (e) {
232
+ const message = String(e?.message || e?.response?.data?.detail || '');
233
+ if (/unique|duplicate|duplic/i.test(message)) {
234
+ setStatus('Arquivo ja anexado.');
235
+ if (onChanged) await onChanged();
236
+ return null;
237
+ }
238
+ throw e;
239
+ }
240
+ },
241
+ [attachSuccessMessage, attachedFileIds, entityId, onChanged, relationActions, relationField, relationResource, saveBeforeLabel, onAttachFile, requireEntity],
242
+ );
243
+
244
+ const handleUpload = useCallback(async () => {
245
+ setError('');
246
+ setStatus('');
247
+ if (requireEntity && !entityId) {
248
+ setError(saveBeforeLabel);
249
+ return;
250
+ }
251
+ const file = await selectFile(acceptedTypes);
252
+ if (!file) return;
253
+ try {
254
+ setUploading(true);
255
+ const uploadedFile =
256
+ typeof onUploadFile === 'function'
257
+ ? await onUploadFile({acceptedTypes, companyId, context, entityId, file})
258
+ : await uploadFileToApi({file, context, peopleId: companyId, entityId});
259
+ setLibraryFiles(current => dedupeFiles([uploadedFile, ...current]));
260
+ if (uploadResultAlreadyAttached) {
261
+ setStatus(uploadSuccessMessage);
262
+ if (onChanged) await onChanged();
263
+ await loadLibrary([uploadedFile]);
264
+ return;
265
+ }
266
+ await attachFileToEntity(uploadedFile, {successMessage: uploadSuccessMessage});
267
+ await loadLibrary([uploadedFile]);
268
+ } catch (e) {
269
+ setError(e?.message || 'Falha ao anexar arquivo.');
270
+ } finally {
271
+ setUploading(false);
272
+ }
273
+ }, [acceptedTypes, attachFileToEntity, companyId, context, entityId, loadLibrary, onChanged, onUploadFile, requireEntity, saveBeforeLabel, uploadSuccessMessage, uploadResultAlreadyAttached]);
274
+
275
+ const handleAttachExisting = useCallback(
276
+ async file => {
277
+ const fileId = extractFileId(file) || getFileName(file);
278
+ try {
279
+ setSavingFileId(fileId);
280
+ await attachFileToEntity(file, {successMessage: attachSuccessMessage, closeManager: true});
281
+ } catch (e) {
282
+ setError(e?.message || 'Falha ao anexar arquivo.');
283
+ } finally {
284
+ setSavingFileId(null);
285
+ }
286
+ },
287
+ [attachFileToEntity, attachSuccessMessage],
288
+ );
289
+
290
+ const handleRemove = useCallback(
291
+ async relation => {
292
+ try {
293
+ setError('');
294
+ setStatus('');
295
+ const attachmentRelation = normalizeAttachmentRelation(relation);
296
+ if (typeof onRemoveAttachment === 'function') {
297
+ await onRemoveAttachment(attachmentRelation);
298
+ const relationId = getEntityId(attachmentRelation);
299
+ if (String(coverId) === String(relationId)) setCoverId(null);
300
+ setStatus(removeSuccessMessage);
301
+ if (onChanged) await onChanged();
302
+ return;
303
+ }
304
+ const relationId = getEntityId(attachmentRelation);
305
+ if (!relationId) throw new Error('Anexo sem identificador para remocao.');
306
+ if (typeof relationActions.remove !== 'function') {
307
+ throw new Error('Fluxo de remocao indisponivel.');
308
+ }
309
+ await relationActions.remove(relationId);
310
+ if (String(coverId) === String(relationId)) setCoverId(null);
311
+ setStatus(removeSuccessMessage);
312
+ if (onChanged) await onChanged();
313
+ } catch (e) {
314
+ setError(e?.message || 'Falha ao remover arquivo.');
315
+ }
316
+ },
317
+ [coverId, onChanged, onRemoveAttachment, relationActions, removeSuccessMessage],
318
+ );
319
+
320
+ const handleSetCover = useCallback(
321
+ async row => {
322
+ setCoverId(row.id);
323
+ if (onCoverChanged) await onCoverChanged(row);
324
+ },
325
+ [onCoverChanged],
326
+ );
327
+
328
+ const openManager = () => setManagerOpen(true);
329
+ const triggerContent =
330
+ typeof renderTrigger === 'function'
331
+ ? renderTrigger({disabled: uploading, openManager, uploading})
332
+ : (
333
+ <TouchableOpacity
334
+ onPress={openManager}
335
+ disabled={uploading}
336
+ style={[styles.attachmentsTrigger, {backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder, borderWidth: 1}, uploading && styles.disabledButton]}>
337
+ <View style={styles.headerButtonContent}>
338
+ <MaterialCommunityIcons name="folder-image" size={16} color={buttonPalette.buttonIcon} />
339
+ <Text style={[styles.attachmentsTriggerText, {color: buttonPalette.buttonText}]}>{uploading ? 'Enviando...' : triggerLabel}</Text>
340
+ </View>
341
+ </TouchableOpacity>
342
+ );
343
+
344
+ const managerModal = (
345
+ <DefaultUploadManagerModal
346
+ visible={managerOpen}
347
+ onClose={() => setManagerOpen(false)}
348
+ managerTitle={managerTitle}
349
+ context={context}
350
+ buttonPalette={buttonPalette}
351
+ librarySearch={librarySearch}
352
+ setLibrarySearch={setLibrarySearch}
353
+ searchPlaceholder={searchPlaceholder}
354
+ handleUpload={handleUpload}
355
+ uploading={uploading}
356
+ uploadButtonLabel={uploadButtonLabel}
357
+ libraryLoading={libraryLoading}
358
+ loadLibrary={loadLibrary}
359
+ attachLabel={attachedLabel}
360
+ savingFileId={savingFileId}
361
+ loadingText={loadingText}
362
+ libraryError={libraryError}
363
+ filteredLibraryFiles={filteredLibraryFiles}
364
+ attachedFileIds={attachedFileIds}
365
+ handleAttachExisting={handleAttachExisting}
366
+ attachmentRows={attachmentRows}
367
+ sortedAttachments={sortedAttachments}
368
+ coverId={coverId}
369
+ handleSetCover={handleSetCover}
370
+ handleRemove={handleRemove}
371
+ emptyLibraryLabel={emptyLibraryLabel}
372
+ emptyAttachmentsLabel={emptyAttachmentLabel}
373
+ status={status}
374
+ error={error}
375
+ />
376
+ );
377
+
378
+ if (!showInlineContent) {
379
+ return (
380
+ <View>
381
+ {triggerContent}
382
+ {managerModal}
383
+ </View>
384
+ );
385
+ }
386
+
387
+ return (
388
+ <DefaultUploadAttachmentsList
389
+ title={title}
390
+ triggerContent={triggerContent}
391
+ status={status}
392
+ error={error}
393
+ sortedAttachments={sortedAttachments}
394
+ emptyAttachmentLabel={emptyAttachmentLabel}
395
+ coverId={coverId}
396
+ handleSetCover={handleSetCover}
397
+ handleRemove={handleRemove}
398
+ buttonPalette={buttonPalette}
399
+ managerModal={managerModal}
400
+ company={company}
401
+ showAttachmentActions={showAttachmentActions}
402
+ />
403
+ );
404
+ };
405
+
406
+ export default DefaultUpload;
@@ -0,0 +1,153 @@
1
+ export const defaultUploadStyles = {
2
+ disabledButton: {opacity: 0.65},
3
+ headerButtonContent: {flexDirection: 'row', alignItems: 'center', gap: 6},
4
+ modalContainer: {
5
+ backgroundColor: '#fff',
6
+ borderTopLeftRadius: 18,
7
+ borderTopRightRadius: 18,
8
+ paddingBottom: 24,
9
+ maxHeight: '86%',
10
+ },
11
+ modalHeader: {
12
+ flexDirection: 'row',
13
+ justifyContent: 'space-between',
14
+ alignItems: 'flex-start',
15
+ paddingHorizontal: 18,
16
+ paddingTop: 18,
17
+ paddingBottom: 12,
18
+ borderBottomWidth: 1,
19
+ borderBottomColor: '#E2E8F0',
20
+ },
21
+ modalTitle: {color: '#0F172A', fontSize: 17, fontWeight: '800'},
22
+ modalSubtitle: {color: '#64748B', fontSize: 12, marginTop: 3},
23
+ iconButton: {padding: 4},
24
+ toolbar: {flexDirection: 'row', alignItems: 'center', gap: 8, padding: 14},
25
+ searchBox: {
26
+ flex: 1,
27
+ minHeight: 42,
28
+ borderWidth: 1,
29
+ borderColor: '#E2E8F0',
30
+ borderRadius: 10,
31
+ paddingHorizontal: 10,
32
+ flexDirection: 'row',
33
+ alignItems: 'center',
34
+ gap: 8,
35
+ backgroundColor: '#F8FAFC',
36
+ },
37
+ searchInput: {flex: 1, minWidth: 80, color: '#0F172A', fontSize: 14},
38
+ uploadButton: {
39
+ minHeight: 42,
40
+ borderRadius: 10,
41
+ backgroundColor: '#0F172A',
42
+ paddingHorizontal: 12,
43
+ flexDirection: 'row',
44
+ alignItems: 'center',
45
+ gap: 6,
46
+ },
47
+ uploadButtonText: {color: '#fff', fontWeight: '700', fontSize: 13},
48
+ refreshButton: {
49
+ width: 42,
50
+ height: 42,
51
+ borderRadius: 10,
52
+ borderWidth: 1,
53
+ borderColor: '#E2E8F0',
54
+ alignItems: 'center',
55
+ justifyContent: 'center',
56
+ backgroundColor: '#fff',
57
+ },
58
+ modalError: {color: '#B91C1C', fontSize: 13, marginHorizontal: 16, marginBottom: 8},
59
+ loadingState: {minHeight: 170, alignItems: 'center', justifyContent: 'center', gap: 8},
60
+ loadingText: {color: '#64748B', fontSize: 13},
61
+ emptyState: {minHeight: 170, alignItems: 'center', justifyContent: 'center', padding: 20, gap: 8},
62
+ emptyText: {color: '#64748B', fontSize: 13, textAlign: 'center'},
63
+ modalList: {maxHeight: 520},
64
+ libraryGrid: {flexDirection: 'row', flexWrap: 'wrap', gap: 10, paddingHorizontal: 14, paddingBottom: 18},
65
+ fileCard: {
66
+ width: 320,
67
+ borderWidth: 1,
68
+ borderColor: '#E2E8F0',
69
+ borderRadius: 8,
70
+ backgroundColor: '#fff',
71
+ overflow: 'hidden',
72
+ },
73
+ fileCardAttached: {borderColor: '#86EFAC', backgroundColor: '#F0FDF4'},
74
+ fileThumb: {width: 320, height: 320, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center'},
75
+ fileThumbFallback: {alignItems: 'center', justifyContent: 'center', gap: 6, height: '100%', width: '100%', backgroundColor: '#EEF2FF'},
76
+ fileExtBadge: {
77
+ color: '#312E81',
78
+ fontSize: 11,
79
+ fontWeight: '800',
80
+ letterSpacing: 0.6,
81
+ backgroundColor: '#E0E7FF',
82
+ paddingHorizontal: 8,
83
+ paddingVertical: 3,
84
+ borderRadius: 999,
85
+ overflow: 'hidden',
86
+ },
87
+ fileImage: {width: '100%', height: '100%'},
88
+ fileInfo: {padding: 9, minHeight: 74},
89
+ fileName: {color: '#0F172A', fontSize: 12, fontWeight: '700', lineHeight: 16},
90
+ mediaTypesLine: {color: '#64748B', fontSize: 11, fontWeight: '600', lineHeight: 14, marginTop: 3},
91
+ fileMetaRow: {flexDirection: 'row', flexWrap: 'wrap', gap: 5, marginTop: 7},
92
+ contextBadge: {
93
+ color: '#475569',
94
+ backgroundColor: '#E2E8F0',
95
+ borderRadius: 999,
96
+ overflow: 'hidden',
97
+ paddingHorizontal: 7,
98
+ paddingVertical: 3,
99
+ fontSize: 10,
100
+ fontWeight: '700',
101
+ },
102
+ attachedBadge: {
103
+ color: '#15803D',
104
+ backgroundColor: '#DCFCE7',
105
+ borderRadius: 999,
106
+ overflow: 'hidden',
107
+ paddingHorizontal: 7,
108
+ paddingVertical: 3,
109
+ fontSize: 10,
110
+ fontWeight: '700',
111
+ },
112
+ fileAction: {
113
+ position: 'absolute',
114
+ top: 8,
115
+ right: 8,
116
+ width: 30,
117
+ height: 30,
118
+ borderRadius: 15,
119
+ backgroundColor: 'rgba(255,255,255,0.92)',
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ },
123
+ attachmentsTitleRow: {width: '100%', marginTop: 8},
124
+ attachmentsHeader: {flexDirection: 'row', alignItems: 'center', marginBottom: 8},
125
+ attachmentsTitle: {flex: 1, fontWeight: '600'},
126
+ attachmentsTrigger: {backgroundColor: '#000', paddingVertical: 8, paddingHorizontal: 12, borderRadius: 6},
127
+ attachmentsTriggerText: {color: '#fff'},
128
+ attachmentsStatus: {color: '#1b7f34', marginBottom: 6},
129
+ attachmentsError: {color: '#b00020', marginBottom: 6},
130
+ attachmentsEmpty: {borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 10},
131
+ attachmentsEmptyText: {color: '#666'},
132
+ attachmentsList: {flexDirection: 'row', gap: 10},
133
+ attachmentCard: {width: 320, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 0, overflow: 'hidden', backgroundColor: '#fff'},
134
+ attachmentThumb: {width: 320, height: 320, backgroundColor: '#f5f5f5', overflow: 'hidden', alignItems: 'center', justifyContent: 'center'},
135
+ attachmentImage: {width: 320, height: 320},
136
+ attachmentActionsRow: {flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, marginTop: 2},
137
+ attachmentIconButton: {width: 36, height: 36, borderRadius: 8, alignItems: 'center', justifyContent: 'center'},
138
+ attachmentRemoveButton: {backgroundColor: '#b00020', paddingVertical: 6, borderRadius: 4},
139
+ attachmentRemoveText: {textAlign: 'center', color: '#fff', fontSize: 12},
140
+ };
141
+
142
+ export const getAttachmentCoverButtonStyle = (coverId, row) => ({
143
+ backgroundColor: String(coverId) === String(row.id) ? '#00695c' : '#efefef',
144
+ paddingVertical: 6,
145
+ borderRadius: 4,
146
+ marginBottom: 6,
147
+ });
148
+
149
+ export const getAttachmentCoverTextStyle = (coverId, row) => ({
150
+ textAlign: 'center',
151
+ color: String(coverId) === String(row.id) ? '#fff' : '#111',
152
+ fontSize: 12,
153
+ });
@@ -0,0 +1,146 @@
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={[
78
+ styles.attachmentCard,
79
+ // app-community#813: card principal 320x320
80
+ {width: 320, padding: 0, overflow: 'hidden'},
81
+ ]}>
82
+ <View
83
+ style={[
84
+ styles.attachmentThumb,
85
+ {width: 320, height: 320, marginBottom: 0},
86
+ ]}>
87
+ {file ? (
88
+ <DefaultFile
89
+ file={file}
90
+ company={company}
91
+ resizeMode="cover"
92
+ style={{width: 320, height: 320}}
93
+ />
94
+ ) : null}
95
+ </View>
96
+ {showAttachmentActions ? (
97
+ <View style={styles.attachmentActionsRow}>
98
+ <TouchableOpacity
99
+ onPress={() => handleSetCover(row)}
100
+ accessibilityLabel={
101
+ isCover ? 'Capa selecionada' : 'Definir como capa'
102
+ }
103
+ style={[
104
+ styles.attachmentIconButton,
105
+ {
106
+ backgroundColor: actionBg,
107
+ borderColor: isCover ? iconActive : actionBorder,
108
+ borderWidth: 1,
109
+ },
110
+ ]}>
111
+ <FeatherIcon
112
+ name="star"
113
+ size={16}
114
+ color={isCover ? iconActive : iconIdle}
115
+ />
116
+ </TouchableOpacity>
117
+ <TouchableOpacity
118
+ onPress={() => handleRemove(row)}
119
+ accessibilityLabel="Remover"
120
+ style={[
121
+ styles.attachmentIconButton,
122
+ {
123
+ backgroundColor: actionBg,
124
+ borderColor: actionBorder,
125
+ borderWidth: 1,
126
+ },
127
+ ]}>
128
+ <FeatherIcon
129
+ name="trash-2"
130
+ size={16}
131
+ color={iconDanger}
132
+ />
133
+ </TouchableOpacity>
134
+ </View>
135
+ ) : null}
136
+ </View>
137
+ );
138
+ })}
139
+ </View>
140
+ </ScrollView>
141
+ )}
142
+
143
+ {managerModal}
144
+ </View>
145
+ );
146
+ }