@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.
Files changed (91) 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/DefaultGoogleMap.styles.js +9 -2
  39. package/src/react/components/map/DefaultGoogleMap.web.js +49 -28
  40. package/src/react/components/map/DefaultMap.native.js +73 -73
  41. package/src/react/components/map/DefaultMap.shared.js +911 -762
  42. package/src/react/components/map/DefaultMap.web.js +124 -68
  43. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  44. package/src/react/components/table/DefaultDebug.js +45 -38
  45. package/src/react/components/table/DefaultSearchModal.js +9 -1
  46. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  47. package/src/react/components/table/DefaultTable.js +215 -109
  48. package/src/react/components/table/DefaultTable.styles.js +367 -314
  49. package/src/react/components/table/DefaultTableBody.js +13 -8
  50. package/src/react/components/table/DefaultTableCards.js +20 -1
  51. package/src/react/components/table/DefaultTableControls.js +180 -205
  52. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  53. package/src/react/components/table/DefaultTableInput.js +1 -0
  54. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  55. package/src/react/components/table/DefaultTableRows.js +224 -363
  56. package/src/react/components/table/DefaultTableSearch.js +28 -5
  57. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  58. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  59. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  60. package/src/react/components/upload/DefaultUpload.js +436 -0
  61. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  62. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  63. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  64. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  65. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  66. package/src/react/components/upload/fileUpload.js +93 -0
  67. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  68. package/src/react/index.js +6 -0
  69. package/src/react/services/addressFormUtils.js +196 -0
  70. package/src/react/services/addressGeo.js +227 -0
  71. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  72. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  73. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  74. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  75. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  76. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  77. package/src/tests/react/components/table/DefaultTable.test.js +54 -2
  78. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  79. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  80. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  81. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  82. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  83. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  84. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  85. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  86. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  87. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  88. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  89. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  90. package/src/tests/react/services/smoke-637/index.html +132 -0
  91. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -1,13 +1,13 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  Modal,
4
- ScrollView,
5
- Text,
6
- TextInput,
7
- TouchableOpacity,
8
- TouchableWithoutFeedback,
9
- View,
10
- useWindowDimensions,
4
+ ScrollView,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ TouchableWithoutFeedback,
9
+ View,
10
+ useWindowDimensions,
11
11
  } from 'react-native';
12
12
  import Icon from 'react-native-vector-icons/Feather';
13
13
  import { getAllStores, useStore } from '@store';
@@ -19,11 +19,16 @@ import {
19
19
  isDateLikeColumn,
20
20
  resolveStoreNameFromList,
21
21
  } from '../inputs/defaultInputUtils';
22
- import CompactFilterSelector from './CompactFilterSelector';
23
- import DateShortcutFilter from './DateShortcutFilter';
24
- import { resolveNextDateFilterValue } from './dateFilterSelection';
25
- import styles from './DefaultExternalFilters.styles';
26
-
22
+ import CompactFilterSelector from './CompactFilterSelector';
23
+ import DateShortcutFilter from './DateShortcutFilter';
24
+ import { resolveNextDateFilterValue } from './dateFilterSelection';
25
+ import {
26
+ persistTableFiltersPreference,
27
+ resolveDefaultTablePreferenceScope,
28
+ sanitizeTableFiltersPreference,
29
+ } from '../../utils/tableVisibleColumnsPreferences';
30
+ import styles from './DefaultExternalFilters.styles';
31
+
27
32
  const DEFAULT_COMPACT_BREAKPOINT = 768;
28
33
  const noop = () => {};
29
34
 
@@ -94,7 +99,7 @@ const shouldIncludeColumn = column =>
94
99
  column?.externalFilter === true &&
95
100
  column?.filter !== false &&
96
101
  column?.filters !== false;
