Merge branch '13683-sorting-and-filtering-of-projects-and-collections'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 26 Jun 2018 13:37:53 +0000 (15:37 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 26 Jun 2018 13:37:53 +0000 (15:37 +0200)
refs #13683

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

src/common/formatters.ts
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/models/resource.ts
src/views-components/project-explorer/project-explorer.tsx
src/views/project-panel/project-panel-selectors.ts
src/views/project-panel/project-panel.tsx

index 1d9a52012445b1478c9dc3b4d37197dd9237bc74..fe7df14c9d12670a3a9c2f88480b6d6e519a902a 100644 (file)
@@ -4,7 +4,8 @@
 
 export const formatDate = (isoDate: string) => {
     const date = new Date(isoDate);
-    return date.toLocaleString();
+    const text = date.toLocaleString();
+    return text === 'Invalid Date' ? "" : text;
 };
 
 export const formatFileSize = (size?: number) => {
index d2ca7f7b7cee2f2dfb489778c49c4dc3b7a8cc3b..94c7be6dab933ff6991edda0a56ebf4b1edc0d2d 100644 (file)
@@ -27,7 +27,8 @@ describe("<DataExplorer />", () => {
             columns={[{ name: "Column 1", render: jest.fn(), selected: true }]} />);
         expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]);
         dataExplorer.find(DataTable).prop("onRowContextMenu")({
-            preventDefault: jest.fn()
+            preventDefault: jest.fn(),
+            stopPropagation: jest.fn()
         }, "Item 1");
         dataExplorer.find(ContextMenu).prop("onActionClick")({ name: "Action 1", icon: "" });
         expect(onContextAction).toHaveBeenCalledWith({ name: "Action 1", icon: "" }, "Item 1");
index 9aacadf342d7206df7baa8cca056a3cff5e42e2b..557b0158ee365a33f34f70e38a61f04071b46dcc 100644 (file)
@@ -88,6 +88,7 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
 
     openContextMenu = (event: React.MouseEvent<HTMLElement>, item: T) => {
         event.preventDefault();
+        event.stopPropagation();
         this.setState({
             contextMenu: {
                 anchorEl: mockAnchorFromMouseEvent(event),
index 28bb349d82761c08a630b6fd1a8d752908434d04..0f5fbc28f68cbd151857c31a59d9b634626a59a2 100644 (file)
@@ -16,7 +16,7 @@ export enum ResourceKind {
     PROJECT = "project",
     COLLECTION = "collection",
     PIPELINE = "pipeline",
-    LEVEL_UP = "levelup",
+    LEVEL_UP = "",
     UNKNOWN = "unknown"
 }
 
index 94ae438986eeb3145ba02a1dfc872966de18c91f..556b23bea19b8111a0212b874ee4cccf1d75733e 100644 (file)
@@ -25,6 +25,8 @@ export interface ProjectExplorerContextActions {
 interface ProjectExplorerProps {
     items: ProjectExplorerItem[];
     onRowClick: (item: ProjectExplorerItem) => void;
+    onToggleSort: (toggledColumn: DataColumn<ProjectExplorerItem>) => void;
+    onChangeFilters: (filters: DataTableFilterItem[]) => void;
 }
 
 interface ProjectExplorerState {
@@ -42,19 +44,12 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
         columns: [{
             name: "Name",
             selected: true,
-            sortDirection: "asc",
+            sortDirection: "desc",
             render: renderName,
             width: "450px"
         }, {
             name: "Status",
             selected: true,
-            filters: [{
-                name: "In progress",
-                selected: true
-            }, {
-                name: "Complete",
-                selected: true
-            }],
             render: renderStatus,
             width: "75px"
         }, {
@@ -64,7 +59,7 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
                 name: "Collection",
                 selected: true
             }, {
-                name: "Group",
+                name: "Project",
                 selected: true
             }],
             render: item => renderType(item.kind),
@@ -77,12 +72,12 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
         }, {
             name: "File size",
             selected: true,
-            sortDirection: "none",
             render: item => renderFileSize(item.fileSize),
             width: "50px"
         }, {
             name: "Last modified",
             selected: true,
+            sortDirection: "none",
             render: item => renderDate(item.lastModified),
             width: "150px"
         }]
@@ -140,14 +135,17 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
         });
     }
 
-    toggleSort = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
-        this.setState({
-            columns: this.state.columns.map(column =>
-                column.name === toggledColumn.name
-                    ? toggleSortDirection(column)
-                    : resetSortDirection(column)
-            )
-        });
+    toggleSort = (column: DataColumn<ProjectExplorerItem>) => {
+        const columns = this.state.columns.map(c =>
+            c.name === column.name
+                ? toggleSortDirection(c)
+                : resetSortDirection(c)
+        );
+        this.setState({ columns });
+        const toggledColumn = columns.find(c => c.name === column.name);
+        if (toggledColumn) {
+            this.props.onToggleSort(toggledColumn);
+        }
     }
 
     changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn<ProjectExplorerItem>) => {
@@ -158,6 +156,7 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
                     : column
             )
         });
+        this.props.onChangeFilters(filters);
     }
 
     executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
