@controleonline/ui-default 1.0.264 → 1.0.265

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 +233 -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 +350 -2025
  22. package/src/react/components/table/DefaultTable.styles.js +217 -97
  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 +208 -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 +340 -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 +58 -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 +945 -96
  45. package/src/tests/react/store/defaultActions.test.js +59 -0
  46. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,2069 +1,394 @@
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';
15
- 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';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { View, useWindowDimensions } from 'react-native';
3
+ import { useIsFocused, useRoute } from '@react-navigation/native';
4
+ import { useStore } from '@store';
5
+ import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
6
+ import { normalizeText } from '../inputs/defaultInputUtils';
7
+ import { DEFAULT_COMPACT_BREAKPOINT, isObject, stableSerialize } from './DefaultTable.utils';
26
8
  import {
27
- formatSaveValue,
28
- getColumnKey,
29
- isEditableColumn,
30
- normalizeId,
31
- normalizeOptionKey,
32
- normalizeText,
33
- resolveStoreNameFromList,
34
- resolveCellText,
35
- resolveEditValue,
36
- } from '../inputs/defaultInputUtils';
37
- import {
38
- persistTableSortPreference,
39
- persistTableViewModePreference,
40
- persistVisibleColumnsPreference,
41
- resolveStoredTableSortPreference,
42
- resolveStoredTableViewModePreference,
43
- resolveStoredVisibleColumnsPreference,
44
- sanitizeVisibleColumnsPreference,
45
- } from '../../utils/tableVisibleColumnsPreferences';
9
+ resolveDefaultTablePreferenceScope,
10
+ resolveStoredTableFiltersPreference,
11
+ resolveStoredTableViewModePreference,
12
+ resolveStoredVisibleColumnsPreference,
13
+ sanitizeTableFiltersPreference,
14
+ sanitizeVisibleColumnsPreference,
15
+ } from '../../utils/tableVisibleColumnsPreferences';
16
+ import DefaultTableBody from './DefaultTableBody';
17
+ import DefaultTableFooter from './DefaultTableFooter';
18
+ import DefaultTableToolbar from './DefaultTableToolbar';
46
19
  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);
160
- }
161
-
162
- if (value && typeof value === 'object') {
163
- return resolveFilterQueryValue(
164
- value.value ?? value.id ?? value['@id'] ?? value.key ?? '',
165
- );
166
- }
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
- };
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);
20
+ import { useDefaultTablePagination } from './useDefaultTablePagination';
21
+ import {
22
+ useDefaultTableSortedData,
23
+ useDefaultTableSortState,
24
+ } from './useDefaultTableSorting';
25
+ import useDefaultTableTheme from './useDefaultTableTheme';
26
+
27
+ export {
28
+ mergeSortedDataWithLiveItems,
29
+ resolveColumnListLoadParams,
30
+ shouldTriggerEndReachedFromScroll,
31
+ } from './DefaultTable.utils';
32
+
33
+ const publishStoreValue = (store, actionName, value, getterName) => {
34
+ if (typeof store?.actions?.[actionName] === 'function') {
35
+ store.actions[actionName](value);
36
+ return;
225
37
  }
226
38
 
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;
39
+ if (store?.getters) {
40
+ store.getters[getterName] = value;
234
41
  }
235
-
236
- return getColumnKey(column);
237
42
  };
238
43
 
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));
44
+ const assignGetterValue = (store, getterName, value) => {
45
+ if (store?.getters) {
46
+ store.getters[getterName] = value;
245
47
  }
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
48
  };
259
49
 
