@controleonline/ui-default 1.0.264 → 1.0.266

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 (46) hide show
  1. package/AGENTS.md +3 -2
  2. package/package.json +21 -18
  3. package/src/react/components/errors/DefaultErrors.js +64 -82
  4. package/src/react/components/filters/CompactFilterSelector.js +58 -23
  5. package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
  6. package/src/react/components/filters/DefaultColumnFilter.js +177 -45
  7. package/src/react/components/filters/DefaultExternalFilters.js +201 -143
  8. package/src/react/components/filters/DefaultSearch.js +27 -25
  9. package/src/react/components/form/DefaultForm.js +72 -53
  10. package/src/react/components/form/DefaultForm.styles.js +71 -66
  11. package/src/react/components/inputs/DefaultDateInput.js +150 -0
  12. package/src/react/components/inputs/DefaultInput.js +44 -11
  13. package/src/react/components/inputs/DefaultInput.styles.js +6 -0
  14. package/src/react/components/inputs/DefaultSelect.js +200 -43
  15. package/src/react/components/inputs/defaultInputUtils.js +40 -19
  16. package/src/react/components/table/DefaultColumnMenu.js +108 -0
  17. package/src/react/components/table/DefaultDebug.js +234 -0
  18. package/src/react/components/table/DefaultFiltersModal.js +142 -0
  19. package/src/react/components/table/DefaultModalButton.js +18 -0
  20. package/src/react/components/table/DefaultSearchModal.js +49 -0
  21. package/src/react/components/table/DefaultTable.js +322 -1964
  22. package/src/react/components/table/DefaultTable.styles.js +234 -102
  23. package/src/react/components/table/DefaultTable.utils.js +502 -0
  24. package/src/react/components/table/DefaultTableBody.js +26 -0
  25. package/src/react/components/table/DefaultTableCards.js +171 -0
  26. package/src/react/components/table/DefaultTableControls.js +205 -0
  27. package/src/react/components/table/DefaultTableEmptyState.js +22 -0
  28. package/src/react/components/table/DefaultTableFooter.js +172 -0
  29. package/src/react/components/table/DefaultTableInput.js +145 -0
  30. package/src/react/components/table/DefaultTableRows.js +344 -0
  31. package/src/react/components/table/DefaultTableSearch.js +100 -0
  32. package/src/react/components/table/DefaultTableToolbar.js +98 -0
  33. package/src/react/components/table/DefaultToolbarAction.js +56 -0
  34. package/src/react/components/table/useDefaultTablePagination.js +370 -0
  35. package/src/react/components/table/useDefaultTableSorting.js +126 -0
  36. package/src/react/components/table/useDefaultTableTheme.js +52 -0
  37. package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
  38. package/src/store/default/actions.js +51 -13
  39. package/src/store/default/getters.js +10 -8
  40. package/src/store/default/mutation_types.js +4 -2
  41. package/src/store/default/mutations.js +10 -1
  42. package/src/tests/react/components/form/DefaultForm.test.js +81 -0
  43. package/src/tests/react/components/inputs/DefaultSelect.test.js +106 -14
  44. package/src/tests/react/components/table/DefaultTable.test.js +1318 -334
  45. package/src/tests/react/store/defaultActions.test.js +59 -0
  46. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,478 +1,53 @@
1
- /* eslint-disable no-unused-vars */
2
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import {
4
- FlatList,
5
- Modal,
6
- ScrollView,
7
- Text,
8
- TouchableOpacity,
9
- View,
10
- useWindowDimensions,
11
- } from 'react-native';
12
- import { useIsFocused } from '@react-navigation/native';
13
- import Icon from 'react-native-vector-icons/Feather';
14
- import { getAllStores, useStore } from '@store';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { TouchableOpacity, View, useWindowDimensions } from 'react-native';
3
+ import { useIsFocused, useRoute } from '@react-navigation/native';
4
+ import { useStore } from '@store';
15
5
  import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
16
- import Formatter from '@controleonline/ui-common/src/utils/formatter.js';
17
- import StateStore from '@controleonline/ui-layout/src/react/components/StateStore';
18
- import { getDateRange } from '@controleonline/ui-common/src/react/utils/dateRangeFilter';
19
- import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
20
- import { resolveThemePalette } from '@controleonline/../../src/styles/branding';
21
- import { colors } from '@controleonline/../../src/styles/colors';
22
- import DefaultColumnFilter from '../filters/DefaultColumnFilter';
23
- import DefaultSearch from '../filters/DefaultSearch';
24
- import DefaultForm from '../form/DefaultForm';
25
- import DefaultInput from '../inputs/DefaultInput';
26
- import {
27
- formatSaveValue,
28
- getColumnKey,
29
- isEditableColumn,
30
- normalizeId,
31
- normalizeOptionKey,
32
- normalizeText,
33
- resolveStoreNameFromList,
34
- resolveCellText,
35
- resolveEditValue,
36
- } from '../inputs/defaultInputUtils';
6
+ import Icon from 'react-native-vector-icons/Feather';
7
+ import { normalizeText } from '../inputs/defaultInputUtils';
8
+ import { DEFAULT_COMPACT_BREAKPOINT, isObject, stableSerialize } from './DefaultTable.utils';
37
9
  import {
38
- persistTableSortPreference,
39
- persistTableViewModePreference,
40
- persistVisibleColumnsPreference,
41
- resolveStoredTableSortPreference,
10
+ resolveDefaultTablePreferenceScope,
11
+ resolveStoredTableFiltersPreference,
42
12
  resolveStoredTableViewModePreference,
43
13
  resolveStoredVisibleColumnsPreference,
14
+ sanitizeTableFiltersPreference,
44
15
  sanitizeVisibleColumnsPreference,
45
16
  } from '../../utils/tableVisibleColumnsPreferences';
