}
interface DataExplorerActionProps<T> {
+ onSetColumns: (columns: DataColumns<T>) => void;
onSearch: (value: string) => void;
onRowClick: (item: T) => void;
onRowDoubleClick: (item: T) => void;
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+ componentDidMount() {
+ if (this.props.onSetColumns) {
+ this.props.onSetColumns(this.props.columns);
+ }
+ }
render() {
const {
columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ReactElement } from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
import * as classnames from "classnames";
import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
import { Dispatch } from "redux";
+import { RouteComponentProps, withRouter } from "react-router";
type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
export interface SidePanelItem {
id: string;
name: string;
+ url: string;
icon: IconType;
- active?: boolean;
open?: boolean;
margin?: boolean;
openAble?: boolean;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
}
-type SidePanelProps = SidePanelDataProps & WithStyles<CssRules>;
+type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-export const SidePanel = withStyles(styles)(
+export const SidePanel = withStyles(styles)(withRouter(
class extends React.Component<SidePanelProps> {
- render(): ReactElement<any> {
+ render() {
const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
const { root, row, list, toggableIconContainer } = classes;
+
+ const path = this.props.location.pathname.split('/');
+ const activeUrl = path.length > 1 ? "/" + path[1] : "/";
return (
<div className={root}>
<List>
- {sidePanelItems.map(it => (
- <span key={it.name}>
+ {sidePanelItems.map(it => {
+ const active = it.url === activeUrl;
+ return <span key={it.name}>
<ListItem button className={list} onClick={() => toggleActive(it.id)}
onContextMenu={this.handleRowContextMenu(it)}>
<span className={row}>
{it.openAble ? (
<i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
<ListItemIcon
- className={this.getToggableIconClassNames(it.open, it.active)}>
+ className={this.getToggableIconClassNames(it.open, active)}>
< SidePanelRightArrowIcon/>
</ListItemIcon>
</i>
) : null}
- <ListItemTextIcon icon={it.icon} name={it.name} isActive={it.active}
+ <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
hasMargin={it.margin}/>
</span>
</ListItem>
{children}
</Collapse>
) : null}
- </span>
- ))}
+ </span>;
+ })}
</List>
</div>
);
(event: React.MouseEvent<HTMLElement>) =>
item.openAble ? this.props.onContextMenu(event, item) : null
}
-);
+));
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
- tag: 'type',
- value: 'payload'
- });
+ tag: 'type',
+ value: 'payload'
+});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
services.apiClient.defaults.headers.common = {
it('should set columns', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
- configurable: true
+ configurable: true,
+ sortDirection: SortDirection.NONE
}];
const state = dataExplorerReducer(undefined,
dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
it('should toggle sorting', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
configurable: true,
sortDirection: SortDirection.ASC
}, {
name: "Column 2",
+ filters: [],
render: jest.fn(),
selected: true,
configurable: true,
it('should set filters', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
- configurable: true
+ configurable: true,
+ sortDirection: SortDirection.NONE
}];
const filters: DataTableFilterItem[] = [{
const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
+const canUpdateColumns = (prevColumns: DataColumns<any>, nextColumns: DataColumns<any>) => {
+ if (prevColumns.length !== nextColumns.length) {
+ return true;
+ }
+ for (let i = 0; i < nextColumns.length; i++) {
+ const pc = prevColumns[i];
+ const nc = nextColumns[i];
+ if (pc.key !== nc.key || pc.name !== nc.name) {
+ return true;
+ }
+ }
+ return false;
+};
+
const setColumns = (columns: DataColumns<any>) =>
(dataExplorer: DataExplorer) =>
- ({ ...dataExplorer, columns });
+ ({ ...dataExplorer, columns: canUpdateColumns(dataExplorer.columns, columns) ? columns : dataExplorer.columns });
const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
(dataExplorer: DataExplorer) =>
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { projectActions, getProjectList } from "../project/project-action";
+import { getProjectList, projectActions } from "../project/project-action";
import { push } from "react-router-redux";
import { TreeItemStatus } from "~/components/tree/tree";
import { findTreeItem } from "../project/project-reducer";
import { RootState } from "../store";
-import { Resource, ResourceKind } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { getCollectionUrl } from "~/models/collection";
import { getProjectUrl, ProjectResource } from "~/models/project";
import { ProjectService } from "~/services/project-service/project-service";
import { ServiceRepository } from "~/services/services";
import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelIdentifiers } from "../side-panel/side-panel-reducer";
+import { SidePanelId } from "../side-panel/side-panel-reducer";
import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
-export const getResourceUrl = <T extends Resource>(resource: T): string => {
- switch (resource.kind) {
- case ResourceKind.PROJECT: return getProjectUrl(resource.uuid);
- case ResourceKind.COLLECTION: return getCollectionUrl(resource.uuid);
- default: return resource.href;
+export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
+ switch (resourceKind) {
+ case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
+ case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
+ default:
+ return '';
}
};
const treeItem = findTreeItem(projects.items, itemId);
if (treeItem) {
- const resourceUrl = getResourceUrl(treeItem.data);
+ const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
if (router.location && !router.location.pathname.includes(resourceUrl)) {
const ancestors = await loadProjectAncestors(itemId, services.projectService);
const uuids = ancestors.map(ancestor => ancestor.uuid);
await loadBranch(uuids, dispatch);
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelIdentifiers.PROJECTS));
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
uuids.forEach(uuid => {
dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
});
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
export const sidePanelActions = unionize({
- TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
- TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
- RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
+ TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
}, {
tag: 'type',
value: 'payload'
import { ProjectsIcon } from "~/components/icon/icon";
describe('side-panel-reducer', () => {
-
- it('should toggle activity on side-panel', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: true,
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
- expect(state).toEqual(project);
- });
-
it('should open side-panel item', () => {
const initialState = [
{
id: "1",
name: "Projects",
+ url: "/projects",
icon: ProjectsIcon,
- open: false,
- active: false,
+ open: false
}
];
const project = [
name: "Projects",
icon: ProjectsIcon,
open: true,
- active: false,
+ url: "/projects"
}
];
const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
expect(state).toEqual(project);
});
-
- it('should remove activity on side-panel item', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: true,
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
- expect(state).toEqual(project);
- });
});
//
// SPDX-License-Identifier: AGPL-3.0
-import * as _ from "lodash";
import { sidePanelActions, SidePanelAction } from './side-panel-action';
import { SidePanelItem } from '~/components/side-panel/side-panel';
import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { projectActions } from "../project/project-action";
import { getProjectUrl } from "../../models/project";
+import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
+import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
export type SidePanelState = SidePanelItem[];
-export const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
- if (state.length === 0) {
- return sidePanelData;
- } else {
- return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
- state.map(it => ({...it, open: itemId === it.id && it.open === false})),
- TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
- const sidePanel = _.cloneDeep(state);
- resetSidePanelActivity(sidePanel);
- sidePanel.forEach(it => {
- if (it.id === itemId) {
- it.active = true;
- }
- });
- return sidePanel;
- },
- RESET_SIDE_PANEL_ACTIVITY: () => {
- const sidePanel = _.cloneDeep(state);
- resetSidePanelActivity(sidePanel);
- return sidePanel;
- },
- default: () => state
- });
- }
+export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
+ return sidePanelActions.match(action, {
+ TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
+ state.map(it => ({...it, open: itemId === it.id && it.open === false})),
+ default: () => state
+ });
};
-export enum SidePanelIdentifiers {
+export enum SidePanelId {
PROJECTS = "Projects",
SHARED_WITH_ME = "SharedWithMe",
WORKFLOWS = "Workflows",
TRASH = "Trash"
}
-export const sidePanelData = [
+export const sidePanelItems = [
{
- id: SidePanelIdentifiers.PROJECTS,
+ id: SidePanelId.PROJECTS,
name: "Projects",
+ url: "/projects",
icon: ProjectsIcon,
open: false,
active: false,
margin: true,
openAble: true,
activeAction: (dispatch: Dispatch, uuid: string) => {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
dispatch(push(getProjectUrl(uuid)));
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(projectPanelActions.REQUEST_ITEMS());
}
},
{
- id: SidePanelIdentifiers.SHARED_WITH_ME,
+ id: SidePanelId.SHARED_WITH_ME,
name: "Shared with me",
+ url: "/shared",
icon: ShareMeIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/shared"));
+ }
},
{
- id: SidePanelIdentifiers.WORKFLOWS,
+ id: SidePanelId.WORKFLOWS,
name: "Workflows",
+ url: "/workflows",
icon: WorkflowIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/workflows"));
+ }
},
{
- id: SidePanelIdentifiers.RECENT_OPEN,
+ id: SidePanelId.RECENT_OPEN,
name: "Recent open",
+ url: "/recent",
icon: RecentIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/recent"));
+ }
},
{
- id: SidePanelIdentifiers.FAVORITES,
+ id: SidePanelId.FAVORITES,
name: "Favorites",
+ url: "/favorites",
icon: FavoriteIcon,
active: false,
activeAction: (dispatch: Dispatch) => {
dispatch(push("/favorites"));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
dispatch(favoritePanelActions.RESET_PAGINATION());
dispatch(favoritePanelActions.REQUEST_ITEMS());
}
},
{
- id: SidePanelIdentifiers.TRASH,
+ id: SidePanelId.TRASH,
name: "Trash",
+ url: "/trash",
icon: TrashIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/trash"));
+ }
}
];
-
-function resetSidePanelActivity(sidePanel: SidePanelItem[]) {
- for (const t of sidePanel) {
- t.active = false;
- }
-}
extractKey?: (item: any) => React.Key;
}
-const mapStateToProps = (state: RootState, { id }: Props) =>
- getDataExplorer(state.dataExplorer, id);
+const mapStateToProps = (state: RootState, { id, columns }: Props) => {
+ const s = getDataExplorer(state.dataExplorer, id);
+ if (s.columns.length === 0) {
+ s.columns = columns;
+ }
+ return s;
+};
const mapDispatchToProps = () => {
- let prevColumns: DataColumns<any>;
- return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
- if (columns !== prevColumns) {
- prevColumns = columns;
+ return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+ onSetColumns: (columns: DataColumns<any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
- }
- return {
- onSearch: (searchValue: string) => {
- dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
- },
+ },
+
+ onSearch: (searchValue: string) => {
+ dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+ },
- onColumnToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
- },
+ onColumnToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+ },
- onSortToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
- },
+ onSortToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+ },
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
- dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
- },
+ onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+ },
- onChangePage: (page: number) => {
- dispatch(dataExplorerActions.SET_PAGE({ id, page }));
- },
+ onChangePage: (page: number) => {
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+ },
- onChangeRowsPerPage: (rowsPerPage: number) => {
- dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
- },
+ onChangeRowsPerPage: (rowsPerPage: number) => {
+ dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+ },
- onRowClick,
+ onRowClick,
- onRowDoubleClick,
+ onRowDoubleClick,
- onContextMenu,
- };
- };
+ onContextMenu,
+ });
};
export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
-import { SidePanelIdentifiers } from '~/store/side-panel/side-panel-reducer';
import { ProjectResource } from '~/models/project';
import { ResourceKind } from '~/models/resource';
import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
toggleActive={itemId => {
this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}} />
</SidePanel>
</Drawer>}
default:
this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}
}}
}
toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);