16439: Merge branch 'master' into 16439-objects-creation-placement-fix
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 2 Jun 2020 19:14:27 +0000 (16:14 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 2 Jun 2020 19:14:27 +0000 (16:14 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

src/components/form-dialog/form-dialog.tsx
src/components/text-field/text-field.tsx
src/store/collections/collection-create-actions.ts
src/store/projects/project-create-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/form-fields/resource-form-fields.tsx [new file with mode: 0644]
src/views-components/side-panel-button/side-panel-button.tsx

index e95693df188309706edf3accbd09809faf760a98..3df874b7a3921e473a7c12b6d9ffe8f06bf9bc1e 100644 (file)
@@ -16,22 +16,24 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
     lastButton: {
         marginLeft: theme.spacing.unit,
-        marginRight: "20px",
+        marginRight: "0",
     },
     formContainer: {
         display: "flex",
         flexDirection: "column",
-        marginTop: "20px",
+        paddingBottom: "0",
     },
     dialogTitle: {
-        paddingBottom: "0"
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
     },
     progressIndicator: {
         position: "absolute",
         minWidth: "20px",
     },
     dialogActions: {
-        marginBottom: theme.spacing.unit * 3
+        marginBottom: theme.spacing.unit,
+        marginRight: theme.spacing.unit * 3,
     }
 });
 
index 82d640d8ff9ff912d1623cd3a026505dc41dab68..1cf9a81d28b497817bf3720f6f53550dea539872 100644 (file)
@@ -19,7 +19,7 @@ type CssRules = 'textField' | 'rte';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     textField: {
-        marginBottom: theme.spacing.unit * 3
+        marginBottom: theme.spacing.unit
     },
     rte: {
         fontFamily: 'Arial',
index 39565f1d43b095c22470926e4f3d0f4d98f6c151..b6f0ddccf61dfebb61e140728becf1cf4e1d59ab 100644 (file)
@@ -12,7 +12,7 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { uploadCollectionFiles } from './collection-upload-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
+import { isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { CollectionResource } from "~/models/collection";
 
@@ -26,12 +26,11 @@ export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName";
 
 export const openCollectionCreateDialog = (ownerUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const router = getState();
-        const properties = getState().properties;
-        if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
+        const { router } = getState();
+        if (!isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
-            dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { userUuid }));
+            dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid: userUuid }));
         } else {
             dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid }));
         }
index a303b5518dc7a5b43ceb28cc7028c89764dea4c7..583a4bd6978237bfc695fd8fe8f1cdae93bd41a1 100644 (file)
@@ -12,6 +12,7 @@ import { ProjectResource } from '~/models/project';
 import { ServiceRepository } from '~/services/services';
 import { matchProjectRoute, matchRunProcessRoute } from '~/routes/routes';
 import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
+import { RouterState } from "react-router-redux";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -28,29 +29,20 @@ export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
 export const PROJECT_CREATE_PROPERTIES_FORM_NAME = 'projectCreatePropertiesFormName';
 export const PROJECT_CREATE_FORM_SELECTOR = formValueSelector(PROJECT_CREATE_FORM_NAME);
 
-export const isProjectOrRunProcessRoute = ({ router }: RootState) => {
+export const isProjectOrRunProcessRoute = (router: RouterState) => {
     const pathname = router.location ? router.location.pathname : '';
     const matchProject = matchProjectRoute(pathname);
     const matchRunProcess = matchRunProcessRoute(pathname);
     return Boolean(matchProject || matchRunProcess);
 };
 
-export const isItemNotInProject = (properties: any) => {
-    if (properties.breadcrumbs) {
-        return Boolean(properties.breadcrumbs[0].label !== 'Projects');
-    } else {
-        return;
-    }
-};
-
 export const openProjectCreateDialog = (ownerUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const router = getState();
-        const properties = getState().properties;
-        if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
+        const { router } = getState();
+        if (!isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
-            dispatch(initialize(PROJECT_CREATE_FORM_NAME, { userUuid }));
+            dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid: userUuid }));
         } else {
             dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid }));
         }
index c69325583957f8ad4f76c0eb20aee4656ccc74dd..e2ff01f7afa71c6df8e6434ce03d08a830f5c0fb 100644 (file)
@@ -237,7 +237,7 @@ export const createProject = (data: projectCreateActions.ProjectCreateFormDialog
                 kind: SnackbarKind.SUCCESS
             }));
             await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
-            dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
+            dispatch<any>(navigateTo(newProject.uuid));
         }
     };
 
@@ -315,7 +315,7 @@ export const createCollection = (data: collectionCreateActions.CollectionCreateF
                 kind: SnackbarKind.SUCCESS
             }));
             dispatch<any>(updateResources([collection]));
-            dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+            dispatch<any>(navigateTo(collection.uuid));
         }
     };
 
index 690cf8e52e09d648a9ce4fd884065ddb92702299..a70030c7d29d8115a7d4c8bdfd7cfd347e556b42 100644 (file)
@@ -8,9 +8,8 @@ import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
-import { require } from '~/validators/require';
 import { FileUploaderField } from '../file-uploader/file-uploader';
-
+import { ResourceParentField } from '../form-fields/resource-form-fields';
 
 type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
@@ -23,11 +22,11 @@ export const DialogCollectionCreate = (props: DialogCollectionProps) =>
     />;
 
 const CollectionAddFields = () => <span>
+    <ResourceParentField />
     <CollectionNameField />
     <CollectionDescriptionField />
     <Field
         name='files'
-        validate={[require]}
         label='Files'
         component={FileUploaderField} />
 </span>;
