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;
</Grid>
</Toolbar>
<DataTable
- columns={[
- ...this.props.columns,
- this.contextMenuColumn]}
+ columns={[...this.props.columns, this.contextMenuColumn]}
items={this.props.items}
onRowClick={(_, item: T) => this.props.onRowClick(item)}
onRowContextMenu={this.openContextMenu}
<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.openContextMenuTrigger(event, item)}>
<MoreVertIcon />
</IconButton>
</Grid>
+ openContextMenuTrigger = (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();
PROJECT = "project",
COLLECTION = "collection",
PIPELINE = "pipeline",
- LEVEL_UP = "",
UNKNOWN = "unknown"
}
export function getResourceKind(itemKind: string) {
switch (itemKind) {
- case "arvados#project": return ResourceKind.PROJECT;
- case "arvados#collection": return ResourceKind.COLLECTION;
- case "arvados#pipeline": return ResourceKind.PIPELINE;
+ case "arvados#project":
+ case "arvados#group":
+ return ResourceKind.PROJECT;
+ case "arvados#collection":
+ return ResourceKind.COLLECTION;
+ case "arvados#pipeline":
+ return ResourceKind.PIPELINE;
default:
return ResourceKind.UNKNOWN;
}
--- /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 { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { DataColumns } from "../../components/data-table/data-table";
+
+const actions = unionize({
+ SET_COLUMNS: ofType<{id: string, columns: DataColumns<any> }>(),
+ SET_FILTERS: ofType<{id: string,columnName: string, filters: DataTableFilterItem[]}>(),
+ SET_ITEMS: ofType<{id: string,items: any[]}>(),
+ SET_PAGE: ofType<{id: string,page: number}>(),
+ SET_ROWS_PER_PAGE: ofType<{id: string,rowsPerPage: number}>(),
+ TOGGLE_COLUMN: ofType<{id: string, columnName: string }>(),
+ TOGGLE_SORT: ofType<{id: string, columnName: string }>(),
+ SET_SEARCH_VALUE: ofType<{id: string,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";
+import { sidePanelData } from "../side-panel/side-panel-reducer";
export const getResourceUrl = (resource: Resource): string => {
switch (resource.kind) {
- case ResourceKind.LEVEL_UP: return `/projects/${resource.ownerUuid}`;
case ResourceKind.PROJECT: return `/projects/${resource.uuid}`;
case ResourceKind.COLLECTION: return `/collections/${resource.uuid}`;
- default:
- return "#";
+ default: return "";
}
};
ACTIVE
}
-export const setProjectItem = (projects: Array<TreeItem<Project>>, itemId: string, itemKind: ResourceKind, itemMode: ItemMode) => (dispatch: Dispatch) => {
+export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { projects, router, sidePanel } = getState();
+ const treeItem = findTreeItem(projects.items, itemId);
- const openProjectItem = (resource: Resource) => {
- if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
- }
+ if (treeItem) {
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
- }
+ dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY());
+ const projectsItem = sidePanelData[0];
+ if(sidePanel.some(item => item.id === projectsItem.id && !item.open)){
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(projectsItem.id));
+ }
- dispatch(push(getResourceUrl({...resource, kind: itemKind})));
- };
+ if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
+ }
- let treeItem = findTreeItem(projects, itemId);
- if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
- treeItem = findTreeItem(projects, treeItem.data.ownerUuid);
- }
+ const resourceUrl = getResourceUrl({ ...treeItem.data });
- if (treeItem) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+ if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+ if (router.location && !router.location.pathname.includes(resourceUrl)) {
+ dispatch(push(resourceUrl));
+ }
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+ }
+
+ const promise = treeItem.status === TreeItemStatus.Loaded
+ ? Promise.resolve()
+ : dispatch<any>(getProjectList(itemId));
+
+ promise
+ .then(() => dispatch<any>(getCollectionList(itemId)))
+ .then(() => dispatch<any>(() => {
+ const { projects, collections } = getState();
+ dispatch(dataExplorerActions.SET_ITEMS({
+ id: PROJECT_PANEL_ID,
+ items: projectPanelItems(
+ projects.items,
+ treeItem.data.uuid,
+ collections
+ )
+ }));
+ }));
- if (treeItem.status === TreeItemStatus.Loaded) {
- openProjectItem(treeItem.data);
- } else {
- dispatch<any>(getProjectList(itemId))
- .then(() => openProjectItem(treeItem!.data));
- }
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- dispatch<any>(getCollectionList(itemId));
}
- }
-};
+ };
PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
- RESET_PROJECT_TREE_ACTIVITY: ofType<string>(),
+ RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
}, {
tag: 'type',
value: 'payload'
resetTreeActivity(items);
const item = findTreeItem(items, itemId);
if (item) {
+ item.toggled = true;
item.active = true;
}
return {
const actions = unionize({
TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
- RESET_SIDE_PANEL_ACTIVITY: ofType<string>(),
+ RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
}, {
tag: 'type',
value: 'payload'
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) {
- dataItems.push({
- name: "..",
- url: getResourceUrl(treeItem.data),
- kind: ResourceKind.LEVEL_UP,
- owner: "",
- uuid: treeItem.data.uuid,
- lastModified: ""
- });
-
if (treeItem.items) {
treeItem.items.forEach(p => {
const item = {
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 { 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 { DataColumns } from '../../components/data-table/data-table';
+import { ResourceKind } from "../../models/resource";
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 { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
-interface ProjectPanelDataProps {
- projects: ProjectState;
- collections: CollectionState;
+export const PROJECT_PANEL_ID = "projectPanel";
+
+type ProjectPanelProps = {
+ currentItemId: string,
+ onItemClick: (item: ProjectPanelItem) => void,
+ onItemRouteChange: (itemId: string) => void
}
+ & DispatchProp
+ & WithStyles<CssRules>
+ & RouteComponentProps<{ id: string }>;
+class ProjectPanel extends React.Component<ProjectPanelProps> {
+ 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.props.onItemClick}
+ 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[];
-}
+ componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) {
+ if (match.params.id !== currentItemId) {
+ this.props.onItemRouteChange(match.params.id);
+ }
+ }
-class ProjectPanel extends React.Component<ProjectPanelProps & WithStyles<CssRules>, ProjectPanelState> {
- state: ProjectPanelState = {
- sort: {
- columnName: "Name",
- direction: "desc"
- },
- filters: ['collection', 'project']
- };
+ toggleColumn = (toggledColumn: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.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>
- );
+ toggleSort = (column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name }));
}
- goToItem = (item: ProjectExplorerItem) => {
- this.props.dispatch<any>(setProjectItem(this.props.projects.items, item.uuid, item.kind, ItemMode.BOTH));
+ changeFilters = (filters: DataTableFilterItem[], column: DataColumn<ProjectPanelItem>) => {
+ this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters }));
}
- toggleSort = (column: DataColumn<ProjectExplorerItem>) => {
- this.setState({
- sort: {
- columnName: column.name,
- direction: column.sortDirection || "none"
- }
- });
+ executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
+ alert(`Executing ${action.name} on ${item.name}`);
}
- changeFilters = (filters: DataTableFilterItem[]) => {
- this.setState({ filters: filters.filter(f => f.selected).map(f => f.name.toLowerCase()) });
+ search = (searchValue: string) => {
+ this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue }));
}
-}
-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;
-};
+ changePage = (page: number) => {
+ this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page }));
+ }
+
+ changeRowsPerPage = (rowsPerPage: number) => {
+ this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage }));
+ }
+
+}
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
}
});
+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.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(
- (state: RootState) => ({
- projects: state.projects,
- collections: state.collections
- })
- )(ProjectPanel));
+ connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ ProjectPanel));
import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
-import { Route, Switch } from "react-router";
+import { Route, Switch, RouteComponentProps, withRouter } 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";
+import { sidePanelData } from '../../store/side-panel/side-panel-reducer';
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, ItemMode.BOTH));
},
onSearch: searchText => {
this.setState({ searchText });
}
render() {
- const branch = getTreePath(this.props.projects, this.props.currentProjectId);
- const breadcrumbs = branch.map(item => ({
+ const path = getTreePath(this.props.projects, this.props.currentProjectId);
+ const breadcrumbs = path.map(item => ({
label: item.data.name,
itemId: item.data.uuid,
status: item.status
sidePanelItems={this.props.sidePanelItems}>
<ProjectTree
projects={this.props.projects}
- toggleOpen={itemId =>
- this.props.dispatch<any>(
- setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.OPEN)
- )}
- toggleActive={itemId =>
- this.props.dispatch<any>(
- setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
- )}
+ toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+ toggleActive={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
/>
</SidePanel>
</Drawer>}
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path="/projects/:name" component={ProjectPanel} />
+ <Route path="/projects/:id" render={this.renderProjectPanel} />
</Switch>
</div>
</main>
</div>
);
}
+
+ renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
+ onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+ onItemClick={item => this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE))}
+ {...props} />
+
}
export default connect<WorkbenchDataProps>(