@@ -197,11 +196,11 @@ const renderName = (item: ProjectExplorerItem) =>
 const renderIcon = (item: ProjectExplorerItem) => {
     switch (item.kind) {
         case ResourceKind.LEVEL_UP:
-            return <i className="icon-level-up" style={{fontSize: "1rem"}}/>;
+            return <i className="icon-level-up" style={{ fontSize: "1rem" }} />;
         case ResourceKind.PROJECT:
-            return <i className="fas fa-folder fa-lg"/>;
+            return <i className="fas fa-folder fa-lg" />;
         case ResourceKind.COLLECTION:
-            return <i className="fas fa-th fa-lg"/>;
+            return <i className="fas fa-th fa-lg" />;
         default:
             return <i />;
     }
index c798ec3db2976396685bddd5bb60c63ba7312258..83bfd603898700502884c20529f7da671e031118 100644 (file)
@@ -19,9 +19,9 @@ export const projectExplorerItems = (projects: Array<TreeItem<Project>>, treeIte
             name: "..",
             url: getResourceUrl(treeItem.data),
             kind: ResourceKind.LEVEL_UP,
-            owner: treeItem.data.ownerUuid,
+            owner: "",
             uuid: treeItem.data.uuid,
-            lastModified: treeItem.data.modifiedAt
+            lastModified: ""
         });
 
         if (treeItem.items) {
index 534f843f31499b064500f7751f18c61afe2198ef..df9721fda775546308052ce26625c1d4480b14dc 100644 (file)
@@ -12,6 +12,9 @@ import { ItemMode, setProjectItem } from "../../store/navigation/navigation-acti
 import ProjectExplorer from "../../views-components/project-explorer/project-explorer";
 import { projectExplorerItems } from "./project-panel-selectors";
 import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
+import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataColumn, SortDirection } from '../../components/data-table/data-column';
+import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
 
 interface ProjectPanelDataProps {
     projects: ProjectState;
@@ -20,29 +23,103 @@ interface ProjectPanelDataProps {
 
 type ProjectPanelProps = ProjectPanelDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
 
-class ProjectPanel extends React.Component<ProjectPanelProps> {
+interface ProjectPanelState {
+    sort: {
+        columnName: string;
+        direction: SortDirection;
+    };
+    filters: string[];
+}
+
+class ProjectPanel extends React.Component<ProjectPanelProps & WithStyles<CssRules>, ProjectPanelState> {
+    state: ProjectPanelState = {
+        sort: {
+            columnName: "Name",
+            direction: "desc"
+        },
+        filters: ['collection', 'project']
+    };
+
     render() {
         const items = projectExplorerItems(
             this.props.projects.items,
             this.props.projects.currentItemId,
             this.props.collections
         );
+        const [goBackItem, ...otherItems] = items;
+        const filteredItems = otherItems.filter(i => this.state.filters.some(f => f === i.kind));
+        const sortedItems = sortItems(this.state.sort, filteredItems);
         return (
-            <ProjectExplorer
-                items={items}
-                onRowClick={this.goToItem}
-            />
+            <div>
+                <div className={this.props.classes.toolbar}>
+                    <Button color="primary" variant="raised" className={this.props.classes.button}>
+                        Create a collection
+                    </Button>
+                    <Button color="primary" variant="raised" className={this.props.classes.button}>
+                        Run a process
+                    </Button>
+                    <Button color="primary" variant="raised" className={this.props.classes.button}>
+                        Create a project
+                    </Button>
+                </div>
+                <ProjectExplorer
+                    items={goBackItem ? [goBackItem, ...sortedItems] : sortedItems}
+                    onRowClick={this.goToItem}
+                    onToggleSort={this.toggleSort}
+                    onChangeFilters={this.changeFilters}
+                />
+            </div>
         );
     }
 
     goToItem = (item: ProjectExplorerItem) => {
         this.props.dispatch<any>(setProjectItem(this.props.projects.items, item.uuid, item.kind, ItemMode.BOTH));
     }
+
+    toggleSort = (column: DataColumn<ProjectExplorerItem>) => {
+        this.setState({
+            sort: {
+                columnName: column.name,
+                direction: column.sortDirection || "none"
+            }
+        });
+    }
+
+    changeFilters = (filters: DataTableFilterItem[]) => {
+        this.setState({ filters: filters.filter(f => f.selected).map(f => f.name.toLowerCase()) });
+    }
 }
 
-export default connect(
-    (state: RootState) => ({
-        projects: state.projects,
-        collections: state.collections
-    })
-)(ProjectPanel);
+const sortItems = (sort: { columnName: string, direction: SortDirection }, items: ProjectExplorerItem[]) => {
+    const sortedItems = items.slice(0);
+    const direction = sort.direction === "asc" ? -1 : 1;
+    sortedItems.sort((a, b) => {
+        if (sort.columnName === "Last modified") {
+            return ((new Date(a.lastModified)).getTime() - (new Date(b.lastModified)).getTime()) * direction;
+        } else {
+            return a.name.localeCompare(b.name) * direction;
+        }
+    });
+    return sortedItems;
+};
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    toolbar: {
+        marginBottom: theme.spacing.unit * 3,
+        display: "flex",
+        justifyContent: "flex-end"
+    },
+    button: {
+        marginLeft: theme.spacing.unit
+    }
+});
+
+export default withStyles(styles)(
+    connect(
+        (state: RootState) => ({
+            projects: state.projects,
+            collections: state.collections
+        })
+    )(ProjectPanel));