@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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -0
- package/.github/workflows/publish-npm.yml +91 -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 +272 -0
- package/docs/wiki.md +1 -0
- package/package.json +4 -3
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +446 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -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 +148 -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 +60 -48
- package/src/react/components/filters/DefaultExternalFilters.js +324 -265
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +48 -12
- package/src/react/components/help/DefaultTooltip.styles.js +20 -0
- 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 +296 -141
- 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 +262 -213
- 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 +22 -3
- package/src/react/components/table/DefaultDebug.js +44 -37
- package/src/react/components/table/DefaultSearchModal.js +9 -1
- package/src/react/components/table/DefaultTable.grouping.js +98 -0
- package/src/react/components/table/DefaultTable.js +217 -111
- package/src/react/components/table/DefaultTable.styles.js +369 -310
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- 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 +357 -325
- 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 +406 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -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 +159 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +262 -0
- package/src/react/services/addressGeo.js +230 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +22 -6
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
- package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
- package/src/tests/react/components/table/DefaultTable.test.js +53 -2
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -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 +104 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +179 -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 +96 -49
|
@@ -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?.
|
|
8
|
-
return peopleGetters.
|
|
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?.mainCompany?.id) {
|
|
8
|
+
return peopleGetters.mainCompany;
|
|
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?.mainCompany?.id) return peopleGetters.mainCompany;
|
|
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;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {extractFileId, resolveDefaultFileUrl} from '@controleonline/ui-common/src/react/utils/fileUrl';
|
|
2
|
+
|
|
3
|
+
const FILE_INPUT_TYPES = new Set([
|
|
4
|
+
'document',
|
|
5
|
+
'documents',
|
|
6
|
+
'file',
|
|
7
|
+
'files',
|
|
8
|
+
'image',
|
|
9
|
+
'images',
|
|
10
|
+
'pdf',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const IMAGE_EXTENSIONS = new Set(['avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp']);
|
|
14
|
+
const PDF_EXTENSIONS = new Set(['pdf']);
|
|
15
|
+
const DOCUMENT_EXTENSIONS = new Set(['doc', 'docx', 'odt', 'rtf', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp']);
|
|
16
|
+
|
|
17
|
+
export const normalizeFileColumnText = value => String(value ?? '').trim();
|
|
18
|
+
|
|
19
|
+
export const normalizeFileColumnType = value =>
|
|
20
|
+
normalizeFileColumnText(value).replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
21
|
+
|
|
22
|
+
export const isFileInputColumn = column =>
|
|
23
|
+
FILE_INPUT_TYPES.has(normalizeFileColumnType(column?.inputType));
|
|
24
|
+
|
|
25
|
+
const asArray = value => {
|
|
26
|
+
if (Array.isArray(value)) return value;
|
|
27
|
+
if (value === undefined || value === null || value === '') return [];
|
|
28
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
29
|
+
return value
|
|
30
|
+
.split(',')
|
|
31
|
+
.map(item => item.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [value];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const pickFilePayload = item =>
|
|
39
|
+
item && typeof item === 'object' && !Array.isArray(item) && item.file
|
|
40
|
+
? item.file
|
|
41
|
+
: item;
|
|
42
|
+
|
|
43
|
+
const resolveForcedFileType = column => {
|
|
44
|
+
const inputType = normalizeFileColumnType(column?.inputType);
|
|
45
|
+
if (inputType === 'image' || inputType === 'images') return 'image';
|
|
46
|
+
if (inputType === 'pdf') return 'application';
|
|
47
|
+
if (inputType === 'document' || inputType === 'documents') return 'document';
|
|
48
|
+
return normalizeFileColumnText(column?.fileType);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const resolveForcedExtension = column => {
|
|
52
|
+
const inputType = normalizeFileColumnType(column?.inputType);
|
|
53
|
+
if (inputType === 'pdf') return 'pdf';
|
|
54
|
+
return normalizeFileColumnText(column?.extension).toLowerCase();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const resolveKind = ({fileType, extension}) => {
|
|
58
|
+
const normalizedType = normalizeFileColumnType(fileType);
|
|
59
|
+
const normalizedExtension = normalizeFileColumnText(extension).replace(/^\./, '').toLowerCase();
|
|
60
|
+
|
|
61
|
+
if (normalizedType === 'image' || IMAGE_EXTENSIONS.has(normalizedExtension)) return 'image';
|
|
62
|
+
if (PDF_EXTENSIONS.has(normalizedExtension)) return 'pdf';
|
|
63
|
+
if (DOCUMENT_EXTENSIONS.has(normalizedExtension) || normalizedType === 'document') return 'document';
|
|
64
|
+
if (normalizedType === 'text') return 'text';
|
|
65
|
+
if (normalizedType === 'audio') return 'audio';
|
|
66
|
+
if (normalizedType === 'video') return 'video';
|
|
67
|
+
return 'file';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const normalizeDefaultFileColumnItems = ({
|
|
71
|
+
column = {},
|
|
72
|
+
company = null,
|
|
73
|
+
row = {},
|
|
74
|
+
value,
|
|
75
|
+
} = {}) =>
|
|
76
|
+
asArray(value ?? row?.[column?.key || column?.name]).map((item, index) => {
|
|
77
|
+
const payload = pickFilePayload(item);
|
|
78
|
+
const isObject = payload && typeof payload === 'object' && !Array.isArray(payload);
|
|
79
|
+
const id = extractFileId(payload);
|
|
80
|
+
const fileType = isObject
|
|
81
|
+
? normalizeFileColumnText(payload.fileType) || resolveForcedFileType(column)
|
|
82
|
+
: resolveForcedFileType(column);
|
|
83
|
+
const extension = isObject
|
|
84
|
+
? normalizeFileColumnText(payload.extension).toLowerCase() || resolveForcedExtension(column)
|
|
85
|
+
: resolveForcedExtension(column);
|
|
86
|
+
const url = isObject && typeof payload.url === 'string'
|
|
87
|
+
? payload.url
|
|
88
|
+
: resolveDefaultFileUrl(payload, {company});
|
|
89
|
+
const driveUrl = isObject && typeof payload.driveUrl === 'string'
|
|
90
|
+
? payload.driveUrl
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
extension,
|
|
95
|
+
fileName: isObject ? normalizeFileColumnText(payload.fileName) : '',
|
|
96
|
+
fileType,
|
|
97
|
+
id,
|
|
98
|
+
key: `${id || normalizeFileColumnText(payload) || index}`,
|
|
99
|
+
kind: resolveKind({fileType, extension}),
|
|
100
|
+
openUrl: driveUrl || url,
|
|
101
|
+
relationId: item && typeof item === 'object' && !Array.isArray(item) ? extractFileId(item) : null,
|
|
102
|
+
source: payload,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Image,
|
|
4
4
|
Modal,
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
TextInput,
|
|
8
8
|
TouchableOpacity,
|
|
9
9
|
TouchableWithoutFeedback,
|
|
10
|
-
useWindowDimensions,
|
|
11
|
-
View,
|
|
12
|
-
} from 'react-native';
|
|
13
|
-
import
|
|
10
|
+
useWindowDimensions,
|
|
11
|
+
View,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { useStore } from '@store';
|
|
14
|
+
import Icon from 'react-native-vector-icons/Feather';
|
|
14
15
|
import {
|
|
15
16
|
formatStoreColumnLabel,
|
|
16
17
|
resolveStoreColumn,
|
|
@@ -19,16 +20,16 @@ import {
|
|
|
19
20
|
import { isValidFeatherIcon } from '../inputs/defaultInputUtils';
|
|
20
21
|
import createStyles from './CompactFilterSelector.styles';
|
|
21
22
|
|
|
22
|
-
const buildTheme = ({ accentColor, themeColors = {} }) => ({
|
|
23
|
-
accentColor: accentColor,
|
|
24
|
-
activeBackgroundColor:
|
|
25
|
-
themeColors.activeBackgroundColor || `${accentColor}14
|
|
26
|
-
activeIconBackgroundColor:
|
|
27
|
-
themeColors.activeIconBackgroundColor || `${accentColor}24
|
|
28
|
-
activeChevronColor:
|
|
29
|
-
themeColors.activeChevronColor ||
|
|
30
|
-
themeColors.activeIconColor ||
|
|
31
|
-
accentColor,
|
|
23
|
+
const buildTheme = ({ accentColor, themeColors = {} }) => ({
|
|
24
|
+
accentColor: accentColor,
|
|
25
|
+
activeBackgroundColor:
|
|
26
|
+
themeColors.activeBackgroundColor || (accentColor ? `${accentColor}14` : themeColors.backgroundColor),
|
|
27
|
+
activeIconBackgroundColor:
|
|
28
|
+
themeColors.activeIconBackgroundColor || (accentColor ? `${accentColor}24` : themeColors.iconBackgroundColor),
|
|
29
|
+
activeChevronColor:
|
|
30
|
+
themeColors.activeChevronColor ||
|
|
31
|
+
themeColors.activeIconColor ||
|
|
32
|
+
accentColor,
|
|
32
33
|
activeIconColor: themeColors.activeIconColor || accentColor,
|
|
33
34
|
activeTextColor: themeColors.activeTextColor || accentColor,
|
|
34
35
|
backgroundColor: themeColors.backgroundColor || '#F8FAFC',
|
|
@@ -44,10 +45,10 @@ const buildTheme = ({ accentColor, themeColors = {} }) => ({
|
|
|
44
45
|
modalTitleColor: themeColors.modalTitleColor || '#0F172A',
|
|
45
46
|
optionBackgroundColor: themeColors.optionBackgroundColor || '#F8FAFC',
|
|
46
47
|
optionBorderColor: themeColors.optionBorderColor || '#E2E8F0',
|
|
47
|
-
optionSelectedTextColor:
|
|
48
|
-
themeColors.optionSelectedTextColor || accentColor,
|
|
49
|
-
textColor: themeColors.textColor || '#0F172A',
|
|
50
|
-
});
|
|
48
|
+
optionSelectedTextColor:
|
|
49
|
+
themeColors.optionSelectedTextColor || accentColor,
|
|
50
|
+
textColor: themeColors.textColor || '#0F172A',
|
|
51
|
+
});
|
|
51
52
|
|
|
52
53
|
const noop = () => {};
|
|
53
54
|
|
|
@@ -82,12 +83,12 @@ const resolveStoreFieldLabel = ({ fallbackLabel = '', field = '', store = '' })
|
|
|
82
83
|
);
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
const CompactFilterSelector = ({
|
|
86
|
-
accentColor =
|
|
87
|
-
active = false,
|
|
88
|
-
children = null,
|
|
89
|
-
dense = false,
|
|
90
|
-
disabled = false,
|
|
86
|
+
const CompactFilterSelector = ({
|
|
87
|
+
accentColor = null,
|
|
88
|
+
active = false,
|
|
89
|
+
children = null,
|
|
90
|
+
dense = false,
|
|
91
|
+
disabled = false,
|
|
91
92
|
icon = 'sliders',
|
|
92
93
|
label = '',
|
|
93
94
|
labelCaption = '',
|
|
@@ -98,18 +99,33 @@ const CompactFilterSelector = ({
|
|
|
98
99
|
options = [],
|
|
99
100
|
searchable = false,
|
|
100
101
|
selectedKey = '',
|
|
101
|
-
field = '',
|
|
102
|
-
store = '',
|
|
103
|
-
themeColors = {},
|
|
104
|
-
title = '',
|
|
105
|
-
}) => {
|
|
102
|
+
field = '',
|
|
103
|
+
store = '',
|
|
104
|
+
themeColors = {},
|
|
105
|
+
title = '',
|
|
106
|
+
}) => {
|
|
107
|
+
const themeStore = useStore('theme');
|
|
106
108
|
const [visible, setVisible] = useState(false);
|
|
107
109
|
const [searchText, setSearchText] = useState('');
|
|
108
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
111
|
+
const themeStoreColors = themeStore?.getters?.colors || {};
|
|
112
|
+
const resolvedAccentColor =
|
|
113
|
+
accentColor ||
|
|
114
|
+
themeColors.primary ||
|
|
115
|
+
themeColors.accent ||
|
|
116
|
+
themeStoreColors.primary ||
|
|
117
|
+
themeStoreColors.accent ||
|
|
118
|
+
'';
|
|
119
|
+
const resolvedTheme = useMemo(
|
|
120
|
+
() => buildTheme({
|
|
121
|
+
accentColor: resolvedAccentColor,
|
|
122
|
+
themeColors: {
|
|
123
|
+
...themeStoreColors,
|
|
124
|
+
...themeColors,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
[resolvedAccentColor, themeColors, themeStoreColors],
|
|
128
|
+
);
|
|
113
129
|
const styles = useMemo(
|
|
114
130
|
() => createStyles(resolvedTheme, dense, windowWidth),
|
|
115
131
|
[dense, resolvedTheme, windowWidth],
|