@controleonline/ui-default 1.0.263 → 1.0.265

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 (61) hide show
  1. package/AGENTS.md +47 -45
  2. package/package.json +20 -20
  3. package/src/react/components/errors/DefaultErrors.js +307 -325
  4. package/src/react/components/files/DefaultFile.js +46 -46
  5. package/src/react/components/filters/CompactFilterSelector.js +78 -24
  6. package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
  7. package/src/react/components/filters/DefaultColumnFilter.js +191 -57
  8. package/src/react/components/filters/DefaultExternalFilters.js +245 -182
  9. package/src/react/components/filters/DefaultSearch.js +131 -92
  10. package/src/react/components/filters/DefaultSearch.styles.js +65 -70
  11. package/src/react/components/form/DefaultForm.js +172 -151
  12. package/src/react/components/form/DefaultForm.styles.js +71 -66
  13. package/src/react/components/help/DefaultTooltip.js +87 -87
  14. package/src/react/components/help/DefaultTooltip.styles.js +61 -61
  15. package/src/react/components/inputs/DefaultDateInput.js +150 -0
  16. package/src/react/components/inputs/DefaultInput.js +162 -127
  17. package/src/react/components/inputs/DefaultInput.styles.js +6 -0
  18. package/src/react/components/inputs/DefaultSelect.js +320 -152
  19. package/src/react/components/inputs/defaultInputUtils.js +40 -19
  20. package/src/react/components/map/DefaultGoogleMap.styles.js +115 -9
  21. package/src/react/components/map/DefaultGoogleMap.web.js +560 -446
  22. package/src/react/components/map/DefaultMap.native.js +73 -71
  23. package/src/react/components/map/DefaultMap.styles.js +16 -16
  24. package/src/react/components/map/DefaultMap.web.js +68 -66
  25. package/src/react/components/map/DefaultNativeMap.native.js +307 -183
  26. package/src/react/components/map/DefaultNativeMap.shared.js +439 -340
  27. package/src/react/components/map/DefaultNativeMap.styles.js +156 -122
  28. package/src/react/components/table/DefaultColumnMenu.js +108 -0
  29. package/src/react/components/table/DefaultDebug.js +233 -0
  30. package/src/react/components/table/DefaultFiltersModal.js +142 -0
  31. package/src/react/components/table/DefaultModalButton.js +18 -0
  32. package/src/react/components/table/DefaultSearchModal.js +49 -0
  33. package/src/react/components/table/DefaultTable.js +345 -1846
  34. package/src/react/components/table/DefaultTable.styles.js +247 -136
  35. package/src/react/components/table/DefaultTable.utils.js +502 -0
  36. package/src/react/components/table/DefaultTableBody.js +26 -0
  37. package/src/react/components/table/DefaultTableCards.js +171 -0
  38. package/src/react/components/table/DefaultTableControls.js +208 -0
  39. package/src/react/components/table/DefaultTableEmptyState.js +22 -0
  40. package/src/react/components/table/DefaultTableFooter.js +172 -0
  41. package/src/react/components/table/DefaultTableInput.js +145 -0
  42. package/src/react/components/table/DefaultTableRows.js +340 -0
  43. package/src/react/components/table/DefaultTableSearch.js +100 -0
  44. package/src/react/components/table/DefaultTableToolbar.js +98 -0
  45. package/src/react/components/table/DefaultToolbarAction.js +56 -0
  46. package/src/react/components/table/useDefaultTablePagination.js +370 -0
  47. package/src/react/components/table/useDefaultTableSorting.js +126 -0
  48. package/src/react/components/table/useDefaultTableTheme.js +58 -0
  49. package/src/react/index.js +10 -10
  50. package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
  51. package/src/store/default/actions.js +51 -13
  52. package/src/store/default/getters.js +10 -8
  53. package/src/store/default/mutation_types.js +4 -2
  54. package/src/store/default/mutations.js +10 -1
  55. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -162
  56. package/src/tests/react/components/files/DefaultFile.test.js +80 -80
  57. package/src/tests/react/components/form/DefaultForm.test.js +81 -0
  58. package/src/tests/react/components/inputs/DefaultSelect.test.js +185 -0
  59. package/src/tests/react/components/table/DefaultTable.test.js +1183 -0
  60. package/src/tests/react/store/defaultActions.test.js +59 -0
  61. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,6 +1,6 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import {
3
- Modal,
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Modal,
4
4
  ScrollView,
5
5
  Text,
6
6
  TextInput,
@@ -8,59 +8,85 @@ import {
8
8
  TouchableWithoutFeedback,
9
9
  View,
10
10
  useWindowDimensions,
11
- } from 'react-native';
12
- import Icon from 'react-native-vector-icons/Feather';
13
- import { useStore } from '@store';
14
- import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
15
- import { colors } from '@controleonline/../../src/styles/colors';
11
+ } from 'react-native';
12
+ import Icon from 'react-native-vector-icons/Feather';
13
+ import { getAllStores, useStore } from '@store';
14
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
15
+ import { colors } from '@controleonline/../../src/styles/colors';
16
+ import {
17
+ buildOptionsFromColumn,
18
+ getColumnKey,
19
+ isDateLikeColumn,
20
+ resolveStoreNameFromList,
21
+ } from '../inputs/defaultInputUtils';
16
22
  import CompactFilterSelector from './CompactFilterSelector';
