@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.
- package/AGENTS.md +3 -2
- package/package.json +21 -18
- package/src/react/components/errors/DefaultErrors.js +64 -82
- package/src/react/components/filters/CompactFilterSelector.js +58 -23
- package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
- package/src/react/components/filters/DefaultColumnFilter.js +177 -45
- package/src/react/components/filters/DefaultExternalFilters.js +201 -143
- package/src/react/components/filters/DefaultSearch.js +27 -25
- package/src/react/components/form/DefaultForm.js +72 -53
- package/src/react/components/form/DefaultForm.styles.js +71 -66
- package/src/react/components/inputs/DefaultDateInput.js +150 -0
- package/src/react/components/inputs/DefaultInput.js +44 -11
- package/src/react/components/inputs/DefaultInput.styles.js +6 -0
- package/src/react/components/inputs/DefaultSelect.js +200 -43
- package/src/react/components/inputs/defaultInputUtils.js +40 -19
- package/src/react/components/table/DefaultColumnMenu.js +108 -0
- package/src/react/components/table/DefaultDebug.js +233 -0
- package/src/react/components/table/DefaultFiltersModal.js +142 -0
- package/src/react/components/table/DefaultModalButton.js +18 -0
- package/src/react/components/table/DefaultSearchModal.js +49 -0
- package/src/react/components/table/DefaultTable.js +350 -2025
- package/src/react/components/table/DefaultTable.styles.js +217 -97
- package/src/react/components/table/DefaultTable.utils.js +502 -0
- package/src/react/components/table/DefaultTableBody.js +26 -0
- package/src/react/components/table/DefaultTableCards.js +171 -0
- package/src/react/components/table/DefaultTableControls.js +208 -0
- package/src/react/components/table/DefaultTableEmptyState.js +22 -0
- package/src/react/components/table/DefaultTableFooter.js +172 -0
- package/src/react/components/table/DefaultTableInput.js +145 -0
- package/src/react/components/table/DefaultTableRows.js +340 -0
- package/src/react/components/table/DefaultTableSearch.js +100 -0
- package/src/react/components/table/DefaultTableToolbar.js +98 -0
- package/src/react/components/table/DefaultToolbarAction.js +56 -0
- package/src/react/components/table/useDefaultTablePagination.js +370 -0
- package/src/react/components/table/useDefaultTableSorting.js +126 -0
- package/src/react/components/table/useDefaultTableTheme.js +58 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
- package/src/store/default/actions.js +51 -13
- package/src/store/default/getters.js +10 -8
- package/src/store/default/mutation_types.js +4 -2
- package/src/store/default/mutations.js +10 -1
- package/src/tests/react/components/form/DefaultForm.test.js +81 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +106 -14
- package/src/tests/react/components/table/DefaultTable.test.js +945 -96
- package/src/tests/react/store/defaultActions.test.js +59 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import Formatter from '@controleonline/ui-common/src/utils/formatter.js';
|
|
2
|
+
import { getDateRange } from '@controleonline/ui-common/src/react/utils/dateRangeFilter';
|
|
3
|
+
import {
|
|
4
|
+
getColumnKey,
|
|
5
|
+
isDateLikeColumn,
|
|
6
|
+
normalizeText,
|
|
7
|
+
resolveCellText,
|
|
8
|
+
resolveStoreNameFromList,
|
|
9
|
+
} from '../inputs/defaultInputUtils';
|
|
10
|
+
import styles from './DefaultTable.styles';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CELL_MIN_WIDTH = 118;
|
|
13
|
+
export const DEFAULT_COMPACT_BREAKPOINT = 768;
|
|
14
|
+
export const COLLAPSED_SEARCH_MAX_VIEWPORT_WIDTH = 400;
|
|
15
|
+
export const COLLAPSED_SEARCH_MAX_CONTAINER_WIDTH = 350;
|
|
16
|
+
export const END_REACHED_THRESHOLD = 0.75;
|
|
17
|
+
export const IDENTITY_CELL_MIN_WIDTH = 76;
|
|
18
|
+
export const MONEY_CELL_MIN_WIDTH = 132;
|
|
19
|
+
export const ACTIONS_CELL_WIDTH = 60;
|
|
20
|
+
|
|
21
|
+
const SUMMARY_OPERATIONS = ['sum', 'count', 'avg', 'min', 'max'];
|
|
22
|
+
const COMPANY_SCOPED_LIST_STORES = new Set([
|
|
23
|
+
'categories',
|
|
24
|
+
'paymentType',
|
|
25
|
+
'wallet',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const shouldIncludeColumn = column =>
|
|
29
|
+
Boolean(getColumnKey(column)) &&
|
|
30
|
+
column?.show !== false &&
|
|
31
|
+
column?.visible !== false &&
|
|
32
|
+
column?.table !== false;
|
|
33
|
+
|
|
34
|
+
export const resolveListActionName = list =>
|
|
35
|
+
normalizeText(list).split('/')[1] || 'getItems';
|
|
36
|
+
|
|
37
|
+
const resolveListLoadParams = ({ currentCompanyId, listStoreName }) => {
|
|
38
|
+
if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return listStoreName === 'categories'
|
|
43
|
+
? { company: currentCompanyId }
|
|
44
|
+
: { people: currentCompanyId };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const resolveColumnListLoadParams = ({
|
|
48
|
+
column,
|
|
49
|
+
currentCompanyId,
|
|
50
|
+
requestParams = {},
|
|
51
|
+
searchValue = '',
|
|
52
|
+
}) => {
|
|
53
|
+
const listStoreName = resolveStoreNameFromList(column?.list);
|
|
54
|
+
const companyScopedParams = resolveListLoadParams({
|
|
55
|
+
currentCompanyId,
|
|
56
|
+
listStoreName,
|
|
57
|
+
});
|
|
58
|
+
const resolvedCustomParams = typeof column?.listRequestParams === 'function'
|
|
59
|
+
? column.listRequestParams({ currentCompanyId, requestParams })
|
|
60
|
+
: column?.listRequestParams;
|
|
61
|
+
const customParams =
|
|
62
|
+
resolvedCustomParams &&
|
|
63
|
+
typeof resolvedCustomParams === 'object' &&
|
|
64
|
+
!Array.isArray(resolvedCustomParams)
|
|
65
|
+
? resolvedCustomParams
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...companyScopedParams,
|
|
70
|
+
...customParams,
|
|
71
|
+
...(normalizeText(searchValue)
|
|
72
|
+
? { [column?.listSearchParam || column?.searchParam || 'search']: normalizeText(searchValue) }
|
|
73
|
+
: {}),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const isObject = value =>
|
|
78
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
|
|
80
|
+
export const getRowKey = (row, index = 0) =>
|
|
81
|
+
String(row?.['@id'] || row?.id || index);
|
|
82
|
+
|
|
83
|
+
export const shouldTriggerEndReachedFromScroll = event => {
|
|
84
|
+
const nativeEvent = event?.nativeEvent || {};
|
|
85
|
+
const viewportHeight = Number(nativeEvent.layoutMeasurement?.height);
|
|
86
|
+
const contentOffsetY = Number(nativeEvent.contentOffset?.y);
|
|
87
|
+
const contentHeight = Number(nativeEvent.contentSize?.height);
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
!Number.isFinite(viewportHeight) ||
|
|
91
|
+
!Number.isFinite(contentOffsetY) ||
|
|
92
|
+
!Number.isFinite(contentHeight) ||
|
|
93
|
+
viewportHeight <= 0 ||
|
|
94
|
+
contentHeight <= viewportHeight
|
|
95
|
+
) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const distanceFromEnd = contentHeight - (contentOffsetY + viewportHeight);
|
|
100
|
+
|
|
101
|
+
return distanceFromEnd <= viewportHeight * END_REACHED_THRESHOLD;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const normalizeRowIdentity = row => {
|
|
105
|
+
const rawId = row?.['@id'] || row?.id;
|
|
106
|
+
if (rawId === null || rawId === undefined || rawId === '') {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return normalizeText(rawId).replace(/\D+/g, '');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const mergeSortedDataWithLiveItems = ({ liveItems = [], sortedData = [] }) => {
|
|
114
|
+
const resolvedSortedData = Array.isArray(sortedData) ? sortedData : [];
|
|
115
|
+
const resolvedLiveItems = Array.isArray(liveItems) ? liveItems : [];
|
|
116
|
+
|
|
117
|
+
if (resolvedLiveItems.length === 0) {
|
|
118
|
+
return resolvedSortedData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (resolvedSortedData.length === 0) {
|
|
122
|
+
return resolvedLiveItems;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const liveItemsById = new Map();
|
|
126
|
+
resolvedLiveItems.forEach(item => {
|
|
127
|
+
const itemId = normalizeRowIdentity(item);
|
|
128
|
+
if (itemId) {
|
|
129
|
+
liveItemsById.set(itemId, item);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (liveItemsById.size === 0) {
|
|
134
|
+
return resolvedSortedData;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return resolvedSortedData.map(item => {
|
|
138
|
+
const itemId = normalizeRowIdentity(item);
|
|
139
|
+
const liveItem = itemId ? liveItemsById.get(itemId) : null;
|
|
140
|
+
|
|
141
|
+
if (!liveItem) {
|
|
142
|
+
return item;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return liveItem;
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const stableSerialize = value => {
|
|
150
|
+
if (value === null || value === undefined) {
|
|
151
|
+
return 'null';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return `[${value.map(item => stableSerialize(item)).join(',')}]`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (value instanceof Date) {
|
|
159
|
+
return JSON.stringify(value.toISOString());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isObject(value)) {
|
|
163
|
+
return `{${Object.keys(value)
|
|
164
|
+
.sort()
|
|
165
|
+
.map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
|
|
166
|
+
.join(',')}}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return JSON.stringify(value);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const resolveDateRangeQuery = value => {
|
|
173
|
+
if (!value || typeof value !== 'object') {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const shortcut = value.shortcut || value.value || 'all';
|
|
178
|
+
const customRange = value.customRange || { from: '', to: '' };
|
|
179
|
+
const dateRange = getDateRange(shortcut, customRange, {
|
|
180
|
+
relativeMode: 'rolling',
|
|
181
|
+
useCurrentMoment: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
after: dateRange?.after || '',
|
|
186
|
+
before: dateRange?.before || '',
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const resolveFilterQueryValue = value => {
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
return value
|
|
193
|
+
.map(item => resolveFilterQueryValue(item))
|
|
194
|
+
.filter(item => item !== '' && item !== null && item !== undefined);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (value && typeof value === 'object') {
|
|
198
|
+
return resolveFilterQueryValue(
|
|
199
|
+
value.value ?? value.id ?? value['@id'] ?? value.key ?? '',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return normalizeText(value);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const normalizeCollectionItems = response => {
|
|
207
|
+
if (Array.isArray(response)) return response;
|
|
208
|
+
if (Array.isArray(response?.member)) return response.member;
|
|
209
|
+
if (Array.isArray(response?.['hydra:member'])) return response['hydra:member'];
|
|
210
|
+
|
|
211
|
+
return [];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const resolveHasMore = ({ hasMore, dataLength, totalItems }) => {
|
|
215
|
+
if (hasMore !== null && hasMore !== undefined) {
|
|
216
|
+
return hasMore;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resolvedTotalItems = Number(totalItems);
|
|
220
|
+
if (!Number.isFinite(resolvedTotalItems)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return dataLength < resolvedTotalItems;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const humanizeSummaryLabel = value =>
|
|
228
|
+
normalizeText(value)
|
|
229
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
230
|
+
.replace(/[._-]+/g, ' ')
|
|
231
|
+
.replace(/\s+/g, ' ')
|
|
232
|
+
.trim();
|
|
233
|
+
|
|
234
|
+
const isMoneySummaryPath = path =>
|
|
235
|
+
/(amount|price|total|value|paid|open|receivable|payable|pending)/i.test(
|
|
236
|
+
Array.isArray(path) ? path.join('.') : normalizeText(path),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const normalizeSummaryMoneyValue = value => {
|
|
240
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
|
|
241
|
+
|
|
242
|
+
const rawValue = normalizeText(value).replace(/[^0-9,.-]/g, '');
|
|
243
|
+
if (!rawValue) return 0;
|
|
244
|
+
|
|
245
|
+
const normalizedValue = rawValue.includes(',')
|
|
246
|
+
? rawValue.replace(/\./g, '').replace(',', '.')
|
|
247
|
+
: rawValue;
|
|
248
|
+
const parsedValue = Number(normalizedValue);
|
|
249
|
+
|
|
250
|
+
return Number.isFinite(parsedValue) ? parsedValue : 0;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const getSummaryOperations = column => {
|
|
254
|
+
if (typeof column?.summary === 'string') return [column.summary];
|
|
255
|
+
if (Array.isArray(column?.summary)) return column.summary;
|
|
256
|
+
if (isObject(column?.summary)) {
|
|
257
|
+
return Object.entries(column.summary)
|
|
258
|
+
.filter(([, value]) => value)
|
|
259
|
+
.map(([operation]) => operation);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return SUMMARY_OPERATIONS.filter(operation => column?.[operation] === true);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const getSummaryField = (column, operation) => {
|
|
266
|
+
if (isObject(column?.summary)) {
|
|
267
|
+
const summaryField = column.summary[operation];
|
|
268
|
+
if (typeof summaryField === 'string') return summaryField;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return getColumnKey(column);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const formatSummaryValue = ({ column, columns, path, storeName, value }) => {
|
|
275
|
+
const columnKey = column ? getColumnKey(column) : '';
|
|
276
|
+
const shouldFormatMoney = isMoneySummaryPath(path) || isMoneySummaryPath(columnKey);
|
|
277
|
+
|
|
278
|
+
if (shouldFormatMoney) {
|
|
279
|
+
return Formatter.formatMoney(normalizeSummaryMoneyValue(value));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (column) {
|
|
283
|
+
return resolveCellText({
|
|
284
|
+
column,
|
|
285
|
+
columns,
|
|
286
|
+
row: { [columnKey]: value },
|
|
287
|
+
storeName,
|
|
288
|
+
value,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return normalizeText(value);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const flattenSummaryEntries = ({
|
|
296
|
+
path = [],
|
|
297
|
+
summaryLabels = {},
|
|
298
|
+
usedPaths,
|
|
299
|
+
value,
|
|
300
|
+
}) => {
|
|
301
|
+
if (!isObject(value)) {
|
|
302
|
+
const pathKey = path.join('.');
|
|
303
|
+
if (!pathKey || usedPaths.has(pathKey)) return [];
|
|
304
|
+
|
|
305
|
+
const fallbackLabel = humanizeSummaryLabel(path[path.length - 1] || pathKey);
|
|
306
|
+
|
|
307
|
+
return [{
|
|
308
|
+
key: pathKey,
|
|
309
|
+
label: summaryLabels[pathKey] || summaryLabels[path[path.length - 1]] || fallbackLabel,
|
|
310
|
+
path,
|
|
311
|
+
value,
|
|
312
|
+
}];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return Object.entries(value).flatMap(([key, childValue]) =>
|
|
316
|
+
flattenSummaryEntries({
|
|
317
|
+
path: [...path, key],
|
|
318
|
+
summaryLabels,
|
|
319
|
+
usedPaths,
|
|
320
|
+
value: childValue,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const isSortableColumn = column => column?.sortable === true;
|
|
326
|
+
|
|
327
|
+
export const getSortField = column => column?.sortField || getColumnKey(column);
|
|
328
|
+
|
|
329
|
+
export const resolveDefaultSort = columns => {
|
|
330
|
+
if (!Array.isArray(columns)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const column of columns) {
|
|
335
|
+
if (!column || column?.defaultSort === undefined || column?.defaultSort === null || column?.defaultSort === false) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const field = getSortField(column);
|
|
340
|
+
const defaultSort = column.defaultSort;
|
|
341
|
+
|
|
342
|
+
if (defaultSort && typeof defaultSort === 'object' && !Array.isArray(defaultSort)) {
|
|
343
|
+
const resolvedDirection = normalizeText(defaultSort.direction || defaultSort.order || 'desc').toLowerCase();
|
|
344
|
+
const resolvedField = normalizeText(
|
|
345
|
+
defaultSort.field || defaultSort.sortField || defaultSort.key || defaultSort.name || field,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
direction: resolvedDirection === 'asc' ? 'asc' : 'desc',
|
|
350
|
+
field: resolvedField || field,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (typeof defaultSort === 'string') {
|
|
355
|
+
const normalizedSort = normalizeText(defaultSort).toLowerCase();
|
|
356
|
+
if (normalizedSort === 'asc' || normalizedSort === 'desc') {
|
|
357
|
+
return {
|
|
358
|
+
direction: normalizedSort,
|
|
359
|
+
field,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
direction: 'desc',
|
|
365
|
+
field: normalizeText(defaultSort) || field,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (defaultSort === true) {
|
|
370
|
+
return {
|
|
371
|
+
direction: 'asc',
|
|
372
|
+
field,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const sanitizeStoredSortPreference = ({
|
|
381
|
+
columns = [],
|
|
382
|
+
fallbackSort = null,
|
|
383
|
+
sort = null,
|
|
384
|
+
}) => {
|
|
385
|
+
const normalizedFallback =
|
|
386
|
+
fallbackSort &&
|
|
387
|
+
typeof fallbackSort === 'object' &&
|
|
388
|
+
typeof fallbackSort.field === 'string' &&
|
|
389
|
+
fallbackSort.field.trim()
|
|
390
|
+
? {
|
|
391
|
+
direction: fallbackSort.direction === 'asc' ? 'asc' : 'desc',
|
|
392
|
+
field: fallbackSort.field.trim(),
|
|
393
|
+
}
|
|
394
|
+
: null;
|
|
395
|
+
|
|
396
|
+
if (
|
|
397
|
+
!sort ||
|
|
398
|
+
typeof sort !== 'object' ||
|
|
399
|
+
typeof sort.field !== 'string' ||
|
|
400
|
+
!sort.field.trim()
|
|
401
|
+
) {
|
|
402
|
+
return normalizedFallback;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const normalizedDirection = sort.direction === 'asc' ? 'asc' : 'desc';
|
|
406
|
+
const normalizedField = sort.field.trim();
|
|
407
|
+
const sortableFields = new Set(
|
|
408
|
+
(Array.isArray(columns) ? columns : [])
|
|
409
|
+
.filter(isSortableColumn)
|
|
410
|
+
.map(column => getSortField(column))
|
|
411
|
+
.filter(Boolean),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (!sortableFields.has(normalizedField)) {
|
|
415
|
+
return normalizedFallback;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
direction: normalizedDirection,
|
|
420
|
+
field: normalizedField,
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const readValueByPath = (object, path) => {
|
|
425
|
+
if (!object || !path) return object;
|
|
426
|
+
|
|
427
|
+
return String(path)
|
|
428
|
+
.split('.')
|
|
429
|
+
.reduce((currentValue, key) => {
|
|
430
|
+
if (currentValue === null || currentValue === undefined) return currentValue;
|
|
431
|
+
return currentValue?.[key];
|
|
432
|
+
}, object);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export const normalizeSortText = value =>
|
|
436
|
+
normalizeText(value)
|
|
437
|
+
.normalize('NFD')
|
|
438
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
439
|
+
.toLowerCase();
|
|
440
|
+
|
|
441
|
+
export const resolveSortComparable = ({ column, row, storeName, columns }) => {
|
|
442
|
+
const fieldName = getColumnKey(column);
|
|
443
|
+
const sortField = getSortField(column);
|
|
444
|
+
const rawValue = sortField === fieldName ? row?.[fieldName] : readValueByPath(row, sortField);
|
|
445
|
+
|
|
446
|
+
if (isDateLikeColumn(column)) {
|
|
447
|
+
const dateValue =
|
|
448
|
+
rawValue && typeof rawValue === 'object'
|
|
449
|
+
? rawValue?.value ??
|
|
450
|
+
rawValue?.date ??
|
|
451
|
+
rawValue?.createdAt ??
|
|
452
|
+
rawValue?.updatedAt ??
|
|
453
|
+
rawValue?.['@id'] ??
|
|
454
|
+
rawValue?.[fieldName] ??
|
|
455
|
+
rawValue
|
|
456
|
+
: rawValue;
|
|
457
|
+
const parsedDate = Date.parse(dateValue);
|
|
458
|
+
return Number.isFinite(parsedDate) ? parsedDate : Number.NEGATIVE_INFINITY;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const resolvedValue = sortField === fieldName
|
|
462
|
+
? resolveCellText({
|
|
463
|
+
column,
|
|
464
|
+
columns,
|
|
465
|
+
row,
|
|
466
|
+
storeName,
|
|
467
|
+
})
|
|
468
|
+
: normalizeText(rawValue ?? resolveCellText({
|
|
469
|
+
column,
|
|
470
|
+
columns,
|
|
471
|
+
row,
|
|
472
|
+
storeName,
|
|
473
|
+
}));
|
|
474
|
+
|
|
475
|
+
const normalizedNumber = Number(
|
|
476
|
+
String(resolvedValue).replace(/[^0-9,.-]/g, '').replace(',', '.'),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (Number.isFinite(normalizedNumber) && String(resolvedValue).match(/[0-9]/)) {
|
|
480
|
+
return normalizedNumber;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return normalizeSortText(resolvedValue);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export const getColumnStyle = column => {
|
|
487
|
+
const key = getColumnKey(column);
|
|
488
|
+
if (column?.isIdentity) return [styles.cell, styles.identityCell];
|
|
489
|
+
if (['price', 'total', 'amount', 'value'].includes(key)) {
|
|
490
|
+
return [styles.cell, styles.moneyCell];
|
|
491
|
+
}
|
|
492
|
+
return styles.cell;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export const getColumnMinWidth = column => {
|
|
496
|
+
const key = getColumnKey(column);
|
|
497
|
+
if (column?.isIdentity) return IDENTITY_CELL_MIN_WIDTH;
|
|
498
|
+
if (['price', 'total', 'amount', 'value'].includes(key)) {
|
|
499
|
+
return MONEY_CELL_MIN_WIDTH;
|
|
500
|
+
}
|
|
501
|
+
return DEFAULT_CELL_MIN_WIDTH;
|
|
502
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
import { useStore } from '@store';
|
|
4
|
+
import DefaultTableCards from './DefaultTableCards';
|
|
5
|
+
import DefaultTableRows from './DefaultTableRows';
|
|
6
|
+
import { DEFAULT_COMPACT_BREAKPOINT } from './DefaultTable.utils';
|
|
7
|
+
|
|
8
|
+
const DefaultTableBody = ({ storeName }) => {
|
|
9
|
+
const store = useStore(storeName);
|
|
10
|
+
const { width } = useWindowDimensions();
|
|
11
|
+
const configs = store?.getters?.configs || {};
|
|
12
|
+
const compactBreakpoint = Number(configs.compactBreakpoint || DEFAULT_COMPACT_BREAKPOINT);
|
|
13
|
+
const isCompactView = width > 0 && width <= compactBreakpoint;
|
|
14
|
+
const effectiveViewMode =
|
|
15
|
+
configs.effectiveViewMode ||
|
|
16
|
+
configs.viewMode ||
|
|
17
|
+
(isCompactView && configs.forceCardsOnCompact !== false ? 'cards' : null) ||
|
|
18
|
+
configs.initialViewMode ||
|
|
19
|
+
'table';
|
|
20
|
+
|
|
21
|
+
return effectiveViewMode === 'cards'
|
|
22
|
+
? <DefaultTableCards storeName={storeName} />
|
|
23
|
+
: <DefaultTableRows storeName={storeName} />;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default DefaultTableBody;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FlatList, Text, View } from 'react-native';
|
|
3
|
+
import { useStore } from '@store';
|
|
4
|
+
import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
|
|
5
|
+
import { getColumnKey, resolveCellText } from '../inputs/defaultInputUtils';
|
|
6
|
+
import DefaultTableEmptyState from './DefaultTableEmptyState';
|
|
7
|
+
import DefaultTableInput from './DefaultTableInput';
|
|
8
|
+
import {
|
|
9
|
+
END_REACHED_THRESHOLD,
|
|
10
|
+
getRowKey,
|
|
11
|
+
mergeSortedDataWithLiveItems,
|
|
12
|
+
shouldTriggerEndReachedFromScroll,
|
|
13
|
+
shouldIncludeColumn,
|
|
14
|
+
} from './DefaultTable.utils';
|
|
15
|
+
import styles from './DefaultTable.styles';
|
|
16
|
+
import useDefaultTableTheme from './useDefaultTableTheme';
|
|
17
|
+
|
|
18
|
+
const DefaultTableCards = ({ storeName }) => {
|
|
19
|
+
const store = useStore(storeName);
|
|
20
|
+
const configs = store?.getters?.configs || {};
|
|
21
|
+
const { palette } = useDefaultTableTheme();
|
|
22
|
+
const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
|
|
23
|
+
const visibleColumns = store?.getters?.visibleColumns || {};
|
|
24
|
+
const tableColumns = columns.filter(
|
|
25
|
+
column => shouldIncludeColumn(column) && visibleColumns[getColumnKey(column)] !== false,
|
|
26
|
+
);
|
|
27
|
+
const sortedData = mergeSortedDataWithLiveItems({
|
|
28
|
+
liveItems: store?.getters?.items,
|
|
29
|
+
sortedData: configs.sortedData,
|
|
30
|
+
});
|
|
31
|
+
const isLoading = Boolean(store?.getters?.isLoadingList || store?.getters?.isLoading);
|
|
32
|
+
const emptyStateLabel = isLoading
|
|
33
|
+
? global.t?.t(storeName, 'label', 'loading')
|
|
34
|
+
: global.t?.t(storeName, 'label', 'empty');
|
|
35
|
+
const tableBorderColor = palette.border;
|
|
36
|
+
const tableMutedColor = palette.textSecondary;
|
|
37
|
+
const tableSurfaceColor = palette.background;
|
|
38
|
+
const tableTextColor = palette.text;
|
|
39
|
+
const handleListScroll = event => {
|
|
40
|
+
if (shouldTriggerEndReachedFromScroll(event)) {
|
|
41
|
+
configs.onEndReached?.();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderCardItem = (row, index = 0) => {
|
|
46
|
+
const rowStyleValue = typeof configs.rowStyle === 'function'
|
|
47
|
+
? configs.rowStyle(row, index)
|
|
48
|
+
: configs.rowStyle;
|
|
49
|
+
const renderField = (fieldName, options = {}) => (
|
|
50
|
+
<DefaultTableInput
|
|
51
|
+
fieldName={fieldName}
|
|
52
|
+
options={options}
|
|
53
|
+
row={row}
|
|
54
|
+
storeName={storeName}
|
|
55
|
+
variant="card"
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
const renderValue = (fieldName, fallback = '-') => {
|
|
59
|
+
const column = columns.find(item => getColumnKey(item) === fieldName);
|
|
60
|
+
if (!column) return fallback;
|
|
61
|
+
|
|
62
|
+
return resolveCellText({ column, columns, row, storeName }) || fallback;
|
|
63
|
+
};
|
|
64
|
+
const RowActionsComponent = configs.rowActionsComponent;
|
|
65
|
+
const hasRowPress = typeof configs.onRowPress === 'function';
|
|
66
|
+
const hasCustomRowActions = typeof RowActionsComponent === 'function';
|
|
67
|
+
const hasEditAction = typeof configs.onEditRow === 'function';
|
|
68
|
+
const hasRowActions = configs.showRowActions !== false && (hasCustomRowActions || hasEditAction);
|
|
69
|
+
const customRowActions = hasCustomRowActions ? (
|
|
70
|
+
<RowActionsComponent
|
|
71
|
+
openEdit={() => configs.onEditRow?.(row)}
|
|
72
|
+
openRow={hasRowPress ? () => configs.onRowPress(row) : null}
|
|
73
|
+
row={row}
|
|
74
|
+
/>
|
|
75
|
+
) : null;
|
|
76
|
+
const editButton = hasEditAction ? (
|
|
77
|
+
<Text onPress={() => configs.onEditRow?.(row)}>
|
|
78
|
+
{global.t?.t(storeName, 'button', 'edit')}
|
|
79
|
+
</Text>
|
|
80
|
+
) : null;
|
|
81
|
+
|
|
82
|
+
if (typeof configs.renderCard === 'function') {
|
|
83
|
+
return (
|
|
84
|
+
<View key={row?.['@id'] || row?.id} style={[styles.cardItem, rowStyleValue]}>
|
|
85
|
+
{configs.renderCard({
|
|
86
|
+
item: row,
|
|
87
|
+
openEdit: () => configs.onEditRow?.(row),
|
|
88
|
+
openRow: hasRowPress ? () => configs.onRowPress(row) : null,
|
|
89
|
+
renderField,
|
|
90
|
+
renderValue,
|
|
91
|
+
row,
|
|
92
|
+
})}
|
|
93
|
+
{hasRowActions ? (
|
|
94
|
+
<View style={styles.cardActions}>
|
|
95
|
+
{customRowActions}
|
|
96
|
+
{editButton}
|
|
97
|
+
</View>
|
|
98
|
+
) : null}
|
|
99
|
+
</View>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View
|
|
105
|
+
key={row?.['@id'] || row?.id}
|
|
106
|
+
style={[
|
|
107
|
+
styles.defaultCard,
|
|
108
|
+
{ backgroundColor: tableSurfaceColor, borderColor: tableBorderColor },
|
|
109
|
+
rowStyleValue,
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
{tableColumns.map(column => {
|
|
113
|
+
const fieldName = getColumnKey(column);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<View key={fieldName} style={styles.defaultCardLine}>
|
|
117
|
+
<Text style={[styles.defaultCardLabel, { color: tableMutedColor }]}>
|
|
118
|
+
{formatStoreColumnLabel({
|
|
119
|
+
columns,
|
|
120
|
+
fieldName,
|
|
121
|
+
fallbackLabel: column?.label || fieldName,
|
|
122
|
+
storeName,
|
|
123
|
+
})}
|
|
124
|
+
</Text>
|
|
125
|
+
{renderField(fieldName, {
|
|
126
|
+
readTextStyle: [styles.defaultCardValue, { color: tableTextColor }],
|
|
127
|
+
numberOfLines: 1,
|
|
128
|
+
})}
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
{hasRowActions ? (
|
|
133
|
+
<View style={styles.cardActionGroup}>
|
|
134
|
+
{customRowActions}
|
|
135
|
+
{editButton}
|
|
136
|
+
</View>
|
|
137
|
+
) : null}
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<FlatList
|
|
144
|
+
data={sortedData}
|
|
145
|
+
keyExtractor={getRowKey}
|
|
146
|
+
renderItem={({ item, index }) => renderCardItem(item, index)}
|
|
147
|
+
style={styles.cardsScroll}
|
|
148
|
+
contentContainerStyle={styles.cardsGrid}
|
|
149
|
+
ListEmptyComponent={(
|
|
150
|
+
<DefaultTableEmptyState
|
|
151
|
+
emptyStateLabel={emptyStateLabel}
|
|
152
|
+
isLoading={isLoading}
|
|
153
|
+
isTable={false}
|
|
154
|
+
tableLayoutStyle={null}
|
|
155
|
+
tableMutedColor={tableMutedColor}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
ListFooterComponent={null}
|
|
159
|
+
nestedScrollEnabled
|
|
160
|
+
onMomentumScrollBegin={configs.onMomentumScrollBegin || undefined}
|
|
161
|
+
onScroll={handleListScroll}
|
|
162
|
+
onScrollBeginDrag={configs.onScrollBeginDrag || undefined}
|
|
163
|
+
onEndReached={configs.onEndReached}
|
|
164
|
+
onEndReachedThreshold={END_REACHED_THRESHOLD}
|
|
165
|
+
scrollEventThrottle={120}
|
|
166
|
+
showsVerticalScrollIndicator={false}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default DefaultTableCards;
|