From: Janicki Artur Date: Thu, 2 Aug 2018 09:20:22 +0000 (+0200) Subject: init collection edit dialog, add reducers, modify store, refactor code X-Git-Tag: 1.2.0~17^2~5 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/4b065bb111ccdfdde1756e6507b84b72ce67c511 init collection edit dialog, add reducers, modify store, refactor code Feature #13903 Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- diff --git a/src/common/custom-theme.ts b/src/common/custom-theme.ts index e5d2e5e7..ecad3913 100644 --- a/src/common/custom-theme.ts +++ b/src/common/custom-theme.ts @@ -96,6 +96,23 @@ const themeOptions: ArvadosThemeOptions = { 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: { diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts index 3c660165..946836a5 100644 --- a/src/store/collection-panel/collection-panel-action.ts +++ b/src/store/collection-panel/collection-panel-action.ts @@ -3,15 +3,14 @@ // 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; @@ -19,7 +18,7 @@ export type CollectionPanelAction = UnionOf; 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 })); diff --git a/src/store/collections/collections-reducer.ts b/src/store/collections/collections-reducer.ts new file mode 100644 index 00000000..966cf29d --- /dev/null +++ b/src/store/collections/collections-reducer.ts @@ -0,0 +1,17 @@ +// 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 diff --git a/src/store/collections/creator/collection-creator-action.ts b/src/store/collections/creator/collection-creator-action.ts index b30f8b80..2a977e33 100644 --- a/src/store/collections/creator/collection-creator-action.ts +++ b/src/store/collections/creator/collection-creator-action.ts @@ -21,7 +21,7 @@ export const collectionCreateActions = unionize({ export const createCollection = (collection: Partial) => (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 diff --git a/src/store/collections/creator/collection-creator-reducer.test.ts b/src/store/collections/creator/collection-creator-reducer.test.ts index 0da18c81..fde58c43 100644 --- a/src/store/collections/creator/collection-creator-reducer.test.ts +++ b/src/store/collections/creator/collection-creator-reducer.test.ts @@ -8,36 +8,24 @@ import { collectionCreateActions } from "./collection-creator-action"; 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); diff --git a/src/store/collections/creator/collection-creator-reducer.ts b/src/store/collections/creator/collection-creator-reducer.ts index 769766e1..1a3cb0d2 100644 --- a/src/store/collections/creator/collection-creator-reducer.ts +++ b/src/store/collections/creator/collection-creator-reducer.ts @@ -4,9 +4,7 @@ import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action'; -export type CollectionCreatorState = { - creator: CollectionCreator -}; +export type CollectionCreatorState = CollectionCreator; interface CollectionCreator { opened: boolean; @@ -15,17 +13,12 @@ interface CollectionCreator { const updateCreator = (state: CollectionCreatorState, creator?: Partial) => ({ ...state, - creator: { - ...state.creator, - ...creator - } + ...creator }); const initialState: CollectionCreatorState = { - creator: { - opened: false, - ownerUuid: "" - } + opened: false, + ownerUuid: '' }; export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => { diff --git a/src/store/collections/updator/collection-updator-action.ts b/src/store/collections/updator/collection-updator-action.ts new file mode 100644 index 00000000..5272baa8 --- /dev/null +++ b/src/store/collections/updator/collection-updator-action.ts @@ -0,0 +1,33 @@ +// 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) => + (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; \ No newline at end of file diff --git a/src/store/collections/updator/collection-updator-reducer.ts b/src/store/collections/updator/collection-updator-reducer.ts new file mode 100644 index 00000000..f47f1960 --- /dev/null +++ b/src/store/collections/updator/collection-updator-reducer.ts @@ -0,0 +1,32 @@ +// 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) => ({ + ...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 + }); +}; diff --git a/src/store/store.ts b/src/store/store.ts index 5c928fca..826a25d0 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -18,8 +18,8 @@ import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middlew 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' && @@ -29,7 +29,7 @@ const composeEnhancers = export interface RootState { auth: AuthState; projects: ProjectState; - collectionCreation: CollectionCreatorState; + collections: CollectionsState; router: RouterState; dataExplorer: DataExplorerState; sidePanel: SidePanelState; @@ -43,7 +43,7 @@ export interface RootState { const rootReducer = combineReducers({ auth: authReducer, projects: projectsReducer, - collectionCreation: collectionCreationReducer, + collections: collectionsReducer, router: routerReducer, dataExplorer: dataExplorerReducer, sidePanel: sidePanelReducer, diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts index 0822b781..7f9cbc13 100644 --- a/src/views-components/context-menu/action-sets/collection-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-action-set.ts @@ -8,13 +8,14 @@ import { toggleFavorite } from "../../../store/favorites/favorites-actions"; 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 })); } }, { diff --git a/src/views-components/create-collection-dialog/create-collection-dialog.tsx b/src/views-components/create-collection-dialog/create-collection-dialog.tsx index e4474a56..0ba2b22a 100644 --- a/src/views-components/create-collection-dialog/create-collection-dialog.tsx +++ b/src/views-components/create-collection-dialog/create-collection-dialog.tsx @@ -14,7 +14,7 @@ import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel"; 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) => ({ @@ -33,7 +33,7 @@ const addCollection = (data: { name: string, description: string }) => (dispatch: Dispatch) => { return dispatch(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 })); diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx index d0f793bf..3e3b74aa 100644 --- a/src/views-components/dialog-create/dialog-collection-create.tsx +++ b/src/views-components/dialog-create/dialog-collection-create.tsx @@ -14,7 +14,7 @@ import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } 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 = theme => ({ button: { @@ -27,17 +27,9 @@ const styles: StyleRulesCallback = theme => ({ 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", @@ -45,7 +37,7 @@ const styles: StyleRulesCallback = theme => ({ right: "110px" }, dialogActions: { - marginBottom: "24px" + marginBottom: theme.spacing.unit * 3 } }); interface DialogCollectionCreateProps { @@ -77,39 +69,41 @@ export const DialogCollectionCreate = compose( -
-
onSubmit(data))}> - Create a collection - - - - - - - - {submitting && } - -
-
+
onSubmit(data))}> + Create a collection + + + + + + + + {submitting && } + +
); } diff --git a/src/views-components/dialog-update/dialog-collection-update.tsx b/src/views-components/dialog-update/dialog-collection-update.tsx new file mode 100644 index 00000000..30a3256d --- /dev/null +++ b/src/views-components/dialog-update/dialog-collection-update.tsx @@ -0,0 +1,130 @@ +// 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 = (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; + +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 { + + render() { + const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props; + return ( + + +
onSubmit(data))}> + Edit Collection + + + + + + +
+ + {submitting && } +
+
+
+
+ ); + } + + renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => ( + + ) + } + ); \ No newline at end of file diff --git a/src/views-components/update-collection-dialog/update-collection-dialog..tsx b/src/views-components/update-collection-dialog/update-collection-dialog..tsx new file mode 100644 index 00000000..8daff014 --- /dev/null +++ b/src/views-components/update-collection-dialog/update-collection-dialog..tsx @@ -0,0 +1,42 @@ +// 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(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(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 diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index b55e50c0..71ec309f 100644 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@ -5,7 +5,7 @@ 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'; @@ -15,7 +15,7 @@ import { MoreOptionsIcon, CollectionIcon } from '../../components/icon/icon'; 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 = (theme: ArvadosTheme) => ({ card: { @@ -24,6 +24,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ iconHeader: { fontSize: '1.875rem', color: theme.customs.colors.yellow700 + }, + tag: { + marginRight: theme.spacing.unit } }); @@ -60,8 +63,10 @@ export const CollectionPanel = withStyles(styles)( - - + + Here I will add copy + + @@ -73,7 +78,9 @@ export const CollectionPanel = withStyles(styles)( - Tags + + + diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 160e12f8..9cf02643 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -41,6 +41,7 @@ import { CreateCollectionDialog } from '../../views-components/create-collection 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; @@ -226,6 +227,7 @@ export const Workbench = withStyles(styles)( +