diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java index 15e7ac53f..a5be5530b 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java @@ -1,9 +1,10 @@ package life.qbic.projectmanagement.application; -import static java.util.Objects.*; +import static java.util.Objects.requireNonNull; +import static life.qbic.logging.service.LoggerFactory.logger; -import java.util.Objects; import life.qbic.application.commons.Result; +import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.sample.SampleInformationService; import life.qbic.projectmanagement.domain.project.experiment.ExperimentId; import org.springframework.beans.factory.annotation.Autowired; @@ -19,8 +20,8 @@ @Service public class DeletionService { + private static final Logger log = logger(DeletionService.class); private final ExperimentInformationService experimentInformationService; - private final SampleInformationService sampleInformationService; @Autowired @@ -47,13 +48,27 @@ public Result deleteAllExperimentalVariables(Experim if (queryResult.isError()) { return Result.fromError(ResponseCode.QUERY_FAILED); } - if (queryResult.isValue() && queryResult.getValue().size() > 0) { + if (queryResult.isValue() && !queryResult.getValue().isEmpty()) { return Result.fromError(ResponseCode.SAMPLES_STILL_ATTACHED_TO_EXPERIMENT); } experimentInformationService.deleteAllExperimentalVariables(id); return Result.fromValue(id); } + public Result deleteAllExperimentalGroups(ExperimentId id) { + var queryResult = sampleInformationService.retrieveSamplesForExperiment(id); + if (queryResult.isError()) { + log.debug("experiment (%s) converting %s to %s".formatted(id, queryResult.getError(), + ResponseCode.QUERY_FAILED)); + return Result.fromError(ResponseCode.QUERY_FAILED); + } + if (queryResult.isValue() && !queryResult.getValue().isEmpty()) { + return Result.fromError(ResponseCode.SAMPLES_STILL_ATTACHED_TO_EXPERIMENT); + } + experimentInformationService.deleteAllExperimentalGroups(id); + return Result.fromValue(id); + } + public enum ResponseCode { SAMPLES_STILL_ATTACHED_TO_EXPERIMENT, QUERY_FAILED } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java index d8fcfe72e..7af7ebc21 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.Result; import life.qbic.logging.api.Logger; @@ -74,7 +73,7 @@ public Result addExperimentalGroupToExperiment( Experiment activeExperiment = loadExperimentById(experimentId); Result result = activeExperiment.addExperimentalGroup( - experimentalGroup.levels(), experimentalGroup.sampleSize()); + experimentalGroup.levels(), experimentalGroup.replicateCount()); if (result.isValue()) { experimentRepository.update(activeExperiment); } @@ -100,15 +99,9 @@ public List experimentalGroupsFor(ExperimentId experimentId) return experiment.getExperimentalGroups().stream().toList(); } - public void deleteExperimentGroup(ExperimentId experimentId, long groupId) { - Experiment experiment = loadExperimentById(experimentId); - experiment.removeExperimentGroup(groupId); - experimentRepository.update(experiment); - } - /** - * ATTENTION! This will remove all existing experimental variables and all defined experimental - * groups in a give experiment! + * ATTENTION! This will remove all existing experimental variables and all defined + * experimental groups in a give experiment! * * @param experimentId the experiment reference to delete the experimental variables from * @since 1.0.0 @@ -119,6 +112,7 @@ public void deleteAllExperimentalVariables(ExperimentId experimentId) { experiment.removeAllExperimentalVariables(); experimentRepository.update(experiment); } + /** * Returns a list of experiment for a given project. * @@ -261,19 +255,62 @@ public List getVariablesOfExperiment(ExperimentId experime } /** - * Checks if the provided ExperimentId contains an experimental Group + * Deletes all experimental groups in a given experiment. + *

+ * This method does not check if samples are already. * - * @param experimentId the {@link ExperimentId} of the {@link Experiment} which should be checked - * if it contains an {@link ExperimentalGroup} - * @return a boolean indicating if the experiment contains an {@link ExperimentalGroup} + * @param id the experiment identifier of the experiment the experimental groups are going to be + * deleted. + * @since 1.0.0 */ - public boolean hasExperimentalGroup(ExperimentId experimentId) { - Experiment experiment = loadExperimentById(experimentId); - return !experiment.getExperimentalGroups().isEmpty(); + public void deleteAllExperimentalGroups(ExperimentId id) { + Experiment experiment = loadExperimentById(id); + experiment.removeAllExperimentalGroups(); + experimentRepository.update(experiment); } - public record ExperimentalGroupDTO(Set levels, int sampleSize) { + /** + * Adds experimental groups to an experiment + * + * @param experimentId the experiment to add the groups to + * @param experimentalGroupDTOS the group information + * @return either the collection of added groups or an appropriate response code + */ + public Result, ResponseCode> addExperimentalGroupsToExperiment( + ExperimentId experimentId, List experimentalGroupDTOS) { + Experiment experiment = loadExperimentById(experimentId); + List addedGroups = new ArrayList<>(); + for (ExperimentalGroupDTO experimentalGroupDTO : experimentalGroupDTOS) { + Result result = experiment.addExperimentalGroup( + experimentalGroupDTO.levels(), + experimentalGroupDTO.replicateCount()); + if (result.isError()) { + return Result.fromError(result.getError()); + } else { + addedGroups.add(result.getValue()); + } + } + experimentRepository.update(experiment); + return Result.fromValue(addedGroups); + } + public void editExperimentInformation(ExperimentId experimentId, String experimentName, + List species, List specimens, List analytes) { + Experiment experiment = loadExperimentById(experimentId); + experiment.setName(experimentName); + experiment.setSpecies(species); + experiment.setAnalytes(analytes); + experiment.setSpecimens(specimens); + experimentRepository.update(experiment); } + /** + * Information about an experimental group + * + * @param levels the levels in the condition of the group + * @param replicateCount the number of biological replicates + */ + public record ExperimentalGroupDTO(Collection levels, int replicateCount) { + + } } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/SampleRegisteredPolicy.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/SampleRegisteredPolicy.java index 527d12558..0842690ff 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/SampleRegisteredPolicy.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/SampleRegisteredPolicy.java @@ -22,7 +22,7 @@ public class SampleRegisteredPolicy { /** * Creates an instance of a {@link SampleRegisteredPolicy} object. *

- * All directives will be created an subscribed upon instantiation. + * All directives will be created and subscribed upon instantiation. * * @param addSampleToBatch directive to update the affected sample * {@link life.qbic.projectmanagement.domain.project.sample.Batch} diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/AddSampleToBatch.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/AddSampleToBatch.java index dd54c14ba..a15560fe3 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/AddSampleToBatch.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/AddSampleToBatch.java @@ -1,13 +1,17 @@ package life.qbic.projectmanagement.application.policy.directive; +import static life.qbic.logging.service.LoggerFactory.logger; + import life.qbic.domain.concepts.DomainEvent; import life.qbic.domain.concepts.DomainEventSubscriber; +import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.batch.BatchRegistrationService; import life.qbic.projectmanagement.domain.project.sample.BatchId; import life.qbic.projectmanagement.domain.project.sample.SampleId; import life.qbic.projectmanagement.domain.project.sample.event.SampleRegistered; +import org.jobrunr.jobs.annotations.Job; import org.jobrunr.scheduling.JobScheduler; - +import org.springframework.stereotype.Component; /** * Directive: Add Sample to Batch *

@@ -16,8 +20,10 @@ * * @since 1.0.0 */ +@Component public class AddSampleToBatch implements DomainEventSubscriber { + private static final Logger log = logger(AddSampleToBatch.class); private final BatchRegistrationService batchRegistrationService; private final JobScheduler jobScheduler; @@ -38,7 +44,8 @@ public void handleEvent(SampleRegistered event) { jobScheduler.enqueue(() -> addSampleToBatch(event.registeredSample(), event.assignedBatch())); } - protected void addSampleToBatch(SampleId sample, BatchId batch) throws RuntimeException { + @Job(name = "Add_Sample_To_Batch") + public void addSampleToBatch(SampleId sample, BatchId batch) throws RuntimeException { batchRegistrationService.addSampleToBatch(sample, batch).onError(responseCode -> { throw new RuntimeException( String.format("Adding sample %s to batch %s failed, response code was %s ", sample, batch, diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/CreateNewSampleStatisticsEntry.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/CreateNewSampleStatisticsEntry.java index 39c4eaa24..d966ba993 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/CreateNewSampleStatisticsEntry.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/policy/directive/CreateNewSampleStatisticsEntry.java @@ -12,6 +12,7 @@ import life.qbic.projectmanagement.domain.project.ProjectId; import life.qbic.projectmanagement.domain.project.event.ProjectRegisteredEvent; import life.qbic.projectmanagement.domain.project.repository.ProjectRepository; +import org.jobrunr.jobs.annotations.Job; import org.jobrunr.scheduling.JobScheduler; import org.springframework.stereotype.Component; @@ -50,6 +51,7 @@ public void handleEvent(ProjectRegisteredEvent event) { jobScheduler.enqueue(() -> createSampleStatisticsEntry(event.createdProject())); } + @Job(name = "Create_Sample_Statistics_Entry") public void createSampleStatisticsEntry(String projectId) throws RuntimeException { var id = ProjectId.parse(projectId); if (sampleStatisticsEntryMissing(id)) { diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/sample/SampleRegistrationService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/sample/SampleRegistrationService.java index b24b65d86..1d990fa2a 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/sample/SampleRegistrationService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/sample/SampleRegistrationService.java @@ -50,7 +50,7 @@ public Result, ResponseCode> registerSamples( Collection sampleRegistrationRequests, ProjectId projectId) { Objects.requireNonNull(sampleRegistrationRequests); Objects.requireNonNull(projectId); - if (sampleRegistrationRequests.size() < 1) { + if (sampleRegistrationRequests.isEmpty()) { return Result.fromError(ResponseCode.NO_SAMPLES_DEFINED); } Map sampleCodesToRegistrationRequests = new HashMap<>(); diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/BiologicalReplicate.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/BiologicalReplicate.java index 184310a1b..e72e7a85e 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/BiologicalReplicate.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/BiologicalReplicate.java @@ -5,6 +5,7 @@ import jakarta.persistence.Entity; import java.io.Serial; import java.io.Serializable; +import java.util.Comparator; import java.util.Objects; /** @@ -102,4 +103,22 @@ public boolean equals(Object o) { public String toString() { return "BiologicalReplicate{" + "id=" + id + ", label='" + label + '\'' + '}'; } + + /** + * Provides sorting functionality for labels ending in numbers, e.g. label1 < label2 < label10. + * This is based on label length and only works for labels starting with the same letters. + */ + public static class LexicographicLabelComparator implements Comparator { + + @Override + public int compare(BiologicalReplicate r1, BiologicalReplicate r2) { + int l1 = r1.label.length(); + int l2 = r2.label.length(); + if (l1 == l2) { + return r1.label.compareTo(r2.label); + } else { + return l1-l2; + } + } + } } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/Experiment.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/Experiment.java index 149fd5623..555fea1ca 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/Experiment.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/Experiment.java @@ -9,7 +9,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Set; +import life.qbic.application.commons.ApplicationException; +import life.qbic.application.commons.ApplicationException.ErrorCode; +import life.qbic.application.commons.ApplicationException.ErrorParameters; import life.qbic.application.commons.Result; import life.qbic.projectmanagement.domain.project.experiment.ExperimentalDesign.AddExperimentalGroupResponse.ResponseCode; import life.qbic.projectmanagement.domain.project.experiment.exception.ConditionExistsException; @@ -40,6 +42,7 @@ public class Experiment { @Embedded private ExperimentalDesign experimentalDesign; + @ElementCollection(targetClass = Analyte.class) private List analytes = new ArrayList<>(); @ElementCollection(targetClass = Species.class) @@ -259,4 +262,50 @@ public List getExperimentalGroups() { public void removeExperimentGroup(long groupId) { experimentalDesign.removeExperimentalGroup(groupId); } + + /** + * Sets the name of the experiment. + */ + public void setName(String name) { + if (name.isEmpty()) { + throw new ApplicationException("An Experiment must have a name"); + } + this.name = name; + } + + /** + * Sets the list of {@link Species}for an experiment. + */ + public void setSpecies( + List species) { + if (species == null || species.isEmpty()) { + throw new ApplicationException(ErrorCode.NO_SPECIES_DEFINED, + ErrorParameters.of(species)); + } + this.species = species; + } + + /** + * Sets the list of {@link Species} for an experiment. + */ + public void setSpecimens( + List specimens) { + if (specimens == null || specimens.isEmpty()) { + throw new ApplicationException(ErrorCode.NO_SPECIMEN_DEFINED, + ErrorParameters.of(specimens)); + } + this.specimens = specimens; + } + + /** + * Sets the list of {@link Analyte} for an experiment. + */ + public void setAnalytes( + List analytes) { + if (analytes == null || analytes.isEmpty()) { + throw new ApplicationException(ErrorCode.NO_ANALYTE_DEFINED, + ErrorParameters.of(analytes)); + } + this.analytes = analytes; + } } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java index 6279fa5ec..bb9de9f74 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java @@ -177,7 +177,7 @@ boolean isConditionDefined(Condition condition) { } Result addVariable(String variableName, List levels) { - if (levels.size() < 1) { + if (levels.isEmpty()) { return Result.fromError(new IllegalArgumentException( "No levels were defined for " + variableName)); } @@ -197,7 +197,7 @@ Result addVariable(String variableName, List 0) { + if (!experimentalGroups.isEmpty()) { throw new IllegalStateException("Cannot delete experimental variables referenced by an experimental group."); } this.variables.clear(); diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/Batch.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/Batch.java index 4322ffd86..c6fee1d60 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/Batch.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/Batch.java @@ -1,9 +1,12 @@ package life.qbic.projectmanagement.domain.project.sample; +import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -21,6 +24,7 @@ public class Batch { @EmbeddedId + @Column(name = "id") private BatchId id; @Column(name = "batchLabel") @@ -29,7 +33,8 @@ public class Batch { @Column(name = "isPilot") private boolean pilot; - @ElementCollection(targetClass = SampleId.class) + @ElementCollection(targetClass = SampleId.class, fetch = FetchType.EAGER) + @CollectionTable(name = "sample_batches_sampleid", joinColumns = @JoinColumn(name = "batch_id")) private List sampleIds; protected Batch() { diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/SampleId.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/SampleId.java index 244dbe0b3..e723abc27 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/SampleId.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/sample/SampleId.java @@ -4,12 +4,12 @@ import jakarta.persistence.AccessType; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; - import java.io.Serial; import java.io.Serializable; import java.util.Objects; import java.util.StringJoiner; import java.util.UUID; +import org.springframework.lang.NonNull; /** * Unique sample identifier. Identifies a sample unambiguously in Tuebingen's @@ -21,6 +21,7 @@ public class SampleId implements Serializable { @Serial private static final long serialVersionUID = 1841536150220843163L; + @NonNull @Column(name = "sample_id") private final String uuid; diff --git a/vaadinfrontend/frontend/themes/datamanager/components/card.css b/vaadinfrontend/frontend/themes/datamanager/components/card.css index 33d0b2a18..afdbd542d 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/card.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/card.css @@ -86,10 +86,30 @@ } .experimental-group-card-collection { + display: flex; + align-content: space-evenly; + flex-flow: column; + gap: 1rem; +} + +.experimental-group-card-collection .groups-header { + justify-content: space-between; + display: flex; +} + +.experimental-group-card-collection .groups-content { display: flex; align-content: space-evenly; flex-flow: row wrap; gap: 1rem; + flex-direction: row; +} + +.experimental-group-card-collection .groups-controls { + display: flex; + align-content: space-evenly; + flex-flow: row wrap; + gap: 0.5rem; } .experimental-group .header { @@ -165,7 +185,7 @@ padding: var(--lumo-space-l); } -.experimental-variables .header { +.card.experimental-variables .header { color: var(--lumo-header-text-color); font-size: var(--lumo-font-size-m); font-weight: bold; @@ -174,7 +194,7 @@ align-items: baseline; } -.experimental-variables .content { +.card.experimental-variables .content { display: flex; align-content: space-evenly; flex-flow: row wrap; diff --git a/vaadinfrontend/frontend/themes/datamanager/components/custom.css b/vaadinfrontend/frontend/themes/datamanager/components/custom.css index 3659f15c4..54d75cdff 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/custom.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/custom.css @@ -33,3 +33,11 @@ .error-text { color: var(--lumo-error-text-color); } + +vaadin-multi-select-combo-box::part(toggle-button)::before { + color: var(--lumo-primary-color); +} + +vaadin-number-field::part(decrease-button), vaadin-number-field::part(increase-button) { + color: var(--lumo-primary-color); +} diff --git a/vaadinfrontend/frontend/themes/datamanager/components/dialog.css b/vaadinfrontend/frontend/themes/datamanager/components/dialog.css index 62a0cd323..eb6b842e5 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/dialog.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/dialog.css @@ -2,6 +2,22 @@ opacity: 0.05; } +vaadin-dialog-overlay::part(content) { + padding: 1rem 4rem 3rem 4rem; +} + +vaadin-dialog-overlay::part(footer) { + padding: 1rem 4rem 1rem 0; +} + +vaadin-dialog-overlay::part(header) { + padding: 2rem 1rem 1rem 4rem; +} + +vaadin-dialog-overlay::part(title) { + margin-inline-start: 0; +} + .batch-registration-dialog .stepper { width: 100%; height: 100%; @@ -117,7 +133,7 @@ font-weight: bold; } -.create-experiment-dialog::part(overlay) { +.experiment-information-dialog::part(overlay) { width: 66vw; } @@ -146,36 +162,55 @@ Used by both project creation and experiment creation dialog, thus the unique na width: 66%; } -.experiment-group-dialog .group-input { - width: 100%; +.experiment-group-dialog .number-field { + min-width: 175px; } -.experiment-group-dialog .group-input .layout { - display: flex; - flex-direction: row; - gap: var(--lumo-space-m); +.experiment-group-dialog .experimental-group-entry { + display: grid; + grid: auto auto auto / 1fr 175px auto; + column-gap: 1rem; + align-items: baseline; } -.experiment-group-dialog .group-input .layout .combo-box { - width: 100%; +.experiment-group-dialog vaadin-icon{ + cursor: pointer; + color: var(--lumo-primary-color); } -.experiment-group-dialog .group-input .layout .number-field { - width: 150px; +.experiment-group-dialog .header{ + font-weight: bold; } .experiment-variable-dialog .content { - padding: var(--lumo-space-m); - gap: var(--lumo-space-m); display: flex; flex-direction: column; } +.experiment-group-dialog .add-new-group-action { + display: flex; + column-gap: 1rem; + color: var(--lumo-primary-color) +} + +.experiment-group-dialog .content { + display: grid; + gap: 1.5rem; +} + +.experiment-group-dialog .content .group-collection { + display: grid; + gap: 1rem; +} + + +.experiment-group-dialog .add-new-group-action span{ + cursor: pointer; +} + .experiment-variable-dialog .content .variables { display: flex; flex-direction: column; - gap: var(--lumo-space-m); - padding: var(--lumo-space-m); } .experiment-variable-dialog .content .variables .header { @@ -185,9 +220,10 @@ Used by both project creation and experiment creation dialog, thus the unique na .experiment-variable-dialog .content .row { display: flex; align-items: center; - gap: var(--lumo-space-m); + gap: var(--lumo-space-l); } .experiment-variable-dialog .content .row vaadin-icon { cursor: pointer; + color: var(--lumo-primary-color); } diff --git a/vaadinfrontend/frontend/themes/datamanager/components/page-area.css b/vaadinfrontend/frontend/themes/datamanager/components/page-area.css index 3e3a99125..20b8fa92e 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/page-area.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/page-area.css @@ -42,7 +42,13 @@ gap: var(--lumo-space-xs); } -.page-area.experiment-details-component .details-content { +.page-area.experiment-details-component .header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.page-area.experiment-details-component .content { padding: var(--lumo-space-m); display: flex; flex-direction: column; diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java new file mode 100644 index 000000000..94c145d5d --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java @@ -0,0 +1,30 @@ +package life.qbic.datamanager.views.general; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEvent; +import java.io.Serial; + +/** + * Add event + *

+ * Indicates that a user wants to add information to a component + * + * @since 1.0.0 + */ +public class AddEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = 9114334039868158765L; + + /** + * Creates a new event using the given source and indicator whether the event originated from the + * client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public AddEvent(T source, boolean fromClient) { + super(source, fromClient); + } +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/EditEvent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/EditEvent.java new file mode 100644 index 000000000..45291ca44 --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/EditEvent.java @@ -0,0 +1,31 @@ +package life.qbic.datamanager.views.general; + + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEvent; +import java.io.Serial; + +/** + * Edit Event + * + *

Indicates that a user wants to edit information in a component.

+ * + * @since 1.0.0 + */ +public class EditEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = -1033061739347133861L; + + /** + * Creates a new event using the given source and indicator whether the event originated from the + * client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public EditEvent(T source, boolean fromClient) { + super(source, fromClient); + } +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/create/DefineExperimentComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/create/DefineExperimentComponent.java index 52db21013..6ab831654 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/create/DefineExperimentComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/create/DefineExperimentComponent.java @@ -1,17 +1,15 @@ package life.qbic.datamanager.views.projects.create; -import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.Composite; import com.vaadin.flow.component.HasValidation; import com.vaadin.flow.component.HasValue; import com.vaadin.flow.component.combobox.MultiSelectComboBox; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.Binder; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -95,6 +93,16 @@ public void reset() { experimentDefinitionLayoutHandler.reset(); } + public void setExperimentInformation(String experimentName, Collection species, + Collection specimen, + Collection analytes) { + experimentNameField.setValue(experimentName); + speciesBox.setValue(species); + specimenBox.setValue(specimen); + analyteBox.setValue(analytes); + + } + private final class ExperimentDefinitionLayoutHandler { private final List> binders = new ArrayList<>(); diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentContentComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentContentComponent.java index aaaa7082f..6609246c7 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentContentComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentContentComponent.java @@ -1,13 +1,16 @@ package life.qbic.datamanager.views.projects.project.experiments; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.UIScope; import java.io.Serial; import life.qbic.datamanager.views.Context; import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentDetailsComponent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentEditEvent; import life.qbic.logging.api.Logger; import life.qbic.logging.service.LoggerFactory; +import life.qbic.projectmanagement.domain.project.experiment.Experiment; import org.springframework.beans.factory.annotation.Autowired; /** @@ -48,4 +51,12 @@ public void setContext(Context context) { experimentDetailsComponent.setContext(context); } + /** + * Propagates the listener which will retrieve notification if a an {@link Experiment} was edited + * in the {@link ExperimentDetailsComponent} within this container + */ + public void addExperimentEditListener( + ComponentEventListener experimentEditListener) { + experimentDetailsComponent.addExperimentEditEventListener(experimentEditListener); + } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentInformationMain.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentInformationMain.java index b5b63d9af..04301d406 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentInformationMain.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentInformationMain.java @@ -54,7 +54,6 @@ public class ExperimentInformationMain extends MainComponent implements BeforeEn private final ExperimentSupportComponent experimentSupportComponent; private final ProjectInformationService projectInformationService; private final ExperimentInformationService experimentInformationService; - private Context context; public ExperimentInformationMain( @@ -75,7 +74,7 @@ public ExperimentInformationMain( this.projectInformationService = projectInformationService; this.experimentInformationService = experimentInformationService; layoutComponent(); - + addListeners(); log.debug(String.format( "New instance for ExperimentInformationMain (#%s) created with ProjectNavigationBar Component (#%s), ExperimentMain component (#%s) and ExperimentSupport component (#%s)", System.identityHashCode(this), System.identityHashCode(projectNavigationBarComponent), @@ -128,7 +127,6 @@ public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { .with(parsedExperimentId); setContext(this.context); - addListeners(); } private ExperimentId activeExperiment(ProjectId parsedProjectId) { @@ -138,20 +136,21 @@ private ExperimentId activeExperiment(ProjectId parsedProjectId) { private void setContext(Context context) { experimentContentComponent.setContext(context); - experimentSupportComponent.projectId(context.projectId().orElseThrow()); - experimentSupportComponent.setExperiments( - experimentInformationService.findAllForProject(context.projectId().orElseThrow())); + experimentSupportComponent.setContext(context); experimentSupportComponent.setSelectedExperiment(context.experimentId().orElseThrow()); projectNavigationBarComponent.projectId(context.projectId().orElseThrow()); this.context = context; } - - private void addListeners() { experimentSupportComponent.addExperimentSelectionListener( event -> routeToExperiment(event.getSource().experimentId())); experimentSupportComponent.addExperimentCreationListener( event -> routeToExperiment(event.experimentId())); + experimentContentComponent.addExperimentEditListener( + event -> { + experimentSupportComponent.setContext(context); + experimentSupportComponent.setSelectedExperiment(event.experimentId()); + }); } private void forwardToExperiment(ExperimentId experimentId, BeforeEnterEvent beforeEnterEvent) { diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentListComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentListComponent.java index 94f26662b..32403612d 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentListComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentListComponent.java @@ -7,14 +7,15 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import life.qbic.application.commons.ApplicationException; +import life.qbic.application.commons.Result; +import life.qbic.datamanager.views.Context; import life.qbic.datamanager.views.general.PageArea; -import life.qbic.datamanager.views.notifications.ErrorMessage; import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.notifications.SuccessMessage; +import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentInformationDialog; import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentCreatedEvent; -import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentCreationContent; -import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentCreationDialog; -import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentCreationRequestedEvent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentInformationContent; import life.qbic.datamanager.views.support.experiment.ExperimentItem; import life.qbic.datamanager.views.support.experiment.ExperimentItemClickedEvent; import life.qbic.datamanager.views.support.experiment.ExperimentItemCollection; @@ -33,7 +34,7 @@ * {@link Experiment} information for all experiments in its {@link ExperimentItemCollection } * within the currently examined {@link life.qbic.projectmanagement.domain.project.Project}. * Additionally, it provides the possibility to create new experiments with its - * {@link ExperimentCreationDialog} and enables the user to select an experiment of interest via + * {@link ExperimentInformationDialog} and enables the user to select an experiment of interest via * clicking on the {@link ExperimentItem} associated with the experiment. Finally, it allows * components to be informed about a new experiment creation or selection via the * {@link ExperimentCreationListener} and {@link ExperimentSelectionListener} provided in this @@ -46,11 +47,12 @@ public class ExperimentListComponent extends PageArea { @Serial private static final long serialVersionUID = -2196400941684042549L; private final ExperimentItemCollection experimentItemCollection; - private ProjectId projectId; private final List selectionListeners = new ArrayList<>(); private final List creationListener = new ArrayList<>(); - private final ExperimentCreationDialog experimentCreationDialog; + private final transient ExperimentInformationService experimentInformationService; private final transient AddExperimentToProjectService addExperimentToProjectService; + private final transient ExperimentalDesignSearchService experimentalDesignSearchService; + private Context context; public ExperimentListComponent( @Autowired ExperimentInformationService experimentInformationService, @@ -60,68 +62,69 @@ public ExperimentListComponent( Objects.requireNonNull(addExperimentToProjectService); Objects.requireNonNull(experimentalDesignSearchService); this.addExperimentToProjectService = addExperimentToProjectService; - this.experimentCreationDialog = new ExperimentCreationDialog(experimentalDesignSearchService); + this.experimentInformationService = experimentInformationService; + this.experimentalDesignSearchService = experimentalDesignSearchService; this.addClassName("list-component"); this.experimentItemCollection = ExperimentItemCollection.create( "Add a new experiment"); this.add(experimentItemCollection); - addListeners(); - } - - private void addListeners() { addItemCollectionListeners(); - addExperimentCreationDialogListener(); - } - - private void addItemCollectionListeners() { - experimentItemCollection.addClickEventListener(this::fireExperimentalItemSelectedEvent); - experimentItemCollection.addCreateEventListener(event -> experimentCreationDialog.open()); } - private void addExperimentCreationDialogListener() { - experimentCreationDialog.addExperimentCreationEventListener( - event -> addExperimentToProject(event, event.getSource().content())); - experimentCreationDialog.addCancelEventListener( - event -> experimentCreationDialog.resetAndClose()); + public void setContext(Context context) { + ProjectId projectId = context.projectId() + .orElseThrow(() -> new ApplicationException("no project id in context " + context)); + context.experimentId() + .orElseThrow(() -> new ApplicationException("no experiment id in context " + context)); + this.context = context; + loadExperimentsForProject(projectId); } - private void addExperimentToProject( - ExperimentCreationRequestedEvent experimentCreationRequestedEvent, - ExperimentCreationContent experimentCreationContent) { - addExperimentToProjectService.addExperimentToProject(projectId, - experimentCreationContent.experimentName(), experimentCreationContent.species(), - experimentCreationContent.specimen(), experimentCreationContent.analytes()) - .onValue(experimentId -> { - displayExperimentCreationSuccess(); - fireExperimentCreatedEvent( - new ExperimentCreatedEvent(experimentCreationRequestedEvent.getSource(), experimentId, - experimentCreationRequestedEvent.isFromClient())); - experimentCreationDialog.resetAndClose(); - setSelectedExperiment(experimentId); - }).onError(error -> displayExperimentCreationFailure()); - } - - /** - * Provides the {@link ProjectId} of the currently selected project to this component - *

- * This method provides the {@link ProjectId} necessary for experiment creation within the - * {@link AddExperimentToProjectService} in this component. - */ - public void setProject(ProjectId projectId) { - this.projectId = projectId; + private void loadExperimentsForProject(ProjectId projectId) { + experimentItemCollection.removeAll(); + Collection foundExperiments = experimentInformationService.findAllForProject( + projectId); + foundExperiments.forEach(this::addExperimentToExperimentItemCollection); } - /** - * Provides the collection of {@link Experiment} to the components within this container - *

- * This method should be used to provide the experiments within a - * {@link life.qbic.projectmanagement.domain.project.Project} to {@link ExperimentListComponent} - */ - public void setExperiments(Collection experiments) { - experimentItemCollection.removeAll(); - addItemCollectionListeners(); - experiments.forEach(experiment -> experimentItemCollection.addExperimentItem( - ExperimentItem.create(experiment))); + private void addItemCollectionListeners() { + experimentItemCollection.addClickEventListener(this::fireExperimentalItemSelectedEvent); + experimentItemCollection.addCreateEventListener(event -> generateExperimentInformationDialog()); + } + + private void generateExperimentInformationDialog() { + var creationDialog = new ExperimentInformationDialog(experimentalDesignSearchService); + creationDialog.addCancelEventListener( + experimentInformationDialogCancelEvent -> creationDialog.close()); + creationDialog.addConfirmEventListener(experimentInformationDialogConfirmEvent -> { + ProjectId projectId = context.projectId().orElseThrow(); + ExperimentId createdExperiment = createExperiment(projectId, + experimentInformationDialogConfirmEvent.getSource() + .content()); + setSelectedExperiment(createdExperiment); + creationDialog.close(); + fireExperimentCreatedEvent( + new ExperimentCreatedEvent(creationDialog, createdExperiment, true)); + displayExperimentCreationSuccess(); + }); + creationDialog.open(); + } + + private ExperimentId createExperiment(ProjectId projectId, + ExperimentInformationContent experimentInformationContent) { + Result result = addExperimentToProjectService.addExperimentToProject( + projectId, + experimentInformationContent.experimentName(), experimentInformationContent.species(), + experimentInformationContent.specimen(), experimentInformationContent.analytes()); + if (result.isValue()) { + return result.getValue(); + } else { + throw new ApplicationException("Experiment Creation failed"); + } + } + + private void addExperimentToExperimentItemCollection(Experiment experiment) { + experimentItemCollection.addExperimentItem(ExperimentItem.create(experiment)); } /** @@ -177,12 +180,6 @@ private void displayExperimentCreationSuccess() { notification.open(); } - private void displayExperimentCreationFailure() { - ErrorMessage errorMessage = new ErrorMessage("Experiment Creation failed", ""); - StyledNotification notification = new StyledNotification(errorMessage); - notification.open(); - } - /** * Experiment Selection Interface *

diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentSupportComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentSupportComponent.java index e20d44d62..77b86b9af 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentSupportComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/ExperimentSupportComponent.java @@ -4,13 +4,12 @@ import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.UIScope; import java.io.Serial; -import java.util.Collection; import java.util.Objects; +import life.qbic.datamanager.views.Context; import life.qbic.datamanager.views.projects.project.experiments.ExperimentListComponent.ExperimentCreationListener; import life.qbic.datamanager.views.projects.project.experiments.ExperimentListComponent.ExperimentSelectionListener; import life.qbic.logging.api.Logger; import life.qbic.logging.service.LoggerFactory; -import life.qbic.projectmanagement.domain.project.ProjectId; import life.qbic.projectmanagement.domain.project.experiment.Experiment; import life.qbic.projectmanagement.domain.project.experiment.ExperimentId; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +26,6 @@ @SpringComponent @UIScope public class ExperimentSupportComponent extends Div { - @Serial private static final long serialVersionUID = -6996282848714468102L; private final ExperimentListComponent experimentListComponent; @@ -44,24 +42,12 @@ private void layoutComponent() { } /** - * Provides the {@link ProjectId} to the components within this container - *

- * This method serves as an entry point providing the necessary {@link ProjectId} to components - * within this component, so they can retrieve the information associated with the - * {@link ProjectId} - */ - public void projectId(ProjectId projectId) { - experimentListComponent.setProject(projectId); - } - - /** - * Provides the collection of {@link Experiment} to the components within this container - *

- * This method should be used to provide the experiments within a - * {@link life.qbic.projectmanagement.domain.project.Project} to {@link ExperimentListComponent} + * Propagates the context to internal components. + * + * @param context the context in which the user is. */ - public void setExperiments(Collection experiments) { - experimentListComponent.setExperiments(experiments); + public void setContext(Context context) { + experimentListComponent.setContext(context); } /** diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java deleted file mode 100644 index 8b10b23a4..000000000 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java +++ /dev/null @@ -1,100 +0,0 @@ -package life.qbic.datamanager.views.projects.project.experiments.experiment; - -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.data.binder.Binder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import life.qbic.datamanager.views.general.DialogWindow; -import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentalGroupInput.ExperimentalGroupBean; -import life.qbic.projectmanagement.domain.project.experiment.VariableLevel; - -/** - * Dialog allowing the addition of experimental groups. - * - *

Displays input/selection fields for the creation of a new experimental group and handles - * validation of said inputs.

- * - * @since 1.0.0 - */ -public class AddExperimentalGroupsDialog extends DialogWindow { - - private Collection levels; - - private ExperimentalGroupInput experimentalGroupInput; - - private final List> binders = new ArrayList<>(); - - private final List submitListeners = new ArrayList<>(); - - public record ExperimentalGroupSubmitEvent(Dialog eventSourceDialog, Set variableLevels, - int sampleSize) { - - } - - @FunctionalInterface - public interface ExperimentalGroupSubmitListener { - - void handle(ExperimentalGroupSubmitEvent event); - } - - private void fireExperimentalGroupSubmitEvent(ExperimentalGroupSubmitEvent event) { - submitListeners.forEach(it -> it.handle(event)); - } - - public void addExperimentalGroupSubmitListener( - ExperimentalGroupSubmitListener experimentalGroupSubmitListener) { - this.submitListeners.add(experimentalGroupSubmitListener); - } - - - public AddExperimentalGroupsDialog() { - super(); - setConfirmButtonLabel("Add"); - setCancelButtonLabel("Cancel"); - addClassName("experiment-group-dialog"); - setHeaderTitle("Please enter group information"); - levels = Collections.emptySet(); - - confirmButton.addClickListener(event -> submit()); - cancelButton.addClickListener(event -> close()); - getFooter().add(cancelButton, confirmButton); - } - - private void submit() { - //TODO validate all fields - binders.forEach(Binder::validate); - if (experimentalGroupInput.isInvalid()) { - return; - } - ExperimentalGroupBean value = experimentalGroupInput.getValue(); - ExperimentalGroupSubmitEvent event = new ExperimentalGroupSubmitEvent( - this, - new HashSet<>(value.getLevels()), - value.getSampleSize()); - fireExperimentalGroupSubmitEvent(event); - } - - public void setLevels(Collection levels) { - if (isOpened()) { - return; - } - this.levels = levels; - } - - @Override - public void open() { - if (Objects.nonNull(experimentalGroupInput)) { - remove(experimentalGroupInput); - } - experimentalGroupInput = new ExperimentalGroupInput(levels); - experimentalGroupInput.setRequiredIndicatorVisible(true); - add(experimentalGroupInput); - super.open(); - } - -} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java index a54ea052e..2a48fb79d 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java @@ -3,9 +3,11 @@ import static life.qbic.logging.service.LoggerFactory.logger; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.Text; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; @@ -20,30 +22,36 @@ import com.vaadin.flow.theme.lumo.LumoUtility.Display; import java.io.Serial; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.ApplicationException.ErrorCode; import life.qbic.application.commons.ApplicationException.ErrorParameters; import life.qbic.application.commons.Result; import life.qbic.datamanager.views.AppRoutes.Projects; import life.qbic.datamanager.views.Context; -import life.qbic.datamanager.views.general.CreationCard; +import life.qbic.datamanager.views.general.ConfirmEvent; import life.qbic.datamanager.views.general.DisclaimerCard; import life.qbic.datamanager.views.general.PageArea; import life.qbic.datamanager.views.general.ToggleDisplayEditComponent; -import life.qbic.datamanager.views.notifications.InformationMessage; -import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.projects.project.experiments.ExperimentInformationMain; -import life.qbic.datamanager.views.projects.project.experiments.experiment.AddExperimentalGroupsDialog.ExperimentalGroupSubmitEvent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentEditEvent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentInfoComponent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupCardCollection; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupsDialog; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupsDialog.ExperimentalGroupContent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariableContent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariablesComponent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariablesDialog; +import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentInformationContent; import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.DeletionService; import life.qbic.projectmanagement.application.ExperimentInformationService; import life.qbic.projectmanagement.application.ExperimentInformationService.ExperimentalGroupDTO; +import life.qbic.projectmanagement.application.ExperimentalDesignSearchService; import life.qbic.projectmanagement.domain.project.Project; import life.qbic.projectmanagement.domain.project.ProjectId; import life.qbic.projectmanagement.domain.project.experiment.Experiment; @@ -69,8 +77,11 @@ public class ExperimentDetailsComponent extends PageArea { @Serial private static final long serialVersionUID = -8992991642015281245L; private final transient ExperimentInformationService experimentInformationService; + private final transient ExperimentalDesignSearchService experimentalDesignSearchService; private final Div content = new Div(); + private final Div header = new Div(); private final Span title = new Span(); + private final Span buttonBar = new Span(); private final Div tagCollection = new Div(); private final TabSheet experimentSheet = new TabSheet(); private final ExperimentalVariablesComponent experimentalVariablesComponent = ExperimentalVariablesComponent.create( @@ -79,26 +90,24 @@ public class ExperimentDetailsComponent extends PageArea { private final Div experimentSummary = new Div(); private final ExperimentalGroupCardCollection experimentalGroupsCollection = new ExperimentalGroupCardCollection(); private final ExperimentalVariablesDialog addExperimentalVariablesDialog; - private final AddExperimentalGroupsDialog experimentalGroupsDialog; private final DisclaimerCard noExperimentalVariablesDefined; - - private final CreationCard experimentalGroupCreationCard = CreationCard.create( - "Add experimental groups"); private final DisclaimerCard addExperimentalVariablesNote; private Context context; private boolean hasExperimentalGroups; private final DeletionService deletionService; + private final List> editListeners = new ArrayList<>(); public ExperimentDetailsComponent( @Autowired ExperimentInformationService experimentInformationService, - @Autowired DeletionService deletionService) { + @Autowired DeletionService deletionService, + @Autowired ExperimentalDesignSearchService experimentalDesignSearchService) { this.experimentInformationService = Objects.requireNonNull(experimentInformationService); this.deletionService = Objects.requireNonNull(deletionService); + this.experimentalDesignSearchService = Objects.requireNonNull(experimentalDesignSearchService); this.addExperimentalVariablesDialog = new ExperimentalVariablesDialog(); this.noExperimentalVariablesDefined = createNoVariableDisclaimer(); this.addExperimentalVariablesNote = createNoVariableDisclaimer(); - this.experimentalGroupsDialog = createExperimentalGroupDialog(); this.addClassName("experiment-details-component"); layoutComponent(); configureComponent(); @@ -117,7 +126,8 @@ private Notification createSampleRegistrationPossibleNotification(String project Component layout = new HorizontalLayout(text, closeButton); layout.addClassName("content"); - notification.setPosition(Position.MIDDLE); + notification.setPosition(Position.BOTTOM_START); + notification.setDuration(3_000); notification.add(layout); return notification; } @@ -129,54 +139,112 @@ private DisclaimerCard createNoVariableDisclaimer() { return disclaimer; } - private AddExperimentalGroupsDialog createExperimentalGroupDialog() { - AddExperimentalGroupsDialog dialog = new AddExperimentalGroupsDialog(); - dialog.addExperimentalGroupSubmitListener(this::onGroupSubmitted); - return dialog; - } - private void layoutComponent() { + this.add(header); + header.addClassName("header"); this.add(content); - content.addClassName("details-content"); - setTitle(); + //Necessary to avoid css collution + content.addClassName("content"); + initButtonBar(); + header.add(title, buttonBar); + title.addClassName("title"); addTagCollectionToContent(); addExperimentNotesComponent(); layoutTabSheet(); } - - private void setTitle() { - title.addClassName("title"); - addComponentAsFirst(title); - } - private void configureComponent() { configureExperimentalGroupCreation(); + configureExperimentalGroupsEdit(); addCancelListenerForAddVariableDialog(); addConfirmListenerForAddVariableDialog(); + addConfirmListenerForEditVariableDialog(); + addListenerForNewEditEvent(); addListenerForNewVariableEvent(); - addEditListenerForExperimentalVariables(); } - private void addEditListenerForExperimentalVariables() { + private void initButtonBar() { + Button editButton = new Button("Edit"); + editButton.addClickListener(event -> openExperimentInformationDialog()); + buttonBar.add(editButton); + } + + private void openExperimentInformationDialog() { + ExperimentId experimentId = context.experimentId().orElseThrow(); + Optional experiment = experimentInformationService.find(experimentId); + experiment.ifPresentOrElse(exp -> { + ExperimentInformationDialog experimentInformationDialog = openExperimentInformationDialog( + exp); + addExperimentInformationDialogListeners(experimentId, experimentInformationDialog); + experimentInformationDialog.open(); + } + , () -> { + throw new ApplicationException( + "Experiment information could not be retrieved from service"); + }); + } + + private ExperimentInformationDialog openExperimentInformationDialog(Experiment experiment) { + ExperimentInformationDialog experimentInformationDialog = ExperimentInformationDialog.prefilled( + experimentalDesignSearchService, + experiment.getName(), experiment.getSpecies(), + experiment.getSpecimens(), experiment.getAnalytes()); + experimentInformationDialog.setConfirmButtonLabel("Save"); + return experimentInformationDialog; + } + + private void addExperimentInformationDialogListeners(ExperimentId experimentId, + ExperimentInformationDialog experimentInformationDialog) { + experimentInformationDialog.addCancelEventListener( + experimentInformationDialogCancelEvent -> experimentInformationDialog.close()); + experimentInformationDialog.addConfirmEventListener(experimentInformationDialogConfirmEvent -> { + ExperimentInformationContent experimentInformationContent = experimentInformationDialogConfirmEvent.getSource() + .content(); + experimentInformationService.editExperimentInformation(experimentId, + experimentInformationContent.experimentName(), experimentInformationContent.species(), + experimentInformationContent.specimen(), experimentInformationContent.analytes()); + experimentInformationDialog.close(); + fireEditEvent(); + }); + } + + private void addConfirmListenerForEditVariableDialog() { experimentalVariablesComponent.subscribeToEditEvent(experimentalVariablesEditEvent -> { ExperimentId experimentId = context.experimentId().orElseThrow(); var editDialog = ExperimentalVariablesDialog.prefilled( experimentInformationService.getVariablesOfExperiment(experimentId)); - editDialog.subscribeToCancelEvent( + editDialog.addCancelEventListener( experimentalVariablesDialogCancelEvent -> editDialog.close()); - editDialog.subscribeToConfirmEvent(experimentalVariablesDialogConfirmEvent -> { - deleteExistingExperimentalVariables(experimentId); - registerExperimentalVariables(experimentalVariablesDialogConfirmEvent.getSource()); - editDialog.close(); - reloadExperimentalVariables(); + editDialog.addConfirmEventListener(experimentalVariablesDialogConfirmEvent -> { + var confirmDialog = experimentalGroupDeletionConfirmDialog(); + confirmDialog.addConfirmListener(confirmDeletionEvent -> { + deleteExistingExperimentalVariables(experimentId); + addExperimentalVariables( + experimentalVariablesDialogConfirmEvent.getSource().definedVariables()); + editDialog.close(); + reloadExperimentalVariables(); + }); + confirmDialog.open(); }); editDialog.open(); }); } + private static ConfirmDialog experimentalGroupDeletionConfirmDialog() { + var confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Your experimental groups will be deleted"); + confirmDialog.setText( + "Editing experimental variables requires all experimental groups to be deleted. Are you sure you want to delete them?"); + confirmDialog.setConfirmText("Delete experimental groups"); + confirmDialog.setCancelable(true); + confirmDialog.setCancelText("Abort"); + confirmDialog.setRejectable(false); + return confirmDialog; + } + private void reloadExperimentalVariables() { - loadExperiment(context.experimentId().orElseThrow()); + experimentInformationService.find(context.experimentId().orElseThrow()) + .ifPresent(this::loadExperimentInformation); } private void deleteExistingExperimentalVariables(ExperimentId experimentId) { @@ -187,6 +255,11 @@ private void deleteExistingExperimentalVariables(ExperimentId experimentId) { }); } + private void addListenerForNewEditEvent() { + this.editListeners.add(event -> experimentInformationService.find(event.experimentId()) + .ifPresent(this::loadExperimentInformation)); + } + private void addListenerForNewVariableEvent() { this.experimentalVariablesComponent.subscribeToAddEvent( listener -> displayAddExperimentalVariablesDialog()); @@ -196,17 +269,6 @@ private void displayAddExperimentalVariablesDialog() { this.addExperimentalVariablesDialog.open(); } - public void onGroupSubmitted(ExperimentalGroupSubmitEvent groupSubmitted) { - Result response = experimentInformationService.addExperimentalGroupToExperiment( - context.experimentId().orElseThrow(), - new ExperimentalGroupDTO(groupSubmitted.variableLevels(), groupSubmitted.sampleSize())); - if (response.isValue()) { - handleGroupSubmittedSuccess(); - } else { - handleDuplicateConditionInput(); - } - } - private void addTagCollectionToContent() { tagCollection.addClassName("tag-collection"); content.add(tagCollection); @@ -228,19 +290,112 @@ private void layoutTabSheet() { } private void configureExperimentalGroupCreation() { - experimentalGroupCreationCard.addListener(event -> experimentalGroupsDialog.open()); + experimentalGroupsCollection.addAddEventListener(listener -> openExperimentalGroupAddDialog()); } - private void addCancelListenerForAddVariableDialog() { - addExperimentalVariablesDialog.subscribeToCancelEvent(it -> it.getSource().close()); + /** + * Adds a listener for an {@link ExperimentEditEvent} + * + * @param listener the listener to add + */ + public void addExperimentEditEventListener( + ComponentEventListener listener) { + this.editListeners.add(listener); + } + + private void fireEditEvent() { + ExperimentId experimentId = context.experimentId().orElseThrow(); + var editEvent = new ExperimentEditEvent(this, experimentId, true); + editListeners.forEach(listener -> listener.onComponentEvent(editEvent)); + } + + private void openExperimentalGroupAddDialog() { + ExperimentId experimentId = context.experimentId().orElseThrow(); + List variables = experimentInformationService.getVariablesOfExperiment( + experimentId); + List levels = variables.stream() + .flatMap(variable -> variable.levels().stream()) + .toList(); + var dialog = getExperimentalGroupsDialogForAdding(levels); + dialog.open(); + } + + private ExperimentalGroupsDialog getExperimentalGroupsDialogForAdding( + List levels) { + var dialog = ExperimentalGroupsDialog.empty(levels); + dialog.addCancelEventListener(cancelEvent -> cancelEvent.getSource().close()); + dialog.addConfirmEventListener(this::onAddExperimentalGroupDialogConfirmed); + return dialog; } - private void handleGroupSubmittedSuccess() { + private void onAddExperimentalGroupDialogConfirmed( + ConfirmEvent confirmEvent) { + ExperimentalGroupsDialog dialog = confirmEvent.getSource(); + addExperimentalGroups(dialog.experimentalGroups()); reloadExperimentalGroups(); - if (hasExperimentalGroups) { - showSampleRegistrationPossibleNotification(); - } - experimentalGroupsDialog.close(); + dialog.close(); + } + + + private void configureExperimentalGroupsEdit() { + experimentalGroupsCollection.addEditEventListener(listener -> { + ExperimentId experimentId = context.experimentId().orElseThrow(); + List variables = experimentInformationService.getVariablesOfExperiment( + experimentId); + List levels = variables.stream() + .flatMap(variable -> variable.levels().stream()).toList(); + var experimentalGroups = experimentInformationService.getExperimentalGroups(experimentId) + .stream().map(this::toContent).toList(); + var dialog = ExperimentalGroupsDialog.prefilled(levels, experimentalGroups); + dialog.addCancelEventListener(cancelEvent -> cancelEvent.getSource().close()); + dialog.addConfirmEventListener( + confirmEvent -> { + editExperimentalGroups(confirmEvent.getSource().experimentalGroups()); + reloadExperimentalGroups(); + dialog.close(); + }); + dialog.open(); + }); + } + + private void editExperimentalGroups( + Collection experimentalGroupContents) { + ExperimentId experimentId = context.experimentId().orElseThrow(); + deletionService.deleteAllExperimentalGroups(experimentId).onError(error -> { + throw new ApplicationException( + "Could not edit experiments because samples are already registered."); + }); + addExperimentalGroups(experimentalGroupContents); + } + + private void addExperimentalGroups( + Collection experimentalGroupContents) { + List experimentalGroupDTOS = experimentalGroupContents.stream() + .map(this::toExperimentalGroupDTO).toList(); + ExperimentId experimentId = context.experimentId().orElseThrow(); + Result, ResponseCode> result = experimentInformationService.addExperimentalGroupsToExperiment( + experimentId, experimentalGroupDTOS); + result.onError(error -> { + throw new ApplicationException( + "Could not save one or more experimental groups %s %nReason: %s".formatted( + Arrays.toString( + experimentalGroupContents.toArray()), error)); + }); + } + + private ExperimentalGroupDTO toExperimentalGroupDTO( + ExperimentalGroupContent experimentalGroupContent) { + return new ExperimentalGroupDTO(experimentalGroupContent.variableLevels(), + experimentalGroupContent.size()); + } + + private ExperimentalGroupContent toContent(ExperimentalGroupDTO experimentalGroupDTO) { + return new ExperimentalGroupContent(experimentalGroupDTO.replicateCount(), + experimentalGroupDTO.levels()); + } + + private void addCancelListenerForAddVariableDialog() { + addExperimentalVariablesDialog.addCancelEventListener(it -> it.getSource().close()); } private void showSampleRegistrationPossibleNotification() { @@ -249,17 +404,12 @@ private void showSampleRegistrationPossibleNotification() { notification.open(); } - private void handleDuplicateConditionInput() { - InformationMessage infoMessage = new InformationMessage( - "A group with the same condition exists already.", ""); - StyledNotification notification = new StyledNotification(infoMessage); - notification.open(); - } - private void reloadExperimentalGroups() { loadExperimentalGroups(); - addCreationCardToExperimentalGroupCollection(); + if (hasExperimentalGroups) { + showSampleRegistrationPossibleNotification(); + } } private void loadExperimentalGroups() { @@ -269,32 +419,14 @@ private void loadExperimentalGroups() { List experimentalGroupsCards = experimentalGroups.stream() .map(ExperimentalGroupCard::new).toList(); - // We register the experimental details component as listener for group deletion events - experimentalGroupsCards.forEach(this::subscribeToDeletionClickEvent); - experimentalGroupsCollection.setComponents(experimentalGroupsCards); + experimentalGroupsCollection.setContent(experimentalGroupsCards); this.hasExperimentalGroups = !experimentalGroupsCards.isEmpty(); } - private void addCreationCardToExperimentalGroupCollection() { - experimentalGroupsCollection.addComponentAsLast(experimentalGroupCreationCard); - } - - private void subscribeToDeletionClickEvent(ExperimentalGroupCard experimentalGroupCard) { - experimentalGroupCard.addDeletionEventListener( - ExperimentDetailsComponent.this::handleDeletionClickedEvent); - } - - private void handleDeletionClickedEvent( - ExperimentalGroupDeletionEvent experimentalGroupDeletionEvent) { - experimentInformationService.deleteExperimentGroup(context.experimentId().orElseThrow(), - experimentalGroupDeletionEvent.getSource().groupId()); - reloadExperimentalGroups(); - } - private void addConfirmListenerForAddVariableDialog() { - addExperimentalVariablesDialog.subscribeToConfirmEvent(it -> { + addExperimentalVariablesDialog.addConfirmEventListener(it -> { try { - registerExperimentalVariables(it.getSource()); + addExperimentalVariables(it.getSource().definedVariables()); it.getSource().close(); setContext(this.context); if (hasExperimentalGroups) { @@ -306,25 +438,21 @@ private void addConfirmListenerForAddVariableDialog() { }); } - private void registerExperimentalVariables( - ExperimentalVariablesDialog experimentalVariablesDialog) { - experimentalVariablesDialog.definedVariables().forEach( + private void addExperimentalVariables( + List experimentalVariableContents) { + experimentalVariableContents.forEach( experimentalVariableContent -> experimentInformationService.addVariableToExperiment( context.experimentId().orElseThrow(), - experimentalVariableContent.name(), experimentalVariableContent.unit(), - experimentalVariableContent.levels())); + experimentalVariableContent.name(), experimentalVariableContent.unit(), + experimentalVariableContent.levels())); } public void setContext(Context context) { ExperimentId experimentId = context.experimentId() .orElseThrow(() -> new ApplicationException("no experiment id in context " + context)); - ProjectId projectId = context.projectId() + context.projectId() .orElseThrow(() -> new ApplicationException("no project id in context " + context)); this.context = context; - loadExperiment(experimentId); - } - - private void loadExperiment(ExperimentId experimentId) { experimentInformationService.find(experimentId).ifPresent(this::loadExperimentInformation); } @@ -332,16 +460,16 @@ private void loadExperimentInformation(Experiment experiment) { title.setText(experiment.getName()); loadTagInformation(experiment); loadExperimentInfo(experiment); - fillExperimentalGroupDialog(); - reloadExperimentalGroups(); + loadExperimentalGroups(); if (experiment.variables().isEmpty()) { - useCaseNoVariablesYet(); + onNoVariablesDefined(); } else { - removeDisclaimer(); - displayExperimentalGroupsCollection(); + removeNoExperimentalVariablesDefinedDisclaimer(); + contentExperimentalGroupsTab.add(experimentalGroupsCollection); } } + private void loadTagInformation(Experiment experiment) { tagCollection.removeAll(); List tags = new ArrayList<>(); @@ -354,44 +482,27 @@ private void loadTagInformation(Experiment experiment) { private void loadExperimentInfo(Experiment experiment) { ExperimentInfoComponent factSheet = ExperimentInfoComponent.create(experiment.getSpecies(), experiment.getSpecimens(), experiment.getAnalytes()); + this.experimentSummary.removeAll(); + this.experimentSummary.add(factSheet); + factSheet.showMenu(); + reloadExperimentalVariables(experiment); + } + + private void reloadExperimentalVariables(Experiment experiment) { this.experimentalVariablesComponent.setExperimentalVariables(experiment.variables()); - ExperimentDetailsComponent.this.experimentSummary.removeAll(); - ExperimentDetailsComponent.this.experimentSummary.add(factSheet); if (experiment.variables().isEmpty()) { - ExperimentDetailsComponent.this.experimentSummary.add(addExperimentalVariablesNote); + this.experimentSummary.add(addExperimentalVariablesNote); } else { - ExperimentDetailsComponent.this.experimentSummary.add(experimentalVariablesComponent); + this.experimentSummary.add(experimentalVariablesComponent); } - factSheet.showMenu(); - } - - private void fillExperimentalGroupDialog() { - List variables = experimentInformationService.getVariablesOfExperiment( - context.experimentId().orElseThrow()); - List levels = variables.stream() - .flatMap(variable -> variable.levels().stream()).toList(); - experimentalGroupsDialog.setLevels(levels); } - private void useCaseNoVariablesYet() { - displayDisclaimer(); - hideExperimentalGroupsCollection(); - } - - private void removeDisclaimer() { - contentExperimentalGroupsTab.remove(noExperimentalVariablesDefined); - } - - private void displayExperimentalGroupsCollection() { - contentExperimentalGroupsTab.add(experimentalGroupsCollection); - } - - private void displayDisclaimer() { + private void onNoVariablesDefined() { contentExperimentalGroupsTab.add(noExperimentalVariablesDefined); - } - - private void hideExperimentalGroupsCollection() { contentExperimentalGroupsTab.remove(experimentalGroupsCollection); } + private void removeNoExperimentalVariablesDefinedDisclaimer() { + contentExperimentalGroupsTab.remove(noExperimentalVariablesDefined); + } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentInformationDialog.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentInformationDialog.java new file mode 100644 index 000000000..27f11109f --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentInformationDialog.java @@ -0,0 +1,166 @@ +package life.qbic.datamanager.views.projects.project.experiments.experiment; + +import com.vaadin.flow.component.ComponentEventListener; +import java.io.Serial; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import life.qbic.datamanager.views.general.CancelEvent; +import life.qbic.datamanager.views.general.ConfirmEvent; +import life.qbic.datamanager.views.general.DialogWindow; +import life.qbic.datamanager.views.projects.create.DefineExperimentComponent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.create.ExperimentInformationContent; +import life.qbic.projectmanagement.application.ExperimentalDesignSearchService; +import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Analyte; +import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Species; +import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Specimen; + +/** + * ExperimentInformationDialog + * + *

Dialog to create or edit experiment information by providing the minimal required information + * in the {@link DefineExperimentComponent}

+ * + * @since 1.0.0 + */ + +public class ExperimentInformationDialog extends DialogWindow { + + @Serial + private static final long serialVersionUID = 2142928219461555700L; + private static final String TITLE = "Experimental Design"; + private final DefineExperimentComponent defineExperimentComponent; + private final List>> cancelEventListeners = new ArrayList<>(); + private final List>> confirmEventListeners = new ArrayList<>(); + private final MODE mode; + + public ExperimentInformationDialog( + ExperimentalDesignSearchService experimentalDesignSearchService) { + this(experimentalDesignSearchService, false); + } + + private ExperimentInformationDialog( + ExperimentalDesignSearchService experimentalDesignSearchService, boolean mode) { + super(); + this.mode = mode ? MODE.EDIT : MODE.ADD; + addClassName("experiment-information-dialog"); + defineExperimentComponent = new DefineExperimentComponent(experimentalDesignSearchService); + layoutComponent(); + initDialogueContent(); + configureComponent(); + } + + private void layoutComponent() { + setHeaderTitle(TITLE); + add(defineExperimentComponent); + setConfirmButtonLabel("Add"); + setCancelButtonLabel("Cancel"); + final DialogFooter footer = getFooter(); + footer.add(this.cancelButton, this.confirmButton); + } + + private void initDialogueContent() { + add(defineExperimentComponent); + } + + private void configureComponent() { + configureCancelling(); + configureConfirmation(); + } + + /** + * Creates a new dialog prefilled with experiment information. + * + * @param experimentalDesignSearchService the service providing the selectable options for the + * analyte, specimen and species within this dialog + * @param experimentName experimentName to be preset within the dialog + * @param species List of {@link Species} to be preset within the dialog + * @param specimen List of {@link Specimen} to be preset within the dialog + * @param analytes List of {@link Analyte} to be preset within the dialog + * @return a new instance of the dialog + */ + public static ExperimentInformationDialog prefilled( + ExperimentalDesignSearchService experimentalDesignSearchService, + String experimentName, Collection species, Collection specimen, + Collection analytes) { + return editDialog(experimentalDesignSearchService, experimentName, species, specimen, analytes); + } + + private static ExperimentInformationDialog editDialog( + ExperimentalDesignSearchService experimentalDesignSearchService, String experimentName, + Collection species, Collection specimen, Collection analytes) { + ExperimentInformationDialog experimentInformationDialog = new ExperimentInformationDialog( + experimentalDesignSearchService, true); + experimentInformationDialog.setExperimentInformation(experimentName, species, specimen, + analytes); + return experimentInformationDialog; + } + + private void setExperimentInformation(String experimentName, + Collection species, Collection specimen, Collection analytes) { + defineExperimentComponent.setExperimentInformation(experimentName, species, specimen, analytes); + } + + private boolean isInputValid() { + return defineExperimentComponent.isValid(); + } + + private void configureConfirmation() { + this.confirmButton.addClickListener(event -> fireConfirmEvent()); + } + + private void configureCancelling() { + this.cancelButton.addClickListener(cancelListener -> fireCancelEvent()); + } + + private void fireConfirmEvent() { + if (isInputValid()) { + this.confirmEventListeners.forEach( + listener -> listener.onComponentEvent(new ConfirmEvent<>(this, true))); + } + } + + private void fireCancelEvent() { + this.cancelEventListeners.forEach( + listener -> listener.onComponentEvent(new CancelEvent<>(this, true))); + } + + + /** + * Adds a listener for {@link ConfirmEvent}s + * + * @param listener the listener to add + */ + public void addConfirmEventListener( + final ComponentEventListener> listener) { + this.confirmEventListeners.add(listener); + } + + /** + * Adds a listener for {@link CancelEvent}s + * + * @param listener the listener to add + */ + public void addCancelEventListener( + final ComponentEventListener> listener) { + this.cancelEventListeners.add(listener); + } + + /** + * Provides the content set in the fields of this dialog + * + * @return {@link ExperimentInformationContent} providing the information filled by the user + * within this dialog + */ + public ExperimentInformationContent content() { + return new ExperimentInformationContent( + defineExperimentComponent.experimentNameField.getValue(), + defineExperimentComponent.speciesBox.getValue().stream().toList(), + defineExperimentComponent.specimenBox.getValue().stream().toList(), + defineExperimentComponent.analyteBox.getValue().stream().toList()); + } + + private enum MODE { + ADD, EDIT + } +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java index 983f93573..8be5613d9 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java @@ -27,29 +27,20 @@ public class ExperimentalGroupCard extends Card { @Serial private static final long serialVersionUID = -8400631799486647200L; private final transient ExperimentalGroup experimentalGroup; - private final List> listenersDeletionEvent; public ExperimentalGroupCard(ExperimentalGroup experimentalGroup) { super(); this.experimentalGroup = experimentalGroup; - this.listenersDeletionEvent = new ArrayList<>(); layoutComponent(); } private void layoutComponent() { addClassName("experimental-group"); - MenuBar menuBar = createMenuBar(); - Div cardHeader = new Div(); cardHeader.addClassName("header"); - Div controls = new Div(); - controls.addClassName("controls"); - controls.add(menuBar); - cardHeader.add(title("Experimental Group")); - cardHeader.add(controls); this.add(cardHeader); Div cardContent = new Div(); @@ -59,16 +50,6 @@ private void layoutComponent() { this.add(cardContent); } - private MenuBar createMenuBar() { - MenuBar menuBar = new MenuBar(); - menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY); - MenuItem menuItem = menuBar.addItem("•••"); - SubMenu subMenu = menuItem.getSubMenu(); - subMenu.addItem("Edit", event -> { - }); - subMenu.addItem("Delete", event -> fireDeletionEvent()); - return menuBar; - } private Span title(String value) { Span cardTitle = new Span(); @@ -97,15 +78,6 @@ private Span sampleSize() { return span; } - public void fireDeletionEvent() { - var deletionEvent = new ExperimentalGroupDeletionEvent(ExperimentalGroupCard.this, true); - listenersDeletionEvent.forEach(listener -> listener.onComponentEvent(deletionEvent)); - } - - public void addDeletionEventListener( - ComponentEventListener listener) { - this.listenersDeletionEvent.add(listener); - } public long groupId() { return this.experimentalGroup.id(); @@ -115,9 +87,4 @@ public ExperimentalGroup experimentalGroup() { return this.experimentalGroup; } - public void subscribeToDeletionEvent( - ComponentEventListener subscriber) { - this.listenersDeletionEvent.add(subscriber); - } - } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java index 74fb80c09..6fe3a5c68 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java @@ -1,19 +1,23 @@ package life.qbic.datamanager.views.projects.project.experiments.experiment; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.ItemLabelGenerator; +import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.combobox.MultiSelectComboBox; import com.vaadin.flow.component.combobox.dataview.ComboBoxListDataView; import com.vaadin.flow.component.customfield.CustomField; -import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.data.binder.Binder; import jakarta.validation.constraints.Min; +import java.io.Serial; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import life.qbic.datamanager.views.general.Container; @@ -32,6 +36,7 @@ * {@link ExperimentalGroup} by defining the {@link Condition} and number of * {@link BiologicalReplicate} associated with the {@link ExperimentalGroup} */ +@Tag(Tag.DIV) public class ExperimentalGroupInput extends CustomField { private static final Comparator VARIABLE_LEVEL_COMPARATOR = Comparator @@ -41,6 +46,7 @@ public class ExperimentalGroupInput extends CustomField { "%s: %s", it.variableName().value(), ExperimentValueFormatter.format(it.experimentalValue())); + private final List> removeEventListeners; private final MultiSelectComboBox variableLevelSelect; private final NumberField replicateCountField; int variableCount = 0; @@ -54,14 +60,16 @@ public class ExperimentalGroupInput extends CustomField { * @param availableLevels Collection of {@link VariableLevel} defined for an {@link Experiment} */ public ExperimentalGroupInput(Collection availableLevels) { - addClassName("group-input"); + addClassName("experimental-group-entry"); + removeEventListeners = new ArrayList<>(); variableLevelSelect = generateVariableLevelSelect(); - replicateCountField = generateSampleSizeField(); + replicateCountField = generateBiologicalReplicateField(); - Span layout = new Span(variableLevelSelect, replicateCountField); - layout.addClassName("layout"); - add(layout); + var deleteIcon = new Icon(VaadinIcon.CLOSE_SMALL); + deleteIcon.addClickListener( + event -> fireRemoveEvent(new RemoveEvent(this, event.isFromClient()))); + add(variableLevelSelect, replicateCountField, deleteIcon); setLevels(availableLevels); addValidationForVariableCount(); variableLevelSelect.addValueChangeListener( @@ -70,6 +78,22 @@ public ExperimentalGroupInput(Collection availableLevels) { event -> setInvalid(variableLevelSelect.isInvalid() || replicateCountField.isInvalid())); } + public void setCondition(Collection levels) { + this.variableLevelSelect.setValue(levels); + } + + public void setReplicateCount(int numberOfReplicates) { + this.replicateCountField.setValue((double) numberOfReplicates); + } + + private void fireRemoveEvent(RemoveEvent event) { + removeEventListeners.forEach(listener -> listener.onComponentEvent(event)); + } + + public void addRemoveEventListener(ComponentEventListener listener) { + removeEventListeners.add(listener); + } + private static void overwriteSelectionOfSameVariable( MultiSelectComboBox selectComboBox) { selectComboBox.addSelectionListener(event -> { @@ -103,18 +127,30 @@ private void setLevels(Collection availableLevels) { @Override protected ExperimentalGroupBean generateModelValue() { - var levels = variableLevelSelect.getValue().stream().sorted(VARIABLE_LEVEL_COMPARATOR) - .toList(); - var sampleSize = Optional - .ofNullable(replicateCountField.getValue()).map(Double::intValue) - .orElse(0); + var levels = getCondition(); + var sampleSize = getReplicateCount(); return new ExperimentalGroupBean(sampleSize, levels); } + public int getReplicateCount() { + return replicateCountField.getOptionalValue().map(Double::intValue).orElse(0); + } + + public List getCondition() { + return variableLevelSelect.getValue().stream() + .sorted(VARIABLE_LEVEL_COMPARATOR) + .toList(); + } + + @Override + public ExperimentalGroupBean getEmptyValue() { + return generateModelValue(); + } + @Override protected void setPresentationValue(ExperimentalGroupBean newPresentationValue) { variableLevelSelect.setValue(newPresentationValue.levels); - replicateCountField.setValue((double) newPresentationValue.sampleSize); + replicateCountField.setValue((double) newPresentationValue.replicateCount); } @Override @@ -138,11 +174,12 @@ private MultiSelectComboBox generateVariableLevelSelect() { selectComboBox.addClassName("chip-badge"); selectComboBox.setAllowCustomValue(false); selectComboBox.setItemLabelGenerator(VARIABLE_LEVEL_ITEM_LABEL_GENERATOR); + selectComboBox.setWidthFull(); overwriteSelectionOfSameVariable(selectComboBox); return selectComboBox; } - private NumberField generateSampleSizeField() { + private NumberField generateBiologicalReplicateField() { NumberField numberField = new NumberField(); numberField.addClassName("number-field"); numberField.setLabel("Biological Replicates"); @@ -176,10 +213,10 @@ public static class ExperimentalGroupBean { private final List levels = new ArrayList<>(); @Min(1) - private final int sampleSize; + private final int replicateCount; - public ExperimentalGroupBean(int sampleSize, List levels) { - this.sampleSize = sampleSize; + public ExperimentalGroupBean(int replicateCount, List levels) { + this.replicateCount = replicateCount; this.levels.addAll(levels); } @@ -187,8 +224,26 @@ public List getLevels() { return Collections.unmodifiableList(levels); } - public int getSampleSize() { - return sampleSize; + public int getReplicateCount() { + return replicateCount; + } + } + + public static class RemoveEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = 2934203596212238997L; + + /** + * Creates a new event using the given source and indicator whether the event originated from + * the client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public RemoveEvent(ExperimentalGroupInput source, boolean fromClient) { + super(source, fromClient); } } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentEditEvent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentEditEvent.java new file mode 100644 index 000000000..928173d95 --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentEditEvent.java @@ -0,0 +1,40 @@ +package life.qbic.datamanager.views.projects.project.experiments.experiment.components; + +import com.vaadin.flow.component.ComponentEvent; +import java.io.Serial; +import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentDetailsComponent; +import life.qbic.projectmanagement.domain.project.experiment.ExperimentId; + +/** + * Experiment Edit Event + *

+ * Event that indicates that the user wants to edit an experiment via the + * {@link ExperimentDetailsComponent} + * + * @since 1.0.0 + */ +public class ExperimentEditEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = -5383275108609304372L; + private final ExperimentId experimentId; + + /** + * Creates a new event using the given source and indicator whether the event originated from the + * client side or the server side. + * + * @param source the source component + * @param experimentId the {@link ExperimentId} of the edited experiment + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public ExperimentEditEvent(ExperimentDetailsComponent source, ExperimentId experimentId, + boolean fromClient) { + super(source, fromClient); + this.experimentId = experimentId; + } + + public ExperimentId experimentId() { + return experimentId; + } +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java index 2365e0e2f..2b4ab8789 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java @@ -1,11 +1,17 @@ package life.qbic.datamanager.views.projects.project.experiments.experiment.components; +import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.HasComponents; -import com.vaadin.flow.component.HasSize; -import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; import java.io.Serial; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import life.qbic.datamanager.views.general.AddEvent; +import life.qbic.datamanager.views.general.EditEvent; import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentalGroupCard; /** @@ -15,33 +21,82 @@ * * @since 1.0.0 */ -@Tag(Tag.DIV) -public class ExperimentalGroupCardCollection extends Component implements HasComponents, HasSize { + +public class ExperimentalGroupCardCollection extends Div { @Serial private static final long serialVersionUID = -5835580091959912561L; + private final Div content = new Div(); + private final List>> addListeners = new ArrayList<>(); + private final List>> editListeners = new ArrayList<>(); + + public ExperimentalGroupCardCollection() { addClassName("experimental-group-card-collection"); + Span title = new Span("Groups"); + title.addClassName("title"); + Div header = new Div(); + header.addClassName("groups-header"); + Div controlItems = new Div(); + controlItems.addClassName("groups-controls"); + content.addClassName("groups-content"); + header.add(title, controlItems); + Button addButton = new Button("Add"); + Button editButton = new Button("Edit"); + controlItems.add(editButton, addButton); + + addButton.addClassName("primary"); + add(header, content); + + addButton.addClickListener(this::emitAddEvent); + editButton.addClickListener(this::emitEditEvent); } - public void setComponents(Collection experimentalGroupComponents) { - removeAll(); - addComponents(experimentalGroupComponents); + private void emitEditEvent(ClickEvent