17
23
  import DateShortcutFilter from './DateShortcutFilter';
18
24
  import { resolveNextDateFilterValue } from './dateFilterSelection';
19
25
  import styles from './DefaultExternalFilters.styles';
20
26
 
21
- const DEFAULT_COMPACT_BREAKPOINT = 768;
22
- const noop = () => {};
23
-
24
- const normalizeText = value => String(value || '').trim();
25
- const DEFAULT_TRANSLATABLE_FIELDS = new Set([
26
- 'active',
27
- 'app',
28
- 'channel',
29
- 'displaytype',
30
- 'featured',
31
- 'frequency',
32
- 'invoicetype',
33
- 'installments',
34
- 'ordertype',
35
- 'peopletype',
36
- 'pricecalculation',
37
- 'productcondition',
38
- 'realstatus',
39
- 'status',
40
- ]);
41
-
42
- const getColumnKey = column => column?.key || column?.name || '';
43
-
44
- const normalizeColumnKey = value =>
45
- normalizeText(value)
46
- .replace(/[^a-zA-Z0-9]/g, '')
47
- .toLowerCase();
48
-
49
- const shouldTranslateOptionLabel = (column, storeName, rawLabel) => {
50
- if (column?.translate === false || !normalizeText(rawLabel) || !normalizeText(storeName)) {
51
- return false;
52
- }
53
-
54
- if (column?.translate === true || Array.isArray(column?.list)) {
55
- return true;
56
- }
57
-
58
- return [column?.key, column?.name, column?.label]
59
- .map(normalizeColumnKey)
60
- .filter(Boolean)
61
- .some(candidate => DEFAULT_TRANSLATABLE_FIELDS.has(candidate));
62
- };
63
-
27
+ const DEFAULT_COMPACT_BREAKPOINT = 768;
28
+ const noop = () => {};
29
+
30
+ const normalizeText = value => String(value || '').trim();
31
+ const resolveListActionName = list =>
32
+ normalizeText(list).split('/')[1] || 'getItems';
33
+
34
+ const COMPANY_SCOPED_LIST_STORES = new Set([
35
+ 'categories',
36
+ 'paymentType',
37
+ 'wallet',
38
+ ]);
39
+
40
+ const resolveListLoadParams = ({currentCompanyId, listStoreName}) => {
41
+ if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
42
+ return {};
43
+ }
44
+
45
+ return listStoreName === 'categories'
46
+ ? {company: currentCompanyId}
47
+ : {people: currentCompanyId};
48
+ };
49
+
50
+ const resolveColumnListLoadParams = ({
51
+ column,
52
+ currentCompanyId,
53
+ requestParams = {},
54
+ searchValue = '',
55
+ }) => {
56
+ const listStoreName = resolveStoreNameFromList(column?.list);
57
+ const resolvedCustomParams = typeof column?.listRequestParams === 'function'
58
+ ? column.listRequestParams({currentCompanyId, requestParams})
59
+ : column?.listRequestParams;
60
+ const customParams =
61
+ resolvedCustomParams &&
62
+ typeof resolvedCustomParams === 'object' &&
63
+ !Array.isArray(resolvedCustomParams)
64
+ ? resolvedCustomParams
65
+ : {};
66
+
67
+ return {
68
+ ...resolveListLoadParams({currentCompanyId, listStoreName}),
69
+ ...customParams,
70
+ ...(normalizeText(searchValue)
71
+ ? {[column?.listSearchParam || column?.searchParam || 'search']: normalizeText(searchValue)}
72
+ : {}),
73
+ };
74
+ };
75
+
76
+ const stableSerialize = value => {
77
+ if (value === null || value === undefined) return 'null';
78
+ if (Array.isArray(value)) return `[${value.map(item => stableSerialize(item)).join(',')}]`;
79
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
80
+ if (typeof value === 'object') {
81
+ return `{${Object.keys(value)
82
+ .sort()
83
+ .map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
84
+ .join(',')}}`;
85
+ }
86
+
87
+ return JSON.stringify(value);
88
+ };
89
+
64
90
  const shouldIncludeColumn = column =>
