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