@controleonline/ui-default 1.0.277 → 1.0.278

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 +18 -7
  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,4 +1,4 @@
1
- import React from 'react';
1
+ import React, {useState} from 'react';
2
2
  import { FlatList, Text, View } from 'react-native';
3
3
  import { useStore } from '@store';
4
4
  import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
@@ -16,12 +16,16 @@ 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
 
24
26
  const DefaultTableCards = ({ storeName }) => {
27
+ const [listWidth, setListWidth] = useState(0);
28
+ const {canOpenRow, setRowInteraction} = useDefaultTableRowInteraction();
25
29
  const store = useStore(storeName);
26
30
  const configs = store?.getters?.configs || {};
27
31
  const { palette } = useDefaultTableTheme();
@@ -38,6 +42,9 @@ const DefaultTableCards = ({ storeName }) => {
38
42
  configs.cardListProps && typeof configs.cardListProps === 'object'
39
43
  ? configs.cardListProps
40
44
  : {};
45
+ const compact = configs.appearance === 'compact';
46
+ const gridColumns = cardListProps.numColumns || 1;
47
+ const compactCardWidth = listWidth ? Math.max(0, (listWidth - 24 - 12 * (gridColumns - 1)) / gridColumns) : undefined;
41
48
  const {
42
49
  contentContainerStyle,
43
50
  key: cardListKey,
@@ -67,7 +74,7 @@ const DefaultTableCards = ({ storeName }) => {
67
74
  const renderField = (fieldName, options = {}) => (
68
75
  <DefaultTableInput
69
76
  fieldName={fieldName}
70
- options={options}
77
+ options={{...options, onInteractionChange: blocked => {options.onInteractionChange?.(blocked); setRowInteraction(row, {key: fieldName}, blocked);}}}
71
78
  row={row}
72
79
  storeName={storeName}
73
80
  variant="card"
@@ -89,10 +96,10 @@ const DefaultTableCards = ({ storeName }) => {
89
96
  component={RowActionsComponent}
90
97
  helpers={{
91
98
  openEdit: () => configs.onEditRow?.(row),
92
- openRow: hasRowPress ? () => configs.onRowPress(row) : null,
99
+ openRow: hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null,
93
100
  }}
94
101
  openEdit={() => configs.onEditRow?.(row)}
95
- openRow={hasRowPress ? () => configs.onRowPress(row) : null}
102
+ openRow={hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null}
96
103
  row={row}
97
104
  storeName={storeName}
98
105
  />
@@ -112,20 +119,21 @@ const DefaultTableCards = ({ storeName }) => {
112
119
  (flatListProps.numColumns || 1) === 1 ? intrinsicHeight : null,
113
120
  hasRowActions ? styles.cardItemWithActions : null,
114
121
  rowStyleValue,
122
+ 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
123
  ]}
116
124
  >
117
- <View style={[styles.cardContent, hasRowActions ? null : intrinsicHeight]}>
125
+ <View style={[styles.cardContent, hasRowActions ? null : intrinsicHeight, compact ? {flex: 0, flexBasis: 'auto', flexGrow: 0} : null]}>
118
126
  {configs.renderCard({
119
127
  item: row,
120
128
  openEdit: () => configs.onEditRow?.(row),
121
- openRow: hasRowPress ? () => configs.onRowPress(row) : null,
129
+ openRow: hasRowPress ? () => {if (canOpenRow(row)) configs.onRowPress(row);} : null,
122
130
  renderField,
123
131
  renderValue,
124
132
  row,
125
133
  })}
126
134
  </View>
127
135
  {hasRowActions ? (
128
- <View style={styles.cardActions}>
136
+ <View style={compact ? [compactStyles.cardActions, {backgroundColor: palette.tableFooterBackground, borderTopColor: palette.border}] : styles.cardActions}>
129
137
  {customRowActions}
130
138
  {editButton}
131
139
  </View>
@@ -177,6 +185,8 @@ const DefaultTableCards = ({ storeName }) => {
177
185
  <FlatList
178
186
  key={listKey || cardListKey || `cards-${flatListProps.numColumns || 1}`}
179
187
  {...flatListProps}
188
+ onLayout={event => {flatListProps.onLayout?.(event); setListWidth(event.nativeEvent.layout.width);}}
189
+ columnWrapperStyle={compact && gridColumns > 1 ? {gap: 12} : flatListProps.columnWrapperStyle}
180
190
  data={sortedData}
181
191
  keyExtractor={getRowKey}
182
192
  renderItem={({ item, index }) => renderCardItem(item, index)}
@@ -184,6 +194,7 @@ const DefaultTableCards = ({ storeName }) => {
184
194
  contentContainerStyle={[
185
195
  styles.cardsGrid,
186
196
  contentContainerStyle,
197
+ compact ? {padding: 12, gap: 12} : null,
187
198
  hasBottomAddButton ? styles.cardsGridWithBottomAdd : null,
188
199
  ]}
189
200
  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
+ }