@controleonline/ui-default 1.0.264 → 1.0.266
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +234 -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 +322 -1964
- package/src/react/components/table/DefaultTable.styles.js +234 -102
- 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 +205 -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 +344 -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 +52 -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 +1318 -334
- package/src/tests/react/store/defaultActions.test.js +59 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
package/AGENTS.md
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
- A tela deve passar preferencialmente apenas `storeName` e contexto minimo, como `requestParams`, `onRowPress`, `renderCard`, `onAdd`, `onEdit` ou componentes extras realmente especificos.
|
|
11
11
|
- O componente pode receber componentes de composicao para variacoes visuais, como `renderCard`, add personalizado, toolbar customizada, acoes auxiliares e modais de apoio; isso nao muda o contrato de que a listagem continua sendo responsabilidade do `DefaultTable`.
|
|
12
12
|
- Quando `data` nao e informada e existe `storeName`, o `DefaultTable` trabalha em modo automatico e carrega os itens pelas actions do store.
|
|
13
|
-
- O componente e responsavel por busca, ordenacao, filtros, paginação, infinite scroll, loading, erro, resumo, visibilidade de colunas, modo desktop e modo card no mobile.
|
|
14
|
-
-
|
|
13
|
+
- O componente e responsavel por busca, ordenacao, filtros, paginação, infinite scroll, loading, erro, resumo, visibilidade de colunas, modo desktop e modo card no mobile.
|
|
14
|
+
- Preferencias de usuario do `DefaultTable`, incluindo filtros, busca, ordenacao, modo de exibicao e visibilidade de colunas, devem ser salvas em `localStorage` na chave `default-table`, sempre no formato `[store][rota] = valores`. Nunca usar chave apenas por store ou apenas por rota.
|
|
15
|
+
- O store nao e fallback: ele e a regra. Se `columns` define um comportamento, o `DefaultTable` deve obedecer sem recriar uma segunda regra na tela.
|
|
15
16
|
- `columns` e a fonte de verdade para label, visibilidade, ordem, `editable`, `sortable`, `inputType`, `list`, `format`, `formatList`, `formatFilter`, `saveFormat`, `searchParam`, `externalFilter`, `summary` e `defaultSort`. `show:false` e um alias aceito para ocultar coluna, junto com `visible:false` e `table:false`.
|
|
16
17
|
- Se uma coluna vier como `editable: false`, ela continua nao editavel mesmo que seja ordenavel. Se vier como `sortable: true`, a ordenacao precisa continuar funcionando.
|
|
17
18
|
- `inputType: 'date-range'` exige filtro de intervalo. `externalFilter: true` habilita o filtro externo quando a listagem suporta esse contrato.
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@controleonline/ui-default",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"author": "ControleOnline",
|
|
5
|
-
"description": "ControleOnline Default Quasar UI Component",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"main": "src/react/index.js",
|
|
8
|
-
"scripts": {},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://github.com/ControleOnline/conline-quasar-ui"
|
|
12
|
-
},
|
|
13
|
-
"bugs": "",
|
|
14
|
-
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
15
|
-
"browserslist": [
|
|
16
|
-
"last 1 version, not dead, ie >= 11"
|
|
17
|
-
]
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@controleonline/ui-default",
|
|
3
|
+
"version": "1.0.266",
|
|
4
|
+
"author": "ControleOnline",
|
|
5
|
+
"description": "ControleOnline Default Quasar UI Component",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/react/index.js",
|
|
8
|
+
"scripts": {},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/ControleOnline/conline-quasar-ui"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "",
|
|
14
|
+
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
15
|
+
"browserslist": [
|
|
16
|
+
"last 1 version, not dead, ie >= 11"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@controleonline/ui-common": "*"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -7,28 +7,16 @@ const AUTO_CLOSE_MS = 5000;
|
|
|
7
7
|
|
|
8
8
|
const resolveText = value => String(value ?? '').trim();
|
|
9
9
|
|
|
10
|
-
const normalizeStoreNames = (store, stores) => {
|
|
11
|
-
const resolvedStoreNames = [
|
|
12
|
-
...(Array.isArray(store) ? store : store ? [store] : []),
|
|
13
|
-
...(Array.isArray(stores) ? stores : []),
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
return resolvedStoreNames.filter(name => typeof name === 'string' && name.trim()).map(name => name.trim());
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
for (const key of keys) {
|
|
21
|
-
const value = colors?.[key];
|
|
22
|
-
|
|
23
|
-
if (typeof value === 'string' && value.trim()) {
|
|
24
|
-
return value.trim();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return fallback;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const DefaultErrors = ({
|
|
10
|
+
const normalizeStoreNames = (store, stores) => {
|
|
11
|
+
const resolvedStoreNames = [
|
|
12
|
+
...(Array.isArray(store) ? store : store ? [store] : []),
|
|
13
|
+
...(Array.isArray(stores) ? stores : []),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
return resolvedStoreNames.filter(name => typeof name === 'string' && name.trim()).map(name => name.trim());
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DefaultErrors = ({
|
|
32
20
|
store = null,
|
|
33
21
|
stores = [],
|
|
34
22
|
error = null,
|
|
@@ -44,10 +32,25 @@ const DefaultErrors = ({
|
|
|
44
32
|
messageStyle = null,
|
|
45
33
|
retryButtonStyle = null,
|
|
46
34
|
retryTextStyle = null,
|
|
47
|
-
}) => {
|
|
48
|
-
const themeStore = useStore('theme');
|
|
49
|
-
const themeColors = themeStore?.getters?.colors || {};
|
|
50
|
-
const
|
|
35
|
+
}) => {
|
|
36
|
+
const themeStore = useStore('theme');
|
|
37
|
+
const themeColors = themeStore?.getters?.colors || {};
|
|
38
|
+
const palette = useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
buttonBackground: themeColors.buttonBackground,
|
|
41
|
+
buttonBorder: themeColors.buttonBorder,
|
|
42
|
+
buttonText: themeColors.buttonText,
|
|
43
|
+
inputErrorBorder: themeColors.inputErrorBorder,
|
|
44
|
+
inputErrorText: themeColors.inputErrorText,
|
|
45
|
+
modalBackground: themeColors.modalBackground,
|
|
46
|
+
modalBorder: themeColors.modalBorder,
|
|
47
|
+
modalCloseIcon: themeColors.modalCloseIcon,
|
|
48
|
+
modalOverlay: themeColors.modalOverlay,
|
|
49
|
+
modalText: themeColors.modalText,
|
|
50
|
+
}),
|
|
51
|
+
[themeColors],
|
|
52
|
+
);
|
|
53
|
+
const allStores = typeof useStores === 'function' ? useStores(state => state) : null;
|
|
51
54
|
const storeNames = useMemo(() => normalizeStoreNames(store, stores), [store, stores]);
|
|
52
55
|
const watchedStoreEntries = useMemo(() => {
|
|
53
56
|
if (!allStores || storeNames.length === 0) {
|
|
@@ -90,40 +93,18 @@ const DefaultErrors = ({
|
|
|
90
93
|
.join('|')}`
|
|
91
94
|
: '';
|
|
92
95
|
const activeDriverSignature = storeDriverSignature || explicitDriverSignature;
|
|
93
|
-
const [dismissedSignatures, setDismissedSignatures] = useState(() => new Set());
|
|
94
|
-
const timerRef = useRef(null);
|
|
95
|
-
const textAlign = align === 'left' ? 'left' : 'center';
|
|
96
|
-
const accentColor =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
const overlayColor = pickThemeColor(
|
|
107
|
-
themeColors,
|
|
108
|
-
['modalOverlay', 'overlayBackground'],
|
|
109
|
-
'rgba(15, 23, 42, 0.55)',
|
|
110
|
-
);
|
|
111
|
-
const titleColor = pickThemeColor(
|
|
112
|
-
themeColors,
|
|
113
|
-
['inputErrorText', 'textDanger', 'modalHeaderText', 'text'],
|
|
114
|
-
'#B91C1C',
|
|
115
|
-
);
|
|
116
|
-
const messageColor = pickThemeColor(
|
|
117
|
-
themeColors,
|
|
118
|
-
['modalText', 'textSecondary', 'text'],
|
|
119
|
-
'#334155',
|
|
120
|
-
);
|
|
121
|
-
const closeColor = pickThemeColor(
|
|
122
|
-
themeColors,
|
|
123
|
-
['modalCloseIcon', 'textDanger', 'primary'],
|
|
124
|
-
accentColor,
|
|
125
|
-
);
|
|
126
|
-
const retryAccentColor = pickThemeColor(themeColors, ['primary'], accentColor);
|
|
96
|
+
const [dismissedSignatures, setDismissedSignatures] = useState(() => new Set());
|
|
97
|
+
const timerRef = useRef(null);
|
|
98
|
+
const textAlign = align === 'left' ? 'left' : 'center';
|
|
99
|
+
const accentColor = palette.inputErrorBorder;
|
|
100
|
+
const backgroundColor = palette.modalBackground;
|
|
101
|
+
const overlayColor = palette.modalOverlay;
|
|
102
|
+
const titleColor = palette.inputErrorText;
|
|
103
|
+
const messageColor = palette.modalText;
|
|
104
|
+
const closeColor = palette.modalCloseIcon;
|
|
105
|
+
const retryBackgroundColor = palette.buttonBackground;
|
|
106
|
+
const retryBorderColor = palette.buttonBorder;
|
|
107
|
+
const retryTextColor = palette.buttonText;
|
|
127
108
|
|
|
128
109
|
const clearStoreErrors = useCallback(() => {
|
|
129
110
|
watchedStoreEntries.forEach(entry => {
|
|
@@ -291,27 +272,27 @@ const DefaultErrors = ({
|
|
|
291
272
|
activeOpacity={0.85}
|
|
292
273
|
onPress={onRetry}
|
|
293
274
|
style={[
|
|
294
|
-
{
|
|
295
|
-
alignItems: 'center',
|
|
296
|
-
alignSelf: textAlign === 'left' ? 'flex-start' : 'center',
|
|
297
|
-
borderRadius: 999,
|
|
298
|
-
borderWidth: 1,
|
|
299
|
-
borderColor:
|
|
300
|
-
backgroundColor:
|
|
301
|
-
marginTop: 4,
|
|
302
|
-
paddingHorizontal: 12,
|
|
303
|
-
paddingVertical: 8,
|
|
275
|
+
{
|
|
276
|
+
alignItems: 'center',
|
|
277
|
+
alignSelf: textAlign === 'left' ? 'flex-start' : 'center',
|
|
278
|
+
borderRadius: 999,
|
|
279
|
+
borderWidth: 1,
|
|
280
|
+
borderColor: retryBorderColor,
|
|
281
|
+
backgroundColor: retryBackgroundColor,
|
|
282
|
+
marginTop: 4,
|
|
283
|
+
paddingHorizontal: 12,
|
|
284
|
+
paddingVertical: 8,
|
|
304
285
|
},
|
|
305
286
|
retryButtonStyle,
|
|
306
287
|
]}
|
|
307
288
|
>
|
|
308
289
|
<Text
|
|
309
|
-
style={[
|
|
310
|
-
{
|
|
311
|
-
color:
|
|
312
|
-
fontSize: 12,
|
|
313
|
-
fontWeight: '800',
|
|
314
|
-
},
|
|
290
|
+
style={[
|
|
291
|
+
{
|
|
292
|
+
color: retryTextColor,
|
|
293
|
+
fontSize: 12,
|
|
294
|
+
fontWeight: '800',
|
|
295
|
+
},
|
|
315
296
|
retryTextStyle,
|
|
316
297
|
]}
|
|
317
298
|
>
|
|
@@ -328,12 +309,13 @@ const DefaultErrors = ({
|
|
|
328
309
|
onPress={dismissPopup}
|
|
329
310
|
testID="default-errors-close"
|
|
330
311
|
style={[
|
|
331
|
-
{
|
|
332
|
-
alignItems: 'center',
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
312
|
+
{
|
|
313
|
+
alignItems: 'center',
|
|
314
|
+
backgroundColor,
|
|
315
|
+
borderColor: palette.modalBorder,
|
|
316
|
+
borderRadius: 999,
|
|
317
|
+
borderWidth: 1,
|
|
318
|
+
height: 28,
|
|
337
319
|
justifyContent: 'center',
|
|
338
320
|
width: 28,
|
|
339
321
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Modal,
|
|
4
|
-
ScrollView,
|
|
5
|
-
Text,
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Modal,
|
|
4
|
+
ScrollView,
|
|
5
|
+
Text,
|
|
6
|
+
TextInput,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
8
9
|
useWindowDimensions,
|
|
9
10
|
View,
|
|
10
11
|
} from 'react-native';
|
|
@@ -88,17 +89,20 @@ const CompactFilterSelector = ({
|
|
|
88
89
|
icon = 'sliders',
|
|
89
90
|
label = '',
|
|
90
91
|
labelCaption = '',
|
|
91
|
-
onClose = null,
|
|
92
|
-
onBeforeOpen = null,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
onClose = null,
|
|
93
|
+
onBeforeOpen = null,
|
|
94
|
+
onSearchChange = null,
|
|
95
|
+
onSelect = null,
|
|
96
|
+
options = [],
|
|
97
|
+
searchable = false,
|
|
98
|
+
selectedKey = '',
|
|
96
99
|
field = '',
|
|
97
100
|
store = '',
|
|
98
101
|
themeColors = {},
|
|
99
102
|
title = '',
|
|
100
103
|
}) => {
|
|
101
|
-
const [visible, setVisible] = useState(false);
|
|
104
|
+
const [visible, setVisible] = useState(false);
|
|
105
|
+
const [searchText, setSearchText] = useState('');
|
|
102
106
|
const { width: windowWidth } = useWindowDimensions();
|
|
103
107
|
const resolvedTheme = useMemo(
|
|
104
108
|
() => buildTheme({ accentColor, themeColors }),
|
|
@@ -116,12 +120,13 @@ const CompactFilterSelector = ({
|
|
|
116
120
|
const resolvedLabelCaption = labelCaption || storeFieldLabel;
|
|
117
121
|
const resolvedTitle = title || storeFieldLabel;
|
|
118
122
|
|
|
119
|
-
const closeModal = useCallback(() => {
|
|
120
|
-
setVisible(false);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const closeModal = useCallback(() => {
|
|
124
|
+
setVisible(false);
|
|
125
|
+
setSearchText('');
|
|
126
|
+
onClose?.();
|
|
127
|
+
}, [onClose]);
|
|
123
128
|
|
|
124
|
-
const openModal = useCallback(() => {
|
|
129
|
+
const openModal = useCallback(() => {
|
|
125
130
|
if (disabled) {
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
@@ -143,7 +148,28 @@ const CompactFilterSelector = ({
|
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
setVisible(true);
|
|
146
|
-
}, [disabled, onBeforeOpen]);
|
|
151
|
+
}, [disabled, onBeforeOpen]);
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* @agents
|
|
155
|
+
* List filters must keep the column/store contract in the default component:
|
|
156
|
+
* open loads the initial page, typing delegates autocomplete to the caller
|
|
157
|
+
* that knows the column list store/action.
|
|
158
|
+
*/
|
|
159
|
+
const handleSearchChange = useCallback(value => {
|
|
160
|
+
setSearchText(value);
|
|
161
|
+
onSearchChange?.(value);
|
|
162
|
+
}, [onSearchChange]);
|
|
163
|
+
|
|
164
|
+
const filteredOptions = useMemo(() => {
|
|
165
|
+
const normalizedSearch = normalizeText(searchText).toLowerCase();
|
|
166
|
+
if (!searchable || onSearchChange || !normalizedSearch) return options;
|
|
167
|
+
|
|
168
|
+
return options.filter(option =>
|
|
169
|
+
normalizeText(option?.label).toLowerCase().includes(normalizedSearch) ||
|
|
170
|
+
normalizeText(option?.key).toLowerCase().includes(normalizedSearch),
|
|
171
|
+
);
|
|
172
|
+
}, [onSearchChange, options, searchable, searchText]);
|
|
147
173
|
|
|
148
174
|
const handleSelect = useCallback(optionKey => {
|
|
149
175
|
const shouldClose = onSelect?.(optionKey, {
|
|
@@ -229,12 +255,21 @@ const CompactFilterSelector = ({
|
|
|
229
255
|
</TouchableOpacity>
|
|
230
256
|
</View>
|
|
231
257
|
|
|
232
|
-
<ScrollView
|
|
233
|
-
style={styles.modalScroll}
|
|
234
|
-
contentContainerStyle={styles.modalContent}
|
|
235
|
-
keyboardShouldPersistTaps="handled"
|
|
236
|
-
>
|
|
237
|
-
{
|
|
258
|
+
<ScrollView
|
|
259
|
+
style={styles.modalScroll}
|
|
260
|
+
contentContainerStyle={styles.modalContent}
|
|
261
|
+
keyboardShouldPersistTaps="handled"
|
|
262
|
+
>
|
|
263
|
+
{searchable ? (
|
|
264
|
+
<TextInput
|
|
265
|
+
style={styles.searchInput}
|
|
266
|
+
value={searchText}
|
|
267
|
+
placeholder={global.t?.t(resolveStoreName(store), 'input', 'search')}
|
|
268
|
+
onChangeText={handleSearchChange}
|
|
269
|
+
/>
|
|
270
|
+
) : null}
|
|
271
|
+
|
|
272
|
+
{filteredOptions.map(option => {
|
|
238
273
|
const isSelected = option.key === selectedKey;
|
|
239
274
|
|
|
240
275
|
return (
|
|
@@ -88,11 +88,23 @@ const createStyles = (theme, dense = false, windowWidth = 0) => {
|
|
|
88
88
|
modalScroll: {
|
|
89
89
|
flexGrow: 0,
|
|
90
90
|
},
|
|
91
|
-
modalContent: {
|
|
92
|
-
paddingBottom: 10,
|
|
93
|
-
gap: 10,
|
|
94
|
-
},
|
|
95
|
-
|
|
91
|
+
modalContent: {
|
|
92
|
+
paddingBottom: 10,
|
|
93
|
+
gap: 10,
|
|
94
|
+
},
|
|
95
|
+
searchInput: {
|
|
96
|
+
minHeight: 42,
|
|
97
|
+
borderWidth: 1,
|
|
98
|
+
borderColor: theme.optionBorderColor,
|
|
99
|
+
borderRadius: 14,
|
|
100
|
+
paddingHorizontal: 12,
|
|
101
|
+
paddingVertical: 8,
|
|
102
|
+
backgroundColor: theme.backgroundColor,
|
|
103
|
+
color: theme.textColor,
|
|
104
|
+
fontSize: 13,
|
|
105
|
+
fontWeight: '700',
|
|
106
|
+
},
|
|
107
|
+
modalOption: {
|
|
96
108
|
minHeight: 50,
|
|
97
109
|
borderWidth: 1,
|
|
98
110
|
borderColor: theme.optionBorderColor,
|
|
@@ -1,62 +1,192 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { TextInput, View } from 'react-native';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { TextInput, View } from 'react-native';
|
|
3
|
+
import { getAllStores, useStore } from '@store';
|
|
4
|
+
import {
|
|
5
|
+
buildOptionsFromColumn,
|
|
6
|
+
getColumnKey,
|
|
7
|
+
isDateLikeColumn,
|
|
8
|
+
mapOptions,
|
|
9
|
+
normalizeOptionKey,
|
|
10
|
+
normalizeText,
|
|
11
|
+
resolveStoreNameFromList,
|
|
12
|
+
} from '../inputs/defaultInputUtils';
|
|
13
|
+
import CompactFilterSelector from './CompactFilterSelector';
|
|
14
|
+
import DateShortcutFilter from './DateShortcutFilter';
|
|
15
|
+
import { resolveNextDateFilterValue } from './dateFilterSelection';
|
|
16
|
+
import {
|
|
17
|
+
normalizeCollectionItems,
|
|
18
|
+
resolveColumnListLoadParams,
|
|
19
|
+
resolveListActionName,
|
|
20
|
+
stableSerialize,
|
|
21
|
+
} from '../table/DefaultTable.utils';
|
|
22
|
+
import {
|
|
23
|
+
persistTableFiltersPreference,
|
|
24
|
+
resolveDefaultTablePreferenceScope,
|
|
25
|
+
sanitizeTableFiltersPreference,
|
|
26
|
+
} from '../../utils/tableVisibleColumnsPreferences';
|
|
27
|
+
import styles from './DefaultColumnFilter.styles';
|
|
13
28
|
|
|
14
29
|
const DefaultColumnFilter = ({
|
|
15
30
|
accentColor = '#2563EB',
|
|
16
31
|
column,
|
|
17
32
|
filters = {},
|
|
18
33
|
onBeforeOpen = null,
|
|
34
|
+
onSearchChange = null,
|
|
19
35
|
getOptionsForColumn = null,
|
|
20
36
|
onChange = null,
|
|
21
37
|
storeName = '',
|
|
22
38
|
style = null,
|
|
23
|
-
}) => {
|
|
24
|
-
const
|
|
39
|
+
}) => {
|
|
40
|
+
const store = useStore(storeName);
|
|
41
|
+
const peopleStore = useStore('people');
|
|
42
|
+
const configs = store?.getters?.configs || {};
|
|
43
|
+
const [loadedItems, setLoadedItems] = useState([]);
|
|
44
|
+
const [loadedKey, setLoadedKey] = useState('');
|
|
45
|
+
const fieldName = getColumnKey(column);
|
|
46
|
+
const currentCompanyId = peopleStore?.getters?.currentCompany?.id;
|
|
47
|
+
const requestChange = useCallback(
|
|
48
|
+
(nextFieldName, value) => {
|
|
49
|
+
if (typeof onChange === 'function') {
|
|
50
|
+
onChange(nextFieldName, value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const currentFilters = store?.getters?.filters || {};
|
|
55
|
+
const nextFilters = { ...(currentFilters || {}) };
|
|
56
|
+
const isEmpty =
|
|
57
|
+
value === null ||
|
|
58
|
+
value === undefined ||
|
|
59
|
+
normalizeText(value) === '' ||
|
|
60
|
+
(Array.isArray(value) && value.length === 0);
|
|
61
|
+
|
|
62
|
+
if (isEmpty) delete nextFilters[nextFieldName];
|
|
63
|
+
else nextFilters[nextFieldName] = value;
|
|
64
|
+
|
|
65
|
+
const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
|
|
66
|
+
const tablePreferenceScope =
|
|
67
|
+
configs.tablePreferenceScope ||
|
|
68
|
+
resolveDefaultTablePreferenceScope({ storeName });
|
|
69
|
+
const persistedFilters = sanitizeTableFiltersPreference({
|
|
70
|
+
columns,
|
|
71
|
+
filters: nextFilters,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
persistTableFiltersPreference(tablePreferenceScope, persistedFilters);
|
|
75
|
+
|
|
76
|
+
if (typeof store?.actions?.setFilters === 'function') {
|
|
77
|
+
store.actions.setFilters(nextFilters);
|
|
78
|
+
} else if (store?.getters) {
|
|
79
|
+
store.getters.filters = nextFilters;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
configs.onFilterChange?.(nextFilters);
|
|
83
|
+
},
|
|
84
|
+
[configs, onChange, store, storeName],
|
|
85
|
+
);
|
|
86
|
+
const resolveOptions = useCallback(
|
|
87
|
+
targetColumn => {
|
|
88
|
+
const configOptions = configs.getOptionsForColumn?.(targetColumn);
|
|
89
|
+
const propOptions = getOptionsForColumn?.(targetColumn);
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(configOptions)) return configOptions;
|
|
92
|
+
if (Array.isArray(propOptions)) return propOptions;
|
|
93
|
+
if (targetColumn === column && loadedItems.length > 0) return loadedItems;
|
|
94
|
+
|
|
95
|
+
return undefined;
|
|
96
|
+
},
|
|
97
|
+
[column, configs, getOptionsForColumn, loadedItems],
|
|
98
|
+
);
|
|
99
|
+
const loadListOptions = useCallback(
|
|
100
|
+
(searchValue = '') => {
|
|
101
|
+
const listStoreName = resolveStoreNameFromList(column?.list);
|
|
102
|
+
const actionName = resolveListActionName(column?.list);
|
|
103
|
+
const listStore = getAllStores?.()?.[listStoreName];
|
|
104
|
+
const listAction = listStore?.actions?.[actionName];
|
|
105
|
+
|
|
106
|
+
if (!listStoreName || typeof listAction !== 'function') {
|
|
107
|
+
return Promise.resolve([]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const listLoadParams = resolveColumnListLoadParams({
|
|
111
|
+
column,
|
|
112
|
+
currentCompanyId,
|
|
113
|
+
requestParams: configs.requestParams || {},
|
|
114
|
+
searchValue,
|
|
115
|
+
});
|
|
116
|
+
const nextLoadedKey = stableSerialize({
|
|
117
|
+
actionName,
|
|
118
|
+
listLoadParams,
|
|
119
|
+
listStoreName,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!normalizeText(searchValue) && loadedKey === nextLoadedKey && loadedItems.length > 0) {
|
|
123
|
+
return Promise.resolve(loadedItems);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return Promise.resolve(
|
|
127
|
+
listAction({
|
|
128
|
+
...listLoadParams,
|
|
129
|
+
__storeMeta: {
|
|
130
|
+
dedupeKey: `default-column-filter-list:${fieldName}:${nextLoadedKey}`,
|
|
131
|
+
skipSystemError: true,
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
.then(response => {
|
|
136
|
+
const items = normalizeCollectionItems(response);
|
|
137
|
+
setLoadedItems(items);
|
|
138
|
+
setLoadedKey(nextLoadedKey);
|
|
139
|
+
return items;
|
|
140
|
+
})
|
|
141
|
+
.catch(() => []);
|
|
142
|
+
},
|
|
143
|
+
[
|
|
144
|
+
column,
|
|
145
|
+
configs.requestParams,
|
|
146
|
+
currentCompanyId,
|
|
147
|
+
fieldName,
|
|
148
|
+
loadedItems,
|
|
149
|
+
loadedKey,
|
|
150
|
+
],
|
|
151
|
+
);
|
|
25
152
|
|
|
26
153
|
if (column?.filter === false || column?.filters === false) {
|
|
27
154
|
return <View style={style} />;
|
|
28
155
|
}
|
|
29
156
|
|
|
30
|
-
if (column
|
|
157
|
+
if (isDateLikeColumn(column)) {
|
|
31
158
|
const filterValue = filters?.[fieldName] || {};
|
|
32
159
|
return (
|
|
33
160
|
<View style={[style, styles.filterCell]}>
|
|
34
|
-
<DateShortcutFilter
|
|
35
|
-
dense
|
|
36
|
-
store={storeName}
|
|
37
|
-
field={fieldName}
|
|
38
|
-
|
|
161
|
+
<DateShortcutFilter
|
|
162
|
+
dense
|
|
163
|
+
store={storeName}
|
|
164
|
+
field={fieldName}
|
|
165
|
+
labelCaption={global.t?.t(storeName, 'label', column?.label || fieldName)}
|
|
166
|
+
value={filterValue.shortcut || 'all'}
|
|
39
167
|
customRange={filterValue.customRange || { from: '', to: '' }}
|
|
40
|
-
onChange={optionKey =>
|
|
41
|
-
|
|
42
|
-
fieldName,
|
|
43
|
-
resolveNextDateFilterValue(filterValue, optionKey),
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
onCustomRangeChange={range =>
|
|
47
|
-
...(filterValue || {}),
|
|
48
|
-
shortcut: 'custom',
|
|
49
|
-
customRange: range,
|
|
168
|
+
onChange={optionKey =>
|
|
169
|
+
requestChange(
|
|
170
|
+
fieldName,
|
|
171
|
+
resolveNextDateFilterValue(filterValue, optionKey),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
onCustomRangeChange={range => requestChange(fieldName, {
|
|
175
|
+
...(filterValue || {}),
|
|
176
|
+
shortcut: 'custom',
|
|
177
|
+
customRange: range,
|
|
50
178
|
})}
|
|
51
179
|
/>
|
|
52
180
|
</View>
|
|
53
181
|
);
|
|
54
182
|
}
|
|
55
183
|
|
|
56
|
-
if (column?.list) {
|
|
57
|
-
const rawOptions =
|
|
184
|
+
if (column?.list) {
|
|
185
|
+
const rawOptions = loadedItems.length > 0
|
|
186
|
+
? mapOptions(column, loadedItems, storeName)
|
|
187
|
+
: buildOptionsFromColumn(column, resolveOptions, storeName);
|
|
58
188
|
const options = [
|
|
59
|
-
{ key: '', label: global.t?.t(storeName, 'label', 'select')
|
|
189
|
+
{ key: '', label: global.t?.t(storeName, 'label', 'select') },
|
|
60
190
|
...rawOptions,
|
|
61
191
|
];
|
|
62
192
|
const selectedKey = normalizeOptionKey(filters?.[fieldName]);
|
|
@@ -69,16 +199,18 @@ const DefaultColumnFilter = ({
|
|
|
69
199
|
store={storeName}
|
|
70
200
|
field={fieldName}
|
|
71
201
|
icon="filter"
|
|
72
|
-
onBeforeOpen={onBeforeOpen}
|
|
202
|
+
onBeforeOpen={onBeforeOpen || (() => loadListOptions())}
|
|
203
|
+
onSearchChange={onSearchChange || (value => loadListOptions(value))}
|
|
73
204
|
accentColor={accentColor}
|
|
74
205
|
active={Boolean(selectedKey)}
|
|
75
206
|
label={selected?.label || options[0]?.label || ''}
|
|
76
|
-
options={options}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
207
|
+
options={options}
|
|
208
|
+
searchable
|
|
209
|
+
selectedKey={selectedKey}
|
|
210
|
+
onSelect={optionKey => {
|
|
211
|
+
requestChange(fieldName, optionKey);
|
|
212
|
+
return true;
|
|
213
|
+
}}
|
|
82
214
|
/>
|
|
83
215
|
</View>
|
|
84
216
|
);
|
|
@@ -87,11 +219,11 @@ const DefaultColumnFilter = ({
|
|
|
87
219
|
return (
|
|
88
220
|
<View style={[style, styles.filterCell]}>
|
|
89
221
|
<TextInput
|
|
90
|
-
style={styles.filterInput}
|
|
91
|
-
value={normalizeText(filters?.[fieldName])}
|
|
92
|
-
placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
|
|
93
|
-
onChangeText={value =>
|
|
94
|
-
/>
|
|
222
|
+
style={styles.filterInput}
|
|
223
|
+
value={normalizeText(filters?.[fieldName])}
|
|
224
|
+
placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
|
|
225
|
+
onChangeText={value => requestChange(fieldName, value)}
|
|
226
|
+
/>
|
|
95
227
|
</View>
|
|
96
228
|
);
|
|
97
229
|
};
|