@controleonline/ui-default 1.0.272 → 1.0.274

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 (45) hide show
  1. package/.github/workflows/publish-npm.yml +91 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/technical/DefaultTable.md +70 -1
  4. package/package.json +22 -22
  5. package/src/react/components/address/DefaultAddress.js +20 -9
  6. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  7. package/src/react/components/address/LatLonReadonlyFields.js +2 -2
  8. package/src/react/components/address/defaultAddressStyles.js +13 -1
  9. package/src/react/components/files/DefaultFile.js +2 -2
  10. package/src/react/components/files/DefaultFileColumn.js +1 -1
  11. package/src/react/components/filters/DefaultColumnFilter.js +2 -7
  12. package/src/react/components/filters/DefaultExternalFilters.js +4 -11
  13. package/src/react/components/filters/filterValue.js +65 -0
  14. package/src/react/components/help/DefaultTooltip.js +73 -63
  15. package/src/react/components/help/DefaultTooltip.styles.js +23 -23
  16. package/src/react/components/inputs/DefaultInput.js +301 -210
  17. package/src/react/components/inputs/defaultInputUtils.js +193 -170
  18. package/src/react/components/table/DefaultColumnMenu.js +127 -109
  19. package/src/react/components/table/DefaultTable.js +43 -92
  20. package/src/react/components/table/DefaultTable.styles.js +10 -4
  21. package/src/react/components/table/DefaultTable.utils.js +38 -4
  22. package/src/react/components/table/DefaultTableRows.js +324 -153
  23. package/src/react/components/table/DefaultTableToolbar.js +17 -7
  24. package/src/react/components/table/DefaultTableView.js +118 -0
  25. package/src/react/components/upload/DefaultUpload.js +6 -36
  26. package/src/react/components/upload/DefaultUpload.styles.js +5 -5
  27. package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
  28. package/src/react/hooks/usePostalCodeLookup.js +15 -1
  29. package/src/react/services/addressFormUtils.js +104 -38
  30. package/src/react/services/addressGeo.js +6 -3
  31. package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
  32. package/src/tests/react/components/files/DefaultFile.test.js +16 -0
  33. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  34. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  35. package/src/tests/react/components/table/DefaultTable.test.js +37 -0
  36. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  37. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  38. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  39. package/src/tests/react/components/table/DefaultTableToolbar.waiter.test.js +42 -0
  40. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
  41. package/src/tests/react/filters/filterValue.test.js +46 -0
  42. package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
  43. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  44. package/src/tests/react/services/addressFormUtils.test.js +6 -3
  45. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +12 -1
@@ -4,20 +4,13 @@ import Icon from 'react-native-vector-icons/Feather';
4
4
  import { useStore } from '@store';
5
5
  import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
6
6
  import { getColumnKey } from '../inputs/defaultInputUtils';
7
+ import DefaultColumnFilter from '../filters/DefaultColumnFilter';
7
8
  import DefaultTableEmptyState from './DefaultTableEmptyState';
8
9
  import DefaultTableInput from './DefaultTableInput';
9
10
  import DefaultTableRowActions, {
10
11
  hasDefaultTableRowActionsComponent,
11
12
  resolveDefaultTableRowActionsWidth,
12
13
  } from './DefaultTableRowActions';
