--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { defaultTo, property } from 'lodash';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { TextField } from "../text-field/text-field";
+
+export interface ConfirmationDialogDataProps {
+ title: string;
+ text: string;
+ cancelButtonLabel?: string;
+ confirmButtonLabel?: string;
+}
+
+export interface ConfirmationDialogProps {
+ onConfirm: () => void;
+}
+
+export const ConfirmationDialog = (props: ConfirmationDialogProps & WithDialogProps<ConfirmationDialogDataProps>) =>
+ <Dialog open={props.open}>
+ <DialogTitle>{props.data.title}</DialogTitle>
+ <DialogContent>
+ <DialogContentText>
+ {props.data.text}
+ </DialogContentText>
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ {props.data.cancelButtonLabel || 'Cancel'}
+ </Button>
+ <Button
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={props.onConfirm}>
+ {props.data.confirmButtonLabel || 'Ok'}
+ </Button>
+ </DialogActions>
+ </Dialog>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { InjectedFormProps, Field } from "redux-form";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { TextField } from "../text-field/text-field";
+
+export const RenameDialog = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
+ <form>
+ <Dialog open={props.open}>
+ <DialogTitle>{`Rename`}</DialogTitle>
+ <DialogContent>
+ <DialogContentText>
+ {`Please, enter a new name for ${props.data}`}
+ </DialogContentText>
+ <Field
+ name='name'
+ component={TextField}
+ />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ disabled={props.submitting}
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={props.handleSubmit}
+ disabled={props.pristine || props.invalid || props.submitting}>
+ {props.submitting
+ ? <CircularProgress size={20} />
+ : 'Ok'}
+ </Button>
+ </DialogActions>
+ </Dialog>
+ </form>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps } from 'redux-form';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { TextField as MaterialTextField, StyleRulesCallback, WithStyles, withStyles } from '../../../node_modules/@material-ui/core';
+
+type CssRules = 'textField';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ textField: {
+ marginBottom: theme.spacing.unit * 3
+ },
+});
+
+export const TextField = withStyles(styles)((props: WrappedFieldProps & WithStyles<CssRules> & { label?: string }) =>
+ <MaterialTextField
+ helperText={props.meta.touched && props.meta.error}
+ className={props.classes.textField}
+ label={props.label}
+ disabled={props.meta.submitting}
+ error={props.meta.touched && !!props.meta.error}
+ autoComplete='off'
+ fullWidth={true}
+ {...props.input}
+ />);
\ No newline at end of file
items?: Array<TreeItem<T>>;
}
-interface TreeProps<T> {
+export interface TreeProps<T> {
items?: Array<TreeItem<T>>;
render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
toggleItemOpen: (id: string, status: TreeItemStatus) => void;
export interface Dialog {
open: boolean;
- data?: any;
+ data: any;
}
export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
CLOSE_DIALOG: ({ id }) => ({
...state,
- [id]: state[id] ? { ...state[id], open: false } : { open: false } }),
+ [id]: state[id] ? { ...state[id], open: false } : { open: false, data: {} } }),
default: () => state,
});
import { Dispatch } from 'redux';
import { dialogActions } from './dialog-actions';
-export type WithDialog<T> = {
+export type WithDialogStateProps<T> = {
open: boolean;
- data?: T;
+ data: T;
};
-export type WithDialogActions = {
+export type WithDialogDispatchProps = {
closeDialog: () => void;
};
+export type WithDialogProps<T> = WithDialogStateProps<T> & WithDialogDispatchProps;
+
export const withDialog = (id: string) =>
- <T>(component: React.ComponentType<WithDialog<T> & WithDialogActions>) =>
+ <T, P>(component: React.ComponentType<WithDialogProps<T> & P>) =>
connect(mapStateToProps(id), mapDispatchToProps(id))(component);
-export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialog<T> => {
+export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialogStateProps<T> => {
const dialog = state.dialog[id];
- return dialog ? dialog : { open: false };
+ return dialog ? dialog : { open: false, data: {} };
};
-export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogActions => ({
+export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogDispatchProps => ({
closeDialog: () => {
dispatch(dialogActions.CLOSE_DIALOG({ id }));
}
import { DialogState, dialogReducer } from './dialog/dialog-reducer';
import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
import { ServiceRepository } from "../services/services";
+import { treePickerReducer } from './tree-picker/tree-picker-reducer';
+import { TreePicker } from './tree-picker/tree-picker';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
snackbar: SnackbarState;
collectionPanelFiles: CollectionPanelFilesState;
dialog: DialogState;
+ treePicker: TreePicker;
}
export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
favorites: favoritesReducer,
snackbar: snackbarReducer,
collectionPanelFiles: collectionPanelFilesReducer,
- dialog: dialogReducer
+ dialog: dialogReducer,
+ treePicker: treePickerReducer,
});
const projectPanelMiddleware = dataExplorerMiddleware(
--- /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 { TreeNode } from "../../models/tree";
+import { TreePickerNode } from "./tree-picker";
+
+export const treePickerActions = unionize({
+ LOAD_TREE_PICKER_NODE: ofType<{ id: string }>(),
+ LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreePickerNode> }>(),
+ TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string }>(),
+ TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ id: string }>()
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+export type TreePickerAction = UnionOf<typeof treePickerActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, getNodeValue, getNodeChildren } from "../../models/tree";
+import { TreePickerNode, createTreePickerNode } from "./tree-picker";
+import { treePickerReducer } from "./tree-picker-reducer";
+import { treePickerActions } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+describe('TreePickerReducer', () => {
+ it('LOAD_TREE_PICKER_NODE - initial state', () => {
+ const tree = createTree<TreePickerNode>();
+ const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' }));
+ expect(newTree).toEqual(tree);
+ });
+
+ it('LOAD_TREE_PICKER_NODE', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' })));
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ status: TreeItemStatus.PENDING
+ });
+ });
+
+ it('LOAD_TREE_PICKER_NODE_SUCCESS - initial state', () => {
+ const tree = createTree<TreePickerNode>();
+ const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+ const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode] }));
+ expect(getNodeChildren('')(newTree)).toEqual(['1.1']);
+ });
+
+ it('LOAD_TREE_PICKER_NODE_SUCCESS', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode] })));
+ expect(getNodeChildren('1')(newTree)).toEqual(['1.1']);
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ status: TreeItemStatus.LOADED
+ });
+ });
+
+ it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - collapsed', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ collapsed: true
+ });
+ });
+
+ it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ collapsed: false
+ });
+ });
+
+ it('TOGGLE_TREE_PICKER_NODE_SELECT - selected', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ selected: true
+ });
+ });
+
+ it('TOGGLE_TREE_PICKER_NODE_SELECT - not selected', () => {
+ const tree = createTree<TreePickerNode>();
+ const node = createTreePickerNode({ id: '1', value: '1' });
+ const [newTree] = [tree]
+ .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })))
+ .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+ expect(getNodeValue('1')(newTree)).toEqual({
+ ...createTreePickerNode({ id: '1', value: '1' }),
+ selected: false
+ });
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTree, mapTreeValues } from "../../models/tree";
+import { TreePicker, TreePickerNode } from "./tree-picker";
+import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
+ treePickerActions.match(action, {
+ LOAD_TREE_PICKER_NODE: ({ id }) =>
+ setNodeValueWith(setPending)(id)(state),
+ LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes }) => {
+ const [newState] = [state]
+ .map(receiveNodes(nodes)(id))
+ .map(setNodeValueWith(setLoaded)(id));
+ return newState;
+ },
+ TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id }) =>
+ setNodeValueWith(toggleCollapse)(id)(state),
+ TOGGLE_TREE_PICKER_NODE_SELECT: ({ id }) =>
+ mapTreeValues(toggleSelect(id))(state),
+ default: () => state
+ });
+
+const setPending = (value: TreePickerNode): TreePickerNode =>
+ ({ ...value, status: TreeItemStatus.PENDING });
+
+const setLoaded = (value: TreePickerNode): TreePickerNode =>
+ ({ ...value, status: TreeItemStatus.LOADED });
+
+const toggleCollapse = (value: TreePickerNode): TreePickerNode =>
+ ({ ...value, collapsed: !value.collapsed });
+
+const toggleSelect = (id: string) => (value: TreePickerNode): TreePickerNode =>
+ value.id === id
+ ? ({ ...value, selected: !value.selected })
+ : ({ ...value, selected: false });
+
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: TreePicker) =>
+ nodes.reduce((tree, node) =>
+ setNode(
+ createTreeNode(parent)(node)
+ )(tree), state);
+
+const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
+ children: [],
+ id: node.id,
+ parent,
+ value: node
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from "../../models/tree";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+export type TreePicker = Tree<TreePickerNode>;
+
+export interface TreePickerNode {
+ id: string;
+ value: any;
+ selected: boolean;
+ collapsed: boolean;
+ status: TreeItemStatus;
+}
+
+export const createTreePickerNode = (data: {id: string, value: any}) => ({
+ ...data,
+ selected: false,
+ collapsed: true,
+ status: TreeItemStatus.INITIAL
+});
\ No newline at end of file
export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
+export const COLLECTION_PROJECT_VALIDATION = [require];
import { ContextMenuActionSet } from "../context-menu-action-set";
import { collectionPanelFilesAction } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+import { openMultipleFilesRemoveDialog } from "../../file-remove-dialog/multiple-files-remove-dialog";
+import { createCollectionWithSelected } from "../../create-collection-dialog-with-selected/create-collection-dialog-with-selected";
export const collectionFilesActionSet: ContextMenuActionSet = [[{
execute: (dispatch) => {
dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
}
-},{
+}, {
name: "Unselect all",
execute: (dispatch) => {
dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
}
-},{
+}, {
name: "Remove selected",
execute: (dispatch, resource) => {
- dispatch(openRemoveDialog('selected files'));
+ dispatch(openMultipleFilesRemoveDialog());
}
-},{
+}, {
name: "Download selected",
execute: (dispatch, resource) => {
return;
}
-},{
+}, {
name: "Create a new collection with selected",
- execute: (dispatch, resource) => {
- return;
+ execute: (dispatch) => {
+ dispatch<any>(createCollectionWithSelected());
}
}]];
import { ContextMenuActionSet } from "../context-menu-action-set";
import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
-import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
-import { openRenameDialog } from "../../rename-dialog/rename-dialog";
+import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
+import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
name: "Rename",
icon: RenameIcon,
execute: (dispatch, resource) => {
- dispatch(openRenameDialog('the item'));
+ dispatch<any>(openRenameFileDialog(resource.name));
}
-},{
+}, {
name: "Download",
icon: DownloadIcon,
execute: (dispatch, resource) => {
return;
}
-},{
+}, {
name: "Remove",
icon: RemoveIcon,
execute: (dispatch, resource) => {
- dispatch(openRemoveDialog('selected file'));
+ dispatch(openFileRemoveDialog(resource.uuid));
}
}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { reduxForm, reset, startSubmit, stopSubmit } from "redux-form";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { DialogCollectionCreateWithSelected } from "../dialog-create/dialog-collection-create-selected";
+import { loadProjectTreePickerProjects } from "../project-tree-picker/project-tree-picker";
+
+export const DIALOG_COLLECTION_CREATE_WITH_SELECTED = 'dialogCollectionCreateWithSelected';
+
+export const createCollectionWithSelected = () =>
+ (dispatch: Dispatch) => {
+ dispatch(reset(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
+ dispatch<any>(loadProjectTreePickerProjects(''));
+ dispatch(dialogActions.OPEN_DIALOG({ id: DIALOG_COLLECTION_CREATE_WITH_SELECTED, data: {} }));
+ };
+
+export const [DialogCollectionCreateWithSelectedFile] = [DialogCollectionCreateWithSelected]
+ .map(withDialog(DIALOG_COLLECTION_CREATE_WITH_SELECTED))
+ .map(reduxForm({
+ form: DIALOG_COLLECTION_CREATE_WITH_SELECTED,
+ onSubmit: (data, dispatch) => {
+ dispatch(startSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
+ setTimeout(() => dispatch(stopSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED, { name: 'Invalid name' })), 2000);
+ }
+ }));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { InjectedFormProps, Field, WrappedFieldProps } from "redux-form";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress, FormHelperText } from "@material-ui/core";
+import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { TextField } from "../../components/text-field/text-field";
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "../../validators/create-project/create-project-validator";
+import { ProjectTreePicker } from "../project-tree-picker/project-tree-picker";
+
+export const DialogCollectionCreateWithSelected = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
+ <form>
+ <Dialog open={props.open}
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+ <DialogTitle>Create a collection</DialogTitle>
+ <DialogContent style={{ display: 'flex' }}>
+ <div>
+ <Field
+ name='name'
+ component={TextField}
+ validate={COLLECTION_NAME_VALIDATION}
+ label="Collection Name" />
+ <Field
+ name='description'
+ component={TextField}
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ label="Description - optional" />
+ </div>
+ <Field
+ name="projectUuid"
+ component={Picker}
+ validate={COLLECTION_PROJECT_VALIDATION} />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ disabled={props.submitting}
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={props.handleSubmit}
+ disabled={props.pristine || props.invalid || props.submitting}>
+ {props.submitting
+ ? <CircularProgress size={20} />
+ : 'Create a collection'}
+ </Button>
+ </DialogActions>
+ </Dialog>
+ </form>;
+
+const Picker = (props: WrappedFieldProps) =>
+ <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+ <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
+ </div>;
import * as React from 'react';
import { reduxForm, Field } from 'redux-form';
import { compose } from 'redux';
-import TextField from '@material-ui/core/TextField';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogContent from '@material-ui/core/DialogContent';
-import DialogTitle from '@material-ui/core/DialogTitle';
+import { TextField } from '../../components/text-field/text-field';
+import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
files: UploadFile[];
}
-interface TextFieldProps {
- label: string;
- floatinglabeltext: string;
- className?: string;
- input?: string;
- meta?: any;
-}
-
export const DialogCollectionCreate = compose(
connect((state: RootState) => ({
files: state.collections.uploader
})),
reduxForm({ form: 'collectionCreateDialog' }),
withStyles(styles))(
- class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
- render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
- const busy = submitting || files.reduce(
- (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
- ) > 0;
- return (
- <Dialog
- open={open}
- onClose={handleClose}
- fullWidth={true}
- maxWidth='sm'
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
- <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
- <DialogContent className={classes.formContainer}>
- <Field name="name"
+ class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
+ render() {
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
+ const busy = submitting || files.reduce(
+ (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
+ ) > 0;
+ return (
+ <Dialog
+ open={open}
+ onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
+ <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+ <DialogContent className={classes.formContainer}>
+ <Field name="name"
disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Collection Name"
+ component={TextField}
validate={COLLECTION_NAME_VALIDATION}
className={classes.textField}
- label="Collection Name"/>
- <Field name="description"
+ label="Collection Name" />
+ <Field name="description"
disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Description - optional"
+ component={TextField}
validate={COLLECTION_DESCRIPTION_VALIDATION}
className={classes.textField}
- label="Description - optional"/>
- <FileUpload
- files={files}
- disabled={busy}
- onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))}/>
- </DialogContent>
- <DialogActions className={classes.dialogActions}>
- <Button onClick={handleClose} className={classes.button} color="primary"
+ label="Description - optional" />
+ <FileUpload
+ files={files}
+ disabled={busy}
+ onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))} />
+ </DialogContent>
+ <DialogActions className={classes.dialogActions}>
+ <Button onClick={handleClose} className={classes.button} color="primary"
disabled={busy}>CANCEL</Button>
- <Button type="submit"
+ <Button type="submit"
className={classes.lastButton}
color="primary"
disabled={invalid || busy || pristine}
variant="contained">
- CREATE A COLLECTION
+ CREATE A COLLECTION
</Button>
- {busy && <CircularProgress size={20} className={classes.createProgress}/>}
- </DialogActions>
- </form>
- </Dialog>
- );
+ {busy && <CircularProgress size={20} className={classes.createProgress} />}
+ </DialogActions>
+ </form>
+ </Dialog>
+ );
+ }
}
-
- renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
- <TextField
- helperText={touched && error}
- label={label}
- className={this.props.classes.textField}
- error={touched && !!error}
- autoComplete='off'
- {...input}
- {...custom}
- />
- )
- }
-);
+ );
import * as React from 'react';
import { reduxForm, Field } from 'redux-form';
import { compose } from 'redux';
-import TextField from '@material-ui/core/TextField';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogContent from '@material-ui/core/DialogContent';
-import DialogTitle from '@material-ui/core/DialogTitle';
+import { TextField } from '../../components/text-field/text-field';
+import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
pristine: boolean;
}
-interface TextFieldProps {
- label: string;
- floatinglabeltext: string;
- className?: string;
- input?: string;
- meta?: any;
-}
-
export const DialogProjectCreate = compose(
reduxForm({ form: 'projectCreateDialog' }),
withStyles(styles))(
project</DialogTitle>
<DialogContent className={classes.formContainer}>
<Field name="name"
- component={this.renderTextField}
- floatinglabeltext="Project Name"
+ component={TextField}
validate={PROJECT_NAME_VALIDATION}
className={classes.textField}
label="Project Name"/>
<Field name="description"
- component={this.renderTextField}
- floatinglabeltext="Description - optional"
+ component={TextField}
validate={PROJECT_DESCRIPTION_VALIDATION}
className={classes.textField}
label="Description - optional"/>
</Dialog>
);
}
-
- renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
- <TextField
- helperText={touched && error}
- label={label}
- className={this.props.classes.textField}
- error={touched && !!error}
- autoComplete='off'
- {...input}
- {...custom}
- />
- )
}
);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+
+const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onConfirm: () => {
+ // TODO: dispatch action that removes single file
+ dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_REMOVE_DIALOG }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing file...', hideDuration: 2000 }));
+ setTimeout(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File removed.', hideDuration: 2000 }));
+ }, 1000);
+ }
+});
+
+export const openFileRemoveDialog = (fileId: string) =>
+ dialogActions.OPEN_DIALOG({
+ id: FILE_REMOVE_DIALOG,
+ data: {
+ title: 'Removing file',
+ text: 'Are you sure you want to remove this file?',
+ confirmButtonLabel: 'Remove',
+ fileId
+ }
+ });
+
+export const [FileRemoveDialog] = [ConfirmationDialog]
+ .map(withDialog(FILE_REMOVE_DIALOG))
+ .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+
+const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onConfirm: () => {
+ // TODO: dispatch action that removes multiple files
+ dispatch(dialogActions.CLOSE_DIALOG({ id: MULTIPLE_FILES_REMOVE_DIALOG }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Removing files...', hideDuration: 2000}));
+ setTimeout(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Files removed.', hideDuration: 2000}));
+ }, 1000);
+ }
+});
+
+export const openMultipleFilesRemoveDialog = () =>
+ dialogActions.OPEN_DIALOG({
+ id: MULTIPLE_FILES_REMOVE_DIALOG,
+ data: {
+ title: 'Removing files',
+ text: 'Are you sure you want to remove selected files?',
+ confirmButtonLabel: 'Remove'
+ }
+ });
+
+export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
+ .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
+ .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { Typography } from "@material-ui/core";
+import { TreePicker } from "../tree-picker/tree-picker";
+import { TreeProps, TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { ProjectResource } from "../../models/project";
+import { treePickerActions } from "../../store/tree-picker/tree-picker-actions";
+import { ListItemTextIcon } from "../../components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon } from "../../components/icon/icon";
+import { createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { RootState } from "../../store/store";
+import { ServiceRepository } from "../../services/services";
+import { FilterBuilder } from "../../common/api/filter-builder";
+
+type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: {onChange: (projectUuid: string) => void}): ProjectTreePickerProps => ({
+ toggleItemActive: id => {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id }));
+ props.onChange(id);
+ },
+ toggleItemOpen: (id, status) => {
+ status === TreeItemStatus.INITIAL
+ ? dispatch<any>(loadProjectTreePickerProjects(id))
+ : dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+ }
+});
+
+export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
+ <div style={{display: 'flex', flexDirection: 'column'}}>
+ <Typography variant='caption' style={{flexShrink: 0}}>
+ Select a project
+ </Typography>
+ <div style={{flexGrow: 1, overflow: 'auto'}}>
+ <TreePicker {...props} render={renderTreeItem} />
+ </div>
+ </div>);
+
+// TODO: move action creator to store directory
+export const loadProjectTreePickerProjects = (id: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id }));
+
+ const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+
+ const filters = FilterBuilder
+ .create()
+ .addEqual('ownerUuid', ownerUuid);
+
+ const { items } = await services.projectService.list({ filters });
+
+ dispatch<any>(receiveProjectTreePickerData(id, items));
+ };
+
+const renderTreeItem = (item: TreeItem<ProjectResource>) =>
+ <ListItemTextIcon
+ icon={ProjectIcon}
+ name={item.data.name}
+ isActive={item.active}
+ hasMargin={true} />;
+
+// TODO: move action creator to store directory
+const receiveProjectTreePickerData = (id: string, projects: ProjectResource[]) =>
+ (dispatch: Dispatch) => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ id,
+ nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
+ }));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+ };
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography } from "@material-ui/core";
-import { withDialog } from "../../store/dialog/with-dialog";
-import { dialogActions } from "../../store/dialog/dialog-actions";
-
-export const RENAME_DIALOG = 'nameDialog';
-
-export const RenameDialog = withDialog(RENAME_DIALOG)(
- (props) =>
- <Dialog open={props.open}>
- <DialogTitle>{`Rename`}</DialogTitle>
- <DialogContent>
- <Typography variant='body1' gutterBottom>
- {`Please, enter a new name for ${props.data}`}
- </Typography>
- <TextField fullWidth={true} placeholder='New name' />
- </DialogContent>
- <DialogActions>
- <Button
- variant='flat'
- color='primary'
- onClick={props.closeDialog}>
- Cancel
- </Button>
- <Button variant='raised' color='primary'>
- Ok
- </Button>
- </DialogActions>
- </Dialog>
-);
-
-export const openRenameDialog = (originalName: string, ) =>
- dialogActions.OPEN_DIALOG({ id: RENAME_DIALOG, data: originalName });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { reduxForm, reset, startSubmit, stopSubmit } from "redux-form";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { RenameDialog } from "../../components/rename-dialog/rename-dialog";
+
+export const RENAME_FILE_DIALOG = 'renameFileDialog';
+
+export const openRenameFileDialog = (originalName: string, ) =>
+ (dispatch: Dispatch) => {
+ dispatch(reset(RENAME_FILE_DIALOG));
+ dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_FILE_DIALOG, data: originalName }));
+ };
+
+export const [RenameFileDialog] = [RenameDialog]
+ .map(withDialog(RENAME_FILE_DIALOG))
+ .map(reduxForm({
+ form: RENAME_FILE_DIALOG,
+ onSubmit: (data, dispatch) => {
+ dispatch(startSubmit(RENAME_FILE_DIALOG));
+ // TODO: call collection file renaming action here
+ setTimeout(() => dispatch(stopSubmit(RENAME_FILE_DIALOG, { name: 'Invalid name' })), 2000);
+ }
+ }));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Tree, TreeProps, TreeItem } from "../../components/tree/tree";
+import { RootState } from "../../store/store";
+import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { getNodeValue, getNodeChildren } from "../../models/tree";
+
+const memoizedMapStateToProps = () => {
+ let prevState: TTreePicker;
+ let prevTree: Array<TreeItem<any>>;
+
+ return (state: RootState): Pick<TreeProps<any>, 'items'> => {
+ if (prevState !== state.treePicker) {
+ prevState = state.treePicker;
+ prevTree = getNodeChildren('')(state.treePicker)
+ .map(treePickerToTreeItems(state.treePicker));
+ }
+ return {
+ items: prevTree
+ };
+ };
+};
+
+const mapDispatchToProps = (): Pick<TreeProps<any>, 'onContextMenu'> => ({
+ onContextMenu: () => { return; },
+});
+
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
+
+const treePickerToTreeItems = (tree: TTreePicker) =>
+ (id: string): TreeItem<any> => {
+ const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
+ const items = getNodeChildren(node.id)(tree)
+ .map(treePickerToTreeItems(tree));
+ return {
+ active: node.selected,
+ data: node.value,
+ id: node.id,
+ items: items.length > 0 ? items : undefined,
+ open: !node.collapsed,
+ status: node.status
+ };
+ };
+
import { CollectionPanel } from '../collection-panel/collection-panel';
import { loadCollection, loadCollectionTags } from '../../store/collection-panel/collection-panel-action';
import { getCollectionUrl } from '../../models/collection';
-import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
-import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
import { AuthService } from "../../services/auth-service/auth-service";
+import { RenameFileDialog } from '../../views-components/rename-file-dialog/rename-file-dialog';
+import { FileRemoveDialog } from '../../views-components/file-remove-dialog/file-remove-dialog';
+import { MultipleFilesRemoveDialog } from '../../views-components/file-remove-dialog/multiple-files-remove-dialog';
+import { DialogCollectionCreateWithSelectedFile } from '../../views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
<Snackbar />
<CreateProjectDialog />
<CreateCollectionDialog />
- <RemoveDialog />
- <RenameDialog />
+ <RenameFileDialog />
+ <DialogCollectionCreateWithSelectedFile />
+ <FileRemoveDialog />
+ <MultipleFilesRemoveDialog />
<UpdateCollectionDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}