--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+---
+"$graph":
+- class: Workflow
+ cwlVersion: v1.2
+ hints:
+ - acrContainerImage: 7009415fdc959d0c2819ee2e9db96561+261
+ class: http://arvados.org/cwl#WorkflowRunnerResources
+ id: "#main"
+ inputs:
+ - id: "#main/bar"
+ type:
+ items: Directory
+ type: array
+ - id: "#main/foo"
+ type:
+ items: File
+ type: array
+ outputs: []
+ steps: []
+cwlVersion: v1.2
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+---
+"$graph":
+- class: Workflow
+ cwlVersion: v1.2
+ hints:
+ - acrContainerImage: 7009415fdc959d0c2819ee2e9db96561+261
+ class: http://arvados.org/cwl#WorkflowRunnerResources
+ id: "#main"
+ inputs:
+ - default: []
+ id: "#main/bar"
+ type:
+ items: Directory
+ type: array
+ - default: []
+ id: "#main/foo"
+ type:
+ items: File
+ type: array
+ outputs: []
+ steps: []
+cwlVersion: v1.2
cy.get('[data-cy=form-dialog]')
.should('contain', 'Rename')
.within(() => {
- cy.get('input').type(`{selectall}{backspace}${to}`);
+ cy.get('input')
+ .type('{selectall}{backspace}')
+ .type(to, { parseSpecialCharSequences: false });
});
cy.get('[data-cy=form-submit-btn]').click();
cy.get('[data-cy=collection-files-panel]')
// Create new collection
cy.get('[data-cy=side-panel-button]').click();
cy.get('[data-cy=side-panel-new-collection]').click();
- const collName = `Test collection (${Math.floor(999999 * Math.random())})`;
+ // Name between brackets tests bugfix #17582
+ const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
cy.get('[data-cy=form-dialog]')
.should('contain', 'New collection')
.within(() => {
.find('button').contains('Run Process').should('not.be.disabled');
});
});
+
+ ['workflow_with_array_fields.yaml', 'workflow_with_default_array_fields.yaml'].forEach((yamlfile) =>
+ it('can select multi files when creating workflow '+yamlfile, () => {
+ cy.createProject({
+ owningUser: activeUser,
+ projectName: 'myProject1',
+ addToFavorites: true
+ });
+
+ cy.createCollection(adminUser.token, {
+ name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:baz\n"
+ })
+ .as('testCollection');
+
+ cy.createCollection(adminUser.token, {
+ name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:buz\n`
+ })
+ .as('testCollection2');
+
+ cy.getAll('@myProject1', '@testCollection', '@testCollection2')
+ .then(function ([myProject1, testCollection, testCollection2]) {
+ cy.readFile('cypress/fixtures/'+yamlfile).then(workflow => {
+ cy.createWorkflow(adminUser.token, {
+ name: `TestWorkflow${Math.floor(Math.random() * 999999)}.cwl`,
+ definition: workflow,
+ owner_uuid: myProject1.uuid,
+ })
+ .as('testWorkflow');
+ });
+
+ cy.loginAs(activeUser);
+
+ cy.get('main').contains(myProject1.name).click();
+
+ cy.get('[data-cy=side-panel-button]').click();
+
+ cy.get('#aside-menu-list').contains('Run a process').click();
+
+ cy.get('@testWorkflow')
+ .then((testWorkflow) => {
+ cy.get('main').contains(testWorkflow.name).click();
+ cy.get('[data-cy=run-process-next-button]').click();
+
+ cy.get('label').contains('#main/foo').parent('div').find('input').click();
+ cy.get('div[role=dialog]')
+ .within(() => {
+ cy.get('p').contains('Projects').closest('div[role=button]')
+ .within(() => {
+ cy.get('svg[role=presentation]')
+ .click({ multiple: true });
+ });
+
+ cy.get(`[data-id=${testCollection.uuid}]`)
+ .find('i').click();
+
+ cy.contains('bar').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+ cy.contains('baz').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+ cy.get('[data-cy=ok-button]').click();
+ });
+
+ cy.get('label').contains('#main/bar').parent('div').find('input').click();
+ cy.get('div[role=dialog]')
+ .within(() => {
+ cy.get('p').contains('Projects').closest('div[role=button]')
+ .within(() => {
+ cy.get('svg[role=presentation]')
+ .click({ multiple: true });
+ });
+
+ cy.get(`[data-id=${testCollection.uuid}]`)
+ .find('input[type=checkbox]').click();
+
+ cy.get(`[data-id=${testCollection2.uuid}]`)
+ .find('input[type=checkbox]').click();
+
+ cy.get('[data-cy=ok-button]').click();
+ });
+ });
+
+ cy.get('label').contains('#main/foo').parent('div')
+ .within(() => {
+ cy.contains('baz');
+ cy.contains('bar');
+ });
+
+ cy.get('label').contains('#main/bar').parent('div')
+ .within(() => {
+ cy.contains(testCollection.name);
+ cy.contains(testCollection2.name);
+ });
+ });
+ }));
})
});
});
});
-});
\ No newline at end of file
+});
cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
});
-})
\ No newline at end of file
+
+ 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)}`,
+ group_class: 'project',
+ }).as('testRootProject').then(function() {
+ cy.createGroup(activeUser.token, {
+ name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ owner_uuid: this.testRootProject.uuid,
+ }).as('testSubProject');
+ });
+ cy.getAll('@testRootProject', '@testSubProject').then(function([testRootProject, testSubProject]) {
+ cy.loginAs(activeUser);
+
+ // Go to subproject and trash it.
+ cy.goToPath(`/projects/${testSubProject.uuid}`);
+ cy.get('[data-cy=side-panel-tree]').should('contain', testSubProject.name);
+ cy.get('[data-cy=breadcrumb-last]')
+ .should('contain', testSubProject.name)
+ .rightclick();
+ cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+
+ // Confirm that the parent project should be displayed.
+ cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
+ cy.url().should('contain', `/projects/${testRootProject.uuid}`);
+ cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
+
+ // Checks for bugfix #17637.
+ cy.get('[data-cy=not-found-content]').should('not.exist');
+ cy.get('[data-cy=not-found-page]').should('not.exist');
+ });
+ });
+
+ it('navigates to the root project after trashing the parent of the one being displayed', function() {
+ cy.createGroup(activeUser.token, {
+ name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('testRootProject').then(function() {
+ cy.createGroup(activeUser.token, {
+ name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ owner_uuid: this.testRootProject.uuid,
+ }).as('testSubProject').then(function() {
+ cy.createGroup(activeUser.token, {
+ name : `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ owner_uuid: this.testSubProject.uuid,
+ }).as('testSubSubProject');
+ });
+ });
+ cy.getAll('@testRootProject', '@testSubProject', '@testSubSubProject').then(function([testRootProject, testSubProject, testSubSubProject]) {
+ cy.loginAs(activeUser);
+
+ // Go to innermost project and trash its parent.
+ cy.goToPath(`/projects/${testSubSubProject.uuid}`);
+ cy.get('[data-cy=side-panel-tree]').should('contain', testSubSubProject.name);
+ cy.get('[data-cy=breadcrumb-last]').should('contain', testSubSubProject.name);
+ cy.get('[data-cy=side-panel-tree]')
+ .contains(testSubProject.name)
+ .rightclick();
+ cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+
+ // Confirm that the trashed project's parent should be displayed.
+ cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
+ cy.url().should('contain', `/projects/${testRootProject.uuid}`);
+ cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
+ cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubSubProject.name);
+
+ // Checks for bugfix #17637.
+ cy.get('[data-cy=not-found-content]').should('not.exist');
+ cy.get('[data-cy=not-found-page]').should('not.exist');
+ });
+ });
+});
\ No newline at end of file
getProperArrowAnimation: Function;
itemsMap?: Map<string, TreeItem<any>>;
classes: any;
+ showSelection: any;
+ useRadioButtons?: boolean;
+ handleCheckboxChange: Function;
}
const FLAT_TREE_ACTIONS = {
toggleActive: 'TOGGLE_ACTIVE',
};
-const ItemIcon = React.memo(({type, kind, active, groupClass, classes}: any) => {
+const ItemIcon = React.memo(({ type, kind, active, groupClass, classes }: any) => {
let Icon = ProjectIcon;
if (groupClass === GroupClass.FILTER) {
}
if (kind) {
- switch(kind) {
+ switch (kind) {
case ResourceKind.COLLECTION:
Icon = CollectionIcon;
break;
{props.getProperArrowAnimation(item.status, item.items!)}
</ListItemIcon>
</i>
+ {props.showSelection(item) && !props.useRadioButtons &&
+ <Checkbox
+ checked={item.selected}
+ className={props.classes.checkbox}
+ color="primary"
+ onClick={props.handleCheckboxChange(item)} />}
+ {props.showSelection(item) && props.useRadioButtons &&
+ <Radio
+ checked={item.selected}
+ className={props.classes.checkbox}
+ color="primary" />}
<div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer}>
<span style={{ display: 'flex', alignItems: 'center' }}>
<ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
{
it.open && it.items && it.items.length > 0 &&
it.flatTree ?
- <FlatTree
- it={it}
- itemsMap={itemsMap}
- classes={this.props.classes}
- levelIndentation={levelIndentation}
+ <FlatTree
+ it={it}
+ itemsMap={itemsMap}
+ showSelection={showSelection}
+ classes={this.props.classes}
+ useRadioButtons={useRadioButtons}
+ levelIndentation={levelIndentation}
+ handleCheckboxChange={this.handleCheckboxChange}
+ onContextMenu={this.props.onContextMenu}
+ handleToggleItemOpen={this.handleToggleItemOpen}
+ toggleItemActive={this.props.toggleItemActive}
+ getToggableIconClassNames={this.getToggableIconClassNames}
+ getProperArrowAnimation={this.getProperArrowAnimation}
+ /> :
+ <Collapse in={it.open} timeout="auto" unmountOnExit>
+ <Tree
+ showSelection={this.props.showSelection}
+ items={it.items}
+ render={render}
+ disableRipple={disableRipple}
+ toggleItemOpen={toggleItemOpen}
+ toggleItemActive={toggleItemActive}
+ level={level + 1}
onContextMenu={this.props.onContextMenu}
- handleToggleItemOpen={this.handleToggleItemOpen}
- toggleItemActive={this.props.toggleItemActive}
- getToggableIconClassNames={this.getToggableIconClassNames}
- getProperArrowAnimation={this.getProperArrowAnimation}
- /> :
- <Collapse in={it.open} timeout="auto" unmountOnExit>
- <Tree
- showSelection={this.props.showSelection}
- items={it.items}
- render={render}
- disableRipple={disableRipple}
- toggleItemOpen={toggleItemOpen}
- toggleItemActive={toggleItemActive}
- level={level + 1}
- onContextMenu={this.props.onContextMenu}
- toggleItemSelection={this.props.toggleItemSelection} />
- </Collapse>
+ toggleItemSelection={this.props.toggleItemSelection} />
+ </Collapse>
}
</div>)}
</List>;
it("#create", async () => {
axiosMock
- .onPost("/resource")
+ .onPost("/resources")
.reply(200, { owner_uuid: "ownerUuidValue" });
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
const resource = await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
expect(resource).toEqual({ ownerUuid: "ownerUuidValue" });
});
it("#create maps request params to snake case", async () => {
axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
- expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
+ expect(axiosInstance.post).toHaveBeenCalledWith("/resources", {resource: {owner_uuid: "ownerUuidValue"}});
});
it("#create ignores fields listed as readonly", async () => {
axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
// UUID fields are read-only on all resources.
await commonResourceService.create({ uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
- expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
+ expect(axiosInstance.post).toHaveBeenCalledWith("/resources", {resource: {owner_uuid: "ownerUuidValue"}});
});
it("#update ignores fields listed as readonly", async () => {
axiosInstance.put = jest.fn(() => Promise.resolve({data: {}}));
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
// UUID fields are read-only on all resources.
await commonResourceService.update('resource-uuid', { uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
- expect(axiosInstance.put).toHaveBeenCalledWith("/resource/resource-uuid", {owner_uuid: "ownerUuidValue"});
+ expect(axiosInstance.put).toHaveBeenCalledWith("/resources/resource-uuid", {resource: {owner_uuid: "ownerUuidValue"}});
});
it("#delete", async () => {
axiosMock
- .onDelete("/resource/uuid")
+ .onDelete("/resources/uuid")
.reply(200, { deleted_at: "now" });
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
const resource = await commonResourceService.delete("uuid");
expect(resource).toEqual({ deletedAt: "now" });
});
it("#get", async () => {
axiosMock
- .onGet("/resource/uuid")
+ .onGet("/resources/uuid")
.reply(200, {
modified_at: "now",
properties: {
}
});
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
const resource = await commonResourceService.get("uuid");
// Only first level keys are mapped to camel case
expect(resource).toEqual({
it("#list", async () => {
axiosMock
- .onGet("/resource")
+ .onGet("/resources")
.reply(200, {
kind: "kind",
offset: 2,
items_available: 20
});
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
const resource = await commonResourceService.list({ limit: 10, offset: 1 });
// First level keys are mapped to camel case inside "items" arrays
expect(resource).toEqual({
it("#list using POST when query string is too big", async () => {
axiosMock
- .onAny("/resource")
+ .onAny("/resources")
.reply(200);
const tooBig = 'x'.repeat(1500);
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
await commonResourceService.list({ filters: tooBig });
expect(axiosMock.history.get.length).toBe(0);
expect(axiosMock.history.post.length).toBe(1);
it("#list using GET when query string is not too big", async () => {
axiosMock
- .onAny("/resource")
+ .onAny("/resources")
.reply(200);
const notTooBig = 'x'.repeat(1480);
- const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+ const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
await commonResourceService.list({ filters: notTooBig });
expect(axiosMock.history.post.length).toBe(0);
expect(axiosMock.history.get.length).toBe(1);
// SPDX-License-Identifier: AGPL-3.0
import { AxiosInstance } from "axios";
+import * as _ from "lodash";
import { Resource } from "src/models/resource";
import { ApiActions } from "~/services/api/api-actions";
import { CommonService } from "~/services/common-service/common-service";
}
create(data?: Partial<T>) {
+ let payload: any;
if (data !== undefined) {
this.readOnlyFields.forEach( field => delete data[field] );
+ payload = {
+ [this.resourceType.slice(0, -1)]: CommonService.mapKeys(_.snakeCase)(data),
+ };
}
- return super.create(data);
+ return super.create(payload);
}
update(uuid: string, data: Partial<T>) {
+ let payload: any;
if (data !== undefined) {
this.readOnlyFields.forEach( field => delete data[field] );
+ payload = {
+ [this.resourceType.slice(0, -1)]: CommonService.mapKeys(_.snakeCase)(data),
+ };
}
- return super.update(uuid, data);
+ return super.update(uuid, payload);
}
}
constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
this.serverApi = serverApi;
- this.resourceType = '/' + resourceType;
+ this.resourceType = resourceType;
this.actions = actions;
this.readOnlyFields = readOnlyFields;
}
create(data?: Partial<T>, showErrors?: boolean) {
return CommonService.defaultResponse(
this.serverApi
- .post<T>(this.resourceType, data && CommonService.mapKeys(_.snakeCase)(data)),
+ .post<T>(`/${this.resourceType}`, data && CommonService.mapKeys(_.snakeCase)(data)),
this.actions,
true, // mapKeys
showErrors
this.validateUuid(uuid);
return CommonService.defaultResponse(
this.serverApi
- .delete(this.resourceType + '/' + uuid),
+ .delete(`/${this.resourceType}/${uuid}`),
this.actions
);
}
this.validateUuid(uuid);
return CommonService.defaultResponse(
this.serverApi
- .get<T>(this.resourceType + '/' + uuid),
+ .get<T>(`/${this.resourceType}/${uuid}`),
this.actions,
true, // mapKeys
showErrors
if (QueryString.stringify(params).length <= 1500) {
return CommonService.defaultResponse(
- this.serverApi.get(this.resourceType, { params }),
+ this.serverApi.get(`/${this.resourceType}`, { params }),
this.actions
);
} else {
}
});
return CommonService.defaultResponse(
- this.serverApi.post(this.resourceType, formData, {
+ this.serverApi.post(`/${this.resourceType}`, formData, {
params: {
_method: 'GET'
}
this.validateUuid(uuid);
return CommonService.defaultResponse(
this.serverApi
- .put<T>(this.resourceType + '/' + uuid, data && CommonService.mapKeys(_.snakeCase)(data)),
+ .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(_.snakeCase)(data)),
this.actions
);
}
const projectService = new ProjectService(axiosInstance, actions);
const resource = await projectService.create({ name: "nameValue" });
expect(axiosInstance.post).toHaveBeenCalledWith("/groups", {
- name: "nameValue",
- group_class: "project"
+ group: {
+ name: "nameValue",
+ group_class: "project"
+ }
});
});
import { updateResources } from "~/store/resources/resources-actions";
import { getProperty } from "~/store/properties/properties";
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
import { ListResults } from '~/services/common-service/common-service';
import { loadContainers } from '~/store/processes/processes-actions';
const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
if (projectUuid === SidePanelTreeCategory.SHARED_WITH_ME) {
await dispatch<any>(loadSharedRoot);
- } else if (node || projectUuid === '') {
+ } else if (node || projectUuid !== '') {
await dispatch<any>(loadProject(projectUuid));
}
};
import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
import { projectPanelActions } from "~/store/project-panel/project-panel-action";
import { ResourceKind } from "~/models/resource";
-import { navigateToTrash } from '~/store/navigation/navigation-action';
+import { navigateTo, navigateToTrash } from '~/store/navigation/navigation-action';
import { matchCollectionRoute } from '~/routes/routes';
export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ let errorMessage = '';
+ let successMessage = '';
try {
if (isTrashed) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash...", kind: SnackbarKind.INFO }));
+ errorMessage = "Could not restore project from trash";
+ successMessage = "Restored from trash";
await services.groupsService.untrash(uuid);
+ dispatch<any>(navigateTo(uuid));
dispatch<any>(activateSidePanelTreeItem(uuid));
- dispatch(trashPanelActions.REQUEST_ITEMS());
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Restored from trash",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
} else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
+ errorMessage = "Could not move project to trash";
+ successMessage = "Added to trash";
await services.groupsService.trash(uuid);
- dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Added to trash",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
+ dispatch<any>(navigateTo(ownerUuid));
}
} catch (e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Could not move project to trash",
+ message: errorMessage,
kind: SnackbarKind.ERROR
}));
}
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: successMessage,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
};
export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ let errorMessage = '';
+ let successMessage = '';
try {
if (isTrashed) {
const { location } = getState().router;
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash...", kind: SnackbarKind.INFO }));
+ errorMessage = "Could not restore collection from trash";
+ successMessage = "Restored from trash";
await services.collectionService.untrash(uuid);
if (matchCollectionRoute(location ? location.pathname : '')) {
dispatch(navigateToTrash);
}
dispatch(trashPanelActions.REQUEST_ITEMS());
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Restored from trash",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
} else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
+ errorMessage = "Could not move collection to trash";
+ successMessage = "Added to trash";
await services.collectionService.trash(uuid);
dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Added to trash",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
}
} catch (e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Could not move collection to trash",
+ message: errorMessage,
kind: SnackbarKind.ERROR
}));
}
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: successMessage,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
};
export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) =>
const node = getNode(id)(picker);
if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
-
const files = await services.collectionService.files(node.value.portableDataHash);
const tree = createCollectionFilesTree(files);
const sorted = sortFilesTree(tree);
export const SidePanelTree = connect(undefined, mapDispatchToProps)(
(props: SidePanelTreeActionProps) =>
- <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+ <span data-cy="side-panel-tree">
+ <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />
+ </span>);
const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
const name = typeof item.data === 'string' ? item.data : item.data.name;
export interface DirectoryArrayInputProps {
input: DirectoryArrayCommandInputParameter;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
});
const formatDirectories = (directories: Directory[] = []) =>
- directories.map(format);
+ directories ? directories.map(format) : [];
const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
portableDataHash: location.replace('keep:', ''),
});
const DirectoryArrayInputComponent = connect(mapStateToProps)(
- class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp, DirectoryArrayInputComponentState> {
+ class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp & {
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+ }, DirectoryArrayInputComponentState> {
state: DirectoryArrayInputComponentState = {
open: false,
directories: [],
.reduce((directories, { value }) =>
'kind' in value &&
value.kind === ResourceKind.COLLECTION &&
- formattedDirectories.find(({ portableDataHash }) => value.portableDataHash === portableDataHash)
+ formattedDirectories.find(({ portableDataHash, name }) => value.portableDataHash === portableDataHash && value.name === name)
? directories.concat(value)
: directories, initialDirectories);
});
const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
- const dir = directories.find(({ portableDataHash }) => portableDataHash === formattedDir.portableDataHash);
+ const dir = directories.find(({ portableDataHash, name }) => portableDataHash === formattedDir.portableDataHash && name === formattedDir.name);
return dir
? [...dirs, dir]
: dirs;
<DialogActions>
<Button onClick={this.closeDialog}>Cancel</Button>
<Button
+ data-cy='ok-button'
variant='contained'
color='primary'
onClick={this.submit}>Ok</Button>
pickerId={this.props.commandInput.id}
includeCollections
showSelection
+ options={this.props.options}
toggleItemSelection={this.refreshDirectories} />
</div>
<Divider />
});
type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
-
-
-
export interface FileArrayInputProps {
input: FileArrayCommandInputParameter;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
export const FileArrayInput = ({ input }: FileArrayInputProps) =>
<Field
});
const formatFiles = (files: File[] = []) =>
- files.map(format);
+ files ? files.map(format) : [];
const format = (file: File): CollectionFile => ({
id: file.location
});
const FileArrayInputComponent = connect(mapStateToProps)(
- class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp, FileArrayInputComponentState> {
+ class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp & {
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+ }, FileArrayInputComponentState> {
state: FileArrayInputComponentState = {
open: false,
files: [],
});
this.setFiles(files);
-
}
refreshFiles = () => {
<DialogActions>
<Button onClick={this.closeDialog}>Cancel</Button>
<Button
+ data-cy='ok-button'
variant='contained'
color='primary'
onClick={this.submit}>Ok</Button>
includeCollections
includeFiles
showSelection
+ options={this.props.options}
toggleItemSelection={this.refreshFiles} />
</div>
<Divider />
});
type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
-
-
-
return <FloatArrayInput input={input as FloatArrayCommandInputParameter} />;
case isArrayOfType(input, CWLType.FILE):
- return <FileArrayInput input={input as FileArrayCommandInputParameter} />;
+ return <FileArrayInput options={{ showOnlyOwned: false, showOnlyWritable: false }} input={input as FileArrayCommandInputParameter} />;
case isArrayOfType(input, CWLType.DIRECTORY):
- return <DirectoryArrayInput input={input as DirectoryArrayCommandInputParameter} />;
+ return <DirectoryArrayInput options={{ showOnlyOwned: false, showOnlyWritable: false }} input={input as DirectoryArrayCommandInputParameter} />;
default:
return null;
#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
set -e -o pipefail
commit="$1"
if git merge-base --is-ancestor "$nearest_tag" "$merge_base" ; then
# x.(y+1).0.devTIMESTAMP, where x.y.z is the newest version that does not contain $commit
- # grep reads the list of tags (-f) that contain $commit and filters them out (-v)
- # this prevents a newer tag from retroactively changing the versions of everything before it
- v=$(git tag | grep -vFf <(git tag --contains "$commit") | sort -Vr | head -n1 | perl -pe 's/\.(\d+)\.\d+/".".($1+1).".0"/e')
+ # grep reads the list of tags (-f) that contain $commit and filters them out (-v)
+ # this prevents a newer tag from retroactively changing the versions of everything before it
+ # We also filter out four-digit faux semantic versioning tags, they gum up the works.
+ v=$(git tag | grep -vFf <(git tag --contains "$commit") |grep -v -P '\d.\d.\d.\d' | sort -Vr | head -n1 | perl -pe 's/\.(\d+)\.\d+/".".($1+1).".0"/e')
else
# x.y.(z+1).devTIMESTAMP, where x.y.z is the latest released ancestor of $commit
v=$(echo $nearest_tag | perl -pe 's/(\d+)$/$1+1/e')