15768: fixed doubleclick bug Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox...
[arvados-workbench2.git] / src / components / data-table / data-table.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, IconButton } from '@material-ui/core';
7 import classnames from 'classnames';
8 import { DataColumn, SortDirection } from './data-column';
9 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
10 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
11 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
12 import { countNodes, getTreeDirty } from 'models/tree';
13 import { IconType, PendingIcon } from 'components/icon/icon';
14 import { SvgIconProps } from '@material-ui/core/SvgIcon';
15 import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
16 import { Checkbox } from '@material-ui/core';
17 import { createTree } from 'models/tree';
18
19 export type DataColumns<I, R> = Array<DataColumn<I, R>>;
20
21 export enum DataTableFetchMode {
22     PAGINATED,
23     INFINITE,
24 }
25
26 export interface DataTableDataProps<I> {
27     items: I[];
28     columns: DataColumns<I, any>;
29     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
30     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: I) => void;
31     onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
32     onSortToggle: (column: DataColumn<I, any>) => void;
33     onFiltersChange: (filters: DataTableFilters, column: DataColumn<I, any>) => void;
34     extractKey?: (item: I) => React.Key;
35     working?: boolean;
36     defaultViewIcon?: IconType;
37     defaultViewMessages?: string[];
38     currentItemUuid?: string;
39     currentRoute?: string;
40 }
41
42 type CssRules = 'tableBody' | 'root' | 'content' | 'noItemsInfo' | 'checkBoxCell' | 'tableCell' | 'arrow' | 'arrowButton' | 'tableCellWorkflows' | 'loader';
43
44 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
45     root: {
46         width: '100%',
47     },
48     content: {
49         display: 'inline-block',
50         width: '100%',
51     },
52     tableBody: {
53         background: theme.palette.background.paper,
54     },
55     loader: {
56         left: '50%',
57         marginLeft: '-84px',
58         position: 'absolute',
59     },
60     noItemsInfo: {
61         textAlign: 'center',
62         padding: theme.spacing.unit,
63     },
64     checkBoxCell: {
65         // border: '1px dotted green',
66         padding: '0',
67         cursor: 'pointer',
68     },
69     tableCell: {
70         wordWrap: 'break-word',
71         paddingRight: '24px',
72         color: '#737373',
73     },
74     tableCellWorkflows: {
75         '&:nth-last-child(2)': {
76             padding: '0px',
77             maxWidth: '48px',
78         },
79         '&:last-child': {
80             padding: '0px',
81             paddingRight: '24px',
82             width: '48px',
83         },
84     },
85     arrow: {
86         margin: 0,
87     },
88     arrowButton: {
89         color: theme.palette.text.primary,
90     },
91 });
92
93 type DataTableState = {
94     // checkedList: Record<string, boolean>;
95 };
96
97 type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules> & DataTableState;
98
99 export const DataTable = withStyles(styles)(
100     class Component<T> extends React.Component<DataTableProps<T>> {
101         state: DataTableState = {
102             // checkedList: {},
103         };
104
105         componentDidUpdate(prevProps: Readonly<DataTableProps<T>>) {
106             if (prevProps.items !== this.props.items) {
107                 // console.log('BEFORE INIT', this.state);
108                 this.initializeCheckedList(this.props.items);
109                 // console.log('AFTER INIT', this.state);
110             }
111         }
112
113         checkBoxColumn: DataColumn<any, any> = {
114             name: 'checkBoxColumn',
115             selected: true,
116             configurable: false,
117             filters: createTree(),
118             render: (uuid) => (
119                 <input
120                     type='checkbox'
121                     name={uuid}
122                     color='primary'
123                     checked={this.state[uuid] ?? false}
124                     onChange={() => this.handleCheck(uuid)}
125                     onDoubleClick={(ev) => ev.stopPropagation()}
126                 ></input>
127             ),
128         };
129
130         initializeCheckedList = async (uuids: any[]) => {
131             let checkedList = this.state;
132             uuids.forEach((uuid) => {
133                 if (!checkedList.hasOwnProperty[uuid]) {
134                     checkedList[uuid] = false;
135                 }
136             });
137         };
138
139         handleCheck = (uuid: string) => {
140             const newCheckedList = { ...this.state };
141             newCheckedList[uuid] = !this.state[uuid];
142             this.setState(newCheckedList);
143         };
144
145         handleSelectAll = () => {
146             const newCheckedList = { ...this.state };
147             for (const key in newCheckedList) {
148                 newCheckedList[key] = true;
149             }
150             this.setState(newCheckedList);
151         };
152
153         handleDeselectAll = () => {
154             const newCheckedList = { ...this.state };
155             for (const key in newCheckedList) {
156                 newCheckedList[key] = false;
157             }
158             this.setState(newCheckedList);
159         };
160
161         handleInvertSelect = () => {
162             const newCheckedList = { ...this.state };
163             for (const key in newCheckedList) {
164                 newCheckedList[key] = !this.state[key];
165             }
166             this.setState(newCheckedList);
167         };
168
169         render() {
170             const { items, classes, working, columns } = this.props;
171             if (columns[0].name === this.checkBoxColumn.name) columns.shift();
172             columns.unshift(this.checkBoxColumn);
173             console.log('ONCHECK', this.state);
174
175             return (
176                 <div className={classes.root}>
177                     <div className={classes.content}>
178                         <Table>
179                             <TableHead>
180                                 <TableRow>{this.mapVisibleColumns(this.renderHeadCell)}</TableRow>
181                             </TableHead>
182                             <TableBody className={classes.tableBody}>{!working && items.map(this.renderBodyRow)}</TableBody>
183                         </Table>
184                         {!!working && (
185                             <div className={classes.loader}>
186                                 <DataTableDefaultView icon={PendingIcon} messages={['Loading data, please wait.']} />
187                             </div>
188                         )}
189                         {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
190                     </div>
191                 </div>
192             );
193         }
194
195         renderNoItemsPlaceholder = (columns: DataColumns<T, any>) => {
196             const dirty = columns.some((column) => getTreeDirty('')(column.filters));
197             return <DataTableDefaultView icon={this.props.defaultViewIcon} messages={this.props.defaultViewMessages} filtersApplied={dirty} />;
198         };
199
200         renderHeadCell = (column: DataColumn<T, any>, index: number) => {
201             const { name, key, renderHeader, filters, sort } = column;
202             const { onSortToggle, onFiltersChange, classes } = this.props;
203             return index === 0 ? (
204                 <TableCell key={key || index} className={classes.checkBoxCell}>
205                     <div onClick={this.handleSelectAll}>select all</div>
206                     <div onClick={this.handleDeselectAll}>deselect all</div>
207                     <div onClick={this.handleInvertSelect}>invert selection</div>
208                 </TableCell>
209             ) : (
210                 <TableCell className={classes.tableCell} key={key || index}>
211                     {renderHeader ? (
212                         renderHeader()
213                     ) : countNodes(filters) > 0 ? (
214                         <DataTableFiltersPopover
215                             name={`${name} filters`}
216                             mutuallyExclusive={column.mutuallyExclusiveFilters}
217                             onChange={(filters) => onFiltersChange && onFiltersChange(filters, column)}
218                             filters={filters}
219                         >
220                             {name}
221                         </DataTableFiltersPopover>
222                     ) : sort ? (
223                         <TableSortLabel
224                             active={sort.direction !== SortDirection.NONE}
225                             direction={sort.direction !== SortDirection.NONE ? sort.direction : undefined}
226                             IconComponent={this.ArrowIcon}
227                             hideSortIcon
228                             onClick={() => onSortToggle && onSortToggle(column)}
229                         >
230                             {name}
231                         </TableSortLabel>
232                     ) : (
233                         <span>{name}</span>
234                     )}
235                 </TableCell>
236             );
237         };
238
239         ArrowIcon = ({ className, ...props }: SvgIconProps) => (
240             <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}>
241                 <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} />
242             </IconButton>
243         );
244
245         renderBodyRow = (item: any, index: number) => {
246             const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
247             return (
248                 <TableRow
249                     hover
250                     key={extractKey ? extractKey(item) : index}
251                     onClick={(event) => onRowClick && onRowClick(event, item)}
252                     onContextMenu={this.handleRowContextMenu(item)}
253                     onDoubleClick={(event) => onRowDoubleClick && onRowDoubleClick(event, item)}
254                     selected={item === currentItemUuid}
255                 >
256                     {this.mapVisibleColumns((column, index) => (
257                         <TableCell
258                             key={column.key || index}
259                             className={currentRoute === '/workflows' ? classes.tableCellWorkflows : index === 0 ? classes.checkBoxCell : classes.tableCell}
260                         >
261                             {column.render(item)}
262                         </TableCell>
263                     ))}
264                 </TableRow>
265             );
266         };
267
268         mapVisibleColumns = (fn: (column: DataColumn<T, any>, index: number) => React.ReactElement<any>) => {
269             return this.props.columns.filter((column) => column.selected).map(fn);
270         };
271
272         handleRowContextMenu = (item: T) => (event: React.MouseEvent<HTMLElement>) => this.props.onContextMenu(event, item);
273     }
274 );