index 02fb67e5f202c8b70b96c1bb59f76587998edf40..c835e04e572b071479bb99186dfee7ca61f7ac46 100644 (file)
@@ -10,6 +10,7 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
 import { CreateProjectPropertiesForm } from '~/views-components/project-properties/create-project-properties-form';
 import { CreateProjectPropertiesList } from '~/views-components/project-properties/create-project-properties-list';
+import { ResourceParentField } from '../form-fields/resource-form-fields';
 
 type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
 
@@ -22,6 +23,7 @@ export const DialogProjectCreate = (props: DialogProjectProps) =>
     />;
 
 const ProjectAddFields = () => <span>
+    <ResourceParentField />
     <ProjectNameField />
     <ProjectDescriptionField />
     <CreateProjectPropertiesForm />
diff --git a/src/views-components/form-fields/resource-form-fields.tsx b/src/views-components/form-fields/resource-form-fields.tsx
new file mode 100644 (file)
index 0000000..0c4ae64
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
+import { Field } from "redux-form";
+import { ResourcesState, getResource } from "~/store/resources/resources";
+import { GroupResource } from "~/models/group";
+import { TextField } from "~/components/text-field/text-field";
+import { getUserUuid } from "~/common/getuser";
+
+interface ResourceParentFieldProps {
+    resources: ResourcesState;
+    userUuid: string|undefined;
+}
+
+export const ResourceParentField = connect(
+    (state: RootState) => {
+        return {
+            resources: state.resources,
+            userUuid: getUserUuid(state),
+        };
+    })
+    ((props: ResourceParentFieldProps) =>
+        <Field
+            name='ownerUuid'
+            disabled={true}
+            label='Parent project'
+            format={
+                (value, name) => {
+                    if (value === props.userUuid) {
+                        return 'Home project';
+                    }
+                    const rsc = getResource<GroupResource>(value)(props.resources);
+                    if (rsc !== undefined) {
+                        return `${rsc.name} (${rsc.uuid})`;
+                    }
+                    return value;
+                }
+            }
+            component={TextField} />
+    );
index 0f79759078d1ca2009a69643c2a7ab7681d467ca..5e547740a1231b25a52fa24207716f40f5cb0acd 100644 (file)
@@ -5,8 +5,6 @@
 import * as React from 'react';
 import { connect, DispatchProp } from 'react-redux';
 import { RootState } from '~/store/store';
-import { getProperty } from '~/store/properties/properties';
-import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { PopoverOrigin } from '@material-ui/core/Popover';
 import { StyleRulesCallback, WithStyles, withStyles, Toolbar, Grid, Button, MenuItem, Menu } from '@material-ui/core';
@@ -15,6 +13,11 @@ import { openProjectCreateDialog } from '~/store/projects/project-create-actions
 import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
 import { navigateToRunProcess } from '~/store/navigation/navigation-action';
 import { runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
+import { getUserUuid } from '~/common/getuser';
+import { matchProjectRoute } from '~/routes/routes';
+import { GroupResource } from '~/models/group';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
 
 type CssRules = 'button' | 'menuItem' | 'icon';
 
@@ -36,6 +39,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 interface SidePanelDataProps {
     location: any;
     currentItemId: string;
+    resources: ResourcesState;
+    currentUserUUID: string | undefined;
 }
 
 interface SidePanelState {
@@ -49,10 +54,21 @@ const transformOrigin: PopoverOrigin = {
     horizontal: 0
 };
 
+const isProjectTrashed = (proj: GroupResource, resources: ResourcesState): boolean => {
+    if (proj.isTrashed) { return true; }
+    if (extractUuidKind(proj.ownerUuid) === ResourceKind.USER) { return false; }
+    const parentProj = getResource<GroupResource>(proj.ownerUuid)(resources);
+    return isProjectTrashed(parentProj!, resources);
+};
+
 export const SidePanelButton = withStyles(styles)(
     connect((state: RootState) => ({
-        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
-        location: state.router.location
+        currentItemId: state.router.location
+            ? state.router.location.pathname.split('/').slice(-1)[0]
+            : null,
+        location: state.router.location,
+        resources: state.resources,
+        currentUserUUID: getUserUuid(state),
     }))(
         class extends React.Component<SidePanelProps> {
 
@@ -61,12 +77,24 @@ export const SidePanelButton = withStyles(styles)(
             };
 
             render() {
-                const { classes } = this.props;
+                const { classes, location, resources, currentUserUUID, currentItemId } = this.props;
                 const { anchorEl } = this.state;
+                let enabled = false;
+                if (currentItemId === currentUserUUID) {
+                    enabled = true;
+                } else if (matchProjectRoute(location ? location.pathname : '')) {
+                    const currentProject = getResource<GroupResource>(currentItemId)(resources);
+                    if (currentProject &&
+                        currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
+                        !isProjectTrashed(currentProject, resources)) {
+                        enabled = true;
+                    }
+                }
                 return <Toolbar>
                     <Grid container>
                         <Grid container item xs alignItems="center" justify="flex-start">
-                            <Button variant="contained" color="primary" size="small" className={classes.button}
+                            <Button variant="contained" disabled={!enabled}
+                                color="primary" size="small" className={classes.button}
                                 aria-owns={anchorEl ? 'aside-menu-list' : undefined}
                                 aria-haspopup="true"
                                 onClick={this.handleOpen}>
@@ -104,7 +132,7 @@ export const SidePanelButton = withStyles(styles)(
                 this.props.dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_PATHNAME(location.pathname));
                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(this.props.currentItemId));
-                
+
                 this.props.dispatch<any>(navigateToRunProcess);
             }