260
- const flattenSummaryEntries = ({
261
- path = [],
50
+ const DefaultTable = ({
51
+ accentColor = null,
52
+ actions = {},
53
+ add = null,
54
+ compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
55
+ columns = [],
56
+ data = undefined,
57
+ filters = {},
58
+ footerComponent = null,
59
+ forceCardsOnCompact = true,
60
+ getOptionsForColumn = null,
61
+ hasMore = null,
62
+ initialViewMode = 'table',
63
+ isLoading = false,
64
+ onAdd = null,
65
+ onDataLoaded = null,
66
+ onEditRow = null,
67
+ onEndReached = null,
68
+ onFilterChange = null,
69
+ onMomentumScrollBegin = null,
70
+ onRowPress = null,
71
+ onSaved = null,
72
+ onScrollBeginDrag = null,
73
+ onSortChange = null,
74
+ pageSize = null,
75
+ renderCard = null,
76
+ requestParams = {},
77
+ rowActionsComponent = null,
78
+ rowStyle = null,
79
+ showColumnFiltersButton = true,
80
+ showRowActions = true,
81
+ showTotalItemsInCompactToolbar = false,
82
+ showTotalItemsInFooter = true,
83
+ sort = null,
84
+ storeName = '',
85
+ summary = undefined,
262
86
  summaryLabels = {},
263
- usedPaths,
264
- value,
87
+ toolbarActions = [],
88
+ visibleColumnsPreferenceKey = '',
265
89
  }) => {
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
-
294
- const resolveDefaultSort = columns => {
295
- if (!Array.isArray(columns)) {
296
- return null;
297
- }
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
- };
344
-
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,
90
+ const { width } = useWindowDimensions();
91
+ const route = useRoute?.();
92
+ const store = useStore(storeName);
93
+ const isFocused = useIsFocused();
94
+ const configsSignatureRef = useRef('');
95
+ const { showError } = useMessage() || {};
96
+ const { themeColors } = useDefaultTableTheme(accentColor);
97
+ const tablePreferenceScope = useMemo(
98
+ () =>
99
+ resolveDefaultTablePreferenceScope({
100
+ preferenceKey: visibleColumnsPreferenceKey,
101
+ route,
444
102
  storeName,
445
- }));
446
-
447
- const normalizedNumber = Number(
448
- String(resolvedValue).replace(/[^0-9,.-]/g, '').replace(',', '.'),
103
+ }),
104
+ [route?.key, route?.name, storeName, visibleColumnsPreferenceKey],
449
105
  );
450
-
451
- if (Number.isFinite(normalizedNumber) && String(resolvedValue).match(/[0-9]/)) {
452
- return normalizedNumber;
106
+ const storedViewMode = useMemo(
107
+ () => resolveStoredTableViewModePreference(tablePreferenceScope, null),
108
+ [initialViewMode, tablePreferenceScope],
109
+ );
110
+ const resolvedActions = useMemo(
111
+ () => ({
112
+ ...(store?.actions || {}),
113
+ ...(actions || {}),
114
+ }),
115
+ [actions, store?.actions],
116
+ );
117
+ const autoMode = data === undefined && normalizeText(storeName) !== '';
118
+ const storeColumns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
119
+ const columnsForTable = storeColumns.length > 0 ? storeColumns : columns;
120
+ const storeFilters = isObject(store?.getters?.filters) ? store.getters.filters : {};
121
+ const requestParamsSeed = isObject(requestParams) ? requestParams : {};
122
+ const resolvedTotalItems = store?.getters?.totalItems;
123
+ const currentConfigs = store?.getters?.configs || {};
124
+ const isCompactView = width > 0 && width <= compactBreakpoint;
125
+
126
+ if (storeColumns.length === 0 && Array.isArray(columns) && columns.length > 0) {
127
+ assignGetterValue(store, 'columns', columns);
453
128
  }
454
129
 
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];
130
+ if (data !== undefined && Array.isArray(data)) {
131
+ assignGetterValue(store, 'items', data);
463
132
  }
464
- return styles.cell;
465
- };
466
133
 
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
- };
134
+ const { requestSort, resolvedSort } = useDefaultTableSortState({
135
+ autoMode,
136
+ columnsForTable,
137
+ onSortChange,
138
+ sort,
139
+ tablePreferenceScope,
140
+ });
141
+ const {
142
+ buildRequestQuery,
143
+ currentPage,
144
+ handleEndReached,
145
+ resolvedData,
146
+ } = useDefaultTablePagination({
147
+ autoMode,
148
+ columnsForTable,
149
+ data,
150
+ filters: storeFilters,
151
+ hasMore,
152
+ isFocused,
153
+ isLoading,
154
+ onEndReached,
155
+ pageSize,
156
+ requestParams: requestParamsSeed,
157
+ resolvedActions,
158
+ resolvedSort,
159
+ resolvedTotalItems,
160
+ showError,
161
+ store,
162
+ storeName,
163
+ });
164
+ const sortedData = useDefaultTableSortedData({
165
+ resolvedData,
166
+ resolvedSort,
167
+ storeName,
168
+ tableColumns: columnsForTable,
169
+ });
170
+ const debugFallbackParameters = useMemo(() => {
171
+ if (autoMode) {
172
+ return buildRequestQuery(currentPage || 1, false);
173
+ }
475
174
 
476
- const DefaultTable = ({
477
- accentColor = null,
478
- actions = {},
479
- add = null,
480
- compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
481
- columns = [],
482
- data = undefined,
483
- filters = {},
484
- forceCardsOnCompact = true,
485
- getOptionsForColumn = null,
486
- hasMore = null,
487
- initialViewMode = 'table',
488
- pageSize = null,
489
- isLoading = false,
490
- onEditRow = null,
491
- onEndReached = null,
492
- onMomentumScrollBegin = null,
493
- onScrollBeginDrag = null,
494
- onAdd = null,
495
- onFilterChange = null,
496
- onRowPress = null,
497
- onSaved = null,
498
- onSortChange = null,
499
- onDataLoaded = null,
500
- requestParams = {},
501
- renderCard = null,
502
- searchProps = null,
503
- toolbarActions = [],
504
- showColumnFiltersButton = true,
505
- showTotalItemsInFooter = true,
506
- showTotalItemsInCompactToolbar = false,
507
- showRowActions = true,
508
- rowStyle = null,
509
- sort = null,
510
- storeName = '',
511
- summary = null,
512
- summaryLabels = null,
513
- totalItems = null,
514
- totalItemsLabel = null,
515
- totalItemsText = null,
516
- footerComponent = null,
517
- visibleColumnsPreferenceKey = '',
518
- }) => {
519
- const { width } = useWindowDimensions();
520
- 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(
541
- () =>
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
- ),
549
- }),
550
- [columns, store?.getters?.columns, visibleColumnsStorageKey],
551
- );
552
- const viewModeSeed = useMemo(
175
+ return {
176
+ filters: storeFilters,
177
+ requestParams: requestParamsSeed,
178
+ sort: resolvedSort || null,
179
+ };
180
+ }, [autoMode, buildRequestQuery, currentPage, requestParamsSeed, resolvedSort, storeFilters]);
181
+ const effectiveViewMode =
182
+ currentConfigs.viewMode ||
183
+ storedViewMode ||
184
+ (isCompactView && forceCardsOnCompact !== false ? 'cards' : initialViewMode);
185
+ const tableFiltersVisible = Boolean(currentConfigs.tableFiltersVisible);
186
+ const defaultTableConfigs = useMemo(
187
+ () => ({
188
+ add,
189
+ compactBreakpoint,
190
+ debugFallbackParameters,
191
+ effectiveViewMode,
192
+ filters,
193
+ footerComponent,
194
+ forceCardsOnCompact,
195
+ getOptionsForColumn,
196
+ initialViewMode,
197
+ onAdd,
198
+ onDataLoaded,
199
+ onEditRow,
200
+ onEndReached: handleEndReached,
201
+ onFilterChange,
202
+ onMomentumScrollBegin,
203
+ onRowPress,
204
+ onSaved,
205
+ onScrollBeginDrag,
206
+ renderCard,
207
+ requestParams: requestParamsSeed,
208
+ requestSort,
209
+ resolvedSort,
210
+ rowActionsComponent,
211
+ rowStyle,
212
+ showColumnFiltersButton,
213
+ showRowActions,
214
+ showTotalItemsInCompactToolbar,
215
+ showTotalItemsInFooter,
216
+ sortedData,
217
+ summary,
218
+ summaryLabels,
219
+ tableFiltersVisible,
220
+ tablePreferenceScope,
221
+ toolbarActions,
222
+ viewMode: effectiveViewMode,
223
+ }),
224
+ [
225
+ add,
226
+ compactBreakpoint,
227
+ debugFallbackParameters,
228
+ effectiveViewMode,
229
+ filters,
230
+ footerComponent,
231
+ forceCardsOnCompact,
232
+ getOptionsForColumn,
233
+ handleEndReached,
234
+ initialViewMode,
235
+ onAdd,
236
+ onDataLoaded,
237
+ onEditRow,
238
+ onFilterChange,
239
+ onMomentumScrollBegin,
240
+ onRowPress,
241
+ onSaved,
242
+ onScrollBeginDrag,
243
+ renderCard,
244
+ requestParamsSeed,
245
+ requestSort,
246
+ resolvedSort,
247
+ rowActionsComponent,
248
+ rowStyle,
249
+ showColumnFiltersButton,
250
+ showRowActions,
251
+ showTotalItemsInCompactToolbar,
252
+ showTotalItemsInFooter,
253
+ sortedData,
254
+ summary,
255
+ summaryLabels,
256
+ tableFiltersVisible,
257
+ tablePreferenceScope,
258
+ toolbarActions,
259
+ ],
260
+ );
261
+ const defaultTableConfigsSignature = useMemo(
553
262
  () =>
554
- resolveStoredTableViewModePreference(
555
- visibleColumnsPreferenceKey,
263
+ stableSerialize({
264
+ add,
265
+ compactBreakpoint,
266
+ debugFallbackParameters,
267
+ effectiveViewMode,
268
+ filters,
269
+ forceCardsOnCompact,
556
270
  initialViewMode,
557
- ),
558
- [initialViewMode, visibleColumnsPreferenceKey],
559
- );
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
- const resolvedActions = useMemo(
614
- () => ({
615
- ...storeActions,
616
- ...(actions || {}),
617
- }),
618
- [actions, storeActions],
619
- );
620
- const storeColumns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
621
- const columnsForTable = storeColumns.length > 0 ? storeColumns : columns;
622
- const storeFilters = isObject(store?.getters?.filters) ? store.getters.filters : {};
623
- 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
- }
814
-
815
- if (normalizedValue !== '') {
816
- query[fieldName] = normalizedValue;
817
- }
818
- });
819
-
820
- if (append) {
821
- query.append = true;
822
- }
823
-
824
- return query;
825
- },
826
- [autoFilters, autoSort?.direction, autoSort?.field, columnsForTable, pageSizeNumber, requestParamsSeed, searchKey],
827
- );
828
- const autoQuerySignature = useMemo(
829
- () =>
830
- stableSerialize({
831
- filters: autoFilters,
832
- pageSize: pageSizeNumber,
833
- requestParams: requestParamsSeed,
834
- searchKey,
835
- sort: autoSort,
836
- }),
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],
896
- );
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
-
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]);
931
-
932
- 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
- ) {
941
- return;
942
- }
943
-
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
- );
965
-
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],
271
+ resolvedSort,
272
+ showColumnFiltersButton,
273
+ showRowActions,
274
+ showTotalItemsInCompactToolbar,
275
+ showTotalItemsInFooter,
276
+ storedViewMode,
277
+ tableFiltersVisible,
278
+ sortedDataLength: sortedData.length,
279
+ toolbarActionsLength: Array.isArray(toolbarActions) ? toolbarActions.length : 0,
280
+ }),
281
+ [
282
+ add,
283
+ compactBreakpoint,
284
+ debugFallbackParameters,
285
+ effectiveViewMode,
286
+ filters,
287
+ forceCardsOnCompact,
288
+ initialViewMode,
289
+ resolvedSort,
290
+ showColumnFiltersButton,
291
+ showRowActions,
292
+ showTotalItemsInCompactToolbar,
293
+ showTotalItemsInFooter,
294
+ storedViewMode,
295
+ tableFiltersVisible,
296
+ sortedData.length,
297
+ toolbarActions,
298
+ ],
990
299
  );
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
- useEffect(() => {
1003
- if (!shouldForceCardsOnCompact) {
1004
- setCompactViewMode(null);
1005
- return;
1006
- }
1007
-
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
300
 
1066
- usedPaths.add(pathKey);
301
+ if (store?.getters) {
302
+ assignGetterValue(store, 'configs', defaultTableConfigs);
303
+ }
1067
304
 
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
- });
305
+ useEffect(() => {
306
+ if (storeColumns.length > 0 || !Array.isArray(columns) || columns.length === 0) {
307
+ return;
308
+ }
1077
309
 
1078
- const genericEntries = flattenSummaryEntries({
1079
- summaryLabels: labels,
1080
- usedPaths,
1081
- value: resolvedSummary,
1082
- });
310
+ publishStoreValue(store, 'setColumns', columns, 'columns');
311
+ }, [columns, store, storeColumns.length]);
1083
312
 
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);
313
+ useEffect(() => {
314
+ if (data === undefined || !Array.isArray(data)) {
315
+ return;
316
+ }
1115
317
 
1116
- if (!sortField || !sortColumn) return items;
318
+ publishStoreValue(store, 'setItems', data, 'items');
319
+ }, [data, store]);
1117
320
 
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
- });
321
+ useEffect(() => {
322
+ if (!columnsForTable.length) {
323
+ return;
324
+ }
1131
325
 
1132
- const comparison =
1133
- typeof leftValue === 'number' && typeof rightValue === 'number'
1134
- ? leftValue - rightValue
1135
- : normalizeSortText(leftValue).localeCompare(normalizeSortText(rightValue));
326
+ const storedVisibleColumns = resolveStoredVisibleColumnsPreference(tablePreferenceScope);
327
+ if (!storedVisibleColumns) {
328
+ return;
329
+ }
1136
330
 
1137
- return sortDirection === 'desc' ? comparison * -1 : comparison;
331
+ const nextVisibleColumns = sanitizeVisibleColumnsPreference({
332
+ columns: columnsForTable,
333
+ visibleColumns: storedVisibleColumns,
1138
334
  });
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]);
1180
-
1181
- useEffect(() => {
1182
- if (typeof onDataLoaded !== 'function') {
1183
- return;
1184
- }
1185
-
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
- }, []);
335
+ const currentVisibleColumns = store?.getters?.visibleColumns || {};
1193
336
 
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);
337
+ if (stableSerialize(currentVisibleColumns) === stableSerialize(nextVisibleColumns)) {
338
+ return;
1213
339
  }
