import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import ColumnSelector, { ColumnSelectorProps, ColumnSelectorTrigger } from "./column-selector";
-import { DataColumn } from "../data-table/data-column";
import { ListItem, Checkbox } from "@material-ui/core";
+import { DataColumns } from "../data-table/data-table";
configure({ adapter: new Adapter() });
describe("<ColumnSelector />", () => {
it("shows only configurable columns", () => {
- const columns: Array<DataColumn<void>> = [
+ const columns: DataColumns<void> = [
{
name: "Column 1",
render: () => <span />,
});
it("renders checked checkboxes next to selected columns", () => {
- const columns: Array<DataColumn<void>> = [
+ const columns: DataColumns<void> = [
{
name: "Column 1",
render: () => <span />,
});
it("calls onColumnToggle with clicked column", () => {
- const columns: Array<DataColumn<void>> = [
+ const columns: DataColumns<void> = [
{
name: "Column 1",
render: () => <span />,
import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
import Popover from "../popover/popover";
import { IconButtonProps } from '@material-ui/core/IconButton';
+import { DataColumns } from '../data-table/data-table';
export interface ColumnSelectorProps {
- columns: Array<DataColumn<any>>;
+ columns: DataColumns<any>;
onColumnToggle: (column: DataColumn<any>) => void;
}
import MoreVertIcon from "@material-ui/icons/MoreVert";
import ContextMenu, { ContextMenuActionGroup, ContextMenuAction } from "../../components/context-menu/context-menu";
import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable from "../../components/data-table/data-table";
+import DataTable, { DataColumns } from "../../components/data-table/data-table";
import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
import { DataColumn } from "../../components/data-table/data-column";
import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
interface DataExplorerProps<T> {
items: T[];
- columns: Array<DataColumn<T>>;
+ columns: DataColumns<T>;
contextActions: ContextMenuActionGroup[];
searchValue: string;
rowsPerPage: number;
+ rowsPerPageOptions?: number[];
page: number;
onSearch: (value: string) => void;
onRowClick: (item: T) => void;
<TablePagination
count={this.props.items.length}
rowsPerPage={this.props.rowsPerPage}
+ rowsPerPageOptions={this.props.rowsPerPageOptions}
page={this.props.page}
onChangePage={this.changePage}
onChangeRowsPerPage={this.changeRowsPerPage}
renderContextMenuTrigger = (item: T) =>
<Grid container justify="flex-end">
- <IconButton onClick={event => this.openContextMenu(event, item)}>
+ <IconButton onClick={event => this.openContextMenuWithTrigger(event, item)}>
<MoreVertIcon />
</IconButton>
</Grid>
+ openContextMenuWithTrigger = (event: React.MouseEvent<HTMLElement>, item: T) => {
+ event.preventDefault();
+ this.setState({
+ contextMenu: {
+ anchorEl: event.currentTarget,
+ item
+ }
+ });
+ }
+
contextMenuColumn = {
name: "Actions",
selected: true,
import { mount, configure } from "enzyme";
import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
import * as Adapter from "enzyme-adapter-react-16";
-import DataTable from "./data-table";
-import { DataColumn } from "./data-column";
+import DataTable, { DataColumns } from "./data-table";
import DataTableFilters from "../data-table-filters/data-table-filters";
configure({ adapter: new Adapter() });
describe("<DataTable />", () => {
it("shows only selected columns", () => {
- const columns: Array<DataColumn<string>> = [
+ const columns: DataColumns<string> = [
{
name: "Column 1",
render: () => <span />,
});
it("renders column name", () => {
- const columns: Array<DataColumn<string>> = [
+ const columns: DataColumns<string> = [
{
name: "Column 1",
render: () => <span />,
});
it("uses renderHeader instead of name prop", () => {
- const columns: Array<DataColumn<string>> = [
+ const columns: DataColumns<string> = [
{
name: "Column 1",
renderHeader: () => <span>Column Header</span>,
});
it("passes column key prop to corresponding cells", () => {
- const columns: Array<DataColumn<string>> = [
+ const columns: DataColumns<string> = [
{
name: "Column 1",
key: "column-1-key",
});
it("renders items", () => {
- const columns: Array<DataColumn<string>> = [
+ const columns: DataColumns<string> = [
{
name: "Column 1",
render: (item) => <Typography>{item}</Typography>,
});
it("passes sorting props to <TableSortLabel />", () => {
- const columns: Array<DataColumn<string>> = [{
+ const columns: DataColumns<string> = [{
name: "Column 1",
sortDirection: "asc",
selected: true,
});
it("passes filter props to <DataTableFilter />", () => {
- const columns: Array<DataColumn<string>> = [{
+ const columns: DataColumns<string> = [{
name: "Column 1",
sortDirection: "asc",
selected: true,
const history = createBrowserHistory();
-const store = configureStore({
- projects: {
- items: [],
- currentItemId: ""
- },
- collections: [
- ],
- router: {
- location: null
- },
- auth: {
- user: undefined
- },
- sidePanel: []
-}, history);
+const store = configureStore(history);
store.dispatch(authActions.INIT());
const rootUuid = authService.getRootUuid();
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { SortDirection, DataColumn } from "../../components/data-table/data-column";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { DataColumns } from "../../components/data-table/data-table";
+
+type WithId<T> = T & { id: string };
+
+const actions = unionize({
+ SET_COLUMNS: ofType<WithId<{ columns: DataColumns<any> }>>(),
+ SET_FILTERS: ofType<WithId<{columnName: string, filters: DataTableFilterItem[]}>>(),
+ SET_ITEMS: ofType<WithId<{items: any[]}>>(),
+ SET_PAGE: ofType<WithId<{page: number}>>(),
+ SET_ROWS_PER_PAGE: ofType<WithId<{rowsPerPage: number}>>(),
+ TOGGLE_COLUMN: ofType<WithId<{ columnName: string }>>(),
+ TOGGLE_SORT: ofType<WithId<{ columnName: string }>>(),
+ SET_SEARCH_VALUE: ofType<WithId<{searchValue: string}>>()
+}, { tag: "type", value: "payload" });
+
+export type DataExplorerAction = UnionOf<typeof actions>;
+
+export default actions;
+
+
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import dataExplorerReducer, { initialDataExplorer } from "./data-explorer-reducer";
+import actions from "./data-explorer-action";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { DataColumns } from "../../components/data-table/data-table";
+
+describe('data-explorer-reducer', () => {
+ it('should set columns', () => {
+ const columns: DataColumns<any> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true
+ }];
+ const state = dataExplorerReducer(undefined,
+ actions.SET_COLUMNS({ id: "Data explorer", columns }));
+ expect(state["Data explorer"].columns).toEqual(columns);
+ });
+
+ it('should toggle sorting', () => {
+ const columns: DataColumns<any> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true,
+ sortDirection: "asc"
+ }, {
+ name: "Column 2",
+ render: jest.fn(),
+ selected: true,
+ sortDirection: "none",
+ }];
+ const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
+ actions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
+ expect(state["Data explorer"].columns[0].sortDirection).toEqual("none");
+ expect(state["Data explorer"].columns[1].sortDirection).toEqual("asc");
+ });
+
+ it('should set filters', () => {
+ const columns: DataColumns<any> = [{
+ name: "Column 1",
+ render: jest.fn(),
+ selected: true,
+ }];
+
+ const filters: DataTableFilterItem[] = [{
+ name: "Filter 1",
+ selected: true
+ }];
+ const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
+ actions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
+ expect(state["Data explorer"].columns[0].filters).toEqual(filters);
+ });
+
+ it('should set items', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_ITEMS({ id: "Data explorer", items: ["Item 1", "Item 2"] }));
+ expect(state["Data explorer"].items).toEqual(["Item 1", "Item 2"]);
+ });
+
+ it('should set page', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_PAGE({ id: "Data explorer", page: 2 }));
+ expect(state["Data explorer"].page).toEqual(2);
+ });
+
+ it('should set rows per page', () => {
+ const state = dataExplorerReducer({ "Data explorer": undefined },
+ actions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
+ expect(state["Data explorer"].rowsPerPage).toEqual(5);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataColumn, toggleSortDirection, resetSortDirection } from "../../components/data-table/data-column";
+import actions, { DataExplorerAction } from "./data-explorer-action";
+import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { DataColumns } from "../../components/data-table/data-table";
+
+interface DataExplorer {
+ columns: DataColumns<any>;
+ items: any[];
+ page: number;
+ rowsPerPage: number;
+ rowsPerPageOptions?: number[];
+ searchValue: string;
+}
+
+export const initialDataExplorer: DataExplorer = {
+ columns: [],
+ items: [],
+ page: 0,
+ rowsPerPage: 10,
+ rowsPerPageOptions: [5, 10, 25, 50],
+ searchValue: ""
+};
+
+export type DataExplorerState = Record<string, DataExplorer | undefined>;
+
+const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
+ actions.match(action, {
+ SET_COLUMNS: ({ id, columns }) =>
+ update(state, id, setColumns(columns)),
+
+ SET_FILTERS: ({ id, columnName, filters }) =>
+ update(state, id, mapColumns(setFilters(columnName, filters))),
+
+ SET_ITEMS: ({ id, items }) =>
+ update(state, id, explorer => ({ ...explorer, items })),
+
+ SET_PAGE: ({ id, page }) =>
+ update(state, id, explorer => ({ ...explorer, page })),
+
+ SET_ROWS_PER_PAGE: ({ id, rowsPerPage }) =>
+ update(state, id, explorer => ({ ...explorer, rowsPerPage })),
+
+ TOGGLE_SORT: ({ id, columnName }) =>
+ update(state, id, mapColumns(toggleSort(columnName))),
+
+ TOGGLE_COLUMN: ({ id, columnName }) =>
+ update(state, id, mapColumns(toggleColumn(columnName))),
+
+ default: () => state
+ });
+
+export default dataExplorerReducer;
+
+export const getDataExplorer = (state: DataExplorerState, id: string) =>
+ state[id] || initialDataExplorer;
+
+const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
+ ({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
+
+const setColumns = (columns: DataColumns<any>) =>
+ (dataExplorer: DataExplorer) =>
+ ({ ...dataExplorer, columns });
+
+const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
+ (dataExplorer: DataExplorer) =>
+ ({ ...dataExplorer, columns: dataExplorer.columns.map(mapFn) });
+
+const toggleSort = (columnName: string) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? toggleSortDirection(column)
+ : resetSortDirection(column);
+
+const toggleColumn = (columnName: string) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? { ...column, selected: !column.selected }
+ : column;
+
+const setFilters = (columnName: string, filters: DataTableFilterItem[]) =>
+ (column: DataColumn<any>) => column.name === columnName
+ ? { ...column, filters }
+ : column;
import { Dispatch } from "redux";
import projectActions, { getProjectList } from "../project/project-action";
import { push } from "react-router-redux";
-import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { TreeItemStatus } from "../../components/tree/tree";
import { getCollectionList } from "../collection/collection-action";
import { findTreeItem } from "../project/project-reducer";
-import { Project } from "../../models/project";
import { Resource, ResourceKind } from "../../models/resource";
import sidePanelActions from "../side-panel/side-panel-action";
+import dataExplorerActions from "../data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { projectPanelItems } from "../../views/project-panel/project-panel-selectors";
+import { RootState } from "../store";
export const getResourceUrl = (resource: Resource): string => {
switch (resource.kind) {
ACTIVE
}
-export const setProjectItem = (projects: Array<TreeItem<Project>>, itemId: string, itemKind: ResourceKind, itemMode: ItemMode) => (dispatch: Dispatch) => {
+export const setProjectItem = (itemId: string, itemKind = ResourceKind.PROJECT, itemMode = ItemMode.OPEN) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { projects } = getState();
- const openProjectItem = (resource: Resource) => {
- if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
+ let treeItem = findTreeItem(projects.items, itemId);
+ if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
+ treeItem = findTreeItem(projects.items, treeItem.data.ownerUuid);
}
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
- }
+ if (treeItem) {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
- dispatch(push(getResourceUrl({...resource, kind: itemKind})));
+ if (treeItem.status === TreeItemStatus.Loaded) {
+ dispatch<any>(openProjectItem(treeItem.data, itemKind, itemMode));
+ } else {
+ dispatch<any>(getProjectList(itemId))
+ .then(() => dispatch<any>(openProjectItem(treeItem!.data, itemKind, itemMode)));
+ }
+ if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+ dispatch<any>(getCollectionList(itemId));
+ }
+ }
};
- let treeItem = findTreeItem(projects, itemId);
- if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
- treeItem = findTreeItem(projects, treeItem.data.ownerUuid);
- }
+const openProjectItem = (resource: Resource, itemKind: ResourceKind, itemMode: ItemMode) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
- if (treeItem) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+ const { collections, projects } = getState();
- if (treeItem.status === TreeItemStatus.Loaded) {
- openProjectItem(treeItem.data);
- } else {
- dispatch<any>(getProjectList(itemId))
- .then(() => openProjectItem(treeItem!.data));
+ if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
}
+
if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- dispatch<any>(getCollectionList(itemId));
+ dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
}
- }
-};
+
+ dispatch(push(getResourceUrl({ ...resource, kind: itemKind })));
+ dispatch(dataExplorerActions.SET_ITEMS({
+ id: PROJECT_PANEL_ID,
+ items: projectPanelItems(
+ projects.items,
+ resource.uuid,
+ collections
+ )
+ }));
+ };
import projectsReducer, { ProjectState } from "./project/project-reducer";
import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
import authReducer, { AuthState } from "./auth/auth-reducer";
+import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
import collectionsReducer, { CollectionState } from "./collection/collection-reducer";
const composeEnhancers =
projects: ProjectState;
collections: CollectionState;
router: RouterState;
+ dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
}
projects: projectsReducer,
collections: collectionsReducer,
router: routerReducer,
+ dataExplorer: dataExplorerReducer,
sidePanel: sidePanelReducer
});
-export default function configureStore(initialState: RootState, history: History) {
+export default function configureStore(history: History) {
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
- return createStore(rootReducer, initialState!, enhancer);
+ return createStore(rootReducer, enhancer);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../store/store";
+import DataExplorer from "../../components/data-explorer/data-explorer";
+import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
+
+export default connect((state: RootState, props: { id: string }) =>
+ getDataExplorer(state.dataExplorer, props.id)
+)(DataExplorer);
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { ProjectExplorerItem } from './project-explorer-item';
-import { Grid, Typography } from '@material-ui/core';
-import { formatDate, formatFileSize } from '../../common/formatters';
-import DataExplorer from '../../components/data-explorer/data-explorer';
-import { DataColumn, toggleSortDirection, resetSortDirection } from '../../components/data-table/data-column';
-import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
-import { ContextMenuAction } from '../../components/context-menu/context-menu';
-import { ResourceKind } from "../../models/resource";
-
-export interface ProjectExplorerContextActions {
- onAddToFavourite: (item: ProjectExplorerItem) => void;
- onCopy: (item: ProjectExplorerItem) => void;
- onDownload: (item: ProjectExplorerItem) => void;
- onMoveTo: (item: ProjectExplorerItem) => void;
- onRemove: (item: ProjectExplorerItem) => void;
- onRename: (item: ProjectExplorerItem) => void;
- onShare: (item: ProjectExplorerItem) => void;
-}
-
-interface ProjectExplorerProps {
- items: ProjectExplorerItem[];
- onRowClick: (item: ProjectExplorerItem) => void;
- onToggleSort: (toggledColumn: DataColumn<ProjectExplorerItem>) => void;
- onChangeFilters: (filters: DataTableFilterItem[]) => void;
-}
-
-interface ProjectExplorerState {
- columns: Array<DataColumn<ProjectExplorerItem>>;
- searchValue: string;
- page: number;
- rowsPerPage: number;
-}
-
-class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplorerState> {
- state: ProjectExplorerState = {
- searchValue: "",
- page: 0,
- rowsPerPage: 10,
- columns: [{
- name: "Name",
- selected: true,
- sortDirection: "desc",
- render: renderName,
- width: "450px"
- }, {
- name: "Status",
- selected: true,
- render: renderStatus,
- width: "75px"
- }, {
- name: "Type",
- selected: true,
- filters: [{
- name: "Collection",
- selected: true
- }, {
- name: "Project",
- selected: true
- }],
- render: item => renderType(item.kind),
- width: "125px"
- }, {
- name: "Owner",
- selected: true,
- render: item => renderOwner(item.owner),
- width: "200px"
- }, {
- name: "File size",
- selected: true,
- render: item => renderFileSize(item.fileSize),
- width: "50px"
- }, {
- name: "Last modified",
- selected: true,
- sortDirection: "none",
- render: item => renderDate(item.lastModified),
- width: "150px"
- }]
- };
-
- contextMenuActions = [[{
- icon: "fas fa-users fa-fw",
- name: "Share"
- }, {
- icon: "fas fa-sign-out-alt fa-fw",
- name: "Move to"
- }, {
- icon: "fas fa-star fa-fw",
- name: "Add to favourite"
- }, {
- icon: "fas fa-edit fa-fw",
- name: "Rename"
- }, {
- icon: "fas fa-copy fa-fw",
- name: "Make a copy"
- }, {
- icon: "fas fa-download fa-fw",
- name: "Download"
- }], [{
- icon: "fas fa-trash-alt fa-fw",
- name: "Remove"
- }
- ]];
-
- render() {
- return <DataExplorer
- items={this.props.items}
- columns={this.state.columns}
- contextActions={this.contextMenuActions}
- searchValue={this.state.searchValue}
- page={this.state.page}
- rowsPerPage={this.state.rowsPerPage}
- onColumnToggle={this.toggleColumn}
- onFiltersChange={this.changeFilters}
- onRowClick={this.props.onRowClick}
- onSortToggle={this.toggleSort}
- onSearch={this.search}
- onContextAction={this.executeAction}
- onChangePage={this.changePage}
- onChangeRowsPerPage={this.changeRowsPerPage} />;
- }
-
- toggleColumn = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- columns: this.state.columns.map(column =>
- column.name === toggledColumn.name
- ? { ...column, selected: !column.selected }
- : 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>) => {
- this.setState({
- columns: this.state.columns.map(column =>
- column.name === updatedColumn.name
- ? { ...column, filters }
- : column
- )
- });
- this.props.onChangeFilters(filters);
- }
-
- executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
- alert(`Executing ${action.name} on ${item.name}`);
- }
-
- search = (searchValue: string) => {
- this.setState({ searchValue });
- }
-
- changePage = (page: number) => {
- this.setState({ page });
- }
-
- changeRowsPerPage = (rowsPerPage: number) => {
- this.setState({ rowsPerPage });
- }
-}
-
-const renderName = (item: ProjectExplorerItem) =>
- <Grid
- container
- alignItems="center"
- wrap="nowrap"
- spacing={16}>
- <Grid item>
- {renderIcon(item)}
- </Grid>
- <Grid item>
- <Typography color="primary">
- {item.name}
- </Typography>
- </Grid>
- </Grid>;
-
-
-const renderIcon = (item: ProjectExplorerItem) => {
- switch (item.kind) {
- case ResourceKind.LEVEL_UP:
- return <i className="icon-level-up" style={{ fontSize: "1rem" }} />;
- case ResourceKind.PROJECT:
- return <i className="fas fa-folder fa-lg" />;
- case ResourceKind.COLLECTION:
- return <i className="fas fa-th fa-lg" />;
- default:
- return <i />;
- }
-};
-
-const renderDate = (date: string) =>
- <Typography noWrap>
- {formatDate(date)}
- </Typography>;
-
-const renderFileSize = (fileSize?: number) =>
- <Typography noWrap>
- {formatFileSize(fileSize)}
- </Typography>;
-
-const renderOwner = (owner: string) =>
- <Typography noWrap color="primary">
- {owner}
- </Typography>;
-
-const renderType = (type: string) =>
- <Typography noWrap>
- {type}
- </Typography>;
-
-const renderStatus = (item: ProjectExplorerItem) =>
- <Typography noWrap align="center">
- {item.status || "-"}
- </Typography>;
-
-export default ProjectExplorer;
//
// SPDX-License-Identifier: AGPL-3.0
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
import { getResourceKind, Resource, ResourceKind } from "../../models/resource";
-export interface ProjectExplorerItem {
+export interface ProjectPanelItem {
uuid: string;
name: string;
kind: ResourceKind;
import { ResourceKind } from "../../models/resource";
import { Collection } from "../../models/collection";
import { getResourceUrl } from "../../store/navigation/navigation-action";
-import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
+import { ProjectPanelItem } from "./project-panel-item";
-export const projectExplorerItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectExplorerItem[] => {
- const dataItems: ProjectExplorerItem[] = [];
+export const projectPanelItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectPanelItem[] => {
+ const dataItems: ProjectPanelItem[] = [];
const treeItem = findTreeItem(projects, treeItemId);
if (treeItem) {
owner: p.data.ownerUuid,
uuid: p.data.uuid,
lastModified: p.data.modifiedAt
- } as ProjectExplorerItem;
+ } as ProjectPanelItem;
dataItems.push(item);
});
owner: c.ownerUuid,
uuid: c.uuid,
lastModified: c.modifiedAt
- } as ProjectExplorerItem;
+ } as ProjectPanelItem;
dataItems.push(item);
});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { RouteComponentProps } from 'react-router';
-import { ProjectState } from '../../store/project/project-reducer';
-import { RootState } from '../../store/store';
-import { connect, DispatchProp } from 'react-redux';
-import { CollectionState } from "../../store/collection/collection-reducer";
-import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
-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 { ProjectPanelItem } from './project-panel-item';
+import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { formatDate, formatFileSize } from '../../common/formatters';
+import DataExplorer from "../../views-components/data-explorer/data-explorer";
+import { DataColumn, toggleSortDirection } from '../../components/data-table/data-column';
import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
+import { ContextMenuAction } from '../../components/context-menu/context-menu';
+import { DispatchProp, connect } from 'react-redux';
+import actions from "../../store/data-explorer/data-explorer-action";
+import { setProjectItem } from "../../store/navigation/navigation-action";
+import { DataColumns } from '../../components/data-table/data-table';
+import { ResourceKind } from "../../models/resource";
-interface ProjectPanelDataProps {
- projects: ProjectState;
- collections: CollectionState;
-}
+export const PROJECT_PANEL_ID = "projectPanel";
+class ProjectPanel extends React.Component<DispatchProp & WithStyles<CssRules>> {
+ render() {
+ return <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>
+ <DataExplorer
+ id={PROJECT_PANEL_ID}
+ contextActions={contextMenuActions}
+ onColumnToggle={this.toggleColumn}
+ onFiltersChange={this.changeFilters}
+ onRowClick={this.openProject}
+ onSortToggle={this.toggleSort}
+ onSearch={this.search}
+ onContextAction={this.executeAction}
+ onChangePage={this.changePage}
+ onChangeRowsPerPage={this.changeRowsPerPage} />;
+ </div>;
+ }
-type ProjectPanelProps = ProjectPanelDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
+ componentDidMount() {
+ this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
+ }
-interface ProjectPanelState {
- sort: {
- columnName: string;
- direction: SortDirection;
- };
- filters: string[];
-}
+ toggleColumn = (toggledColumn: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.name }));
+ }
-class ProjectPanel extends React.Component<ProjectPanelProps & WithStyles<CssRules>, ProjectPanelState> {
- state: ProjectPanelState = {
- sort: {
- columnName: "Name",
- direction: "desc"
- },
- filters: ['collection', 'project']
- };
+ toggleSort = (column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name }));
+ }
- 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 (
- <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>
- );
+ changeFilters = (filters: DataTableFilterItem[], column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters }));
}
- goToItem = (item: ProjectExplorerItem) => {
- this.props.dispatch<any>(setProjectItem(this.props.projects.items, item.uuid, item.kind, ItemMode.BOTH));
+ executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
+ alert(`Executing ${action.name} on ${item.name}`);
}
- toggleSort = (column: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- sort: {
- columnName: column.name,
- direction: column.sortDirection || "none"
- }
- });
+ search = (searchValue: string) => {
+ this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue }));
}
- changeFilters = (filters: DataTableFilterItem[]) => {
- this.setState({ filters: filters.filter(f => f.selected).map(f => f.name.toLowerCase()) });
+ changePage = (page: number) => {
+ this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page }));
}
-}
-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;
-};
+ changeRowsPerPage = (rowsPerPage: number) => {
+ this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage }));
+ }
+
+ openProject = (item: ProjectPanelItem) => {
+ this.props.dispatch<any>(setProjectItem(item.uuid));
+ }
+}
type CssRules = "toolbar" | "button";
const styles: StyleRulesCallback<CssRules> = theme => ({
toolbar: {
- marginBottom: theme.spacing.unit * 3,
- display: "flex",
- justifyContent: "flex-end"
+ paddingBottom: theme.spacing.unit * 3,
+ textAlign: "right"
},
button: {
marginLeft: theme.spacing.unit
}
});
-export default withStyles(styles)(
- connect(
- (state: RootState) => ({
- projects: state.projects,
- collections: state.collections
- })
- )(ProjectPanel));
+const renderName = (item: ProjectPanelItem) =>
+ <Grid
+ container
+ alignItems="center"
+ wrap="nowrap"
+ spacing={16}>
+ <Grid item>
+ {renderIcon(item)}
+ </Grid>
+ <Grid item>
+ <Typography color="primary">
+ {item.name}
+ </Typography>
+ </Grid>
+ </Grid>;
+
+
+const renderIcon = (item: ProjectPanelItem) => {
+ switch (item.kind) {
+ case ResourceKind.LEVEL_UP:
+ return <i className="icon-level-up" style={{ fontSize: "1rem" }} />;
+ case ResourceKind.PROJECT:
+ return <i className="fas fa-folder fa-lg" />;
+ case ResourceKind.COLLECTION:
+ return <i className="fas fa-th fa-lg" />;
+ default:
+ return <i />;
+ }
+};
+
+const renderDate = (date: string) =>
+ <Typography noWrap>
+ {formatDate(date)}
+ </Typography>;
+
+const renderFileSize = (fileSize?: number) =>
+ <Typography noWrap>
+ {formatFileSize(fileSize)}
+ </Typography>;
+
+const renderOwner = (owner: string) =>
+ <Typography noWrap color="primary">
+ {owner}
+ </Typography>;
+
+const renderType = (type: string) =>
+ <Typography noWrap>
+ {type}
+ </Typography>;
+
+const renderStatus = (item: ProjectPanelItem) =>
+ <Typography noWrap align="center">
+ {item.status || "-"}
+ </Typography>;
+
+const columns: DataColumns<ProjectPanelItem> = [{
+ name: "Name",
+ selected: true,
+ sortDirection: "desc",
+ render: renderName,
+ width: "450px"
+}, {
+ name: "Status",
+ selected: true,
+ render: renderStatus,
+ width: "75px"
+}, {
+ name: "Type",
+ selected: true,
+ filters: [{
+ name: "Collection",
+ selected: true
+ }, {
+ name: "Project",
+ selected: true
+ }],
+ render: item => renderType(item.kind),
+ width: "125px"
+}, {
+ name: "Owner",
+ selected: true,
+ render: item => renderOwner(item.owner),
+ width: "200px"
+}, {
+ name: "File size",
+ selected: true,
+ render: item => renderFileSize(item.fileSize),
+ width: "50px"
+}, {
+ name: "Last modified",
+ selected: true,
+ sortDirection: "none",
+ render: item => renderDate(item.lastModified),
+ width: "150px"
+}];
+
+const contextMenuActions = [[{
+ icon: "fas fa-users fa-fw",
+ name: "Share"
+}, {
+ icon: "fas fa-sign-out-alt fa-fw",
+ name: "Move to"
+}, {
+ icon: "fas fa-star fa-fw",
+ name: "Add to favourite"
+}, {
+ icon: "fas fa-edit fa-fw",
+ name: "Rename"
+}, {
+ icon: "fas fa-copy fa-fw",
+ name: "Make a copy"
+}, {
+ icon: "fas fa-download fa-fw",
+ name: "Download"
+}], [{
+ icon: "fas fa-trash-alt fa-fw",
+ name: "Remove"
+}
+]];
+
+export default withStyles(styles)(connect()(ProjectPanel));
import { connect, DispatchProp } from "react-redux";
import { Route, Switch } from "react-router";
import authActions from "../../store/auth/auth-action";
+import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
import MainAppBar, {
import ProjectTree from '../../views-components/project-tree/project-tree';
import { TreeItem } from "../../components/tree/tree";
import { Project } from "../../models/project";
-import { getTreePath } from '../../store/project/project-reducer';
-import ProjectPanel from '../project-panel/project-panel';
+import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
import sidePanelActions from '../../store/side-panel/side-panel-action';
import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
import { ResourceKind } from "../../models/resource";
import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
import projectActions from "../../store/project/project-action";
+import ProjectPanel from "../project-panel/project-panel";
const drawerWidth = 240;
const appBarHeight = 102;
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch<any>(
- setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.BOTH)
- );
+ this.props.dispatch<any>(setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.BOTH));
},
onSearch: searchText => {
this.setState({ searchText });
projects={this.props.projects}
toggleOpen={itemId =>
this.props.dispatch<any>(
- setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.OPEN)
+ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.OPEN)
)}
toggleActive={itemId =>
this.props.dispatch<any>(
- setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
+ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
)}
/>
</SidePanel>