+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React 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 { SidePanelRightArrowIcon, IconType } from '../icon/icon';
-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';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- overflowY: 'auto',
- minWidth: '240px',
- whiteSpace: 'nowrap',
- marginTop: '52px',
- display: 'flex',
- flexGrow: 1,
- },
- list: {
- padding: '5px 0px 5px 14px',
- minWidth: '240px',
- },
- row: {
- display: 'flex',
- alignItems: 'center',
- },
- toggableIconContainer: {
- color: theme.palette.grey["700"],
- height: '14px',
- width: '14px'
- },
- toggableIcon: {
- fontSize: '14px'
- },
- active: {
- color: theme.palette.primary.main,
- },
- iconClose: {
- transition: 'all 0.1s ease',
- },
- iconOpen: {
- transition: 'all 0.1s ease',
- transform: 'rotate(90deg)',
- }
-});
-
-export interface SidePanelItem {
- id: string;
- name: string;
- url: string;
- icon: IconType;
- open?: boolean;
- margin?: boolean;
- openAble?: boolean;
- activeAction?: (dispatch: Dispatch, uuid?: string) => void;
-}
-
-interface SidePanelDataProps {
- toggleOpen: (id: string) => void;
- toggleActive: (id: string) => void;
- sidePanelItems: SidePanelItem[];
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
-}
-
-type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-
-export const SidePanel = withStyles(styles)(withRouter(
- class extends React.Component<SidePanelProps> {
- 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 => {
- 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, active)}>
- < SidePanelRightArrowIcon/>
- </ListItemIcon>
- </i>
- ) : null}
- <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
- hasMargin={it.margin}/>
- </span>
- </ListItem>
- {it.openAble ? (
- <Collapse in={it.open} timeout="auto" unmountOnExit>
- {children}
- </Collapse>
- ) : null}
- </span>;
- })}
- </List>
- </div>
- );
- }
-
- getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
- const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
- return classnames(toggableIcon, {
- [iconOpen]: isOpen,
- [iconClose]: !isOpen,
- [active]: isActive
- });
- }
-
- handleRowContextMenu = (item: SidePanelItem) =>
- (event: React.MouseEvent<HTMLElement>) =>
- item.openAble ? this.props.onContextMenu(event, item) : null
- }
-));
onContextMenu={this.handleRowContextMenu(it)}>
{it.status === TreeItemStatus.PENDING ?
<CircularProgress size={10} className={loader} /> : null}
- <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
+ <i onClick={this.handleToggleItemOpen(it.id, it.status)}
className={toggableIconContainer}>
<ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
{this.getProperArrowAnimation(it.status, it.items!)}
}
: undefined;
}
+
+ handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent<HTMLElement>) => {
+ event.stopPropagation();
+ this.props.toggleItemOpen(id, status);
+ }
}
);
import { ResourceKind, Resource, extractUuidKind } from '~/models/resource';
import { getCollectionUrl } from "~/models/collection";
import { getProjectUrl } from "~/models/project";
-import { getResource } from '~/store/resources/resources';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-action';
import { snackbarActions } from '../snackbar/snackbar-actions';
import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
import { Routes } from '~/routes/routes';
import { loadResource } from '../resources/resources-actions';
-import { ServiceRepository } from '~/services/services';
import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
import { projectPanelColumns } from '~/views/project-panel/project-panel';
import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
export const navigateTo = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
+ async (dispatch: Dispatch) => {
const kind = extractUuidKind(uuid);
if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER) {
dispatch<any>(navigateToProject(uuid));
}
};
-const getResourceNavigationAction = (resource: Resource) => {
- switch (resource.kind) {
- case ResourceKind.COLLECTION:
- return navigateToCollection(resource.uuid);
- case ResourceKind.PROJECT:
- case ResourceKind.USER:
- return navigateToProject(resource.uuid);
- default:
- return cannotNavigateToResource(resource);
- }
-};
-
export const loadWorkbench = () =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ async (dispatch: Dispatch, getState: () => RootState) => {
const { auth, router } = getState();
const { user } = auth;
if (user) {
export const navigateToFavorites = push(Routes.FAVORITES);
export const loadFavorites = () =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ (dispatch: Dispatch) => {
dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
dispatch<any>(loadFavoritePanel());
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
import { resourcesActions } from '../resources/resources-actions';
import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
import { TreeItemStatus } from "~/components/tree/tree";
-import { getNodeAncestors, getNodeValue } from '~/models/tree';
+import { getNodeAncestors, getNodeValue, getNodeAncestorsIds } from '~/models/tree';
import { ProjectResource } from '~/models/project';
export enum SidePanelTreeCategory {
export const activateSidePanelTreeItem = (nodeId: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const node = getSidePanelTreeNode(nodeId)(getState());
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
if (node && !node.selected) {
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
}
export const activateSidePanelTreeProject = (nodeId: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const node = getSidePanelTreeNode(nodeId)(getState());
+ const { treePicker } = getState();
+ const node = getSidePanelTreeNode(nodeId)(treePicker);
if (node && node.status !== TreeItemStatus.LOADED) {
await dispatch<any>(loadSidePanelTreeProjects(nodeId));
- dispatch<any>(expandSidePanelTreeItem(nodeId));
} else if (node === undefined) {
await dispatch<any>(activateSidePanelTreeBranch(nodeId));
}
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker),
+ pickerId: SIDE_PANEL_TREE
+ }));
+ dispatch<any>(expandSidePanelTreeItem(nodeId));
};
export const activateSidePanelTreeBranch = (nodeId: string) =>
for (const ancestor of ancestors) {
await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
}
- for (const ancestor of ancestors) {
- dispatch<any>(expandSidePanelTreeItem(ancestor.uuid));
- }
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: ancestors.map(ancestor => ancestor.uuid),
+ pickerId: SIDE_PANEL_TREE
+ }));
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
};
export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
async (dispatch: Dispatch, getState: () => RootState) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && node.status === TreeItemStatus.INITIAL) {
+ await dispatch<any>(loadSidePanelTreeProjects(node.nodeId));
+ }
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
};
export const expandSidePanelTreeItem = (nodeId: string) =>
async (dispatch: Dispatch, getState: () => RootState) => {
- const node = getSidePanelTreeNode(nodeId)(getState());
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
if (node && node.collapsed) {
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
}
};
-const getSidePanelTreeNode = (nodeId: string) => (state: RootState) => {
- const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(state.treePicker);
+const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
return sidePanelTree
? getNodeValue(nodeId)(sidePanelTree)
: undefined;
};
+const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+ return sidePanelTree
+ ? getNodeAncestorsIds(nodeId)(sidePanelTree)
+ : [];
+};
import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { RootState } from '~/store/store';
-import { extractUuidKind, ResourceKind } from '~/models/resource';
-import { openProjectContextMenu } from '~/store/context-menu/context-menu-actions';
-import { openRootProjectContextMenu } from '../context-menu/context-menu-actions';
export const navigateFromSidePanel = (id: string) =>
(dispatch: Dispatch) => {
LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array<TreePickerNode>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODES: ofType<{ nodeIds: string[], pickerId: string }>(),
RESET_TREE_PICKER: ofType<{ pickerId: string }>()
});
updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)),
TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) =>
updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))),
- RESET_TREE_PICKER: ({ pickerId }) =>
+ RESET_TREE_PICKER: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, createTree),
+ EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) =>
+ updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))),
default: () => state
});
return { ...state, [pickerId]: updatedPicker };
};
+const expand = (ids: string[]) => (node: TreePickerNode): TreePickerNode =>
+ ids.some(id => id === node.nodeId)
+ ? { ...node, collapsed: false }
+ : node;
+
const setPending = (value: TreePickerNode): TreePickerNode =>
({ ...value, status: TreeItemStatus.PENDING });