1214
340
 
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);
1260
- return;
1261
- }
1262
-
1263
- onSortChange?.(nextSort);
1264
- }, [
1265
- autoMode,
1266
- onSortChange,
1267
- resolvedSort?.direction,
1268
- resolvedSort?.field,
1269
- visibleColumnsPreferenceKey,
1270
- ]);
341
+ publishStoreValue(store, 'setVisibleColumns', nextVisibleColumns, 'visibleColumns');
342
+ }, [columnsForTable, store, tablePreferenceScope]);
1271
343
 
1272
- const openEditModal = useCallback(row => {
1273
- if (typeof onEditRow === 'function') {
1274
- onEditRow(row);
1275
- return;
1276
- }
1277
-
1278
- setFormMode('edit');
1279
- setEditingRow(row);
1280
- }, [onEditRow]);
1281
-
1282
- const openAddForm = useCallback(() => {
1283
- if (typeof onAdd === 'function') {
1284
- onAdd();
1285
- return;
1286
- }
1287
-
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);
1305
- return;
1306
- }
1307
-
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;
1346
- });
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
-
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
- ) {
1388
- return;
1389
- }
1390
-
1391
- endReachedLockRef.current = true;
1392
-
1393
- if (autoMode) {
1394
- const nextPage = autoPageRef.current + 1;
1395
- loadAutoPage(nextPage, { append: true });
1396
- return;
1397
- }
1398
-
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
- }
1580
-
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
- };
1663
-
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
1672
- style={[
1673
- styles.loadingOverlayCard,
1674
- { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor },
1675
- ]}
1676
- >
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
- });
344
+ useEffect(() => {
345
+ if (!columnsForTable.length || Object.keys(storeFilters).length > 0) {
346
+ return;
347
+ }
1951
348
 
1952
- const sortFieldName = getSortField(column);
349
+ const storedFilters = resolveStoredTableFiltersPreference(tablePreferenceScope);
350
+ if (!storedFilters) {
351
+ return;
352
+ }
1953
353
 
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>
354
+ const nextFilters = sanitizeTableFiltersPreference({
355
+ columns: columnsForTable,
356
+ filters: storedFilters,
357
+ });
1979
358
 
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}
359
+ if (Object.keys(nextFilters).length === 0) {
360
+ return;
361
+ }
1990
362
 
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
- )}
363
+ publishStoreValue(store, 'setFilters', nextFilters, 'filters');
364
+ }, [columnsForTable, store, storeFilters, tablePreferenceScope]);
2009
365
 
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>
2063
- ) : null}
366
+ useEffect(() => {
367
+ if (configsSignatureRef.current === defaultTableConfigsSignature) {
368
+ return;
369
+ }
2064
370
 
2065
- {renderColumnMenuModal()}
2066
- {renderEditModal()}
371
+ configsSignatureRef.current = defaultTableConfigsSignature;
372
+ publishStoreValue(store, 'setConfigs', defaultTableConfigs, 'configs');
373
+ }, [defaultTableConfigs, defaultTableConfigsSignature, store]);
374
+
375
+ useEffect(() => {
376
+ onDataLoaded?.(sortedData);
377
+ }, [onDataLoaded, sortedData]);
378
+
379
+ return (
380
+ <View
381
+ style={[
382
+ styles.wrap,
383
+ {
384
+ borderColor: themeColors.panelBorder,
385
+ backgroundColor: themeColors.panelBackground,
386
+ },
387
+ ]}
388
+ >
389
+ <DefaultTableToolbar storeName={storeName} />
390
+ <DefaultTableBody storeName={storeName} />
391
+ <DefaultTableFooter storeName={storeName} />
2067
392
  </View>
2068
393
  );
2069
394
  };