@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
@@ -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?.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 Icon from 'react-native-vector-icons/Feather';
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 = '#2563EB',
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 resolvedTheme = useMemo(
110
- () => buildTheme({ accentColor, themeColors }),
111
- [accentColor, themeColors],
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],