Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>
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;
{
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"
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';
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} />;
isActive?: boolean;
hasMargin?: boolean;
iconSize?: number;
+ nameDecorator?: JSX.Element;
}
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({
<Icon style={{ fontSize: `${iconSize}rem` }} />
</ListItemIcon>
+ {nameDecorator || null}
<ListItemText primary={
- <Typography className={classnames(classes.listItemText, {
+ <Typography className={classnames(classes.listItemText, {
[classes.active]: isActive
})}>
{name}
--- /dev/null
+// 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
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',
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):
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
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));
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 }));
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 }));
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;
};
} 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));
}
}
/**
- * 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) => {
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 }));
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 }));
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;
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));
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 ;
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 }));
}
};
--- /dev/null
+// 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;
+};
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];
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}>
</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>
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';
<Field
name="ownerUuid"
component={ProjectTreePickerField}
- validate={COPY_FILE_VALIDATION}
+ validate={COPY_FILE_VALIDATION}
pickerId={pickerId}/>
</span>);
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";
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 = () =>
+++ /dev/null
-// 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>;
+++ /dev/null
-// 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);
- });
-});
+++ /dev/null
-// 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>
- );
- }
- }
-);
--- /dev/null
+// 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
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;
(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'
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';
</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} />