@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
@@ -0,0 +1,208 @@
1
+ import React from 'react';
2
+ import { Text, TouchableOpacity } from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import { useStore } from '@store';
5
+ import DefaultColumnMenu from './DefaultColumnMenu';
6
+ import DefaultDebug from './DefaultDebug';
7
+ import DefaultFiltersModal from './DefaultFiltersModal';
8
+ import DefaultModalButton from './DefaultModalButton';
9
+ import styles from './DefaultTable.styles';
10
+ import { normalizeText } from '../inputs/defaultInputUtils';
11
+ import {
12
+ persistTableViewModePreference,
13
+ resolveDefaultTablePreferenceScope,
14
+ } from '../../utils/tableVisibleColumnsPreferences';
15
+ import { shouldIncludeColumn } from './DefaultTable.utils';
16
+ import useDefaultTableTheme from './useDefaultTableTheme';
17
+
18
+ const DefaultTableControls = ({ storeName }) => {
19
+ const store = useStore(storeName);
20
+ const configs = store?.getters?.configs || {};
21
+ const effectiveViewMode =
22
+ configs.effectiveViewMode ||
23
+ configs.viewMode ||
24
+ configs.initialViewMode ||
25
+ 'table';
26
+ const tablePreferenceScope =
27
+ configs.tablePreferenceScope ||
28
+ resolveDefaultTablePreferenceScope({ storeName });
29
+ const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
30
+ const filters = store?.getters?.filters || {};
31
+ const hasFilterableColumns = columns.some(
32
+ column =>
33
+ configs.showColumnFiltersButton !== false &&
34
+ shouldIncludeColumn(column) &&
35
+ column?.filter !== false &&
36
+ column?.filters !== false,
37
+ );
38
+ const tableFiltersVisible = Boolean(configs.tableFiltersVisible);
39
+ const activeFilterCount = Object.values(filters).filter(value => normalizeText(value) !== '').length;
40
+ const addConfig = store?.getters?.add;
41
+ const shouldRenderAddButton =
42
+ (addConfig === true || configs.add === true) &&
43
+ (typeof configs.onAdd === 'function' || typeof store?.actions?.save === 'function');
44
+ const { tableButtonColors } = useDefaultTableTheme();
45
+ const {
46
+ backgroundColor,
47
+ borderColor,
48
+ iconColor,
49
+ pressedBackgroundColor,
50
+ pressedBorderColor,
51
+ pressedIconColor,
52
+ textColor,
53
+ } = tableButtonColors;
54
+
55
+ const buttonStyle = [
56
+ styles.toolbarButton,
57
+ { borderColor, backgroundColor },
58
+ ];
59
+ const pressedStyle = {
60
+ backgroundColor: pressedBackgroundColor,
61
+ borderColor: pressedBorderColor,
62
+ };
63
+ const updateConfigs = nextConfigs => {
64
+ if (typeof store?.actions?.setConfigs === 'function') {
65
+ store.actions.setConfigs(nextConfigs);
66
+ return;
67
+ }
68
+
69
+ if (store?.getters) {
70
+ store.getters.configs = nextConfigs;
71
+ }
72
+ };
73
+
74
+ return (
75
+ <>
76
+ <DefaultDebug storeName={storeName} />
77
+ {hasFilterableColumns && effectiveViewMode === 'table' ? (
78
+ <TouchableOpacity
79
+ style={[
80
+ ...buttonStyle,
81
+ tableFiltersVisible ? pressedStyle : null,
82
+ ]}
83
+ activeOpacity={0.82}
84
+ onPress={() =>
85
+ updateConfigs({
86
+ ...configs,
87
+ tableFiltersVisible: !tableFiltersVisible,
88
+ })
89
+ }
90
+ >
91
+ <Icon
92
+ name="filter"
93
+ size={14}
94
+ color={tableFiltersVisible ? pressedIconColor : iconColor}
95
+ />
96
+ {activeFilterCount > 0 ? (
97
+ <Text
98
+ style={[
99
+ styles.toolbarBadgeText,
100
+ { color: tableFiltersVisible ? pressedIconColor : iconColor },
101
+ ]}
102
+ >
103
+ {activeFilterCount}
104
+ </Text>
105
+ ) : null}
106
+ </TouchableOpacity>
107
+ ) : null}
108
+ {hasFilterableColumns && effectiveViewMode === 'cards' ? (
109
+ <DefaultModalButton
110
+ renderButton={({ isOpen, open }) => (
111
+ <TouchableOpacity
112
+ style={[
113
+ ...buttonStyle,
114
+ isOpen ? pressedStyle : null,
115
+ ]}
116
+ activeOpacity={0.82}
117
+ onPress={open}
118
+ >
119
+ <Icon
120
+ name="filter"
121
+ size={14}
122
+ color={isOpen ? pressedIconColor : iconColor}
123
+ />
124
+ {activeFilterCount > 0 ? (
125
+ <Text
126
+ style={[
127
+ styles.toolbarBadgeText,
128
+ { color: isOpen ? pressedIconColor : iconColor },
129
+ ]}>
130
+ {activeFilterCount}
131
+ </Text>
132
+ ) : null}
133
+ </TouchableOpacity>
134
+ )}
135
+ >
136
+ {({ close, isOpen }) => (
137
+ <DefaultFiltersModal
138
+ storeName={storeName}
139
+ visible={isOpen}
140
+ onClose={close}
141
+ />
142
+ )}
143
+ </DefaultModalButton>
144
+ ) : null}
145
+ <TouchableOpacity
146
+ style={[
147
+ ...buttonStyle,
148
+ effectiveViewMode !== 'table' ? pressedStyle : null,
149
+ ]}
150
+ activeOpacity={0.82}
151
+ onPress={() => {
152
+ const nextViewMode = effectiveViewMode === 'table' ? 'cards' : 'table';
153
+ const nextConfigs = {
154
+ ...configs,
155
+ effectiveViewMode: nextViewMode,
156
+ viewMode: nextViewMode,
157
+ };
158
+
159
+ persistTableViewModePreference(tablePreferenceScope, nextConfigs.viewMode);
160
+ updateConfigs(nextConfigs);
161
+ }}
162
+ >
163
+ <Icon
164
+ name={effectiveViewMode === 'table' ? 'list' : 'grid'}
165
+ size={14}
166
+ color={effectiveViewMode !== 'table' ? pressedIconColor : iconColor}
167
+ />
168
+ </TouchableOpacity>
169
+ <DefaultModalButton
170
+ renderButton={({ isOpen, toggle }) => (
171
+ <TouchableOpacity
172
+ style={[
173
+ ...buttonStyle,
174
+ isOpen ? pressedStyle : null,
175
+ ]}
176
+ activeOpacity={0.82}
177
+ onPress={toggle}
178
+ >
179
+ <Icon name="columns" size={14} color={isOpen ? pressedIconColor : textColor} />
180
+ </TouchableOpacity>
181
+ )}
182
+ >
183
+ {({ close, isOpen }) => (
184
+ <DefaultColumnMenu
185
+ storeName={storeName}
186
+ visible={isOpen}
187
+ onClose={close}
188
+ />
189
+ )}
190
+ </DefaultModalButton>
191
+ {shouldRenderAddButton ? (
192
+ <TouchableOpacity
193
+ style={[
194
+ styles.toolbarButton,
195
+ styles.toolbarAddButton,
196
+ { backgroundColor: backgroundColor, borderColor: backgroundColor },
197
+ ]}
198
+ activeOpacity={0.85}
199
+ onPress={() => configs.onAdd?.()}
200
+ >
201
+ <Icon name="plus" size={16} color={textColor} />
202
+ </TouchableOpacity>
203
+ ) : null}
204
+ </>
205
+ );
206
+ };
207
+
208
+ export default DefaultTableControls;
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import StateStore from '@controleonline/ui-common/src/react/components/StateStore';
4
+ import styles from './DefaultTable.styles';
5
+
6
+ const DefaultTableEmptyState = ({
7
+ emptyStateLabel,
8
+ isLoading,
9
+ isTable = false,
10
+ tableLayoutStyle = null,
11
+ tableMutedColor,
12
+ }) => (
13
+ <View style={[styles.emptyBox, isTable ? tableLayoutStyle : null]}>
14
+ {isLoading ? (
15
+ <StateStore compact loading={emptyStateLabel} />
16
+ ) : (
17
+ <Text style={[styles.emptyText, { color: tableMutedColor }]}>{emptyStateLabel}</Text>
18
+ )}
19
+ </View>
20
+ );
21
+
22
+ export default DefaultTableEmptyState;
@@ -0,0 +1,172 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import { useStore } from '@store';
4
+ import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
5
+ import { getColumnKey, normalizeText } from '../inputs/defaultInputUtils';
6
+ import {
7
+ flattenSummaryEntries,
8
+ formatSummaryValue,
9
+ getSummaryField,
10
+ getSummaryOperations,
11
+ isObject,
12
+ } from './DefaultTable.utils';
13
+ import styles from './DefaultTable.styles';
14
+ import useDefaultTableTheme from './useDefaultTableTheme';
15
+
16
+ const DefaultTableFooter = ({ storeName }) => {
17
+ const store = useStore(storeName);
18
+ const configs = store?.getters?.configs || {};
19
+ const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
20
+ const footerComponent = configs.footerComponent || null;
21
+ const isCompactView = false;
22
+ const showTotalItemsInCompactToolbar = configs.showTotalItemsInCompactToolbar === true;
23
+ const showTotalItemsInFooter = configs.showTotalItemsInFooter !== false;
24
+ const tableColumns = columns;
25
+ const { themeColors } = useDefaultTableTheme();
26
+ const tableFooterBackgroundColor = themeColors.tableFooterBackground;
27
+ const tableFooterBorderColor = themeColors.tableFooterBorder;
28
+ const tableFooterTextColor = themeColors.tableFooterText;
29
+ const toolbarCountBackgroundColor = themeColors.badgeBackground;
30
+ const toolbarCountTextColor = themeColors.badgeText;
31
+ const storeTotalItems = store?.getters?.totalItems;
32
+ const resolvedTotalItems = storeTotalItems;
33
+ const totalItemsNumber = Number(resolvedTotalItems);
34
+ const shouldRenderTotalItems =
35
+ resolvedTotalItems !== null &&
36
+ resolvedTotalItems !== undefined &&
37
+ Number.isFinite(totalItemsNumber);
38
+ const shouldRenderFooterTotalItems =
39
+ shouldRenderTotalItems &&
40
+ showTotalItemsInFooter !== false;
41
+ const shouldRenderCompactToolbarTotalItems =
42
+ showTotalItemsInCompactToolbar === true &&
43
+ isCompactView &&
44
+ shouldRenderTotalItems &&
45
+ !shouldRenderFooterTotalItems;
46
+ const resolvedTotalItemsText =
47
+ shouldRenderTotalItems
48
+ ? `${totalItemsNumber} ${global.t?.t(storeName, 'label', 'items')}`
49
+ : '';
50
+ const configuredSummary = configs.summary;
51
+ const resolvedSummary = configuredSummary !== undefined
52
+ ? configuredSummary
53
+ : store?.getters?.summary;
54
+ const summaryLabels = isObject(configs.summaryLabels) ? configs.summaryLabels : {};
55
+ const shouldReadSummary = resolvedSummary !== false && isObject(resolvedSummary);
56
+ const summaryEntries = useMemo(() => {
57
+ if (!shouldReadSummary) return [];
58
+
59
+ const usedPaths = new Set();
60
+ const columnEntries = tableColumns.flatMap(column => {
61
+ const operations = getSummaryOperations(column);
62
+ if (!operations.length) return [];
63
+
64
+ const fieldName = getColumnKey(column);
65
+ const columnLabel = formatStoreColumnLabel({
66
+ columns,
67
+ fieldName,
68
+ fallbackLabel: column?.label || fieldName,
69
+ storeName,
70
+ });
71
+
72
+ return operations.map(operation => {
73
+ const summaryField = getSummaryField(column, operation);
74
+ const path = [operation, summaryField];
75
+ const pathKey = path.join('.');
76
+ const value = resolvedSummary?.[operation]?.[summaryField];
77
+ if (value === undefined) return null;
78
+
79
+ usedPaths.add(pathKey);
80
+
81
+ return {
82
+ key: pathKey,
83
+ label: operations.length > 1 ? `${columnLabel} ${operation}` : columnLabel,
84
+ path,
85
+ value,
86
+ column,
87
+ };
88
+ }).filter(Boolean);
89
+ });
90
+
91
+ const genericEntries = flattenSummaryEntries({
92
+ summaryLabels,
93
+ usedPaths,
94
+ value: resolvedSummary,
95
+ });
96
+
97
+ return [...columnEntries, ...genericEntries].filter(entry =>
98
+ entry?.value !== undefined &&
99
+ entry?.value !== null &&
100
+ normalizeText(entry.value) !== '',
101
+ );
102
+ }, [columns, resolvedSummary, shouldReadSummary, storeName, summaryLabels, tableColumns]);
103
+ const shouldRenderFooterBar =
104
+ (shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems) ||
105
+ summaryEntries.length > 0;
106
+
107
+ if (footerComponent) {
108
+ const footerProps = {
109
+ columns,
110
+ resolvedTotalItemsText,
111
+ shouldRenderCompactToolbarTotalItems,
112
+ shouldRenderFooterTotalItems,
113
+ storeName,
114
+ summaryEntries,
115
+ };
116
+
117
+ return React.isValidElement(footerComponent)
118
+ ? footerComponent
119
+ : React.createElement(footerComponent, footerProps);
120
+ }
121
+
122
+ if (!shouldRenderFooterBar) {
123
+ return null;
124
+ }
125
+
126
+ return (
127
+ <View style={[styles.footerBar, { backgroundColor: tableFooterBackgroundColor, borderTopColor: tableFooterBorderColor }]}>
128
+ {summaryEntries.length > 0 ? (
129
+ <View style={styles.footerSummaryList}>
130
+ {summaryEntries.map(entry => (
131
+ <View key={entry.key} style={styles.footerSummaryItem}>
132
+ <Text style={[styles.footerSummaryLabel, { color: tableFooterTextColor }]} numberOfLines={1}>
133
+ {entry.label}
134
+ </Text>
135
+ <Text
136
+ style={[
137
+ styles.footerSummaryValue,
138
+ { color: tableFooterTextColor },
139
+ ]}
140
+ numberOfLines={1}
141
+ adjustsFontSizeToFit
142
+ minimumFontScale={0.78}
143
+ >
144
+ {formatSummaryValue({
145
+ column: entry.column,
146
+ columns,
147
+ path: entry.path,
148
+ storeName,
149
+ value: entry.value,
150
+ })}
151
+ </Text>
152
+ </View>
153
+ ))}
154
+ </View>
155
+ ) : null}
156
+ {shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems ? (
157
+ <View style={[styles.footerCountPill, { backgroundColor: toolbarCountBackgroundColor }]}>
158
+ <Text
159
+ style={[styles.footerCountText, { color: toolbarCountTextColor }]}
160
+ numberOfLines={1}
161
+ adjustsFontSizeToFit
162
+ minimumFontScale={0.82}
163
+ >
164
+ {resolvedTotalItemsText}
165
+ </Text>
166
+ </View>
167
+ ) : null}
168
+ </View>
169
+ );
170
+ };
171
+
172
+ export default DefaultTableFooter;
@@ -0,0 +1,145 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import { useStore } from '@store';
4
+ import DefaultInput from '../inputs/DefaultInput';
5
+ import {
6
+ formatSaveValue,
7
+ getColumnKey,
8
+ isEditableColumn,
9
+ normalizeId,
10
+ } from '../inputs/defaultInputUtils';
11
+ import { getColumnStyle } from './DefaultTable.utils';
12
+ import styles from './DefaultTable.styles';
13
+ import useDefaultTableTheme from './useDefaultTableTheme';
14
+
15
+ const STORE_ACTION_META_KEY = '__storeMeta';
16
+
17
+ const isPlainObject = value =>
18
+ value !== null && typeof value === 'object' && !Array.isArray(value);
19
+
20
+ const asStyleArray = style => (Array.isArray(style) ? style : [style]).filter(Boolean);
21
+
22
+ const buildSavedItemPatch = (column, fieldName, value) => {
23
+ if (!column?.list || !fieldName || !isPlainObject(value)) {
24
+ return {};
25
+ }
26
+
27
+ if (isPlainObject(value.object)) {
28
+ return {
29
+ [fieldName]: value.object,
30
+ };
31
+ }
32
+
33
+ if (value.value == null && value.label == null) {
34
+ return {};
35
+ }
36
+
37
+ const labelField = column?.listSearchParam || column?.searchParam || fieldName;
38
+
39
+ return {
40
+ [fieldName]: {
41
+ id: value.value,
42
+ value: value.value,
43
+ label: value.label,
44
+ [labelField]: value.label,
45
+ },
46
+ };
47
+ };
48
+
49
+ const DefaultTableInput = ({
50
+ column: columnProp = null,
51
+ fieldName = '',
52
+ options = {},
53
+ row = {},
54
+ storeName = '',
55
+ variant = 'cell',
56
+ }) => {
57
+ const store = useStore(storeName);
58
+ const configs = store?.getters?.configs || {};
59
+ const [isEditing, setIsEditing] = useState(false);
60
+ const [isSaving, setIsSaving] = useState(false);
61
+ const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
62
+ const hasRowPress = false;
63
+ const { resolvedAccentColor } = useDefaultTableTheme();
64
+ const column = columnProp || columns.find(item => getColumnKey(item) === fieldName);
65
+
66
+ if (!column) return null;
67
+
68
+ const resolvedFieldName = getColumnKey(column);
69
+ const input = (
70
+ <DefaultInput
71
+ accentColor={options.accentColor || resolvedAccentColor}
72
+ column={column}
73
+ columns={columns}
74
+ containerStyle={options.containerStyle}
75
+ displayValue={options.displayValue}
76
+ editing={isEditing}
77
+ getOptionsForColumn={configs.getOptionsForColumn}
78
+ inputStyle={options.inputStyle}
79
+ label={options.label}
80
+ numberOfLines={options.numberOfLines}
81
+ onCancelEditing={() => setIsEditing(false)}
82
+ onSave={value => {
83
+ if (typeof store?.actions?.save !== 'function') {
84
+ setIsEditing(false);
85
+ return Promise.resolve(null);
86
+ }
87
+
88
+ setIsSaving(true);
89
+ const savedItemPatch = buildSavedItemPatch(column, resolvedFieldName, value);
90
+ const storeMeta =
91
+ Object.keys(savedItemPatch).length > 0
92
+ ? {
93
+ [STORE_ACTION_META_KEY]: {
94
+ savedItemPatch,
95
+ },
96
+ }
97
+ : {};
98
+
99
+ return Promise.resolve(
100
+ store.actions.save({
101
+ id: normalizeId(row?.['@id'] || row?.id),
102
+ [resolvedFieldName]: formatSaveValue(column, value, row),
103
+ ...storeMeta,
104
+ }),
105
+ )
106
+ .then(savedItem => {
107
+ configs.onSaved?.(savedItem, row);
108
+ return savedItem;
109
+ })
110
+ .finally(() => {
111
+ setIsSaving(false);
112
+ setIsEditing(false);
113
+ });
114
+ }}
115
+ onStartEditing={() => setIsEditing(true)}
116
+ readTextStyle={options.readTextStyle || options.textStyle}
117
+ row={row}
118
+ saving={isSaving}
119
+ showLabel={options.showLabel}
120
+ storeName={storeName}
121
+ variant={options.variant || variant}
122
+ />
123
+ );
124
+
125
+ if (variant !== 'cell') {
126
+ return input;
127
+ }
128
+
129
+ const shouldDelegatePress = hasRowPress && !isEditableColumn(column);
130
+
131
+ return (
132
+ <View
133
+ style={[
134
+ ...asStyleArray(getColumnStyle(column)),
135
+ ...asStyleArray(options.cellStyle),
136
+ isEditing ? styles.editingCell : null,
137
+ ]}
138
+ pointerEvents={shouldDelegatePress ? 'none' : 'auto'}
139
+ >
140
+ {input}
141
+ </View>
142
+ );
143
+ };
144
+
145
+ export default DefaultTableInput;