@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,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,17 @@ 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 { isFilledFilterValue, omitEmptyFilters } from './filterValue';
26
+ import {
27
+ persistTableFiltersPreference,
28
+ resolveDefaultTablePreferenceScope,
29
+ sanitizeTableFiltersPreference,
30
+ } from '../../utils/tableVisibleColumnsPreferences';
31
+ import styles from './DefaultExternalFilters.styles';
32
+
27
33
  const DEFAULT_COMPACT_BREAKPOINT = 768;
28
34
  const noop = () => {};
29
35
 
@@ -94,7 +100,7 @@ const shouldIncludeColumn = column =>
94
100
  column?.externalFilter === true &&
95
101
  column?.filter !== false &&
96
102
  column?.filters !== false;
97
-
103
+
98
104
  const normalizeFilterValue = value => {
99
105
  if (value && typeof value === 'object') {
100
106
  return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? value.key ?? '');
@@ -102,56 +108,47 @@ const normalizeFilterValue = value => {
102
108
 
103
109
  return normalizeText(value);
104
110
  };
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';
111
+
112
+ const resolveDateState = filterValue => {
113
+ if (!filterValue || typeof filterValue !== 'object') {
114
+ return {
115
+ customRange: { from: '', to: '' },
116
+ value: 'all',
117
+ };
118
+ }
119
+
120
+ if (filterValue.shortcut) {
121
+ return {
122
+ customRange: filterValue.customRange || { from: '', to: '' },
123
+ value: filterValue.shortcut,
124
+ };
125
+ }
126
+
127
+ if (filterValue.start || filterValue.end || filterValue.after || filterValue.before) {
128
+ return {
129
+ customRange: {
130
+ from: filterValue.start || filterValue.after || '',
131
+ to: filterValue.end || filterValue.before || '',
132
+ },
133
+ value: 'custom',
134
+ };
135
+ }
136
+
137
+ return {
138
+ customRange: { from: '', to: '' },
139
+ value: 'all',
140
+ };
141
+ };
142
+
143
+ const resolveIcon = column => {
144
+ const key = getColumnKey(column);
145
+ if (key === 'status') return 'check-circle';
146
+ if (key === 'category') return 'tag';
150
147
  if (isDateLikeColumn(column)) return 'calendar';
151
148
  return 'sliders';
152
149
  };
153
-
154
- const DefaultExternalFilters = ({
150
+
151
+ const DefaultExternalFilters = ({
155
152
  accentColor = null,
156
153
  compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
157
154
  dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
@@ -165,22 +162,30 @@ const DefaultExternalFilters = ({
165
162
  const { width } = useWindowDimensions();
166
163
  const externalFilterStore = useStore(storeName);
167
164
  const peopleStore = useStore('people');
165
+ const storeConfigs = externalFilterStore?.getters?.configs || {};
166
+ const tablePreferenceScope =
167
+ storeConfigs.tablePreferenceScope ||
168
+ resolveDefaultTablePreferenceScope({
169
+ companyId: peopleStore?.getters?.currentCompany?.id,
170
+ storeName,
171
+ });
168
172
  const themeStore = useStore('theme');
169
173
  const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
174
+ const [loadedListItemsByColumn, setLoadedListItemsByColumn] = useState({});
170
175
  const loadedListStoresRef = useRef(new Set());
171
176
  const { currentCompany } = peopleStore.getters || {};
172
177
  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;
178
+ const themeTokens = useMemo(
179
+ () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
180
+ [currentCompany?.theme?.colors, themeColors],
181
+ );
182
+ const palette = useMemo(
183
+ () => resolveThemePalette(themeTokens, colors),
184
+ [themeTokens],
185
+ );
186
+ const resolvedAccentColor = accentColor || palette.primary;
187
+ const surfaceColor = themeTokens['bg-odd-light'] || palette.background;
188
+ const panelColor = themeTokens['bg-headers-light'] || palette.background;
184
189
  const borderColor = palette.border;
185
190
  const textColor = palette.text;
186
191
  const textSecondaryColor = palette.textSecondary;
@@ -199,35 +204,61 @@ const DefaultExternalFilters = ({
199
204
  () => resolvedColumns.filter(shouldIncludeColumn),
200
205
  [resolvedColumns],
201
206
  );
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
-
207
+ const activeCount = useMemo(
208
+ () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
209
+ [filterColumns, filters],
210
+ );
211
+
212
+ useEffect(() => {
213
+ onActiveCountChange?.(activeCount);
214
+ }, [activeCount, onActiveCountChange]);
215
+
216
+ const applyFilters = useCallback(nextFilters => {
217
+ const resolvedFilters = omitEmptyFilters(
218
+ nextFilters && typeof nextFilters === 'object' && !Array.isArray(nextFilters)
219
+ ? nextFilters
220
+ : {},
221
+ );
222
+ const columns = Array.isArray(externalFilterStore?.getters?.columns)
223
+ ? externalFilterStore.getters.columns
224
+ : [];
225
+ const persistedFilters = sanitizeTableFiltersPreference({
226
+ columns,
227
+ filters: resolvedFilters,
228
+ });
229
+
230
+ persistTableFiltersPreference(tablePreferenceScope, persistedFilters);
231
+
232
+ if (typeof externalFilterStore?.actions?.setFilters === 'function') {
233
+ externalFilterStore.actions.setFilters(resolvedFilters);
234
+ } else if (externalFilterStore?.getters) {
235
+ externalFilterStore.getters.filters = resolvedFilters;
236
+ }
237
+
238
+ onChangeFilters?.(resolvedFilters);
239
+ }, [externalFilterStore, onChangeFilters, tablePreferenceScope]);
240
+
241
+ const updateFilter = useCallback((key, value) => {
242
+ const nextFilters = { ...(filters || {}) };
243
+ if (!isFilledFilterValue(value)) {
244
+ delete nextFilters[key];
245
+ } else {
246
+ nextFilters[key] = value;
247
+ }
248
+ applyFilters(nextFilters);
249
+ }, [applyFilters, filters]);
250
+
251
+ const clearFilters = useCallback(() => {
252
+ applyFilters({});
253
+ }, [applyFilters]);
254
+
225
255
  useEffect(() => {
226
256
  if (!isCompactView) setIsFiltersModalOpen(false);
227
257
  }, [isCompactView]);
228
258
 
229
259
  useEffect(() => {
230
260
  loadedListStoresRef.current.clear();
261
+ setLoadedListItemsByColumn({});
231
262
  }, [currentCompany?.id]);
232
263
 
233
264
  const loadListOptionsForColumn = useCallback((column, searchValue = '') => {
@@ -254,18 +285,14 @@ const DefaultExternalFilters = ({
254
285
  });
255
286
  const hasScopedParams = Object.keys(listLoadParams).length > 0;
256
287
 
288
+ const loadKey = `${getColumnKey(column)}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
289
+ const columnKey = getColumnKey(column);
257
290
  if (
258
- !normalizeText(searchValue) &&
259
- !hasScopedParams &&
260
- Array.isArray(listStore?.getters?.items) &&
261
- listStore.getters.items.length > 0
291
+ loadedListStoresRef.current.has(loadKey) &&
292
+ columnKey &&
293
+ Array.isArray(loadedListItemsByColumn[columnKey])
262
294
  ) {
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 || []);
295
+ return Promise.resolve(loadedListItemsByColumn[columnKey]);
269
296
  }
270
297
 
271
298
  loadedListStoresRef.current.add(loadKey);
@@ -277,65 +304,93 @@ const DefaultExternalFilters = ({
277
304
  skipSystemError: true,
278
305
  },
279
306
  }),
280
- ).catch(error => {
281
- loadedListStoresRef.current.delete(loadKey);
282
- throw error;
283
- });
284
- }, [currentCompany?.id, getOptionsForColumn, requestParams]);
307
+ )
308
+ .then(response => {
309
+ const items = Array.isArray(response)
310
+ ? response
311
+ : Array.isArray(response?.member)
312
+ ? response.member
313
+ : Array.isArray(response?.['hydra:member'])
314
+ ? response['hydra:member']
315
+ : listStore?.getters?.items || [];
316
+ const resolvedColumnKey = getColumnKey(column);
317
+ if (resolvedColumnKey) {
318
+ setLoadedListItemsByColumn(current => ({
319
+ ...current,
320
+ [resolvedColumnKey]: items,
321
+ }));
322
+ }
323
+ return items;
324
+ })
325
+ .catch(error => {
326
+ loadedListStoresRef.current.delete(loadKey);
327
+ throw error;
328
+ });
329
+ }, [currentCompany?.id, getOptionsForColumn, loadedListItemsByColumn, requestParams]);
285
330
 
286
331
  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
-
332
+
333
+ const renderFilterField = (column, compact = false) => {
334
+ const key = getColumnKey(column);
335
+ const fieldStyle = compact ? styles.modalField : styles.field;
336
+
292
337
  if (isDateLikeColumn(column)) {
293
338
  const dateState = resolveDateState(filters[key]);
294
-
295
- return (
296
- <View key={key} style={fieldStyle}>
339
+
340
+ return (
341
+ <View key={key} style={fieldStyle}>
297
342
  <DateShortcutFilter
298
343
  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
344
+ onChange={optionKey => {
345
+ updateFilter(
346
+ key,
347
+ resolveNextDateFilterValue(filters[key], optionKey),
348
+ );
349
+ }}
350
+ customRange={dateState.customRange}
351
+ onCustomRangeChange={range => {
352
+ updateFilter(key, {
353
+ ...(filters[key] || {}),
354
+ shortcut: 'custom',
355
+ customRange: range,
356
+ });
357
+ }}
358
+ dense
314
359
  store={storeName}
315
360
  field={key}
316
361
  labelCaption={global.t?.t(storeName, 'label', column.label || key)}
317
362
  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
- );
363
+ colors={{
364
+ accent: resolvedAccentColor,
365
+ appBg: 'transparent',
366
+ border: borderColor,
367
+ borderSoft: withOpacity(borderColor, 0.72),
368
+ cardBg: palette.background,
369
+ cardBgSoft: surfaceColor,
370
+ danger: palette.error,
371
+ isLight: true,
372
+ panelBg: panelColor,
373
+ pillTextDark: onAccentColor,
374
+ textPrimary: textColor,
375
+ textSecondary: textSecondaryColor,
376
+ }}
377
+ />
378
+ </View>
379
+ );
335
380
  }
336
381
 
337
382
  if (column.list) {
338
383
  const listStoreName = resolveStoreNameFromList(column?.list);
384
+ const columnKey = getColumnKey(column);
385
+ const explicitOptions = getOptionsForColumn?.(column);
386
+ const loadedItems = loadedListItemsByColumn[columnKey] || [];
387
+ /*
388
+ * @agents Do not read shared store items here — only options loaded on
389
+ * modal open (or explicitly provided) so company-scoped lists stay correct.
390
+ */
391
+ const listSource = Array.isArray(explicitOptions) && explicitOptions.length > 0
392
+ ? explicitOptions
393
+ : loadedItems;
339
394
  const options = [
340
395
  {
341
396
  key: '',
@@ -345,21 +400,25 @@ const DefaultExternalFilters = ({
345
400
  : null) ||
346
401
  global.t?.t(storeName || 'invoice', 'label', 'select'),
347
402
  },
348
- ...buildOptionsFromColumn(column, getOptionsForColumn, storeName),
403
+ ...buildOptionsFromColumn(
404
+ column,
405
+ () => listSource,
406
+ storeName,
407
+ ),
349
408
  ];
350
409
  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)}
410
+ const selectedLabel =
411
+ options.find(option => option.key === selectedKey)?.label ||
412
+ options[0]?.label ||
413
+ '';
414
+
415
+ return (
416
+ <View key={key} style={fieldStyle}>
417
+ <CompactFilterSelector
418
+ icon={resolveIcon(column)}
419
+ label={selectedLabel}
420
+ accentColor={resolvedAccentColor}
421
+ active={Boolean(selectedKey)}
363
422
  dense
364
423
  store={storeName}
365
424
  field={key}
@@ -368,36 +427,36 @@ const DefaultExternalFilters = ({
368
427
  options={options}
369
428
  searchable
370
429
  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
-
430
+ onSelect={optionKey => {
431
+ updateFilter(key, optionKey);
432
+ return true;
433
+ }}
434
+ />
435
+ </View>
436
+ );
437
+ }
438
+
439
+ return (
440
+ <View key={key} style={[fieldStyle, styles.inputWrap, { borderColor, backgroundColor: surfaceColor }]}>
441
+ <Text style={[styles.inputLabel, { color: textSecondaryColor }]}>
442
+ {global.t?.t(storeName, 'input', column.label || key)}
443
+ </Text>
444
+ <TextInput
445
+ style={[styles.input, { color: textColor }]}
446
+ value={normalizeText(filters[key])}
447
+ onChangeText={value => updateFilter(key, value)}
448
+ onSubmitEditing={() => applyFilters(filters)}
449
+ />
450
+ </View>
451
+ );
452
+ };
453
+
454
+ const filterTitle = global.t?.t(storeName, 'label', 'filters');
455
+ const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
456
+
398
457
  if (isCompactView) {
399
- return (
400
- <View style={styles.mobileWrap}>
458
+ return (
459
+ <View style={styles.mobileWrap}>
401
460
  <TouchableOpacity
402
461
  style={[
403
462
  styles.mobileButton,
@@ -422,83 +481,83 @@ const DefaultExternalFilters = ({
422
481
  <Text style={styles.mobileCountBadgeText}>{activeCount}</Text>
423
482
  </View>
424
483
  ) : 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}
484
+ </TouchableOpacity>
485
+
486
+ <Modal
487
+ transparent
488
+ visible={isFiltersModalOpen}
489
+ animationType="fade"
490
+ onRequestClose={() => setIsFiltersModalOpen(false)}
491
+ >
492
+ <TouchableWithoutFeedback onPress={() => setIsFiltersModalOpen(false)}>
493
+ <View style={[styles.modalOverlay, { backgroundColor: withOpacity(textColor, 0.42) }]}>
494
+ <TouchableWithoutFeedback onPress={noop}>
495
+ <View style={[styles.modalCard, { borderColor, backgroundColor: surfaceColor }]}>
496
+ <View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
497
+ <Text style={[styles.modalTitle, { color: textColor }]}>{filterTitle}</Text>
498
+ <TouchableOpacity
499
+ style={[styles.modalCloseButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
500
+ activeOpacity={0.82}
501
+ onPress={() => setIsFiltersModalOpen(false)}
502
+ >
503
+ <Icon name="x" size={18} color={textSecondaryColor} />
504
+ </TouchableOpacity>
505
+ </View>
506
+
507
+ <ScrollView
508
+ style={styles.modalScroll}
509
+ contentContainerStyle={styles.modalContent}
510
+ keyboardShouldPersistTaps="handled"
511
+ >
512
+ {filterFields(true)}
513
+ </ScrollView>
514
+
515
+ <View style={[styles.modalActions, { borderTopColor: borderColor }]}>
516
+ {activeCount > 0 ? (
517
+ <TouchableOpacity
518
+ style={[styles.modalSecondaryButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
519
+ activeOpacity={0.84}
520
+ onPress={clearFilters}
521
+ >
522
+ <Text style={[styles.modalSecondaryButtonText, { color: textColor }]}>
523
+ {global.t?.t(storeName, 'button', 'clear')}
524
+ </Text>
525
+ </TouchableOpacity>
526
+ ) : null}
468
527
  <TouchableOpacity
469
528
  style={[styles.modalPrimaryButton, { backgroundColor: buttonBackgroundColor }]}
470
529
  activeOpacity={0.84}
471
530
  onPress={() => setIsFiltersModalOpen(false)}
472
531
  >
473
532
  <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;
533
+ {global.t?.t(storeName, 'button', 'apply')}
534
+ </Text>
535
+ </TouchableOpacity>
536
+ </View>
537
+ </View>
538
+ </TouchableWithoutFeedback>
539
+ </View>
540
+ </TouchableWithoutFeedback>
541
+ </Modal>
542
+ </View>
543
+ );
544
+ }
545
+
546
+ return (
547
+ <View style={styles.wrap}>
548
+ {filterFields(false)}
549
+
550
+ {activeCount > 0 ? (
551
+ <TouchableOpacity
552
+ style={[styles.clearButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
553
+ activeOpacity={0.82}
554
+ onPress={clearFilters}
555
+ >
556
+ <Icon name="x" size={14} color={textSecondaryColor} />
557
+ </TouchableOpacity>
558
+ ) : null}
559
+ </View>
560
+ );
561
+ };
562
+
563
+ export default DefaultExternalFilters;