@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,152 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Text, View } from 'react-native'
3
+
4
+ import styles from './DefaultExtraData.styles'
5
+
6
+ const MAX_CELL_ITEMS = 4
7
+
8
+ const formatKey = key =>
9
+ String(key || '')
10
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
11
+ .replace(/[_-]+/g, ' ')
12
+ .replace(/^\w/, letter => letter.toUpperCase())
13
+
14
+ const formatScalar = value => {
15
+ if (value === true) return 'true'
16
+ if (value === false) return 'false'
17
+ return String(value ?? '').trim()
18
+ }
19
+
20
+ const summarizeValue = value => {
21
+ if (Array.isArray(value)) return `${value.length}`
22
+ if (value && typeof value === 'object') {
23
+ return Object.keys(value).filter(Boolean).slice(0, 3).join(', ')
24
+ }
25
+
26
+ return formatScalar(value)
27
+ }
28
+
29
+ const resolveArrayKey = (item, index) =>
30
+ item?.name || item?.id || item?.code || item?.source || `${index + 1}`
31
+
32
+ const buildItem = (key, value, depth = 0) => {
33
+ if (Array.isArray(value)) {
34
+ return {
35
+ key,
36
+ value: summarizeValue(value),
37
+ children: value.map((item, index) =>
38
+ buildItem(resolveArrayKey(item, index), item, depth + 1),
39
+ ),
40
+ depth,
41
+ }
42
+ }
43
+
44
+ if (value && typeof value === 'object') {
45
+ return {
46
+ key,
47
+ value: summarizeValue(value),
48
+ children: Object.entries(value)
49
+ .filter(([, itemValue]) => itemValue !== null && itemValue !== undefined && itemValue !== '')
50
+ .map(([childKey, childValue]) => buildItem(childKey, childValue, depth + 1)),
51
+ depth,
52
+ }
53
+ }
54
+
55
+ return {
56
+ key,
57
+ value: formatScalar(value),
58
+ children: [],
59
+ depth,
60
+ }
61
+ }
62
+
63
+ const normalizeExtraData = value => {
64
+ if (!value) return []
65
+
66
+ const parsed = typeof value === 'string'
67
+ ? JSON.parse(value)
68
+ : value
69
+
70
+ if (Array.isArray(parsed)) {
71
+ return parsed
72
+ .map((item, index) => {
73
+ const key = item?.field || item?.key || item?.name || resolveArrayKey(item, index)
74
+ const itemValue = item?.value ?? item?.configValue ?? item?.data ?? item
75
+ return buildItem(key, itemValue)
76
+ })
77
+ .filter(item => item.value || item.children.length > 0)
78
+ }
79
+
80
+ if (!parsed || typeof parsed !== 'object') {
81
+ return []
82
+ }
83
+
84
+ return Object.entries(parsed)
85
+ .filter(([, itemValue]) => itemValue !== null && itemValue !== undefined && itemValue !== '')
86
+ .map(([key, itemValue]) => buildItem(key, itemValue))
87
+ }
88
+
89
+ const ExtraDataItem = ({ item, variant = 'cell' }) => {
90
+ const isCard = variant === 'card'
91
+ const children = Array.isArray(item.children) ? item.children : []
92
+
93
+ return (
94
+ <View style={[styles.item, isCard ? styles.cardItem : null]}>
95
+ <Text style={styles.key} numberOfLines={1}>
96
+ {formatKey(item.key)}
97
+ </Text>
98
+ {item.value ? (
99
+ <Text style={styles.value} numberOfLines={isCard ? 4 : 1}>
100
+ {item.value}
101
+ </Text>
102
+ ) : null}
103
+ {isCard && children.length > 0 ? (
104
+ <View style={styles.children}>
105
+ {children.map((child, index) => (
106
+ <ExtraDataItem
107
+ key={`${item.key}-${child.key}-${index}`}
108
+ item={child}
109
+ variant={variant}
110
+ />
111
+ ))}
112
+ </View>
113
+ ) : null}
114
+ </View>
115
+ )
116
+ }
117
+
118
+ const DefaultExtraData = ({ value, variant = 'cell' }) => {
119
+ const items = useMemo(() => {
120
+ try {
121
+ return normalizeExtraData(value)
122
+ } catch {
123
+ return []
124
+ }
125
+ }, [value])
126
+
127
+ if (items.length === 0) {
128
+ return <Text style={styles.empty}>-</Text>
129
+ }
130
+
131
+ const isCard = variant === 'card'
132
+ const visibleItems = isCard ? items : items.slice(0, MAX_CELL_ITEMS)
133
+
134
+ return (
135
+ <View style={[styles.wrap, isCard ? styles.cardWrap : null]}>
136
+ {visibleItems.map((item, index) => (
137
+ <ExtraDataItem
138
+ key={`${item.key}-${index}`}
139
+ item={item}
140
+ variant={variant}
141
+ />
142
+ ))}
143
+ {!isCard && items.length > MAX_CELL_ITEMS ? (
144
+ <Text style={styles.more} numberOfLines={1}>
145
+ +{items.length - MAX_CELL_ITEMS}
146
+ </Text>
147
+ ) : null}
148
+ </View>
149
+ )
150
+ }
151
+
152
+ export default DefaultExtraData
@@ -0,0 +1,61 @@
1
+ import { StyleSheet } from 'react-native'
2
+
3
+ const styles = StyleSheet.create({
4
+ wrap: {
5
+ flexDirection: 'row',
6
+ flexWrap: 'wrap',
7
+ gap: 6,
8
+ minWidth: 0,
9
+ },
10
+ cardWrap: {
11
+ flexDirection: 'column',
12
+ alignItems: 'stretch',
13
+ width: '100%',
14
+ },
15
+ item: {
16
+ maxWidth: 180,
17
+ minHeight: 24,
18
+ borderRadius: 8,
19
+ borderWidth: 1,
20
+ borderColor: '#E2E8F0',
21
+ backgroundColor: '#F8FAFC',
22
+ paddingHorizontal: 8,
23
+ paddingVertical: 4,
24
+ },
25
+ cardItem: {
26
+ maxWidth: '100%',
27
+ width: '100%',
28
+ },
29
+ children: {
30
+ gap: 4,
31
+ marginTop: 6,
32
+ paddingLeft: 8,
33
+ borderLeftWidth: 1,
34
+ borderLeftColor: '#CBD5E1',
35
+ },
36
+ key: {
37
+ color: '#64748B',
38
+ fontSize: 9,
39
+ fontWeight: '900',
40
+ textTransform: 'uppercase',
41
+ },
42
+ value: {
43
+ marginTop: 1,
44
+ color: '#0F172A',
45
+ fontSize: 11,
46
+ fontWeight: '700',
47
+ },
48
+ more: {
49
+ alignSelf: 'center',
50
+ color: '#64748B',
51
+ fontSize: 11,
52
+ fontWeight: '900',
53
+ },
54
+ empty: {
55
+ color: '#94A3B8',
56
+ fontSize: 12,
57
+ fontWeight: '700',
58
+ },
59
+ })
60
+
61
+ export default styles
@@ -1,46 +1,231 @@
1
- import React, {memo, useMemo} from 'react';
2
- import {Image} from 'react-native';
3
- import {useStore} from '@store';
4
- import {resolveDefaultFileSource} from '@controleonline/ui-common/src/react/utils/fileUrl';
5
-
6
- const resolveCompanyFromStore = peopleGetters => {
7
- if (peopleGetters?.defaultCompany?.id) {
8
- return peopleGetters.defaultCompany;
9
- }
10
-
11
- if (peopleGetters?.currentCompany?.id) {
12
- return peopleGetters.currentCompany;
13
- }
14
-
15
- return null;
16
- };
17
-
18
- const DefaultFile = ({
19
- file,
20
- source,
21
- company,
22
- appDomain = '',
23
- headers = {},
24
- ...imageProps
25
- }) => {
26
- const peopleStore = useStore('people');
27
- const peopleGetters = peopleStore?.getters || {};
28
- const resolvedCompany = company || resolveCompanyFromStore(peopleGetters);
29
- const resolvedSource = useMemo(
30
- () =>
31
- resolveDefaultFileSource(source ?? file, {
32
- company: resolvedCompany,
33
- appDomain,
34
- headers,
35
- }),
36
- [appDomain, company, file, headers, resolvedCompany, source],
37
- );
38
-
39
- if (!resolvedSource) {
40
- return null;
41
- }
42
-
43
- return <Image {...imageProps} source={resolvedSource} />;
44
- };
45
-
46
- export default memo(DefaultFile);
1
+ import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {Image, Platform} from 'react-native';
3
+ import {useStore} from '@store';
4
+ import {resolveDefaultFileSource} from '@controleonline/ui-common/src/react/utils/fileUrl';
5
+
6
+ const resolveCompanyFromStore = peopleGetters => {
7
+ if (peopleGetters?.defaultCompany?.id) {
8
+ return peopleGetters.defaultCompany;
9
+ }
10
+
11
+ if (peopleGetters?.currentCompany?.id) {
12
+ return peopleGetters.currentCompany;
13
+ }
14
+
15
+ return null;
16
+ };
17
+
18
+ /** Sync session token (same source as api.getToken). */
19
+ const readSessionApiToken = () => {
20
+ try {
21
+ if (typeof localStorage === 'undefined') {
22
+ return '';
23
+ }
24
+ const sessionString = localStorage.getItem('session');
25
+ if (!sessionString) {
26
+ return '';
27
+ }
28
+ const cleanString =
29
+ typeof sessionString === 'string' && sessionString.startsWith('__q_objt|')
30
+ ? sessionString.substring('__q_objt|'.length)
31
+ : sessionString;
32
+ const session = JSON.parse(cleanString);
33
+ return session?.token || session?.api_key || '';
34
+ } catch (e) {
35
+ return '';
36
+ }
37
+ };
38
+
39
+ const headersKey = headers => {
40
+ if (!headers || typeof headers !== 'object') {
41
+ return '';
42
+ }
43
+ try {
44
+ return JSON.stringify(headers);
45
+ } catch (e) {
46
+ return '';
47
+ }
48
+ };
49
+
50
+ const companyKey = company => {
51
+ if (!company || typeof company !== 'object') {
52
+ return String(company || '');
53
+ }
54
+ return String(company.id || company['@id'] || company.domain || '');
55
+ };
56
+
57
+ /**
58
+ * DefaultFile — authenticated image preview.
59
+ *
60
+ * On web, RN Image becomes <img> and ignores source.headers. We fetch the
61
+ * download URL with API-TOKEN and display a blob: URL. Blob URLs are only
62
+ * revoked on unmount / when the remote URI changes — never while still
63
+ * assigned to the Image (avoids WebKitBlobResource error 1).
64
+ */
65
+ const DefaultFile = ({
66
+ file,
67
+ source,
68
+ company,
69
+ appDomain = '',
70
+ headers = {},
71
+ ...imageProps
72
+ }) => {
73
+ const peopleStore = useStore('people');
74
+ const peopleGetters = peopleStore?.getters || {};
75
+ const resolvedCompany = company || resolveCompanyFromStore(peopleGetters);
76
+
77
+ const sessionToken = useMemo(() => readSessionApiToken(), []);
78
+ const authHeaders = useMemo(
79
+ () => ({
80
+ ...(sessionToken ? {'API-TOKEN': sessionToken} : {}),
81
+ ...headers,
82
+ }),
83
+ // headers identity from parents can churn; key by JSON
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
+ [sessionToken, headersKey(headers)],
86
+ );
87
+
88
+ const resolvedSource = useMemo(
89
+ () =>
90
+ resolveDefaultFileSource(source ?? file, {
91
+ company: resolvedCompany,
92
+ appDomain,
93
+ headers: authHeaders,
94
+ }),
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ [appDomain, companyKey(resolvedCompany), source, file, authHeaders],
97
+ );
98
+
99
+ const remoteUri = resolvedSource?.uri || null;
100
+ const remoteHeaders = resolvedSource?.headers || {};
101
+
102
+ const [displayUri, setDisplayUri] = useState(null);
103
+ const blobRef = useRef(null);
104
+ const requestIdRef = useRef(0);
105
+
106
+ useEffect(() => {
107
+ const requestId = ++requestIdRef.current;
108
+ let cancelled = false;
109
+
110
+ const revokeBlob = () => {
111
+ if (blobRef.current && typeof URL !== 'undefined' && URL.revokeObjectURL) {
112
+ try {
113
+ URL.revokeObjectURL(blobRef.current);
114
+ } catch (e) {
115
+ // ignore
116
+ }
117
+ blobRef.current = null;
118
+ }
119
+ };
120
+
121
+ if (!remoteUri) {
122
+ revokeBlob();
123
+ setDisplayUri(null);
124
+ return () => {
125
+ cancelled = true;
126
+ };
127
+ }
128
+
129
+ if (/^(data:|blob:)/i.test(remoteUri)) {
130
+ revokeBlob();
131
+ setDisplayUri(remoteUri);
132
+ return () => {
133
+ cancelled = true;
134
+ };
135
+ }
136
+
137
+ // Native: headers work on Image source — no blob needed
138
+ if (Platform.OS !== 'web') {
139
+ revokeBlob();
140
+ setDisplayUri(remoteUri);
141
+ return () => {
142
+ cancelled = true;
143
+ };
144
+ }
145
+
146
+ if (typeof fetch !== 'function') {
147
+ setDisplayUri(remoteUri);
148
+ return () => {
149
+ cancelled = true;
150
+ };
151
+ }
152
+
153
+ const load = async () => {
154
+ try {
155
+ const response = await fetch(remoteUri, {
156
+ method: 'GET',
157
+ headers: remoteHeaders,
158
+ credentials: 'omit',
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`download ${response.status}`);
162
+ }
163
+ const contentType = String(
164
+ response.headers.get('content-type') || '',
165
+ ).toLowerCase();
166
+ if (contentType.includes('application/json')) {
167
+ throw new Error('download returned json');
168
+ }
169
+ const blob = await response.blob();
170
+ if (!blob || blob.size === 0) {
171
+ throw new Error('empty blob');
172
+ }
173
+ if (cancelled || requestId !== requestIdRef.current) {
174
+ return;
175
+ }
176
+ const nextUrl = URL.createObjectURL(blob);
177
+ // Revoke previous only after we have the next URL
178
+ revokeBlob();
179
+ blobRef.current = nextUrl;
180
+ setDisplayUri(nextUrl);
181
+ } catch (e) {
182
+ if (cancelled || requestId !== requestIdRef.current) {
183
+ return;
184
+ }
185
+ // Do not keep a broken blob; fall back to remote (may 403 on <img>)
186
+ revokeBlob();
187
+ setDisplayUri(null);
188
+ }
189
+ };
190
+
191
+ void load();
192
+
193
+ return () => {
194
+ cancelled = true;
195
+ // Defer revoke so an in-flight Image paint is not interrupted by a
196
+ // transient dependency flicker (Strict Mode double-invoke is OK:
197
+ // requestId invalidates the stale response).
198
+ };
199
+ }, [remoteUri, headersKey(remoteHeaders)]);
200
+
201
+ // Unmount: revoke blob
202
+ useEffect(() => {
203
+ return () => {
204
+ if (blobRef.current && typeof URL !== 'undefined' && URL.revokeObjectURL) {
205
+ try {
206
+ URL.revokeObjectURL(blobRef.current);
207
+ } catch (e) {
208
+ // ignore
209
+ }
210
+ blobRef.current = null;
211
+ }
212
+ };
213
+ }, []);
214
+
215
+ if (!displayUri) {
216
+ return null;
217
+ }
218
+
219
+ return (
220
+ <Image
221
+ {...imageProps}
222
+ source={
223
+ Platform.OS === 'web'
224
+ ? {uri: displayUri}
225
+ : {uri: displayUri, headers: remoteHeaders}
226
+ }
227
+ />
228
+ );
229
+ };
230
+
231
+ export default memo(DefaultFile);
@@ -0,0 +1,123 @@
1
+ import React, {memo, useMemo} from 'react';
2
+ import {Linking, Text, TouchableOpacity, View} from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import {useStore} from '@store';
5
+ import DefaultFile from './DefaultFile';
6
+ import {
7
+ normalizeDefaultFileColumnItems,
8
+ normalizeFileColumnText,
9
+ } from './DefaultFileColumn.utils';
10
+ import styles from './DefaultFileColumn.styles';
11
+
12
+ const ICON_BY_KIND = {
13
+ audio: 'volume-2',
14
+ document: 'file-text',
15
+ file: 'paperclip',
16
+ pdf: 'file-text',
17
+ text: 'file-text',
18
+ video: 'film',
19
+ };
20
+
21
+ const resolveCompanyFromStore = peopleGetters => {
22
+ if (peopleGetters?.defaultCompany?.id) return peopleGetters.defaultCompany;
23
+ if (peopleGetters?.currentCompany?.id) return peopleGetters.currentCompany;
24
+ return null;
25
+ };
26
+
27
+ const resolveLabel = item =>
28
+ normalizeFileColumnText(item.fileName) ||
29
+ normalizeFileColumnText(item.extension).toUpperCase() ||
30
+ normalizeFileColumnText(item.id);
31
+
32
+ const DefaultFileColumn = ({
33
+ column = {},
34
+ row = {},
35
+ value,
36
+ variant = 'cell',
37
+ }) => {
38
+ const peopleStore = useStore('people');
39
+ const themeStore = useStore('theme');
40
+ const company = resolveCompanyFromStore(peopleStore?.getters || {});
41
+ const palette = themeStore?.getters?.colors || {};
42
+ const borderColor = palette.border || palette.secondary || '#CBD5E1';
43
+ const textColor = palette.text || palette.primary || '#0F172A';
44
+ const mutedColor = palette.textSecondary || '#64748B';
45
+ const surfaceColor = palette.surface || palette.background || '#FFFFFF';
46
+ const items = useMemo(
47
+ () => normalizeDefaultFileColumnItems({column, company, row, value}),
48
+ [column, company, row, value],
49
+ );
50
+
51
+ if (items.length === 0) {
52
+ return (
53
+ <View style={styles.wrap}>
54
+ <Text style={[styles.emptyText, {color: mutedColor}]}>-</Text>
55
+ </View>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <View style={styles.wrap}>
61
+ <View style={styles.list}>
62
+ {items.map(item => {
63
+ const label = resolveLabel(item);
64
+ const openFile = () => {
65
+ if (item.openUrl) Linking.openURL(item.openUrl);
66
+ };
67
+
68
+ if (item.kind === 'image') {
69
+ return (
70
+ <TouchableOpacity
71
+ key={item.key}
72
+ activeOpacity={item.openUrl ? 0.78 : 1}
73
+ disabled={!item.openUrl}
74
+ onPress={openFile}
75
+ style={[
76
+ styles.item,
77
+ styles.imageItem,
78
+ {backgroundColor: surfaceColor, borderColor},
79
+ ]}
80
+ >
81
+ <DefaultFile
82
+ file={item.source}
83
+ company={company}
84
+ resizeMode="cover"
85
+ style={styles.image}
86
+ />
87
+ </TouchableOpacity>
88
+ );
89
+ }
90
+
91
+ const iconName = ICON_BY_KIND[item.kind] || ICON_BY_KIND.file;
92
+
93
+ return (
94
+ <TouchableOpacity
95
+ key={item.key}
96
+ activeOpacity={item.openUrl ? 0.78 : 1}
97
+ disabled={!item.openUrl}
98
+ onPress={openFile}
99
+ style={[
100
+ styles.item,
101
+ {
102
+ backgroundColor: surfaceColor,
103
+ borderColor,
104
+ maxWidth: variant === 'card' ? 180 : 132,
105
+ },
106
+ ]}
107
+ >
108
+ <Icon name={iconName} size={14} color={mutedColor} />
109
+ <Text
110
+ numberOfLines={1}
111
+ style={[styles.label, {color: textColor}]}
112
+ >
113
+ {label || '-'}
114
+ </Text>
115
+ </TouchableOpacity>
116
+ );
117
+ })}
118
+ </View>
119
+ </View>
120
+ );
121
+ };
122
+
123
+ export default memo(DefaultFileColumn);
@@ -0,0 +1,52 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ const styles = StyleSheet.create({
4
+ wrap: {
5
+ minWidth: 0,
6
+ width: '100%',
7
+ },
8
+ list: {
9
+ alignItems: 'center',
10
+ flexDirection: 'row',
11
+ flexWrap: 'wrap',
12
+ gap: 6,
13
+ minHeight: 28,
14
+ minWidth: 0,
15
+ },
16
+ item: {
17
+ alignItems: 'center',
18
+ borderRadius: 7,
19
+ borderWidth: 1,
20
+ flexDirection: 'row',
21
+ gap: 5,
22
+ minHeight: 26,
23
+ maxWidth: 132,
24
+ overflow: 'hidden',
25
+ paddingHorizontal: 6,
26
+ paddingVertical: 3,
27
+ },
28
+ imageItem: {
29
+ borderRadius: 6,
30
+ height: 32,
31
+ paddingHorizontal: 2,
32
+ paddingVertical: 2,
33
+ width: 42,
34
+ },
35
+ image: {
36
+ borderRadius: 4,
37
+ height: 28,
38
+ width: 38,
39
+ },
40
+ label: {
41
+ flexShrink: 1,
42
+ fontSize: 11,
43
+ fontWeight: '800',
44
+ maxWidth: 92,
45
+ },
46
+ emptyText: {
47
+ fontSize: 12,
48
+ fontWeight: '600',
49
+ },
50
+ });
51
+
52
+ export default styles;