46
- import styles from './DefaultTable.styles';
47
-
48
- const DEFAULT_CELL_MIN_WIDTH = 118;
49
- const DEFAULT_COMPACT_BREAKPOINT = 768;
50
- const END_REACHED_THRESHOLD = 0.35;
51
- const IDENTITY_CELL_MIN_WIDTH = 76;
52
- const MONEY_CELL_MIN_WIDTH = 132;
53
- const ACTIONS_CELL_WIDTH = 60;
54
- const SUMMARY_OPERATIONS = ['sum', 'count', 'avg', 'min', 'max'];
55
- const LIST_OPTIONS_PAGE_SIZE = 100;
56
-
57
- const shouldIncludeColumn = column =>
58
- Boolean(getColumnKey(column)) &&
59
- column?.show !== false &&
60
- column?.visible !== false &&
61
- column?.table !== false;
62
-
63
- const resolveListActionName = list =>
64
- normalizeText(list).split('/')[1] || 'getItems';
65
-
66
- const COMPANY_SCOPED_LIST_STORES = new Set([
67
- 'categories',
68
- 'paymentType',
69
- 'wallet',
70
- ]);
71
-
72
- const resolveListLoadParams = ({currentCompanyId, listStoreName}) => {
73
- if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
74
- return {};
75
- }
76
-
77
- return listStoreName === 'categories'
78
- ? {company: currentCompanyId}
79
- : {people: currentCompanyId};
80
- };
81
-
82
- export const resolveColumnListLoadParams = ({
83
- column,
84
- currentCompanyId,
85
- requestParams = {},
86
- }) => {
87
- const listStoreName = resolveStoreNameFromList(column?.list);
88
- const companyScopedParams = resolveListLoadParams({
89
- currentCompanyId,
90
- listStoreName,
91
- });
92
- const resolvedCustomParams = typeof column?.listRequestParams === 'function'
93
- ? column.listRequestParams({currentCompanyId, requestParams})
94
- : column?.listRequestParams;
95
- const customParams =
96
- resolvedCustomParams &&
97
- typeof resolvedCustomParams === 'object' &&
98
- !Array.isArray(resolvedCustomParams)
99
- ? resolvedCustomParams
100
- : {};
101
-
102
- return {
103
- ...companyScopedParams,
104
- ...customParams,
105
- };
106
- };
107
-
108
- const isObject = value =>
109
- value !== null && typeof value === 'object' && !Array.isArray(value);
110
-
111
- const getRowKey = (row, index = 0) =>
112
- String(row?.['@id'] || row?.id || index);
113
-
114
- const stableSerialize = value => {
115
- if (value === null || value === undefined) {
116
- return 'null';
117
- }
118
-
119
- if (Array.isArray(value)) {
120
- return `[${value.map(item => stableSerialize(item)).join(',')}]`;
121
- }
122
-
123
- if (value instanceof Date) {
124
- return JSON.stringify(value.toISOString());
125
- }
126
-
127
- if (isObject(value)) {
128
- return `{${Object.keys(value)
129
- .sort()
130
- .map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
131
- .join(',')}}`;
132
- }
133
-
134
- return JSON.stringify(value);
135
- };
136
-
137
- const resolveDateRangeQuery = value => {
138
- if (!value || typeof value !== 'object') {
139
- return {};
140
- }
141
-
142
- const shortcut = value.shortcut || value.value || 'all';
143
- const customRange = value.customRange || { from: '', to: '' };
144
- const dateRange = getDateRange(shortcut, customRange, {
145
- relativeMode: 'rolling',
146
- useCurrentMoment: true,
147
- });
148
-
149
- return {
150
- after: dateRange?.after || '',
151
- before: dateRange?.before || '',
152
- };
153
- };
154
-
155
- const resolveFilterQueryValue = value => {
156
- if (Array.isArray(value)) {
157
- return value
158
- .map(item => resolveFilterQueryValue(item))
159
- .filter(item => item !== '' && item !== null && item !== undefined);
17
+ import DefaultTableBody from './DefaultTableBody';
18
+ import DefaultTableFooter from './DefaultTableFooter';
19
+ import DefaultTableToolbar from './DefaultTableToolbar';
20
+ import styles from './DefaultTable.styles';
21
+ import { useDefaultTablePagination } from './useDefaultTablePagination';
22
+ import {
23
+ useDefaultTableSortedData,
24
+ useDefaultTableSortState,
25
+ } from './useDefaultTableSorting';
26
+ import useDefaultTableTheme from './useDefaultTableTheme';
27
+
28
+ export {
29
+ mergeSortedDataWithLiveItems,
30
+ resolveColumnListLoadParams,
31
+ shouldTriggerEndReachedFromScroll,
32
+ } from './DefaultTable.utils';
33
+
34
+ const publishStoreValue = (store, actionName, value, getterName) => {
35
+ if (typeof store?.actions?.[actionName] === 'function') {
36
+ store.actions[actionName](value);
37
+ return;
160
38
  }
161
39
 
162
- if (value && typeof value === 'object') {
163
- return resolveFilterQueryValue(
164
- value.value ?? value.id ?? value['@id'] ?? value.key ?? '',
165
- );
40
+ if (store?.getters) {
41
+ store.getters[getterName] = value;
166
42
  }
167
-
168
- return normalizeText(value);
169
- };
170
-
171
- const normalizeCollectionItems = response => {
172
- if (Array.isArray(response)) return response;
173
- if (Array.isArray(response?.member)) return response.member;
174
- if (Array.isArray(response?.['hydra:member'])) return response['hydra:member'];
175
-
176
- return [];
177
43
  };
178
-
179
- const resolveHasMore = ({ hasMore, dataLength, totalItems }) => {
180
- if (hasMore !== null && hasMore !== undefined) {
181
- return hasMore;
182
- }
183
-
184
- const resolvedTotalItems = Number(totalItems);
185
- if (!Number.isFinite(resolvedTotalItems)) {
186
- return false;
187
- }
188
-
189
- return dataLength < resolvedTotalItems;
190
- };
191
-
192
- const humanizeSummaryLabel = value =>
193
- normalizeText(value)
194
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
195
- .replace(/[._-]+/g, ' ')
196
- .replace(/\s+/g, ' ')
197
- .trim();
198
-
199
- const isMoneySummaryPath = path =>
200
- /(amount|price|total|value|paid|open|receivable|payable|pending)/i.test(
201
- Array.isArray(path) ? path.join('.') : normalizeText(path),
202
- );
203
-
204
- const normalizeSummaryMoneyValue = value => {
205
- if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
206
-
207
- const rawValue = normalizeText(value).replace(/[^0-9,.-]/g, '');
208
- if (!rawValue) return 0;
209
-
210
- const normalizedValue = rawValue.includes(',')
211
- ? rawValue.replace(/\./g, '').replace(',', '.')
212
- : rawValue;
213
- const parsedValue = Number(normalizedValue);
214
-
215
- return Number.isFinite(parsedValue) ? parsedValue : 0;
216
- };
217
-
218
- const getSummaryOperations = column => {
219
- if (typeof column?.summary === 'string') return [column.summary];
220
- if (Array.isArray(column?.summary)) return column.summary;
221
- if (isObject(column?.summary)) {
222
- return Object.entries(column.summary)
223
- .filter(([, value]) => value)
224
- .map(([operation]) => operation);
225
- }
226
-
227
- return SUMMARY_OPERATIONS.filter(operation => column?.[operation] === true);
228
- };
229
-
230
- const getSummaryField = (column, operation) => {
231
- if (isObject(column?.summary)) {
232
- const summaryField = column.summary[operation];
233
- if (typeof summaryField === 'string') return summaryField;
234
- }
235
-
236
- return getColumnKey(column);
237
- };
238
-
239
- const formatSummaryValue = ({ column, columns, path, storeName, value }) => {
240
- const columnKey = column ? getColumnKey(column) : '';
241
- const shouldFormatMoney = isMoneySummaryPath(path) || isMoneySummaryPath(columnKey);
242
-
243
- if (shouldFormatMoney) {
244
- return Formatter.formatMoney(normalizeSummaryMoneyValue(value));
245
- }
246
-
247
- if (column) {
248
- return resolveCellText({
249
- column,
250
- columns,
251
- row: { [columnKey]: value },
252
- storeName,
253
- value,
254
- });
255
- }
256
-
257
- return normalizeText(value);
258
- };
259
-
260
- const flattenSummaryEntries = ({
261
- path = [],
262
- summaryLabels = {},
263
- usedPaths,
264
- value,
265
- }) => {
266
- if (!isObject(value)) {
267
- const pathKey = path.join('.');
268
- if (!pathKey || usedPaths.has(pathKey)) return [];
269
-
270
- const fallbackLabel = humanizeSummaryLabel(path[path.length - 1] || pathKey);
271
-
272
- return [{
273
- key: pathKey,
274
- label: summaryLabels[pathKey] || summaryLabels[path[path.length - 1]] || fallbackLabel,
275
- path,
276
- value,
277
- }];
278
- }
279
-
280
- return Object.entries(value).flatMap(([key, childValue]) =>
281
- flattenSummaryEntries({
282
- path: [...path, key],
283
- summaryLabels,
284
- usedPaths,
285
- value: childValue,
286
- }),
287
- );
288
- };
289
-
290
- const isSortableColumn = column => column?.sortable === true;
291
-
292
- const getSortField = column => column?.sortField || getColumnKey(column);
293
44
 
294
- const resolveDefaultSort = columns => {
295
- if (!Array.isArray(columns)) {
296
- return null;
45
+ const assignGetterValue = (store, getterName, value) => {
46
+ if (store?.getters) {
47
+ store.getters[getterName] = value;
297
48
  }
298
-
299
- for (const column of columns) {
300
- if (!column || column?.defaultSort === undefined || column?.defaultSort === null || column?.defaultSort === false) {
301
- continue;
302
- }
303
-
304
- const field = getSortField(column);
305
- const defaultSort = column.defaultSort;
306
-
307
- if (defaultSort && typeof defaultSort === 'object' && !Array.isArray(defaultSort)) {
308
- const resolvedDirection = normalizeText(defaultSort.direction || defaultSort.order || 'desc').toLowerCase();
309
- const resolvedField = normalizeText(
310
- defaultSort.field || defaultSort.sortField || defaultSort.key || defaultSort.name || field,
311
- );
312
-
313
- return {
314
- direction: resolvedDirection === 'asc' ? 'asc' : 'desc',
315
- field: resolvedField || field,
316
- };
317
- }
318
-
319
- if (typeof defaultSort === 'string') {
320
- const normalizedSort = normalizeText(defaultSort).toLowerCase();
321
- if (normalizedSort === 'asc' || normalizedSort === 'desc') {
322
- return {
323
- direction: normalizedSort,
324
- field,
325
- };
326
- }
327
-
328
- return {
329
- direction: 'desc',
330
- field: normalizeText(defaultSort) || field,
331
- };
332
- }
333
-
334
- if (defaultSort === true) {
335
- return {
336
- direction: 'asc',
337
- field,
338
- };
339
- }
340
- }
341
-
342
- return null;
343
49
  };
344
50
 
345
- const sanitizeStoredSortPreference = ({
346
- columns = [],
347
- fallbackSort = null,
348
- sort = null,
349
- }) => {
350
- const normalizedFallback =
351
- fallbackSort &&
352
- typeof fallbackSort === 'object' &&
353
- typeof fallbackSort.field === 'string' &&
354
- fallbackSort.field.trim()
355
- ? {
356
- direction: fallbackSort.direction === 'asc' ? 'asc' : 'desc',
357
- field: fallbackSort.field.trim(),
358
- }
359
- : null;
360
-
361
- if (
362
- !sort ||
363
- typeof sort !== 'object' ||
364
- typeof sort.field !== 'string' ||
365
- !sort.field.trim()
366
- ) {
367
- return normalizedFallback;
368
- }
369
-
370
- const normalizedDirection = sort.direction === 'asc' ? 'asc' : 'desc';
371
- const normalizedField = sort.field.trim();
372
- const sortableFields = new Set(
373
- (Array.isArray(columns) ? columns : [])
374
- .filter(isSortableColumn)
375
- .map(column => getSortField(column))
376
- .filter(Boolean),
377
- );
378
-
379
- if (!sortableFields.has(normalizedField)) {
380
- return normalizedFallback;
381
- }
382
-
383
- return {
384
- direction: normalizedDirection,
385
- field: normalizedField,
386
- };
387
- };
388
-
389
- const readValueByPath = (object, path) => {
390
- if (!object || !path) return object;
391
-
392
- return String(path)
393
- .split('.')
394
- .reduce((currentValue, key) => {
395
- if (currentValue === null || currentValue === undefined) return currentValue;
396
- return currentValue?.[key];
397
- }, object);
398
- };
399
-
400
- const normalizeSortText = value =>
401
- normalizeText(value)
402
- .normalize('NFD')
403
- .replace(/[\u0300-\u036f]/g, '')
404
- .toLowerCase();
405
-
406
- const isDateLikeColumn = column =>
407
- column?.inputType === 'date' ||
408
- column?.inputType === 'date-range' ||
409
- column?.type === 'date' ||
410
- column?.type === 'range-date' ||
411
- /date/i.test(getColumnKey(column));
412
-
413
- const resolveSortComparable = ({ column, row, storeName, columns }) => {
414
- const fieldName = getColumnKey(column);
415
- const sortField = getSortField(column);
416
- const rawValue = sortField === fieldName ? row?.[fieldName] : readValueByPath(row, sortField);
417
-
418
- if (isDateLikeColumn(column)) {
419
- const dateValue =
420
- rawValue && typeof rawValue === 'object'
421
- ? rawValue?.value ??
422
- rawValue?.date ??
423
- rawValue?.createdAt ??
424
- rawValue?.updatedAt ??
425
- rawValue?.['@id'] ??
426
- rawValue?.[fieldName] ??
427
- rawValue
428
- : rawValue;
429
- const parsedDate = Date.parse(dateValue);
430
- return Number.isFinite(parsedDate) ? parsedDate : Number.NEGATIVE_INFINITY;
431
- }
432
-
433
- const resolvedValue = sortField === fieldName
434
- ? resolveCellText({
435
- column,
436
- columns,
437
- row,
438
- storeName,
439
- })
440
- : normalizeText(rawValue ?? resolveCellText({
441
- column,
442
- columns,
443
- row,
444
- storeName,
445
- }));
446
-
447
- const normalizedNumber = Number(
448
- String(resolvedValue).replace(/[^0-9,.-]/g, '').replace(',', '.'),
449
- );
450
-
451
- if (Number.isFinite(normalizedNumber) && String(resolvedValue).match(/[0-9]/)) {
452
- return normalizedNumber;
453
- }
454
-
455
- return normalizeSortText(resolvedValue);
456
- };
457
-
458
- const getColumnStyle = column => {
459
- const key = getColumnKey(column);
460
- if (column?.isIdentity) return [styles.cell, styles.identityCell];
461
- if (['price', 'total', 'amount', 'value'].includes(key)) {
462
- return [styles.cell, styles.moneyCell];
463
- }
464
- return styles.cell;
465
- };
466
-
467
- const getColumnMinWidth = column => {
468
- const key = getColumnKey(column);
469
- if (column?.isIdentity) return IDENTITY_CELL_MIN_WIDTH;
470
- if (['price', 'total', 'amount', 'value'].includes(key)) {
471
- return MONEY_CELL_MIN_WIDTH;
472
- }
473
- return DEFAULT_CELL_MIN_WIDTH;
474
- };
475
-
476
51
  const DefaultTable = ({
477
52
  accentColor = null,
478
53
  actions = {},
@@ -481,1591 +56,374 @@ const DefaultTable = ({
481
56
  columns = [],
482
57
  data = undefined,
483
58
  filters = {},
59
+ footerComponent = null,
484
60
  forceCardsOnCompact = true,
485
61
  getOptionsForColumn = null,
486
62
  hasMore = null,
487
63
  initialViewMode = 'table',
488
- pageSize = null,
489
64
  isLoading = false,
65
+ onAdd = null,
66
+ onDataLoaded = null,
490
67
  onEditRow = null,
491
68
  onEndReached = null,
492
- onMomentumScrollBegin = null,
493
- onScrollBeginDrag = null,
494
- onAdd = null,
495
69
  onFilterChange = null,
70
+ onMomentumScrollBegin = null,
496
71
  onRowPress = null,
497
72
  onSaved = null,
73
+ onScrollBeginDrag = null,
498
74
  onSortChange = null,
499
- onDataLoaded = null,
500
- requestParams = {},
75
+ pageSize = null,
76
+ pinRowActions = true,
501
77
  renderCard = null,
502
- searchProps = null,
503
- toolbarActions = [],
78
+ requestParams = {},
79
+ rowActionsComponent = null,
80
+ rowStyle = null,
504
81
  showColumnFiltersButton = true,
505
- showTotalItemsInFooter = true,
506
- showTotalItemsInCompactToolbar = false,
507
82
  showRowActions = true,
508
- rowStyle = null,
83
+ showToolbar = true,
84
+ showTotalItemsInCompactToolbar = false,
85
+ showTotalItemsInFooter = true,
509
86
  sort = null,
510
87
  storeName = '',
511
- summary = null,
512
- summaryLabels = null,
513
- totalItems = null,
514
- totalItemsLabel = null,
515
- totalItemsText = null,
516
- footerComponent = null,
88
+ summary = undefined,
89
+ summaryLabels = {},
90
+ toolbarActions = [],
517
91
  visibleColumnsPreferenceKey = '',
518
92
  }) => {
519
93
  const { width } = useWindowDimensions();
94
+ const route = useRoute?.();
520
95
  const store = useStore(storeName);
521
- const visibleColumnsStorageKey = visibleColumnsPreferenceKey || storeName;
522
- const peopleStore = useStore('people');
523
- const themeStore = useStore('theme');
524
- const [editingCell, setEditingCell] = useState(null);
525
- const [editingRow, setEditingRow] = useState(null);
526
- const [formMode, setFormMode] = useState('edit');
527
- const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
528
- const [listOptionsByColumn, setListOptionsByColumn] = useState({});
529
- const [savingCell, setSavingCell] = useState(null);
530
- const [showColumnFilters, setShowColumnFilters] = useState(false);
531
- const [tableContainerWidth, setTableContainerWidth] = useState(0);
532
- const endReachedLockRef = useRef(false);
533
- const loadedListStoresRef = useRef(new Set());
534
- const previousPaginationStateRef = useRef({
535
- dataLength: Array.isArray(data) ? data.length : 0,
536
- filtersKey: JSON.stringify(filters || {}),
537
- sortDirection: sort?.direction,
538
- sortField: sort?.field,
539
- });
540
- const visibleColumnsSeed = useMemo(
96
+ const isFocused = useIsFocused();
97
+ const configsSignatureRef = useRef('');
98
+ const { showError } = useMessage() || {};
99
+ const { themeColors } = useDefaultTableTheme(accentColor);
100
+ const floatingAddBackgroundColor =
101
+ themeColors.buttonBackground || themeColors.primary || accentColor || '#2563EB';
102
+ const floatingAddIconColor = themeColors.buttonText || '#FFFFFF';
103
+ const tablePreferenceScope = useMemo(
541
104
  () =>
542
- sanitizeVisibleColumnsPreference({
543
- columns: Array.isArray(store?.getters?.columns) && store.getters.columns.length > 0
544
- ? store.getters.columns
545
- : columns,
546
- visibleColumns: resolveStoredVisibleColumnsPreference(
547
- visibleColumnsStorageKey,
548
- ),
105
+ resolveDefaultTablePreferenceScope({
106
+ preferenceKey: visibleColumnsPreferenceKey,
107
+ route,
108
+ storeName,
549
109
  }),
550
- [columns, store?.getters?.columns, visibleColumnsStorageKey],
110
+ [route?.key, route?.name, storeName, visibleColumnsPreferenceKey],
551
111
  );
552
- const viewModeSeed = useMemo(
553
- () =>
554
- resolveStoredTableViewModePreference(
555
- visibleColumnsPreferenceKey,
556
- initialViewMode,
557
- ),
558
- [initialViewMode, visibleColumnsPreferenceKey],
112
+ const storedViewMode = useMemo(
113
+ () => resolveStoredTableViewModePreference(tablePreferenceScope, null),
114
+ [initialViewMode, tablePreferenceScope],
559
115
  );
560
- const [visibleColumns, setVisibleColumns] = useState(() => visibleColumnsSeed);
561
- const [viewMode, setViewMode] = useState(() => viewModeSeed);
562
- const [compactViewMode, setCompactViewMode] = useState(null);
563
- const { currentCompany } = peopleStore.getters || {};
564
- const { colors: themeColors } = themeStore.getters;
565
- const themeTokens = useMemo(
566
- () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
567
- [currentCompany?.theme?.colors, themeColors],
568
- );
569
- const palette = useMemo(
570
- () => resolveThemePalette(themeTokens, colors),
571
- [themeTokens],
572
- );
573
- const resolvedAccentColor = accentColor || palette.primary;
574
- const tableHeaderColor = themeTokens['bg-headers-light'] || resolvedAccentColor;
575
- const tableEvenColor = themeTokens.listItemEvenRow || themeTokens['bg-even-light'] || palette.background;
576
- const tableOddColor = themeTokens.listItemOddRow || themeTokens['bg-odd-light'] || palette.background;
577
- const tableBorderColor = palette.border;
578
- const tableSurfaceColor = palette.background;
579
- const tableTextColor = palette.text;
580
- const tableMutedColor = palette.textSecondary;
581
- const panelBackgroundColor = themeColors.panelBackground;
582
- const panelBorderColor = themeColors.panelBorder;
583
- const modalBackgroundColor = themeColors.modalBackground;
584
- const modalBorderColor = themeColors.modalBorder;
585
- const modalCloseIconColor = themeColors.modalCloseIcon;
586
- const modalHeaderTextColor = themeColors.modalHeaderText;
587
- const modalOverlayColor = themeColors.modalOverlay;
588
- const modalTextColor = themeColors.modalText;
589
- const checkboxBorderColor = themeColors.checkboxBorder;
590
- const checkboxSelectedMarkColor = themeColors.checkboxSelectedMark;
591
- const toolbarBackgroundColor = themeColors.toolbarBackground;
592
- const toolbarBorderColor = themeColors.toolbarBorder;
593
- const toolbarCountBackgroundColor = themeColors.badgeBackground;
594
- const toolbarCountTextColor = themeColors.badgeText;
595
- const tableActionBackgroundColor = themeColors.tableActionBackground;
596
- const tableActionBorderColor = themeColors.tableActionBorder;
597
- const tableActionTextColor = themeColors.tableActionIcon;
598
- const tableButtonBackgroundColor = themeColors.buttonBackground;
599
- const tableButtonBorderColor = themeColors.buttonBorder;
600
- const tableButtonIconColor = themeColors.buttonIcon;
601
- const tableButtonPressedBackgroundColor = themeColors.buttonPressedBackground;
602
- const tableButtonPressedBorderColor = themeColors.buttonPressedBorder;
603
- const tableButtonPressedIconColor = themeColors.buttonPressedIcon;
604
- const tableButtonTextColor = themeColors.buttonText;
605
- const tableFilterBackgroundColor = themeColors.tableFilterBackground;
606
- const tableFilterBorderColor = themeColors.tableFilterBorder;
607
- const tableFilterTextColor = themeColors.tableFilterText;
608
- const isFocused = useIsFocused();
609
- const {showError} = useMessage() || {};
610
- const autoMode = data === undefined && normalizeText(storeName) !== '';
611
- const searchKey = searchProps?.searchKey || 'search';
612
- const storeActions = store?.actions || {};
613
116
  const resolvedActions = useMemo(
614
117
  () => ({
615
- ...storeActions,
118
+ ...(store?.actions || {}),
616
119
  ...(actions || {}),
617
120
  }),
618
- [actions, storeActions],
121
+ [actions, store?.actions],
619
122
  );
123
+ const autoMode = data === undefined && normalizeText(storeName) !== '';
620
124
  const storeColumns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
621
125
  const columnsForTable = storeColumns.length > 0 ? storeColumns : columns;
622
126
  const storeFilters = isObject(store?.getters?.filters) ? store.getters.filters : {};
623
127
  const requestParamsSeed = isObject(requestParams) ? requestParams : {};
624
- const initialFiltersSeed = autoMode
625
- ? (Object.keys(filters || {}).length > 0 ? filters : storeFilters)
626
- : (isObject(filters) ? filters : {});
627
- const [autoFilters, setAutoFilters] = useState(() => initialFiltersSeed);
628
- const defaultSortSeed = useMemo(
629
- () => resolveDefaultSort(columnsForTable),
630
- [columnsForTable],
631
- );
632
- const storedSortSeed = useMemo(
633
- () =>
634
- sanitizeStoredSortPreference({
635
- columns: columnsForTable,
636
- fallbackSort: sort || defaultSortSeed || null,
637
- sort: resolveStoredTableSortPreference(visibleColumnsPreferenceKey),
638
- }),
639
- [columnsForTable, defaultSortSeed, sort, visibleColumnsPreferenceKey],
640
- );
641
- const [autoSort, setAutoSort] = useState(() => storedSortSeed);
642
- const [autoHasLoaded, setAutoHasLoaded] = useState(false);
643
- const [autoLoading, setAutoLoading] = useState(false);
644
- const [autoLoadingMore, setAutoLoadingMore] = useState(false);
645
- const [autoLastPageCount, setAutoLastPageCount] = useState(0);
646
- const autoRequestIdRef = useRef(0);
647
- const autoLoadedQueryKeyRef = useRef('');
648
- const autoErroredQueryKeyRef = useRef('');
649
- const autoPageRef = useRef(0);
650
- const pageSizeNumber = Number(requestParamsSeed.itemsPerPage || pageSize || 50) || 50;
651
- const resolvedSort = autoMode ? autoSort : sort;
652
- const resolvedFilters = autoMode ? autoFilters : (filters || {});
653
- const resolvedSearchValue = normalizeText(resolvedFilters?.[searchKey]);
654
- const resolvedGetOptionsForColumn = useCallback(
655
- column => {
656
- const explicitOptions = getOptionsForColumn?.(column);
657
- if (Array.isArray(explicitOptions)) return explicitOptions;
658
-
659
- const columnKey = getColumnKey(column);
660
- return Array.isArray(listOptionsByColumn[columnKey])
661
- ? listOptionsByColumn[columnKey]
662
- : undefined;
663
- },
664
- [getOptionsForColumn, listOptionsByColumn],
665
- );
666
-
667
- useEffect(() => {
668
- setListOptionsByColumn({});
669
- loadedListStoresRef.current.clear();
670
- }, [currentCompany?.id]);
671
-
672
- const loadListOptionsForColumns = useCallback(
673
- (targetColumns = []) => {
674
- const columnsToLoad = (Array.isArray(targetColumns) ? targetColumns : []).filter(
675
- column => normalizeText(column?.list),
676
- );
677
-
678
- if (columnsToLoad.length === 0) {
679
- return Promise.resolve([]);
680
- }
681
-
682
- const stores = getAllStores?.() || {};
683
- const loadPromises = [];
684
-
685
- columnsToLoad.forEach(column => {
686
- const explicitOptions = getOptionsForColumn?.(column);
687
- if (Array.isArray(explicitOptions)) {
688
- return;
689
- }
690
-
691
- const listStoreName = resolveStoreNameFromList(column.list);
692
- const actionName = resolveListActionName(column.list);
693
- const listStore = stores?.[listStoreName];
694
- const listAction = listStore?.actions?.[actionName];
695
-
696
- const listLoadParams = resolveColumnListLoadParams({
697
- column,
698
- currentCompanyId: currentCompany?.id,
699
- requestParams: requestParamsSeed,
700
- });
701
- const isCompanyScopedList = Object.keys(listLoadParams).length > 0;
702
-
703
- if (!listStoreName || typeof listAction !== 'function') {
704
- return;
705
- }
706
-
707
- if (
708
- !isCompanyScopedList &&
709
- Array.isArray(listStore?.getters?.items) &&
710
- listStore.getters.items.length > 0
711
- ) {
712
- const columnKey = getColumnKey(column);
713
- setListOptionsByColumn(current => ({
714
- ...current,
715
- [columnKey]: normalizeCollectionItems(listStore.getters.items),
716
- }));
717
- return;
718
- }
719
-
720
- const columnKey = getColumnKey(column);
721
- const loadKey = `${columnKey}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
722
- if (loadedListStoresRef.current.has(loadKey)) {
723
- return;
724
- }
725
-
726
- loadedListStoresRef.current.add(loadKey);
727
- loadPromises.push(
728
- Promise.resolve(
729
- listAction({
730
- ...listLoadParams,
731
- itemsPerPage: LIST_OPTIONS_PAGE_SIZE,
732
- __storeMeta: {
733
- dedupeKey: `default-table-list-options:${loadKey}`,
734
- skipSystemError: true,
735
- },
736
- }),
737
- )
738
- .then(response => ({columnKey, items: normalizeCollectionItems(response)}))
739
- .catch(() => {
740
- loadedListStoresRef.current.delete(loadKey);
741
- return {columnKey, items: []};
742
- }),
743
- );
744
- });
745
-
746
- if (loadPromises.length === 0) {
747
- return Promise.resolve([]);
748
- }
749
-
750
- return Promise.all(loadPromises).then(results => {
751
- const nextOptionsByColumn = Object.fromEntries(
752
- results.map(result => [result.columnKey, result.items]),
753
- );
754
-
755
- if (Object.keys(nextOptionsByColumn).length > 0) {
756
- setListOptionsByColumn(current => ({
757
- ...current,
758
- ...nextOptionsByColumn,
759
- }));
760
- }
761
-
762
- return results;
763
- });
764
- },
765
- [currentCompany?.id, getOptionsForColumn, requestParamsSeed],
766
- );
767
-
768
- const buildRequestQuery = useCallback(
769
- (page, append = false) => {
770
- const query = {
771
- ...requestParamsSeed,
772
- itemsPerPage: pageSizeNumber,
773
- page,
774
- };
775
-
776
- if (autoSort?.field && autoSort?.direction) {
777
- query[`order[${autoSort.field}]`] = autoSort.direction;
778
- }
779
-
780
- Object.entries(autoFilters || {}).forEach(([fieldName, value]) => {
781
- if (!fieldName) return;
782
-
783
- if (fieldName === searchKey) {
784
- const normalizedSearch = normalizeText(value).replace(/^#/, '');
785
- if (normalizedSearch) {
786
- query.search = normalizedSearch;
787
- if (searchKey !== 'search') {
788
- query[searchKey] = normalizedSearch;
789
- }
790
- }
791
- return;
792
- }
793
-
794
- const column = columnsForTable.find(item => getColumnKey(item) === fieldName);
795
-
796
- if (column?.inputType === 'date-range' || column?.type === 'range-date') {
797
- const dateRange = resolveDateRangeQuery(value);
798
- if (dateRange.after) {
799
- query[`${fieldName}[after]`] = dateRange.after;
800
- }
801
- if (dateRange.before) {
802
- query[`${fieldName}[before]`] = dateRange.before;
803
- }
804
- return;
805
- }
806
-
807
- const normalizedValue = resolveFilterQueryValue(value);
808
- if (Array.isArray(normalizedValue)) {
809
- if (normalizedValue.length > 0) {
810
- query[fieldName] = normalizedValue;
811
- }
812
- return;
813
- }
128
+ const resolvedTotalItems = store?.getters?.totalItems;
129
+ const currentConfigs = store?.getters?.configs || {};
130
+ const isCompactView = width > 0 && width <= compactBreakpoint;
131
+ const addConfig = store?.getters?.add;
132
+ const shouldRenderFloatingAddButton =
133
+ showToolbar === false &&
134
+ (addConfig === true || add === true) &&
135
+ (typeof onAdd === 'function' || typeof store?.actions?.save === 'function');
136
+
137
+ if (storeColumns.length === 0 && Array.isArray(columns) && columns.length > 0) {
138
+ assignGetterValue(store, 'columns', columns);
139
+ }
814
140
 
815
- if (normalizedValue !== '') {
816
- query[fieldName] = normalizedValue;
817
- }
818
- });
141
+ if (data !== undefined && Array.isArray(data)) {
142
+ assignGetterValue(store, 'items', data);
143
+ }
819
144
 
820
- if (append) {
821
- query.append = true;
822
- }
145
+ const { requestSort, resolvedSort } = useDefaultTableSortState({
146
+ autoMode,
147
+ columnsForTable,
148
+ onSortChange,
149
+ sort,
150
+ tablePreferenceScope,
151
+ });
152
+ const {
153
+ buildRequestQuery,
154
+ currentPage,
155
+ handleEndReached,
156
+ resolvedData,
157
+ } = useDefaultTablePagination({
158
+ autoMode,
159
+ columnsForTable,
160
+ data,
161
+ filters: storeFilters,
162
+ hasMore,
163
+ isFocused,
164
+ isLoading,
165
+ onEndReached,
166
+ pageSize,
167
+ requestParams: requestParamsSeed,
168
+ resolvedActions,
169
+ resolvedSort,
170
+ resolvedTotalItems,
171
+ showError,
172
+ store,
173
+ storeName,
174
+ });
175
+ const sortedData = useDefaultTableSortedData({
176
+ resolvedData,
177
+ resolvedSort,
178
+ storeName,
179
+ tableColumns: columnsForTable,
180
+ });
181
+ const debugFallbackParameters = useMemo(() => {
182
+ if (autoMode) {
183
+ return buildRequestQuery(currentPage || 1, false);
184
+ }
823
185
 
824
- return query;
825
- },
826
- [autoFilters, autoSort?.direction, autoSort?.field, columnsForTable, pageSizeNumber, requestParamsSeed, searchKey],
186
+ return {
187
+ filters: storeFilters,
188
+ requestParams: requestParamsSeed,
189
+ sort: resolvedSort || null,
190
+ };
191
+ }, [autoMode, buildRequestQuery, currentPage, requestParamsSeed, resolvedSort, storeFilters]);
192
+ const effectiveViewMode =
193
+ currentConfigs.viewMode ||
194
+ storedViewMode ||
195
+ (isCompactView && forceCardsOnCompact !== false ? 'cards' : initialViewMode);
196
+ const tableFiltersVisible = Boolean(currentConfigs.tableFiltersVisible);
197
+ const defaultTableConfigs = useMemo(
198
+ () => ({
199
+ add,
200
+ compactBreakpoint,
201
+ debugFallbackParameters,
202
+ effectiveViewMode,
203
+ filters,
204
+ footerComponent,
205
+ forceCardsOnCompact,
206
+ getOptionsForColumn,
207
+ initialViewMode,
208
+ onAdd,
209
+ onDataLoaded,
210
+ onEditRow,
211
+ onEndReached: handleEndReached,
212
+ onFilterChange,
213
+ onMomentumScrollBegin,
214
+ onRowPress,
215
+ onSaved,
216
+ onScrollBeginDrag,
217
+ pinRowActions,
218
+ renderCard,
219
+ requestParams: requestParamsSeed,
220
+ requestSort,
221
+ resolvedSort,
222
+ rowActionsComponent,
223
+ rowStyle,
224
+ showColumnFiltersButton,
225
+ showRowActions,
226
+ showToolbar,
227
+ showTotalItemsInCompactToolbar,
228
+ showTotalItemsInFooter,
229
+ sortedData,
230
+ summary,
231
+ summaryLabels,
232
+ tableFiltersVisible,
233
+ tablePreferenceScope,
234
+ toolbarActions,
235
+ viewMode: effectiveViewMode,
236
+ }),
237
+ [
238
+ add,
239
+ compactBreakpoint,
240
+ debugFallbackParameters,
241
+ effectiveViewMode,
242
+ filters,
243
+ footerComponent,
244
+ forceCardsOnCompact,
245
+ getOptionsForColumn,
246
+ handleEndReached,
247
+ initialViewMode,
248
+ onAdd,
249
+ onDataLoaded,
250
+ onEditRow,
251
+ onFilterChange,
252
+ onMomentumScrollBegin,
253
+ onRowPress,
254
+ onSaved,
255
+ onScrollBeginDrag,
256
+ pinRowActions,
257
+ renderCard,
258
+ requestParamsSeed,
259
+ requestSort,
260
+ resolvedSort,
261
+ rowActionsComponent,
262
+ rowStyle,
263
+ showColumnFiltersButton,
264
+ showRowActions,
265
+ showToolbar,
266
+ showTotalItemsInCompactToolbar,
267
+ showTotalItemsInFooter,
268
+ sortedData,
269
+ summary,
270
+ summaryLabels,
271
+ tableFiltersVisible,
272
+ tablePreferenceScope,
273
+ toolbarActions,
274
+ ],
827
275
  );
828
- const autoQuerySignature = useMemo(
276
+ const defaultTableConfigsSignature = useMemo(
829
277
  () =>
830
278
  stableSerialize({
831
- filters: autoFilters,
832
- pageSize: pageSizeNumber,
833
- requestParams: requestParamsSeed,
834
- searchKey,
835
- sort: autoSort,
279
+ add,
280
+ compactBreakpoint,
281
+ debugFallbackParameters,
282
+ effectiveViewMode,
283
+ filters,
284
+ forceCardsOnCompact,
285
+ initialViewMode,
286
+ pinRowActions,
287
+ resolvedSort,
288
+ showColumnFiltersButton,
289
+ showRowActions,
290
+ showToolbar,
291
+ showTotalItemsInCompactToolbar,
292
+ showTotalItemsInFooter,
293
+ storedViewMode,
294
+ tableFiltersVisible,
295
+ sortedDataLength: sortedData.length,
296
+ toolbarActionsLength: Array.isArray(toolbarActions) ? toolbarActions.length : 0,
836
297
  }),
837
- [autoFilters, autoSort, pageSizeNumber, requestParamsSeed, searchKey],
838
- );
839
-
840
- const loadAutoPage = useCallback(
841
- (page, { append = false } = {}) => {
842
- if (!autoMode || typeof resolvedActions.getItems !== 'function') {
843
- return Promise.resolve([]);
844
- }
845
-
846
- const requestId = autoRequestIdRef.current + 1;
847
- autoRequestIdRef.current = requestId;
848
-
849
- if (append) {
850
- setAutoLoadingMore(true);
851
- } else {
852
- setAutoLoading(true);
853
- setAutoHasLoaded(false);
854
- setAutoLastPageCount(0);
855
- autoPageRef.current = 0;
856
- }
857
-
858
- const query = buildRequestQuery(page, append);
859
-
860
- return Promise.resolve(resolvedActions.getItems(query))
861
- .then(response => {
862
- if (autoRequestIdRef.current !== requestId) {
863
- return response;
864
- }
865
-
866
- const pageItems = normalizeCollectionItems(response);
867
- autoPageRef.current = page;
868
- autoLoadedQueryKeyRef.current = autoQuerySignature;
869
- autoErroredQueryKeyRef.current = '';
870
- setAutoHasLoaded(true);
871
- setAutoLastPageCount(pageItems.length);
872
- return pageItems;
873
- })
874
- .catch(error => {
875
- if (autoRequestIdRef.current === requestId) {
876
- autoErroredQueryKeyRef.current = autoQuerySignature;
877
- showError?.(error?.message || 'Nao foi possivel carregar os registros.');
878
- }
879
-
880
- return [];
881
- })
882
- .finally(() => {
883
- if (autoRequestIdRef.current === requestId) {
884
- setAutoLoading(false);
885
- setAutoLoadingMore(false);
886
- endReachedLockRef.current = false;
887
- }
888
- });
889
- },
890
- [autoMode, autoQuerySignature, buildRequestQuery, resolvedActions, showError],
891
- );
892
-
893
- const availableColumns = useMemo(
894
- () => columnsForTable.filter(column => shouldIncludeColumn(column)),
895
- [columnsForTable],
298
+ [
299
+ add,
300
+ compactBreakpoint,
301
+ debugFallbackParameters,
302
+ effectiveViewMode,
303
+ filters,
304
+ forceCardsOnCompact,
305
+ initialViewMode,
306
+ pinRowActions,
307
+ resolvedSort,
308
+ showColumnFiltersButton,
309
+ showRowActions,
310
+ showToolbar,
311
+ showTotalItemsInCompactToolbar,
312
+ showTotalItemsInFooter,
313
+ storedViewMode,
314
+ tableFiltersVisible,
315
+ sortedData.length,
316
+ toolbarActions,
317
+ ],
896
318
  );
897
-
898
- useEffect(() => {
899
- setVisibleColumns(prev =>
900
- stableSerialize(prev) === stableSerialize(visibleColumnsSeed)
901
- ? prev
902
- : visibleColumnsSeed
903
- );
904
- }, [visibleColumnsSeed]);
905
-
906
- useEffect(() => {
907
- setViewMode(viewModeSeed);
908
- }, [viewModeSeed]);
909
319
 
910
- useEffect(() => {
911
- if (!autoMode) return;
912
-
913
- setAutoSort(prev =>
914
- stableSerialize(prev) === stableSerialize(storedSortSeed)
915
- ? prev
916
- : storedSortSeed,
917
- );
918
- }, [autoMode, storedSortSeed]);
919
-
920
- useEffect(() => {
921
- if (!autoMode) return;
922
-
923
- setAutoFilters(prev => {
924
- if (stableSerialize(prev) === stableSerialize(storeFilters)) {
925
- return prev;
926
- }
927
-
928
- return storeFilters;
929
- });
930
- }, [autoMode, storeFilters]);
320
+ if (store?.getters) {
321
+ assignGetterValue(store, 'configs', defaultTableConfigs);
322
+ }
931
323
 
932
324
  useEffect(() => {
933
- if (!autoMode || !isFocused) return;
934
- if (typeof resolvedActions.getItems !== 'function') return;
935
- if (
936
- autoLoading ||
937
- autoLoadingMore ||
938
- autoLoadedQueryKeyRef.current === autoQuerySignature ||
939
- autoErroredQueryKeyRef.current === autoQuerySignature
940
- ) {
325
+ if (storeColumns.length > 0 || !Array.isArray(columns) || columns.length === 0) {
941
326
  return;
942
327
  }
943
328
 
944
- endReachedLockRef.current = false;
945
- loadAutoPage(1, { append: false });
946
- }, [
947
- autoLoading,
948
- autoLoadingMore,
949
- autoMode,
950
- autoQuerySignature,
951
- isFocused,
952
- loadAutoPage,
953
- resolvedActions,
954
- ]);
955
-
956
- const tableColumns = useMemo(
957
- () => columnsForTable.filter(column => shouldIncludeColumn(column) && visibleColumns[getColumnKey(column)] !== false),
958
- [columnsForTable, visibleColumns],
959
- );
960
-
961
- const editableColumns = useMemo(
962
- () => tableColumns.filter(isEditableColumn),
963
- [tableColumns],
964
- );
329
+ publishStoreValue(store, 'setColumns', columns, 'columns');
330
+ }, [columns, store, storeColumns.length]);
965
331
 
966
- const activeFilterCount = useMemo(
967
- () => Object.values(resolvedFilters || {}).filter(value => normalizeText(value) !== '').length,
968
- [resolvedFilters],
969
- );
970
- const hasRowActions =
971
- showRowActions !== false &&
972
- (editableColumns.length > 0 || typeof onEditRow === 'function');
973
- const storeAddConfig = store?.getters?.add;
974
- const addConfig = add !== null && add !== undefined ? add : storeAddConfig;
975
- const hasAddInstruction = addConfig === true || (isObject(addConfig) && addConfig.enabled !== false);
976
- const shouldRenderAddButton =
977
- hasAddInstruction &&
978
- (typeof onAdd === 'function' || typeof resolvedActions.save === 'function');
979
- const resolvedIsLoading = autoMode ? (autoLoading || autoLoadingMore) : Boolean(isLoading);
980
- const resolvedData = autoMode
981
- ? (autoHasLoaded && Array.isArray(store?.getters?.items) ? store.getters.items : [])
982
- : (Array.isArray(data) ? data : []);
983
- const tableMinimumWidth = useMemo(
984
- () =>
985
- tableColumns.reduce(
986
- (totalWidth, column) => totalWidth + getColumnMinWidth(column),
987
- hasRowActions ? ACTIONS_CELL_WIDTH : 0,
988
- ),
989
- [hasRowActions, tableColumns],
990
- );
991
- const tableWidth = Math.max(tableContainerWidth, tableMinimumWidth);
992
- const tableLayoutStyle = useMemo(
993
- () => (tableWidth > 0 ? { minWidth: tableWidth, width: tableWidth } : null),
994
- [tableWidth],
995
- );
996
- const isCompactView = width > 0 && width <= compactBreakpoint;
997
- const shouldForceCardsOnCompact = forceCardsOnCompact !== false;
998
- const effectiveViewMode =
999
- isCompactView && shouldForceCardsOnCompact
1000
- ? (compactViewMode || 'cards')
1001
- : viewMode;
1002
332
  useEffect(() => {
1003
- if (!shouldForceCardsOnCompact) {
1004
- setCompactViewMode(null);
333
+ if (data === undefined || !Array.isArray(data)) {
1005
334
  return;
1006
335
  }
1007
336
 
1008
- if (isCompactView) {
1009
- setCompactViewMode('cards');
1010
- return;
1011
- }
1012
-
1013
- setCompactViewMode(null);
1014
- }, [isCompactView, shouldForceCardsOnCompact]);
1015
- const emptyStateLabel = resolvedIsLoading
1016
- ? global.t?.t(storeName, 'label', 'loading') || 'Carregando...'
1017
- : 'Nenhum registro encontrado';
1018
- const storeTotalItems = store?.getters?.totalItems;
1019
- const resolvedTotalItems = totalItems !== null && totalItems !== undefined
1020
- ? totalItems
1021
- : storeTotalItems;
1022
- const totalItemsNumber = Number(resolvedTotalItems);
1023
- const shouldRenderTotalItems =
1024
- resolvedTotalItems !== null &&
1025
- resolvedTotalItems !== undefined &&
1026
- Number.isFinite(totalItemsNumber);
1027
- const shouldRenderFooterTotalItems =
1028
- shouldRenderTotalItems &&
1029
- showTotalItemsInFooter !== false;
1030
- const shouldRenderCompactToolbarTotalItems =
1031
- showTotalItemsInCompactToolbar === true &&
1032
- isCompactView &&
1033
- shouldRenderTotalItems;
1034
- const resolvedTotalItemsText =
1035
- normalizeText(totalItemsText) !== ''
1036
- ? totalItemsText
1037
- : shouldRenderTotalItems
1038
- ? `${totalItemsNumber} ${totalItemsLabel || global.t?.t(storeName, 'label', 'items') || 'registros'}`
1039
- : '';
1040
- const storeSummary = store?.getters?.summary;
1041
- const resolvedSummary = summary !== null && summary !== undefined ? summary : storeSummary;
1042
- const shouldReadSummary = resolvedSummary !== false && isObject(resolvedSummary);
1043
- const summaryEntries = useMemo(() => {
1044
- if (!shouldReadSummary) return [];
1045
-
1046
- const labels = summaryLabels || {};
1047
- const usedPaths = new Set();
1048
- const columnEntries = tableColumns.flatMap(column => {
1049
- const operations = getSummaryOperations(column);
1050
- if (!operations.length) return [];
1051
-
1052
- const columnLabel = formatStoreColumnLabel({
1053
- columns: columnsForTable,
1054
- fieldName: getColumnKey(column),
1055
- fallbackLabel: column?.label || getColumnKey(column),
1056
- storeName,
1057
- });
1058
-
1059
- return operations.map(operation => {
1060
- const fieldName = getSummaryField(column, operation);
1061
- const path = [operation, fieldName];
1062
- const pathKey = path.join('.');
1063
- const value = resolvedSummary?.[operation]?.[fieldName];
1064
- if (value === undefined) return null;
1065
-
1066
- usedPaths.add(pathKey);
1067
-
1068
- return {
1069
- key: pathKey,
1070
- label: labels[pathKey] || (operations.length > 1 ? `${columnLabel} ${operation}` : columnLabel),
1071
- path,
1072
- value,
1073
- column,
1074
- };
1075
- }).filter(Boolean);
1076
- });
1077
-
1078
- const genericEntries = flattenSummaryEntries({
1079
- summaryLabels: labels,
1080
- usedPaths,
1081
- value: resolvedSummary,
1082
- });
1083
-
1084
- return [...columnEntries, ...genericEntries].filter(entry =>
1085
- entry?.value !== undefined &&
1086
- entry?.value !== null &&
1087
- normalizeText(entry.value) !== '',
1088
- );
1089
- }, [columnsForTable, resolvedSummary, shouldReadSummary, storeName, summaryLabels, tableColumns]);
1090
- const shouldRenderFooterBar =
1091
- (shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems) ||
1092
- summaryEntries.length > 0;
1093
- const footerProps = {
1094
- columns: columnsForTable,
1095
- resolvedAccentColor,
1096
- resolvedTotalItemsText,
1097
- shouldRenderCompactToolbarTotalItems,
1098
- shouldRenderFooterTotalItems,
1099
- storeName,
1100
- summaryEntries,
1101
- summaryLabels,
1102
- tableBorderColor,
1103
- tableMutedColor,
1104
- tableSurfaceColor,
1105
- tableTextColor,
1106
- toolbarCountBackgroundColor,
1107
- toolbarCountTextColor,
1108
- };
1109
-
1110
- const sortedData = useMemo(() => {
1111
- const items = Array.isArray(resolvedData) ? [...resolvedData] : [];
1112
- const sortField = resolvedSort?.field;
1113
- const sortDirection = resolvedSort?.direction === 'desc' ? 'desc' : 'asc';
1114
- const sortColumn = tableColumns.find(column => getSortField(column) === sortField);
1115
-
1116
- if (!sortField || !sortColumn) return items;
1117
-
1118
- return items.sort((left, right) => {
1119
- const leftValue = resolveSortComparable({
1120
- column: sortColumn,
1121
- columns: tableColumns,
1122
- row: left,
1123
- storeName,
1124
- });
1125
- const rightValue = resolveSortComparable({
1126
- column: sortColumn,
1127
- columns: tableColumns,
1128
- row: right,
1129
- storeName,
1130
- });
1131
-
1132
- const comparison =
1133
- typeof leftValue === 'number' && typeof rightValue === 'number'
1134
- ? leftValue - rightValue
1135
- : normalizeSortText(leftValue).localeCompare(normalizeSortText(rightValue));
1136
-
1137
- return sortDirection === 'desc' ? comparison * -1 : comparison;
1138
- });
1139
- }, [resolvedData, resolvedSort?.direction, resolvedSort?.field, storeName, tableColumns]);
1140
-
1141
- const resolvedHasMore = useMemo(
1142
- () => {
1143
- if (autoMode) {
1144
- if (Number.isFinite(Number(resolvedTotalItems)) && Number(resolvedTotalItems) > 0) {
1145
- return sortedData.length < Number(resolvedTotalItems);
1146
- }
1147
-
1148
- return autoLastPageCount >= pageSizeNumber && sortedData.length > 0;
1149
- }
1150
-
1151
- return resolveHasMore({
1152
- hasMore,
1153
- dataLength: sortedData.length,
1154
- totalItems: resolvedTotalItems,
1155
- });
1156
- },
1157
- [autoLastPageCount, autoMode, hasMore, pageSizeNumber, resolvedTotalItems, sortedData.length],
1158
- );
1159
-
1160
- useEffect(() => {
1161
- const nextPaginationState = {
1162
- dataLength: sortedData.length,
1163
- filtersKey: stableSerialize(resolvedFilters || {}),
1164
- sortDirection: resolvedSort?.direction,
1165
- sortField: resolvedSort?.field,
1166
- };
1167
-
1168
- const previousPaginationState = previousPaginationStateRef.current;
1169
- const didPaginationStateChange =
1170
- previousPaginationState.dataLength !== nextPaginationState.dataLength ||
1171
- previousPaginationState.filtersKey !== nextPaginationState.filtersKey ||
1172
- previousPaginationState.sortDirection !== nextPaginationState.sortDirection ||
1173
- previousPaginationState.sortField !== nextPaginationState.sortField;
1174
-
1175
- if (didPaginationStateChange) {
1176
- endReachedLockRef.current = false;
1177
- previousPaginationStateRef.current = nextPaginationState;
1178
- }
1179
- }, [resolvedFilters, resolvedSort?.direction, resolvedSort?.field, sortedData]);
337
+ publishStoreValue(store, 'setItems', data, 'items');
338
+ }, [data, store]);
1180
339
 
1181
340
  useEffect(() => {
1182
- if (typeof onDataLoaded !== 'function') {
341
+ if (!columnsForTable.length) {
1183
342
  return;
1184
343
  }
1185
344
 
1186
- onDataLoaded(sortedData);
1187
- }, [onDataLoaded, sortedData]);
1188
-
1189
- const beginEdit = useCallback((row, column) => {
1190
- if (!isEditableColumn(column)) return;
1191
- setEditingCell(`${row?.id || row?.['@id']}:${getColumnKey(column)}`);
1192
- }, []);
1193
-
1194
- const clearEdit = useCallback(() => {
1195
- setEditingCell(null);
1196
- }, []);
1197
-
1198
- const saveCell = useCallback((row, column, nextValue) => {
1199
- const fieldName = getColumnKey(column);
1200
- if (!fieldName || typeof resolvedActions.save !== 'function') {
1201
- clearEdit();
1202
- return Promise.resolve(null);
1203
- }
1204
-
1205
- const currentValue = resolveEditValue(column, row);
1206
- const normalizedNextValue = nextValue && typeof nextValue === 'object'
1207
- ? normalizeOptionKey(nextValue)
1208
- : normalizeText(nextValue);
1209
-
1210
- if (normalizeText(currentValue) === normalizeText(normalizedNextValue)) {
1211
- clearEdit();
1212
- return Promise.resolve(null);
1213
- }
1214
-
1215
- const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1216
- const payload = {
1217
- id: normalizeId(row?.['@id'] || row?.id),
1218
- [fieldName]: formatSaveValue(column, nextValue, row),
1219
- };
1220
-
1221
- setSavingCell(cellKey);
1222
-
1223
- return resolvedActions.save(payload)
1224
- .then(savedItem => {
1225
- onSaved?.(savedItem || { ...row, [fieldName]: nextValue }, row);
1226
- return savedItem;
1227
- })
1228
- .catch(error => {
1229
- console.error(error);
1230
- return null;
1231
- })
1232
- .finally(() => {
1233
- setSavingCell(null);
1234
- clearEdit();
1235
- });
1236
- }, [clearEdit, onSaved, resolvedActions]);
1237
-
1238
- const requestSort = useCallback(column => {
1239
- if (!isSortableColumn(column)) return;
1240
-
1241
- const fieldName = getSortField(column);
1242
- const nextDirection =
1243
- resolvedSort?.field === fieldName && resolvedSort?.direction === 'asc'
1244
- ? 'desc'
1245
- : 'asc';
1246
- const nextSort = {
1247
- direction: nextDirection,
1248
- field: fieldName,
1249
- };
1250
-
1251
- if (visibleColumnsPreferenceKey) {
1252
- persistTableSortPreference(
1253
- visibleColumnsPreferenceKey,
1254
- nextSort,
1255
- );
1256
- }
1257
-
1258
- if (autoMode) {
1259
- setAutoSort(nextSort);
345
+ const storedVisibleColumns = resolveStoredVisibleColumnsPreference(tablePreferenceScope);
346
+ if (!storedVisibleColumns) {
1260
347
  return;
1261
348
  }
1262
349
 
1263
- onSortChange?.(nextSort);
1264
- }, [
1265
- autoMode,
1266
- onSortChange,
1267
- resolvedSort?.direction,
1268
- resolvedSort?.field,
1269
- visibleColumnsPreferenceKey,
1270
- ]);
1271
-
1272
- const openEditModal = useCallback(row => {
1273
- if (typeof onEditRow === 'function') {
1274
- onEditRow(row);
350
+ const nextVisibleColumns = sanitizeVisibleColumnsPreference({
351
+ columns: columnsForTable,
352
+ visibleColumns: storedVisibleColumns,
353
+ });
354
+ const currentVisibleColumns = store?.getters?.visibleColumns || {};
355
+
356
+ if (stableSerialize(currentVisibleColumns) === stableSerialize(nextVisibleColumns)) {
1275
357
  return;
1276
358
  }
1277
359
 
1278
- setFormMode('edit');
1279
- setEditingRow(row);
1280
- }, [onEditRow]);
360
+ publishStoreValue(store, 'setVisibleColumns', nextVisibleColumns, 'visibleColumns');
361
+ }, [columnsForTable, store, tablePreferenceScope]);
1281
362
 
1282
- const openAddForm = useCallback(() => {
1283
- if (typeof onAdd === 'function') {
1284
- onAdd();
363
+ useEffect(() => {
364
+ if (!columnsForTable.length || Object.keys(storeFilters).length > 0) {
1285
365
  return;
1286
366
  }
1287
367
 
1288
- if (typeof resolvedActions.save !== 'function') return;
1289
-
1290
- setFormMode('create');
1291
- setEditingRow({});
1292
- }, [onAdd, resolvedActions]);
1293
-
1294
- const closeEditModal = useCallback(() => {
1295
- setEditingRow(null);
1296
- setFormMode('edit');
1297
- }, []);
1298
-
1299
- const commitFilters = useCallback(nextFilters => {
1300
- const resolvedNextFilters = isObject(nextFilters) ? nextFilters : {};
1301
-
1302
- if (autoMode) {
1303
- setAutoFilters(resolvedNextFilters);
1304
- resolvedActions.setFilters?.(resolvedNextFilters);
368
+ const storedFilters = resolveStoredTableFiltersPreference(tablePreferenceScope);
369
+ if (!storedFilters) {
1305
370
  return;
1306
371
  }
1307
372
 
1308
- onFilterChange?.(resolvedNextFilters);
1309
- }, [autoMode, onFilterChange, resolvedActions]);
1310
-
1311
- const updateFilter = useCallback((fieldName, value) => {
1312
- const nextFilters = { ...(resolvedFilters || {}) };
1313
- const isEmpty =
1314
- value === null ||
1315
- value === undefined ||
1316
- normalizeText(value) === '' ||
1317
- (Array.isArray(value) && value.length === 0);
1318
-
1319
- if (isEmpty) delete nextFilters[fieldName];
1320
- else nextFilters[fieldName] = value;
1321
-
1322
- commitFilters(nextFilters);
1323
- }, [commitFilters, resolvedFilters]);
1324
-
1325
- const toggleColumn = useCallback(column => {
1326
- const fieldName = getColumnKey(column);
1327
- if (!fieldName) return;
1328
-
1329
- setVisibleColumns(prev => {
1330
- const next = {
1331
- ...sanitizeVisibleColumnsPreference({
1332
- columns: columnsForTable,
1333
- visibleColumns: prev,
1334
- }),
1335
- [fieldName]: prev[fieldName] === false,
1336
- };
1337
-
1338
- resolvedActions?.setVisibleColumns?.(next);
1339
- if (visibleColumnsStorageKey) {
1340
- persistVisibleColumnsPreference(
1341
- visibleColumnsStorageKey,
1342
- next,
1343
- );
1344
- }
1345
- return next;
373
+ const nextFilters = sanitizeTableFiltersPreference({
374
+ columns: columnsForTable,
375
+ filters: storedFilters,
1346
376
  });
1347
- }, [
1348
- resolvedActions,
1349
- columnsForTable,
1350
- visibleColumnsStorageKey,
1351
- ]);
1352
-
1353
- const toggleViewMode = useCallback(() => {
1354
- const currentViewMode =
1355
- isCompactView && shouldForceCardsOnCompact
1356
- ? (compactViewMode || 'cards')
1357
- : viewMode;
1358
- const nextViewMode = currentViewMode === 'table' ? 'cards' : 'table';
1359
-
1360
- if (isCompactView && shouldForceCardsOnCompact) {
1361
- setCompactViewMode(nextViewMode);
1362
- }
1363
-
1364
- setViewMode(nextViewMode);
1365
-
1366
- if (!visibleColumnsPreferenceKey) {
1367
- return;
1368
- }
1369
377
 
1370
- persistTableViewModePreference(
1371
- visibleColumnsPreferenceKey,
1372
- nextViewMode,
1373
- );
1374
- }, [
1375
- compactViewMode,
1376
- isCompactView,
1377
- shouldForceCardsOnCompact,
1378
- viewMode,
1379
- visibleColumnsPreferenceKey,
1380
- ]);
1381
-
1382
- const handleEndReached = useCallback(() => {
1383
- if (
1384
- !resolvedHasMore ||
1385
- resolvedIsLoading ||
1386
- endReachedLockRef.current === true
1387
- ) {
378
+ if (Object.keys(nextFilters).length === 0) {
1388
379
  return;
1389
380
  }
1390
381
 
1391
- endReachedLockRef.current = true;
382
+ publishStoreValue(store, 'setFilters', nextFilters, 'filters');
383
+ }, [columnsForTable, store, storeFilters, tablePreferenceScope]);
1392
384
 
1393
- if (autoMode) {
1394
- const nextPage = autoPageRef.current + 1;
1395
- loadAutoPage(nextPage, { append: true });
385
+ useEffect(() => {
386
+ if (configsSignatureRef.current === defaultTableConfigsSignature) {
1396
387
  return;
1397
388
  }
1398
389
 
1399
- if (typeof onEndReached === 'function') {
1400
- onEndReached();
1401
- }
1402
- }, [autoMode, loadAutoPage, onEndReached, resolvedHasMore, resolvedIsLoading]);
1403
-
1404
- const handleLayout = useCallback(event => {
1405
- const nextWidth = Math.floor(event?.nativeEvent?.layout?.width || 0);
1406
- if (nextWidth > 0) setTableContainerWidth(nextWidth);
1407
- }, []);
1408
-
1409
- const renderEditableCell = (row, column) => {
1410
- const fieldName = getColumnKey(column);
1411
- const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1412
- const isEditing = editingCell === cellKey;
1413
- const isSaving = savingCell === cellKey;
1414
- const shouldDelegatePress =
1415
- typeof onRowPress === 'function' &&
1416
- !isEditableColumn(column);
1417
-
1418
- return (
1419
- <View
1420
- style={[getColumnStyle(column), isEditing ? styles.editingCell : null]}
1421
- pointerEvents={shouldDelegatePress ? 'none' : 'auto'}
1422
- >
1423
- <DefaultInput
1424
- accentColor={resolvedAccentColor}
1425
- column={column}
1426
- columns={columnsForTable}
1427
- editing={isEditing}
1428
- getOptionsForColumn={resolvedGetOptionsForColumn}
1429
- onBeforeOpen={() => loadListOptionsForColumns([column])}
1430
- onCancelEditing={clearEdit}
1431
- onSave={value => saveCell(row, column, value)}
1432
- onStartEditing={() => beginEdit(row, column)}
1433
- row={row}
1434
- saving={isSaving}
1435
- storeName={storeName}
1436
- variant="cell"
1437
- />
1438
- </View>
1439
- );
1440
- };
1441
-
1442
- const renderColumnFilter = column => {
1443
- return (
1444
- <DefaultColumnFilter
1445
- accentColor={resolvedAccentColor}
1446
- column={column}
1447
- filters={resolvedFilters}
1448
- onBeforeOpen={column?.list ? () => loadListOptionsForColumns([column]) : null}
1449
- getOptionsForColumn={resolvedGetOptionsForColumn}
1450
- onChange={updateFilter}
1451
- storeName={storeName}
1452
- style={getColumnStyle(column)}
1453
- />
1454
- );
1455
- };
1456
-
1457
- const renderToolbarAction = action => {
1458
- if (!action || action.hidden) return null;
1459
-
1460
- const isActive = action.active === true;
1461
- const hasLabel = normalizeText(action.label) !== '';
1462
- const actionColor = action.color || tableActionTextColor;
1463
-
1464
- return (
1465
- <TouchableOpacity
1466
- key={action.key || action.icon || action.label}
1467
- accessibilityRole="button"
1468
- accessibilityLabel={action.accessibilityLabel || action.label || action.key || action.icon}
1469
- style={[
1470
- styles.toolbarButton,
1471
- { backgroundColor: tableActionBackgroundColor, borderColor: tableActionBorderColor },
1472
- action.style,
1473
- ]}
1474
- activeOpacity={0.82}
1475
- disabled={action.disabled === true}
1476
- onPress={action.onPress}
1477
- >
1478
- {action.icon ? (
1479
- <Icon
1480
- name={action.icon}
1481
- size={action.iconSize || 14}
1482
- color={actionColor}
1483
- />
1484
- ) : null}
1485
- {hasLabel ? (
1486
- <Text style={[styles.toolbarActionLabel, { color: actionColor }, action.labelStyle]} numberOfLines={1}>
1487
- {action.label}
1488
- </Text>
1489
- ) : null}
1490
- {action.badge !== undefined && action.badge !== null ? (
1491
- <Text style={[styles.toolbarBadgeText, { color: action.badgeColor || tableActionTextColor }]}>
1492
- {action.badge}
1493
- </Text>
1494
- ) : null}
1495
- </TouchableOpacity>
1496
- );
1497
- };
1498
-
1499
- const renderToolbarActions = () =>
1500
- Array.isArray(toolbarActions) && toolbarActions.length > 0 ? (
1501
- <View style={styles.toolbarActionGroup}>{toolbarActions.map(renderToolbarAction)}</View>
1502
- ) : null;
1503
-
1504
- const getColumnByField = useCallback(
1505
- fieldName => columnsForTable.find(column => getColumnKey(column) === fieldName),
1506
- [columnsForTable],
1507
- );
1508
-
1509
- const buildRowHelpers = useCallback(
1510
- row => {
1511
- const openEdit = () => openEditModal(row);
1512
- const openRow = typeof onRowPress === 'function' ? () => onRowPress(row) : null;
1513
- const renderValue = (fieldName, fallback = '-') => {
1514
- const column = getColumnByField(fieldName);
1515
- if (!column) return fallback;
1516
- return resolveCellText({ column, columns: columnsForTable, row, storeName });
1517
- };
1518
- const renderField = (fieldName, options = {}) => {
1519
- const column = getColumnByField(fieldName);
1520
- if (!column) return null;
1521
-
1522
- const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1523
- const isEditing = editingCell === cellKey;
1524
- const isSaving = savingCell === cellKey;
1525
-
1526
- return (
1527
- <DefaultInput
1528
- accentColor={options.accentColor || resolvedAccentColor}
1529
- column={column}
1530
- columns={columnsForTable}
1531
- containerStyle={options.containerStyle}
1532
- displayValue={options.displayValue}
1533
- editing={isEditing}
1534
- getOptionsForColumn={resolvedGetOptionsForColumn}
1535
- inputStyle={options.inputStyle}
1536
- label={options.label}
1537
- numberOfLines={options.numberOfLines}
1538
- onBeforeOpen={() => loadListOptionsForColumns([column])}
1539
- onCancelEditing={clearEdit}
1540
- onSave={value => saveCell(row, column, value)}
1541
- onStartEditing={() => beginEdit(row, column)}
1542
- readTextStyle={options.readTextStyle || options.textStyle}
1543
- row={row}
1544
- saving={isSaving}
1545
- showLabel={options.showLabel}
1546
- storeName={storeName}
1547
- variant={options.variant || 'card'}
1548
- />
1549
- );
1550
- };
1551
-
1552
- return {
1553
- openEdit,
1554
- openRow,
1555
- renderField,
1556
- renderValue,
1557
- };
1558
- },
1559
- [
1560
- resolvedAccentColor,
1561
- beginEdit,
1562
- clearEdit,
1563
- columnsForTable,
1564
- editingCell,
1565
- getColumnByField,
1566
- resolvedGetOptionsForColumn,
1567
- openEditModal,
1568
- onRowPress,
1569
- saveCell,
1570
- savingCell,
1571
- storeName,
1572
- ],
1573
- );
1574
-
1575
- const resolveRowStyle = useCallback(
1576
- (row, index) => {
1577
- if (typeof rowStyle === 'function') {
1578
- return rowStyle(row, index);
1579
- }
390
+ configsSignatureRef.current = defaultTableConfigsSignature;
391
+ publishStoreValue(store, 'setConfigs', defaultTableConfigs, 'configs');
392
+ }, [defaultTableConfigs, defaultTableConfigsSignature, store]);
1580
393
 
1581
- return rowStyle;
1582
- },
1583
- [rowStyle],
1584
- );
1585
-
1586
- const renderCardItem = (row, index = 0) => {
1587
- const helpers = buildRowHelpers(row);
1588
- const rowStyleValue = resolveRowStyle(row, index);
1589
-
1590
- if (typeof renderCard === 'function') {
1591
- return (
1592
- <View key={row?.['@id'] || row?.id} style={[styles.cardItem, rowStyleValue]}>
1593
- {renderCard({
1594
- item: row,
1595
- openEdit: helpers.openEdit,
1596
- openRow: helpers.openRow,
1597
- renderField: helpers.renderField,
1598
- renderValue: helpers.renderValue,
1599
- row,
1600
- })}
1601
- {hasRowActions ? (
1602
- <View style={styles.cardActions}>
1603
- <TouchableOpacity
1604
- style={[styles.iconButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1605
- activeOpacity={0.82}
1606
- onPress={helpers.openEdit}>
1607
- <Icon name="edit-2" size={14} color={tableMutedColor} />
1608
- </TouchableOpacity>
1609
- </View>
1610
- ) : null}
1611
- </View>
1612
- );
1613
- }
1614
-
1615
- return (
1616
- <View
1617
- key={row?.['@id'] || row?.id}
1618
- style={[
1619
- styles.defaultCard,
1620
- { backgroundColor: tableSurfaceColor, borderColor: tableBorderColor },
1621
- rowStyleValue,
1622
- ]}
1623
- >
1624
- {tableColumns.map(column => (
1625
- <View key={getColumnKey(column)} style={styles.defaultCardLine}>
1626
- <Text style={[styles.defaultCardLabel, { color: tableMutedColor }]}>
1627
- {formatStoreColumnLabel({
1628
- columns: columnsForTable,
1629
- fieldName: getColumnKey(column),
1630
- fallbackLabel: column?.label || getColumnKey(column),
1631
- storeName,
1632
- })}
1633
- </Text>
1634
- {helpers.renderField(getColumnKey(column), {
1635
- readTextStyle: [styles.defaultCardValue, { color: tableTextColor }],
1636
- numberOfLines: 1,
1637
- })}
1638
- </View>
1639
- ))}
1640
- {hasRowActions ? (
1641
- <TouchableOpacity
1642
- style={[styles.cardEditButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1643
- activeOpacity={0.82}
1644
- onPress={helpers.openEdit}>
1645
- <Icon name="edit-2" size={14} color={tableMutedColor} />
1646
- </TouchableOpacity>
1647
- ) : null}
1648
- </View>
1649
- );
1650
- };
1651
-
1652
- const renderEmptyState = isTable => {
1653
- return (
1654
- <View style={[styles.emptyBox, isTable ? tableLayoutStyle : null]}>
1655
- {resolvedIsLoading ? (
1656
- <StateStore compact loading={emptyStateLabel} />
1657
- ) : (
1658
- <Text style={[styles.emptyText, { color: tableMutedColor }]}>{emptyStateLabel}</Text>
1659
- )}
1660
- </View>
1661
- );
1662
- };
394
+ useEffect(() => {
395
+ onDataLoaded?.(sortedData);
396
+ }, [onDataLoaded, sortedData]);
1663
397
 
1664
- const renderLoadingOverlay = () => {
1665
- if (!resolvedIsLoading || sortedData.length === 0) {
1666
- return null;
1667
- }
1668
-
1669
- return (
1670
- <View pointerEvents="none" style={styles.loadingOverlay}>
1671
- <View
398
+ return (
399
+ <View
400
+ style={[
401
+ styles.wrap,
402
+ {
403
+ borderColor: themeColors.panelBorder,
404
+ backgroundColor: themeColors.panelBackground,
405
+ },
406
+ ]}
407
+ >
408
+ {showToolbar !== false ? <DefaultTableToolbar storeName={storeName} /> : null}
409
+ <DefaultTableBody storeName={storeName} />
410
+ <DefaultTableFooter storeName={storeName} />
411
+ {shouldRenderFloatingAddButton ? (
412
+ <TouchableOpacity
413
+ accessibilityRole="button"
414
+ accessibilityLabel={global.t?.t(storeName, 'button', 'add') || 'Adicionar'}
415
+ activeOpacity={0.84}
1672
416
  style={[
1673
- styles.loadingOverlayCard,
1674
- { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor },
417
+ styles.floatingAddButton,
418
+ { backgroundColor: floatingAddBackgroundColor },
1675
419
  ]}
420
+ onPress={() => onAdd?.()}
1676
421
  >
1677
- <StateStore compact loading="Carregando mais registros..." />
1678
- </View>
1679
- </View>
1680
- );
1681
- };
1682
-
1683
- const renderTableItem = ({ item: row, index }) => {
1684
- const hasRowPress = typeof onRowPress === 'function';
1685
- const RowComponent = hasRowPress ? TouchableOpacity : View;
1686
- const rowStyleValue = resolveRowStyle(row, index);
1687
- const rowPressProps = hasRowPress
1688
- ? {
1689
- activeOpacity: 0.84,
1690
- onPress: () => onRowPress(row),
1691
- }
1692
- : {};
1693
- const rowBackgroundColor = index % 2 === 0 ? tableOddColor : tableEvenColor;
1694
-
1695
- return (
1696
- <RowComponent
1697
- key={getRowKey(row)}
1698
- style={[
1699
- styles.row,
1700
- tableLayoutStyle,
1701
- { backgroundColor: rowBackgroundColor, borderBottomColor: tableBorderColor },
1702
- rowStyleValue,
1703
- ]}
1704
- {...rowPressProps}
1705
- >
1706
- {tableColumns.map(column => (
1707
- <React.Fragment key={getColumnKey(column)}>
1708
- {renderEditableCell(row, column)}
1709
- </React.Fragment>
1710
- ))}
1711
- {hasRowActions ? (
1712
- <View style={[styles.cell, styles.actionsCell]}>
1713
- <TouchableOpacity
1714
- style={[styles.iconButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1715
- activeOpacity={0.82}
1716
- onPress={() => openEditModal(row)}>
1717
- <Icon name="edit-2" size={14} color={tableMutedColor} />
1718
- </TouchableOpacity>
1719
- </View>
1720
- ) : null}
1721
- </RowComponent>
1722
- );
1723
- };
1724
-
1725
- const renderEditModal = () => {
1726
- const isCreate = formMode === 'create';
1727
-
1728
- return (
1729
- <Modal visible={Boolean(editingRow)} transparent animationType="fade" onRequestClose={closeEditModal}>
1730
- <View style={[styles.modalOverlay, { backgroundColor: modalOverlayColor }]}>
1731
- <View style={[styles.modalCard, { borderColor: modalBorderColor, backgroundColor: modalBackgroundColor }]}>
1732
- <View style={[styles.modalHeader, { borderBottomColor: modalBorderColor }]}>
1733
- <Text style={[styles.modalTitle, { color: modalHeaderTextColor }]}>
1734
- {isCreate
1735
- ? global.t?.t(storeName, 'button', 'add') || 'Adicionar'
1736
- : global.t?.t(storeName, 'button', 'edit') || 'Editar'}
1737
- </Text>
1738
- <TouchableOpacity
1739
- style={[styles.modalCloseButton, { borderColor: modalBorderColor, backgroundColor: modalBackgroundColor }]}
1740
- onPress={closeEditModal}>
1741
- <Icon name="x" size={18} color={modalCloseIconColor} />
1742
- </TouchableOpacity>
1743
- </View>
1744
-
1745
- <DefaultForm
1746
- accentColor={resolvedAccentColor}
1747
- actions={resolvedActions}
1748
- columns={isCreate ? columnsForTable : editableColumns}
1749
- getOptionsForColumn={resolvedGetOptionsForColumn}
1750
- mode={formMode}
1751
- onBeforeOpen={column => loadListOptionsForColumns([column])}
1752
- onCancel={closeEditModal}
1753
- onSaved={(savedItem, originalRow) => {
1754
- onSaved?.(savedItem, originalRow);
1755
- closeEditModal();
1756
- }}
1757
- row={editingRow || {}}
1758
- storeName={storeName}
1759
- />
1760
- </View>
1761
- </View>
1762
- </Modal>
1763
- );
1764
- };
1765
-
1766
- const renderColumnMenuModal = () => {
1767
- if (!isColumnMenuOpen) return null;
1768
-
1769
- return (
1770
- <Modal visible transparent animationType="fade" onRequestClose={() => setIsColumnMenuOpen(false)}>
1771
- <View style={[styles.modalOverlay, { backgroundColor: modalOverlayColor }]}>
1772
- <View style={[styles.modalCard, styles.columnMenuModalCard, { borderColor: modalBorderColor, backgroundColor: modalBackgroundColor }]}>
1773
- <View style={[styles.modalHeader, { borderBottomColor: modalBorderColor }]}>
1774
- <Text style={[styles.modalTitle, { color: modalHeaderTextColor }]} numberOfLines={1}>
1775
- Colunas
1776
- </Text>
1777
- <TouchableOpacity
1778
- style={[styles.modalCloseButton, { borderColor: modalBorderColor, backgroundColor: modalBackgroundColor }]}
1779
- activeOpacity={0.82}
1780
- onPress={() => setIsColumnMenuOpen(false)}
1781
- >
1782
- <Icon name="x" size={16} color={modalCloseIconColor} />
1783
- </TouchableOpacity>
1784
- </View>
1785
- <ScrollView style={styles.columnMenuModalBody} contentContainerStyle={styles.columnMenuModalList}>
1786
- {availableColumns.map(column => {
1787
- const fieldName = getColumnKey(column);
1788
- const label = formatStoreColumnLabel({
1789
- columns: columnsForTable,
1790
- fieldName,
1791
- fallbackLabel: column?.label || fieldName,
1792
- storeName,
1793
- });
1794
- const checked = visibleColumns[fieldName] !== false;
1795
-
1796
- return (
1797
- <TouchableOpacity key={fieldName} style={styles.columnMenuItem} activeOpacity={0.82} onPress={() => toggleColumn(column)}>
1798
- <Icon name={checked ? 'check-square' : 'square'} size={16} color={checked ? checkboxSelectedMarkColor : checkboxBorderColor} />
1799
- <Text style={[styles.columnMenuText, { color: modalTextColor }]} numberOfLines={1}>{label}</Text>
1800
- </TouchableOpacity>
1801
- );
1802
- })}
1803
- </ScrollView>
1804
- </View>
1805
- </View>
1806
- </Modal>
1807
- );
1808
- };
1809
-
1810
- return (
1811
- <View style={[styles.wrap, { borderColor: panelBorderColor, backgroundColor: panelBackgroundColor }]} onLayout={handleLayout}>
1812
- <View style={[styles.toolbar, { borderBottomColor: toolbarBorderColor, backgroundColor: toolbarBackgroundColor }]}>
1813
- {shouldRenderCompactToolbarTotalItems ? (
1814
- <View style={styles.toolbarCompactLead}>
1815
- <View style={[styles.toolbarCountPill, { backgroundColor: toolbarCountBackgroundColor }]}>
1816
- <Text
1817
- style={[styles.toolbarCountText, { color: toolbarCountTextColor }]}
1818
- numberOfLines={1}
1819
- adjustsFontSizeToFit
1820
- minimumFontScale={0.82}
1821
- >
1822
- {resolvedTotalItemsText}
1823
- </Text>
1824
- </View>
1825
- {searchProps ? (
1826
- <DefaultSearch
1827
- accentColor={resolvedAccentColor}
1828
- compact
1829
- storeName={storeName}
1830
- {...searchProps}
1831
- filters={autoMode ? resolvedFilters : searchProps?.filters}
1832
- onChangeFilters={autoMode ? commitFilters : searchProps?.onChangeFilters}
1833
- value={autoMode ? resolvedSearchValue : searchProps?.value}
1834
- style={[styles.toolbarSearch, styles.toolbarCompactSearch, searchProps?.style]}
1835
- />
1836
- ) : null}
1837
- {renderToolbarActions()}
1838
- </View>
1839
- ) : null}
1840
-
1841
- <View style={shouldRenderCompactToolbarTotalItems ? styles.toolbarCompactActions : styles.toolbarLeft}>
1842
- {!shouldRenderCompactToolbarTotalItems && searchProps ? (
1843
- <DefaultSearch
1844
- accentColor={resolvedAccentColor}
1845
- compact
1846
- storeName={storeName}
1847
- {...searchProps}
1848
- filters={autoMode ? resolvedFilters : searchProps?.filters}
1849
- onChangeFilters={autoMode ? commitFilters : searchProps?.onChangeFilters}
1850
- value={autoMode ? resolvedSearchValue : searchProps?.value}
1851
- style={[styles.toolbarSearch, searchProps?.style]}
1852
- />
1853
- ) : null}
1854
- {!shouldRenderCompactToolbarTotalItems ? renderToolbarActions() : null}
1855
- {showColumnFiltersButton ? (
1856
- <TouchableOpacity
1857
- style={[
1858
- styles.toolbarButton,
1859
- { borderColor: tableButtonBorderColor, backgroundColor: tableButtonBackgroundColor },
1860
- showColumnFilters
1861
- ? { backgroundColor: tableButtonPressedBackgroundColor, borderColor: tableButtonPressedBorderColor }
1862
- : null,
1863
- ]}
1864
- activeOpacity={0.82}
1865
- onPress={() => setShowColumnFilters(prev => !prev)}
1866
- >
1867
- <Icon
1868
- name="filter"
1869
- size={14}
1870
- color={showColumnFilters ? tableButtonPressedIconColor : tableButtonIconColor}
1871
- />
1872
- {activeFilterCount > 0 ? (
1873
- <Text
1874
- style={[
1875
- styles.toolbarBadgeText,
1876
- { color: showColumnFilters ? tableButtonPressedIconColor : tableButtonIconColor },
1877
- ]}>
1878
- {activeFilterCount}
1879
- </Text>
1880
- ) : null}
1881
- </TouchableOpacity>
1882
- ) : null}
1883
- <TouchableOpacity
1884
- style={[
1885
- styles.toolbarButton,
1886
- { borderColor: tableButtonBorderColor, backgroundColor: tableButtonBackgroundColor },
1887
- effectiveViewMode !== 'table'
1888
- ? { backgroundColor: tableButtonPressedBackgroundColor, borderColor: tableButtonPressedBorderColor }
1889
- : null,
1890
- ]}
1891
- activeOpacity={0.82}
1892
- onPress={toggleViewMode}
1893
- >
1894
- <Icon
1895
- name={effectiveViewMode === 'table' ? 'list' : 'grid'}
1896
- size={14}
1897
- color={effectiveViewMode !== 'table' ? tableButtonPressedIconColor : tableButtonIconColor}
1898
- />
1899
- </TouchableOpacity>
1900
- <TouchableOpacity
1901
- style={[styles.toolbarButton, { borderColor: tableButtonBorderColor, backgroundColor: tableButtonBackgroundColor }]}
1902
- activeOpacity={0.82}
1903
- onPress={() => setIsColumnMenuOpen(prev => !prev)}>
1904
- <Icon name="columns" size={14} color={tableButtonTextColor} />
1905
- </TouchableOpacity>
1906
- {shouldRenderAddButton ? (
1907
- <TouchableOpacity
1908
- style={[
1909
- styles.toolbarButton,
1910
- styles.toolbarAddButton,
1911
- { backgroundColor: tableButtonBackgroundColor, borderColor: tableButtonBackgroundColor },
1912
- ]}
1913
- activeOpacity={0.85}
1914
- onPress={openAddForm}
1915
- >
1916
- <Icon name="plus" size={16} color={tableButtonTextColor} />
1917
- </TouchableOpacity>
1918
- ) : null}
1919
- </View>
1920
-
1921
- </View>
1922
-
1923
- {effectiveViewMode === 'cards' ? (
1924
- <FlatList
1925
- data={sortedData}
1926
- keyExtractor={getRowKey}
1927
- renderItem={({ item, index }) => renderCardItem(item, index)}
1928
- style={styles.cardsScroll}
1929
- contentContainerStyle={styles.cardsGrid}
1930
- ListEmptyComponent={renderEmptyState(false)}
1931
- ListFooterComponent={null}
1932
- nestedScrollEnabled
1933
- onMomentumScrollBegin={onMomentumScrollBegin || undefined}
1934
- onScrollBeginDrag={onScrollBeginDrag || undefined}
1935
- onEndReached={handleEndReached}
1936
- onEndReachedThreshold={END_REACHED_THRESHOLD}
1937
- showsVerticalScrollIndicator={false}
1938
- />
1939
- ) : (
1940
- <ScrollView horizontal style={styles.scroll}>
1941
- <View style={[styles.content, tableLayoutStyle]}>
1942
- <View style={[styles.headerRow, tableLayoutStyle, { backgroundColor: tableHeaderColor, borderBottomColor: tableBorderColor }]}>
1943
- {tableColumns.map(column => {
1944
- const fieldName = getColumnKey(column);
1945
- const label = formatStoreColumnLabel({
1946
- columns: columnsForTable,
1947
- fieldName,
1948
- fallbackLabel: column?.label || fieldName,
1949
- storeName,
1950
- });
1951
-
1952
- const sortFieldName = getSortField(column);
1953
-
1954
- return (
1955
- <TouchableOpacity
1956
- key={fieldName}
1957
- style={getColumnStyle(column)}
1958
- activeOpacity={isSortableColumn(column) ? 0.8 : 1}
1959
- onPress={() => requestSort(column)}
1960
- >
1961
- <View style={styles.sortableHeader}>
1962
- <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={1}>{label}</Text>
1963
- {isSortableColumn(column) && resolvedSort?.field === sortFieldName ? (
1964
- <Icon name={resolvedSort?.direction === 'desc' ? 'chevron-down' : 'chevron-up'} size={12} color={tableTextColor} />
1965
- ) : isSortableColumn(column) ? (
1966
- <Icon name="chevrons-up" size={12} color={tableBorderColor} />
1967
- ) : null}
1968
- {resolvedFilters?.[fieldName] ? <Icon name="filter" size={11} color={tableTextColor} /> : null}
1969
- </View>
1970
- </TouchableOpacity>
1971
- );
1972
- })}
1973
- {hasRowActions ? (
1974
- <View style={[styles.cell, styles.actionsCell]}>
1975
- <Text style={[styles.headerText, { color: tableTextColor }]}>Acoes</Text>
1976
- </View>
1977
- ) : null}
1978
- </View>
1979
-
1980
- {showColumnFiltersButton && showColumnFilters ? (
1981
- <View style={[styles.filterRow, tableLayoutStyle, { backgroundColor: tableSurfaceColor, borderBottomColor: tableBorderColor }]}>
1982
- {tableColumns.map(column => (
1983
- <React.Fragment key={getColumnKey(column)}>
1984
- {renderColumnFilter(column)}
1985
- </React.Fragment>
1986
- ))}
1987
- {hasRowActions ? <View style={[styles.cell, styles.actionsCell]} /> : null}
1988
- </View>
1989
- ) : null}
1990
-
1991
- <FlatList
1992
- data={sortedData}
1993
- keyExtractor={getRowKey}
1994
- renderItem={renderTableItem}
1995
- style={styles.tableList}
1996
- contentContainerStyle={styles.tableListContent}
1997
- ListEmptyComponent={renderEmptyState(true)}
1998
- ListFooterComponent={null}
1999
- nestedScrollEnabled
2000
- onMomentumScrollBegin={onMomentumScrollBegin || undefined}
2001
- onScrollBeginDrag={onScrollBeginDrag || undefined}
2002
- onEndReached={handleEndReached}
2003
- onEndReachedThreshold={END_REACHED_THRESHOLD}
2004
- showsVerticalScrollIndicator={false}
2005
- />
2006
- </View>
2007
- </ScrollView>
2008
- )}
2009
-
2010
- {renderLoadingOverlay()}
2011
-
2012
- {footerComponent ? (
2013
- React.isValidElement(footerComponent) ? (
2014
- footerComponent
2015
- ) : (
2016
- React.createElement(footerComponent, footerProps)
2017
- )
2018
- ) : shouldRenderFooterBar ? (
2019
- <View style={[styles.footerBar, { backgroundColor: tableSurfaceColor, borderTopColor: tableBorderColor }]}>
2020
- {summaryEntries.length > 0 ? (
2021
- <View style={styles.footerSummaryList}>
2022
- {summaryEntries.map(entry => (
2023
- <View key={entry.key} style={styles.footerSummaryItem}>
2024
- <Text style={[styles.footerSummaryLabel, { color: tableMutedColor }]} numberOfLines={1}>
2025
- {entry.label}
2026
- </Text>
2027
- <Text
2028
- style={[
2029
- styles.footerSummaryValue,
2030
- entry.path?.[0] === 'sum' || isMoneySummaryPath(entry.path)
2031
- ? { color: resolvedAccentColor }
2032
- : { color: tableTextColor },
2033
- ]}
2034
- numberOfLines={1}
2035
- adjustsFontSizeToFit
2036
- minimumFontScale={0.78}
2037
- >
2038
- {formatSummaryValue({
2039
- column: entry.column,
2040
- columns: columnsForTable,
2041
- path: entry.path,
2042
- storeName,
2043
- value: entry.value,
2044
- })}
2045
- </Text>
2046
- </View>
2047
- ))}
2048
- </View>
2049
- ) : null}
2050
- {shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems ? (
2051
- <View style={[styles.footerCountPill, { backgroundColor: toolbarCountBackgroundColor }]}>
2052
- <Text
2053
- style={[styles.footerCountText, { color: toolbarCountTextColor }]}
2054
- numberOfLines={1}
2055
- adjustsFontSizeToFit
2056
- minimumFontScale={0.82}
2057
- >
2058
- {resolvedTotalItemsText}
2059
- </Text>
2060
- </View>
2061
- ) : null}
2062
- </View>
422
+ <Icon name="plus" size={24} color={floatingAddIconColor} />
423
+ </TouchableOpacity>
2063
424
  ) : null}
2064
-
2065
- {renderColumnMenuModal()}
2066
- {renderEditModal()}
2067
- </View>
2068
- );
2069
- };
2070
-
2071
- export default DefaultTable;
425
+ </View>
426
+ );
427
+ };
428
+
429
+ export default DefaultTable;