13
- import {
14
- persistStoreSelection,
15
- flattenGroupedTableItems,
16
- isRowSelected,
17
- resolveStoreSelectedIds,
18
- toggleGroupSelection,
19
- toggleSelectedId,
20
- } from './DefaultTable.grouping';
21
14
  import {
22
15
  END_REACHED_THRESHOLD,
23
16
  getColumnStyle,
@@ -32,17 +25,8 @@ import {
32
25
  import styles from './DefaultTable.styles';
33
26
  import useDefaultTableTheme from './useDefaultTableTheme';
34
27
 
35
- const SELECTION_COLUMN_WIDTH = 44;
36
-
37
- /** Row stripe colors from themes-map listItemEvenRow / listItemOddRow — never status.color. */
38
- export const resolveRowBackgroundColor = ({
39
- selected,
40
- index,
41
- tableOddColor,
42
- tableEvenColor,
43
- selectedBackground = '#ECFEFF',
44
- }) => {
45
- if (selected) return selectedBackground;
28
+ /** Row stripes: themes-map listItemEvenRow / listItemOddRow — never status.color. */
29
+ export const resolveRowBackgroundColor = ({ index, tableOddColor, tableEvenColor }) => {
46
30
  return index % 2 === 0 ? tableOddColor : tableEvenColor;
47
31
  };
48
32
 
@@ -59,179 +43,366 @@ const DefaultTableRows = ({ storeName }) => {
59
43
  liveItems: store?.getters?.items,
60
44
  sortedData: configs.sortedData,
61
45
  });
62
- const listData = useMemo(() => flattenGroupedTableItems(sortedData, columns), [columns, sortedData]);
63
- const selectable = configs.selectable === true;
64
- const selectedIds = resolveStoreSelectedIds(store);
46
+ const resolvedFilters = store?.getters?.filters || {};
65
47
  const isLoading = Boolean(store?.getters?.isLoadingList || store?.getters?.isLoading);
66
- const emptyStateLabel = isLoading ? global.t?.t(storeName, 'label', 'loading') : global.t?.t(storeName, 'label', 'empty');
48
+ const emptyStateLabel = isLoading
49
+ ? global.t?.t(storeName, 'label', 'loading')
50
+ : global.t?.t(storeName, 'label', 'empty');
67
51
  const tableBorderColor = tableBorderColors.rowBorderColor;
68
52
  const tableHeaderBorderColor = tableBorderColors.headerBorderColor || tableBorderColor;
69
53
  const tableEvenColor = themeTokens.listItemEvenRow || themeTokens['bg-even-light'] || palette.background;
70
54
  const tableHeaderColor = themeTokens['bg-headers-light'] || resolvedAccentColor;
71
55
  const tableMutedColor = palette.textSecondary;
72
56
  const tableOddColor = themeTokens.listItemOddRow || themeTokens['bg-odd-light'] || palette.background;
57
+ const tableSurfaceColor = palette.background;
73
58
  const tableTextColor = palette.text;
74
59
  const rowStyle = configs.rowStyle;
75
60
  const hasRowPress = typeof configs.onRowPress === 'function';
76
- const RowActionsComponent = configs.rowActionsComponent;
77
- const hasCustomRowActions = hasDefaultTableRowActionsComponent(RowActionsComponent);
61
+ const hasCustomRowActions = hasDefaultTableRowActionsComponent(configs.rowActionsComponent);
78
62
  const hasEditAction = typeof configs.onEditRow === 'function';
79
- const hasRowActions =
80
- configs.showRowActions !== false && (hasCustomRowActions || hasEditAction);
81
- const pinRowActions = configs.pinRowActions !== false;
63
+ const hasRowActions = configs.showRowActions !== false && (hasCustomRowActions || hasEditAction);
64
+ const shouldPinRowActions = configs.pinRowActions !== false;
65
+ const hasColumnFilters =
66
+ configs.showColumnFiltersButton !== false &&
67
+ configs.tableFiltersVisible === true;
82
68
  const actionsCellWidth = hasRowActions ? resolveDefaultTableRowActionsWidth(configs) : 0;
83
- const selectionWidth = selectable ? SELECTION_COLUMN_WIDTH : 0;
84
69
  const tableMinWidth = useMemo(
85
- () => tableColumns.reduce((totalWidth, column) => totalWidth + getColumnMinWidth(column), actionsCellWidth + selectionWidth),
86
- [actionsCellWidth, selectionWidth, tableColumns],
70
+ () =>
71
+ tableColumns.reduce(
72
+ (totalWidth, column) => totalWidth + getColumnMinWidth(column),
73
+ actionsCellWidth,
74
+ ),
75
+ [actionsCellWidth, tableColumns],
87
76
  );
88
- const [horizontalMetrics, setHorizontalMetrics] = useState({ contentWidth: 0, viewportWidth: 0, x: 0 });
77
+ const [horizontalMetrics, setHorizontalMetrics] = useState({
78
+ contentWidth: 0,
79
+ viewportWidth: 0,
80
+ x: 0,
81
+ });
89
82
  const updateHorizontalMetrics = patch => {
90
83
  setHorizontalMetrics(current => {
91
- const next = { ...current, ...patch };
92
- return next.contentWidth === current.contentWidth && next.viewportWidth === current.viewportWidth && next.x === current.x ? current : next;
84
+ const next = {
85
+ ...current,
86
+ ...patch,
87
+ };
88
+
89
+ return next.contentWidth === current.contentWidth &&
90
+ next.viewportWidth === current.viewportWidth &&
91
+ next.x === current.x
92
+ ? current
93
+ : next;
93
94
  });
94
95
  };
95
- const tableWidth = Math.max(tableMinWidth, Number(horizontalMetrics.viewportWidth || 0));
96
- const emitSelection = nextIds => persistStoreSelection(store, nextIds, sortedData, columns);
97
-
98
- const renderSelectionCell = (selected, onPress, accessibilityLabel, backgroundColor) => (
99
- <TouchableOpacity accessibilityRole="checkbox" accessibilityState={{ checked: selected }} accessibilityLabel={accessibilityLabel} onPress={onPress} style={[styles.cell, { minWidth: SELECTION_COLUMN_WIDTH, width: SELECTION_COLUMN_WIDTH, alignItems: 'center', justifyContent: 'center', backgroundColor }]}>
100
- <Icon name={selected ? 'check-square' : 'square'} size={16} color={selected ? '#0284C7' : tableMutedColor} />
101
- </TouchableOpacity>
102
- );
96
+ const stickyBodyTransforms = useMemo(() => {
97
+ const viewportWidth = Number(horizontalMetrics.viewportWidth || 0);
98
+ const contentWidth = Number(horizontalMetrics.contentWidth || 0);
99
+ const x = Number(horizontalMetrics.x || 0);
100
+ const canScrollHorizontally = viewportWidth > 0 && contentWidth > viewportWidth;
101
+ const actionsTranslateX = canScrollHorizontally
102
+ ? x + viewportWidth - contentWidth
103
+ : 0;
103
104
 
104
- const renderActionsCell = (row, rowBackgroundColor) => {
105
- if (!hasRowActions) return null;
106
-
107
- const customRowActions = hasCustomRowActions ? (
108
- <DefaultTableRowActions
109
- component={RowActionsComponent}
110
- helpers={{
111
- openEdit: () => configs.onEditRow?.(row),
112
- openRow: hasRowPress ? () => configs.onRowPress(row) : null,
113
- }}
114
- openEdit={() => configs.onEditRow?.(row)}
115
- openRow={hasRowPress ? () => configs.onRowPress(row) : null}
116
- row={row}
117
- storeName={storeName}
118
- />
119
- ) : null;
120
-
121
- const editButton = hasEditAction ? (
122
- <Text onPress={() => configs.onEditRow?.(row)} style={{ color: '#0284C7', fontWeight: '800', fontSize: 12 }}>
123
- {global.t?.t(storeName, 'button', 'edit') || 'Editar'}
124
- </Text>
125
- ) : null;
126
-
127
- return (
128
- <View
129
- style={[
130
- styles.cell,
131
- styles.actionsCell,
132
- pinRowActions ? styles.stickyActionsCell : null,
133
- pinRowActions ? styles.pinnedActionsCell : null,
134
- {
135
- minWidth: actionsCellWidth,
136
- width: actionsCellWidth,
137
- maxWidth: actionsCellWidth,
138
- flexBasis: actionsCellWidth,
139
- backgroundColor: rowBackgroundColor,
140
- },
141
- ]}
142
- >
143
- <View style={styles.rowActionsGroup}>
144
- {customRowActions}
145
- {editButton}
146
- </View>
147
- </View>
148
- );
105
+ return {
106
+ actions:
107
+ shouldPinRowActions && actionsTranslateX !== 0
108
+ ? [{ translateX: actionsTranslateX }]
109
+ : null,
110
+ };
111
+ }, [horizontalMetrics, shouldPinRowActions]);
112
+ const handleHorizontalScroll = event => {
113
+ updateHorizontalMetrics({
114
+ x: Number(event?.nativeEvent?.contentOffset?.x || 0),
115
+ });
149
116
  };
150
-
151
- const renderActionsHeader = () => {
152
- if (!hasRowActions) return null;
153
- return (
154
- <View
155
- style={[
156
- styles.cell,
157
- styles.actionsCell,
158
- pinRowActions ? styles.stickyActionsCell : null,
159
- pinRowActions ? styles.stickyHeaderCell : null,
160
- {
161
- minWidth: actionsCellWidth,
162
- width: actionsCellWidth,
163
- maxWidth: actionsCellWidth,
164
- flexBasis: actionsCellWidth,
165
- backgroundColor: tableHeaderColor,
166
- },
167
- ]}
168
- >
169
- <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={1}>
170
- {global.t?.t(storeName, 'label', 'actions') || 'Ações'}
171
- </Text>
172
- </View>
173
- );
117
+ const handleHorizontalLayout = event => {
118
+ updateHorizontalMetrics({
119
+ viewportWidth: Number(event?.nativeEvent?.layout?.width || 0),
120
+ });
174
121
  };
175
-
176
- const renderTableItem = ({ item, index }) => {
177
- if (item?.type === 'group') {
178
- const groupRows = item.group?.rows || [];
179
- const groupSelected = selectable && groupRows.length > 0 && groupRows.every(row => isRowSelected(row, selectedIds));
180
- return (
181
- <View testID={`default-table-group-${item.group?.id}`} style={[styles.row, { backgroundColor: '#ECFEFF', borderBottomColor: tableBorderColor, borderBottomWidth: tableBorderColor ? 1 : 0, minWidth: tableWidth, width: tableWidth, paddingVertical: 8 }]}>
182
- {selectable ? renderSelectionCell(groupSelected, () => emitSelection(toggleGroupSelection(selectedIds, groupRows)), `Selecionar grupo ${item.group?.label || ''}`, '#ECFEFF') : null}
183
- <View style={{ flex: 1, paddingHorizontal: 8 }}>
184
- <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={2}>{item.group?.label}</Text>
185
- <Text style={{ color: tableMutedColor, fontSize: 12 }}>{item.group?.count} item(s)</Text>
186
- </View>
187
- {hasRowActions ? <View style={{ minWidth: actionsCellWidth, width: actionsCellWidth }} /> : null}
188
- </View>
189
- );
122
+ const handleHorizontalContentSizeChange = width => {
123
+ updateHorizontalMetrics({
124
+ contentWidth: Number(width || 0),
125
+ });
126
+ };
127
+ const handleListScroll = event => {
128
+ if (shouldTriggerEndReachedFromScroll(event)) {
129
+ configs.onEndReached?.();
190
130
  }
131
+ };
132
+ const tableWidth = Math.max(
133
+ tableMinWidth,
134
+ Number(horizontalMetrics.viewportWidth || 0),
135
+ );
191
136
 
192
- const row = item?.row || item;
193
- const RowComponent = hasRowPress || selectable ? TouchableOpacity : View;
194
- const selected = selectable && isRowSelected(row, selectedIds);
137
+ const renderTableItem = ({ item: row, index }) => {
138
+ const RowComponent = hasRowPress ? TouchableOpacity : View;
139
+ const rowStyleValue = typeof rowStyle === 'function' ? rowStyle(row, index) : rowStyle;
140
+ const rowPressProps = hasRowPress
141
+ ? {
142
+ activeOpacity: 0.84,
143
+ onPress: () => configs.onRowPress(row),
144
+ }
145
+ : {};
195
146
  const rowBackgroundColor = resolveRowBackgroundColor({
196
- selected,
197
147
  index,
198
148
  tableOddColor,
199
149
  tableEvenColor,
200
150
  });
151
+ const RowActionsComponent = configs.rowActionsComponent;
152
+
201
153
  return (
202
- <RowComponent key={item?.id || getRowKey(row)} style={[styles.row, { backgroundColor: rowBackgroundColor, borderBottomColor: selected ? '#67E8F9' : tableBorderColor, borderBottomWidth: tableBorderColor ? 1 : 0, minWidth: tableWidth, width: tableWidth }, typeof rowStyle === 'function' ? rowStyle(row, index) : rowStyle]} {...(hasRowPress || selectable ? { activeOpacity: 0.84, onPress: () => { if (selectable) emitSelection(toggleSelectedId(selectedIds, row)); configs.onRowPress?.(row); } } : {})}>
203
- {selectable ? renderSelectionCell(selected, () => emitSelection(toggleSelectedId(selectedIds, row)), `Selecionar linha ${getRowKey(row)}`, rowBackgroundColor) : null}
154
+ <RowComponent
155
+ key={getRowKey(row)}
156
+ style={[
157
+ styles.row,
158
+ {
159
+ backgroundColor: rowBackgroundColor,
160
+ borderBottomColor: tableBorderColor,
161
+ borderBottomWidth: tableBorderColor ? 1 : 0,
162
+ minWidth: tableWidth,
163
+ width: tableWidth,
164
+ },
165
+ rowStyleValue,
166
+ ]}
167
+ {...rowPressProps}
168
+ >
204
169
  {tableColumns.map(column => (
205
170
  <React.Fragment key={getColumnKey(column)}>
206
- <DefaultTableInput column={column} options={column?.isIdentity ? { cellStyle: [styles.pinnedIdentityCell, styles.stickyIdentityCell, { backgroundColor: rowBackgroundColor }] } : { cellStyle: { backgroundColor: rowBackgroundColor } }} row={row} storeName={storeName} variant="cell" />
171
+ <DefaultTableInput
172
+ column={column}
173
+ options={column?.isIdentity ? {
174
+ cellStyle: [
175
+ styles.pinnedIdentityCell,
176
+ styles.stickyIdentityCell,
177
+ {
178
+ backgroundColor: rowBackgroundColor,
179
+ },
180
+ ],
181
+ } : {
182
+ cellStyle: {
183
+ backgroundColor: rowBackgroundColor,
184
+ },
185
+ }}
186
+ row={row}
187
+ storeName={storeName}
188
+ variant="cell"
189
+ />
207
190
  </React.Fragment>
208
191
  ))}
209
- {renderActionsCell(row, rowBackgroundColor)}
192
+ {hasRowActions ? (
193
+ <View
194
+ testID={`default-table-row-actions-${getRowKey(row)}`}
195
+ style={[
196
+ styles.cell,
197
+ styles.actionsCell,
198
+ shouldPinRowActions ? styles.pinnedActionsCell : null,
199
+ shouldPinRowActions ? styles.stickyActionsCell : null,
200
+ {
201
+ minWidth: actionsCellWidth,
202
+ width: actionsCellWidth,
203
+ flexBasis: actionsCellWidth,
204
+ maxWidth: actionsCellWidth,
205
+ ...(stickyBodyTransforms.actions
206
+ ? { transform: stickyBodyTransforms.actions }
207
+ : {}),
208
+ backgroundColor: rowBackgroundColor,
209
+ },
210
+ ]}
211
+ >
212
+ <View style={styles.rowActionsGroup}>
213
+ {hasCustomRowActions ? (
214
+ <DefaultTableRowActions
215
+ component={RowActionsComponent}
216
+ helpers={{
217
+ openEdit: () => configs.onEditRow?.(row),
218
+ openRow: hasRowPress ? () => configs.onRowPress(row) : null,
219
+ }}
220
+ openEdit={() => configs.onEditRow?.(row)}
221
+ openRow={hasRowPress ? () => configs.onRowPress(row) : null}
222
+ row={row}
223
+ storeName={storeName}
224
+ />
225
+ ) : null}
226
+ {hasEditAction ? (
227
+ <TouchableOpacity
228
+ style={[styles.iconButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
229
+ activeOpacity={0.82}
230
+ onPress={() => configs.onEditRow?.(row)}
231
+ >
232
+ <Icon name="edit-2" size={14} color={tableMutedColor} />
233
+ </TouchableOpacity>
234
+ ) : null}
235
+ </View>
236
+ </View>
237
+ ) : null}
210
238
  </RowComponent>
211
239
  );
212
240
  };
213
241
 
214
242
  return (
215
- <ScrollView horizontal onContentSizeChange={width => updateHorizontalMetrics({ contentWidth: Number(width || 0) })} onLayout={event => updateHorizontalMetrics({ viewportWidth: Number(event?.nativeEvent?.layout?.width || 0) })} onScroll={event => updateHorizontalMetrics({ x: Number(event?.nativeEvent?.contentOffset?.x || 0) })} scrollEventThrottle={16} style={styles.scroll} contentContainerStyle={styles.horizontalScrollContent}>
216
- <View style={[styles.content, { minWidth: tableWidth, width: tableWidth }]}>
217
- <View style={[styles.headerRow, { backgroundColor: tableHeaderColor, borderBottomColor: tableHeaderBorderColor, borderBottomWidth: tableHeaderBorderColor ? 1 : 0, minWidth: tableWidth, width: tableWidth }]}>
218
- {selectable ? <View style={[styles.cell, { minWidth: SELECTION_COLUMN_WIDTH, width: SELECTION_COLUMN_WIDTH }]} /> : null}
219
- {tableColumns.map(column => {
220
- const fieldName = getColumnKey(column);
221
- return (
222
- <TouchableOpacity key={fieldName} style={[getColumnStyle(column), column?.isIdentity ? [styles.stickyIdentityCell, styles.stickyHeaderCell, { backgroundColor: tableHeaderColor }] : null]} activeOpacity={isSortableColumn(column) ? 0.8 : 1} onPress={() => configs.requestSort?.(column)}>
223
- <View style={styles.sortableHeader}>
224
- <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={1}>{formatStoreColumnLabel({ columns, fieldName, fallbackLabel: column?.label || fieldName, storeName })}</Text>
225
- {isSortableColumn(column) && configs.resolvedSort?.field === getSortField(column) ? <Icon name={configs.resolvedSort?.direction === 'desc' ? 'chevron-down' : 'chevron-up'} size={12} color={tableTextColor} /> : isSortableColumn(column) ? <Icon name="chevrons-up" size={12} color={tableBorderColor} /> : null}
226
- </View>
227
- </TouchableOpacity>
228
- );
229
- })}
230
- {renderActionsHeader()}
243
+ <>
244
+ <ScrollView
245
+ horizontal
246
+ onContentSizeChange={handleHorizontalContentSizeChange}
247
+ onLayout={handleHorizontalLayout}
248
+ onScroll={handleHorizontalScroll}
249
+ scrollEventThrottle={16}
250
+ style={styles.scroll}
251
+ contentContainerStyle={styles.horizontalScrollContent}
252
+ >
253
+ <View style={[styles.content, { minWidth: tableWidth, width: tableWidth }]}>
254
+ <View
255
+ style={[
256
+ styles.headerRow,
257
+ {
258
+ backgroundColor: tableHeaderColor,
259
+ borderBottomColor: tableHeaderBorderColor,
260
+ borderBottomWidth: tableHeaderBorderColor ? 1 : 0,
261
+ minWidth: tableWidth,
262
+ width: tableWidth,
263
+ },
264
+ ]}
265
+ >
266
+ {tableColumns.map(column => {
267
+ const fieldName = getColumnKey(column);
268
+ const label = formatStoreColumnLabel({
269
+ columns,
270
+ fieldName,
271
+ fallbackLabel: column?.label || fieldName,
272
+ storeName,
273
+ });
274
+ const sortFieldName = getSortField(column);
275
+
276
+ return (
277
+ <TouchableOpacity
278
+ key={fieldName}
279
+ style={[
280
+ getColumnStyle(column),
281
+ column?.isIdentity
282
+ ? [styles.stickyIdentityCell, styles.stickyHeaderCell, { backgroundColor: tableHeaderColor }]
283
+ : null,
284
+ ]}
285
+ activeOpacity={isSortableColumn(column) ? 0.8 : 1}
286
+ onPress={() => configs.requestSort?.(column)}
287
+ >
288
+ <View style={styles.sortableHeader}>
289
+ <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={1}>{label}</Text>
290
+ {isSortableColumn(column) && configs.resolvedSort?.field === sortFieldName ? (
291
+ <Icon name={configs.resolvedSort?.direction === 'desc' ? 'chevron-down' : 'chevron-up'} size={12} color={tableTextColor} />
292
+ ) : isSortableColumn(column) ? (
293
+ <Icon name="chevrons-up" size={12} color={tableBorderColor} />
294
+ ) : null}
295
+ {resolvedFilters?.[fieldName] ? <Icon name="filter" size={11} color={tableTextColor} /> : null}
296
+ </View>
297
+ </TouchableOpacity>
298
+ );
299
+ })}
300
+ {hasRowActions ? (
301
+ <View
302
+ testID="default-table-row-actions-header"
303
+ style={[
304
+ styles.cell,
305
+ styles.actionsCell,
306
+ shouldPinRowActions ? styles.stickyActionsCell : null,
307
+ shouldPinRowActions ? styles.stickyHeaderCell : null,
308
+ {
309
+ minWidth: actionsCellWidth,
310
+ width: actionsCellWidth,
311
+ flexBasis: actionsCellWidth,
312
+ maxWidth: actionsCellWidth,
313
+ backgroundColor: tableHeaderColor,
314
+ },
315
+ ]}
316
+ >
317
+ <Text style={[styles.headerText, { color: tableTextColor }]}>
318
+ {global.t?.t(storeName, 'label', 'actions')}
319
+ </Text>
320
+ </View>
321
+ ) : null}
322
+ </View>
323
+ {hasColumnFilters ? (
324
+ <View
325
+ style={[
326
+ styles.filterRow,
327
+ {
328
+ backgroundColor: tableHeaderColor,
329
+ borderBottomColor: tableHeaderBorderColor,
330
+ borderBottomWidth: tableHeaderBorderColor ? 1 : 0,
331
+ },
332
+ ]}
333
+ >
334
+ {tableColumns.map(column => {
335
+ const fieldName = getColumnKey(column);
336
+
337
+ return (
338
+ <View
339
+ key={`${fieldName}-filter`}
340
+ style={[
341
+ getColumnStyle(column),
342
+ column?.isIdentity
343
+ ? [styles.stickyIdentityCell, { backgroundColor: tableHeaderColor }]
344
+ : null,
345
+ ]}
346
+ >
347
+ {column?.filter !== false && column?.filters !== false ? (
348
+ <DefaultColumnFilter
349
+ column={column}
350
+ filters={resolvedFilters}
351
+ storeName={storeName}
352
+ style={styles.filterCell}
353
+ />
354
+ ) : null}
355
+ </View>
356
+ );
357
+ })}
358
+ {hasRowActions ? (
359
+ <View
360
+ style={[
361
+ styles.cell,
362
+ styles.actionsCell,
363
+ shouldPinRowActions ? styles.stickyActionsCell : null,
364
+ {
365
+ minWidth: actionsCellWidth,
366
+ width: actionsCellWidth,
367
+ flexBasis: actionsCellWidth,
368
+ maxWidth: actionsCellWidth,
369
+ backgroundColor: tableHeaderColor,
370
+ },
371
+ ]}
372
+ />
373
+ ) : null}
374
+ </View>
375
+ ) : null}
376
+
377
+ <FlatList
378
+ data={sortedData}
379
+ keyExtractor={getRowKey}
380
+ renderItem={renderTableItem}
381
+ style={styles.tableList}
382
+ contentContainerStyle={styles.tableListContent}
383
+ ListEmptyComponent={(
384
+ <DefaultTableEmptyState
385
+ emptyStateLabel={emptyStateLabel}
386
+ isLoading={isLoading}
387
+ isTable
388
+ tableMutedColor={tableMutedColor}
389
+ />
390
+ )}
391
+ ListFooterComponent={null}
392
+ nestedScrollEnabled
393
+ onMomentumScrollBegin={configs.onMomentumScrollBegin || undefined}
394
+ onScroll={handleListScroll}
395
+ onScrollBeginDrag={configs.onScrollBeginDrag || undefined}
396
+ onEndReached={configs.onEndReached}
397
+ onEndReachedThreshold={END_REACHED_THRESHOLD}
398
+ onRefresh={configs.onRefresh}
399
+ refreshing={Boolean(configs.refreshing)}
400
+ scrollEventThrottle={120}
401
+ showsVerticalScrollIndicator={false}
402
+ />
231
403
  </View>
232
- <FlatList data={listData} extraData={selectedIds} keyExtractor={item => item?.id || getRowKey(item?.row || item)} renderItem={renderTableItem} style={styles.tableList} contentContainerStyle={styles.tableListContent} ListEmptyComponent={<DefaultTableEmptyState emptyStateLabel={emptyStateLabel} isLoading={isLoading} isTable tableMutedColor={tableMutedColor} />} nestedScrollEnabled onEndReached={configs.onEndReached} onEndReachedThreshold={END_REACHED_THRESHOLD} onRefresh={configs.onRefresh} refreshing={Boolean(configs.refreshing)} onScroll={event => { if (shouldTriggerEndReachedFromScroll(event)) configs.onEndReached?.(); }} scrollEventThrottle={120} showsVerticalScrollIndicator={false} />
233
- </View>
234
- </ScrollView>
404
+ </ScrollView>
405
+ </>
235
406
  );
236
407
  };
237
408
 
@@ -34,7 +34,11 @@ const resolveConfiguredLabel = config => {
34
34
  return global.t?.t?.('defaultTable', 'button', 'import');
35
35
  };
36
36
 
37
- const DefaultTableToolbar = ({ storeName }) => {
37
+ const DefaultTableToolbar = ({
38
+ storeName,
39
+ showToolbarActions: showToolbarActionsProp,
40
+ showToolbarControls: showToolbarControlsProp,
41
+ }) => {
38
42
  const store = useStore(storeName);
39
43
  const [isImportModalVisible, setIsImportModalVisible] = useState(false);
40
44
  const { width } = useWindowDimensions();
@@ -44,6 +48,14 @@ const DefaultTableToolbar = ({ storeName }) => {
44
48
  const showTotalItemsInCompactToolbar = configs.showTotalItemsInCompactToolbar === true;
45
49
  const showTotalItemsInFooter = configs.showTotalItemsInFooter !== false;
46
50
  const toolbarActions = Array.isArray(configs.toolbarActions) ? configs.toolbarActions : [];
51
+ const showToolbarActions =
52
+ showToolbarActionsProp === null || showToolbarActionsProp === undefined
53
+ ? configs.showToolbarActions !== false
54
+ : showToolbarActionsProp !== false;
55
+ const showToolbarControls =
56
+ showToolbarControlsProp === null || showToolbarControlsProp === undefined
57
+ ? configs.showToolbarControls !== false
58
+ : showToolbarControlsProp !== false;
47
59
  const buildTransferAction = (action, fallback) => {
48
60
  if (!action) return null;
49
61
 
@@ -99,11 +111,9 @@ const DefaultTableToolbar = ({ storeName }) => {
99
111
  label: global.t?.t?.('defaultTable', 'button', 'export'),
100
112
  },
101
113
  );
102
- const resolvedToolbarActions = [
103
- importTableAction,
104
- exportTableAction,
105
- ...toolbarActions,
106
- ].filter(Boolean);
114
+ const resolvedToolbarActions = showToolbarActions
115
+ ? [importTableAction, exportTableAction, ...toolbarActions].filter(Boolean)
116
+ : [];
107
117
  const { tableBorderColors, toolbarColors } = useDefaultTableTheme();
108
118
  const {
109
119
  backgroundColor: toolbarBackgroundColor,
@@ -178,7 +188,7 @@ const DefaultTableToolbar = ({ storeName }) => {
178
188
  <DefaultTableCollapsedSearch storeName={storeName} />
179
189
  <View style={styles.toolbarActionGroup}>
180
190
  {!shouldRenderCompactToolbarTotalItems ? renderToolbarActions() : null}
181
- <DefaultTableControls storeName={storeName} />
191
+ {showToolbarControls ? <DefaultTableControls storeName={storeName} /> : null}
182
192
  </View>
183
193
  </View>
184
194
  <DefaultTableImportModal