@controleonline/ui-default 1.0.277 → 1.0.279

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 (37) hide show
  1. package/docs/compact-order-history-1047.md +25 -0
  2. package/package.json +1 -1
  3. package/src/react/components/filters/CompactFilterSelector.js +194 -185
  4. package/src/react/components/filters/DefaultColumnFilter.js +8 -6
  5. package/src/react/components/filters/DefaultCompactFilterPicker.js +35 -0
  6. package/src/react/components/table/DefaultColumnMenu.js +36 -2
  7. package/src/react/components/table/DefaultCompactDialog.js +36 -0
  8. package/src/react/components/table/DefaultDebug.js +199 -197
  9. package/src/react/components/table/DefaultFiltersModal.js +44 -12
  10. package/src/react/components/table/DefaultTable.js +27 -27
  11. package/src/react/components/table/DefaultTableCards.js +44 -10
  12. package/src/react/components/table/DefaultTableCompact.helpers.js +26 -0
  13. package/src/react/components/table/DefaultTableCompact.styles.js +28 -0
  14. package/src/react/components/table/DefaultTableCompactToolbar.js +112 -0
  15. package/src/react/components/table/DefaultTableInput.js +30 -11
  16. package/src/react/components/table/DefaultTablePaginationBar.js +38 -0
  17. package/src/react/components/table/DefaultTableRows.js +17 -9
  18. package/src/react/components/table/DefaultTableToolbar.js +7 -0
  19. package/src/react/components/table/DefaultTableView.js +5 -2
  20. package/src/react/components/table/useDefaultTableCreateForm.js +29 -0
  21. package/src/react/components/table/useDefaultTablePagination.js +15 -1
  22. package/src/react/components/table/useDefaultTableRowInteraction.js +19 -0
  23. package/src/react/components/table/useDefaultTableStoreSync.js +30 -21
  24. package/src/react/components/table/useDefaultTableTheme.js +44 -41
  25. package/src/tests/react/components/table/DefaultTable.group1.test.js +441 -0
  26. package/src/tests/react/components/table/DefaultTable.group2.test.js +453 -0
  27. package/src/tests/react/components/table/DefaultTable.group3.test.js +289 -0
  28. package/src/tests/react/components/table/DefaultTable.test.js +1 -1381
  29. package/src/tests/react/components/table/DefaultTableToolbar.waiter.test.js +1 -0
  30. package/src/tests/react/components/table/compactDialogs.test.js +53 -0
  31. package/src/tests/react/components/table/compactFilters.test.js +47 -0
  32. package/src/tests/react/components/table/compactPagination.test.js +66 -0
  33. package/src/tests/react/components/table/compactTheme.test.js +34 -0
  34. package/src/tests/react/components/table/compactToolbar.test.js +41 -0
  35. package/src/tests/react/components/table/defaultTableStoreHarness.cjs +35 -0
  36. package/src/tests/react/components/table/defaultTableTestSetup.cjs +207 -0
  37. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +38 -1
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, {useEffect, useState} from 'react';
2
2
  import { Modal, ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
3
  import Icon from 'react-native-vector-icons/Feather';
4
4
  import { useStore } from '@store';
@@ -6,6 +6,7 @@ import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/util
6
6
  import DefaultColumnFilter from '../filters/DefaultColumnFilter';
7
7
  import { getColumnKey, normalizeText } from '../inputs/defaultInputUtils';
8
8
  import styles from './DefaultTable.styles';
9
+ import {getCompactColumnLabel} from './DefaultTableCompact.helpers';
9
10
  import { shouldIncludeColumn } from './DefaultTable.utils';
10
11
  import {
11
12
  persistTableFiltersPreference,
@@ -13,8 +14,9 @@ import {
13
14
  sanitizeTableFiltersPreference,
14
15
  } from '../../utils/tableVisibleColumnsPreferences';
15
16
  import useDefaultTableTheme from './useDefaultTableTheme';
17
+ import DefaultCompactDialog from './DefaultCompactDialog';
16
18
 
17
- const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
19
+ const DefaultFiltersModal = ({ storeName, visible = false, deferred = false, onClose }) => {
18
20
  const store = useStore(storeName);
19
21
  const columns = (Array.isArray(store?.getters?.columns) ? store.getters.columns : []).filter(
20
22
  column =>
@@ -24,6 +26,9 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
24
26
  );
25
27
  const filters = store?.getters?.filters || {};
26
28
  const configs = store?.getters?.configs || {};
29
+ const [draft, setDraft] = useState(filters);
30
+ useEffect(() => {if (visible) setDraft({...filters});}, [visible]);
31
+ const activeFilters = deferred ? draft : filters;
27
32
  const tablePreferenceScope =
28
33
  configs.tablePreferenceScope ||
29
34
  resolveDefaultTablePreferenceScope({ storeName });
@@ -35,6 +40,10 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
35
40
 
36
41
  persistTableFiltersPreference(tablePreferenceScope, persistedFilters);
37
42
 
43
+ if (deferred && typeof configs.onFilterChange === 'function') {
44
+ configs.onFilterChange(nextFilters);
45
+ return;
46
+ }
38
47
  if (typeof store?.actions?.setFilters === 'function') {
39
48
  store.actions.setFilters(nextFilters);
40
49
  } else if (store?.getters) {
@@ -44,7 +53,7 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
44
53
  configs.onFilterChange?.(nextFilters);
45
54
  };
46
55
  const updateFilter = (fieldName, value) => {
47
- const nextFilters = { ...(filters || {}) };
56
+ const nextFilters = { ...(activeFilters || {}) };
48
57
  const isEmpty =
49
58
  value === null ||
50
59
  value === undefined ||
@@ -54,12 +63,13 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
54
63
  if (isEmpty) delete nextFilters[fieldName];
55
64
  else nextFilters[fieldName] = value;
56
65
 
57
- applyFilters(nextFilters);
66
+ if (deferred) setDraft(nextFilters);
67
+ else applyFilters(nextFilters);
58
68
  };
59
- const applyLabel = global.t?.t(storeName, 'button', 'apply');
60
- const clearLabel = global.t?.t(storeName, 'button', 'clear');
61
- const title = global.t?.t(storeName, 'label', 'filters');
62
- const { modalColors, resolvedAccentColor } = useDefaultTableTheme();
69
+ const applyLabel = deferred ? 'Aplicar filtros' : global.t?.t(storeName, 'button', 'apply');
70
+ const clearLabel = deferred ? 'Limpar' : global.t?.t(storeName, 'button', 'clear');
71
+ const title = deferred ? 'Filtros' : global.t?.t(storeName, 'label', 'filters');
72
+ const { modalColors, resolvedAccentColor, themeColors } = useDefaultTableTheme();
63
73
  if (!visible) return null;
64
74
 
65
75
  const {
@@ -71,6 +81,21 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
71
81
  textColor,
72
82
  } = modalColors;
73
83
 
84
+ if (configs.appearance === 'compact') {
85
+ const fields = columns.filter(column => column.compactFilter !== false);
86
+ const action = (label, onPress, primary = false) => <TouchableOpacity accessibilityRole="button" accessibilityLabel={label} onPress={onPress}
87
+ style={{minHeight: 44, justifyContent: 'center', paddingHorizontal: 18, borderRadius: 9, borderWidth: 1, borderColor, backgroundColor: primary ? themeColors.buttonBackground : themeColors.modalBackground}}>
88
+ <Text style={{color: primary ? themeColors.buttonText : textColor, fontSize: 14, fontWeight: '600'}}>{label}</Text>
89
+ </TouchableOpacity>;
90
+ return <DefaultCompactDialog visible title="Filtros" description="Combine os filtros e aplique para atualizar os resultados." onClose={onClose}
91
+ bodyHeight={fields.length * 86 + 40} footer={<>{action(clearLabel, () => setDraft({}))}{action(applyLabel, () => {applyFilters(draft); onClose?.();}, true)}</>}>
92
+ {fields.map(column => <View key={getColumnKey(column)} style={{gap: 8}}>
93
+ <Text style={{fontSize: 14, fontWeight: '600', color: textColor}}>{getCompactColumnLabel(column, formatStoreColumnLabel({columns, fieldName: getColumnKey(column), fallbackLabel: column.label || getColumnKey(column), storeName}))}</Text>
94
+ <DefaultColumnFilter column={column} filters={activeFilters} onChange={updateFilter} storeName={storeName} />
95
+ </View>)}
96
+ </DefaultCompactDialog>;
97
+ }
98
+
74
99
  return (
75
100
  <Modal visible transparent animationType="fade" onRequestClose={onClose}>
76
101
  <View style={[styles.modalOverlay, { backgroundColor: overlayColor }]}>
@@ -80,6 +105,8 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
80
105
  {title}
81
106
  </Text>
82
107
  <TouchableOpacity
108
+ accessibilityRole="button"
109
+ accessibilityLabel="Fechar filtros"
83
110
  style={[styles.modalCloseButton, { borderColor, backgroundColor }]}
84
111
  activeOpacity={0.82}
85
112
  onPress={onClose}
@@ -90,12 +117,13 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
90
117
  <ScrollView style={styles.filtersModalBody} contentContainerStyle={styles.filtersModalList}>
91
118
  {columns.map(column => {
92
119
  const fieldName = getColumnKey(column);
93
- const label = formatStoreColumnLabel({
120
+ const defaultLabel = formatStoreColumnLabel({
94
121
  columns,
95
122
  fieldName,
96
123
  fallbackLabel: column?.label || fieldName,
97
124
  storeName,
98
125
  });
126
+ const label = configs.appearance === 'compact' ? getCompactColumnLabel(column, defaultLabel) : defaultLabel;
99
127
 
100
128
  return (
101
129
  <View key={fieldName} style={styles.filtersModalField}>
@@ -104,7 +132,7 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
104
132
  </Text>
105
133
  <DefaultColumnFilter
106
134
  column={column}
107
- filters={filters}
135
+ filters={activeFilters}
108
136
  onChange={updateFilter}
109
137
  storeName={storeName}
110
138
  style={styles.filtersModalInput}
@@ -115,18 +143,22 @@ const DefaultFiltersModal = ({ storeName, visible = false, onClose }) => {
115
143
  </ScrollView>
116
144
  <View style={[styles.modalActions, { borderTopColor: borderColor }]}>
117
145
  <TouchableOpacity
146
+ accessibilityRole="button"
147
+ accessibilityLabel={clearLabel}
118
148
  style={[styles.secondaryButton, { borderColor }]}
119
149
  activeOpacity={0.82}
120
- onPress={() => applyFilters({})}
150
+ onPress={() => deferred ? setDraft({}) : applyFilters({})}
121
151
  >
122
152
  <Text style={[styles.secondaryButtonText, { color: textColor }]}>
123
153
  {clearLabel}
124
154
  </Text>
125
155
  </TouchableOpacity>
126
156
  <TouchableOpacity
157
+ accessibilityRole="button"
158
+ accessibilityLabel={applyLabel}
127
159
  style={[styles.primaryButton, { backgroundColor: resolvedAccentColor }]}
128
160
  activeOpacity={0.82}
129
- onPress={onClose}
161
+ onPress={() => {if (deferred) applyFilters(draft); onClose?.();}}
130
162
  >
131
163
  <Text style={styles.primaryButtonText}>
132
164
  {applyLabel}
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
2
  import { useWindowDimensions } from 'react-native';
3
3
  import { NavigationRouteContext, useIsFocused } from '@react-navigation/native';
4
4
  import { useStore } from '@store';
@@ -16,6 +16,7 @@ import {
16
16
  } from './useDefaultTableSorting';
17
17
  import useDefaultTableTheme from './useDefaultTableTheme';
18
18
  import DefaultTableView from './DefaultTableView';
19
+ import useDefaultTableCreateForm from './useDefaultTableCreateForm';
19
20
 
20
21
  import {
21
22
  assignGetterValue,
@@ -63,6 +64,8 @@ const DefaultTable = ({
63
64
  onSelectionChange = null,
64
65
  onSortChange = null,
65
66
  pageSize = null,
67
+ appearance = 'default',
68
+ paginationMode = 'infinite',
66
69
  pinRowActions = true,
67
70
  renderCard = null,
68
71
  requestParams = {},
@@ -86,7 +89,6 @@ const DefaultTable = ({
86
89
  toolbarActions = [],
87
90
  visibleColumnsPreferenceKey = '',
88
91
  }) => {
89
- const [isCreateFormOpen, setIsCreateFormOpen] = useState(false);
90
92
  const { width } = useWindowDimensions();
91
93
  const route = React.useContext(NavigationRouteContext);
92
94
  const store = useStore(storeName);
@@ -94,7 +96,7 @@ const DefaultTable = ({
94
96
  const isFocused = useIsFocused();
95
97
  const storeDeclaredConfigsRef = useRef(null);
96
98
  const { showError } = useMessage() || {};
97
- const { tableBorderColors, themeColors } = useDefaultTableTheme(accentColor);
99
+ const { tableBorderColors, themeColors } = useDefaultTableTheme(accentColor, appearance);
98
100
  const tablePanelBorderColor = tableBorderColors.containerBorderColor;
99
101
  const floatingAddBackgroundColor =
100
102
  themeColors.buttonBackground || themeColors.primary || accentColor;
@@ -165,6 +167,9 @@ const DefaultTable = ({
165
167
  const {
166
168
  buildRequestQuery,
167
169
  currentPage,
170
+ pageSizeNumber,
171
+ goToPage,
172
+ resolvedIsLoading,
168
173
  handleEndReached,
169
174
  handleRefresh,
170
175
  resolvedData,
@@ -180,6 +185,7 @@ const DefaultTable = ({
180
185
  onEndReached,
181
186
  onRefresh,
182
187
  pageSize,
188
+ paginationMode,
183
189
  requestParams: requestParamsSeed,
184
190
  resolvedActions,
185
191
  resolvedSort,
@@ -194,23 +200,10 @@ const DefaultTable = ({
194
200
  storeName,
195
201
  tableColumns: columnsForTable,
196
202
  });
197
- const closeCreateForm = useCallback(() => setIsCreateFormOpen(false), []);
198
- const canUseDefaultCreateForm = typeof resolvedActions?.save === 'function';
199
- const handleAdd = useCallback(() => {
200
- if (typeof onAdd === 'function') {
201
- return onAdd();
202
- }
203
- if (!canUseDefaultCreateForm) {
204
- return null;
205
- }
206
- setIsCreateFormOpen(true);
207
- return null;
208
- }, [canUseDefaultCreateForm, onAdd]);
209
- const resolvedOnAdd = typeof onAdd === 'function' || canUseDefaultCreateForm
210
- ? handleAdd
211
- : null;
203
+ const {isCreateFormOpen, closeCreateForm, resolvedOnAdd, handleDefaultCreateSaved} =
204
+ useDefaultTableCreateForm({onAdd, onSaved, resolvedActions, handleRefresh});
212
205
  const hasAddAction =
213
- (addConfig === true || add === true) &&
206
+ add !== false && (addConfig === true || add === true) &&
214
207
  typeof resolvedOnAdd === 'function';
215
208
  const shouldRenderFloatingAddButton =
216
209
  hasAddAction &&
@@ -219,14 +212,6 @@ const DefaultTable = ({
219
212
  hasAddAction &&
220
213
  showToolbar !== false &&
221
214
  normalizedAddButtonPlacement === 'bottom';
222
- const handleDefaultCreateSaved = useCallback(
223
- savedItem => {
224
- closeCreateForm();
225
- onSaved?.(savedItem, null);
226
- handleRefresh?.();
227
- },
228
- [closeCreateForm, handleRefresh, onSaved],
229
- );
230
215
  const debugFallbackParameters = useMemo(() => {
231
216
  if (autoMode) {
232
217
  return buildRequestQuery(currentPage || 1, false);
@@ -237,7 +222,11 @@ const DefaultTable = ({
237
222
  sort: resolvedSort || null,
238
223
  };
239
224
  }, [autoMode, buildRequestQuery, currentPage, requestParamsSeed, resolvedSort, storeFilters]);
225
+ const previousCompactRef = useRef(isCompactView);
226
+ const enteringCompact = isCompactView && !previousCompactRef.current;
227
+ useEffect(() => {previousCompactRef.current = isCompactView;}, [isCompactView]);
240
228
  const effectiveViewMode =
229
+ enteringCompact && forceCardsOnCompact !== false ? 'cards' :
241
230
  currentConfigs.viewMode ||
242
231
  storedViewMode ||
243
232
  (isCompactView && forceCardsOnCompact !== false ? 'cards' : initialViewMode);
@@ -245,6 +234,12 @@ const DefaultTable = ({
245
234
  const defaultTableConfigs = useMemo(
246
235
  () => ({
247
236
  ...storeDeclaredConfigs,
237
+ appearance,
238
+ paginationMode,
239
+ currentPage,
240
+ pageSizeNumber,
241
+ onPageChange: goToPage,
242
+ paginationLoading: resolvedIsLoading,
248
243
  add,
249
244
  addButtonPlacement: normalizedAddButtonPlacement,
250
245
  addLabel: resolvedAddLabel,
@@ -311,6 +306,7 @@ const DefaultTable = ({
311
306
  viewMode: effectiveViewMode,
312
307
  }),
313
308
  [
309
+ appearance, paginationMode, currentPage, pageSizeNumber, goToPage, resolvedIsLoading,
314
310
  add,
315
311
  normalizedAddButtonPlacement,
316
312
  resolvedAddLabel,
@@ -373,6 +369,7 @@ const DefaultTable = ({
373
369
  const defaultTableConfigsSignature = useMemo(
374
370
  () =>
375
371
  stableSerialize({
372
+ appearance, paginationMode, currentPage, pageSizeNumber, resolvedIsLoading,
376
373
  configuredImport: storeDeclaredConfigs.import || null,
377
374
  add,
378
375
  addButtonPlacement: normalizedAddButtonPlacement,
@@ -408,6 +405,7 @@ const DefaultTable = ({
408
405
  toolbarActionsLength: Array.isArray(toolbarActions) ? toolbarActions.length : 0,
409
406
  }),
410
407
  [
408
+ appearance, paginationMode, currentPage, pageSizeNumber, goToPage, resolvedIsLoading,
411
409
  add,
412
410
  normalizedAddButtonPlacement,
413
411
  resolvedAddLabel,
@@ -452,6 +450,8 @@ const DefaultTable = ({
452
450
  // opening Adicionar on DefaultTable (categories). Sync only in the hook
453
451
  // when defaultTableConfigsSignature changes.
454
452
  useDefaultTableStoreSync({
453
+ filters,
454
+ onFilterChange,
455
455
  columns,
456
456
  columnsForTable,
457
457
  data,
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import { FlatList, Text, View } from 'react-native';
1
+ import React, {useState} from 'react';
2
+ import { FlatList, Text, View, useWindowDimensions } from 'react-native';
3
3
  import { useStore } from '@store';
4
4
  import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
5
5
  import { getColumnKey, resolveCellText } from '../inputs/defaultInputUtils';
@@ -16,12 +16,25 @@ import {
16
16
  shouldIncludeColumn,
17
17
  } from './DefaultTable.utils';
18
18
  import styles from './DefaultTable.styles';
19
+ import compactStyles from './DefaultTableCompact.styles';
20
+ import useDefaultTableRowInteraction from './useDefaultTableRowInteraction';
19
21
  import useDefaultTableTheme from './useDefaultTableTheme';
20
22
 
21
23
  // Vertical cards must keep their intrinsic height; zero basis is only for grid rows.
22
24
  const intrinsicHeight = {flexBasis: 'auto', flexGrow: 0, flexShrink: 0};
23
25
 
26
+ /** Responsive card grid (OrderHistory / compact lists). Explicit cardListProps.numColumns wins. */
27
+ export const resolveResponsiveCardColumns = width => {
28
+ const w = Number(width) || 0;
29
+ if (w >= 1280) return 3;
30
+ if (w >= 800) return 2;
31
+ return 1;
32
+ };
33
+
34
+
24
35
  const DefaultTableCards = ({ storeName }) => {
36
+ const [listWidth, setListWidth] = useState(0);
37
+ const {canOpenRow, setRowInteraction} = useDefaultTableRowInteraction();
25
38
  const store = useStore(storeName);
26
39
  const configs = store?.getters?.configs || {};
27
40
  const { palette } = useDefaultTableTheme();
@@ -38,10 +51,21 @@ const DefaultTableCards = ({ storeName }) => {
38
51
  configs.cardListProps && typeof configs.cardListProps === 'object'
39
52
  ? configs.cardListProps
40
53
  : {};
54
+ const compact = configs.appearance === 'compact';
55
+ const {width: windowWidth} = useWindowDimensions();
56
+ const explicitNumColumns = Number(cardListProps.numColumns);
57
+ const hasExplicitNumColumns = Number.isFinite(explicitNumColumns) && explicitNumColumns > 0;
58
+ // Default responsive grid for all card tables (same breakpoints as OrderHistory).
59
+ // Callers may still force a fixed column count via cardListProps.numColumns.
60
+ const gridColumns = hasExplicitNumColumns
61
+ ? explicitNumColumns
62
+ : resolveResponsiveCardColumns(windowWidth);
63
+ const compactCardWidth = listWidth ? Math.max(0, (listWidth - 24 - 12 * (gridColumns - 1)) / gridColumns) : undefined;
41
64
  const {
42
65
  contentContainerStyle,
43
66
  key: cardListKey,
44
67
  listKey,
68
+ numColumns: _ignoredNumColumns,
45
69
  ...flatListProps
46
70
  } = cardListProps;
47
71
  const isLoading = Boolean(store?.getters?.isLoadingList || store?.getters?.isLoading);
@@ -67,7 +91,7 @@ const DefaultTableCards = ({ storeName }) => {
67
91
  const renderField = (fieldName, options = {}) => (
68
92
  <DefaultTableInput
69
93
  fieldName={fieldName}
70
- options={options}
94
+ options={{...options, onInteractionChange: blocked => {options.onInteractionChange?.(blocked); setRowInteraction(row, {key: fieldName}, blocked);}}}
71
95
  row={row}
72
96
  storeName={storeName}
73
97
  variant="card"
@@ -89,10 +113,10 @@ const DefaultTableCards = ({ storeName }) => {
89
113
  component={RowActionsComponent}
90
114
  helpers={{
91
115
  openEdit: () => configs.onEditRow?.(row),
92
- openRow: hasRowPress ? () => configs.onRowPress(row) : null,
116
+ openRow: hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null,
93
117
  }}
94
118
  openEdit={() => configs.onEditRow?.(row)}
95
- openRow={hasRowPress ? () => configs.onRowPress(row) : null}
119
+ openRow={hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null}
96
120
  row={row}
97
121
  storeName={storeName}
98
122
  />
@@ -109,23 +133,24 @@ const DefaultTableCards = ({ storeName }) => {
109
133
  key={row?.['@id'] || row?.id}
110
134
  style={[
111
135
  styles.cardItem,
112
- (flatListProps.numColumns || 1) === 1 ? intrinsicHeight : null,
136
+ gridColumns === 1 ? intrinsicHeight : null,
113
137
  hasRowActions ? styles.cardItemWithActions : null,
114
138
  rowStyleValue,
139
+ compact ? [compactStyles.cardItem, {flex: 0, flexGrow: 0, flexShrink: 0, flexBasis: 'auto', width: compactCardWidth, maxWidth: compactCardWidth, backgroundColor: palette.cardBackground || palette.panelBackground, borderColor: palette.cardBorder || palette.inputBorder}] : null,
115
140
  ]}
116
141
  >
117
- <View style={[styles.cardContent, hasRowActions ? null : intrinsicHeight]}>
142
+ <View style={[styles.cardContent, hasRowActions ? null : intrinsicHeight, compact ? {flex: 0, flexBasis: 'auto', flexGrow: 0} : null]}>
118
143
  {configs.renderCard({
119
144
  item: row,
120
145
  openEdit: () => configs.onEditRow?.(row),
121
- openRow: hasRowPress ? () => configs.onRowPress(row) : null,
146
+ openRow: hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null,
122
147
  renderField,
123
148
  renderValue,
124
149
  row,
125
150
  })}
126
151
  </View>
127
152
  {hasRowActions ? (
128
- <View style={styles.cardActions}>
153
+ <View style={compact ? [compactStyles.cardActions, {backgroundColor: palette.tableFooterBackground, borderTopColor: palette.border}] : styles.cardActions}>
129
154
  {customRowActions}
130
155
  {editButton}
131
156
  </View>
@@ -173,10 +198,18 @@ const DefaultTableCards = ({ storeName }) => {
173
198
  );
174
199
  };
175
200
 
201
+ // RN FlatList throws if numColumns changes without remount. Always derive key from numColumns
202
+ // and apply it AFTER the spread so callers cannot override it with a stale key.
203
+ const resolvedNumColumns = gridColumns;
204
+ const resolvedListKey = listKey || cardListKey || `cards-${resolvedNumColumns}`;
205
+
176
206
  return (
177
207
  <FlatList
178
- key={listKey || cardListKey || `cards-${flatListProps.numColumns || 1}`}
179
208
  {...flatListProps}
209
+ key={resolvedListKey}
210
+ numColumns={resolvedNumColumns}
211
+ onLayout={event => {flatListProps.onLayout?.(event); setListWidth(event.nativeEvent.layout.width);}}
212
+ columnWrapperStyle={compact && gridColumns > 1 ? {gap: 12} : flatListProps.columnWrapperStyle}
180
213
  data={sortedData}
181
214
  keyExtractor={getRowKey}
182
215
  renderItem={({ item, index }) => renderCardItem(item, index)}
@@ -184,6 +217,7 @@ const DefaultTableCards = ({ storeName }) => {
184
217
  contentContainerStyle={[
185
218
  styles.cardsGrid,
186
219
  contentContainerStyle,
220
+ compact ? {padding: 12, gap: 12} : null,
187
221
  hasBottomAddButton ? styles.cardsGridWithBottomAdd : null,
188
222
  ]}
189
223
  ListEmptyComponent={(
@@ -0,0 +1,26 @@
1
+ export const isActiveTableFilter = value => {
2
+ if (value === null || value === undefined || value === '') return false;
3
+ if (Array.isArray(value)) return value.length > 0;
4
+ if (typeof value === 'object') {
5
+ if (value.shortcut) return value.shortcut !== 'all';
6
+ if ('value' in value) return isActiveTableFilter(value.value);
7
+ return Object.values(value).some(isActiveTableFilter);
8
+ }
9
+ return true;
10
+ };
11
+
12
+ export const getCompactFilterLabel = value => {
13
+ if (Array.isArray(value)) return value.map(getCompactFilterLabel).join(', ');
14
+ if (value && typeof value === 'object') {
15
+ if (value.shortcut) return ({today: 'Hoje', yesterday: 'Ontem', all: 'Todos', custom: 'Personalizado'})[value.shortcut] || value.shortcut;
16
+ return String(value.label || value.value || 'Selecionado');
17
+ }
18
+ return String(value ?? '');
19
+ };
20
+
21
+ export const getVisiblePageNumbers = (page, totalPages) => {
22
+ const start = Math.max(1, Math.min(page - 1, totalPages - 2));
23
+ return Array.from({length: Math.min(3, totalPages)}, (_, index) => start + index);
24
+ };
25
+
26
+ export const getCompactColumnLabel = (column, fallback) => column?.compactLabel || fallback;
@@ -0,0 +1,28 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ toolbar: {padding: 12, gap: 8, borderBottomWidth: 1, flexShrink: 0},
5
+ row: {flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 8},
6
+ search: {flex: 1, maxWidth: 620, minWidth: 210, flexDirection: 'row', alignItems: 'center', gap: 8, borderWidth: 1, borderRadius: 8, paddingHorizontal: 10},
7
+ searchMobile: {flexBasis: '100%', maxWidth: '100%', minWidth: 0},
8
+ input: {flex: 1, minWidth: 0, height: 42, fontSize: 14},
9
+ inputMobile: {fontSize: 16},
10
+ button: {minHeight: 44, minWidth: 44, paddingHorizontal: 10, borderWidth: 1, borderRadius: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6},
11
+ label: {fontSize: 12, fontWeight: '600'},
12
+ small: {fontSize: 12},
13
+ chips: {flexDirection: 'row', flexWrap: 'wrap', gap: 6, alignItems: 'center'},
14
+ chip: {minHeight: 30, maxWidth: '100%', borderWidth: 1, borderRadius: 18, paddingHorizontal: 9, flexDirection: 'row', alignItems: 'center', gap: 6},
15
+ secondary: {flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8},
16
+ mode: {flexDirection: 'row', gap: 3},
17
+ backdrop: {flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16},
18
+ sheet: {width: '100%', maxWidth: 480, maxHeight: '85%', borderWidth: 1, borderRadius: 14, padding: 16, gap: 12},
19
+ sheetHeader: {flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10},
20
+ sheetTitle: {fontSize: 17, fontWeight: '600'},
21
+ option: {minHeight: 44, paddingVertical: 10, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8},
22
+ footer: {padding: 10, flexShrink: 0, borderTopWidth: 1, flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8},
23
+ pager: {flexDirection: 'row', flexWrap: 'wrap', gap: 4, alignItems: 'center'},
24
+ page: {minWidth: 44, minHeight: 44, borderWidth: 1, borderRadius: 7, justifyContent: 'center', alignItems: 'center'},
25
+ disabled: {opacity: 0.45},
26
+ cardItem: {flexDirection: 'column', alignItems: 'stretch', borderWidth: 1, borderRadius: 12, overflow: 'hidden', margin: 0},
27
+ cardActions: {alignSelf: 'stretch', flexDirection: 'row', justifyContent: 'flex-end', borderTopWidth: 1, paddingHorizontal: 12, paddingVertical: 8},
28
+ });
@@ -0,0 +1,112 @@
1
+ import React, {useEffect, useState} from 'react';
2
+ import {Text, TextInput, TouchableOpacity, View, useWindowDimensions} from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import {useStore} from '@store';
5
+ import {formatStoreColumnLabel} from '@controleonline/ui-common/src/react/utils/storeColumns';
6
+ import {getColumnKey} from '../inputs/defaultInputUtils';
7
+ import {getSortField, isSortableColumn} from './DefaultTable.utils';
8
+ import {persistTableFiltersPreference, persistTableViewModePreference} from '../../utils/tableVisibleColumnsPreferences';
9
+ import DefaultCompactDialog from './DefaultCompactDialog';
10
+ import DefaultFiltersModal from './DefaultFiltersModal';
11
+ import DefaultColumnMenu from './DefaultColumnMenu';
12
+ import DefaultDebug from './DefaultDebug';
13
+ import DefaultToolbarAction from './DefaultToolbarAction';
14
+ import useDefaultTableTheme from './useDefaultTableTheme';
15
+ import {getCompactColumnLabel, getCompactFilterLabel, isActiveTableFilter} from './DefaultTableCompact.helpers';
16
+ import s from './DefaultTableCompact.styles';
17
+
18
+ export default function DefaultTableCompactToolbar({storeName, actions = [], showControls = true}) {
19
+ const store = useStore(storeName);
20
+ const configs = store?.getters?.configs || {};
21
+ const filters = store?.getters?.filters || {};
22
+ const columns = store?.getters?.columns || [];
23
+ const {themeColors: c} = useDefaultTableTheme();
24
+ const {width} = useWindowDimensions();
25
+ const mobile = width <= (configs.compactBreakpoint || 768);
26
+ const [modal, setModal] = useState('');
27
+ const searchKey = configs.searchKey || 'search';
28
+ const [draft, setDraft] = useState(String(filters[searchKey] || ''));
29
+ useEffect(() => setDraft(String(filters[searchKey] || '')), [filters[searchKey]]);
30
+ const entries = Object.entries(filters).filter(([, value]) => isActiveTableFilter(value));
31
+ const text = c.textPrimary || c.text;
32
+ const muted = c.textSecondary || text;
33
+ const panel = c.panelBackground || c.cardBackground || c.appBackground;
34
+ const border = c.inputBorder || c.cardBorder || c.border;
35
+ const apply = next => {
36
+ persistTableFiltersPreference(configs.tablePreferenceScope, next);
37
+ if (configs.onFilterChange) configs.onFilterChange(next);
38
+ else store.actions?.setFilters?.(next);
39
+ };
40
+ const remove = key => {const next = {...filters}; delete next[key]; apply(next);};
41
+ const submit = () => {const next = {...filters}; if (draft.trim()) next[searchKey] = draft.trim(); else delete next[searchKey]; apply(next);};
42
+ const button = (label, icon, onPress, selected = false, primary = false) => (
43
+ <TouchableOpacity accessibilityRole="button" accessibilityLabel={label} accessibilityState={{selected}} onPress={onPress}
44
+ style={[s.button, {backgroundColor: primary ? c.buttonBackground : selected ? c.chipSelectedBackground || panel : panel, borderColor: border}, !mobile && icon === 'sliders' ? {marginLeft: 'auto'} : null]}>
45
+ <Icon name={icon} size={16} color={primary ? c.buttonIcon || c.buttonText : text} />
46
+ {label && (!mobile || width >= 360 && (label === 'Filtros' || primary)) ? <Text style={[s.label, {color: primary ? c.buttonText : text}]}>{primary && mobile ? 'Novo' : label}</Text> : null}
47
+ </TouchableOpacity>
48
+ );
49
+ const mode = configs.effectiveViewMode || 'table';
50
+ const setMode = value => {
51
+ persistTableViewModePreference(configs.tablePreferenceScope, value);
52
+ store.actions?.setConfigs?.({...configs, effectiveViewMode: value, viewMode: value});
53
+ };
54
+ const canAdd = configs.add !== false && (configs.add === true || store.getters?.add === true) && configs.onAdd;
55
+ const buttonLabel = configs.addLabel || 'Novo pedido';
56
+ const columnButton = () => button('Colunas', 'columns', () => setModal('columns'));
57
+ const addButton = () => button(buttonLabel, 'plus', configs.onAdd, false, true);
58
+ const sortColumn = columns.find(column => getSortField(column) === configs.resolvedSort?.field);
59
+ const labelFor = column => getCompactColumnLabel(column, formatStoreColumnLabel({columns, fieldName: getColumnKey(column), fallbackLabel: column?.label || getColumnKey(column), storeName}));
60
+ return (
61
+ <View style={[s.toolbar, {backgroundColor: c.toolbarBackground || panel, borderBottomColor: border}]}>
62
+ <View style={[s.row, {gap: mobile ? 6 : 8}]}>
63
+ {configs.showSearch !== false ? <View style={[s.search, mobile && s.searchMobile, {borderColor: border, backgroundColor: c.inputBackground || panel}]}>
64
+ <Icon name="search" size={16} color={c.inputIcon || muted} />
65
+ <TextInput accessibilityLabel={configs.searchPlaceholder || 'Buscar'} placeholder={configs.searchPlaceholder || 'Buscar'} placeholderTextColor={c.inputPlaceholderText || muted}
66
+ value={draft} onChangeText={setDraft} onSubmitEditing={submit} returnKeyType="search" style={[s.input, mobile && s.inputMobile, {color: c.inputText || text}]} />
67
+ <TouchableOpacity accessibilityRole="button" accessibilityLabel="Pesquisar" onPress={submit} style={{padding: 8}}><Icon name="arrow-right" size={16} color={text} /></TouchableOpacity>
68
+ </View> : null}
69
+ {showControls ? <>
70
+ {button('Filtros', 'sliders', () => setModal('filters'))}
71
+ <View style={s.mode}>{button('Tabela', 'list', () => setMode('table'), mode === 'table')}{button('Cards', 'grid', () => setMode('cards'), mode === 'cards')}</View>
72
+ {!mobile ? columnButton() : null}
73
+ {button('Mais opções', 'more-horizontal', () => setModal('more'))}
74
+ {canAdd ? addButton() : null}
75
+ </> : null}
76
+ </View>
77
+ {entries.length ? <View style={s.chips}>{entries.map(([key, value]) => {
78
+ const column = columns.find(item => getColumnKey(item) === key);
79
+ const options = column ? configs.getOptionsForColumn?.(column) || column.options || [] : [];
80
+ const valueLabel = Array.isArray(options) ? (Array.isArray(value) ? value : [value]).map(item => options.find(option => String(option.value ?? option.key) === String(item))?.label || getCompactFilterLabel(item)).join(', ') : getCompactFilterLabel(value);
81
+ const label = `${column ? labelFor(column) + ': ' : key === searchKey ? 'Busca: ' : key + ': '}${valueLabel}`;
82
+ return <TouchableOpacity key={key} accessibilityRole="button" accessibilityLabel={`Remover filtro ${label}`} onPress={() => remove(key)} style={[s.chip, {borderColor: border, backgroundColor: c.chipBackground || panel}]}>
83
+ <Text numberOfLines={1} style={[s.small, {color: c.chipText || text, flexShrink: 1}]}>{label}</Text><Icon name="x" size={12} color={text} />
84
+ </TouchableOpacity>;
85
+ })}<TouchableOpacity accessibilityRole="button" onPress={() => apply({})}><Text style={[s.small, {color: muted}]}>Limpar filtros</Text></TouchableOpacity></View> : null}
86
+ <View style={s.secondary}>
87
+ <Text style={[s.small, {color: muted}]}>{configs.showTotalItemsInFooter !== false ? `${store.getters?.totalItems ?? 0} ${Number(store.getters?.totalItems) === 1 ? 'resultado' : 'resultados'}` : 'Pedidos'}</Text>
88
+ {showControls ? <TouchableOpacity accessibilityRole="button" accessibilityLabel="Escolher ordenação" onPress={() => setModal('sort')} style={s.row}>
89
+ <Icon name="arrow-down" size={14} color={muted} /><Text style={[s.small, {color: text}]}>{sortColumn ? labelFor(sortColumn) : 'Ordenar'} · {configs.resolvedSort?.direction === 'asc' ? '↑' : '↓'}</Text>
90
+ </TouchableOpacity> : null}
91
+ </View>
92
+ <DefaultFiltersModal storeName={storeName} visible={modal === 'filters'} deferred onClose={() => setModal('')} />
93
+ <DefaultColumnMenu storeName={storeName} visible={modal === 'columns'} onClose={() => setModal('')} />
94
+ <DefaultCompactDialog visible={modal === 'more' || modal === 'sort'} title={modal === 'sort' ? 'Ordenação' : 'Mais opções'}
95
+ description={modal === 'sort' ? 'Escolha a coluna. Toque novamente para inverter a ordem.' : undefined}
96
+ onClose={() => setModal('')} bodyHeight={modal === 'sort' ? columns.filter(isSortableColumn).length * 54 + 32 : 180}>
97
+ {modal === 'sort' ? columns.filter(isSortableColumn).map(column => <TouchableOpacity key={getColumnKey(column)} accessibilityRole="button"
98
+ accessibilityLabel={`Ordenar por ${labelFor(column)}`} style={[s.option, {borderBottomWidth: 1, borderBottomColor: c.tableRowBorder}]}
99
+ onPress={() => {configs.requestSort?.(column); setModal('');}}>
100
+ <Text style={{color: text, fontSize: 14}}>{labelFor(column)}</Text><Text style={{color: muted}}>{getSortField(column) === configs.resolvedSort?.field ? configs.resolvedSort.direction === 'asc' ? '↑' : '↓' : '↕'}</Text>
101
+ </TouchableOpacity>) : <View style={{gap: 8}}>{mobile ? <DefaultToolbarAction action={{label: 'Colunas visíveis', icon: 'columns', onPress: () => setModal('columns'), color: text,
102
+ style: {width: '100%', minHeight: 48, justifyContent: 'flex-start', paddingHorizontal: 12, borderWidth: 0, backgroundColor: c.inputBackground}, labelStyle: {fontSize: 14}}} /> : null}
103
+ {actions.filter(action => !action.hidden).map(action => <DefaultToolbarAction key={action.key || action.icon}
104
+ action={{...action, label: action.label || action.accessibilityLabel, color: text,
105
+ style: {width: '100%', minHeight: 48, justifyContent: 'flex-start', paddingHorizontal: 12, borderWidth: 0, backgroundColor: c.inputBackground}, labelStyle: {fontSize: 14, fontWeight: '400'},
106
+ onPress: () => {setModal(''); action.onPress?.();}}} />)}
107
+ <DefaultDebug storeName={storeName} />
108
+ </View>}
109
+ </DefaultCompactDialog>
110
+ </View>
111
+ );
112
+ }