root: {
padding: '8px 16px'
}
+ },
+ MuiInput: {
+ underline: {
+ '&:after': {
+ borderBottomColor: purple800
+ },
+ '&:hover:not($disabled):not($focused):not($error):before': {
+ borderBottom: '1px solid inherit'
+ }
+ }
+ },
+ MuiFormLabel: {
+ focused: {
+ "&$focused:not($error)": {
+ color: purple800
+ }
+ }
}
},
mixins: {
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
-import { apiClient } from "../../common/api/server-api";
import { Dispatch } from "redux";
import { ResourceKind } from "../../models/resource";
import { CollectionResource } from "../../models/collection";
+import { collectionService } from "../../services/services";
export const collectionPanelActions = unionize({
LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
- LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
}, { tag: 'type', value: 'payload' });
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
export const loadCollection = (uuid: string, kind: ResourceKind) =>
(dispatch: Dispatch) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
- return new CommonResourceService(apiClient, "collections")
+ return collectionService
.get(uuid)
.then(item => {
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: item as CollectionResource }));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { combineReducers } from 'redux';
+import * as creator from "./creator/collection-creator-reducer";
+import * as updator from "./updator/collection-updator-reducer";
+
+export type CollectionsState = {
+ creator: creator.CollectionCreatorState;
+ updator: updator.CollectionUpdatorState;
+};
+
+export const collectionsReducer = combineReducers({
+ creator: creator.collectionCreationReducer,
+ updator: updator.collectionCreationReducer
+});
\ No newline at end of file
export const createCollection = (collection: Partial<CollectionResource>) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const { ownerUuid } = getState().collectionCreation.creator;
+ const { ownerUuid } = getState().collections.creator;
const collectiontData = { ownerUuid, ...collection };
dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
return collectionService
describe('collection-reducer', () => {
it('should open collection creator dialog', () => {
- const initialState = {
- creator: { opened: false, ownerUuid: "" }
- };
- const collection = {
- creator: { opened: true, ownerUuid: "" },
- };
-
- const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState.creator));
+ const initialState = { opened: false, ownerUuid: "" };
+ const collection = { opened: true, ownerUuid: "" };
+
+ const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
expect(state).toEqual(collection);
});
it('should close collection creator dialog', () => {
- const initialState = {
- creator: { opened: true, ownerUuid: "" }
- };
- const collection = {
- creator: { opened: false, ownerUuid: "" },
- };
+ const initialState = { opened: true, ownerUuid: "" };
+ const collection = { opened: false, ownerUuid: "" };
const state = collectionCreationReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
expect(state).toEqual(collection);
});
it('should reset collection creator dialog props', () => {
- const initialState = {
- creator: { opened: true, ownerUuid: "test" }
- };
- const collection = {
- creator: { opened: false, ownerUuid: "" },
- };
+ const initialState = { opened: true, ownerUuid: "test" };
+ const collection = { opened: false, ownerUuid: "" };
const state = collectionCreationReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
expect(state).toEqual(collection);
import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
-export type CollectionCreatorState = {
- creator: CollectionCreator
-};
+export type CollectionCreatorState = CollectionCreator;
interface CollectionCreator {
opened: boolean;
const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreator>) => ({
...state,
- creator: {
- ...state.creator,
- ...creator
- }
+ ...creator
});
const initialState: CollectionCreatorState = {
- creator: {
- opened: false,
- ownerUuid: ""
- }
+ opened: false,
+ ownerUuid: ''
};
export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
--- /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 { Dispatch } from "redux";
+
+import { RootState } from "../../store";
+import { collectionService } from '../../../services/services';
+import { CollectionResource } from '../../../models/collection';
+
+export const collectionUpdatorActions = unionize({
+ OPEN_COLLECTION_UPDATOR: ofType<{ ownerUuid: string }>(),
+ CLOSE_COLLECTION_UPDATOR: ofType<{}>(),
+ UPDATE_COLLECTION: ofType<{}>(),
+ UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+export const updateCollection = (collection: Partial<CollectionResource>) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { ownerUuid } = getState().collections.creator;
+ const collectiontData = { ownerUuid, ...collection };
+ dispatch(collectionUpdatorActions.UPDATE_COLLECTION(collectiontData));
+ return collectionService
+ // change for update
+ .create(collectiontData)
+ .then(collection => dispatch(collectionUpdatorActions.UPDATE_COLLECTION_SUCCESS(collection)));
+ };
+
+export type CollectionUpdatorAction = UnionOf<typeof collectionUpdatorActions>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionUpdatorActions, CollectionUpdatorAction } from './collection-updator-action';
+
+export type CollectionUpdatorState = CollectionUpdator;
+
+interface CollectionUpdator {
+ opened: boolean;
+ ownerUuid: string;
+}
+
+const updateCollection = (state: CollectionUpdatorState, updator?: Partial<CollectionUpdator>) => ({
+ ...state,
+ ...updator
+});
+
+const initialState: CollectionUpdatorState = {
+ opened: false,
+ ownerUuid: ''
+};
+
+export const collectionCreationReducer = (state: CollectionUpdatorState = initialState, action: CollectionUpdatorAction) => {
+ return collectionUpdatorActions.match(action, {
+ OPEN_COLLECTION_UPDATOR: ({ ownerUuid }) => updateCollection(state, { ownerUuid, opened: true }),
+ CLOSE_COLLECTION_UPDATOR: () => updateCollection(state, { opened: false }),
+ UPDATE_COLLECTION: () => updateCollection(state),
+ UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, ownerUuid: "" }),
+ default: () => state
+ });
+};
import { reducer as formReducer } from 'redux-form';
import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
-import { CollectionCreatorState, collectionCreationReducer } from './collections/creator/collection-creator-reducer';
import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
export interface RootState {
auth: AuthState;
projects: ProjectState;
- collectionCreation: CollectionCreatorState;
+ collections: CollectionsState;
router: RouterState;
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
const rootReducer = combineReducers({
auth: authReducer,
projects: projectsReducer,
- collectionCreation: collectionCreationReducer,
+ collections: collectionsReducer,
router: routerReducer,
dataExplorer: dataExplorerReducer,
sidePanel: sidePanelReducer,
import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
+import { collectionUpdatorActions } from "../../../store/collections/updator/collection-updator-action";
export const collectionActionSet: ContextMenuActionSet = [[
{
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- // add code
+ dispatch(collectionUpdatorActions.OPEN_COLLECTION_UPDATOR({ ownerUuid: resource.uuid }));
}
},
{
import { snackbarActions } from "../../store/snackbar/snackbar-actions";
const mapStateToProps = (state: RootState) => ({
- open: state.collectionCreation.creator.opened
+ open: state.collections.creator.opened
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
(dispatch: Dispatch) => {
return dispatch<any>(createCollection(data)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Created a new collection",
+ message: "Collection has been successfully created.",
hideDuration: 2000
}));
dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
const styles: StyleRulesCallback<CssRules> = theme => ({
button: {
formContainer: {
display: "flex",
flexDirection: "column",
- marginTop: "20px",
- },
- dialogTitle: {
- paddingBottom: "0"
},
textField: {
- marginTop: "32px",
- },
- dialog: {
- minWidth: "600px",
- minHeight: "320px"
+ marginBottom: theme.spacing.unit * 3
},
createProgress: {
position: "absolute",
right: "110px"
},
dialogActions: {
- marginBottom: "24px"
+ marginBottom: theme.spacing.unit * 3
}
});
interface DialogCollectionCreateProps {
<Dialog
open={open}
onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
disableBackdropClick={true}
disableEscapeKeyDown={true}>
- <div className={classes.dialog}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
- <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a collection</DialogTitle>
- <DialogContent className={classes.formContainer}>
- <Field name="name"
- component={this.renderTextField}
- floatinglabeltext="Collection Name"
- validate={COLLECTION_NAME_VALIDATION}
- className={classes.textField}
- label="Collection Name"/>
- <Field name="description"
- component={this.renderTextField}
- floatinglabeltext="Description - optional"
- validate={COLLECTION_DESCRIPTION_VALIDATION}
- className={classes.textField}
- label="Description - optional"/>
- </DialogContent>
- <DialogActions className={classes.dialogActions}>
- <Button onClick={handleClose} className={classes.button} color="primary"
- disabled={submitting}>CANCEL</Button>
- <Button type="submit"
- className={classes.lastButton}
- color="primary"
- disabled={invalid || submitting || pristine}
- variant="contained">
- CREATE A COLLECTION
- </Button>
- {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
- </DialogActions>
- </form>
- </div>
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+ <DialogContent className={classes.formContainer}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name"/>
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional"/>
+ </DialogContent>
+ <DialogActions className={classes.dialogActions}>
+ <Button onClick={handleClose} className={classes.button} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <Button type="submit"
+ className={classes.lastButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ CREATE A COLLECTION
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+ </DialogActions>
+ </form>
</Dialog>
);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+
+type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ content: {
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ actions: {
+ margin: 0,
+ padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
+ ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
+ },
+ textField: {
+ marginBottom: theme.spacing.unit * 3
+ },
+ buttonWrapper: {
+ position: 'relative'
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface DialogCollectionDataProps {
+ open: boolean;
+ handleSubmit: any;
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface DialogCollectionAction {
+ handleClose: () => void;
+ onSubmit: (data: { name: string, description: string }) => void;
+}
+
+type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+export const DialogCollectionUpdate = compose(
+ reduxForm({ form: 'collectionEditDialog' }),
+ withStyles(styles))(
+
+ class DialogCollectionUpdate extends React.Component<DialogCollectionProps> {
+
+ render() {
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
+ return (
+ <Dialog open={open}
+ onClose={handleClose}
+ fullWidth={true}
+ maxWidth='sm'
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}>
+
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <DialogTitle>Edit Collection</DialogTitle>
+ <DialogContent className={classes.content}>
+ <Field name="name"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Collection Name"
+ validate={COLLECTION_NAME_VALIDATION}
+ className={classes.textField}
+ label="Collection Name" />
+ <Field name="description"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Description - optional"
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ className={classes.textField}
+ label="Description - optional" />
+ </DialogContent>
+ <DialogActions className={classes.actions}>
+ <Button onClick={handleClose} color="primary"
+ disabled={submitting}>CANCEL</Button>
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ SAVE
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </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}
+ />
+ )
+ }
+ );
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+import { RootState } from "../../store/store";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { collectionUpdatorActions, updateCollection } from "../../store/collections/updator/collection-updator-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
+
+const mapStateToProps = (state: RootState) => ({
+ open: state.collections.updator.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ handleClose: () => {
+ dispatch(collectionUpdatorActions.CLOSE_COLLECTION_UPDATOR());
+ },
+ onSubmit: (data: { name: string, description: string }) => {
+ return dispatch<any>(editCollection(data))
+ .catch((e: any) => {
+ throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
+ });
+ }
+});
+
+const editCollection = (data: { name: string, description: string }) =>
+ (dispatch: Dispatch) => {
+ return dispatch<any>(updateCollection(data)).then(() => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ });
+ };
+
+export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
\ No newline at end of file
import * as React from 'react';
import {
StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid
+ CardHeader, IconButton, CardContent, Grid, Chip
} from '@material-ui/core';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
import { CollectionResource } from '../../models/collection';
-type CssRules = 'card' | 'iconHeader';
+type CssRules = 'card' | 'iconHeader' | 'tag';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
iconHeader: {
fontSize: '1.875rem',
color: theme.customs.colors.yellow700
+ },
+ tag: {
+ marginRight: theme.spacing.unit
}
});
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
- <DetailsAttribute label='Collection UUID' value={item && item.uuid} />
- <DetailsAttribute label='Content size' value='54 MB' />
+ <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+ Here I will add copy
+ </DetailsAttribute>
+ <DetailsAttribute label='Content size' value='54 MB' />
<DetailsAttribute label='Owner' value={item && item.ownerUuid} />
</Grid>
</Grid>
<CardContent>
<Grid container direction="column">
<Grid item xs={4}>
- Tags
+ <Chip label="Tag 1" className={classes.tag}/>
+ <Chip label="Tag 2" className={classes.tag}/>
+ <Chip label="Tag 3" className={classes.tag}/>
</Grid>
</Grid>
</CardContent>
import { CollectionPanel } from '../collection-panel/collection-panel';
import { loadCollection } from '../../store/collection-panel/collection-panel-action';
import { getCollectionUrl } from '../../models/collection';
+import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
const drawerWidth = 240;
const appBarHeight = 100;
<Snackbar />
<CreateProjectDialog />
<CreateCollectionDialog />
+ <UpdateCollectionDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}