15768: selected list passes into popover functions Arvados-DCO-1.1-Signed-off-by...
[arvados-workbench2.git] / src / components / data-table / data-table.tsx
index d9157a6a5d0e14851f141309c8a89f96af818c50..b36d546e420599bed92e23d1418fab3d7689cb65 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
+import React from 'react';
+import {
+    Table,
+    TableBody,
+    TableRow,
+    TableCell,
+    TableHead,
+    TableSortLabel,
+    StyleRulesCallback,
+    Theme,
+    WithStyles,
+    withStyles,
+    IconButton,
+    Tooltip,
+} from '@material-ui/core';
+import classnames from 'classnames';
 import { DataColumn, SortDirection } from './data-column';
 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
+import { DataTableMultiselectPopover } from '../data-table-multiselect-popover/data-table-multiselect-popover';
 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
-import { countNodes } from '~/models/tree';
-
-export type DataColumns<T> = Array<DataColumn<T>>;
-
-export interface DataTableDataProps<T> {
-    items: T[];
-    columns: DataColumns<T>;
-    onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
-    onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
-    onSortToggle: (column: DataColumn<T>) => void;
-    onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
-    extractKey?: (item: T) => React.Key;
+import { countNodes, getTreeDirty } from 'models/tree';
+import { IconType, PendingIcon } from 'components/icon/icon';
+import { SvgIconProps } from '@material-ui/core/SvgIcon';
+import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
+import { createTree } from 'models/tree';
+import arraysAreCongruent from 'validators/arrays-are-congruent';
+import { DataTableMultiselectOption } from '../data-table-multiselect-popover/data-table-multiselect-popover';
+
+export type DataColumns<I, R> = Array<DataColumn<I, R>>;
+
+export enum DataTableFetchMode {
+    PAGINATED,
+    INFINITE,
+}
+
+export interface DataTableDataProps<I> {
+    items: I[];
+    columns: DataColumns<I, any>;
+    onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: I) => void;
+    onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
+    onSortToggle: (column: DataColumn<I, any>) => void;
+    onFiltersChange: (filters: DataTableFilters, column: DataColumn<I, any>) => void;
+    extractKey?: (item: I) => React.Key;
     working?: boolean;
-    defaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
+    currentItemUuid?: string;
+    currentRoute?: string;
 }
 
-type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell';
+type CssRules =
+    | 'tableBody'
+    | 'root'
+    | 'content'
+    | 'noItemsInfo'
+    | 'checkBoxHead'
+    | 'checkBoxCell'
+    | 'checkBox'
+    | 'tableCell'
+    | 'arrow'
+    | 'arrowButton'
+    | 'tableCellWorkflows'
+    | 'loader';
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     root: {
-        overflowX: 'auto',
-        overflowY: 'hidden'
+        width: '100%',
     },
     content: {
         display: 'inline-block',
-        width: '100%'
+        width: '100%',
     },
     tableBody: {
-        background: theme.palette.background.paper
+        background: theme.palette.background.paper,
+    },
+    loader: {
+        left: '50%',
+        marginLeft: '-84px',
+        position: 'absolute',
     },
     noItemsInfo: {
-        textAlign: "center",
-        padding: theme.spacing.unit
+        textAlign: 'center',
+        padding: theme.spacing.unit,
+    },
+    checkBoxHead: {
+        padding: '0',
+        display: 'flex',
+    },
+    checkBoxCell: {
+        padding: '0',
+        paddingLeft: '10px',
+    },
+    checkBox: {
+        cursor: 'pointer',
     },
     tableCell: {
-        wordWrap: 'break-word'
-    }
+        wordWrap: 'break-word',
+        paddingRight: '24px',
+        color: '#737373',
+    },
+    tableCellWorkflows: {
+        '&:nth-last-child(2)': {
+            padding: '0px',
+            maxWidth: '48px',
+        },
+        '&:last-child': {
+            padding: '0px',
+            paddingRight: '24px',
+            width: '48px',
+        },
+    },
+    arrow: {
+        margin: 0,
+    },
+    arrowButton: {
+        color: theme.palette.text.primary,
+    },
 });
 
+type DataTableState = {
+    isSelected: boolean;
+    checkedList: Record<string, boolean>;
+};
+
 type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
 
