15856: Merge branch 'master' into 15856-illegal-chars-warning 15856-illegal-chars-warning
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 3 Dec 2019 18:09:52 +0000 (15:09 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 3 Dec 2019 18:34:59 +0000 (15:34 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

29 files changed:
src/components/breadcrumbs/breadcrumbs.tsx
src/components/icon/icon.tsx
src/components/list-item-text-icon/list-item-text-icon.tsx
src/components/warning/warning.tsx [new file with mode: 0644]
src/services/common-service/common-resource-service.ts
src/store/collections/collection-copy-actions.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-move-actions.ts
src/store/collections/collection-partial-copy-actions.ts
src/store/collections/collection-update-actions.ts
src/store/groups-panel/groups-panel-actions.ts
src/store/processes/process-move-actions.ts
src/store/processes/process-update-actions.ts
src/store/projects/project-create-actions.ts
src/store/projects/project-move-actions.ts
src/store/projects/project-update-actions.ts
src/store/workbench/workbench-actions.ts
src/validators/valid-name.tsx [new file with mode: 0644]
src/validators/validators.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/dialog-copy/dialog-copy.tsx
src/views-components/dialog-move/dialog-move-to.tsx
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/project-tree-picker/project-tree-picker.tsx [deleted file]
src/views-components/project-tree/project-tree.test.tsx [deleted file]
src/views-components/project-tree/project-tree.tsx [deleted file]
src/views-components/projects-tree-picker/tree-picker-field.tsx [new file with mode: 0644]
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/collection-panel/collection-panel.tsx

index 444ac75ef51b97c0f1df5ba8cf55b828bbf571c1..207823307c9284b31fa0a4ca969f974b74a9ea23 100644 (file)
@@ -6,6 +6,7 @@ import * as React from 'react';
 import { Button, Grid, StyleRulesCallback, WithStyles, Typography, Tooltip } from '@material-ui/core';
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
 import { withStyles } from '@material-ui/core';
+import { IllegalNamingWarning } from '../warning/warning';
 
 export interface Breadcrumb {
     label: string;
@@ -37,8 +38,10 @@ export const Breadcrumbs = withStyles(styles)(
     {
         items.map((item, index) => {
             const isLastItem = index === items.length - 1;
+            const isFirstItem = index === 0;
             return (
                 <React.Fragment key={index}>
+                    {isFirstItem ? null : <IllegalNamingWarning name={item.label} />}
                     <Tooltip title={item.label}>
                         <Button
                             color="inherit"
index 44a2b2334bbfb0b2099796aa1d901d5133b41c90..a3d01e9439bcd10c9a59e0a0313b0d9dc7d2e662 100644 (file)
@@ -21,6 +21,7 @@ import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
 import Delete from '@material-ui/icons/Delete';
 import DeviceHub from '@material-ui/icons/DeviceHub';
 import Edit from '@material-ui/icons/Edit';
+import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import Folder from '@material-ui/icons/Folder';
 import GetApp from '@material-ui/icons/GetApp';
@@ -73,6 +74,7 @@ export const DetailsIcon: IconType = (props) => <Info {...props} />;
 export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
 export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
 export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
+export const ErrorIcon: IconType = (props) => <ErrorRoundedIcon style={{color: '#ff0000'}} {...props} />;
 export const FavoriteIcon: IconType = (props) => <Star {...props} />;
 export const HelpIcon: IconType = (props) => <Help {...props} />;
 export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
index 3afc2cf8773293a18566c468253aaade648c3a4a..375538d56a2f753203b94f55ad4f1633750c4777 100644 (file)
@@ -33,6 +33,7 @@ export interface ListItemTextIconDataProps {
     isActive?: boolean;
     hasMargin?: boolean;
     iconSize?: number;
+    nameDecorator?: JSX.Element;
 }
 
 type ListItemTextIconProps = ListItemTextIconDataProps & WithStyles<CssRules>;
@@ -40,7 +41,7 @@ type ListItemTextIconProps = ListItemTextIconDataProps & WithStyles<CssRules>;
 export const ListItemTextIcon = withStyles(styles)(
     class extends React.Component<ListItemTextIconProps, {}> {
         render() {
-            const { classes, isActive, hasMargin, name, icon: Icon, iconSize } = this.props;
+            const { classes, isActive, hasMargin, name, icon: Icon, iconSize, nameDecorator } = this.props;
             return (
                 <Typography component='span' className={classes.root}>
                     <ListItemIcon className={classnames({
@@ -50,8 +51,9 @@ export const ListItemTextIcon = withStyles(styles)(
 
                         <Icon style={{ fontSize: `${iconSize}rem` }} />
                     </ListItemIcon>
+                    {nameDecorator || null}
                     <ListItemText primary={
-                        <Typography  className={classnames(classes.listItemText, {
+                        <Typography className={classnames(classes.listItemText, {
                                 [classes.active]: isActive
                             })}>
                             {name}
diff --git a/src/components/warning/warning.tsx b/src/components/warning/warning.tsx
new file mode 100644 (file)
index 0000000..9a49ff0
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ErrorIcon } from "~/components/icon/icon";
+import { invalidNamingRules } from "~/validators/valid-name";
+import { Tooltip } from "@material-ui/core";
+
+interface WarningComponentProps {
+    text: string;
+    rules: RegExp[];
+    message: string;
+}
+
+export const WarningComponent = ({ text, rules, message }: WarningComponentProps) =>
+    rules.find(aRule => text.match(aRule) !== null)
+    ? message
+        ? <Tooltip title={message}><ErrorIcon /></Tooltip>
+        : <ErrorIcon />
+    : null;
+
+interface IllegalNamingWarningProps {
+    name: string;
+}
+
+export const IllegalNamingWarning = ({ name }: IllegalNamingWarningProps) =>
+    <WarningComponent
+        text={name} rules={invalidNamingRules}
+        message="Names being '.', '..' or including '/' cause issues with WebDAV, please edit it to something different." />;
\ No newline at end of file
index 17c287d22ebaf6fad2d60b774d0fc400f29af5b7..d29ea15642f47dd51153c4d9ceb6e1986617c4db 100644 (file)
@@ -8,7 +8,7 @@ import { ApiActions } from "~/services/api/api-actions";
 import { CommonService } from "~/services/common-service/common-service";
 
 export enum CommonResourceServiceError {
-    UNIQUE_VIOLATION = 'UniqueViolation',
+    UNIQUE_NAME_VIOLATION = 'UniqueNameViolation',
     OWNERSHIP_CYCLE = 'OwnershipCycle',
     MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
     NAME_HAS_ALREADY_BEEN_TAKEN = 'NameHasAlreadyBeenTaken',
@@ -27,7 +27,7 @@ export const getCommonResourceServiceError = (errorResponse: any) => {
         const error = errorResponse.errors.join('');
         switch (true) {
             case /UniqueViolation/.test(error):
-                return CommonResourceServiceError.UNIQUE_VIOLATION;
+                return CommonResourceServiceError.UNIQUE_NAME_VIOLATION;
             case /ownership cycle/.test(error):
                 return CommonResourceServiceError.OWNERSHIP_CYCLE;
             case /Mounts cannot be modified in state 'Final'/.test(error):
index 0ce92dfabcecbdc86a3c4cd087b97d4bcee2fddd..b13d08aaad236eb8eae6962a969fc1aaae79e326 100644 (file)
@@ -38,7 +38,7 @@ export const copyCollection = (resource: CopyFormDialogData) =>
             return newCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(
                     COLLECTION_COPY_FORM_NAME,
                     { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors
index c7dc2ae8ff7bb316410401c4d1662ad0c5ecc405..e077b2a5b33c47b717630baf51c2f83b7bb600a7 100644 (file)
@@ -53,7 +53,7 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
             return newCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
             } else if (error === CommonResourceServiceError.NONE) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME));
index 0351e746b3c8a334d0f950a7cc280f7cec40d96b..6ccd0caaf8dba1afeaa6af58358cfd7a5dddb181 100644 (file)
@@ -38,7 +38,7 @@ export const moveCollection = (resource: MoveToFormDialogData) =>
             return collection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
index 65561aa48a33e4bd6f28860332a08e81b9011b86..72374e65970aae31c50a36387d2788107ec30222 100644 (file)
@@ -72,7 +72,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                 dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
             } catch (e) {
                 const error = getCommonResourceServiceError(e);
-                if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+                if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                     dispatch(stopSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME, { name: 'Collection with this name already exists.' } as FormErrors));
                 } else if (error === CommonResourceServiceError.UNKNOWN) {
                     dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
index 02ec8bb5824920879691a7ae7935ac64263ab30e..5b176beac5bd6d2c67bcbca2204e70b307e1c369 100644 (file)
@@ -31,19 +31,23 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const uuid = collection.uuid || '';
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
+        dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
         try {
-            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
             const updatedCollection = await services.collectionService.update(uuid, collection);
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             return updatedCollection;
         } catch (e) {
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
+            } else {
+                // Unknown error, handling left to caller.
+                dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+                throw(e);
             }
-            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
-            return;
         }
+        return;
     };
index af48b25b3878e838bd95002441ffcddb00e2f5df..cfd8438cbde246b45b79c89005b908770210f3e1 100644 (file)
@@ -101,7 +101,7 @@ export const createGroup = ({ name, users = [] }: CreateGroupFormData) =>
         } catch (e) {
 
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(CREATE_GROUP_FORM, { name: 'Group with the same name already exists.' } as FormErrors));
             }
 
@@ -118,9 +118,9 @@ interface AddGroupMemberArgs {
 }
 
 /**
- * Group membership is determined by whether the group has can_read permission on an object. 
+ * Group membership is determined by whether the group has can_read permission on an object.
  * If a group G can_read an object A, then we say A is a member of G.
- * 
+ *
  * [Permission model docs](https://doc.arvados.org/api/permission-model.html)
  */
 export const addGroupMember = async ({ user, group, ...args }: AddGroupMemberArgs) => {
index 475b4c1fb80c393aa43f70321de579244463a051..5ad41fd5f7f42d9cde5ea4fd57330494c5d0970a 100644 (file)
@@ -41,7 +41,7 @@ export const moveProcess = (resource: MoveToFormDialogData) =>
             return process;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' } as FormErrors));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
index f8d78bc60a0a94930afa8689bf059b85f055b92f..22008d1453c6ee4ba1ed4aa3ddecf2678128e65c 100644 (file)
@@ -41,7 +41,7 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
             return updatedProcess;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' } as FormErrors));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
index 7f12b7cfeeb1fd7526267be2cc613689a82395b1..a303b5518dc7a5b43ceb28cc7028c89764dea4c7 100644 (file)
@@ -67,7 +67,7 @@ export const createProject = (project: Partial<ProjectResource>) =>
             return newProject;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
             }
             return undefined;
index d8eebb8194c05576a33e6eac6f37c4b6e7fed8e0..4dcaf2f514c914a17c6525fb88f2abb9fc9f1cd3 100644 (file)
@@ -38,7 +38,7 @@ export const moveProject = (resource: MoveToFormDialogData) =>
             return newProject;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' } as FormErrors));
             } else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) {
                 dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' } as FormErrors));
index b92069762a0e300e62044f0f289510fae68911ae..2449b9ce9bf1a049eeb7790cfe1fc84ee2ef6b3b 100644 (file)
@@ -39,7 +39,7 @@ export const updateProject = (project: Partial<ProjectResource>) =>
             return updatedProject;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
-            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
             }
             return ;
index 4f3317566a0d86dc85ec5dd7f18beec767dbc4f4..81e84ac52951542b2b5647e538a7ec637a5ac342 100644 (file)
@@ -307,15 +307,19 @@ export const createCollection = (data: collectionCreateActions.CollectionCreateF
 
 export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
     async (dispatch: Dispatch) => {
-        const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
-        if (collection) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Collection has been successfully updated.",
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-            dispatch<any>(updateResources([collection]));
-            dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+        try {
+            const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
+            if (collection) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Collection has been successfully updated.",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch<any>(updateResources([collection]));
+                dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+            }
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors.join(''), hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
     };
 
diff --git a/src/validators/valid-name.tsx b/src/validators/valid-name.tsx
new file mode 100644 (file)
index 0000000..468811d
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+
+const ERROR_MESSAGE = "Name cannot be '.' or '..' or contain '/' characters";
+
+export const invalidNamingRules = [/\//, /^\.{1,2}$/];
+
+export const validName = (value: string) => {
+    return invalidNamingRules.find(aRule => value.match(aRule) !== null)
+        ? ERROR_MESSAGE
+        : undefined;
+};
index acef9744311ccd82a5d43dcc482eb0250c47cb60..13ce4e6ac63cf2c8228717bc6254e09b742bcabf 100644 (file)
@@ -6,13 +6,14 @@ import { require } from './require';
 import { maxLength } from './max-length';
 import { isRsaKey } from './is-rsa-key';
 import { isRemoteHost } from "./is-remote-host";
+import { validName } from "./valid-name";
 
 export const TAG_KEY_VALIDATION = [require, maxLength(255)];
 export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
 
-export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
+export const PROJECT_NAME_VALIDATION = [require, validName, maxLength(255)];
 
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_NAME_VALIDATION = [require, validName, maxLength(255)];
 export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
 export const COLLECTION_PROJECT_VALIDATION = [require];
 
index 4179d60eb3f25120dbd0940eb0f93f2159f7207f..8d2713f5689012c273cb10e4f337e08965a05fa4 100644 (file)
@@ -26,6 +26,7 @@ import { LinkResource } from '~/models/link';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { withResourceData } from '~/views-components/data-explorer/with-resources';
 import { CollectionResource } from '~/models/collection';
+import { IllegalNamingWarning } from '~/components/warning/warning';
 
 const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -34,6 +35,9 @@ const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind
         </Grid>
         <Grid item>
             <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
+                { item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
+                    ? <IllegalNamingWarning name={item.name} />
+                    : null }
                 {item.name}
             </Typography>
         </Grid>
index de8a321cf695183ef17b435887469c183bce12f2..2a95f2eb11c73d500be9be8b48bea6f21419a401 100644 (file)
@@ -7,7 +7,7 @@ import { memoize } from 'lodash/fp';
 import { InjectedFormProps, Field } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
+import { ProjectTreePickerField } from '~/views-components/projects-tree-picker/tree-picker-field';
 import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
 import { TextField } from "~/components/text-field/text-field";
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
@@ -34,6 +34,6 @@ const CopyDialogFields = memoize((pickerId: string) =>
             <Field
                 name="ownerUuid"
                 component={ProjectTreePickerField}
-                validate={COPY_FILE_VALIDATION} 
+                validate={COPY_FILE_VALIDATION}
                 pickerId={pickerId}/>
         </span>);
index c962522f3cf8292853bd743d08cfe92f55c18d7d..7f96f478da90a54bcf9008650d1deca412f174d8 100644 (file)
@@ -7,7 +7,7 @@ import { memoize } from 'lodash/fp';
 import { InjectedFormProps, Field } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
+import { ProjectTreePickerField } from '~/views-components/projects-tree-picker/tree-picker-field';
 import { MOVE_TO_VALIDATION } from '~/validators/validators';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { PickerIdProp } from "~/store/tree-picker/picker-id";
index 2ebcf08ef323e19e48e7af2d566d96f224448e89..f6dc5d5545e4c13d284578221ccb847c7e8872c3 100644 (file)
@@ -6,7 +6,7 @@ import * as React from "react";
 import { Field } from "redux-form";
 import { TextField } from "~/components/text-field/text-field";
 import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePickerField, CollectionTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { ProjectTreePickerField, CollectionTreePickerField } from "~/views-components/projects-tree-picker/tree-picker-field";
 import { PickerIdProp } from '~/store/tree-picker/picker-id';
 
 export const CollectionNameField = () =>
diff --git a/src/views-components/project-tree-picker/project-tree-picker.tsx b/src/views-components/project-tree-picker/project-tree-picker.tsx
deleted file mode 100644 (file)
index b9a2bd5..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-// 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, TreePickerProps } from "../tree-picker/tree-picker";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { ProjectResource } from "~/models/project";
-import { treePickerActions, loadProjectTreePickerProjects, loadFavoriteTreePickerProjects, loadPublicFavoriteTreePickerProjects } from "~/store/tree-picker/tree-picker-actions";
-import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
-import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, PublicFavoriteIcon } from '~/components/icon/icon';
-import { RootState } from "~/store/store";
-import { getUserUuid } from "~/common/getuser";
-import { ServiceRepository } from "~/services/services";
-import { WrappedFieldProps } from 'redux-form';
-import { TreePickerId } from '~/models/tree';
-import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
-import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
-import { PickerIdProp } from '~/store/tree-picker/picker-id';
-
-type ProjectTreePickerProps = Pick<TreePickerProps<ProjectResource>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
-
-const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
-    onContextMenu: () => { return; },
-    toggleItemActive: (_, { id }, pickerId) => {
-        getNotSelectedTreePickerKind(pickerId)
-            .forEach(pickerId => dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: '', pickerId })));
-        dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId }));
-
-        props.onChange(id);
-    },
-    toggleItemOpen: (_, { id, status }, pickerId) => {
-        dispatch<any>(toggleItemOpen(id, status, pickerId));
-    },
-    toggleItemSelection: (_, { id }, pickerId) => {
-        dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId }));
-    },
-});
-
-const toggleItemOpen = (id: string, status: TreeItemStatus, pickerId: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        if (status === TreeItemStatus.INITIAL) {
-            if (pickerId === TreePickerId.PROJECTS) {
-                dispatch<any>(loadProjectTreePickerProjects(id));
-            } else if (pickerId === TreePickerId.FAVORITES) {
-                dispatch<any>(loadFavoriteTreePickerProjects(id === getUserUuid(getState()) ? '' : id));
-            } else if (pickerId === TreePickerId.PUBLIC_FAVORITES) {
-                dispatch<any>(loadPublicFavoriteTreePickerProjects(id === getUserUuid(getState()) ? '' : id));
-                // TODO: load sharedWithMe
-            }
-        } else {
-            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
-        }
-    };
-
-const getNotSelectedTreePickerKind = (pickerId: string) => {
-    return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId);
-};
-
-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} pickerId={TreePickerId.PROJECTS} />
-            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.SHARED_WITH_ME} />
-            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.FAVORITES} />
-            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PUBLIC_FAVORITES} />
-        </div>
-    </div>);
-
-const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
-    switch (item.data.name) {
-        case TreePickerId.FAVORITES:
-            return FavoriteIcon;
-        case TreePickerId.PROJECTS:
-            return ProjectsIcon;
-        case TreePickerId.SHARED_WITH_ME:
-            return ShareMeIcon;
-        case TreePickerId.PUBLIC_FAVORITES:
-            return PublicFavoriteIcon;
-        default:
-            return ProjectIcon;
-    }
-};
-
-const renderTreeItem = (item: TreeItem<ProjectResource>) =>
-    <ListItemTextIcon
-        icon={getProjectPickerIcon(item)}
-        name={typeof item.data === 'string' ? item.data : item.data.name}
-        isActive={item.active}
-        hasMargin={true} />;
-
-export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
-    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectsTreePicker
-            pickerId={props.pickerId}
-            toggleItemActive={handleChange(props)} />
-        {props.meta.dirty && props.meta.error &&
-            <Typography variant='caption' color='error'>
-                {props.meta.error}
-            </Typography>}
-    </div>;
-
-const handleChange = (props: WrappedFieldProps) =>
-    (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
-        props.input.onChange(id);
-
-export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
-    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectsTreePicker
-            pickerId={props.pickerId}
-            toggleItemActive={handleChange(props)}
-            includeCollections />
-        {props.meta.dirty && props.meta.error &&
-            <Typography variant='caption' color='error'>
-                {props.meta.error}
-            </Typography>}
-    </div>;
diff --git a/src/views-components/project-tree/project-tree.test.tsx b/src/views-components/project-tree/project-tree.test.tsx
deleted file mode 100644 (file)
index 18efdaf..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import * as Enzyme from 'enzyme';
-import { mount } from 'enzyme';
-import * as Adapter from 'enzyme-adapter-react-16';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import { Collapse } from '@material-ui/core';
-import CircularProgress from '@material-ui/core/CircularProgress';
-
-import { ProjectTree } from './project-tree';
-import { TreeItem, TreeItemStatus } from '../../components/tree/tree';
-import { ProjectResource } from '../../models/project';
-import { mockProjectResource } from '../../models/test-utils';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-describe("ProjectTree component", () => {
-
-    it("should render ListItemIcon", () => {
-        const project: TreeItem<ProjectResource> = {
-            data: mockProjectResource(),
-            id: "3",
-            open: true,
-            active: true,
-            status: TreeItemStatus.PENDING
-        };
-        const wrapper = mount(<ProjectTree
-            projects={[project]}
-            toggleOpen={jest.fn()}
-            toggleActive={jest.fn()}
-            onContextMenu={jest.fn()} />);
-
-        expect(wrapper.find(ListItemIcon)).toHaveLength(2);
-    });
-
-    it("should render Collapse", () => {
-        const project: Array<TreeItem<ProjectResource>> = [
-            {
-                data: mockProjectResource(),
-                id: "3",
-                open: true,
-                active: true,
-                status: TreeItemStatus.LOADED,
-                items: [
-                    {
-                        data: mockProjectResource(),
-                        id: "3",
-                        open: true,
-                        active: true,
-                        status: TreeItemStatus.PENDING
-                    }
-                ]
-            }
-        ];
-        const wrapper = mount(<ProjectTree
-            projects={project}
-            toggleOpen={jest.fn()}
-            toggleActive={jest.fn()}
-            onContextMenu={jest.fn()} />);
-
-        expect(wrapper.find(Collapse)).toHaveLength(1);
-    });
-
-    it("should render CircularProgress", () => {
-        const project: TreeItem<ProjectResource> = {
-            data: mockProjectResource(),
-            id: "3",
-            open: false,
-            active: true,
-            status: TreeItemStatus.PENDING
-        };
-        const wrapper = mount(<ProjectTree
-            projects={[project]}
-            toggleOpen={jest.fn()}
-            toggleActive={jest.fn()}
-            onContextMenu={jest.fn()} />);
-
-        expect(wrapper.find(CircularProgress)).toHaveLength(1);
-    });
-});
diff --git a/src/views-components/project-tree/project-tree.tsx b/src/views-components/project-tree/project-tree.tsx
deleted file mode 100644 (file)
index b615f5b..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { ReactElement } from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { Tree, TreeItem } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { ProjectIcon } from '~/components/icon/icon';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { ListItemTextIcon } from '~/components/list-item-text-icon/list-item-text-icon';
-
-type CssRules = 'root';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        marginLeft: `${theme.spacing.unit * 1.5}px`,
-    }
-});
-
-export interface ProjectTreeProps<T> {
-    projects: Array<TreeItem<ProjectResource>>;
-    toggleOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
-    toggleActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectResource>) => void;
-}
-
-export const ProjectTree = withStyles(styles)(
-    class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps<T> & WithStyles<CssRules>> {
-        render(): ReactElement<any> {
-            const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
-            return (
-                <div className={classes.root}>
-                    <Tree items={projects}
-                        onContextMenu={onContextMenu}
-                        toggleItemOpen={toggleOpen}
-                        toggleItemActive={toggleActive}
-                        render={
-                            (project: TreeItem<ProjectResource>) =>
-                                <ListItemTextIcon
-                                    icon={ProjectIcon}
-                                    name={project.data.name}
-                                    isActive={project.active}
-                                    hasMargin={true} />
-                        } />
-                </div>
-            );
-        }
-    }
-);
diff --git a/src/views-components/projects-tree-picker/tree-picker-field.tsx b/src/views-components/projects-tree-picker/tree-picker-field.tsx
new file mode 100644 (file)
index 0000000..a8ab05f
--- /dev/null
@@ -0,0 +1,38 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Typography } from "@material-ui/core";
+import { TreeItem } from "~/components/tree/tree";
+import { WrappedFieldProps } from 'redux-form';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
+
+export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
+    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
+        <ProjectsTreePicker
+            pickerId={props.pickerId}
+            toggleItemActive={handleChange(props)} />
+        {props.meta.dirty && props.meta.error &&
+            <Typography variant='caption' color='error'>
+                {props.meta.error}
+            </Typography>}
+    </div>;
+
+const handleChange = (props: WrappedFieldProps) =>
+    (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
+        props.input.onChange(id);
+
+export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
+    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
+        <ProjectsTreePicker
+            pickerId={props.pickerId}
+            toggleItemActive={handleChange(props)}
+            includeCollections />
+        {props.meta.dirty && props.meta.error &&
+            <Typography variant='caption' color='error'>
+                {props.meta.error}
+            </Typography>}
+    </div>;
\ No newline at end of file
index c407bc1fc34f6b721e3104a729e30c73e4fef40b..26aee59af4ee792790ca17bb8a79295eb90d7211 100644 (file)
@@ -14,6 +14,8 @@ import { WorkflowIcon } from '~/components/icon/icon';
 import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
 import { noop } from 'lodash';
+import { ResourceKind } from "~/models/resource";
+import { IllegalNamingWarning } from "~/components/warning/warning";
 export interface SidePanelTreeProps {
     onItemActivation: (id: string) => void;
     sidePanelProgress?: boolean;
@@ -39,14 +41,20 @@ export const SidePanelTree = connect(undefined, mapDispatchToProps)(
     (props: SidePanelTreeActionProps) =>
         <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
 
-const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
-    <ListItemTextIcon
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
+    const name = typeof item.data === 'string' ? item.data : item.data.name;
+    const warn = typeof item.data !== 'string' && item.data.kind === ResourceKind.PROJECT
+        ? <IllegalNamingWarning name={name} />
+        : undefined;
+    return <ListItemTextIcon
         icon={getProjectPickerIcon(item)}
-        name={typeof item.data === 'string' ? item.data : item.data.name}
+        name={name}
+        nameDecorator={warn}
         isActive={item.active}
         hasMargin={true}
         iconSize={1.25}
     />;
+};
 
 const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
     typeof item.data === 'string'
index 99edc1a26912e8822d730d7291784b819ec2ab87..b92557f9de35b59557318ea6a4ba8b69f5f3c588 100644 (file)
@@ -24,6 +24,7 @@ import { formatFileSize } from "~/common/formatters";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { PropertyChipComponent } from '~/views-components/resource-properties-form/property-chip';
+import { IllegalNamingWarning } from '~/components/warning/warning';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link';
 
@@ -89,7 +90,7 @@ export const CollectionPanel = withStyles(styles)(
                                         </IconButton>
                                     </Tooltip>
                                 }
-                                title={item && item.name}
+                                title={item && <span><IllegalNamingWarning name={item.name}/>{item.name}</span>}
                                 titleTypographyProps={this.titleProps}
                                 subheader={item && item.description}
                                 subheaderTypographyProps={this.titleProps} />