Merge branch 'master' into 14155-edit-process
authorJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 10 Sep 2018 15:51:00 +0000 (17:51 +0200)
committerJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 10 Sep 2018 15:51:00 +0000 (17:51 +0200)
refs #14155

Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki@contractors.roche.com>

15 files changed:
package.json
src/store/collections/collection-copy-actions.ts
src/store/copy-dialog/copy-dialog.ts [new file with mode: 0644]
src/store/processes/process-copy-actions.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/action-sets/process-action-set.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/details-panel/details-panel.tsx
src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
src/views-components/dialog-copy/dialog-copy.tsx [moved from src/views-components/dialog-copy/dialog-collection-copy.tsx with 78% similarity]
src/views-components/dialog-forms/copy-collection-dialog.ts
src/views-components/dialog-forms/copy-process-dialog.ts [new file with mode: 0644]
src/views-components/form-fields/collection-form-fields.tsx
src/views/workbench/workbench.tsx
yarn.lock

index 0e6435ebf536da29d695f224423f56780f4fca40..84d1510f5660226cf8a1f7abac8a1ca20a2c0509 100644 (file)
@@ -21,6 +21,7 @@
     "react-router-dom": "4.3.1",
     "react-router-redux": "5.0.0-alpha.9",
     "react-scripts-ts": "2.17.0",
+    "react-transition-group": "2.4.0",
     "redux": "4.0.0",
     "redux-thunk": "2.3.0",
     "unionize": "2.1.2"
index 87ba0424be5df6bba68f31dbade878b57559077c..09d4e04e7659aa13a54e137ec7b5571feae980ab 100644 (file)
@@ -9,24 +9,19 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
-export interface CollectionCopyFormDialogData {
-    name: string;
-    ownerUuid: string;
-    uuid: string;
-}
-
 export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
-        const initialData: CollectionCopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
+        const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
         dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
     };
 
