Commit 8a74d740 authored by Thomas Matthews's avatar Thomas Matthews

Merge branch 'development' into lock-after-failed-logins

parents af7b9780 16d3c23a
Pipeline #6468 failed with stage
in 25 minutes and 8 seconds
......@@ -18,6 +18,7 @@ Changes
* [UI]: Clean up of the main navigation bar code, and removed its dependency on angular-ui.
* [UI]: Fixed reflow layout of pipeline launch page.
* [UI]: Changed the wording of 'copying' samples to 'sharing' samples.
* [UI]: Allow users to share (copy) samples from a remote project. Disabled menu items for move and merge.
* [Developer]: Ran `prettier` on all javascript files within `resources/js`.
* [Developer]: Ran `prettier` on all scss files within `resources/sass`.
* [Developer]: Add a git pre-commit hook to ensure `prettier` formatting.
......
......@@ -257,7 +257,7 @@ Alternatively, there is a dropdown next to the select all checkbox that allows y
An alternative to [sharing samples between projects](#sharing-samples-between-projects) is to **move** a sample between projects. Unlike sharing, when a sample is moved, the original sample is removed.
Like sharing samples, you must be a project <img src="images/manager-icon.png" class="inline" alt="Manager role icon."> **Manager** on **both** the project that you are moving the sample *from*, and the project that you are moving the sample *to*.
Like sharing samples, you must be a project <img src="images/manager-icon.png" class="inline" alt="Manager role icon."> **Manager** on **both** the project that you are moving the sample *from*, and the project that you are moving the sample *to*. In addition, the source project you are sharing *from* must **not** be a remote project.
Start by [selecting the samples](#selecting-samples) that you want to move to the other project. When you've selected the samples that you want to move, click on the "Samples" button just above the samples list and select "Move Samples":
......@@ -275,7 +275,7 @@ Once you've selected the project that you want to move the samples to, click on
### Merging samples within a project
If a sample was created when sequencing data was uploaded with an incorrect name, you may want to merge two samples together. When you merge two samples, you will move all of the **sequencing files** and **assembled genomes** from one sample to another, then **delete the original sample**. **None** of the sample metadata will be copied between the merged samples, instead you will select one sample as the target for the sample merge. Only users with the project <img src="images/manager-icon.png" class="inline" alt="Manager role icon."> **Manager** role can merge samples in a project.
If a sample was created when sequencing data was uploaded with an incorrect name, you may want to merge two samples together. When you merge two samples, you will move all of the **sequencing files** and **assembled genomes** from one sample to another, then **delete the original sample**. **None** of the sample metadata will be copied between the merged samples, instead you will select one sample as the target for the sample merge. Only users with the project <img src="images/manager-icon.png" class="inline" alt="Manager role icon."> **Manager** role can merge samples in a project and samples cannot be merged within **remote** projects.
Start by [selecting the samples](#selecting-samples) that you want to merge. You **must** select more than one sample to enable the merge samples button. Once you've selected the two or more samples that you would like to merge, click on the "Samples" button just above the samples list and select "Merge Samples":
......
......@@ -2,7 +2,6 @@ package ca.corefacility.bioinformatics.irida.ria.web.models.datatables;
import java.util.Date;
import ca.corefacility.bioinformatics.irida.model.joins.Join;
import ca.corefacility.bioinformatics.irida.model.joins.impl.ProjectUserJoin;
import ca.corefacility.bioinformatics.irida.model.project.Project;
import ca.corefacility.bioinformatics.irida.model.user.User;
......
......@@ -70,6 +70,9 @@ public class ProjectControllerUtils {
boolean isOwner = projectOwnerPermission.isAllowed(authentication, project);
model.addAttribute("isOwner", isOwner);
boolean isOwnerAllowRemote = projectMembersPermission.isAllowed(authentication, project);
model.addAttribute("isOwnerAllowRemote", isOwnerAllowRemote);
boolean manageMembers = projectMembersPermission.isAllowed(authentication, project);
model.addAttribute("manageMembers", manageMembers);
......
......@@ -26,6 +26,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
......@@ -262,20 +263,23 @@ public class ProjectSamplesController {
}
/**
* Create a modal dialogue for moving or copying {@link Sample} to another {@link Project}
* Create a modal dialogue for moving or sharing {@link Sample} to another {@link Project}
*
* @param ids {@link List} of identifiers for {@link Sample}s to copy or move.
* @param projectId Identifier for the current {@link Project}
* @param model UI Model
* @param move Whether or not to display copy or move wording.
* @return Path to copy or move modal template.
* @param move Whether or not to display share or move wording.
* @return Path to share or move modal template.
*/
@RequestMapping(value = "/projects/{projectId}/templates/copy-move-modal", produces = MediaType.TEXT_HTML_VALUE)
public String getCopySamplesModal(@RequestParam(name = "sampleIds[]") List<Long> ids, @PathVariable Long projectId,
public String getShareSamplesModal(@RequestParam(name = "sampleIds[]") List<Long> ids, @PathVariable Long projectId,
Model model, @RequestParam(required = false) boolean move) {
model.addAllAttributes(generateCopyMoveSamplesContent(projectId, ids));
Project project = projectService.read(projectId);
model.addAllAttributes(generateShareMoveSamplesContent(project, ids));
model.addAttribute("projectId", projectId);
model.addAttribute("type", move ? "move" : "copy");
model.addAttribute("isRemoteProject", project.isRemote());
return PROJECT_TEMPLATE_DIR + "copy-move-modal.tmpl";
}
......@@ -324,16 +328,16 @@ public class ProjectSamplesController {
}
/**
* Generate a {@link Map} of {@link Sample} to move or copy.
* Generate a {@link Map} of {@link Sample} to move or share.
*
* @param project The {@link Project}.
* @param ids
* {@link Long} of ids for {@link Sample}
*
* @return {@link Map} of samples to be moved or copied
* @return {@link Map} of samples to be moved or shared.
*/
private Map<String, List<Sample>> generateCopyMoveSamplesContent(Long projectId, List<Long> ids) {
private Map<String, List<Sample>> generateShareMoveSamplesContent(Project project, List<Long> ids) {
Map<String, List<Sample>> model = new HashMap<>();
Project project = projectService.read(projectId);
List<Sample> samples = new ArrayList<>();
List<Sample> extraSamples = new ArrayList<>();
List<Sample> lockedSamples = new ArrayList<>();
......@@ -549,10 +553,10 @@ public class ProjectSamplesController {
}
/**
* Copy or move samples from one project to another
* Share or move samples from one project to another
*
* @param projectId The original project id
* @param sampleIds the sample identifiers to copy
* @param sampleIds the sample identifiers to share
* @param newProjectId The new project id
* @param remove true/false whether to remove the samples from the original project
* @param giveOwner whether to give ownership of the sample to the new project
......@@ -561,7 +565,7 @@ public class ProjectSamplesController {
*/
@RequestMapping(value = "/projects/{projectId}/ajax/samples/copy", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> copySampleToProject(@PathVariable Long projectId,
public Map<String, Object> shareSampleToProject(@PathVariable Long projectId,
@RequestParam(value = "sampleIds[]") List<Long> sampleIds, @RequestParam Long newProjectId,
@RequestParam(required = false) boolean remove,
@RequestParam(required = false, defaultValue = "false") boolean giveOwner, Locale locale) {
......@@ -576,12 +580,20 @@ public class ProjectSamplesController {
List<ProjectSampleJoin> successful = new ArrayList<>();
try {
successful = projectService.copyOrMoveSamples(originalProject, newProject, Lists.newArrayList(samples),
remove, giveOwner);
if (remove) {
successful = projectService.moveSamples(originalProject, newProject, Lists.newArrayList(samples), giveOwner);
} else {
successful = projectService.shareSamples(originalProject, newProject, Lists.newArrayList(samples), giveOwner);
}
} catch (EntityExistsException ex) {
logger.warn("Attempt to add project to sample failed", ex);
warnings.add(ex.getLocalizedMessage());
} catch (AccessDeniedException ex) {
logger.warn("Access denied adding samples to project " + newProjectId, ex);
String msg = remove ? "project.samples.move.sample-denied" : "project.samples.copy.sample-denied";
warnings.add(
messageSource.getMessage(msg, new Object[] { newProject.getName() }, locale));
}
if (!warnings.isEmpty() || successful.size() == 0) {
......
......@@ -17,7 +17,7 @@ import ca.corefacility.bioinformatics.irida.security.ProjectSynchronizationAuthe
/**
* Confirms that a given user is the owner of a project
*
*
*
*/
@Component
......@@ -26,7 +26,7 @@ public class ProjectOwnerPermission extends ModifyProjectPermission {
private static final String PERMISSION_PROVIDED = "isProjectOwner";
/**
* Construct an instance of {@link ReadProjectPermission}.
* Construct an instance of {@link ProjectOwnerPermission}.
*
* @param projectRepository the project repository.
* @param userRepository the user repository.
......
......@@ -153,23 +153,38 @@ public interface ProjectService extends CRUDService<Long, Project> {
public ProjectSampleJoin moveSampleBetweenProjects(Project source, Project destination, Sample sample, boolean owner);
/**
* Copy or move a list of {@link Sample} between 2 {@link Project}
* Share a list of {@link Sample}s between two {@link Project}s.
*
* @param source
* the source {@link Project}
* @param destination
* the {@link Project} being copied to
* the {@link Project} being shared into
* @param samples
* a collection of {@link Sample}
* @param move
* boolean whether to move or copy. true for move
* @param giveOwner
* whether to give ownership rights to the destination
* {@link Project}
* @return a list of new {@link ProjectSampleJoin}
*/
public List<ProjectSampleJoin> copyOrMoveSamples(Project source, Project destination, Collection<Sample> samples,
boolean move, boolean giveOwner);
public List<ProjectSampleJoin> shareSamples(Project source, Project destination, Collection<Sample> samples,
boolean giveOwner);
/**
* Move a list of {@link Sample}s between 2 {@link Project}
*
* @param source
* the source {@link Project}
* @param destination
* the {@link Project} being moved to
* @param samples
* a collection of {@link Sample}s
* @param giveOwner
* whether to give ownership rights to the destination
* {@link Project}
* @return a list of new {@link ProjectSampleJoin}
*/
public List<ProjectSampleJoin> moveSamples(Project source, Project destination, Collection<Sample> samples,
boolean giveOwner);
/**
* Remove the specified {@link Sample} from the {@link Project}. The {@link Sample} will also be deleted from the
......
......@@ -415,27 +415,46 @@ public class ProjectServiceImpl extends CRUDServiceImpl<Long, Project> implement
@Override
@Transactional
@LaunchesProjectEvent(SampleAddedProjectEvent.class)
@PreAuthorize("hasPermission(#source, 'isProjectOwner') " + "and hasPermission(#destination, 'isProjectOwner') "
+ "and hasPermission(#samples, 'canReadSample') "
+ "and ((not #giveOwner) or hasPermission(#samples, 'canUpdateSample') )")
public List<ProjectSampleJoin> copyOrMoveSamples(Project source, Project destination, Collection<Sample> samples,
boolean move, boolean giveOwner) {
@PreAuthorize("hasPermission(#source, 'canManageLocalProjectSettings')"
+ " and hasPermission(#destination, 'isProjectOwner')"
+ " and hasPermission(#samples, 'canReadSample')"
+ " and ((not #giveOwner) or hasPermission(#samples, 'canUpdateSample'))")
public List<ProjectSampleJoin> shareSamples(Project source, Project destination, Collection<Sample> samples,
boolean giveOwner) {
List<ProjectSampleJoin> newJoins = new ArrayList<>();
for (Sample sample : samples) {
ProjectSampleJoin newJoin = addSampleToProject(destination, sample, giveOwner);
logger.trace("Shared sample " + sample.getId() + " to project " + destination.getId());
ProjectSampleJoin newJoin;
if (move) {
newJoin = moveSampleBetweenProjects(source, destination, sample, giveOwner);
} else {
newJoin = addSampleToProject(destination, sample, giveOwner);
}
newJoins.add(newJoin);
}
return newJoins;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional
@LaunchesProjectEvent(SampleAddedProjectEvent.class)
@PreAuthorize("hasPermission(#source, 'isProjectOwner') and hasPermission(#destination, 'isProjectOwner') "
+ "and hasPermission(#samples, 'canReadSample') "
+ "and ((not #giveOwner) or hasPermission(#samples, 'canUpdateSample') )")
public List<ProjectSampleJoin> moveSamples(Project source, Project destination, Collection<Sample> samples,
boolean giveOwner) {
logger.trace("Copied sample " + sample.getId() + " to project " + destination.getId());
List<ProjectSampleJoin> newJoins = new ArrayList<>();
newJoins.add(newJoin);
for (Sample sample : samples) {
ProjectSampleJoin newJoin = moveSampleBetweenProjects(source, destination, sample, giveOwner);
logger.trace("Moved sample " + sample.getId() + " to project " + destination.getId());
newJoins.add(newJoin);
}
return newJoins;
......@@ -446,7 +465,7 @@ public class ProjectServiceImpl extends CRUDServiceImpl<Long, Project> implement
*/
@Override
@Transactional
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'isProjectOwner')")
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'canManageLocalProjectSettings')")
@LaunchesProjectEvent(SampleRemovedProjectEvent.class)
public void removeSampleFromProject(Project project, Sample sample) {
ProjectSampleJoin readSampleForProject = psjRepository.readSampleForProject(project, sample);
......@@ -463,7 +482,7 @@ public class ProjectServiceImpl extends CRUDServiceImpl<Long, Project> implement
*/
@Override
@Transactional
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'isProjectOwner')")
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'canManageLocalProjectSettings')")
@LaunchesProjectEvent(SampleRemovedProjectEvent.class)
public void removeSamplesFromProject(Project project, Iterable<Sample> samples) {
for (Sample s : samples) {
......@@ -605,7 +624,7 @@ public class ProjectServiceImpl extends CRUDServiceImpl<Long, Project> implement
* {@inheritDoc}
*/
@Override
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#p, 'isProjectOwner')")
@PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#p, 'canManageLocalProjectSettings')")
public Page<Project> getUnassociatedProjects(final Project p, final String searchName, final Integer page, final Integer count,
final Direction sortDirection, final String... sortedBy) {
......
......@@ -581,6 +581,8 @@ project.samples.move-success=Successfully moved {0} sample(s).
project.samples.copy.project=New Project
project.samples.copy.sample-exists=Sample {0} was not shared as it already exists in {1}.
project.samples.move.sample-exists=Sample {0} was not moved as it already exists in {1}.
project.samples.copy.sample-denied=Permission denied when sharing samples to {0}.
project.samples.move.sample-denied=Permission denied when moving samples to {0}.
project.samples.copy.no-project-selected=Project must be selected.
project.samples.run-pipeline=Run Pipeline
project.samples.run-modal.title=Select a Pipeline to Run
......@@ -597,6 +599,10 @@ project.samples.tooltip.share.requires-more-than-one=Select at least one sample
project.samples.tooltip.move.requires-more-than-one=Select at least one sample from this project to move to another project.
project.samples.tooltip.remove.requires-more-than-one=Select one or more samples to remove from this project.
project.samples.tooltip.download.requires-more-than-one=Select one or more samples to download from this project.
project.samples.tooltip.merge.remote-project=Merging samples cannot be performed in a remote project.
project.samples.tooltip.move.remote-project=Moving a remote sample cannot be performed.
project.samples.tooltip.import-metadata.remote-project=Cannot import metadata for a remote project.
project.samples.tooltip.new-sample.remote-project=Cannot add new sample to a remote project.
project.samples.tooltip.ncbi.requires-more-than-one=Select one or more samples from this project to upload to the NCBI SRA.
project.samples.only-for-project-tooltip=This cannot be used when associated projects are displayed.
project.samples.locked-title=Sample is locked from modification
......@@ -653,6 +659,7 @@ project.samples.modal.copy.select-label-plural=Select a project to share the sam
project.samples.modal.copy.complete=Share Samples
project.samples.modal.copy.locked=Shared samples will be non-modifiable as one or more locked sample are selected.
project.samples.modal.copy.lock-copied=Allow modification access to samples in the new project.
project.samples.modal.copy.remote=All samples will be read only as this project is a remote project.
project.samples.modal.move.title-plural=Move Sample
......
......@@ -10,6 +10,7 @@
<link rel="stylesheet" th:href="@{/resources/dist/css/project-samples-filter.bundle.css}">
<script th:inline="javascript">
var PAGE = {
isRemoteProject: /*[[${project.isRemote()}]]*/ false,
linker: /*[[${linker}]]*/'',
urls: {
projects: {
......@@ -57,8 +58,8 @@
<div class="row">
<div class="col-md-12">
<div class="btn-toolbar dt-btn-toolbar" role="toolbar">
<div class="btn-group" th:if="${isOwner || isAdmin}">
<div th:if="${isOwner || isAdmin}" class="dropdown">
<div class="btn-group" th:if="${isOwnerAllowRemote}">
<div class="dropdown">
<button id="sample-tools" class="btn btn-default btn-sm dropdown-toggle t-sample-tools"
data-toggle="dropdown">
<th:block th:text="#{project.samples.nav.sample-tools}"/>
......@@ -69,6 +70,7 @@
<a href="#" class="js-sample-tool-btn t-merge-btn"
data-enabled-at="2" data-toggle="tooltip"
data:enabled-msg="#{project.samples.tooltip.merge.requires-more-than-one}"
data:remote-msg="#{project.samples.tooltip.merge.remote-project}"
data:associated-msg="#{project.samples.only-for-project-tooltip}"
data:url="@{/projects/{projectId}/templates/merge-modal(projectId=${project.getId()})}"
data:script="@{/resources/dist/js/project-samples-merge.bundle.js}">
......@@ -93,6 +95,7 @@
data-enabled-at="1" data-params="{&quot;move&quot;: true}"
data-toggle="tooltip"
data:enabled-msg="#{project.samples.tooltip.move.requires-more-than-one}"
data:remote-msg="#{project.samples.tooltip.move.remote-project}"
data:associated-msg="#{project.samples.only-for-project-tooltip}"
data:url="@{/projects/{projectId}/templates/copy-move-modal(projectId=${project.getId()})}"
data:script="@{/resources/dist/js/project-samples-copy.bundle.js}">
......@@ -116,7 +119,11 @@
<!-- IMPORT METADATA LINK-->
<li role="menuitem">
<a th:href="@{/projects/{id}/sample-metadata/upload(id=${project.getId()})}">
<a href="#"
class="js-sample-project-tool-btn t-import-metadata-btn"
data:url="@{/projects/{id}/sample-metadata/upload(id=${project.getId()})}"
data-enabled-at="0" data-toggle="tooltip"
data:remote-msg="#{project.samples.tooltip.import-metadata.remote-project}">
<i class="fa fa-upload fa-fw spaced-right__sm" aria-hidden="true"></i>
<th:block th:text="#{project.nav.samples.import-metadata}"/>
</a>
......@@ -125,7 +132,11 @@
<!-- NEW SAMPLE LINK -->
<li role="menuitem">
<a th:href="@{/projects/{id}/samples/new(id=${project.getId()})}">
<a href="#"
class="js-sample-project-tool-btn t-new-sample-btn"
data:url="@{/projects/{id}/samples/new(id=${project.getId()})}"
data-enabled-at="0" data-toggle="tooltip"
data:remote-msg="#{project.samples.tooltip.new-sample.remote-project}">
<i class="fa fa-plus fa-fw spaced-right__sm" aria-hidden="true"></i>
<th:block th:text="#{project.samples.nav.new}"/>
</a>
......
......@@ -54,13 +54,18 @@
<input type="hidden" value="false" name="giveOwner" id="giveOwner" />
</div>
<div class="alert alert-warning" th:if="${isRemoteProject}">
<span th:text="#{project.samples.modal.copy.remote}">_All samples will be read only as this project is a remote project_</span>
</div>
<div th:if="${not isRemoteProject}">
<!-- Only the the allow modification checkbox for the copy not the move -->
<div class="form-group" th:if="${#lists.size(lockedSamples) == 0 and #strings.equals(type, 'copy')}">
<div class="checkbox">
<label>
<input type="checkbox" value="true" id="giveOwner" name="giveOwner"/>
<span th:text="#{project.samples.modal.copy.lock-copied}">_Allow modification in new project_</span>
</label>
<div class="form-group" th:if="${#lists.size(lockedSamples) == 0 and #strings.equals(type, 'copy')}">
<div class="checkbox">
<label>
<input type="checkbox" value="true" id="giveOwner" name="giveOwner"/>
<span th:text="#{project.samples.modal.copy.lock-copied}">_Allow modification in new project_</span>
</label>
</div>
</div>
</div>
......
......@@ -10,11 +10,9 @@ import $ from "jquery";
* Check the state of the calling button to see if it should be enabled or not.
* @param count the number if samples currently selected.
* @param hasAssociated if there are associated project currently displayed.
* @param isRemote If the project is a remote project.
*/
function checkState(count, hasAssociated) {
// Remove the tooltip. A new one will be created based on the
// information provided.
this.$node.tooltip("destroy");
function checkState(count, hasAssociated, isRemote) {
if (hasAssociated && this.$node.data("associatedMsg")) {
this.$node.parent().addClass("disabled");
// Activate the tooltip
......@@ -23,18 +21,26 @@ function checkState(count, hasAssociated) {
placement: "right",
title: this.$node.data("associatedMsg")
});
} else if (isRemote && this.$node.data("remoteMsg")) {
this.$node.parent().addClass("disabled");
// Activate the tooltip
this.$node.tooltip({
container: "body",
placement: "right",
title: this.$node.data("remoteMsg")
});
} else if (count < this.$node.data("enabledAt")) {
this.$node.parent().addClass("disabled");
// Activate the tooltip
this.$node.tooltip({
container: "body",
placement: "right",
title: this.$node.data("enabledMsg")
});
} else {
if (count < this.$node.data("enabledAt")) {
this.$node.parent().addClass("disabled");
// Activate the tooltip
this.$node.tooltip({
container: "body",
placement: "right",
title: this.$node.data("enabledMsg")
});
} else {
this.$node.parent().removeClass("disabled");
}
// Remove the tooltip.
this.$node.tooltip("destroy");
this.$node.parent().removeClass("disabled");
}
}
......@@ -94,15 +100,16 @@ export class SampleExportButton {
* Check the state the button should be in.
* @param {number} count current number of selected samples (defaults to 0)
* @param {boolean} hasAssociated if associated projects are displayed in the table.
* @param {boolean} isRemote Whether the project is a remote project.
*/
checkState(count = 0, hasAssociated = false) {
checkState.call(this, count, hasAssociated);
checkState(count = 0, hasAssociated = false, isRemote = false) {
checkState.call(this, count, hasAssociated, isRemote);
}
}
/**
* This class represents the state and function of buttons within the
* project > sample > Sample Tools dropdown menu.
* project > sample > Sample Tools dropdown menu which apply to individual samples.
*/
export class SampleDropdownButton {
/**
......@@ -130,8 +137,45 @@ export class SampleDropdownButton {
* number of currently selected samples
* @param {number} count of selected samples
* @param {boolean} hasAssociated whether associated projects are being displayed.
* @param {boolean} isRemote Whether the project is a remote project.
*/
checkState(count = 0, hasAssociated = false, isRemote = false) {
checkState.call(this, count, hasAssociated, isRemote);
}
}
/**
* This class represents the state and function of buttons within the
* project > sample > Sample Tools dropdown menu which apply to the entire project.
*/
export class SampleProjectDropdownButton {
/**
* Link the dom and the EventListener for a button.
* @param {node} node - actual button DOM node.
*/
constructor(node, isRemote) {
const btn = this;
this.$node = $(node);
this.$node.on("click", function() {
btn.clickHandler();
});
this.checkState(undefined, undefined, isRemote);
}
clickHandler() {
if (!this.$node.parent().hasClass("disabled")) {
window.location.href = this.$node.attr("data-url");
}
}
/**
* Check to see if the button should be disabled based upon type of project.
* @param {number} count of selected samples
* @param {boolean} hasAssociated whether associated projects are being displayed.
* @param {boolean} isRemote Whether the project is a remote project.
*/
checkState(count = 0, hasAssociated = false) {
checkState.call(this, count, hasAssociated);
checkState(count = 0, hasAssociated = false, isRemote = false) {
checkState.call(this, count, hasAssociated, isRemote);
}
}
......@@ -14,7 +14,8 @@ import { CART } from "../../../utilities/events-utilities";
import {
SampleCartButton,
SampleDropdownButton,
SampleExportButton
SampleExportButton,
SampleProjectDropdownButton
} from "./SampleButtons";
import { FILTERS, SAMPLE_EVENTS } from "./constants";
import { download } from "../../../utilities/file.utilities";
......@@ -38,6 +39,8 @@ const POPOVER_OPTIONS = {
template: $("#popover-template").clone()
};
const IS_REMOTE_PROJECT = window.PAGE.isRemoteProject;
/*
Initialize the sample tools menu. This is used to check the status of the buttons.
*/
......@@ -93,6 +96,11 @@ const EXPORT_HANDLERS = {
);
}
});
[...document.querySelectorAll(".js-sample-project-tool-btn")].forEach(elm => {
SAMPLE_TOOL_BUTTONS.push(
new SampleProjectDropdownButton(elm, IS_REMOTE_PROJECT)
);
});
/*
Initialize the add to cart button
......@@ -316,7 +324,7 @@ function checkToolButtonState(count = $dt.select.selected()[0].size) {
Update the state of the buttons in the navbar.
*/
for (const btn of SAMPLE_TOOL_BUTTONS) {
btn.checkState(count, ASSOCIATED_PROJECTS.size > 0);
btn.checkState(count, ASSOCIATED_PROJECTS.size > 0, IS_REMOTE_PROJECT);
}
}
......
......@@ -6,6 +6,7 @@ import java.util.stream.Collectors;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
......@@ -243,7 +244,7 @@ public class ProjectSamplesPage extends ProjectPageBase {
return isAnchorElementEnabled(mergeBtn);
}
public boolean isCopyBtnEnabled() {
public boolean isShareBtnEnabled() {
return isAnchorElementEnabled(copyBtn);
}
......@@ -336,13 +337,18 @@ public class ProjectSamplesPage extends ProjectPageBase {
mergeBtnOK.click();
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("merge-modal")));
}
public void waitUntilShareButtonVisible() {
WebDriverWait wait = openToolsDropdownAndWait();
wait.until(ExpectedConditions.visibilityOf(copyBtn));
}
public void copySamples(String project, boolean owner) {
public void shareSamples(String project, boolean owner) {
WebDriverWait wait = openToolsDropdownAndWait();
wait.until(ExpectedConditions.visibilityOf(copyBtn));
copyBtn.click();
copyMoveSamples(project, owner);
shareMoveSamples(project, owner);
}
public void moveSamples(String projectNum) {
......@@ -350,7 +356,7 @@ public class ProjectSamplesPage extends ProjectPageBase {
wait.until(ExpectedConditions.visibilityOf(moveBtn));
moveBtn.click();
// Setting owner to false because we removed the checkbox from the move.
copyMoveSamples(projectNum, false);
shareMoveSamples(projectNum, false);
}
private void openFilterModal() {
......@@ -425,19 +431,22 @@ public class ProjectSamplesPage extends ProjectPageBase {
// Wait for select2 to be open properly.
waitForTime(500);
sendInputTextSlowly(value, select2Input);
WebDriverWait wait = new WebDriverWait(driver, 10);
// Wait needed to allow select2 to populate.
waitForTime(500);
select2Input.sendKeys(Keys.RETURN);
}
private void copyMoveSamples(String project, boolean owner) {
private void shareMoveSamples(String project, boolean owner) {
WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.visibilityOf(copySamplesModal));
enterSelect2Value(project);
if(owner){
giveOwnerBtn.click();
if(owner) {
try {
giveOwnerBtn.click();
} catch (NoSuchElementException e) {
throw new GiveOwnerNotDisplayedException();
}
}
wait.until(ExpectedConditions.elementToBeClickable(copyModalConfirmBtn));
......@@ -465,4 +474,13 @@ public class ProjectSamplesPage extends ProjectPageBase {
}
return locked;
}
/**
* Exception which is thrown when attempting to give owner to a sample
* during copy/move and button is not displayed. Used for verifying no give
* owner button when copying remote samples.
*/
@SuppressWarnings("serial")
public static class GiveOwnerNotDisplayedException extends RuntimeException {
}
}
package ca.corefacility.bioinformatics.irida.ria.integration.pipelines;
import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver;
import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage;
import ca.corefacility.bioinformatics.irida.ria.integration.pages.pipelines.PipelinesPhylogenomicsPage;
import ca.corefacility.bioinformatics.irida.ria.integration.pages.pipelines.PipelinesSelectionPage;
import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSamplesPage;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;