@controleonline/ui-default 1.0.272 → 1.0.273

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) 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 +4 -4
  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/upload/DefaultUpload.js +6 -36
  24. package/src/react/components/upload/DefaultUpload.styles.js +5 -5
  25. package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
  26. package/src/react/hooks/usePostalCodeLookup.js +15 -1
  27. package/src/react/services/addressFormUtils.js +104 -38
  28. package/src/react/services/addressGeo.js +6 -3
  29. package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
  30. package/src/tests/react/components/files/DefaultFile.test.js +16 -0
  31. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  32. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  33. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  34. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  35. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  36. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
  37. package/src/tests/react/filters/filterValue.test.js +46 -0
  38. package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
  39. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  40. package/src/tests/react/services/addressFormUtils.test.js +6 -3
  41. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +12 -1
@@ -436,10 +436,10 @@ const DefaultTable = ({
436
436
  ],
437
437
  );
438
438
 
439
- if (store?.getters) {
440
- assignGetterValue(store, 'configs', defaultTableConfigs);
441
- }
442
-
439
+ // Do not mutate store.configs during render. A new configs object is built
440
+ // every pass; writing it here retriggers subscribers and React #185 when
441
+ // opening Adicionar on DefaultTable (categories). Sync only in the hook
442
+ // when defaultTableConfigsSignature changes.
443
443
  useDefaultTableStoreSync({
444
444
  columns,
445
445
  columnsForTable,
@@ -470,11 +470,13 @@ const styles = StyleSheet.create({
470
470
  textTransform: 'uppercase',
471
471
  },
472
472
  actionsCell: {
473
- minWidth: 96,
473
+ minWidth: 60,
474
+ width: 60,
475
+ flexBasis: 60,
474
476
  flexGrow: 0,
475
477
  flexShrink: 0,
478
+ maxWidth: 72,
476
479
  alignItems: 'center',
477
- paddingHorizontal: 8,
478
480
  },
479
481
  rowActionsGroup: {
480
482
  flexDirection: 'row',
@@ -517,10 +519,11 @@ const styles = StyleSheet.create({
517
519
  },
518
520
  cardsGrid: {
519
521
  paddingVertical: 10,
520
- paddingHorizontal: 8,
521
- gap: 10,
522
+ paddingHorizontal: 0,
523
+ gap: 8,
522
524
  flexGrow: 1,
523
525
  width: '100%',
526
+ alignSelf: 'stretch',
524
527
  },
525
528
  cardsGridWithBottomAdd: {
526
529
  paddingBottom: 86,
@@ -539,8 +542,11 @@ const styles = StyleSheet.create({
539
542
  cardItem: {
540
543
  position: 'relative',
541
544
  flex: 1,
545
+ flexBasis: 0,
546
+ flexGrow: 1,
542
547
  minWidth: 0,
543
548
  width: '100%',
549
+ maxWidth: '100%',
544
550
  alignSelf: 'stretch',
545
551
  },
546
552
  cardItemWithActions: {
@@ -438,6 +438,42 @@ export const normalizeSortText = value =>
438
438
  .replace(/[\u0300-\u036f]/g, '')
439
439
  .toLowerCase();
440
440
 
441
+ export const parseSortNumber = value => {
442
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
443
+ if (value == null) return NaN;
444
+
445
+ const raw = String(value).trim();
446
+ if (!raw || !/[0-9]/.test(raw)) return NaN;
447
+
448
+ const compact = raw.replace(/[^0-9,.-]/g, '');
449
+ if (!compact) return NaN;
450
+
451
+ const hasComma = compact.includes(',');
452
+ const hasDot = compact.includes('.');
453
+ let normalized = compact;
454
+
455
+ if (hasComma && hasDot) {
456
+ normalized = compact.lastIndexOf(',') > compact.lastIndexOf('.')
457
+ ? compact.replace(/\./g, '').replace(',', '.')
458
+ : compact.replace(/,/g, '');
459
+ } else if (hasComma) {
460
+ const parts = compact.split(',');
461
+ normalized = parts.length === 2 && parts[1].length <= 2
462
+ ? `${parts[0].replace(/\./g, '')}.${parts[1]}`
463
+ : compact.replace(/,/g, '');
464
+ } else if (hasDot) {
465
+ const parts = compact.split('.');
466
+ if (parts.length > 2) {
467
+ normalized = compact.replace(/\./g, '');
468
+ } else if (parts.length === 2 && parts[1].length === 3 && parts[0].length <= 3) {
469
+ normalized = compact.replace(/\./g, '');
470
+ }
471
+ }
472
+
473
+ const parsed = Number(normalized);
474
+ return Number.isFinite(parsed) ? parsed : NaN;
475
+ };
476
+
441
477
  export const resolveSortComparable = ({ column, row, storeName, columns }) => {
442
478
  const fieldName = getColumnKey(column);
443
479
  const sortField = getSortField(column);
@@ -472,11 +508,9 @@ export const resolveSortComparable = ({ column, row, storeName, columns }) => {
472
508
  storeName,
473
509
  }));
474
510
 
475
- const normalizedNumber = Number(
476
- String(resolvedValue).replace(/[^0-9,.-]/g, '').replace(',', '.'),
477
- );
511
+ const normalizedNumber = parseSortNumber(resolvedValue);
478
512
 
479
- if (Number.isFinite(normalizedNumber) && String(resolvedValue).match(/[0-9]/)) {
513
+ if (Number.isFinite(normalizedNumber)) {
480
514
  return normalizedNumber;
481
515
  }
482
516
 
@@ -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