@controleonline/ui-default 1.0.269 → 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/DefaultGoogleMap.styles.js +9 -2
- package/src/react/components/map/DefaultGoogleMap.web.js +49 -28
- 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 +45 -38
- 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 +215 -109
- 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 +54 -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,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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|