97
-
102
+
98
103
  const normalizeFilterValue = value => {
99
104
  if (value && typeof value === 'object') {
100
105
  return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? value.key ?? '');
@@ -102,56 +107,56 @@ const normalizeFilterValue = value => {
102
107
 
103
108
  return normalizeText(value);
104
109
  };
105
-
106
- const isFilledFilterValue = value => {
107
- if (Array.isArray(value)) return value.length > 0;
108
- if (value && typeof value === 'object') {
109
- return Object.values(value).some(isFilledFilterValue);
110
- }
111
-
112
- return normalizeText(value) !== '';
113
- };
114
-
115
- const resolveDateState = filterValue => {
116
- if (!filterValue || typeof filterValue !== 'object') {
117
- return {
118
- customRange: { from: '', to: '' },
119
- value: 'all',
120
- };
121
- }
122
-
123
- if (filterValue.shortcut) {
124
- return {
125
- customRange: filterValue.customRange || { from: '', to: '' },
126
- value: filterValue.shortcut,
127
- };
128
- }
129
-
130
- if (filterValue.start || filterValue.end || filterValue.after || filterValue.before) {
131
- return {
132
- customRange: {
133
- from: filterValue.start || filterValue.after || '',
134
- to: filterValue.end || filterValue.before || '',
135
- },
136
- value: 'custom',
137
- };
138
- }
139
-
140
- return {
141
- customRange: { from: '', to: '' },
142
- value: 'all',
143
- };
144
- };
145
-
146
- const resolveIcon = column => {
147
- const key = getColumnKey(column);
148
- if (key === 'status') return 'check-circle';
149
- if (key === 'category') return 'tag';
110
+
111
+ const isFilledFilterValue = value => {
112
+ if (Array.isArray(value)) return value.length > 0;
113
+ if (value && typeof value === 'object') {
114
+ return Object.values(value).some(isFilledFilterValue);
115
+ }
116
+
117
+ return normalizeText(value) !== '';
118
+ };
119
+
120
+ const resolveDateState = filterValue => {
121
+ if (!filterValue || typeof filterValue !== 'object') {
122
+ return {
123
+ customRange: { from: '', to: '' },
124
+ value: 'all',
125
+ };
126
+ }
127
+
128
+ if (filterValue.shortcut) {
129
+ return {
130
+ customRange: filterValue.customRange || { from: '', to: '' },
131
+ value: filterValue.shortcut,
132
+ };
133
+ }
134
+
135
+ if (filterValue.start || filterValue.end || filterValue.after || filterValue.before) {
136
+ return {
137
+ customRange: {
138
+ from: filterValue.start || filterValue.after || '',
139
+ to: filterValue.end || filterValue.before || '',
140
+ },
141
+ value: 'custom',
142
+ };
143
+ }
144
+
145
+ return {
146
+ customRange: { from: '', to: '' },
147
+ value: 'all',
148
+ };
149
+ };
150
+
151
+ const resolveIcon = column => {
152
+ const key = getColumnKey(column);
153
+ if (key === 'status') return 'check-circle';
154
+ if (key === 'category') return 'tag';
150
155
  if (isDateLikeColumn(column)) return 'calendar';
151
156
  return 'sliders';
152
157
  };
153
-
154
- const DefaultExternalFilters = ({
158
+
159
+ const DefaultExternalFilters = ({
155
160
  accentColor = null,
156
161
  compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
157
162
  dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
@@ -165,22 +170,30 @@ const DefaultExternalFilters = ({
165
170
  const { width } = useWindowDimensions();
166
171
  const externalFilterStore = useStore(storeName);
167
172
  const peopleStore = useStore('people');
173
+ const storeConfigs = externalFilterStore?.getters?.configs || {};
174
+ const tablePreferenceScope =
175
+ storeConfigs.tablePreferenceScope ||
176
+ resolveDefaultTablePreferenceScope({
177
+ companyId: peopleStore?.getters?.currentCompany?.id,
178
+ storeName,
179
+ });
168
180
  const themeStore = useStore('theme');
169
181
  const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
182
+ const [loadedListItemsByColumn, setLoadedListItemsByColumn] = useState({});
170
183
  const loadedListStoresRef = useRef(new Set());
171
184
  const { currentCompany } = peopleStore.getters || {};
172
185
  const { colors: themeColors } = themeStore.getters;
173
- const themeTokens = useMemo(
174
- () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
175
- [currentCompany?.theme?.colors, themeColors],
176
- );
177
- const palette = useMemo(
178
- () => resolveThemePalette(themeTokens, colors),
179
- [themeTokens],
180
- );
181
- const resolvedAccentColor = accentColor || palette.primary;
182
- const surfaceColor = themeTokens['bg-odd-light'] || palette.background;
183
- const panelColor = themeTokens['bg-headers-light'] || palette.background;
186
+ const themeTokens = useMemo(
187
+ () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
188
+ [currentCompany?.theme?.colors, themeColors],
189
+ );
190
+ const palette = useMemo(
191
+ () => resolveThemePalette(themeTokens, colors),
192
+ [themeTokens],
193
+ );
194
+ const resolvedAccentColor = accentColor || palette.primary;
195
+ const surfaceColor = themeTokens['bg-odd-light'] || palette.background;
196
+ const panelColor = themeTokens['bg-headers-light'] || palette.background;
184
197
  const borderColor = palette.border;
185
198
  const textColor = palette.text;
186
199
  const textSecondaryColor = palette.textSecondary;
@@ -199,35 +212,60 @@ const DefaultExternalFilters = ({
199
212
  () => resolvedColumns.filter(shouldIncludeColumn),
200
213
  [resolvedColumns],
201
214
  );
202
- const activeCount = useMemo(
203
- () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
204
- [filterColumns, filters],
205
- );
206
-
207
- useEffect(() => {
208
- onActiveCountChange?.(activeCount);
209
- }, [activeCount, onActiveCountChange]);
210
-
211
- const updateFilter = useCallback((key, value) => {
212
- const nextFilters = { ...(filters || {}) };
213
- if (!isFilledFilterValue(value)) {
214
- delete nextFilters[key];
215
- } else {
216
- nextFilters[key] = value;
217
- }
218
- onChangeFilters?.(nextFilters);
219
- }, [filters, onChangeFilters]);
220
-
221
- const clearFilters = useCallback(() => {
222
- onChangeFilters?.({});
223
- }, [onChangeFilters]);
224
-
215
+ const activeCount = useMemo(
216
+ () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
217
+ [filterColumns, filters],
218
+ );
219
+
220
+ useEffect(() => {
221
+ onActiveCountChange?.(activeCount);
222
+ }, [activeCount, onActiveCountChange]);
223
+
224
+ const applyFilters = useCallback(nextFilters => {
225
+ const resolvedFilters =
226
+ nextFilters && typeof nextFilters === 'object' && !Array.isArray(nextFilters)
227
+ ? nextFilters
228
+ : {};
229
+ const columns = Array.isArray(externalFilterStore?.getters?.columns)
230
+ ? externalFilterStore.getters.columns
231
+ : [];
232
+ const persistedFilters = sanitizeTableFiltersPreference({
233
+ columns,
234
+ filters: resolvedFilters,
235
+ });
236
+
237
+ persistTableFiltersPreference(tablePreferenceScope, persistedFilters);
238
+
239
+ if (typeof externalFilterStore?.actions?.setFilters === 'function') {
240
+ externalFilterStore.actions.setFilters(resolvedFilters);
241
+ } else if (externalFilterStore?.getters) {
242
+ externalFilterStore.getters.filters = resolvedFilters;
243
+ }
244
+
245
+ onChangeFilters?.(resolvedFilters);
246
+ }, [externalFilterStore, onChangeFilters, tablePreferenceScope]);
247
+
248
+ const updateFilter = useCallback((key, value) => {
249
+ const nextFilters = { ...(filters || {}) };
250
+ if (!isFilledFilterValue(value)) {
251
+ delete nextFilters[key];
252
+ } else {
253
+ nextFilters[key] = value;
254
+ }
255
+ applyFilters(nextFilters);
256
+ }, [applyFilters, filters]);
257
+
258
+ const clearFilters = useCallback(() => {
259
+ applyFilters({});
260
+ }, [applyFilters]);
261
+
225
262
  useEffect(() => {
226
263
  if (!isCompactView) setIsFiltersModalOpen(false);
227
264
  }, [isCompactView]);
228
265
 
229
266
  useEffect(() => {
230
267
  loadedListStoresRef.current.clear();
268
+ setLoadedListItemsByColumn({});
231
269
  }, [currentCompany?.id]);
232
270
 
233
271
  const loadListOptionsForColumn = useCallback((column, searchValue = '') => {
@@ -254,18 +292,14 @@ const DefaultExternalFilters = ({
254
292
  });
255
293
  const hasScopedParams = Object.keys(listLoadParams).length > 0;
256
294
 
295
+ const loadKey = `${getColumnKey(column)}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
296
+ const columnKey = getColumnKey(column);
257
297
  if (
258
- !normalizeText(searchValue) &&
259
- !hasScopedParams &&
260
- Array.isArray(listStore?.getters?.items) &&
261
- listStore.getters.items.length > 0
298
+ loadedListStoresRef.current.has(loadKey) &&
299
+ columnKey &&
300
+ Array.isArray(loadedListItemsByColumn[columnKey])
262
301
  ) {
263
- return Promise.resolve(listStore.getters.items);
264
- }
265
-
266
- const loadKey = `${getColumnKey(column)}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
267
- if (loadedListStoresRef.current.has(loadKey)) {
268
- return Promise.resolve(listStore?.getters?.items || []);
302
+ return Promise.resolve(loadedListItemsByColumn[columnKey]);
269
303
  }
270
304
 
271
305
  loadedListStoresRef.current.add(loadKey);
@@ -277,65 +311,93 @@ const DefaultExternalFilters = ({
277
311
  skipSystemError: true,
278
312
  },
279
313
  }),
280
- ).catch(error => {
281
- loadedListStoresRef.current.delete(loadKey);
282
- throw error;
283
- });
284
- }, [currentCompany?.id, getOptionsForColumn, requestParams]);
314
+ )
315
+ .then(response => {
316
+ const items = Array.isArray(response)
317
+ ? response
318
+ : Array.isArray(response?.member)
319
+ ? response.member
320
+ : Array.isArray(response?.['hydra:member'])
321
+ ? response['hydra:member']
322
+ : listStore?.getters?.items || [];
323
+ const resolvedColumnKey = getColumnKey(column);
324
+ if (resolvedColumnKey) {
325
+ setLoadedListItemsByColumn(current => ({
326
+ ...current,
327
+ [resolvedColumnKey]: items,
328
+ }));
329
+ }
330
+ return items;
331
+ })
332
+ .catch(error => {
333
+ loadedListStoresRef.current.delete(loadKey);
334
+ throw error;
335
+ });
336
+ }, [currentCompany?.id, getOptionsForColumn, loadedListItemsByColumn, requestParams]);
285
337
 
286
338
  if (filterColumns.length === 0) return null;
287
-
288
- const renderFilterField = (column, compact = false) => {
289
- const key = getColumnKey(column);
290
- const fieldStyle = compact ? styles.modalField : styles.field;
291
-
339
+
340
+ const renderFilterField = (column, compact = false) => {
341
+ const key = getColumnKey(column);
342
+ const fieldStyle = compact ? styles.modalField : styles.field;
343
+
292
344
  if (isDateLikeColumn(column)) {
293
345
  const dateState = resolveDateState(filters[key]);
294
-
295
- return (
296
- <View key={key} style={fieldStyle}>
346
+
347
+ return (
348
+ <View key={key} style={fieldStyle}>
297
349
  <DateShortcutFilter
298
350
  value={dateState.value}
299
- onChange={optionKey => {
300
- updateFilter(
301
- key,
302
- resolveNextDateFilterValue(filters[key], optionKey),
303
- );
304
- }}
305
- customRange={dateState.customRange}
306
- onCustomRangeChange={range => {
307
- updateFilter(key, {
308
- ...(filters[key] || {}),
309
- shortcut: 'custom',
310
- customRange: range,
311
- });
312
- }}
313
- dense
351
+ onChange={optionKey => {
352
+ updateFilter(
353
+ key,
354
+ resolveNextDateFilterValue(filters[key], optionKey),
355
+ );
356
+ }}
357
+ customRange={dateState.customRange}
358
+ onCustomRangeChange={range => {
359
+ updateFilter(key, {
360
+ ...(filters[key] || {}),
361
+ shortcut: 'custom',
362
+ customRange: range,
363
+ });
364
+ }}
365
+ dense
314
366
  store={storeName}
315
367
  field={key}
316
368
  labelCaption={global.t?.t(storeName, 'label', column.label || key)}
317
369
  optionKeys={dateOptionKeys}
318
- colors={{
319
- accent: resolvedAccentColor,
320
- appBg: 'transparent',
321
- border: borderColor,
322
- borderSoft: withOpacity(borderColor, 0.72),
323
- cardBg: palette.background,
324
- cardBgSoft: surfaceColor,
325
- danger: palette.error,
326
- isLight: true,
327
- panelBg: panelColor,
328
- pillTextDark: onAccentColor,
329
- textPrimary: textColor,
330
- textSecondary: textSecondaryColor,
331
- }}
332
- />
333
- </View>
334
- );
370
+ colors={{
371
+ accent: resolvedAccentColor,
372
+ appBg: 'transparent',
373
+ border: borderColor,
374
+ borderSoft: withOpacity(borderColor, 0.72),
375
+ cardBg: palette.background,
376
+ cardBgSoft: surfaceColor,
377
+ danger: palette.error,
378
+ isLight: true,
379
+ panelBg: panelColor,
380
+ pillTextDark: onAccentColor,
381
+ textPrimary: textColor,
382
+ textSecondary: textSecondaryColor,
383
+ }}
384
+ />
385
+ </View>
386
+ );
335
387
  }
336
388
 
337
389
  if (column.list) {
338
390
  const listStoreName = resolveStoreNameFromList(column?.list);
391
+ const columnKey = getColumnKey(column);
392
+ const explicitOptions = getOptionsForColumn?.(column);
393
+ const loadedItems = loadedListItemsByColumn[columnKey] || [];
394
+ /*
395
+ * @agents Do not read shared store items here — only options loaded on
396
+ * modal open (or explicitly provided) so company-scoped lists stay correct.
397
+ */
398
+ const listSource = Array.isArray(explicitOptions) && explicitOptions.length > 0
399
+ ? explicitOptions
400
+ : loadedItems;
339
401
  const options = [
340
402
  {
341
403
  key: '',
@@ -345,21 +407,25 @@ const DefaultExternalFilters = ({
345
407
  : null) ||
346
408
  global.t?.t(storeName || 'invoice', 'label', 'select'),
347
409
  },
348
- ...buildOptionsFromColumn(column, getOptionsForColumn, storeName),
410
+ ...buildOptionsFromColumn(
411
+ column,
412
+ () => listSource,
413
+ storeName,
414
+ ),
349
415
  ];
350
416
  const selectedKey = normalizeFilterValue(filters[key]);
351
- const selectedLabel =
352
- options.find(option => option.key === selectedKey)?.label ||
353
- options[0]?.label ||
354
- '';
355
-
356
- return (
357
- <View key={key} style={fieldStyle}>
358
- <CompactFilterSelector
359
- icon={resolveIcon(column)}
360
- label={selectedLabel}
361
- accentColor={resolvedAccentColor}
362
- active={Boolean(selectedKey)}
417
+ const selectedLabel =
418
+ options.find(option => option.key === selectedKey)?.label ||
419
+ options[0]?.label ||
420
+ '';
421
+
422
+ return (
423
+ <View key={key} style={fieldStyle}>
424
+ <CompactFilterSelector
425
+ icon={resolveIcon(column)}
426
+ label={selectedLabel}
427
+ accentColor={resolvedAccentColor}
428
+ active={Boolean(selectedKey)}
363
429
  dense
364
430
  store={storeName}
365
431
  field={key}
@@ -368,36 +434,36 @@ const DefaultExternalFilters = ({
368
434
  options={options}
369
435
  searchable
370
436
  selectedKey={selectedKey}
371
- onSelect={optionKey => {
372
- updateFilter(key, optionKey);
373
- return true;
374
- }}
375
- />
376
- </View>
377
- );
378
- }
379
-
380
- return (
381
- <View key={key} style={[fieldStyle, styles.inputWrap, { borderColor, backgroundColor: surfaceColor }]}>
382
- <Text style={[styles.inputLabel, { color: textSecondaryColor }]}>
383
- {global.t?.t(storeName, 'input', column.label || key)}
384
- </Text>
385
- <TextInput
386
- style={[styles.input, { color: textColor }]}
387
- value={normalizeText(filters[key])}
388
- onChangeText={value => updateFilter(key, value)}
389
- onSubmitEditing={() => onChangeFilters?.(filters)}
390
- />
391
- </View>
392
- );
393
- };
394
-
395
- const filterTitle = global.t?.t(storeName, 'label', 'filters');
396
- const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
397
-
437
+ onSelect={optionKey => {
438
+ updateFilter(key, optionKey);
439
+ return true;
440
+ }}
441
+ />
442
+ </View>
443
+ );
444
+ }
445
+
446
+ return (
447
+ <View key={key} style={[fieldStyle, styles.inputWrap, { borderColor, backgroundColor: surfaceColor }]}>
448
+ <Text style={[styles.inputLabel, { color: textSecondaryColor }]}>
449
+ {global.t?.t(storeName, 'input', column.label || key)}
450
+ </Text>
451
+ <TextInput
452
+ style={[styles.input, { color: textColor }]}
453
+ value={normalizeText(filters[key])}
454
+ onChangeText={value => updateFilter(key, value)}
455
+ onSubmitEditing={() => applyFilters(filters)}
456
+ />
457
+ </View>
458
+ );
459
+ };
460
+
461
+ const filterTitle = global.t?.t(storeName, 'label', 'filters');
462
+ const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
463
+
398
464
  if (isCompactView) {
399
- return (
400
- <View style={styles.mobileWrap}>
465
+ return (
466
+ <View style={styles.mobileWrap}>
401
467
  <TouchableOpacity
402
468
  style={[
403
469
  styles.mobileButton,
@@ -422,83 +488,83 @@ const DefaultExternalFilters = ({
422
488
  <Text style={styles.mobileCountBadgeText}>{activeCount}</Text>
423
489
  </View>
424
490
  ) : null}
425
- </TouchableOpacity>
426
-
427
- <Modal
428
- transparent
429
- visible={isFiltersModalOpen}
430
- animationType="fade"
431
- onRequestClose={() => setIsFiltersModalOpen(false)}
432
- >
433
- <TouchableWithoutFeedback onPress={() => setIsFiltersModalOpen(false)}>
434
- <View style={[styles.modalOverlay, { backgroundColor: withOpacity(textColor, 0.42) }]}>
435
- <TouchableWithoutFeedback onPress={noop}>
436
- <View style={[styles.modalCard, { borderColor, backgroundColor: surfaceColor }]}>
437
- <View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
438
- <Text style={[styles.modalTitle, { color: textColor }]}>{filterTitle}</Text>
439
- <TouchableOpacity
440
- style={[styles.modalCloseButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
441
- activeOpacity={0.82}
442
- onPress={() => setIsFiltersModalOpen(false)}
443
- >
444
- <Icon name="x" size={18} color={textSecondaryColor} />
445
- </TouchableOpacity>
446
- </View>
447
-
448
- <ScrollView
449
- style={styles.modalScroll}
450
- contentContainerStyle={styles.modalContent}
451
- keyboardShouldPersistTaps="handled"
452
- >
453
- {filterFields(true)}
454
- </ScrollView>
455
-
456
- <View style={[styles.modalActions, { borderTopColor: borderColor }]}>
457
- {activeCount > 0 ? (
458
- <TouchableOpacity
459
- style={[styles.modalSecondaryButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
460
- activeOpacity={0.84}
461
- onPress={clearFilters}
462
- >
463
- <Text style={[styles.modalSecondaryButtonText, { color: textColor }]}>
464
- {global.t?.t(storeName, 'button', 'clear')}
465
- </Text>
466
- </TouchableOpacity>
467
- ) : null}
491
+ </TouchableOpacity>
492
+
493
+ <Modal
494
+ transparent
495
+ visible={isFiltersModalOpen}
496
+ animationType="fade"
497
+ onRequestClose={() => setIsFiltersModalOpen(false)}
498
+ >
499
+ <TouchableWithoutFeedback onPress={() => setIsFiltersModalOpen(false)}>
500
+ <View style={[styles.modalOverlay, { backgroundColor: withOpacity(textColor, 0.42) }]}>
501
+ <TouchableWithoutFeedback onPress={noop}>
502
+ <View style={[styles.modalCard, { borderColor, backgroundColor: surfaceColor }]}>
503
+ <View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
504
+ <Text style={[styles.modalTitle, { color: textColor }]}>{filterTitle}</Text>
505
+ <TouchableOpacity
506
+ style={[styles.modalCloseButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
507
+ activeOpacity={0.82}
508
+ onPress={() => setIsFiltersModalOpen(false)}
509
+ >
510
+ <Icon name="x" size={18} color={textSecondaryColor} />
511
+ </TouchableOpacity>
512
+ </View>
513
+
514
+ <ScrollView
515
+ style={styles.modalScroll}
516
+ contentContainerStyle={styles.modalContent}
517
+ keyboardShouldPersistTaps="handled"
518
+ >
519
+ {filterFields(true)}
520
+ </ScrollView>
521
+
522
+ <View style={[styles.modalActions, { borderTopColor: borderColor }]}>
523
+ {activeCount > 0 ? (
524
+ <TouchableOpacity
525
+ style={[styles.modalSecondaryButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
526
+ activeOpacity={0.84}
527
+ onPress={clearFilters}
528
+ >
529
+ <Text style={[styles.modalSecondaryButtonText, { color: textColor }]}>
530
+ {global.t?.t(storeName, 'button', 'clear')}
531
+ </Text>
532
+ </TouchableOpacity>
533
+ ) : null}
468
534
  <TouchableOpacity
469
535
  style={[styles.modalPrimaryButton, { backgroundColor: buttonBackgroundColor }]}
470
536
  activeOpacity={0.84}
471
537
  onPress={() => setIsFiltersModalOpen(false)}
472
538
  >
473
539
  <Text style={[styles.modalPrimaryButtonText, { color: buttonTextColor }]}>
474
- {global.t?.t(storeName, 'button', 'apply')}
475
- </Text>
476
- </TouchableOpacity>
477
- </View>
478
- </View>
479
- </TouchableWithoutFeedback>
480
- </View>
481
- </TouchableWithoutFeedback>
482
- </Modal>
483
- </View>
484
- );
485
- }
486
-
487
- return (
488
- <View style={styles.wrap}>
489
- {filterFields(false)}
490
-
491
- {activeCount > 0 ? (
492
- <TouchableOpacity
493
- style={[styles.clearButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
494
- activeOpacity={0.82}
495
- onPress={clearFilters}
496
- >
497
- <Icon name="x" size={14} color={textSecondaryColor} />
498
- </TouchableOpacity>
499
- ) : null}
500
- </View>
501
- );
502
- };
503
-
504
- export default DefaultExternalFilters;
540
+ {global.t?.t(storeName, 'button', 'apply')}
541
+ </Text>
542
+ </TouchableOpacity>
543
+ </View>
544
+ </View>
545
+ </TouchableWithoutFeedback>
546
+ </View>
547
+ </TouchableWithoutFeedback>
548
+ </Modal>
549
+ </View>
550
+ );
551
+ }
552
+
553
+ return (
554
+ <View style={styles.wrap}>
555
+ {filterFields(false)}
556
+
557
+ {activeCount > 0 ? (
558
+ <TouchableOpacity
559
+ style={[styles.clearButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
560
+ activeOpacity={0.82}
561
+ onPress={clearFilters}
562
+ >
563
+ <Icon name="x" size={14} color={textSecondaryColor} />
564
+ </TouchableOpacity>
565
+ ) : null}
566
+ </View>
567
+ );
568
+ };
569
+
570
+ export default DefaultExternalFilters;