+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-describe('Favorites tests', function() {
- let activeUser;
- let adminUser;
-
- before(function() {
- // Only set up common users once. These aren't set up as aliases because
- // aliases are cleaned up after every test. Also it doesn't make sense
- // to set the same users on beforeEach() over and over again, so we
- // separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'Admin', 'User', true, true)
- .as('adminUser').then(function() {
- adminUser = this.adminUser;
- }
- );
- cy.getUser('collectionuser1', 'Collection', 'User', false, true)
- .as('activeUser').then(function() {
- activeUser = this.activeUser;
- }
- );
- })
-
- beforeEach(function() {
- cy.clearCookies()
- cy.clearLocalStorage()
- })
-
- it('checks that Public favorites does not appear under shared with me', function() {
- cy.loginAs(adminUser);
- cy.contains('Shared with me').click();
- cy.get('main').contains('Public favorites').should('not.exist');
- })
-
- it('creates and removes a public favorite', function() {
- cy.loginAs(adminUser);
- cy.createGroup(adminUser.token, {
- name: `my-favorite-project`,
- group_class: 'project',
- }).as('myFavoriteProject').then(function() {
- cy.contains('Refresh').click();
- cy.get('main').contains('my-favorite-project').rightclick();
- cy.contains('Add to public favorites').click();
- cy.contains('Public Favorites').click();
- cy.get('main').contains('my-favorite-project').rightclick();
- cy.contains('Remove from public favorites').click();
- cy.get('main').contains('my-favorite-project').should('not.exist');
- cy.trashGroup(adminUser.token, this.myFavoriteProject.uuid);
- });
- })
-})
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Favorites tests', function () {
+ let activeUser;
+ let adminUser;
+
+ before(function () {
+ // Only set up common users once. These aren't set up as aliases because
+ // aliases are cleaned up after every test. Also it doesn't make sense
+ // to set the same users on beforeEach() over and over again, so we
+ // separate a little from Cypress' 'Best Practices' here.
+ cy.getUser('admin', 'Admin', 'User', true, true)
+ .as('adminUser').then(function () {
+ adminUser = this.adminUser;
+ }
+ );
+ cy.getUser('collectionuser1', 'Collection', 'User', false, true)
+ .as('activeUser').then(function () {
+ activeUser = this.activeUser;
+ }
+ );
+ })
+
+ beforeEach(function () {
+ cy.clearCookies()
+ cy.clearLocalStorage()
+ })
+
+ it('checks that Public favorites does not appear under shared with me', function () {
+ cy.loginAs(adminUser);
+ cy.contains('Shared with me').click();
+ cy.get('main').contains('Public favorites').should('not.exist');
+ });
+
+ it('creates and removes a public favorite', function () {
+ cy.loginAs(adminUser);
+ cy.createGroup(adminUser.token, {
+ name: `my-favorite-project`,
+ group_class: 'project',
+ }).as('myFavoriteProject').then(function () {
+ cy.contains('Refresh').click();
+ cy.get('main').contains('my-favorite-project').rightclick();
+ cy.contains('Add to public favorites').click();
+ cy.contains('Public Favorites').click();
+ cy.get('main').contains('my-favorite-project').rightclick();
+ cy.contains('Remove from public favorites').click();
+ cy.get('main').contains('my-favorite-project').should('not.exist');
+ cy.trashGroup(adminUser.token, this.myFavoriteProject.uuid);
+ });
+ });
+
+ it('can copy collection to favorites', () => {
+ cy.loginAs(adminUser);
+
+ cy.createGroup(adminUser.token, {
+ name: `my-shared-writable-project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('mySharedWritableProject').then(function (mySharedWritableProject) {
+ cy.contains('Refresh').click();
+ cy.get('main').contains(mySharedWritableProject.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Share').click();
+ });
+ cy.get('[id="select-permissions"]').as('selectPermissions');
+ cy.get('@selectPermissions').click();
+ cy.contains('Write').click();
+ cy.get('.sharing-dialog').as('sharingDialog');
+ cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
+ cy.get('[role=tooltip]').click();
+ cy.get('@sharingDialog').contains('Save').click();
+ });
+
+ cy.createGroup(adminUser.token, {
+ name: `my-shared-readonly-project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('mySharedReadonlyProject').then(function (mySharedReadonlyProject) {
+ cy.contains('Refresh').click();
+ cy.get('main').contains(mySharedReadonlyProject.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Share').click();
+ });
+ cy.get('.sharing-dialog').as('sharingDialog');
+ cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
+ cy.get('[role=tooltip]').click();
+ cy.get('@sharingDialog').contains('Save').click();
+ });
+
+ cy.createGroup(activeUser.token, {
+ name: `my-project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('myProject1');
+
+ 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"
+ })
+ .as('testCollection');
+
+ cy.getAll('@mySharedWritableProject', '@mySharedReadonlyProject', '@myProject1', '@testCollection')
+ .then(function ([mySharedWritableProject, mySharedReadonlyProject, myProject1, testCollection]) {
+ cy.loginAs(activeUser);
+
+ cy.contains('Shared with me').click();
+
+ cy.get('main').contains(mySharedWritableProject.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Add to favorites').click();
+ });
+
+ cy.get('main').contains(mySharedReadonlyProject.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Add to favorites').click();
+ });
+
+ cy.doSearch(`${activeUser.user.uuid}`);
+
+ cy.get('main').contains(myProject1.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Add to favorites').click();
+ });
+
+ cy.contains(testCollection.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Move to').click();
+ });
+
+ cy.get('[data-cy=form-dialog]').within(function () {
+ cy.get('[data-cy=projects-tree-favourites-tree-picker]').find('i').click();
+ cy.contains(myProject1.name);
+ cy.contains(mySharedWritableProject.name);
+ cy.get('[data-cy=projects-tree-favourites-tree-picker]')
+ .should('not.contain', mySharedReadonlyProject.name);
+ cy.contains(mySharedWritableProject.name).click();
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+
+ cy.doSearch(`${mySharedWritableProject.uuid}`);
+ cy.get('main').contains(testCollection.name);
+ });
+ });
+
+ it('can copy selected into the collection', () => {
+ cy.loginAs(adminUser);
+
+ cy.createCollection(adminUser.token, {
+ name: `Test source collection ${Math.floor(Math.random() * 999999)}`,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ })
+ .as('testSourceCollection').then(function (testSourceCollection) {
+ cy.contains('Refresh').click();
+ cy.get('main').contains(testSourceCollection.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Share').click();
+ });
+ cy.get('[id="select-permissions"]').as('selectPermissions');
+ cy.get('@selectPermissions').click();
+ cy.contains('Write').click();
+ cy.get('.sharing-dialog').as('sharingDialog');
+ cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
+ cy.get('[role=tooltip]').click();
+ cy.get('@sharingDialog').contains('Save').click();
+ });
+
+ cy.createCollection(adminUser.token, {
+ name: `Test target collection ${Math.floor(Math.random() * 999999)}`,
+ })
+ .as('testTargetCollection').then(function (testTargetCollection) {
+ cy.contains('Refresh').click();
+ cy.get('main').contains(testTargetCollection.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Share').click();
+ });
+ cy.get('[id="select-permissions"]').as('selectPermissions');
+ cy.get('@selectPermissions').click();
+ cy.contains('Write').click();
+ cy.get('.sharing-dialog').as('sharingDialog');
+ cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
+ cy.get('[role=tooltip]').click();
+ cy.get('@sharingDialog').contains('Save').click();
+ });
+
+ cy.getAll('@testSourceCollection', '@testTargetCollection')
+ .then(function ([testSourceCollection, testTargetCollection]) {
+ cy.loginAs(activeUser);
+
+ cy.get('.layout-pane-primary')
+ .contains('Projects').click();
+
+ cy.get('main').contains(testTargetCollection.name).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Add to favorites').click();
+ });
+
+ cy.get('main').contains(testSourceCollection.name).click();
+ cy.get('[data-cy=collection-files-panel]').contains('bar');
+ cy.get('[data-cy=collection-files-panel]').find('input[type=checkbox]').click({ force: true });
+ cy.get('[data-cy=collection-files-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]')
+ .contains('Copy selected into the collection').click();
+
+ cy.get('[data-cy=projects-tree-favourites-tree-picker]')
+ .find('i')
+ .click();
+
+ cy.get('[data-cy=projects-tree-favourites-tree-picker]')
+ .contains(testTargetCollection.name)
+ .click();
+
+ cy.get('[data-cy=form-submit-btn]').click();
+
+ cy.get('.layout-pane-primary')
+ .contains('Projects').click();
+
+ cy.get('main').contains(testTargetCollection.name).click();
+
+ cy.get('[data-cy=collection-files-panel]').contains('bar');
+ });
+ });
+});
\ No newline at end of file
const systemToken = Cypress.env('system_token');
Cypress.Commands.add(
- "doRequest", (method='GET', path='', data=null, qs=null,
- token=systemToken, auth=false, followRedirect=true) => {
- return cy.request({
- method: method,
- url: `${controllerURL}/${path}`,
- body: data,
- qs: auth ? qs : Object.assign({api_token: token}, qs),
- auth: auth ? {bearer: `${token}`} : undefined,
- followRedirect: followRedirect
- })
- }
+ "doRequest", (method = 'GET', path = '', data = null, qs = null,
+ token = systemToken, auth = false, followRedirect = true) => {
+ return cy.request({
+ method: method,
+ url: `${controllerURL}/${path}`,
+ body: data,
+ qs: auth ? qs : Object.assign({ api_token: token }, qs),
+ auth: auth ? { bearer: `${token}` } : undefined,
+ followRedirect: followRedirect
+ })
+}
)
// This resets the DB removing all content and seeding it with the fixtures.
)
Cypress.Commands.add(
- "getUser", (username, first_name='', last_name='', is_admin=false, is_active=true) => {
+ "getUser", (username, first_name = '', last_name = '', is_admin = false, is_active = true) => {
// Create user if not already created
return cy.doRequest('POST', '/auth/controller/callback', {
auth_info: JSON.stringify({
}),
return_to: ',https://example.local'
}, null, systemToken, true, false) // Don't follow redirects so we can catch the token
- .its('headers.location').as('location')
- // Get its token and set the account up as admin and/or active
- .then(function() {
- this.userToken = this.location.split("=")[1]
- assert.isString(this.userToken)
- return cy.doRequest('GET', '/arvados/v1/users', null, {
- filters: `[["username", "=", "${username}"]]`
- })
- .its('body.items.0')
- .as('aUser')
- .then(function() {
- cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
- user: {
- is_admin: is_admin,
- is_active: is_active
- }
- })
- .its('body')
- .as('theUser')
- .then(function() {
- return {user: this.theUser, token: this.userToken};
+ .its('headers.location').as('location')
+ // Get its token and set the account up as admin and/or active
+ .then(function () {
+ this.userToken = this.location.split("=")[1]
+ assert.isString(this.userToken)
+ return cy.doRequest('GET', '/arvados/v1/users', null, {
+ filters: `[["username", "=", "${username}"]]`
})
+ .its('body.items.0')
+ .as('aUser')
+ .then(function () {
+ cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
+ user: {
+ is_admin: is_admin,
+ is_active: is_active
+ }
+ })
+ .its('body')
+ .as('theUser')
+ .then(function () {
+ return { user: this.theUser, token: this.userToken };
+ })
+ })
})
- })
}
)
Cypress.Commands.add(
"createResource", (token, suffix, data) => {
- return cy.doRequest('POST', '/arvados/v1/'+suffix, data, null, token, true)
- .its('body').as('resource')
- .then(function() {
- return this.resource;
- })
+ return cy.doRequest('POST', '/arvados/v1/' + suffix, data, null, token, true)
+ .its('body').as('resource')
+ .then(function () {
+ return this.resource;
+ })
}
)
Cypress.Commands.add(
"deleteResource", (token, suffix, uuid) => {
- return cy.doRequest('DELETE', '/arvados/v1/'+suffix+'/'+uuid)
- .its('body').as('resource')
- .then(function() {
- return this.resource;
- })
+ return cy.doRequest('DELETE', '/arvados/v1/' + suffix + '/' + uuid)
+ .its('body').as('resource')
+ .then(function () {
+ return this.resource;
+ })
}
)
Cypress.Commands.add(
"updateResource", (token, suffix, uuid, data) => {
- return cy.doRequest('PUT', '/arvados/v1/'+suffix+'/'+uuid, data, null, token, true)
- .its('body').as('resource')
- .then(function() {
- return this.resource;
- })
+ return cy.doRequest('PUT', '/arvados/v1/' + suffix + '/' + uuid, data, null, token, true)
+ .its('body').as('resource')
+ .then(function () {
+ return this.resource;
+ })
}
)
"doSearch", (searchTerm) => {
cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`);
}
-)
\ No newline at end of file
+)
+
+Cypress.Commands.add('getAll', (...elements) => {
+ const promise = cy.wrap([], { log: false })
+
+ for (let element of elements) {
+ promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])))
+ }
+
+ return promise
+})
\ No newline at end of file
results.items.map(item => this.linkService.delete(item.uuid))));
}
- list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}, showOnlyOwned: boolean = true): Promise<ListResults<GroupContentsResource>> {
const listFilters = new FilterBuilder()
.addEqual('owner_uuid', userUuid)
.addEqual('link_class', LinkClass.STAR)
})
.then(results => {
const uuids = results.items.map(item => item.headUuid);
- return this.groupsService.contents(userUuid, {
+ return this.groupsService.contents(showOnlyOwned ? userUuid : '', {
limit,
offset,
order: contentOrder,
dispatch(startSubmit(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
const state = getState();
const currentCollection = state.collectionPanel.item;
+
+ if (currentCollection && !currentCollection.manifestText) {
+ const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid);
+ currentCollection.manifestText = fetchedCurrentCollection.manifestText;
+ currentCollection.unsignedManifestText = fetchedCurrentCollection.unsignedManifestText;
+ }
+
if (currentCollection) {
try {
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
}
});
const diffPathToRemove = _.difference(paths, pathsToRemove);
- await services.collectionService.deleteFiles(selectedCollection.uuid, pathsToRemove);
+ await services.collectionService.deleteFiles(selectedCollection.uuid, pathsToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid);
- await services.collectionService.update(collectionUuid, { manifestText: `${collectionWithDeletedFiles.manifestText}${currentCollection.manifestText ? currentCollection.manifestText : currentCollection.unsignedManifestText}` });
- await services.collectionService.deleteFiles(collectionWithDeletedFiles.uuid, diffPathToRemove);
+ await services.collectionService.update(collectionUuid, { manifestText: `${collectionWithDeletedFiles.manifestText}${(currentCollection.manifestText ? currentCollection.manifestText : currentCollection.unsignedManifestText) || ''}` });
+ await services.collectionService.deleteFiles(collectionWithDeletedFiles.uuid, diffPathToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'Files has been copied to selected collection.',
import { LinkResource, LinkClass } from "~/models/link";
import { mapTreeValues } from "~/models/tree";
import { sortFilesTree } from "~/services/collection-service/collection-service-files-response";
+import { GroupResource } from "~/models/group";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
includeFiles?: boolean;
}
-export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
+export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
+ options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const { pickerId, includeCollections = false, includeFiles = false } = params;
const uuid = getUserUuid(getState());
fb => fb.getFilters(),
)(new FilterBuilder());
- const { items } = await services.favoriteService.list(uuid, { filters });
+ const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
dispatch<any>(receiveTreePickerData<GroupContentsResource>({
id: 'Favorites',
pickerId,
- data: items,
+ data: items.filter((item) => {
+ if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
+ return false;
+ }
+
+ return true;
+ }),
extractNodeData: item => ({
id: item.uuid,
value: item,
export const FavoritesTreePicker = connect(() => ({
rootItemIcon: FavoriteIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
- loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
- dispatch<any>(loadFavoritesProject({ pickerId, includeCollections, includeFiles }));
+ loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+ dispatch<any>(loadFavoritesProject({ pickerId, includeCollections, includeFiles }, options));
},
}))(ProjectsTreePicker);
\ No newline at end of file
showSelection?: boolean;
relatedTreePickers?: string[];
disableActivation?: string[];
- loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string, includeCollections?: boolean, includeFiles?: boolean) => void;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+ loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string,
+ includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
}
export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
showSelection: isSelectionVisible(showSelection),
});
-const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, relatedTreePickers, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
+const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
onContextMenu: () => { return; },
toggleItemActive: (event, item, pickerId) => {
: loadProject({ id, pickerId, includeCollections, includeFiles })
);
} else if (!('type' in data) && loadRootItem) {
- loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, includeCollections, includeFiles);
+ loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, includeCollections, includeFiles, options);
}
} else if (status === TreeItemStatus.LOADED) {
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
includeCollections?: boolean;
includeFiles?: boolean;
showSelection?: boolean;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
toggleItemActive?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectsTreePickerItem>, pickerId: string) => void;
toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectsTreePickerItem>, pickerId: string) => void;
}
<HomeTreePicker pickerId={home} {...p} />
<SharedTreePicker pickerId={shared} {...p} />
<PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
- <FavoritesTreePicker pickerId={favorites} {...p} />
+ <div data-cy="projects-tree-favourites-tree-picker">
+ <FavoritesTreePicker pickerId={favorites} {...p} />
+ </div>
</div>;
};
<div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
<ProjectsTreePicker
pickerId={props.pickerId}
- toggleItemActive={handleChange(props)} />
+ toggleItemActive={handleChange(props)}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }} />
{props.meta.dirty && props.meta.error &&
<Typography variant='caption' color='error'>
{props.meta.error}
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }}
includeCollections />
{props.meta.dirty && props.meta.error &&
<Typography variant='caption' color='error'>
const { children, open, loading, advancedEnabled, saveEnabled, onAdvanced, onClose, onExited, onSave } = props;
return <Dialog
{...{ open, onClose, onExited }}
+ className="sharing-dialog"
fullWidth
maxWidth='sm'
disableBackdropClick
export default () =>
<Grid container spacing={8}>
- <Grid item xs={8}>
+ <Grid data-cy="invite-people-field" item xs={8}>
<InvitedPeopleField />
</Grid>
- <Grid item xs={4}>
+ <Grid data-cy="permission-select-field" item xs={4}>
<PermissionSelectField />
</Grid>
</Grid>;