+const multiselectOptions: DataTableMultiselectOption[] = [
+    { name: 'First Option', fn: (checkedList) => console.log('one', checkedList) },
+    { name: 'Second Option', fn: (checkedList) => console.log('two', checkedList) },
+    { name: 'Third Option', fn: (checkedList) => console.log('three', checkedList) },
+];
+
 export const DataTable = withStyles(styles)(
     class Component<T> extends React.Component<DataTableProps<T>> {
+        state: DataTableState = {
+            isSelected: false,
+            checkedList: {},
+        };
+
+        componentDidMount(): void {
+            this.initializeCheckedList(this.props.items);
+        }
+
+        componentDidUpdate(prevProps: Readonly<DataTableProps<T>>) {
+            if (!arraysAreCongruent(prevProps.items, this.props.items)) {
+                this.initializeCheckedList(this.props.items);
+            }
+        }
+
+        checkBoxColumn: DataColumn<any, any> = {
+            name: 'checkBoxColumn',
+            selected: true,
+            configurable: false,
+            filters: createTree(),
+            render: (uuid) => (
+                <input
+                    type='checkbox'
+                    name={uuid}
+                    className={this.props.classes.checkBox}
+                    checked={this.state.checkedList[uuid] ?? false}
+                    onChange={() => this.handleCheck(uuid)}
+                    onDoubleClick={(ev) => ev.stopPropagation()}
+                ></input>
+            ),
+        };
+
+        initializeCheckedList = (uuids: any[]): void => {
+            const { checkedList } = this.state;
+            uuids.forEach((uuid) => {
+                if (!checkedList.hasOwnProperty[uuid]) {
+                    checkedList[uuid] = false;
+                }
+            });
+        };
+
+        handleSelectorSelect = (): void => {
+            const { isSelected, checkedList } = this.state;
+            const newCheckedList = { ...checkedList };
+            for (const key in newCheckedList) {
+                newCheckedList[key] = !isSelected;
+            }
+            this.setState({ isSelected: !isSelected, checkedList: newCheckedList });
+        };
+
+        handleCheck = (uuid: string): void => {
+            const { checkedList } = this.state;
+            const newCheckedList = { ...checkedList };
+            newCheckedList[uuid] = !checkedList[uuid];
+            this.setState({ checkedList: newCheckedList });
+        };
+
+        handleInvertSelect = (): void => {
+            const { checkedList } = this.state;
+            const newCheckedList = { ...checkedList };
+            for (const key in newCheckedList) {
+                newCheckedList[key] = !checkedList[key];
+            }
+            this.setState({ checkedList: newCheckedList });
+        };
+
         render() {
-            const { items, classes } = this.props;
-            return <div className={classes.root}>
-                <div className={classes.content}>
-                    <Table>
-                        <TableHead>
-                            <TableRow>
-                                {this.mapVisibleColumns(this.renderHeadCell)}
-                            </TableRow>
-                        </TableHead>
-                        <TableBody className={classes.tableBody}>
-                            {items.map(this.renderBodyRow)}
-                        </TableBody>
-                    </Table>
-                    {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
+            const { items, classes, working, columns } = this.props;
+            if (columns[0].name === this.checkBoxColumn.name) columns.shift();
+            columns.unshift(this.checkBoxColumn);
+
+            return (
+                <div className={classes.root}>
+                    <div className={classes.content}>
+                        <Table>
+                            <TableHead>
+                                <TableRow>{this.mapVisibleColumns(this.renderHeadCell)}</TableRow>
+                            </TableHead>
+                            <TableBody className={classes.tableBody}>{!working && items.map(this.renderBodyRow)}</TableBody>
+                        </Table>
+                        {!!working && (
+                            <div className={classes.loader}>
+                                <DataTableDefaultView icon={PendingIcon} messages={['Loading data, please wait.']} />
+                            </div>
+                        )}
+                        {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
+                    </div>
                 </div>
-            </div>;
+            );
         }
 
-        renderNoItemsPlaceholder = () => {
-            return this.props.defaultView
-                ? this.props.defaultView
-                : <DataTableDefaultView/>;
-        }
+        renderNoItemsPlaceholder = (columns: DataColumns<T, any>) => {
+            const dirty = columns.some((column) => getTreeDirty('')(column.filters));
+            return <DataTableDefaultView icon={this.props.defaultViewIcon} messages={this.props.defaultViewMessages} filtersApplied={dirty} />;
+        };
 
-        renderHeadCell = (column: DataColumn<T>, index: number) => {
-            const { name, key, renderHeader, filters, sortDirection } = column;
-            const { onSortToggle, onFiltersChange } = this.props;
-            return <TableCell key={key || index}>
-                {renderHeader ?
-                    renderHeader() :
-                    countNodes(filters) > 0
-                        ? <DataTableFiltersPopover
+        renderHeadCell = (column: DataColumn<T, any>, index: number) => {
+            const { name, key, renderHeader, filters, sort } = column;
+            const { onSortToggle, onFiltersChange, classes } = this.props;
+            return index === 0 ? (
+                <TableCell key={key || index} className={classes.checkBoxCell}>
+                    <div className={classes.checkBoxHead}>
+                        <Tooltip title={this.state.isSelected ? 'Deselect All' : 'Select All'}>
+                            <input type='checkbox' className={classes.checkBox} checked={this.state.isSelected} onChange={this.handleSelectorSelect}></input>
+                        </Tooltip>
+                        <DataTableMultiselectPopover
+                            name={`multiselect options`}
+                            options={multiselectOptions}
+                            checkedList={this.state.checkedList}
+                        ></DataTableMultiselectPopover>
+                    </div>
+                </TableCell>
+            ) : (
+                <TableCell className={classes.tableCell} key={key || index}>
+                    {renderHeader ? (
+                        renderHeader()
+                    ) : countNodes(filters) > 0 ? (
+                        <DataTableFiltersPopover
                             name={`${name} filters`}
-                            onChange={filters =>
-                                onFiltersChange &&
-                                onFiltersChange(filters, column)}
-                            filters={filters}>
+                            mutuallyExclusive={column.mutuallyExclusiveFilters}
+                            onChange={(filters) => onFiltersChange && onFiltersChange(filters, column)}
+                            filters={filters}
+                        >
                             {name}
                         </DataTableFiltersPopover>
-                        : sortDirection
-                            ? <TableSortLabel
-                                active={sortDirection !== SortDirection.NONE}
-                                direction={sortDirection !== SortDirection.NONE ? sortDirection : undefined}
-                                onClick={() =>
-                                    onSortToggle &&
-                                    onSortToggle(column)}>
-                                {name}
-                            </TableSortLabel>
-                            : <span>
-                                {name}
-                            </span>}
-            </TableCell>;
-        }
+                    ) : sort ? (
+                        <TableSortLabel
+                            active={sort.direction !== SortDirection.NONE}
+                            direction={sort.direction !== SortDirection.NONE ? sort.direction : undefined}
+                            IconComponent={this.ArrowIcon}
+                            hideSortIcon
+                            onClick={() => onSortToggle && onSortToggle(column)}
+                        >
+                            {name}
+                        </TableSortLabel>
+                    ) : (
+                        <span>{name}</span>
+                    )}
+                </TableCell>
+            );
+        };
 
-        renderBodyRow = (item: T, index: number) => {
-            const { onRowClick, onRowDoubleClick, extractKey, classes } = this.props;
-            return <TableRow
-                hover
-                key={extractKey ? extractKey(item) : index}
-                onClick={event => onRowClick && onRowClick(event, item)}
-                onContextMenu={this.handleRowContextMenu(item)}
-                onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}>
-                {this.mapVisibleColumns((column, index) => (
-                    <TableCell key={column.key || index} className={classes.tableCell}>
-                        {column.render(item)}
-                    </TableCell>
-                ))}
-            </TableRow>;
-        }
+        ArrowIcon = ({ className, ...props }: SvgIconProps) => (
+            <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}>
+                <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} />
+            </IconButton>
+        );
 
-        mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
-            return this.props.columns.filter(column => column.selected).map(fn);
-        }
+        renderBodyRow = (item: any, index: number) => {
+            const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
+            return (
+                <TableRow
+                    hover
+                    key={extractKey ? extractKey(item) : index}
+                    onClick={(event) => onRowClick && onRowClick(event, item)}
+                    onContextMenu={this.handleRowContextMenu(item)}
+                    onDoubleClick={(event) => onRowDoubleClick && onRowDoubleClick(event, item)}
+                    selected={item === currentItemUuid}
+                >
+                    {this.mapVisibleColumns((column, index) => (
+                        <TableCell
+                            key={column.key || index}
+                            className={currentRoute === '/workflows' ? classes.tableCellWorkflows : index === 0 ? classes.checkBoxCell : classes.tableCell}
+                        >
+                            {column.render(item)}
+                        </TableCell>
+                    ))}
+                </TableRow>
+            );
+        };
 
-        handleRowContextMenu = (item: T) =>
-            (event: React.MouseEvent<HTMLElement>) =>
-                this.props.onContextMenu(event, item)
+        mapVisibleColumns = (fn: (column: DataColumn<T, any>, index: number) => React.ReactElement<any>) => {
+            return this.props.columns.filter((column) => column.selected).map(fn);
+        };
 
+        handleRowContextMenu = (item: T) => (event: React.MouseEvent<HTMLElement>) => this.props.onContextMenu(event, item);
     }
 );