diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermJpaRepository.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermJpaRepository.java index 6424159d2..d4dc5254f 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermJpaRepository.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermJpaRepository.java @@ -41,14 +41,15 @@ public List query(int offset, int limit) { private String buildSearchTerm(String searchString) { StringBuilder searchTermBuilder = new StringBuilder(); for(String word : searchString.split(" ")) { - searchTermBuilder.append(" +" + word); + searchTermBuilder.append(" +").append(word); } searchTermBuilder.append("*"); return searchTermBuilder.toString().trim(); } @Override - public List query(String searchString, List ontologyAbbreviations, + public List query(String searchString, + List ontologyAbbreviations, int offset, int limit, List sortOrders) { List orders = sortOrders.stream().map(it -> { diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepository.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepository.java index ee77c9263..95c3a79e3 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepository.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepository.java @@ -19,9 +19,5 @@ public interface OntologyTermRepository extends countQuery = "SELECT count(*) FROM ontology_classes WHERE MATCH(label) AGAINST(?1 IN BOOLEAN MODE) AND ontology in (?2);", nativeQuery = true) Page findByLabelFulltextMatching( - String termFilter, List ontology, Pageable pageable); - - Page findByLabelNotNullAndOntologyIn(List ontologies, Pageable pageable); - - Page findByLabelStartingWithIgnoreCaseAndOntologyIn(String filter, List ontology, Pageable pageable); + String termFilter, List ontologyAbbreviations, Pageable pageable); } diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepositoryImpl.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepositoryImpl.java index fe71219a7..44fd4e037 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepositoryImpl.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/OntologyTermRepositoryImpl.java @@ -38,7 +38,7 @@ public OntologyTermRepositoryImpl(QbicOntologyTermRepo ontologyTermRepo) { @Override public List find(String name) { - return ontologyTermRepo.findOntologyTermByName(name); + return ontologyTermRepo.findOntologyClassEntitiesByClassName(name); } @Override diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/QbicOntologyTermRepo.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/QbicOntologyTermRepo.java index 178c24211..343b66d60 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/QbicOntologyTermRepo.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/ontology/QbicOntologyTermRepo.java @@ -17,5 +17,5 @@ */ public interface QbicOntologyTermRepo extends CrudRepository { - List findOntologyTermByName(String name); + List findOntologyClassEntitiesByClassName(String name); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/ContactRepository.java b/project-management/src/main/java/life/qbic/projectmanagement/application/ContactRepository.java new file mode 100644 index 000000000..ec5a91170 --- /dev/null +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/ContactRepository.java @@ -0,0 +1,29 @@ +package life.qbic.projectmanagement.application; + +import java.util.List; +import life.qbic.projectmanagement.domain.model.project.Contact; +import org.springframework.stereotype.Component; + +@Component +public class ContactRepository { + + public List findAll() { + //TODO implement + return dummyContacts(); + } + + private static List dummyContacts() { + return List.of( + new Contact("Max Mustermann", "max.mustermann@qbic.uni-tuebingen.de"), + new Contact("David Müller", "david.mueller@qbic.uni-tuebingen.de"), + new Contact("John Koch", "john.koch@qbic.uni-tuebingen.de"), + new Contact("Trevor Noah", "trevor.noah@qbic.uni-tuebingen.de"), + new Contact("Sarah Connor", "sarah.connor@qbic.uni-tuebingen.de"), + new Contact("Anna Bell", "anna.bell@qbic.uni-tuebingen.de"), + new Contact("Sophia Turner", "sophia.turner@qbic.uni-tuebingen.de"), + new Contact("Tylor Smith", "tylor.smith@qbic.uni-tuebingen.de") + ); + } + + +} diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyClassEntity.java b/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyClassEntity.java index c729a2fde..58b5db588 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyClassEntity.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyClassEntity.java @@ -1,5 +1,6 @@ package life.qbic.projectmanagement.application; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -10,18 +11,19 @@ @Table(name = "ontology_classes") public class OntologyClassEntity { - String ontology; - + @Column(name = "ontology") + String ontologyAbbreviation; + @Column(name = "ontologyVersion") String ontologyVersion; - + @Column(name = "ontologyIri") String ontologyIri; - - String label; - - String name; - + @Column(name = "label") + String classLabel; + @Column(name = "name") + String className; + @Column(name = "description", length = 2000) String description; - + @Column(name = "classIri") String classIri; @Id @@ -31,23 +33,24 @@ public class OntologyClassEntity { public OntologyClassEntity() { } - public OntologyClassEntity(String ontology, String ontologyVersion, String ontologyIri, - String label, String name, String description, String classIri) { - this.ontology = ontology; + public OntologyClassEntity(String ontologyAbbreviation, String ontologyVersion, + String ontologyIri, + String classLabel, String className, String description, String classIri) { + this.ontologyAbbreviation = ontologyAbbreviation; this.ontologyVersion = ontologyVersion; this.ontologyIri = ontologyIri; - this.label = label; - this.name = name; + this.classLabel = classLabel; + this.className = className; this.description = description; this.classIri = classIri; } - public String getOntology() { - return ontology; + public String getOntologyAbbreviation() { + return ontologyAbbreviation; } - public void setOntology(String ontology) { - this.ontology = ontology; + public void setOntologyAbbreviation(String ontology) { + this.ontologyAbbreviation = ontology; } public String getOntologyVersion() { @@ -66,20 +69,20 @@ public void setOntologyIri(String ontologyIri) { this.ontologyIri = ontologyIri; } - public String getLabel() { - return label; + public String getClassLabel() { + return classLabel; } - public void setLabel(String label) { - this.label = label; + public void setClassLabel(String label) { + this.classLabel = label; } - public String getName() { - return name; + public String getClassName() { + return className; } - public void setName(String name) { - this.name = name; + public void setClassName(String name) { + this.className = name; } public String getDescription() { @@ -107,16 +110,17 @@ public boolean equals(Object o) { return false; } OntologyClassEntity that = (OntologyClassEntity) o; - return Objects.equals(ontology, that.ontology) && Objects.equals( + return Objects.equals(ontologyAbbreviation, that.ontologyAbbreviation) && Objects.equals( ontologyVersion, that.ontologyVersion) && Objects.equals(ontologyIri, - that.ontologyIri) && Objects.equals(label, that.label) && Objects.equals( - name, that.name) && Objects.equals(description, that.description) + that.ontologyIri) && Objects.equals(classLabel, that.classLabel) && Objects.equals( + className, that.className) && Objects.equals(description, that.description) && Objects.equals(classIri, that.classIri); } @Override public int hashCode() { - return Objects.hash(ontology, ontologyVersion, ontologyIri, label, name, description, classIri); + return Objects.hash(ontologyAbbreviation, ontologyVersion, ontologyIri, classLabel, className, + description, classIri); } public Long getId() { @@ -126,4 +130,4 @@ public Long getId() { public void setId(Long id) { this.id = id; } -} \ No newline at end of file +} diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyTermInformationService.java b/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyTermInformationService.java index ce27760f0..e0baf5d76 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyTermInformationService.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/OntologyTermInformationService.java @@ -45,7 +45,7 @@ public List queryOntologyTerm(String termFilter, List sortOrders) { // returned by JPA -> UnmodifiableRandomAccessList List termList = ontologyTermLookup.query(termFilter, ontologyAbbreviations, offset, - 50, sortOrders); + limit, sortOrders); // the list must be modifiable for spring security to filter it return new ArrayList<>(termList); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/sample/SamplePreview.java b/project-management/src/main/java/life/qbic/projectmanagement/application/sample/SamplePreview.java index ccafa0a0f..0d7feb773 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/sample/SamplePreview.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/sample/SamplePreview.java @@ -3,7 +3,6 @@ import jakarta.persistence.AttributeOverride; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Embedded; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; @@ -11,13 +10,11 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import java.util.Objects; -import life.qbic.projectmanagement.domain.model.Ontology; import life.qbic.projectmanagement.domain.model.batch.Batch; import life.qbic.projectmanagement.domain.model.experiment.BiologicalReplicate; import life.qbic.projectmanagement.domain.model.experiment.Experiment; import life.qbic.projectmanagement.domain.model.experiment.ExperimentId; import life.qbic.projectmanagement.domain.model.experiment.ExperimentalGroup; -import life.qbic.projectmanagement.domain.model.experiment.repository.jpa.OntologyClassAttributeConverter; import life.qbic.projectmanagement.domain.model.experiment.vocabulary.OntologyClassDTO; import life.qbic.projectmanagement.domain.model.sample.Sample; import life.qbic.projectmanagement.domain.model.sample.SampleCode; @@ -54,11 +51,8 @@ public class SamplePreview { @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "experimentalGroupId") private ExperimentalGroup experimentalGroup; - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO species; - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO specimen; - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO analyte; protected SamplePreview() { diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/Experiment.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/Experiment.java index 9d564ff0d..cfe242733 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/Experiment.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/Experiment.java @@ -1,7 +1,6 @@ package life.qbic.projectmanagement.domain.model.experiment; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embedded; import jakarta.persistence.EmbeddedId; @@ -18,7 +17,6 @@ import life.qbic.projectmanagement.domain.model.experiment.exception.ConditionExistsException; import life.qbic.projectmanagement.domain.model.experiment.exception.ExperimentalVariableExistsException; import life.qbic.projectmanagement.domain.model.experiment.exception.ExperimentalVariableNotDefinedException; -import life.qbic.projectmanagement.domain.model.experiment.repository.jpa.OntologyClassAttributeConverter; import life.qbic.projectmanagement.domain.model.experiment.vocabulary.OntologyClassDTO; @@ -43,13 +41,15 @@ public class Experiment { private ExperimentalDesign experimentalDesign; @ElementCollection(targetClass = OntologyClassDTO.class) - @Convert(converter = OntologyClassAttributeConverter.class) + @Column(name = "analytes", columnDefinition = "longtext CHECK (json_valid(`analyte`))") + //FIXME should be `analyte`in the database and here private List analytes = new ArrayList<>(); @ElementCollection(targetClass = OntologyClassDTO.class) - @Convert(converter = OntologyClassAttributeConverter.class) + @Column(name = "species", columnDefinition = "longtext CHECK (json_valid(`species`))") private List species = new ArrayList<>(); @ElementCollection(targetClass = OntologyClassDTO.class) - @Convert(converter = OntologyClassAttributeConverter.class) + @Column(name = "specimens", columnDefinition = "longtext CHECK (json_valid(`specimen`))") + //FIXME should be `specimen`in the database and here private List specimens = new ArrayList<>(); diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/repository/jpa/OntologyClassAttributeConverter.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/repository/jpa/OntologyClassAttributeConverter.java index 344433891..ad2ac2c76 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/repository/jpa/OntologyClassAttributeConverter.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/repository/jpa/OntologyClassAttributeConverter.java @@ -6,9 +6,10 @@ import jakarta.persistence.Converter; import life.qbic.projectmanagement.domain.model.experiment.vocabulary.OntologyClassDTO; -@Converter() -public class OntologyClassAttributeConverter implements AttributeConverter { +@Converter(autoApply = true) + +public class OntologyClassAttributeConverter implements + AttributeConverter { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -23,11 +24,11 @@ public String convertToDatabaseColumn(OntologyClassDTO attribute) { @Override public OntologyClassDTO convertToEntityAttribute(String dbData) { - try { - return objectMapper.readValue(dbData, OntologyClassDTO.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + try { + return objectMapper.readValue(dbData, OntologyClassDTO.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/vocabulary/OntologyClassDTO.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/vocabulary/OntologyClassDTO.java index 0c1799153..fe8ee66c8 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/vocabulary/OntologyClassDTO.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/experiment/vocabulary/OntologyClassDTO.java @@ -1,5 +1,6 @@ package life.qbic.projectmanagement.domain.model.experiment.vocabulary; +import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serial; import java.io.Serializable; import java.util.Objects; @@ -19,57 +20,59 @@ public class OntologyClassDTO implements Serializable { @Serial private static final long serialVersionUID = 1459801951948902353L; - String ontology; - - String ontologyVersion; - - String ontologyIri; - - String label; - - String name; - - String description; - - String classIri; + @JsonProperty("ontology") //FIXME should be ontologyAbbreviation in the database and here + private String ontologyAbbreviation; + @JsonProperty("ontologyVersion") + private String ontologyVersion; + @JsonProperty("ontologyIri") + private String ontologyIri; + @JsonProperty("label") //FIXME should be classLabel in the database and here + private String classLabel; + @JsonProperty("name") //FIXME should be className in the database and here + private String className; + @JsonProperty("description") + private String description; + @JsonProperty("classIri") + private String classIri; public OntologyClassDTO() { } /** - * @param ontology - the abbreviation of the ontology a class/term belongs + * @param ontologyAbbreviation - the abbreviation of the ontology a class/term belongs * to * @param ontologyVersion - the version of the ontology * @param ontologyIri - the iri of this ontology (e.g. link to the owl) - * @param label - a humanly readable label for the term - * @param name - the identifier unique for this ontology and term + * @param classLabel - a humanly readable classLabel for the term + * @param className - the identifier unique for this ontology and term * (e.g. NCBITaxon_9606) * @param description - an optional description of the term * @param classIri - the iri where this specific class is found/described */ - public OntologyClassDTO(String ontology, String ontologyVersion, String ontologyIri, - String label, String name, String description, String classIri) { - this.ontology = ontology; + public OntologyClassDTO(String ontologyAbbreviation, String ontologyVersion, String ontologyIri, + String classLabel, String className, String description, String classIri) { + this.ontologyAbbreviation = ontologyAbbreviation; this.ontologyVersion = ontologyVersion; this.ontologyIri = ontologyIri; - this.label = label; - this.name = name; + this.classLabel = classLabel; + this.className = className; this.description = description; this.classIri = classIri; } public static OntologyClassDTO from(OntologyClassEntity lookupEntity) { - return new OntologyClassDTO(lookupEntity.getOntology(), lookupEntity.getOntologyVersion(), - lookupEntity.getOntologyIri(), lookupEntity.getLabel(), lookupEntity.getName(), + return new OntologyClassDTO(lookupEntity.getOntologyAbbreviation(), + lookupEntity.getOntologyVersion(), + lookupEntity.getOntologyIri(), lookupEntity.getClassLabel(), lookupEntity.getClassName(), lookupEntity.getDescription(), lookupEntity.getClassIri()); } - public String getOntology() { - return ontology; + public String getOntologyAbbreviation() { + return ontologyAbbreviation; } - public void setOntology(String ontology) { - this.ontology = ontology; + public void setOntologyAbbreviation(String ontologyAbbreviation) { + this.ontologyAbbreviation = ontologyAbbreviation; } public String getOntologyVersion() { @@ -89,19 +92,19 @@ public void setOntologyIri(String ontologyIri) { } public String getLabel() { - return label; + return classLabel; } public void setLabel(String termLabel) { - this.label = termLabel; + this.classLabel = termLabel; } public String getName() { - return name; + return className; } public void setName(String name) { - this.name = name; + this.className = name; } public String getDescription() { @@ -130,30 +133,51 @@ public void setClassIri(String classIri) { * @return a formatted String representing the name and ontology */ public String formatted() { - return "%s (%s)".formatted(name, - Ontology.findOntologyByAbbreviation(ontology).getName()); + return "%s (%s)".formatted(className, + Ontology.findOntologyByAbbreviation(ontologyAbbreviation).getName()); } @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(Object object) { + if (this == object) { return true; } - if (o == null || getClass() != o.getClass()) { + if (object == null || getClass() != object.getClass()) { return false; } - OntologyClassDTO that = (OntologyClassDTO) o; - return Objects.equals(ontology, that.ontology) && Objects.equals( - ontologyVersion, that.ontologyVersion) && Objects.equals(ontologyIri, - that.ontologyIri) && Objects.equals(label, that.label) && Objects.equals( - name, that.name) && Objects.equals(description, that.description) - && Objects.equals(classIri, that.classIri); + + OntologyClassDTO that = (OntologyClassDTO) object; + + if (!Objects.equals(ontologyAbbreviation, that.ontologyAbbreviation)) { + return false; + } + if (!Objects.equals(ontologyVersion, that.ontologyVersion)) { + return false; + } + if (!Objects.equals(ontologyIri, that.ontologyIri)) { + return false; + } + if (!Objects.equals(classLabel, that.classLabel)) { + return false; + } + if (!Objects.equals(className, that.className)) { + return false; + } + if (!Objects.equals(description, that.description)) { + return false; + } + return Objects.equals(classIri, that.classIri); } @Override public int hashCode() { - return Objects.hash(ontology, ontologyVersion, ontologyIri, label, name, - description, classIri); + int result = ontologyAbbreviation != null ? ontologyAbbreviation.hashCode() : 0; + result = 31 * result + (ontologyVersion != null ? ontologyVersion.hashCode() : 0); + result = 31 * result + (ontologyIri != null ? ontologyIri.hashCode() : 0); + result = 31 * result + (classLabel != null ? classLabel.hashCode() : 0); + result = 31 * result + (className != null ? className.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (classIri != null ? classIri.hashCode() : 0); + return result; } - } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/sample/SampleOrigin.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/sample/SampleOrigin.java index e8c4a4ee4..c751661f8 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/sample/SampleOrigin.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/sample/SampleOrigin.java @@ -1,9 +1,7 @@ package life.qbic.projectmanagement.domain.model.sample; -import jakarta.persistence.Convert; import java.util.Objects; import life.qbic.projectmanagement.domain.model.experiment.Experiment; -import life.qbic.projectmanagement.domain.model.experiment.repository.jpa.OntologyClassAttributeConverter; import life.qbic.projectmanagement.domain.model.experiment.vocabulary.OntologyClassDTO; /** @@ -18,11 +16,8 @@ */ public class SampleOrigin { - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO species; - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO specimen; - @Convert(converter = OntologyClassAttributeConverter.class) private OntologyClassDTO analyte; protected SampleOrigin() { diff --git a/project-management/src/test/groovy/life/qbic/projectmanagement/application/ExperimentInformationServiceSpec.groovy b/project-management/src/test/groovy/life/qbic/projectmanagement/application/ExperimentInformationServiceSpec.groovy index c2acfe911..35b0356f7 100644 --- a/project-management/src/test/groovy/life/qbic/projectmanagement/application/ExperimentInformationServiceSpec.groovy +++ b/project-management/src/test/groovy/life/qbic/projectmanagement/application/ExperimentInformationServiceSpec.groovy @@ -6,10 +6,7 @@ import life.qbic.projectmanagement.domain.model.experiment.ExperimentId import life.qbic.projectmanagement.domain.model.experiment.ExperimentalValue import life.qbic.projectmanagement.domain.model.experiment.ExperimentalVariable import life.qbic.projectmanagement.domain.model.experiment.repository.ExperimentRepository -import life.qbic.projectmanagement.domain.model.experiment.vocabulary.Analyte import life.qbic.projectmanagement.domain.model.experiment.vocabulary.OntologyClassDTO -import life.qbic.projectmanagement.domain.model.experiment.vocabulary.Species -import life.qbic.projectmanagement.domain.model.experiment.vocabulary.Specimen import life.qbic.projectmanagement.domain.repository.ProjectRepository import spock.lang.Specification @@ -29,7 +26,7 @@ class ExperimentInformationServiceSpec extends Specification { when: "specimens are added to an experiment" OntologyClassDTO specimen1 = new OntologyClassDTO(); OntologyClassDTO specimen2 = new OntologyClassDTO("ontology", "ontologyVersion", "ontologyIri", - "label", "name", "description", "classIri"); + "classLabel", "name", "description", "classIri"); experimentInformationService.addSpecimenToExperiment(experiment.experimentId(), specimen1, specimen2) then: "the experiment contains the added specimens" @@ -47,7 +44,7 @@ class ExperimentInformationServiceSpec extends Specification { when: "analytes are added to an experiment" OntologyClassDTO analyte2 = new OntologyClassDTO(); OntologyClassDTO analyte1 = new OntologyClassDTO("ontology", "ontologyVersion", "ontologyIri", - "label", "name", "description", "classIri"); + "classLabel", "name", "description", "classIri"); experimentInformationService.addAnalyteToExperiment(experiment.experimentId(), analyte1, analyte2) then: "the experiment contains the added analytes" @@ -65,7 +62,7 @@ class ExperimentInformationServiceSpec extends Specification { when: "species are added to an experiment" OntologyClassDTO species1 = new OntologyClassDTO(); OntologyClassDTO species2 = new OntologyClassDTO("ontology", "ontologyVersion", "ontologyIri", - "label", "name", "description", "classIri"); + "classLabel", "name", "description", "classIri"); OntologyClassDTO species3 = new OntologyClassDTO(); experimentInformationService.addSpeciesToExperiment(experiment.experimentId(), species1, species2, species3) diff --git a/user-interface/frontend/themes/datamanager/components/dialog.css b/user-interface/frontend/themes/datamanager/components/dialog.css index 1dc8f2de8..2532211ab 100644 --- a/user-interface/frontend/themes/datamanager/components/dialog.css +++ b/user-interface/frontend/themes/datamanager/components/dialog.css @@ -167,6 +167,12 @@ vaadin-dialog-overlay::part(title) { align-items: baseline; } +.contact-field .contact-selection, +.contact-field .input-fields, +.contact-field .input-fields > * { + width: 100%; +} + .edit-project-dialog .form-content .contact-field .name-field { flex-grow: 1; } @@ -262,7 +268,7 @@ vaadin-dialog-overlay::part(title) { gap: var(--lumo-space-l); } -.project-creation-dialog::part(content) { +.add-project-dialog::part(content) { display: flex; flex-direction: column; height: 100%; @@ -271,25 +277,23 @@ vaadin-dialog-overlay::part(title) { padding: 0; } -.project-creation-dialog::part(overlay) { - width: 100%; +.add-project-dialog::part(overlay) { + width: 66%; height: 100%; - max-width: 66%; - max-height: 100% } -.project-creation-dialog::part(footer) { +.add-project-dialog::part(footer) { background-color: transparent; justify-content: space-between; padding-inline: 4rem; } -.project-creation-dialog .footer-right-buttons-container { +.add-project-dialog .footer-right-buttons-container { gap: var(--lumo-space-m); display: inline-flex; } -.project-creation-dialog .collaborators-layout { +.add-project-dialog .collaborators-layout { width: 100%; height: 100%; gap: 1em; @@ -297,38 +301,27 @@ vaadin-dialog-overlay::part(title) { flex-direction: column; } -.project-creation-dialog .layout-container { +.add-project-dialog .layout-container { /*dialog content paddings*/ height: 100%; padding: 1rem 4rem 3rem 4rem } -.project-creation-dialog .collaborators-layout { - gap: var(--lumo-space-m); - display: inline-flex; -} - -.project-creation-dialog .collaborators-layout .contact-field .name-field { - width: 100%; -} - -.project-creation-dialog .collaborators-layout .contact-field .email-field { - width: 100%; -} - -.project-creation-dialog .experiment-information-layout{ +.add-project-dialog .experiment-information-layout { width: 100%; height: 100%; gap: 1em; + /*gap: var(--lumo-space-m)*/ display: flex; + /*display: inline-flex:*/ flex-direction: column; } -.project-creation-dialog .experiment-information-layout .experiment-name-field { +.add-project-dialog .experiment-information-layout .experiment-name-field { width: 50% } -.project-creation-dialog .funding-information-layout { +.add-project-dialog .funding-information-layout { width: 100%; height: 100%; gap: 1em; @@ -336,17 +329,18 @@ vaadin-dialog-overlay::part(title) { flex-direction: column; } -.project-creation-dialog .funding-information-layout .funding-field .grant-label-field { +.add-project-dialog +.add-project-dialog .funding-information-layout .funding-field .grant-label-field { min-width: 15vw; } -.project-creation-dialog .funding-information-layout .funding-field .input-fields { +.add-project-dialog .funding-information-layout .funding-field .input-fields { width: 100%; gap: var(--lumo-space-xl); display: inline-flex; } -.project-creation-dialog .project-design-layout { +.add-project-dialog .project-design-layout { width: 100%; height: 100%; gap: 1em; @@ -354,26 +348,26 @@ vaadin-dialog-overlay::part(title) { flex-direction: column; } -.project-creation-dialog .project-design-layout .description-field { +.add-project-dialog .project-design-layout .description-field { height: 100%; width: 100%; min-height: 15vh; } -.project-creation-dialog .project-design-layout .title-field { +.add-project-dialog .project-design-layout .title-field { width: 100%; margin-left: var(--lumo-space-m); } -.project-creation-dialog .project-design-layout .code-field { +.add-project-dialog .project-design-layout .code-field { max-width: 20%; } -.project-creation-dialog .project-design-layout .search-field { +.add-project-dialog .project-design-layout .search-field { max-width: 30%; } -.project-creation-dialog .project-design-layout .code-and-title { +.add-project-dialog .project-design-layout .code-and-title { width: 100%; gap: var(--lumo-space-s); display: inline-flex; diff --git a/user-interface/frontend/themes/datamanager/components/info.css b/user-interface/frontend/themes/datamanager/components/info.css index 4666f4326..6fd7cee51 100644 --- a/user-interface/frontend/themes/datamanager/components/info.css +++ b/user-interface/frontend/themes/datamanager/components/info.css @@ -33,10 +33,13 @@ gap: 4rem; } +.contact-item, .person-contact-display { display: grid; } + +.contact-item .contact-email, .person-contact-display .email { font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color); diff --git a/user-interface/frontend/themes/datamanager/components/page-area.css b/user-interface/frontend/themes/datamanager/components/page-area.css index e842fbfb8..d5de7d998 100644 --- a/user-interface/frontend/themes/datamanager/components/page-area.css +++ b/user-interface/frontend/themes/datamanager/components/page-area.css @@ -1,4 +1,8 @@ /* All custom css for components which extend the page-area component go in here! */ +.page { + height: 100%; + width: 100%; +} .page-area { background-color: var(--lumo-base-color); diff --git a/user-interface/pom.xml b/user-interface/pom.xml index 4eac030b9..9ebe2ddd3 100644 --- a/user-interface/pom.xml +++ b/user-interface/pom.xml @@ -126,6 +126,11 @@ com.mysql mysql-connector-j + + mysql + mysql-connector-java + 8.0.28 + com.h2database h2 diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBinderValidation.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBinderValidation.java new file mode 100644 index 000000000..d3c05155d --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBinderValidation.java @@ -0,0 +1,71 @@ +package life.qbic.datamanager.views.general; + +import static java.util.Objects.requireNonNull; + +import com.vaadin.flow.component.shared.HasValidationProperties; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.BinderValidationStatus; +import com.vaadin.flow.data.binder.ValidationResult; + +/** + * Mixin interface for components that provide a binder for validation. + * Sets properties for invalid state and error message string to show when invalid. + */ +public interface HasBinderValidation extends HasValidationProperties { + + /** + * Returns a binder used for validation. Must not be null! + *

+ * This binder is used for validation. + * + * @return the binder used for validation + */ + Binder getBinder(); + + /** + * The default error message if all of the failing validators provide a blank error messages. Can + * be empty, or null. + * + * @return the default error message + */ + default String getDefaultErrorMessage() { + return null; + } + + /** + * Validates based on the binder returned by {@link #getBinder()}. Updates the validation status + * and sets an appropriate error message. + *

+ * By default, the first failing validator's error message is set as the error message. If the + * error message is blank, the next non-blank error message is set. + *

+ * If all error messages are blank or null, {@link #getDefaultErrorMessage()} is used to set the + * error message. + * + * @return this instance. + */ + default HasBinderValidation validate() { + requireNonNull(getBinder(), "getBinder() must not be null"); + BinderValidationStatus validationStatus = getBinder().validate(); + setInvalid(validationStatus.hasErrors()); + setErrorMessage(validationStatus + .getValidationErrors().stream() + .filter(it -> !it.getErrorMessage().isBlank()) + .findFirst() + .map(ValidationResult::getErrorMessage) + .orElse(getDefaultErrorMessage())); + return this; + } + + /** + * Missing method from HasValidationProperties. Indiates whether this is marked as invalid. + *

+ * Does not perform validation. + * + * @return true if no invalidation marker exists, false otherwise + */ + default boolean isValid() { + return !isInvalid(); + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyComponent.java index 394c9209c..fa5ffc7a3 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyComponent.java @@ -11,7 +11,8 @@ @Tag(Tag.DIV) public class OntologyComponent extends Component implements HasComponents { public OntologyComponent(OntologyClassDTO contentDTO) { - String ontologyName = Ontology.findOntologyByAbbreviation(contentDTO.getOntology()).getName(); + String ontologyName = Ontology.findOntologyByAbbreviation(contentDTO.getOntologyAbbreviation()) + .getName(); addClassName("ontology-component"); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/Stepper.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/Stepper.java index 6a44fafcf..674267cc6 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/general/Stepper.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/Stepper.java @@ -9,11 +9,12 @@ import com.vaadin.flow.component.avatar.AvatarVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.shared.Registration; import java.io.Serial; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Supplier; import org.slf4j.Logger; import org.springframework.util.CollectionUtils; @@ -24,37 +25,52 @@ * defined steps

*/ -@SpringComponent public class Stepper extends Div { private static final Logger log = getLogger(Stepper.class); - private final List stepList = new ArrayList<>(); - private Step selectedStep; + private final List stepList = new ArrayList<>(); + private StepIndicator selectedStep; + private final Supplier separator; - public Stepper() { + public Stepper(Supplier separatorSupplier) { + this.separator = separatorSupplier; addClassName("stepper"); log.debug("New instance for {} {}{} created", this.getClass().getSimpleName(), "#", System.identityHashCode(this)); } - /** * Add a listener that is called, when a new {@link StepSelectedEvent event} is emitted. * * @param listener a listener that should be called + * @return */ - public void addListener(ComponentEventListener listener) { + public Registration addStepSelectionListener(ComponentEventListener listener) { Objects.requireNonNull(listener); - addListener(StepSelectedEvent.class, listener); + return addListener(StepSelectedEvent.class, listener); + } + + + public boolean isLastStep(StepIndicator stepIndicator) { + return stepList.lastIndexOf(stepIndicator) == stepList.size() - 1; } + public boolean isFirstStep(StepIndicator stepIndicator) { + return stepList.indexOf(stepIndicator) == 0; + } /** - * Creates and adds a new Step to the stepper with the provided label + * Creates and adds a new StepIndicator to the stepper with the provided label * * @param label the label with which the step should be created */ - public Step addStep(String label) { - Step newStep = createStep(label); + public StepIndicator addStep(String label) { + StepIndicator newStep = createStep(label); + if (!stepList.isEmpty()) { + Component separator = this.separator.get(); + separator.addClassName("separator-" + stepList.size()); + add(separator); + } + stepList.add(newStep); add(newStep); return newStep; } @@ -65,144 +81,133 @@ public Step addStep(String label) { * * @param step the step to be removed from the stepper */ - public void removeStep(Step step) { + public void removeStep(StepIndicator step) { + int stepIndex = stepList.lastIndexOf(step); + remove(step); + stepList.remove(step); + if (getChildren().anyMatch(component -> component.equals(step))) { remove(step); } stepList.remove(step); } - /** - * Adds a component to the stepper which should not act as a step - * - * @param component the component to be removed from the stepper - */ - public void addComponent(Component component) { - add(component); - } - - - /** - * Removes a component from the stepper - * - * @param component the component to be removed from the stepper - */ - public void removeComponent(Component component) { - if (getChildren().anyMatch(cmp -> cmp.equals(component))) { - remove(component); - } - } /** * Specifies to which step the stepper should be set * * @param step the step to which the stepper should be set - * @param fromClient indicates if the step was selected by the client */ - public void setSelectedStep(Step step, boolean fromClient) { - if (selectedStep != null && stepList.contains(step)) { - Step originalStep = getSelectedStep(); - setStepAsActive(step); - selectedStep = step; - fireStepSelected(this, getSelectedStep(), originalStep, fromClient); - } else { - selectedStep = step; + public void setSelectedStep(StepIndicator step) { + if (step == null) { + return; + } + if (!stepList.contains(step)) { + return; } + StepIndicator originalStep = getSelectedStep(); + setStepAsActive(step); + selectedStep = step; + fireStepSelected(this, getSelectedStep(), originalStep); } /** * Specifies that the stepper should be set to the next step if possible * - * @param fromClient indicates if the step was selected by the client */ - public void selectNextStep(boolean fromClient) { - Step originalStep = getSelectedStep(); + public void selectNextStep() { + StepIndicator originalStep = getSelectedStep(); int originalIndex = stepList.indexOf(originalStep); if (originalIndex < stepList.size() - 1) { - setSelectedStep(stepList.get(originalIndex + 1), fromClient); + setSelectedStep(stepList.get(originalIndex + 1)); } } /** * Specifies that the stepper should be set to the previous step if possible * - * @param fromClient indicates if the step was selected by the client */ - public void selectPreviousStep(boolean fromClient) { - Step originalStep = getSelectedStep(); + public void selectPreviousStep() { + StepIndicator originalStep = getSelectedStep(); int currentIndex = stepList.indexOf(originalStep); if (currentIndex > 0) { - setSelectedStep(stepList.get(currentIndex - 1), fromClient); + setSelectedStep(stepList.get(currentIndex - 1)); } } /** * Returns the currently selected step in the Stepper component */ - public Step getSelectedStep() { + public StepIndicator getSelectedStep() { return selectedStep; } /** * Returns a list of defined steps within the Stepper component */ - public List getDefinedSteps() { + public List getDefinedSteps() { return stepList; } /** * Returns the first defined step in the Stepper component */ - public Step getFirstStep() { + public StepIndicator getFirstStep() { return CollectionUtils.firstElement(stepList); } /** * Returns the last defined step in the Stepper component */ - public Step getLastStep() { + public StepIndicator getLastStep() { return CollectionUtils.lastElement(stepList); } - private void setStepAsActive(Step activatableStep) { - selectedStep.getElement().setAttribute("selected", false); + private void setStepAsActive(StepIndicator activatableStep) { + if (selectedStep != null) { + selectedStep.getElement().setAttribute("selected", false); + } activatableStep.getElement().setAttribute("selected", true); } - private Step createStep(String label) { + private StepIndicator createStep(String label) { String stepNumber = String.valueOf(stepList.size() + 1); Avatar stepAvatar = new Avatar(stepNumber); stepAvatar.addClassName("avatar"); stepAvatar.addThemeVariants(AvatarVariant.LUMO_XSMALL); - Step step = new Step(stepAvatar, new Span(label)); + StepIndicator step = new StepIndicator(stepAvatar, label); step.addClassName("step"); step.setEnabled(false); - stepList.add(step); - setSelectedStep(getFirstStep(), false); return step; } - private void fireStepSelected(Div source, Step selectedStep, Step previousStep, - boolean fromClient) { + private void fireStepSelected(Div source, StepIndicator selectedStep, + StepIndicator previousStep) { var stepSelectedEvent = new StepSelectedEvent(source, - selectedStep, previousStep, fromClient); + selectedStep, previousStep, false); fireEvent(stepSelectedEvent); } - public static class Step extends Div { + public static class StepIndicator extends Div { private final Avatar avatar; + private final String label; - public Step(Avatar avatar, Component label) { + + public StepIndicator(Avatar avatar, String label) { this.avatar = avatar; + this.label = label; this.add(avatar); - this.add(label); + this.add(new Span(label)); } public Avatar getAvatar() { return avatar; } + public String getLabel() { + return label; + } } public static class StepSelectedEvent extends @@ -210,20 +215,20 @@ public static class StepSelectedEvent extends @Serial private static final long serialVersionUID = -8239112805330234097L; - private final Step selectedStep; - private final Step previousStep; + private final StepIndicator selectedStep; + private final StepIndicator previousStep; /** * Creates a new event using the given source and indicator whether the event originated from * the client side or the server side. * - * @param selectedStep the Step which was selected when this event was triggered - * @param previousStep the previously selected Step + * @param selectedStep the StepIndicator which was selected when this event was triggered + * @param previousStep the previously selected StepIndicator * @param fromClient true if the event originated from the client * side, false otherwise */ - public StepSelectedEvent(Div source, Step selectedStep, Step previousStep, + public StepSelectedEvent(Div source, StepIndicator selectedStep, StepIndicator previousStep, boolean fromClient) { super(source, fromClient); this.selectedStep = selectedStep; @@ -233,14 +238,14 @@ public StepSelectedEvent(Div source, Step selectedStep, Step previousStep, /** * Provides the step which was selected to trigger this event */ - public Step getSelectedStep() { + public StepIndicator getSelectedStep() { return selectedStep; } /** * Provides the step which was selected before the event was triggered */ - public Step getPreviousStep() { + public StepIndicator getPreviousStep() { return previousStep; } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/AutocompleteContactField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/AutocompleteContactField.java new file mode 100644 index 000000000..0e0af8f2e --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/AutocompleteContactField.java @@ -0,0 +1,179 @@ +package life.qbic.datamanager.views.general.contact; + +import static java.util.Objects.isNull; + +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.customfield.CustomField; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.validator.EmailValidator; +import java.util.ArrayList; +import java.util.List; +import life.qbic.datamanager.views.general.HasBinderValidation; + +/** + * A component for contact person input + * + *

Provides components to add a contact person with a name and email. + * Includes methods for basic input validation.

+ * + * @since 1.0.0 + */ +public class AutocompleteContactField extends CustomField implements + HasBinderValidation { + + + private final ComboBox contactSelection; + + private final ComboBox nameField; + private final ComboBox emailField; + private final Binder binder; + + public AutocompleteContactField(String label) { + setLabel(label); + addClassName("contact-field"); + binder = new Binder<>(); + binder.addStatusChangeListener(event -> updateValidationProperty()); + + contactSelection = new ComboBox<>(); + contactSelection.addClassName("contact-selection"); + contactSelection.setPlaceholder("(Optional) select from existing contacts"); + contactSelection.setAllowCustomValue(false); + contactSelection.setClearButtonVisible(true); + contactSelection.setRenderer(new ComponentRenderer<>(AutocompleteContactField::renderContact)); + contactSelection.setItemLabelGenerator( + contact -> "%s - %s".formatted(contact.getFullName(), contact.getEmail())); + + nameField = new ComboBox<>(); + nameField.setAllowCustomValue(true); + nameField.addCustomValueSetListener( + customValueSet -> customValueSet.getSource().setValue(customValueSet.getDetail())); + nameField.setRequired(false); + nameField.addClassName("name-field"); + nameField.setPlaceholder("Please enter a name"); + + emailField = new ComboBox<>(); + emailField.setAllowCustomValue(true); + emailField.addCustomValueSetListener( + customValueSet -> customValueSet.getSource().setValue(customValueSet.getDetail())); + emailField.setRequired(false); + emailField.addClassName("email-field"); + emailField.setPlaceholder("Please enter an email address"); + + contactSelection.addValueChangeListener(this::onContactSelectionChanged); //write only + + binder.forField(nameField) + .withValidator(it -> !isRequired() + || !(isNull(it) || it.isBlank()), "Please provide a name") + .withValidator(it -> isNull(it) || !it.isBlank() || emailField.isEmpty(), + "Please provide a name") // when an email is provided require a name as well + .bind(Contact::getFullName, + Contact::setFullName); + + binder.forField(emailField) + .withValidator(it -> !isRequired() || !(isNull(it) || it.isBlank()), + "Please provide an email address") + .withValidator(new EmailValidator( + "The email address '{0}' is invalid. Please provide a valid email name@domain.de", + !isRequired())) + .withValidator(it -> isNull(it) || !it.isBlank() || nameField.isEmpty(), + "Please provide an email address") // when a name is provided require an email as well + .bind(Contact::getEmail, + Contact::setEmail); + + Div layout = new Div(nameField, emailField); + layout.addClassName("input-fields"); + add(contactSelection, layout); + setItems(new ArrayList<>()); + clear(); + } + + private void updateValidationProperty() { + this.getElement().setProperty("invalid", !binder.isValid()); + } + + private static Div renderContact(Contact contact) { + var contactName = new Span(contact.getFullName()); + contactName.addClassName("contact-name"); + var contactEmail = new Span(contact.getEmail()); + contactEmail.addClassName("contact-email"); + var container = new Div(); + container.addClassName("contact-item"); + container.add(contactName, contactEmail); + return container; + } + + private void onContactSelectionChanged( + ComponentValueChangeEvent, Contact> valueChanged) { + //ignore clearing the combobox or empty selection + if (valueChanged.getValue() == null) { + return; + } + if (valueChanged.getValue().isEmpty()) { + return; + } + // update the contact to the selected value + setContact(valueChanged.getValue()); + // clear selection box + valueChanged.getHasValue().clear(); + } + + public void setContact(Contact contact) { + binder.setBean(contact); + updateValidationProperty(); + } + + public void setItems(List contacts) { + List fullNames = contacts.stream() + .map(Contact::getFullName) + .distinct() + .toList(); + List emails = contacts.stream() + .map(Contact::getEmail) + .distinct() + .toList(); + + contactSelection.setItems(contacts); + nameField.setItems(fullNames); + emailField.setItems(emails); + } + + @Override + protected Contact generateModelValue() { + return new Contact(nameField.getValue(), emailField.getValue()); + } + + @Override + protected void setPresentationValue(Contact contact) { + nameField.setValue(contact.getFullName()); + emailField.setValue(contact.getEmail()); + } + + + /** + * Sets the component to required + * + * @param required whether the user is required to setProjectInformation the field + */ + public void setRequired(boolean required) { + nameField.setRequired(required); + emailField.setRequired(required); + setRequiredIndicatorVisible(required); + } + + public boolean isRequired() { + return isRequiredIndicatorVisible(); + } + + @Override + public Contact getEmptyValue() { + return new Contact(nameField.getEmptyValue(), emailField.getEmptyValue()); + } + + @Override + public Binder getBinder() { + return binder; + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/Contact.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/Contact.java index a474f4ebc..eccbb2df7 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/Contact.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/Contact.java @@ -1,7 +1,5 @@ package life.qbic.datamanager.views.general.contact; -import static java.util.Objects.requireNonNull; - import java.io.Serial; import java.io.Serializable; import java.util.Objects; @@ -22,8 +20,6 @@ public final class Contact implements Serializable { private String email; public Contact(String fullName, String email) { - requireNonNull(fullName, "fullName must not be null"); - requireNonNull(email, "email must not be null"); this.fullName = fullName; this.email = email; } @@ -44,7 +40,18 @@ public String getEmail() { return email; } + public boolean isEmpty() { + return (fullName == null || fullName.isBlank()) && (email == null || email.isBlank()); + } + + public boolean isComplete() { + return fullName != null && !fullName.isBlank() && email != null && !email.isBlank(); + } + public life.qbic.projectmanagement.domain.model.project.Contact toDomainContact() { + if (!isComplete()) { + throw new RuntimeException("Contact is not complete and cannot be converted: " + this); + } return new life.qbic.projectmanagement.domain.model.project.Contact(getFullName(), getEmail()); } @Override diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/ProjectFormLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/ProjectFormLayout.java deleted file mode 100644 index 4fcd14d27..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/ProjectFormLayout.java +++ /dev/null @@ -1,301 +0,0 @@ -package life.qbic.datamanager.views.projects; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.html.Div; -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.TextArea; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.data.binder.Binder; -import com.vaadin.flow.data.value.ValueChangeMode; -import com.vaadin.flow.spring.annotation.SpringComponent; -import com.vaadin.flow.spring.annotation.UIScope; -import jakarta.validation.constraints.NotEmpty; -import java.io.Serial; -import java.io.Serializable; -import java.util.Objects; -import life.qbic.datamanager.views.general.contact.ContactField; -import life.qbic.datamanager.views.general.funding.FundingField; -import life.qbic.datamanager.views.projects.edit.EditProjectInformationDialog.ProjectInformation; -import life.qbic.finances.api.Offer; -import life.qbic.finances.api.OfferSummary; -import life.qbic.projectmanagement.domain.model.project.ProjectCode; -import life.qbic.projectmanagement.domain.model.project.ProjectObjective; -import life.qbic.projectmanagement.domain.model.project.ProjectTitle; - -/** - * Project Form Layout - * - *

Used to style and list the common elements of Project Edit functionality

- * - * @since 1.0.0 - */ -@SpringComponent -@UIScope -public class ProjectFormLayout extends FormLayout { - - @Serial - private static final long serialVersionUID = 972380320581239752L; - private final Div projectContactsLayout = new Div(); - private final Binder binder; - private final TextField titleField; - private final TextArea projectObjective; - private final ContactField principalInvestigatorField; - private final ContactField responsiblePersonField; - private final ContactField projectManagerField; - - private final FundingField fundingField; - - public ProjectFormLayout() { - super(); - - addClassName("form-content"); - binder = new Binder<>(); - binder.setBean(new ProjectInformation()); - - titleField = new TextField("Title"); - titleField.addClassName("title"); - titleField.setId("project-title-field"); - titleField.setRequired(true); - restrictProjectTitleLength(); - binder.forField(titleField) - .withValidator(it -> !it.isBlank(), "Please provide a project title") - .bind((ProjectInformation::getProjectTitle), - ProjectInformation::setProjectTitle); - - projectObjective = new TextArea("Objective"); - projectObjective.setRequired(true); - restrictProjectObjectiveLength(); - binder.forField(projectObjective) - .withValidator(value -> !value.isBlank(), "Please provide an objective") - .bind((ProjectInformation::getProjectObjective), - ProjectInformation::setProjectObjective); - - fundingField = new FundingField("Funding Information"); - binder.forField(fundingField).withValidator(value -> { - if (value == null) { - return true; - } - return !value.getReferenceId().isBlank() || value.getLabel().isBlank(); - }, "Please provide the grant ID for the given grant") - .withValidator(value -> { - if (value == null) { - return true; - } - return value.getReferenceId().isBlank() || !value.getLabel().isBlank(); - }, - "Please provide the grant for the given grant ID.") - .bind( - projectInformation -> projectInformation.getFundingEntry().orElse(null), - ProjectInformation::setFundingEntry); - - projectContactsLayout.setClassName("project-contacts"); - - Span projectContactsTitle = new Span("Project Contacts"); - projectContactsTitle.addClassName("title"); - - Span projectContactsDescription = new Span("Important contact people of the project"); - - projectContactsLayout.add(projectContactsTitle); - projectContactsLayout.add(projectContactsDescription); - - principalInvestigatorField = new ContactField("Principal Investigator"); - principalInvestigatorField.setRequired(true); - principalInvestigatorField.setId("principal-investigator"); - binder.forField(principalInvestigatorField) - .bind((ProjectInformation::getPrincipalInvestigator), - ProjectInformation::setPrincipalInvestigator); - - responsiblePersonField = new ContactField("Project Responsible (optional)"); - responsiblePersonField.setRequired(false); - responsiblePersonField.setId("responsible-person"); - responsiblePersonField.setHelperText("Should be contacted about project-related questions"); - binder.forField(responsiblePersonField) - .bind(projectInformation -> projectInformation.getResponsiblePerson().orElse(null), - (projectInformation, contact) -> { - if (contact.getFullName().isEmpty() || contact.getEmail().isEmpty()) { - projectInformation.setResponsiblePerson(null); - } else { - projectInformation.setResponsiblePerson(contact); - } - }); - - projectManagerField = new ContactField("Project Manager"); - projectManagerField.setRequired(true); - projectManagerField.setId("project-manager"); - binder.forField(projectManagerField) - .bind((ProjectInformation::getProjectManager), - ProjectInformation::setProjectManager); - } - - private static void addConsumedLengthHelper(TextField textField, String newValue) { - int maxLength = textField.getMaxLength(); - int consumedLength = newValue.length(); - textField.setHelperText(consumedLength + "/" + maxLength); - } - - private static void addConsumedLengthHelper(TextArea textArea, String newValue) { - int maxLength = textArea.getMaxLength(); - int consumedLength = newValue.length(); - textArea.setHelperText(consumedLength + "/" + maxLength); - } - - public ProjectFormLayout buildEditProjectLayout() { - add( - titleField, - projectObjective, - fundingField, - projectContactsLayout, - principalInvestigatorField, - responsiblePersonField, - projectManagerField - ); - setColspan(titleField, 2); - setColspan(projectObjective, 2); - setColspan(fundingField, 2); - setColspan(principalInvestigatorField, 2); - setColspan(responsiblePersonField, 2); - setColspan(projectManagerField, 2); - - return this; - } - - public ProjectFormLayout buildAddProjectLayout(ComboBox offerSearchField, - TextField codeField) { - - Button generateCodeButton = new Button(new Icon(VaadinIcon.REFRESH)); - generateCodeButton.getElement().setAttribute("aria-label", "Generate Code"); - generateCodeButton.setId("generate-code-btn"); - generateCodeButton.addThemeVariants(ButtonVariant.LUMO_ICON); - generateCodeButton.addClickListener( - buttonClickEvent -> codeField.setValue(ProjectCode.random().value())); - - Span codeAndTitleLayout = new Span(); - codeAndTitleLayout.addClassName("code-and-title"); - codeAndTitleLayout.add(codeField, generateCodeButton, titleField); - - add( - offerSearchField, - codeAndTitleLayout, - projectObjective, - fundingField, - projectContactsLayout, - principalInvestigatorField, - responsiblePersonField, - projectManagerField - ); - setColspan(offerSearchField, 2); - setColspan(codeAndTitleLayout, 2); - setColspan(projectObjective, 2); - setColspan(fundingField, 2); - setColspan(principalInvestigatorField, 2); - setColspan(responsiblePersonField, 2); - setColspan(projectManagerField, 2); - - return this; - } - - public void fillProjectInformationFromOffer(Offer offer) { - titleField.setValue(offer.title()); - projectObjective.setValue(offer.objective().replace("\n", " ")); - } - - public void validate() { - binder.validate(); - principalInvestigatorField.validate(); - responsiblePersonField.validate(); - projectManagerField.validate(); - } - - /** - * Resets the values and validity of all components that implement value storing and validity - * interfaces - */ - public void reset() { - principalInvestigatorField.clear(); - projectManagerField.clear(); - fundingField.clear(); - binder.setBean(new ProjectInformation()); - } - - private void restrictProjectObjectiveLength() { - projectObjective.setValueChangeMode(ValueChangeMode.EAGER); - projectObjective.setMaxLength((int) ProjectObjective.maxLength()); - addConsumedLengthHelper(projectObjective, projectObjective.getValue()); - projectObjective.addValueChangeListener( - e -> addConsumedLengthHelper(e.getSource(), e.getValue())); - } - - private void restrictProjectTitleLength() { - titleField.setMaxLength((int) ProjectTitle.maxLength()); - titleField.setValueChangeMode(ValueChangeMode.EAGER); - addConsumedLengthHelper(titleField, titleField.getValue()); - titleField.addValueChangeListener(e -> addConsumedLengthHelper(e.getSource(), e.getValue())); - } - - public Binder getBinder() { - return binder; - } - - public static final class ProjectDraft implements Serializable { - - @Serial - private static final long serialVersionUID = 1997619416908358254L; - private final String offerId = ""; - private ProjectInformation projectInformation = new ProjectInformation(); - @NotEmpty - private String projectCode = ""; - - public String getOfferId() { - return offerId; - } - - public String getProjectCode() { - return projectCode; - } - - public void setProjectCode(String projectCode) { - this.projectCode = projectCode; - } - - public ProjectInformation getProjectInformation() { - return projectInformation; - } - - public void setProjectInformation(ProjectInformation projectInformation) { - this.projectInformation = projectInformation; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null || getClass() != object.getClass()) { - return false; - } - - ProjectDraft that = (ProjectDraft) object; - - if (!Objects.equals(offerId, that.offerId)) { - return false; - } - if (!Objects.equals(projectInformation, that.projectInformation)) { - return false; - } - return Objects.equals(projectCode, that.projectCode); - } - - @Override - public int hashCode() { - int result = offerId != null ? offerId.hashCode() : 0; - result = 31 * result + (projectInformation != null ? projectInformation.hashCode() : 0); - result = 31 * result + (projectCode != null ? projectCode.hashCode() : 0); - return result; - } - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java index 529fd0f2d..dd460c655 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java @@ -1,5 +1,8 @@ package life.qbic.datamanager.views.projects.create; +import static java.util.Objects.requireNonNull; + +import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEvent; import com.vaadin.flow.component.ComponentEventListener; @@ -11,17 +14,22 @@ 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.shared.Registration; import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.UIScope; import java.io.Serial; -import java.util.Objects; +import java.util.HashMap; +import java.util.Map; +import life.qbic.datamanager.views.general.HasBinderValidation; import life.qbic.datamanager.views.general.Stepper; -import life.qbic.datamanager.views.general.Stepper.Step; +import life.qbic.datamanager.views.general.Stepper.StepIndicator; +import life.qbic.datamanager.views.general.contact.Contact; import life.qbic.datamanager.views.general.funding.FundingEntry; import life.qbic.datamanager.views.projects.create.CollaboratorsLayout.ProjectCollaborators; import life.qbic.datamanager.views.projects.create.ExperimentalInformationLayout.ExperimentalInformation; import life.qbic.datamanager.views.projects.create.ProjectDesignLayout.ProjectDesign; import life.qbic.finances.api.FinanceService; +import life.qbic.projectmanagement.application.ContactRepository; import life.qbic.projectmanagement.application.OntologyTermInformationService; import life.qbic.projectmanagement.domain.model.project.Project; @@ -38,59 +46,128 @@ public class AddProjectDialog extends Dialog { @Serial private static final long serialVersionUID = 7643754818237178416L; - private final Div dialogContent = new Div(); - private static final String TITLE = "Create Project"; - private final Stepper stepper = new Stepper(); + private final Div dialogContent; + private final Stepper stepper; private final ProjectDesignLayout projectDesignLayout; private final FundingInformationLayout fundingInformationLayout; private final CollaboratorsLayout collaboratorsLayout; private final ExperimentalInformationLayout experimentalInformationLayout; - private final Button confirmButton = new Button("Confirm"); - private final Button cancelButton = new Button("Cancel"); - private final Button backButton = new Button("Back"); - private final Button nextButton = new Button("Next"); - private Step projectDesignStep; - private Step fundingInformationStep; - private Step projectCollaboratorsStep; - private Step experimentalInformationStep; + + private final Button confirmButton; + private final Button backButton; + private final Button nextButton; + + private final Map stepContent; + + + private StepIndicator addStep(Stepper stepper, String label, Component layout) { + stepContent.put(label, layout); + return stepper.addStep(label); + } public AddProjectDialog(FinanceService financeService, - OntologyTermInformationService ontologyTermInformationService) { + OntologyTermInformationService ontologyTermInformationService, + ContactRepository contactRepository) { super(); - Objects.requireNonNull(financeService, - financeService.getClass().getSimpleName() + " must not be null"); - Objects.requireNonNull(ontologyTermInformationService, - ontologyTermInformationService.getClass().getSimpleName() + " must not be null"); + addClassName("add-project-dialog"); + requireNonNull(financeService, "financeService must not be null"); + requireNonNull(ontologyTermInformationService, + "ontologyTermInformationService must not be null"); this.projectDesignLayout = new ProjectDesignLayout(financeService); this.fundingInformationLayout = new FundingInformationLayout(); this.collaboratorsLayout = new CollaboratorsLayout(); this.experimentalInformationLayout = new ExperimentalInformationLayout( ontologyTermInformationService); - initDialog(); - initListeners(); - addClassName("project-creation-dialog"); - } - private void initDialog() { - setHeaderTitle(TITLE); - add(generateSectionDivider(), stepper, generateSectionDivider(), dialogContent, - generateSectionDivider()); - initStepper(); + collaboratorsLayout.setPrincipalInvestigators(contactRepository.findAll().stream() + .map(contact -> new Contact(contact.fullName(), contact.emailAddress())).toList()); + collaboratorsLayout.setProjectManagers(contactRepository.findAll().stream() + .map(contact -> new Contact(contact.fullName(), contact.emailAddress())).toList()); + + stepContent = new HashMap<>(); + + setHeaderTitle("Create Project"); + dialogContent = new Div(); dialogContent.addClassName("layout-container"); - nextButton.addClassName("primary"); - confirmButton.addClassName("primary"); + + stepper = new Stepper(this::createArrowSpan); + add(generateSectionDivider(), + stepper, + generateSectionDivider(), + dialogContent, + generateSectionDivider()); + + StepIndicator projectDesign = addStep(stepper, "Project Design", projectDesignLayout); + addStep(stepper, "Funding Information", fundingInformationLayout); + addStep(stepper, "Project Collaborators", collaboratorsLayout); + addStep(stepper, "Experimental Information", experimentalInformationLayout); + stepper.setSelectedStep(projectDesign); + + nextButton = new Button("Next"); + nextButton.addClassNames("primary", "next"); + nextButton.addClickListener(this::onNextClicked); + + confirmButton = new Button("Confirm"); + confirmButton.addClassNames("primary", "confirm"); + confirmButton.addClickListener(this::onConfirmClicked); + + setDialogContent(stepper.getFirstStep()); + + stepper.addStepSelectionListener( + stepSelectedEvent -> { + setDialogContent(stepSelectedEvent.getSelectedStep()); + adaptFooterButtons(stepSelectedEvent.getSelectedStep()); + }); + + Button cancelButton = new Button("Cancel"); + cancelButton.addClassName("cancel"); + cancelButton.addClickListener(this::onCancelClicked); + backButton = new Button("Back"); + backButton.addClassName("back"); + backButton.addClickListener(this::onBackClicked); + + DialogFooter footer = getFooter(); + Div rightButtons = new Div(); + rightButtons.addClassName("footer-right-buttons-container"); + rightButtons.add(cancelButton, nextButton, confirmButton); + footer.add(backButton, rightButtons); adaptFooterButtons(stepper.getFirstStep()); } - private void initStepper() { - projectDesignStep = stepper.addStep("Project Design"); - stepper.addComponent(createArrowSpan()); - fundingInformationStep = stepper.addStep("Funding Information"); - stepper.addComponent(createArrowSpan()); - projectCollaboratorsStep = stepper.addStep("Project Collaborators"); - stepper.addComponent(createArrowSpan()); - experimentalInformationStep = stepper.addStep("Experimental Information"); + + private void onCancelClicked(ClickEvent