});
});
+ it('attempts to use a preexisting name creating or updating a collection', function() {
+ const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
+ cy.createCollection(adminUser.token, {
+ name: name,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ });
+ cy.loginAs(activeUser);
+ cy.goToPath(`/projects/${activeUser.user.uuid}`);
+ cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+ cy.get('[data-cy=breadcrumb-last]').should('not.exist');
+ // Attempt to create new collection with a duplicate name
+ cy.get('[data-cy=side-panel-button]').click();
+ cy.get('[data-cy=side-panel-new-collection]').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'New collection')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input').type(name);
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ // Error message should display, allowing editing the name
+ cy.get('[data-cy=form-dialog]').should('exist')
+ .and('contain', 'Collection with the same name already exists')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input').type(' renamed');
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ cy.get('[data-cy=form-dialog]').should('not.exist');
+ // Attempt to rename the collection with the duplicate name
+ cy.get('[data-cy=collection-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Edit collection').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'Edit Collection')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input')
+ .type('{selectall}{backspace}')
+ .type(name);
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ cy.get('[data-cy=form-dialog]').should('exist')
+ .and('contain', 'Collection with the same name already exists');
+ });
+
it('uses the property editor (from edit dialog) with vocabulary terms', function () {
cy.createCollection(adminUser.token, {
name: `Test collection ${Math.floor(Math.random() * 999999)}`,
// Confirm proper vocabulary labels are displayed on the UI.
cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
+ // Value field should not complain about being required just after
+ // adding a new property. See #19732
+ cy.get('[data-cy=form-dialog]').should('not.contain', 'This field is required');
+
cy.get('[data-cy=form-submit-btn]').click();
// Confirm that the user was taken to the newly created collection
cy.get('[data-cy=form-dialog]').should('not.exist');
cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
});
+ it('attempts to use a preexisting name creating a project', function() {
+ const name = `Test project ${Math.floor(Math.random() * 999999)}`;
+ cy.createGroup(activeUser.token, {
+ name: name,
+ group_class: 'project',
+ });
+ cy.loginAs(activeUser);
+ cy.goToPath(`/projects/${activeUser.user.uuid}`);
+
+ // Attempt to create new collection with a duplicate name
+ cy.get('[data-cy=side-panel-button]').click();
+ cy.get('[data-cy=side-panel-new-project]').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'New Project')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input').type(name);
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ // Error message should display, allowing editing the name
+ cy.get('[data-cy=form-dialog]').should('exist')
+ .and('contain', 'Project with the same name already exists')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input').type(' renamed');
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+ cy.get('[data-cy=form-dialog]').should('not.exist');
+ });
+
it('navigates to the parent project after trashing the one being displayed', function() {
cy.createGroup(activeUser.token, {
name: `Test root project ${Math.floor(Math.random() * 999999)}`,
});
describe('Frozen projects', () => {
- beforeEach(() => {
+ beforeEach(() => {
cy.createGroup(activeUser.token, {
name: `Main project ${Math.floor(Math.random() * 999999)}`,
group_class: 'project',
}).as('mainProject');
-
+
cy.createGroup(adminUser.token, {
name: `Admin project ${Math.floor(Math.random() * 999999)}`,
group_class: 'project',
name: `Main collection ${Math.floor(Math.random() * 999999)}`,
owner_uuid: mainProject.uuid,
manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('mainCollection');
+ }).as('mainCollection');
});
});
cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
cy.get('main').contains(adminProject.name).rightclick();
-
+
cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
});
});
return super.get(uuid, showErrors, selectParam, session);
}
- create(data?: Partial<CollectionResource>) {
- return super.create({ ...data, preserveVersion: true });
+ create(data?: Partial<CollectionResource>, showErrors?: boolean) {
+ return super.create({ ...data, preserveVersion: true }, showErrors);
}
- update(uuid: string, data: Partial<CollectionResource>) {
+ update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
const select = [...Object.keys(data), 'version', 'modifiedAt'];
- return super.update(uuid, { ...data, preserveVersion: true }, select);
+ return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
}
async files(uuid: string) {
]));
}
- create(data?: Partial<T>) {
+ create(data?: Partial<T>, showErrors?: boolean) {
let payload: any;
if (data !== undefined) {
this.readOnlyFields.forEach( field => delete data[field] );
[this.resourceType.slice(0, -1)]: CommonService.mapKeys(snakeCase)(data),
};
}
- return super.create(payload);
+ return super.create(payload, showErrors);
}
- update(uuid: string, data: Partial<T>, select?: string[]) {
+ update(uuid: string, data: Partial<T>, showErrors?: boolean, select?: string[]) {
let payload: any;
if (data !== undefined) {
this.readOnlyFields.forEach( field => delete data[field] );
payload.select = ['uuid', ...select.map(field => snakeCase(field))];
};
}
- return super.update(uuid, payload);
+ return super.update(uuid, payload, showErrors);
}
}
export const getCommonResourceServiceError = (errorResponse: any) => {
- if ('errors' in errorResponse && 'errorToken' in errorResponse) {
+ if ('errors' in errorResponse) {
const error = errorResponse.errors.join('');
switch (true) {
case /UniqueViolation/.test(error):
}
}
- update(uuid: string, data: Partial<T>) {
+ update(uuid: string, data: Partial<T>, showErrors?: boolean) {
this.validateUuid(uuid);
return CommonService.defaultResponse(
this.serverApi
.put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)),
- this.actions
+ this.actions,
+ undefined, // mapKeys
+ showErrors
);
}
}
import { FilterBuilder, joinFilters } from "services/api/filter-builder";
export class ProjectService extends GroupsService<ProjectResource> {
- create(data: Partial<ProjectResource>) {
+ create(data: Partial<ProjectResource>, showErrors?: boolean) {
const projectData = { ...data, groupClass: GroupClass.PROJECT };
- return super.create(projectData);
+ return super.create(projectData, showErrors);
}
list(args: ListArguments = {}) {
let newCollection: CollectionResource | undefined;
try {
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
- newCollection = await services.collectionService.create(data);
+ newCollection = await services.collectionService.create(data, false);
await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
dispatch(reset(COLLECTION_CREATE_FORM_NAME));
const error = getCommonResourceServiceError(e);
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) {
+ } else {
dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
+ const errMsg = e.errors
+ ? e.errors.join('')
+ : 'There was an error while creating the collection';
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: 'Collection has not been created.',
+ message: errMsg,
hideDuration: 2000,
kind: SnackbarKind.ERROR
}));
name: collection.name,
storageClassesDesired: collection.storageClassesDesired,
description: collection.description,
- properties: collection.properties }
+ properties: collection.properties }, false
).then(updatedCollection => {
updatedCollection = {...cachedCollection, ...updatedCollection};
dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+ const errMsg = e.errors
+ ? e.errors.join('')
+ : 'There was an error while updating the collection';
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: e.errors.join(''),
+ message: errMsg,
hideDuration: 2000,
kind: SnackbarKind.ERROR }));
}
import { matchProjectRoute, matchRunProcessRoute } from 'routes/routes';
import { RouterState } from "react-router-redux";
import { GroupClass } from "models/group";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
export interface ProjectCreateFormDialogData {
ownerUuid: string;
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(PROJECT_CREATE_FORM_NAME));
try {
- const newProject = await services.projectService.create(project);
+ dispatch(progressIndicatorActions.START_WORKING(PROJECT_CREATE_FORM_NAME));
+ const newProject = await services.projectService.create(project, false);
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
dispatch(reset(PROJECT_CREATE_FORM_NAME));
return newProject;
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+ } else {
+ dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME));
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
+ const errMsg = e.errors
+ ? e.errors.join('')
+ : 'There was an error while creating the collection';
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: errMsg,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR
+ }));
}
return undefined;
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(PROJECT_CREATE_FORM_NAME));
}
};
import { ProjectProperties } from "./project-create-actions";
import { getResource } from "store/resources/resources";
import { ProjectResource } from "models/project";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
export interface ProjectUpdateFormDialogData {
uuid: string;
name: project.name,
description: project.description,
properties: project.properties,
- });
+ },
+ false);
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(reset(PROJECT_UPDATE_FORM_NAME));
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+ } else {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+ const errMsg = e.errors
+ ? e.errors.join('')
+ : 'There was an error while updating the project';
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: errMsg,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR }));
}
- return ;
+ return;
}
};
import { isRemoteHost } from "./is-remote-host";
import { validFilePath, validName, validNameAllowSlash } from "./valid-name";
-export const TAG_KEY_VALIDATION = [require, maxLength(255)];
-export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
+export const TAG_KEY_VALIDATION = [maxLength(255)];
+export const TAG_VALUE_VALIDATION = [maxLength(255)];
export const PROJECT_NAME_VALIDATION = [require, validName, maxLength(255)];
export const PROJECT_NAME_VALIDATION_ALLOW_SLASH = [require, validNameAllowSlash, maxLength(255)];
const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps) =>
(value: string) =>
- getTagValues(propertyKeyId, vocabulary).find(v => v.label === value)
+ getTagValues(propertyKeyId, vocabulary).find(v => !value || v.label === value)
? undefined
: 'Incorrect value';
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { InjectedFormProps } from 'redux-form';
+import { RootState } from 'store/store';
+import { connect } from 'react-redux';
+import { formValueSelector, InjectedFormProps } from 'redux-form';
import { Grid, withStyles, WithStyles } from '@material-ui/core';
import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME, PROPERTY_KEY_FIELD_ID } from './property-key-field';
import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME, PROPERTY_VALUE_FIELD_ID } from './property-value-field';
import { ProgressButton } from 'components/progress-button/progress-button';
import { GridClassKey } from '@material-ui/core/Grid';
+const AddButton = withStyles(theme => ({
+ root: { marginTop: theme.spacing.unit }
+}))(ProgressButton);
+
+const mapStateToProps = (state: RootState) => {
+ return {
+ applySelector: (selector) => selector(state, 'key', 'value', 'keyID', 'valueID')
+ }
+}
+
+interface ApplySelector {
+ applySelector: (selector) => any;
+}
+
export interface ResourcePropertiesFormData {
uuid: string;
[PROPERTY_KEY_FIELD_NAME]: string;
clearPropertyKeyOnSelect?: boolean;
}
-export type ResourcePropertiesFormProps = {uuid: string; clearPropertyKeyOnSelect?: boolean } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string; }> & WithStyles<GridClassKey>;
+type ResourcePropertiesFormProps = {uuid: string; clearPropertyKeyOnSelect?: boolean } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string;}> & WithStyles<GridClassKey> & ApplySelector;
-export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, invalid, classes, uuid, clearPropertyKeyOnSelect }: ResourcePropertiesFormProps ) => {
+export const ResourcePropertiesForm = connect(mapStateToProps)(({ handleSubmit, change, submitting, invalid, classes, uuid, clearPropertyKeyOnSelect, applySelector, ...props }: ResourcePropertiesFormProps ) => {
change('uuid', uuid); // Sets the uuid field to the uuid of the resource.
+ const propertyValue = applySelector(formValueSelector(props.form));
return <form data-cy='resource-properties-form' onSubmit={handleSubmit}>
<Grid container spacing={16} classes={classes}>
<Grid item xs>
<PropertyValueField />
</Grid>
<Grid item>
- <Button
+ <AddButton
data-cy='property-add-btn'
- disabled={invalid}
+ disabled={invalid || !(propertyValue.key && propertyValue.value)}
loading={submitting}
color='primary'
variant='contained'
type='submit'>
Add
- </Button>
+ </AddButton>
</Grid>
</Grid>
- </form>};
-
-export const Button = withStyles(theme => ({
- root: { marginTop: theme.spacing.unit }
-}))(ProgressButton);
+ </form>}
+);
\ No newline at end of file
root: {
position: 'relative',
backgroundColor: theme.palette.grey["200"],
- '&::after': {
- content: `''`,
- position: 'absolute',
- top: 0,
- left: 0,
- bottom: 0,
- right: 0,
- background: 'url("arvados-logo-big.png") no-repeat center center',
- opacity: 0.2,
- }
+ background: 'url("arvados-logo-big.png") no-repeat center center',
+ backgroundBlendMode: 'soft-light',
},
ontop: {
zIndex: 10
type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps & InactivePanelStateProps;
-
export const InactivePanelRoot = ({ classes, startLinking, inactivePageText, isLoginClusterFederation }: InactivePanelProps) =>
<Grid container justify="center" alignItems="center" direction="column" spacing={24}
className={classes.root}