65
91
  Boolean(getColumnKey(column)) &&
66
92
  column?.show !== false &&
@@ -69,13 +95,13 @@ const shouldIncludeColumn = column =>
69
95
  column?.filter !== false &&
70
96
  column?.filters !== false;
71
97
 
72
- const normalizeFilterValue = value => {
73
- if (value && typeof value === 'object') {
74
- return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? '');
75
- }
76
-
77
- return normalizeText(value);
78
- };
98
+ const normalizeFilterValue = value => {
99
+ if (value && typeof value === 'object') {
100
+ return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? value.key ?? '');
101
+ }
102
+
103
+ return normalizeText(value);
104
+ };
79
105
 
80
106
  const isFilledFilterValue = value => {
81
107
  if (Array.isArray(value)) return value.length > 0;
@@ -86,52 +112,6 @@ const isFilledFilterValue = value => {
86
112
  return normalizeText(value) !== '';
87
113
  };
88
114
 
89
- const resolveOptionLabel = (column, option, storeName = '') => {
90
- if (!option) return '';
91
-
92
- const translateOptionLabel = rawLabel => {
93
- const normalizedLabel = normalizeText(rawLabel);
94
- if (!shouldTranslateOptionLabel(column, storeName, normalizedLabel)) {
95
- return normalizedLabel;
96
- }
97
-
98
- return global.t?.t(storeName, 'label', normalizedLabel) || normalizedLabel;
99
- };
100
-
101
- if (typeof column?.formatList === 'function') {
102
- const formatted = column.formatList(option, null, column);
103
- if (formatted && typeof formatted === 'object') {
104
- return translateOptionLabel(formatted.label ?? formatted.value);
105
- }
106
- if (formatted) return translateOptionLabel(formatted);
107
- }
108
-
109
- const rawLabel = normalizeText(
110
- option.label ??
111
- option[column?.searchParam] ??
112
- option[column?.name] ??
113
- option.name ??
114
- option.status ??
115
- option.wallet ??
116
- option.paymentType ??
117
- option.alias ??
118
- option.id,
119
- );
120
-
121
- return translateOptionLabel(rawLabel);
122
- };
123
-
124
- const buildColumnOptions = (column, options = [], storeName = '') => [
125
- {
126
- key: '',
127
- label: global.t?.t(storeName || 'invoice', 'label', 'select'),
128
- },
129
- ...(Array.isArray(options) ? options : []).map(option => ({
130
- key: normalizeFilterValue(option),
131
- label: resolveOptionLabel(column, option, storeName) || '-',
132
- })),
133
- ];
134
-
135
115
  const resolveDateState = filterValue => {
136
116
  if (!filterValue || typeof filterValue !== 'object') {
137
117
  return {
@@ -167,27 +147,29 @@ const resolveIcon = column => {
167
147
  const key = getColumnKey(column);
168
148
  if (key === 'status') return 'check-circle';
169
149
  if (key === 'category') return 'tag';
170
- if (column?.inputType === 'date-range' || column?.type === 'range-date') return 'calendar';
150
+ if (isDateLikeColumn(column)) return 'calendar';
171
151
  return 'sliders';
172
152
  };
173
153
 
174
154
  const DefaultExternalFilters = ({
175
- accentColor = null,
176
- compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
177
- columns = [],
178
- dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
179
- filters = {},
180
- getOptionsForColumn = null,
181
- onActiveCountChange = null,
182
- onChangeFilters = null,
183
- storeName = '',
184
- }) => {
185
- const { width } = useWindowDimensions();
186
- const peopleStore = useStore('people');
187
- const themeStore = useStore('theme');
188
- const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
189
- const { currentCompany } = peopleStore.getters || {};
190
- const { colors: themeColors } = themeStore.getters || {};
155
+ accentColor = null,
156
+ compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
157
+ dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
158
+ filters = {},
159
+ getOptionsForColumn = null,
160
+ onActiveCountChange = null,
161
+ onChangeFilters = null,
162
+ requestParams = {},
163
+ storeName = '',
164
+ }) => {
165
+ const { width } = useWindowDimensions();
166
+ const externalFilterStore = useStore(storeName);
167
+ const peopleStore = useStore('people');
168
+ const themeStore = useStore('theme');
169
+ const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
170
+ const loadedListStoresRef = useRef(new Set());
171
+ const { currentCompany } = peopleStore.getters || {};
172
+ const { colors: themeColors } = themeStore.getters;
191
173
  const themeTokens = useMemo(
192
174
  () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
193
175
  [currentCompany?.theme?.colors, themeColors],
@@ -199,15 +181,24 @@ const DefaultExternalFilters = ({
199
181
  const resolvedAccentColor = accentColor || palette.primary;
200
182
  const surfaceColor = themeTokens['bg-odd-light'] || palette.background;
201
183
  const panelColor = themeTokens['bg-headers-light'] || palette.background;
202
- const borderColor = palette.border;
203
- const textColor = palette.text;
204
- const textSecondaryColor = palette.textSecondary;
205
- const onAccentColor = palette.secondary || palette.text;
206
- const isCompactView = width > 0 && width <= compactBreakpoint;
207
- const filterColumns = useMemo(
208
- () => columns.filter(shouldIncludeColumn),
209
- [columns],
210
- );
184
+ const borderColor = palette.border;
185
+ const textColor = palette.text;
186
+ const textSecondaryColor = palette.textSecondary;
187
+ const buttonBackgroundColor = themeColors.buttonBackground;
188
+ const buttonTextColor = themeColors.buttonText;
189
+ const tableFilterBackgroundColor = themeColors.tableFilterBackground;
190
+ const tableFilterBorderColor = themeColors.tableFilterBorder;
191
+ const tableFilterTextColor = themeColors.tableFilterText;
192
+ const onAccentColor = palette.secondary || palette.text;
193
+ const isCompactView = width > 0 && width <= compactBreakpoint;
194
+ const resolvedColumns = useMemo(() => {
195
+ const storeColumns = externalFilterStore?.getters?.columns;
196
+ return Array.isArray(storeColumns) ? storeColumns : [];
197
+ }, [externalFilterStore?.getters?.columns]);
198
+ const filterColumns = useMemo(
199
+ () => resolvedColumns.filter(shouldIncludeColumn),
200
+ [resolvedColumns],
201
+ );
211
202
  const activeCount = useMemo(
212
203
  () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
213
204
  [filterColumns, filters],
@@ -231,23 +222,80 @@ const DefaultExternalFilters = ({
231
222
  onChangeFilters?.({});
232
223
  }, [onChangeFilters]);
233
224
 
234
- useEffect(() => {
235
- if (!isCompactView) setIsFiltersModalOpen(false);
236
- }, [isCompactView]);
237
-
238
- if (filterColumns.length === 0) return null;
225
+ useEffect(() => {
226
+ if (!isCompactView) setIsFiltersModalOpen(false);
227
+ }, [isCompactView]);
228
+
229
+ useEffect(() => {
230
+ loadedListStoresRef.current.clear();
231
+ }, [currentCompany?.id]);
232
+
233
+ const loadListOptionsForColumn = useCallback((column, searchValue = '') => {
234
+ const explicitOptions = getOptionsForColumn?.(column);
235
+ if (!normalizeText(searchValue) && Array.isArray(explicitOptions) && explicitOptions.length > 0) {
236
+ return Promise.resolve([]);
237
+ }
238
+
239
+ const listStoreName = resolveStoreNameFromList(column?.list);
240
+ const actionName = resolveListActionName(column?.list);
241
+ const stores = getAllStores?.() || {};
242
+ const listStore = stores?.[listStoreName];
243
+ const listAction = listStore?.actions?.[actionName];
244
+
245
+ if (!listStoreName || typeof listAction !== 'function') {
246
+ return Promise.resolve([]);
247
+ }
248
+
249
+ const listLoadParams = resolveColumnListLoadParams({
250
+ column,
251
+ currentCompanyId: currentCompany?.id,
252
+ requestParams,
253
+ searchValue,
254
+ });
255
+ const hasScopedParams = Object.keys(listLoadParams).length > 0;
256
+
257
+ if (
258
+ !normalizeText(searchValue) &&
259
+ !hasScopedParams &&
260
+ Array.isArray(listStore?.getters?.items) &&
261
+ listStore.getters.items.length > 0
262
+ ) {
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 || []);
269
+ }
270
+
271
+ loadedListStoresRef.current.add(loadKey);
272
+ return Promise.resolve(
273
+ listAction({
274
+ ...listLoadParams,
275
+ __storeMeta: {
276
+ dedupeKey: `default-external-filter-list-options:${loadKey}`,
277
+ skipSystemError: true,
278
+ },
279
+ }),
280
+ ).catch(error => {
281
+ loadedListStoresRef.current.delete(loadKey);
282
+ throw error;
283
+ });
284
+ }, [currentCompany?.id, getOptionsForColumn, requestParams]);
285
+
286
+ if (filterColumns.length === 0) return null;
239
287
 
240
288
  const renderFilterField = (column, compact = false) => {
241
289
  const key = getColumnKey(column);
242
290
  const fieldStyle = compact ? styles.modalField : styles.field;
243
291
 
244
- if (column.inputType === 'date-range' || column.type === 'range-date') {
292
+ if (isDateLikeColumn(column)) {
245
293
  const dateState = resolveDateState(filters[key]);
246
294
 
247
295
  return (
248
296
  <View key={key} style={fieldStyle}>
249
- <DateShortcutFilter
250
- value={dateState.value}
297
+ <DateShortcutFilter
298
+ value={dateState.value}
251
299
  onChange={optionKey => {
252
300
  updateFilter(
253
301
  key,
@@ -263,9 +311,10 @@ const DefaultExternalFilters = ({
263
311
  });
264
312
  }}
265
313
  dense
266
- store={storeName}
267
- field={key}
268
- optionKeys={dateOptionKeys}
314
+ store={storeName}
315
+ field={key}
316
+ labelCaption={global.t?.t(storeName, 'label', column.label || key)}
317
+ optionKeys={dateOptionKeys}
269
318
  colors={{
270
319
  accent: resolvedAccentColor,
271
320
  appBg: 'transparent',
@@ -283,11 +332,22 @@ const DefaultExternalFilters = ({
283
332
  />
284
333
  </View>
285
334
  );
286
- }
287
-
288
- if (column.list) {
289
- const options = buildColumnOptions(column, getOptionsForColumn?.(column) || [], storeName);
290
- const selectedKey = normalizeFilterValue(filters[key]);
335
+ }
336
+
337
+ if (column.list) {
338
+ const listStoreName = resolveStoreNameFromList(column?.list);
339
+ const options = [
340
+ {
341
+ key: '',
342
+ label:
343
+ (column?.emptyOptionLabel
344
+ ? global.t?.t(storeName || 'invoice', 'label', column.emptyOptionLabel) || column.emptyOptionLabel
345
+ : null) ||
346
+ global.t?.t(storeName || 'invoice', 'label', 'select'),
347
+ },
348
+ ...buildOptionsFromColumn(column, getOptionsForColumn, storeName),
349
+ ];
350
+ const selectedKey = normalizeFilterValue(filters[key]);
291
351
  const selectedLabel =
292
352
  options.find(option => option.key === selectedKey)?.label ||
293
353
  options[0]?.label ||
@@ -300,11 +360,14 @@ const DefaultExternalFilters = ({
300
360
  label={selectedLabel}
301
361
  accentColor={resolvedAccentColor}
302
362
  active={Boolean(selectedKey)}
303
- dense
304
- store={storeName}
305
- field={key}
306
- options={options}
307
- selectedKey={selectedKey}
363
+ dense
364
+ store={storeName}
365
+ field={key}
366
+ onBeforeOpen={listStoreName ? () => loadListOptionsForColumn(column) : null}
367
+ onSearchChange={listStoreName ? value => loadListOptionsForColumn(column, value) : null}
368
+ options={options}
369
+ searchable
370
+ selectedKey={selectedKey}
308
371
  onSelect={optionKey => {
309
372
  updateFilter(key, optionKey);
310
373
  return true;
@@ -332,33 +395,33 @@ const DefaultExternalFilters = ({
332
395
  const filterTitle = global.t?.t(storeName, 'label', 'filters');
333
396
  const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
334
397
 
335
- if (isCompactView) {
398
+ if (isCompactView) {
336
399
  return (
337
400
  <View style={styles.mobileWrap}>
338
- <TouchableOpacity
339
- style={[
340
- styles.mobileButton,
341
- { borderColor: borderColor, backgroundColor: surfaceColor },
342
- activeCount > 0 ? { borderColor: resolvedAccentColor, backgroundColor: withOpacity(resolvedAccentColor, 0.14) } : null,
343
- ]}
344
- activeOpacity={0.84}
345
- onPress={() => setIsFiltersModalOpen(true)}
346
- >
347
- <Icon name="filter" size={15} color={activeCount > 0 ? resolvedAccentColor : textSecondaryColor} />
348
- <Text
349
- style={[
350
- styles.mobileButtonText,
351
- { color: textColor },
352
- activeCount > 0 ? { color: resolvedAccentColor } : null,
353
- ]}
354
- >
355
- {filterTitle}
356
- </Text>
357
- {activeCount > 0 ? (
358
- <View style={[styles.mobileCountBadge, { backgroundColor: resolvedAccentColor }]}>
359
- <Text style={styles.mobileCountBadgeText}>{activeCount}</Text>
360
- </View>
361
- ) : null}
401
+ <TouchableOpacity
402
+ style={[
403
+ styles.mobileButton,
404
+ { borderColor: borderColor, backgroundColor: surfaceColor },
405
+ activeCount > 0 ? { borderColor: tableFilterBorderColor, backgroundColor: tableFilterBackgroundColor } : null,
406
+ ]}
407
+ activeOpacity={0.84}
408
+ onPress={() => setIsFiltersModalOpen(true)}
409
+ >
410
+ <Icon name="filter" size={15} color={activeCount > 0 ? tableFilterTextColor : textSecondaryColor} />
411
+ <Text
412
+ style={[
413
+ styles.mobileButtonText,
414
+ { color: textColor },
415
+ activeCount > 0 ? { color: tableFilterTextColor } : null,
416
+ ]}
417
+ >
418
+ {filterTitle}
419
+ </Text>
420
+ {activeCount > 0 ? (
421
+ <View style={[styles.mobileCountBadge, { backgroundColor: tableFilterBorderColor }]}>
422
+ <Text style={styles.mobileCountBadgeText}>{activeCount}</Text>
423
+ </View>
424
+ ) : null}
362
425
  </TouchableOpacity>
363
426
 
364
427
  <Modal
@@ -402,12 +465,12 @@ const DefaultExternalFilters = ({
402
465
  </Text>
403
466
  </TouchableOpacity>
404
467
  ) : null}
405
- <TouchableOpacity
406
- style={[styles.modalPrimaryButton, { backgroundColor: resolvedAccentColor }]}
407
- activeOpacity={0.84}
408
- onPress={() => setIsFiltersModalOpen(false)}
409
- >
410
- <Text style={[styles.modalPrimaryButtonText, { color: onAccentColor }]}>
468
+ <TouchableOpacity
469
+ style={[styles.modalPrimaryButton, { backgroundColor: buttonBackgroundColor }]}
470
+ activeOpacity={0.84}
471
+ onPress={() => setIsFiltersModalOpen(false)}
472
+ >
473
+ <Text style={[styles.modalPrimaryButtonText, { color: buttonTextColor }]}>
411
474
  {global.t?.t(storeName, 'button', 'apply')}
412
475
  </Text>
413
476
  </TouchableOpacity>