-export const copyCollection = (resource: CollectionCopyFormDialogData) =>
+export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
         try {
diff --git a/src/store/copy-dialog/copy-dialog.ts b/src/store/copy-dialog/copy-dialog.ts
new file mode 100644 (file)
index 0000000..4450cfc
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface CopyFormDialogData {
+    name: string;
+    uuid: string;
+    ownerUuid: string;
+}
\ No newline at end of file
diff --git a/src/store/processes/process-copy-actions.ts b/src/store/processes/process-copy-actions.ts
new file mode 100644 (file)
index 0000000..bb8d8f5
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { initialize, startSubmit } from 'redux-form';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
+
+export const openCopyProcessDialog = (resource: { name: string, uuid: string }) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const process = getProcess(resource.uuid)(getState().resources);
+        if (process) {
+            const processStatus = getProcessStatus(process);
+            if (processStatus === ProcessStatus.DRAFT) {
+                dispatch<any>(resetPickerProjectTree());
+                const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
+                dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
+                dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can copy only draft processes.', hideDuration: 2000 }));
+            }
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const copyProcess = (resource: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
+        try {
+            const process = await services.containerRequestService.get(resource.uuid);
+            const uuidKey = 'uuid';
+            delete process[uuidKey];
+            await services.containerRequestService.create({ ...process, ownerUuid: resource.ownerUuid, name: resource.name });
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            return process;
+        } catch (e) {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            throw new Error('Could not copy the process.');
+        }
+    };
\ No newline at end of file
index deb07ab33ce01e2785e11e2d23ea62de9e3df8eb..d1b0fae953281b562d7edebeb2c9c5d1bcf8435d 100644 (file)
@@ -31,10 +31,12 @@ import * as collectionMoveActions from '~/store/collections/collection-move-acti
 import * as processesActions from '../processes/processes-actions';
 import * as processMoveActions from '~/store/processes/process-move-actions';
 import * as processUpdateActions from '~/store/processes/process-update-actions';
+import * as processCopyActions from '~/store/processes/process-copy-actions';
 import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
 import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const loadWorkbench = () =>
     async (dispatch: Dispatch, getState: () => RootState) => {
@@ -161,7 +163,7 @@ export const updateCollection = (data: collectionUpdateActions.CollectionUpdateF
         }
     };
 
-export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+export const copyCollection = (data: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
             const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
@@ -224,6 +226,18 @@ export const moveProcess = (data: MoveToFormDialogData) =>
         }
     };
 
+export const copyProcess = (data: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await dispatch<any>(processCopyActions.copyProcess(data));
+            dispatch<any>(updateResources([process]));
+            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
 export const loadProcessLog = (uuid: string) =>
     async (dispatch: Dispatch) => {
         const process = await dispatch<any>(processesActions.loadProcess(uuid));
index 284b9e8e861679a2598539a0a9a0e27e6b8263fd..1a291dfd506d11b9cb358bb5d7ce732542d3dabd 100644 (file)
@@ -13,6 +13,7 @@ import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-acti
 import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
 import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
 import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
 
 export const processActionSet: ContextMenuActionSet = [[
     {
@@ -43,9 +44,7 @@ export const processActionSet: ContextMenuActionSet = [[
     {
         icon: CopyIcon,
         name: "Copy to project",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
     },
     {
         icon: ReRunProcessIcon,
index 9deaff961fde49002070b189b5728c42bfa48238..b1985232e5f5ab5162d409a98a8c4cb5556605eb 100644 (file)
@@ -9,6 +9,7 @@ import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon }
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
 import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
 
 export const processResourceActionSet: ContextMenuActionSet = [[
     {
@@ -39,9 +40,7 @@ export const processResourceActionSet: ContextMenuActionSet = [[
     {
         icon: CopyIcon,
         name: "Copy to project",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
     },
     {
         icon: DetailsIcon,
index c0d4797fa50f68ac89f8dc11b1781b636dfbbdd4..f0075558dfe20549a808a5ebf47840139381dece 100644 (file)
@@ -3,8 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
+import { IconButton, Tabs, Tab, Typography, Grid, Tooltip } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Transition } from 'react-transition-group';
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 import { connect } from 'react-redux';
@@ -20,45 +21,40 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
 
-type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
+type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
-const drawerWidth = 320;
+const DRAWER_WIDTH = 320;
+const SLIDE_TIMEOUT = 500;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        width: 0,
-        overflowX: 'hidden',
-        transition: 'width 0.5s ease',
         background: theme.palette.background.paper,
         borderLeft: `1px solid ${theme.palette.divider}`,
         height: '100%',
+        overflow: 'hidden',
+        transition: `width ${SLIDE_TIMEOUT}ms ease`,
+        width: 0,
     },
     opened: {
-        width: drawerWidth,
+        width: DRAWER_WIDTH,
     },
     container: {
-        width: drawerWidth,
-    },
-    drawerPaper: {
-        position: 'relative',
-        width: drawerWidth
+        maxWidth: 'none',
+        width: DRAWER_WIDTH,
     },
     headerContainer: {
         color: theme.palette.grey["600"],
         margin: `${theme.spacing.unit}px 0`,
-        textAlign: 'center'
+        textAlign: 'center',
     },
     headerIcon: {
-        fontSize: '2.125rem'
-    },
-    headerTitle: {
-        overflowWrap: 'break-word',
-        wordWrap: 'break-word'
+        fontSize: '2.125rem',
     },
     tabContainer: {
-        padding: theme.spacing.unit * 3
-    }
+        overflow: 'auto',
+        padding: theme.spacing.unit * 3,
+    },
 });
 
 const getItem = (resource: DetailsResource): DetailsData => {
@@ -108,50 +104,68 @@ export const DetailsPanel = withStyles(styles)(
                 this.setState({ tabsValue: value });
             }
 
-            renderTabContainer = (children: React.ReactElement<any>) =>
-                <Typography className={this.props.classes.tabContainer} component="div">
-                    {children}
-                </Typography>
-
             render() {
-                const { classes, onCloseDrawer, isOpened, item } = this.props;
-                const { tabsValue } = this.state;
+                const { classes, isOpened } = this.props;
                 return (
-                    <div className={classnames([classes.root, { [classes.opened]: isOpened }])}>
-                        <div className={classes.container}>
-                            <div className={classes.headerContainer}>
-                                <Grid container alignItems='center' justify='space-around'>
-                                    <Grid item xs={2}>
-                                        {item.getIcon(classes.headerIcon)}
-                                    </Grid>
-                                    <Grid item xs={8}>
-                                        <Typography variant="title" className={classes.headerTitle}>
-                                            {item.getTitle()}
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item>
-                                        <IconButton color="inherit" onClick={onCloseDrawer}>
-                                            {<CloseIcon />}
-                                        </IconButton>
-                                    </Grid>
-                                </Grid>
-                            </div>
-                            <Tabs value={tabsValue} onChange={this.handleChange}>
-                                <Tab disableRipple label="Details" />
-                                <Tab disableRipple label="Activity" disabled />
-                            </Tabs>
-                            {tabsValue === 0 && this.renderTabContainer(
-                                <Grid container direction="column">
-                                    {item.getDetails()}
-                                </Grid>
-                            )}
-                            {tabsValue === 1 && this.renderTabContainer(
-                                <Grid container direction="column" />
-                            )}
-                        </div>
-                    </div>
+                    <Grid
+                        container
+                        direction="column"
+                        className={classnames([classes.root, { [classes.opened]: isOpened }])}>
+                        <Transition
+                            in={isOpened}
+                            timeout={SLIDE_TIMEOUT}
+                            unmountOnExit>
+                            {this.renderContent()}
+                        </Transition>
+                    </Grid>
                 );
             }
+
+            renderContent() {
+                const { classes, onCloseDrawer, item } = this.props;
+                const { tabsValue } = this.state;
+                return <Grid
+                    container
+                    direction="column"
+                    item
+                    xs
+                    className={classes.container} >
+                    <Grid
+                        item
+                        className={classes.headerContainer}
+                        container
+                        alignItems='center'
+                        justify='space-around'
+                        wrap="nowrap">
+                        <Grid item xs={2}>
+                            {item.getIcon(classes.headerIcon)}
+                        </Grid>
+                        <Grid item xs={8}>
+                            <Tooltip title={item.getTitle()}>
+                                <Typography variant="title" noWrap>
+                                    {item.getTitle()}
+                                </Typography>
+                            </Tooltip>
+                        </Grid>
+                        <Grid item>
+                            <IconButton color="inherit" onClick={onCloseDrawer}>
+                                <CloseIcon />
+                            </IconButton>
+                        </Grid>
+                    </Grid>
+                    <Grid item>
+                        <Tabs value={tabsValue} onChange={this.handleChange}>
+                            <Tab disableRipple label="Details" />
+                            <Tab disableRipple label="Activity" disabled />
+                        </Tabs>
+                    </Grid>
+                    <Grid item xs className={this.props.classes.tabContainer} >
+                        {tabsValue === 0
+                            ? item.getDetails()
+                            : null}
+                    </Grid>
+                </Grid >;
+            }
         }
     )
 );
index 7fc301fa6c1d44a92e4b38385ddde7683a2920a1..7c335a358c9048cff8af1b136143252a009aad3b 100644 (file)
@@ -19,10 +19,8 @@ export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyPr
         {...props}
     />;
 
-export const CollectionPartialCopyFields = () => <div style={{ display: 'flex' }}>
-    <div>
-        <CollectionNameField />
-        <CollectionDescriptionField />
-    </div>
+export const CollectionPartialCopyFields = () => <div>
+    <CollectionNameField />
+    <CollectionDescriptionField />
     <CollectionProjectPickerField />
 </div>;
similarity index 78%
rename from src/views-components/dialog-copy/dialog-collection-copy.tsx
rename to src/views-components/dialog-copy/dialog-copy.tsx
index 029db578520e197ed37e1f86511e8d20ae69dbbe..415541595c564ff1d3b672062852af92aaf25461 100644 (file)
@@ -9,19 +9,19 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
 import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
 import { TextField } from "~/components/text-field/text-field";
-import { CollectionCopyFormDialogData } from "~/store/collections/collection-copy-actions";
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
-type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CollectionCopyFormDialogData>;
+type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
 
-export const DialogCollectionCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps) =>
     <FormDialog
         dialogTitle='Make a copy'
-        formFields={CollectionCopyFields}
+        formFields={CopyDialogFields}
         submitLabel='Copy'
         {...props}
     />;
 
-const CollectionCopyFields = () => <span>
+const CopyDialogFields = () => <span>
     <Field
         name='name'
         component={TextField}
index 245465fa55edfc2bcb32fab615122ec42c62a90f..41309fdff6952762ed9ae9d9c2922f53dc3e5082 100644 (file)
@@ -5,16 +5,17 @@
 import { compose } from "redux";
 import { withDialog } from "~/store/dialog/with-dialog";
 import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
-import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { COLLECTION_COPY_FORM_NAME } from '~/store/collections/collection-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
 import { copyCollection } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const CopyCollectionDialog = compose(
     withDialog(COLLECTION_COPY_FORM_NAME),
-    reduxForm<CollectionCopyFormDialogData>({
+    reduxForm<CopyFormDialogData>({
         form: COLLECTION_COPY_FORM_NAME,
         onSubmit: (data, dispatch) => {
             dispatch(copyCollection(data));
         }
     })
-)(DialogCollectionCopy);
\ No newline at end of file
+)(DialogCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/copy-process-dialog.ts b/src/views-components/dialog-forms/copy-process-dialog.ts
new file mode 100644 (file)
index 0000000..4ec17c6
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_COPY_FORM_NAME } from '~/store/processes/process-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
+import { copyProcess } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+
+export const CopyProcessDialog = compose(
+    withDialog(PROCESS_COPY_FORM_NAME),
+    reduxForm<CopyFormDialogData>({
+        form: PROCESS_COPY_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(copyProcess(data));
+        }
+    })
+)(DialogCopy);
\ No newline at end of file
index af240fc5666c1735bf4938ade1b0bffe3ad358fe..ddd5bceff37d8aabdc60a631f6367d7216bc0abf 100644 (file)
@@ -29,6 +29,6 @@ export const CollectionProjectPickerField = () =>
         validate={COLLECTION_PROJECT_VALIDATION} />;
 
 const ProjectPicker = (props: WrappedFieldProps) =>
-    <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+    <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
         <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
     </div>;
index aa7cfaba7016e9831151ffe3870b0b8ff89eab14..56fabeba0cadcf2b92733707007d8944023aa31c 100644 (file)
@@ -30,6 +30,7 @@ import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
 import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
 import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
 import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { CopyProcessDialog } from '~/views-components/dialog-forms/copy-process-dialog';
 import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
 import { UpdateProcessDialog } from '~/views-components/dialog-forms/update-process-dialog';
 import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
@@ -150,6 +151,7 @@ export const Workbench = withStyles(styles)(
                     <PartialCopyCollectionDialog />
                     <FileRemoveDialog />
                     <CopyCollectionDialog />
+                    <CopyProcessDialog />
                     <FileRemoveDialog />
                     <MultipleFilesRemoveDialog />
                     <UpdateCollectionDialog />
index 67c12647b4625337eae116a1f6feea7f9322da8e..359927100d4493eca58c01ca52ccc458a55361eb 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -6307,7 +6307,7 @@ react-test-renderer@^16.0.0-0:
     prop-types "^15.6.0"
     react-is "^16.4.1"
 
-react-transition-group@^2.2.1:
+react-transition-group@2.4.0, react-transition-group@^2.2.1:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
   dependencies: