@arcblock/ux 2.1.6 → 2.1.9

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.
@@ -0,0 +1,406 @@
1
+ import React, { useState, useRef, useEffect, isValidElement } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TableFilter, TableViewCol } from 'mui-datatables';
4
+ import styled from 'styled-components';
5
+ import ReactToPrint, { PrintContextConsumer } from 'react-to-print';
6
+ import IconButton from '@mui/material/IconButton';
7
+ import Tooltip from '@mui/material/Tooltip';
8
+ import DownloadIcon from '@mui/icons-material/CloudDownload';
9
+ import PrintIcon from '@mui/icons-material/Print';
10
+ import ViewColumnIcon from '@mui/icons-material/ViewColumn';
11
+ import FilterIcon from '@mui/icons-material/FilterList';
12
+ import Popover from '@mui/material/Popover';
13
+ import MoreVertIcon from '@mui/icons-material/MoreVert';
14
+ import Menu from '@mui/material/Menu';
15
+ import MenuItem from '@mui/material/MenuItem';
16
+ import ListItemIcon from '@mui/material/ListItemIcon';
17
+ import ListItemText from '@mui/material/ListItemText';
18
+ import useMediaQuery from '@mui/material/useMediaQuery';
19
+ import { useTheme } from '@mui/material/styles';
20
+ import LinearProgress from '@mui/material/LinearProgress';
21
+ import { handleCSVDownload } from './utils';
22
+ import TableSearch from './TableSearch';
23
+ import { useDatatableContext } from './DatatableContext';
24
+
25
+ function useMobile() {
26
+ const theme = useTheme();
27
+ return useMediaQuery(theme.breakpoints.down('sm'));
28
+ }
29
+
30
+ export default function CustomToolbar(props) {
31
+ const [menuIconEl, setMenuIconEl] = useState(null);
32
+ const moreBtn = useRef(null);
33
+ const isMobile = useMobile();
34
+ const toolbarId = useRef(Math.random().toString(32).slice(2));
35
+ const [searchOpened, setSearchOpened] = useState(false);
36
+ const { customButtons, loading, disabled } = useDatatableContext();
37
+
38
+ const {
39
+ data,
40
+ options,
41
+ components,
42
+ columns,
43
+ filterList,
44
+ filterData,
45
+ filterUpdate,
46
+ resetFilters,
47
+ updateFilterByType,
48
+ toggleViewColumn,
49
+ updateColumns,
50
+ title,
51
+ searchText,
52
+ searchTextUpdate,
53
+ searchClose,
54
+ } = props;
55
+
56
+ const customToolbarEle = options.customToolbar ? options.customToolbar(props) : '';
57
+
58
+ const { search, downloadCsv, print, viewColumns, filterTable } = options.textLabels.toolbar;
59
+
60
+ const hideSearch = options.search === false || options.search === 'false';
61
+ const hidePrint = options.print === false || options.print === 'false';
62
+
63
+ const TableFilterComponent = components.TableFilter || TableFilter;
64
+ const TableViewColComponent = components.TableViewCol || TableViewCol;
65
+
66
+ useEffect(() => {
67
+ if (loading || disabled) {
68
+ setAllPopsEl({});
69
+ }
70
+ }, [loading, disabled]);
71
+
72
+ const printArea = func => {
73
+ return (
74
+ <ReactToPrint content={() => props.tableRef()}>
75
+ <PrintContextConsumer>{func}</PrintContextConsumer>
76
+ </ReactToPrint>
77
+ );
78
+ };
79
+
80
+ const getPopId = key => `toolbar-pop-${toolbarId.current}-${key}`;
81
+
82
+ const defaultButtons = [];
83
+
84
+ // download/viewColumns/filter button behaviours, rendered using custom button logic
85
+ if (!(options.download === false || options.download === 'false')) {
86
+ defaultButtons.push({
87
+ Icon: DownloadIcon,
88
+ title: downloadCsv,
89
+ onClick: () => {
90
+ handleCSVDownload(props);
91
+ },
92
+ });
93
+ }
94
+
95
+ if (!(options.viewColumns === false || options.viewColumns === 'false')) {
96
+ defaultButtons.push({
97
+ Icon: ViewColumnIcon,
98
+ title: viewColumns,
99
+ popRender() {
100
+ return (
101
+ <TableViewColComponent
102
+ data={data}
103
+ columns={columns}
104
+ options={options}
105
+ onColumnUpdate={toggleViewColumn}
106
+ updateColumns={updateColumns}
107
+ components={components}
108
+ />
109
+ );
110
+ },
111
+ });
112
+ }
113
+
114
+ if (!(options.filter === false || options.filter === 'false')) {
115
+ defaultButtons.push({
116
+ Icon: FilterIcon,
117
+ title: filterTable,
118
+ popRender() {
119
+ return (
120
+ <TableFilterComponent
121
+ customFooter={options.customFilterDialogFooter}
122
+ columns={columns}
123
+ options={options}
124
+ filterList={filterList}
125
+ filterData={filterData}
126
+ onFilterUpdate={filterUpdate}
127
+ onFilterReset={resetFilters}
128
+ updateFilterByType={updateFilterByType}
129
+ components={components}
130
+ />
131
+ );
132
+ },
133
+ });
134
+ }
135
+
136
+ const showMore =
137
+ [!hidePrint, ...defaultButtons, ...customButtons].filter(e => !!e).length > 1 && isMobile;
138
+
139
+ const allPops = [];
140
+ const [allPopsEl, setAllPopsEl] = useState({});
141
+
142
+ // Large screens show the toolbar buttons directly, small screens show the drop-down menu style buttons
143
+ // The right-hand button of the form toolbar in desktop mode
144
+ const toolbarButtons = [...defaultButtons, ...customButtons].map((e, index) => {
145
+ if (isValidElement(e)) {
146
+ return e;
147
+ }
148
+
149
+ const popId = getPopId(index);
150
+
151
+ if (e.Icon) {
152
+ const { Icon, popRender } = e;
153
+ // When popRender is present, clicking the button will bubble up the content returned by the popRender
154
+ if (popRender) {
155
+ allPops.push(
156
+ <Popover
157
+ open={!!allPopsEl[popId]}
158
+ anchorEl={() => allPopsEl[popId]}
159
+ onClose={() => {
160
+ setAllPopsEl({});
161
+ }}
162
+ key={popId}
163
+ anchorOrigin={{
164
+ vertical: 'bottom',
165
+ horizontal: 'right',
166
+ }}>
167
+ <div>{popRender()}</div>
168
+ </Popover>
169
+ );
170
+ }
171
+
172
+ return (
173
+ <Tooltip title={e.title} key={popId}>
174
+ <IconButton
175
+ data-testid={`${e.title}-iconButton`}
176
+ id={`btn-${popId}`}
177
+ aria-label={e.title}
178
+ onClick={() => {
179
+ if (e.onClick) {
180
+ e.onClick();
181
+ }
182
+
183
+ if (popRender) {
184
+ // On the large screen, the bubble is positioned at the corresponding button
185
+ setAllPopsEl({
186
+ [popId]: document.getElementById(`btn-${popId}`),
187
+ });
188
+ }
189
+ }}>
190
+ <Icon />
191
+ </IconButton>
192
+ </Tooltip>
193
+ );
194
+ }
195
+
196
+ return e;
197
+ });
198
+
199
+ // The toolbar menu in the mobile to replace toolbarButtons
200
+ const menuItems = [...defaultButtons, ...customButtons].map((e, index) => {
201
+ const popId = getPopId(index);
202
+
203
+ let content;
204
+
205
+ if (isValidElement(e)) {
206
+ content = e;
207
+ } else if (e.Icon) {
208
+ const { Icon } = e;
209
+
210
+ content = (
211
+ <>
212
+ <ListItemIcon>
213
+ <Icon fontSize="small" />
214
+ </ListItemIcon>
215
+ <ListItemText>{e.title}</ListItemText>
216
+ </>
217
+ );
218
+ }
219
+
220
+ return (
221
+ <MenuItem
222
+ key={popId}
223
+ onClick={() => {
224
+ setMenuIconEl(null);
225
+ if (e.onClick) {
226
+ e.onClick();
227
+ }
228
+
229
+ if (e.popRender) {
230
+ // On the small screen, the bubbles are positioned at the three dot buttons
231
+ setAllPopsEl({
232
+ [popId]: moreBtn.current,
233
+ });
234
+ }
235
+ }}>
236
+ {content}
237
+ </MenuItem>
238
+ );
239
+ });
240
+
241
+ return (
242
+ <div>
243
+ <Container>
244
+ <div
245
+ className={`custom-toobar-title ${
246
+ isMobile && searchOpened && isValidElement(title) ? 'toobar-title-hidden' : ''
247
+ }`}>
248
+ <div className="custom-toobar-title-inner">
249
+ <span>{title}</span>
250
+ </div>
251
+ </div>
252
+ <div className={`custom-toobar-btns ${loading || disabled ? 'toobar-btns-disabled' : ''}`}>
253
+ {!hideSearch && (
254
+ <TableSearch
255
+ search={search}
256
+ options={options}
257
+ searchText={searchText}
258
+ searchTextUpdate={searchTextUpdate}
259
+ searchClose={searchClose}
260
+ onSearchOpen={setSearchOpened}
261
+ />
262
+ )}
263
+ {!showMore && (
264
+ <>
265
+ {!hidePrint &&
266
+ printArea(({ handlePrint }) => (
267
+ <span>
268
+ <Tooltip title={print}>
269
+ <IconButton
270
+ data-testid={`${print}-iconButton`}
271
+ aria-label={print}
272
+ disabled={options.print === 'disabled'}
273
+ onClick={handlePrint}>
274
+ <PrintIcon />
275
+ </IconButton>
276
+ </Tooltip>
277
+ </span>
278
+ ))}
279
+
280
+ {toolbarButtons}
281
+ </>
282
+ )}
283
+ {showMore && (
284
+ <IconButton
285
+ ref={moreBtn}
286
+ aria-haspopup="true"
287
+ aria-expanded={menuIconEl ? 'true' : undefined}
288
+ onClick={event => setMenuIconEl(event.currentTarget)}
289
+ style={{ flexShrink: 0 }}>
290
+ <MoreVertIcon />
291
+ </IconButton>
292
+ )}
293
+ </div>
294
+ {customToolbarEle}
295
+ </Container>
296
+
297
+ <Menu
298
+ anchorEl={menuIconEl}
299
+ open={!!menuIconEl}
300
+ onClose={() => setMenuIconEl(null)}
301
+ MenuListProps={{
302
+ 'aria-labelledby': 'more-button',
303
+ }}>
304
+ {!hidePrint &&
305
+ printArea(({ handlePrint }) => (
306
+ <MenuItem
307
+ onClick={() => {
308
+ setMenuIconEl(null);
309
+ handlePrint();
310
+ }}>
311
+ <ListItemIcon>
312
+ <PrintIcon fontSize="small" />
313
+ </ListItemIcon>
314
+ <ListItemText>{print}</ListItemText>
315
+ </MenuItem>
316
+ ))}
317
+ {menuItems}
318
+ </Menu>
319
+ {allPops.map((e, index) => (
320
+ <div key={getPopId(index)}>{e}</div>
321
+ ))}
322
+ {loading && <LinearProgress />}
323
+ </div>
324
+ );
325
+ }
326
+
327
+ CustomToolbar.propTypes = {
328
+ data: PropTypes.array,
329
+ options: PropTypes.object.isRequired,
330
+ components: PropTypes.object,
331
+ columns: PropTypes.array.isRequired,
332
+ filterList: PropTypes.array,
333
+ filterData: PropTypes.array,
334
+ filterUpdate: PropTypes.func.isRequired,
335
+ resetFilters: PropTypes.func.isRequired,
336
+ updateFilterByType: PropTypes.func.isRequired,
337
+ toggleViewColumn: PropTypes.func.isRequired,
338
+ updateColumns: PropTypes.func.isRequired,
339
+ title: PropTypes.any,
340
+ searchText: PropTypes.any,
341
+ searchTextUpdate: PropTypes.func.isRequired,
342
+ searchClose: PropTypes.func.isRequired,
343
+ tableRef: PropTypes.func.isRequired,
344
+ };
345
+
346
+ CustomToolbar.defaultProps = {
347
+ data: [],
348
+ components: {},
349
+ filterList: [],
350
+ filterData: [],
351
+ title: '',
352
+ searchText: null,
353
+ };
354
+
355
+ const Container = styled.div`
356
+ display: flex;
357
+ align-items: center;
358
+ height: 56px;
359
+ .custom-toobar-title {
360
+ position: relative;
361
+ flex: 1;
362
+ font-size: 18px;
363
+ font-weight: 800;
364
+ height: 56px;
365
+ transition: all ease 0.3s;
366
+ &-inner {
367
+ line-height: 56px;
368
+ width: 100%;
369
+ height: 56px;
370
+ position: absolute;
371
+ left: 0;
372
+ top: 0;
373
+ span {
374
+ display: inline-block;
375
+ max-width: 100%;
376
+ white-space: nowrap;
377
+ text-overflow: ellipsis;
378
+ overflow: hidden;
379
+ }
380
+ }
381
+ }
382
+ .custom-toobar-btns {
383
+ display: flex;
384
+ justify-content: center;
385
+ align-items: center;
386
+ &.toobar-btns-disabled {
387
+ position: relative;
388
+ opacity: 0.5;
389
+ &:after {
390
+ position: absolute;
391
+ display: block;
392
+ z-index: 2;
393
+ width: 100%;
394
+ height: 100%;
395
+ left: 0;
396
+ top: 0;
397
+ content: '';
398
+ cursor: not-allowed;
399
+ }
400
+ }
401
+ }
402
+ .toobar-title-hidden {
403
+ opacity: 0;
404
+ cursor: none;
405
+ }
406
+ `;
@@ -0,0 +1,32 @@
1
+ import React, { createContext, useContext, useState } from 'react';
2
+
3
+ const DatatableContext = createContext({});
4
+
5
+ const { Provider } = DatatableContext;
6
+
7
+ // eslint-disable-next-line react/prop-types
8
+ const DatatableProvide = ({ children }) => {
9
+ const [customButtons, setCustomButtons] = useState([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [disabled, setDisabled] = useState(false);
12
+ const [filterLabel, setFilterLabel] = useState('Filter');
13
+
14
+ const value = {
15
+ customButtons,
16
+ setCustomButtons,
17
+ filterLabel,
18
+ setFilterLabel,
19
+ loading,
20
+ setLoading,
21
+ disabled,
22
+ setDisabled,
23
+ };
24
+
25
+ return <Provider value={value}>{children}</Provider>;
26
+ };
27
+
28
+ function useDatatableContext() {
29
+ return useContext(DatatableContext);
30
+ }
31
+
32
+ export { DatatableProvide, useDatatableContext };
@@ -0,0 +1,130 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import IconButton from '@mui/material/IconButton';
4
+ import Tooltip from '@mui/material/Tooltip';
5
+ import SearchIcon from '@mui/icons-material/Search';
6
+ import TextField from '@mui/material/TextField';
7
+ import ClearIcon from '@mui/icons-material/Clear';
8
+ import styled from 'styled-components';
9
+
10
+ export default function TableSearch({
11
+ search,
12
+ options,
13
+ searchText,
14
+ searchTextUpdate,
15
+ searchClose,
16
+ onSearchOpen,
17
+ }) {
18
+ const [inputMode, setInputMode] = useState(false);
19
+
20
+ const clickSearchIcon = () => {
21
+ setInputMode(true);
22
+ onSearchOpen(true);
23
+ };
24
+
25
+ const onChange = event => {
26
+ searchTextUpdate(event.currentTarget.value);
27
+ };
28
+
29
+ const clickClose = () => {
30
+ setInputMode(false);
31
+ searchClose();
32
+ onSearchOpen(false);
33
+ };
34
+
35
+ return (
36
+ <Container>
37
+ {inputMode ? (
38
+ <div className="toolbar-search-icon-placeholder">
39
+ <SearchIcon />
40
+ </div>
41
+ ) : (
42
+ <Tooltip title={search} disableFocusListener>
43
+ <IconButton
44
+ aria-label={search}
45
+ data-testid={`${search}-iconButton`}
46
+ disabled={options.search === 'disabled'}
47
+ onClick={clickSearchIcon}>
48
+ <SearchIcon />
49
+ </IconButton>
50
+ </Tooltip>
51
+ )}
52
+
53
+ <div className={`toolbar-search-area ${inputMode ? 'toolbar-btn-show' : ''}`}>
54
+ {inputMode && (
55
+ <TextField
56
+ variant="standard"
57
+ spacing={2}
58
+ onChange={onChange}
59
+ value={searchText || ''}
60
+ autoFocus
61
+ />
62
+ )}
63
+ </div>
64
+ <div className={`toolbar-search-close ${inputMode ? 'toolbar-btn-show' : ''}`}>
65
+ <IconButton onClick={clickClose}>
66
+ <ClearIcon />
67
+ </IconButton>
68
+ </div>
69
+ </Container>
70
+ );
71
+ }
72
+
73
+ TableSearch.propTypes = {
74
+ search: PropTypes.string,
75
+ searchText: PropTypes.string,
76
+ onSearchOpen: PropTypes.func,
77
+ options: PropTypes.object.isRequired,
78
+ searchTextUpdate: PropTypes.func.isRequired,
79
+ searchClose: PropTypes.func.isRequired,
80
+ };
81
+
82
+ TableSearch.defaultProps = {
83
+ search: '',
84
+ searchText: '',
85
+ onSearchOpen: () => {},
86
+ };
87
+
88
+ const Container = styled.div`
89
+ display: flex;
90
+ align-items: center;
91
+ .toolbar-search-area {
92
+ width: 0;
93
+ transition: all ease 0.3s;
94
+ overflow: hidden;
95
+ .MuiFormControl-root {
96
+ width: inherit;
97
+ margin: 0 12px;
98
+ }
99
+ &.toolbar-btn-show {
100
+ width: 260px;
101
+ padding-left: 8px;
102
+
103
+ ${props => props.theme.breakpoints.down('md')} {
104
+ width: 200px;
105
+ }
106
+
107
+ ${props => props.theme.breakpoints.down('sm')} {
108
+ width: 180px;
109
+ }
110
+ &.small-textfield {
111
+ width: 200px;
112
+ }
113
+ }
114
+ }
115
+ .toolbar-search-close {
116
+ width: 0;
117
+ transition: all ease 0.3s;
118
+ overflow: hidden;
119
+ &.toolbar-btn-show {
120
+ width: 40px;
121
+ }
122
+ }
123
+ .toolbar-search-icon-placeholder {
124
+ display: flex;
125
+ justify-content: center;
126
+ align-items: center;
127
+ width: 40px;
128
+ height: 40px;
129
+ }
130
+ `;