diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/ProjectInformationService.java b/project-management/src/main/java/life/qbic/projectmanagement/application/ProjectInformationService.java index 199476166..ae3306a71 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/ProjectInformationService.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/ProjectInformationService.java @@ -153,7 +153,14 @@ public void setResponsibility(ProjectId projectId, Contact contact) { } @PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'WRITE')") - public void stateObjective(ProjectId projectId, String objective) { + public void removeResponsibility(ProjectId projectId) { + Project project = loadProject(projectId); + project.removeResponsiblePerson(); + projectRepository.update(project); + } + + @PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'WRITE')") + public void updateObjective(ProjectId projectId, String objective) { ProjectObjective projectObjective = ProjectObjective.create(objective); Project project = loadProject(projectId); project.stateObjective(projectObjective); @@ -161,7 +168,7 @@ public void stateObjective(ProjectId projectId, String objective) { } @PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'WRITE')") - public void addFunding(ProjectId projectId, String label, String referenceId) { + public void setFunding(ProjectId projectId, String label, String referenceId) { Funding funding = Funding.of(label, referenceId); var project = loadProject(projectId); project.setFunding(funding); diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/project/Project.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/project/Project.java index 77a24887f..3b4e964c0 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/project/Project.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/project/Project.java @@ -166,6 +166,10 @@ public void addExperiment(Experiment experiment) { lastModified = Instant.now(); } + public void removeResponsiblePerson() { + responsiblePerson = null; + } + public void stateObjective(ProjectObjective projectObjective) { Objects.requireNonNull(projectObjective); if (projectObjective.equals(projectIntent.objective())) { @@ -278,6 +282,10 @@ public List experiments() { return experiments.stream().map(Experiment::experimentId).toList(); } + public Instant getLastModified() { + return lastModified; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/project-management/src/test/groovy/life/qbic/projectmanagement/application/ProjectInformationServiceSpec.groovy b/project-management/src/test/groovy/life/qbic/projectmanagement/application/ProjectInformationServiceSpec.groovy index 5705bc453..6ed5b111b 100644 --- a/project-management/src/test/groovy/life/qbic/projectmanagement/application/ProjectInformationServiceSpec.groovy +++ b/project-management/src/test/groovy/life/qbic/projectmanagement/application/ProjectInformationServiceSpec.groovy @@ -91,7 +91,7 @@ class ProjectInformationServiceSpec extends Specification { when: "the project objective is updated for a project" String projectObjective = "All your objectives are belong to us" - projectInformationService.stateObjective(project.getId(), projectObjective) + projectInformationService.updateObjective(project.getId(), projectObjective) then: "the project intent contains the new project objective" project.projectIntent.objective().objective() == projectObjective diff --git a/user-interface/frontend/themes/datamanager/components/all.css b/user-interface/frontend/themes/datamanager/components/all.css new file mode 100644 index 000000000..8ace66eaf --- /dev/null +++ b/user-interface/frontend/themes/datamanager/components/all.css @@ -0,0 +1,209 @@ +.flex-horizontal { + display: flex; + flex-direction: row; +} + +.heading-with-icon { + color: #878787; + column-gap: var(--lumo-space-s); + display: flex; + flex-direction: row; + font-weight: bold; + line-height: 1; + font-size: var(--lumo-space-m); +} + +.heading-with-icon vaadin-icon { + height: auto; +} + +.section { + width: 100%; +} + +.full-width { + width: 100%; +} + +.section-header { + display: flex; + flex-direction: column; + width: 100%; +} + + +.trailing-margin-large { + margin-bottom: var(--lumo-space-xl); +} + +.trailing-margin-normal { + margin-bottom: var(--lumo-space-l); +} + +.trailing-margin-small { + margin-bottom: var(--lumo-space-s); +} + +.section-header-row { + display: flex; + width: 100%; +} + +.section-content { + display: flex; + flex-direction: column; + width: 100%; + row-gap: var(--lumo-space-m); +} + +.section-title { + font-weight: bold; + color: var(--lumo-secondary-text-color); + margin-bottom: 0.5rem; + width: 100%; +} + +.font-size-small { + font-size: var(--lumo-font-size-l); +} + +.font-size-medium { + font-size: var(--lumo-font-size-xl); +} + +.font-size-large { + font-size: var(--lumo-font-size-xxl); +} + +.sub-header { + font-size: var(--lumo-font-size-s); + color: #878787; + margin-bottom: 0.5rem; +} + +.tag-list { + width: 100%; + display: flex; +} + +.detail-box { + display: flex; + flex-direction: column; + border: 1px solid lightgray; + border-radius: var(--lumo-space-s); + max-height: 10rem; +} + +.detail-box-header { + display: flex; + flex-direction: row; + width: 100%; + gap: var(--lumo-space-s); + font-size: var(--lumo-font-size-m); + font-weight: bold; + color: #878787; + line-height: 1; +} + +.detail-box-header vaadin-icon { + height: auto; +} + +.detail-box-child { + border-width: 1px 1px 1px 1px; + border-color: lightgray; + padding: var(--lumo-space-m) var(--lumo-space-m) var(--lumo-space-m); +} + +.detail-box-child:first-child { + border-width: 0 0 1px 0; + border-style: solid; + border-color: lightgray; +} + +.icon-label { + font-size: var(--lumo-font-size-m); + line-height: 1; + display: flex; + flex-direction: row; + gap: var(--lumo-space-l); +} + +.icon-label-container { + display: flex; + flex-direction: row; + font-size: var(--lumo-font-size-m); + color: #878787; + font-weight: bold; + gap: var(--lumo-space-s); +} + +.icon-label-container vaadin-icon { + height: auto; +} + +.vertical-list { + display: flex; + flex-direction: column; + width: 100%; +} + +.horizontal-list { + display: flex; + flex-direction: row; + width: 100%; +} + +.dynamic-growing-flex-item { + flex-grow: 1; +} + +.wrapping-flex-container { + flex-wrap: wrap; +} + +.overflow-hidden-ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.gap-large { + gap: var(--lumo-space-l); +} + +.gap-medium { + gap: var(--lumo-space-m); +} + +.gap-small { + gap: var(--lumo-space-s); +} + +.allow-row-wrap { + flex-wrap: wrap; +} + +.overflow-scroll-height { + overflow: auto; +} + +.ontology-term { + width: 100%; + display: flex; + flex-direction: row; + gap: var(--lumo-space-s); + white-space: nowrap; + flex-wrap: wrap; +} + +.fixed-medium-width { + width: 25rem; +} + +.simple-paragraph { + font-size: var(--lumo-font-size-m); + margin-bottom: var(--lumo-space-m); + max-width: 40em; +} + diff --git a/user-interface/frontend/themes/datamanager/components/custom.css b/user-interface/frontend/themes/datamanager/components/custom.css index 7966200d3..f52334d70 100644 --- a/user-interface/frontend/themes/datamanager/components/custom.css +++ b/user-interface/frontend/themes/datamanager/components/custom.css @@ -1,6 +1,7 @@ /* This file defines the styling and layout for all custom components. The components are sorted alphabetically */ /*Make sure to import the css classes for each custom component to avoid cluttering the master-style-sheet */ +@import "all.css"; @import "button.css"; @import "card.css"; @import "combobox.css"; diff --git a/user-interface/frontend/themes/datamanager/components/dialog.css b/user-interface/frontend/themes/datamanager/components/dialog.css index 4ac0beb3f..804aaa782 100644 --- a/user-interface/frontend/themes/datamanager/components/dialog.css +++ b/user-interface/frontend/themes/datamanager/components/dialog.css @@ -18,6 +18,10 @@ vaadin-dialog-overlay::part(title) { margin-inline-start: 0; } +.large-dialog::part(overlay) { + width: 66vw; +} + /* set the width of the notification */ .notification-dialog::part(overlay) { width: 36.75rem; diff --git a/user-interface/frontend/themes/datamanager/components/page-area.css b/user-interface/frontend/themes/datamanager/components/page-area.css index d65319483..3ecfaf364 100644 --- a/user-interface/frontend/themes/datamanager/components/page-area.css +++ b/user-interface/frontend/themes/datamanager/components/page-area.css @@ -6,7 +6,7 @@ .page-area { background-color: var(--lumo-base-color); - padding: var(--lumo-space-m); + padding: var(--lumo-space-l); flex-direction: column; display: flex; gap: var(--lumo-space-s); diff --git a/user-interface/frontend/themes/datamanager/styles.css b/user-interface/frontend/themes/datamanager/styles.css index 2d1ae19f0..f2aac70d1 100644 --- a/user-interface/frontend/themes/datamanager/styles.css +++ b/user-interface/frontend/themes/datamanager/styles.css @@ -1,2 +1,3 @@ +@import "theme-editor.css"; @import "components/custom.css"; -@import "components/vaadin-custom.css"; +@import "components/vaadin-custom.css"; \ No newline at end of file diff --git a/user-interface/src/main/bundles/dev.bundle b/user-interface/src/main/bundles/dev.bundle index 6df1f911c..9cc2d6a51 100644 Binary files a/user-interface/src/main/bundles/dev.bundle and b/user-interface/src/main/bundles/dev.bundle differ diff --git a/user-interface/src/main/java/life/qbic/datamanager/export/rocrate/ROCreateBuilder.java b/user-interface/src/main/java/life/qbic/datamanager/export/rocrate/ROCreateBuilder.java index 49c4cb44f..dca9bcd09 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/export/rocrate/ROCreateBuilder.java +++ b/user-interface/src/main/java/life/qbic/datamanager/export/rocrate/ROCreateBuilder.java @@ -5,8 +5,6 @@ import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.RoCrate.RoCrateBuilder; -import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; -import edu.kit.datamanager.ro_crate.context.RoCrateMetadataContext; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; import edu.kit.datamanager.ro_crate.entities.data.FileEntity.FileEntityBuilder; diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/Column.java b/user-interface/src/main/java/life/qbic/datamanager/parser/Column.java new file mode 100644 index 000000000..d52dcb78a --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/Column.java @@ -0,0 +1,17 @@ +package life.qbic.datamanager.parser; + +import java.util.Optional; +import life.qbic.datamanager.parser.ExampleProvider.Helper; + +/** + * TODO! + * short description + * + *

detailed description

+ * + * @since + */ +public interface Column { + + Optional getFillHelp(); +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/ExampleProvider.java b/user-interface/src/main/java/life/qbic/datamanager/parser/ExampleProvider.java new file mode 100644 index 000000000..96b79240d --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/ExampleProvider.java @@ -0,0 +1,19 @@ +package life.qbic.datamanager.parser; + +/** + * TODO! + * short description + * + *

detailed description

+ * + * @since + */ +public interface ExampleProvider { + + record Helper(String exampleValue, String description) { + + } + + Helper getHelper(Column column); + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java b/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java index 0e7993cf9..9b00aef0b 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java @@ -132,7 +132,7 @@ private List convertNewProteomicsMeasurement(ParsingResult var technicalReplicateName = parsingResult.getValueOrDefault(i, ProteomicsMeasurementRegisterColumn.TECHNICAL_REPLICATE_NAME.headerName(), ""); var organisationId = parsingResult.getValueOrDefault(i, - ProteomicsMeasurementRegisterColumn.ORGANISATION_ID.headerName(), ""); + ProteomicsMeasurementRegisterColumn.ORGANISATION_URL.headerName(), ""); var msDevice = parsingResult.getValueOrDefault(i, ProteomicsMeasurementRegisterColumn.MS_DEVICE.headerName(), ""); var samplePoolGroup = parsingResult.getValueOrDefault(i, @@ -192,7 +192,7 @@ private List convertExistingProteomicsMeasurement( var technicalReplicateName = parsingResult.getValueOrDefault(i, ProteomicsMeasurementEditColumn.TECHNICAL_REPLICATE_NAME.headerName(), ""); var organisationId = parsingResult.getValueOrDefault(i, - ProteomicsMeasurementEditColumn.ORGANISATION_ID.headerName(), ""); + ProteomicsMeasurementEditColumn.ORGANISATION_URL.headerName(), ""); var msDevice = parsingResult.getValueOrDefault(i, ProteomicsMeasurementEditColumn.MS_DEVICE.headerName(), ""); var samplePoolGroup = parsingResult.getValueOrDefault(i, @@ -252,7 +252,7 @@ private List convertExistingNGSMeasurement(ParsingResult pa "")) ); var organisationId = parsingResult.getValueOrDefault(i, - NGSMeasurementEditColumn.ORGANISATION_ID.headerName(), ""); + NGSMeasurementEditColumn.ORGANISATION_URL.headerName(), ""); var instrument = parsingResult.getValueOrDefault(i, NGSMeasurementEditColumn.INSTRUMENT.headerName(), ""); var facility = parsingResult.getValueOrDefault(i, @@ -304,7 +304,7 @@ private List convertNewNGSMeasurement(ParsingResult parsing "")) ); var organisationId = parsingResult.getValueOrDefault(i, - NGSMeasurementRegisterColumn.ORGANISATION_ID.headerName(), ""); + NGSMeasurementRegisterColumn.ORGANISATION_URL.headerName(), ""); var instrument = parsingResult.getValueOrDefault(i, NGSMeasurementRegisterColumn.INSTRUMENT.headerName(), ""); var facility = parsingResult.getValueOrDefault(i, diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementEditColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementEditColumn.java index 5fef50044..36c276085 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementEditColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementEditColumn.java @@ -1,6 +1,10 @@ package life.qbic.datamanager.parser.measurement; import java.util.Arrays; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; /** * NGS Measurement Columns @@ -10,13 +14,13 @@ * column index and if the column should be set to readOnly in the generated sheet *

*/ -public enum NGSMeasurementEditColumn { +public enum NGSMeasurementEditColumn implements Column { MEASUREMENT_ID("Measurement ID", 0, true, true), SAMPLE_ID("QBiC Sample Id", 1, true, true), SAMPLE_NAME("Sample Name", 2, true, false), POOL_GROUP("Sample Pool Group", 3, true, false), - ORGANISATION_ID("Organisation ID", 4, false, true), + ORGANISATION_URL("Organisation URL", 4, false, true), ORGANISATION_NAME("Organisation Name", 5, true, false), FACILITY("Facility", 6, false, true), INSTRUMENT("Instrument", 7, false, true), @@ -30,15 +34,67 @@ public enum NGSMeasurementEditColumn { COMMENT("Comment", 15, false, false), ; + + private static ExampleProvider exampleProvider = (Column column) -> { + + if (column instanceof NGSMeasurementEditColumn ngsMeasurementEditColumn) { + return switch (ngsMeasurementEditColumn) { + case MEASUREMENT_ID -> new Helper("QBiC Measurement ID", + "A unique identifier of the measurement that will be linked to each sample."); + case SAMPLE_ID -> new Helper("QBiC sample IDs, e.g. Q2001, Q2002", + "The sample(s) that will be linked to the measurement."); + case SAMPLE_NAME -> new Helper("Free text, e.g. RNA Sample 1, RNA Sample 2", + "A visual aid to simplify sample navigation for the person managing the metadata."); + case POOL_GROUP -> new Helper("Free text, e.g. pool group 1", + "A group of samples that are pooled together for a measurement. All samples in a pool group should have the same label."); + case ORGANISATION_URL -> new Helper("ROR URL, e.g. https://ror.org/03a1kwz48", """ + A unique identifier of the organisation where the measurement has been conducted. + Tip: You can click on the column header (%s) to go to the ROR registry website where you can search your organisation and find its ROR URL. + """.formatted(ORGANISATION_URL.headerName())); + case ORGANISATION_NAME -> new Helper("Free text, e.g. University of Tübingen", + "The name of the organisation where the measurement has been conducted."); + case FACILITY -> new Helper("Free text, e.g. Quantitative Biology Centre", + "The facilities name within the organisation (group name, etc.)"); + case INSTRUMENT -> new Helper("CURIE (ontology), e.g. EFO:0008637", """ + The instrument that has been used for the measurement. + We expect an ontology term CURIE. + Tip: You can click on the column header (%s) to go to the Data Manager where you can use our Ontology Search to query the CURIE for your instrument. + """.formatted(INSTRUMENT.headerName())); + case INSTRUMENT_NAME -> new Helper("Free text, e.g. Illumina HiSeq", + "The name of the instrument model that has been used for the measurement."); + case SEQUENCING_READ_TYPE -> new Helper("Free text, e.g. paired-end", + "The sequencing read type used to generate the sequence data."); + case LIBRARY_KIT -> new Helper("Free text, e.g. NEBNext Ultra II Directional RNA mRNA UMI", + "Provides important information for downstream analysis data use that is usually required for troubleshooting."); + case FLOW_CELL -> + new Helper("Free text, e.g. S4", "The flow cell type used for sequencing."); + case SEQUENCING_RUN_PROTOCOL -> new Helper("Free text, e.g. 104+19+10+104", + "Information on how many cycles for each read and index."); + case INDEX_I7 -> new Helper("Free text, e.g. NEBNext UDI UMI Set 1 B12 S789", + "Index used for multiplexing."); + case INDEX_I5 -> new Helper("Free text, e.g. NEBNext UDI UMI Set 1 B12 S579", + "Index used for multiplexing."); + case COMMENT -> + new Helper("Free text", "Notes about the measurement. (Max 500 characters)"); + }; + } else { + throw new IllegalArgumentException( + "Column not of class " + NGSMeasurementEditColumn.class.getName() + " but is " + + column.getClass().getName()); + } + }; private final String headerName; private final int columnIndex; private final boolean readOnly; private final boolean mandatory; + @Override + public Optional getFillHelp() { + return Optional.ofNullable(exampleProvider.getHelper(this)); + } + static int maxColumnIndex() { - return Arrays.stream(values()) - .mapToInt(NGSMeasurementEditColumn::columnIndex) - .max().orElse(0); + return Arrays.stream(values()).mapToInt(NGSMeasurementEditColumn::columnIndex).max().orElse(0); } /** diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementRegisterColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementRegisterColumn.java index 283e91f3a..a17af28c0 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementRegisterColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/NGSMeasurementRegisterColumn.java @@ -1,6 +1,10 @@ package life.qbic.datamanager.parser.measurement; import java.util.Arrays; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; /** * NGS Measurement Columns @@ -10,21 +14,21 @@ * column index and if the column should be set to readOnly in the generated sheet *

*/ -public enum NGSMeasurementRegisterColumn { +public enum NGSMeasurementRegisterColumn implements Column { SAMPLE_ID("QBiC Sample Id", 0, false, true), - SAMPLE_NAME("Sample Name", 1, false, false), + SAMPLE_NAME("Sample Name", 1, true, false), POOL_GROUP("Sample Pool Group", 2, false, false), - ORGANISATION_ID("Organisation ID", 3, false, true), + ORGANISATION_URL("Organisation URL", 3, false, true), FACILITY("Facility", 4, false, true), INSTRUMENT("Instrument", 5, false, true), SEQUENCING_READ_TYPE("Sequencing Read Type", 6, false, true), LIBRARY_KIT("Library Kit", 7, false, false), FLOW_CELL("Flow Cell", 8, false, false), - SEQUENCING_RUN_PROTOCOL("Sequencing Run Protocol", 11, false, false), - INDEX_I7("Index i7", 9, false, false), - INDEX_I5("Index i5", 10, false, false), - COMMENT("Comment", 11, false, false), + SEQUENCING_RUN_PROTOCOL("Sequencing Run Protocol", 9, false, false), + INDEX_I7("Index i7", 10, false, false), + INDEX_I5("Index i5", 11, false, false), + COMMENT("Comment", 12, false, false), ; private final String headerName; @@ -32,6 +36,48 @@ public enum NGSMeasurementRegisterColumn { private final boolean readOnly; private final boolean mandatory; + private static ExampleProvider exampleProvider = (Column column) -> { + if (column instanceof NGSMeasurementRegisterColumn ngsMeasurementRegisterColumn) { + return switch (ngsMeasurementRegisterColumn) { + case SAMPLE_ID -> new Helper("QBiC sample IDs, e.g. Q2001, Q2002", + "The sample(s) that will be linked to the measurement."); + case SAMPLE_NAME -> new Helper("Free text, e.g. RNA Sample 1, RNA Sample 2", + "A visual aid to simplify sample navigation for the person managing the metadata. Is ignored after upload."); + case POOL_GROUP -> new Helper("Free text, e.g. pool group 1", + "A group of samples that are pooled together for a measurement. All samples in a pool group should have the same label."); + case ORGANISATION_URL -> new Helper("ROR URL, e.g. https://ror.org/03a1kwz48", """ + A unique identifier of the organisation where the measurement has been conducted. + Tip: You can click on the column header (%s) to go to the ROR registry website where you can search your organisation and find its ROR URL. + """.formatted(ORGANISATION_URL.headerName())); + case FACILITY -> new Helper("Free text, e.g. Quantitative Biology Centre", + "The facilities name within the organisation (group name, etc.)"); + case INSTRUMENT -> new Helper("CURIE (ontology), e.g. EFO:0008637", """ + The instrument that has been used for the measurement. + We expect an ontology term CURIE. + Tip: You can click on the column header (%s) to go to the Data Manager where you can use our Ontology Search to query the CURIE for your instrument. + """.formatted(INSTRUMENT.headerName())); + case SEQUENCING_READ_TYPE -> new Helper("Free text, e.g. paired-end", + "The sequencing read type used to generate the sequence data."); + case LIBRARY_KIT -> new Helper("Free text, e.g. NEBNext Ultra II Directional RNA mRNA UMI", + "Provides important information for downstream analysis data use that is usually required for troubleshooting."); + case FLOW_CELL -> + new Helper("Free text, e.g. S4", "The flow cell type used for sequencing."); + case SEQUENCING_RUN_PROTOCOL -> new Helper("Free text, e.g. 104+19+10+104", + "Information on how many cycles for each read and index."); + case INDEX_I7 -> new Helper("Free text, e.g. NEBNext UDI UMI Set 1 B12 S789", + "Index used for multiplexing."); + case INDEX_I5 -> new Helper("Free text, e.g. NEBNext UDI UMI Set 1 B12 S579", + "Index used for multiplexing."); + case COMMENT -> + new Helper("Free text", "Notes about the measurement. (Max 500 characters)"); + }; + } else { + throw new IllegalArgumentException( + "Column not of class " + NGSMeasurementRegisterColumn.class.getName() + " but is " + + column.getClass().getName()); + } + }; + static int maxColumnIndex() { return Arrays.stream(values()) .mapToInt(NGSMeasurementRegisterColumn::columnIndex) @@ -60,11 +106,16 @@ public int columnIndex() { return columnIndex; } - public boolean readOnly() { + public boolean isReadOnly() { return readOnly; } public boolean isMandatory() { return mandatory; } + + @Override + public Optional getFillHelp() { + return Optional.ofNullable(exampleProvider.getHelper(this)); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementEditColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementEditColumn.java index db45d76b8..46d942b78 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementEditColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementEditColumn.java @@ -1,5 +1,10 @@ package life.qbic.datamanager.parser.measurement; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; + /** * NGS Measurement Columns * @@ -8,7 +13,7 @@ * column index and if the column should be set to readOnly in the generated sheet *

*/ -public enum ProteomicsMeasurementEditColumn { +public enum ProteomicsMeasurementEditColumn implements Column { MEASUREMENT_ID("Measurement ID", 0, true, true), SAMPLE_ID("QBiC Sample Id", 1, true, true), @@ -16,7 +21,7 @@ public enum ProteomicsMeasurementEditColumn { "Sample Name", 2, true, false), POOL_GROUP("Sample Pool Group", 3, true, false), TECHNICAL_REPLICATE_NAME("Technical Replicate", 4, false, false), - ORGANISATION_ID("Organisation ID", 5, false, true), + ORGANISATION_URL("Organisation URL", 5, false, true), ORGANISATION_NAME("Organisation Name", 6, true, false), FACILITY("Facility", 7, false, true), MS_DEVICE("MS Device", 8, false, true), @@ -36,6 +41,62 @@ public enum ProteomicsMeasurementEditColumn { private final int columnIndex; private final boolean readOnly; private final boolean mandatory; + private static final ExampleProvider exampleProvider = (Column column) -> + { + if (column instanceof ProteomicsMeasurementEditColumn proteomicsMeasurementEditColumn) { + return switch (proteomicsMeasurementEditColumn) { + case MEASUREMENT_ID -> new Helper("QBiC Measurement ID", + "A unique identifier of the measurement that will be linked to each sample."); + case SAMPLE_ID -> new Helper("QBiC sample IDs, e.g. Q29866", + "The sample(s) that will be linked to the measurement."); + case SAMPLE_NAME -> new Helper("Free text, e.g. MySample 01", + "A visual aid to simplify sample navigation for the person managing the metadata. Will be ignored after upload."); + case POOL_GROUP -> new Helper("Free text, e.g. pool group 1", + "A group of samples that are pooled together for a measurement. All samples in a pool group should have the same label."); + case TECHNICAL_REPLICATE_NAME -> new Helper("Free text, e.g. Sample 1A, Sample 1B", + "Repeated measurements of the same sample that represent independent measures of the random noise associated with protocols or equipment."); + case ORGANISATION_URL -> new Helper("ROR URL, e.g. https://ror.org/03a1kwz48", """ + A unique identifier of the organisation where the measurement has been conducted. + Tip: You can click on the column header (%s) to go to the ROR registry website where you can search your organisation and find its ROR URL. + """.formatted(ORGANISATION_URL.headerName())); + case ORGANISATION_NAME -> new Helper("Free text, e.g. University of Tübingen", + "The name of the organisation where the measurement has been conducted."); + case FACILITY -> new Helper("Free text, e.g. Quantitative Biology Center", + "The facility's name within the organisation."); + case MS_DEVICE -> new Helper("CURIE (ontology), e.g. NCIT:C12434", """ + The instrument that has been used for the measurement. + We expect an ontology term CURIE. + Tip: You can click on the column header (%s) to go to the Data Manager where you can use our Ontology Search to query the CURIE for your instrument. + """.formatted(MS_DEVICE.headerName())); + case MS_DEVICE_NAME -> new Helper("Free text, e.g. Illumina HiSeq", + "The name of the MS device model that has been used for the measurement."); + case CYCLE_FRACTION_NAME -> new Helper("Free text, e.g. Fraction01, AB", + "Sometimes a sample is fractionated and all fractions are measured. With this property you can indicate which fraction it is."); + case DIGESTION_METHOD -> new Helper("Enumeration, Select a value from the dropdown", + "Method that has been used to break proteins into peptides. Please use the dropdown menu to select one of the values."); + case DIGESTION_ENZYME -> new Helper("Free text, e.g. Trypsin, Chymotrypsin", + "Information about the enzymes used for the proteolytic."); + case ENRICHMENT_METHOD -> new Helper("Free text, e.g. Phosphopeptide Enrichment", + "Enrichment of proteins or peptides of different characteristics."); + case INJECTION_VOLUME -> new Helper("Whole number, e.g. 1, 6, 8", + "The sample volume injected into the LC column in microliter."); + case LC_COLUMN -> new Helper("Free text, can be a commercial name or brand", + "The type of column that has been used."); + case LCMS_METHOD -> new Helper("Free text", + "Laboratory specific methods that have been used for LCMS measurement."); + case LABELING_TYPE -> new Helper("Free text, e.g. Dimethyl, SILAC", + "The label type that has been used to label the sample for measurement."); + case LABEL -> new Helper("Free text, e.g. Light, Medium, Heavy", + "The label value for the label type that has been used."); + case COMMENT -> + new Helper("Free text", "Notes about the measurement. (Max 500 characters)"); + }; + } else { + throw new IllegalArgumentException( + "Column not of class " + NGSMeasurementEditColumn.class.getName() + " but is " + + column.getClass().getName()); + } + }; ProteomicsMeasurementEditColumn(String headerName, int columnIndex, boolean readOnly, boolean mandatory) { @@ -60,4 +121,9 @@ public boolean isReadOnly() { public boolean isMandatory() { return mandatory; } + + @Override + public Optional getFillHelp() { + return Optional.ofNullable(exampleProvider.getHelper(this)); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementRegisterColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementRegisterColumn.java index 12fdcaadb..8e78d11fc 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementRegisterColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/measurement/ProteomicsMeasurementRegisterColumn.java @@ -1,22 +1,28 @@ package life.qbic.datamanager.parser.measurement; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; + /** - * TODO! - * short description - * - *

detailed description

+ * Proteomics Measurement Columns * - * @since + *

Enumeration of the columns shown in the file used for proteomics measurement registration + * in the context of measurement file based upload. Provides the name of the header column, the + * column index and if the column should be set to readOnly in the generated sheet. Also provides + * information on whether the column is mandatory and can offer some help for filling it. + *

*/ -public enum ProteomicsMeasurementRegisterColumn { +public enum ProteomicsMeasurementRegisterColumn implements Column { - SAMPLE_ID("QBiC Sample Id", 0, true, true), + SAMPLE_ID("QBiC Sample Id", 0, false, true), SAMPLE_NAME( "Sample Name", 1, true, false), - POOL_GROUP("Sample Pool Group", 2, true, false), + POOL_GROUP("Sample Pool Group", 2, false, false), TECHNICAL_REPLICATE_NAME("Technical Replicate", 3, false, false), CYCLE_FRACTION_NAME("Cycle/Fraction Name", 4, false, false), - ORGANISATION_ID("Organisation ID", 5, false, true), + ORGANISATION_URL("Organisation URL", 5, false, true), FACILITY("Facility", 6, false, true), LC_COLUMN("LC Column", 7, false, true), MS_DEVICE("MS Device", 8, false, true), @@ -33,6 +39,55 @@ public enum ProteomicsMeasurementRegisterColumn { private final int columnIndex; private final boolean readOnly; private final boolean mandatory; + private static final ExampleProvider exampleProvider = (Column column) -> { + if (column instanceof ProteomicsMeasurementRegisterColumn proteomicsMeasurementRegisterColumn) { + return switch (proteomicsMeasurementRegisterColumn) { + case SAMPLE_ID -> new Helper("QBiC sample IDs, e.g. Q29866", + "The sample(s) that will be linked to the measurement."); + case SAMPLE_NAME -> new Helper("Free text, e.g. MySample 01", + "A visual aid to simplify sample navigation for the person managing the metadata. Will be ignored after upload."); + case POOL_GROUP -> new Helper("Free text, e.g. pool group 1", + "A group of samples that are pooled together for a measurement. All samples in a pool group should have the same label."); + case TECHNICAL_REPLICATE_NAME -> new Helper("Free text, e.g. Sample 1A, Sample 1B", + "Repeated measurements of the same sample that represent independent measures of the random noise associated with protocols or equipment."); + case ORGANISATION_URL -> new Helper("ROR URL, e.g. https://ror.org/03a1kwz48", """ + A unique identifier of the organisation where the measurement has been conducted. + Tip: You can click on the column header (%s) to go to the ROR registry website where you can search your organisation and find its ROR URL. + """.formatted(ORGANISATION_URL.headerName())); + case FACILITY -> new Helper("Free text, e.g. Quantitative Biology Center", + "The facility's name within the organisation."); + case MS_DEVICE -> new Helper("CURIE (ontology), e.g. NCIT:C12434", """ + The instrument that has been used for the measurement. + We expect an ontology term CURIE. + Tip: You can click on the column header (%s) to go to the Data Manager where you can use our Ontology Search to query the CURIE for your instrument. + """.formatted(MS_DEVICE.headerName())); + case CYCLE_FRACTION_NAME -> new Helper("Free text, e.g. Fraction01, AB", + "Sometimes a sample is fractionated and all fractions are measured. With this property you can indicate which fraction it is."); + case DIGESTION_METHOD -> new Helper("Enumeration, Select a value from the dropdown", + "Method that has been used to break proteins into peptides. Please use the dropdown menu to select one of the values."); + case DIGESTION_ENZYME -> new Helper("Free text, e.g. Trypsin, Chymotrypsin", + "Information about the enzymes used for the proteolytic."); + case ENRICHMENT_METHOD -> new Helper("Free text, e.g. Phosphopeptide Enrichment", + "Enrichment of proteins or peptides of different characteristics."); + case INJECTION_VOLUME -> new Helper("Whole number, e.g. 1,6,8", + "The sample volume injected into the LC column in microliter."); + case LC_COLUMN -> new Helper("Free text, can be a commercial name or brand", + "The type of column that has been used."); + case LCMS_METHOD -> new Helper("Free text", + "Laboratory specific methods that have been used for LCMS measurement."); + case LABELING_TYPE -> new Helper("Free text, e.g. Dimethyl, SILAC", + "The label type that has been used to label the sample for measurement."); + case LABEL -> new Helper("Free text, e.g. Light, Medium, Heavy", + "The label value for the label type that has been used."); + case COMMENT -> + new Helper("Free text", "Notes about the measurement. (Max 500 characters)"); + }; + } else { + throw new IllegalArgumentException( + "Column not of class " + NGSMeasurementRegisterColumn.class.getName() + " but is " + + column.getClass().getName()); + } + }; ProteomicsMeasurementRegisterColumn(String headerName, int columnIndex, boolean readOnly, boolean mandatory) { @@ -57,4 +112,9 @@ public boolean isReadOnly() { public boolean isMandatory() { return mandatory; } + + @Override + public Optional getFillHelp() { + return Optional.ofNullable(exampleProvider.getHelper(this)); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/sample/EditColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/sample/EditColumn.java index a562e8084..ae3aebaef 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/sample/EditColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/sample/EditColumn.java @@ -1,6 +1,10 @@ package life.qbic.datamanager.parser.sample; import java.util.Arrays; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; /** * Sample Edit Columns @@ -10,17 +14,48 @@ * column index and if the column should be set to readOnly in the generated sheet *

*/ -public enum EditColumn { +public enum EditColumn implements Column { SAMPLE_ID("QBiC Sample Id", 0, true, true), SAMPLE_NAME("Sample Name", 1, false, true), ANALYSIS("Analysis to be performed", 2, false, true), BIOLOGICAL_REPLICATE("Biological Replicate", 3, false, false), CONDITION("Condition", 4, false, true), SPECIES("Species", 5, false, true), - ANALYTE("Analyte", 6, false, true), - SPECIMEN("Specimen", 7, false, true), + SPECIMEN("Specimen", 6, false, true), + ANALYTE("Analyte", 7, false, true), COMMENT("Comment", 8, false, false); + private static final ExampleProvider exampleProvider = column -> { + if (column instanceof EditColumn editColumn) { + return switch (editColumn) { + case SAMPLE_ID -> new Helper("QBiC sample IDs, e.g. Q2001, Q2002", + "The sample(s) that will be linked to the measurement."); + case SAMPLE_NAME -> new Helper("Free text, e.g. RNA Sample 1, RNA Sample 2", + "A visual aid to simplify navigation for the person managing the metadata."); + case ANALYSIS -> new Helper("Enumeration, Select a value from the dropdown", + "The test performed on samples for the purpose of finding and measuring chemical substances."); + case BIOLOGICAL_REPLICATE -> new Helper("Free text, e.g. patient1, patient2, Mouse1", """ + Different samples measured accross multiple conditions. + Tip: You can use this column to identifiy whether the samples belong to the same source."""); + case CONDITION -> new Helper("Enumeration, Select a value from the dropdown", """ + A distinct value or condition of the independent variable at which the dependent variable is measured in order to carry out statistical analysis. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case SPECIES -> new Helper("Enumeration, Select a value from the dropdown", """ + Scientific name of the organism(s) from which the biological material is derived. E.g. Homo sapiens, Mus musculus. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case ANALYTE -> new Helper("Enumeration, Select a value from the dropdown", """ + The chemical substance extracted from the biological material that is identified and measured. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case SPECIMEN -> new Helper("Enumeration, Select a value from the dropdown", """ + Name of the biological material from which the analytes would be extracted. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case COMMENT -> new Helper("Free text", "Notes about the sample. (Max 500 characters)"); + }; + } + throw new IllegalArgumentException( + "Column not of class " + EditColumn.class.getName() + " but is " + + column.getClass().getName()); + }; private final String headerName; private final int columnIndex; private final boolean readOnly; @@ -61,4 +96,8 @@ public boolean isMandatory() { return mandatory; } + @Override + public Optional getFillHelp() { + return Optional.ofNullable(exampleProvider.getHelper(this)); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/sample/RegisterColumn.java b/user-interface/src/main/java/life/qbic/datamanager/parser/sample/RegisterColumn.java index ec7c400d2..524b07b8e 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/sample/RegisterColumn.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/sample/RegisterColumn.java @@ -1,6 +1,10 @@ package life.qbic.datamanager.parser.sample; import java.util.Arrays; +import java.util.Optional; +import life.qbic.datamanager.parser.Column; +import life.qbic.datamanager.parser.ExampleProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; /** * Sample Register Columns @@ -10,17 +14,47 @@ * column index and if the column should be set to readOnly in the generated sheet *

*/ -public enum RegisterColumn { +public enum RegisterColumn implements Column { SAMPLE_NAME("Sample Name", 0, false, true), ANALYSIS("Analysis to be performed", 1, false, true), BIOLOGICAL_REPLICATE("Biological Replicate", 2, false, false), CONDITION("Condition", 3, false, true), SPECIES("Species", 4, false, true), - ANALYTE("Analyte", 5, false, true), - SPECIMEN("Specimen", 6, false, true), + SPECIMEN("Specimen", 5, false, true), + ANALYTE("Analyte", 6, false, true), COMMENT("Comment", 7, false, false); + private static final ExampleProvider exampleProvider = column -> { + if (column instanceof RegisterColumn registerColumn) { + return switch (registerColumn) { + case SAMPLE_NAME -> new Helper("Free text, e.g. RNA Sample 1, RNA Sample 2", + "A visual aid to simplify navigation for the person managing the metadata."); + case ANALYSIS -> new Helper("Enumeration, Select a value from the dropdown", + "The test performed on samples for the purpose of finding and measuring chemical substances."); + case BIOLOGICAL_REPLICATE -> new Helper("Free text, e.g. patient1, patient2, Mouse1", """ + Different samples measured accross multiple conditions. + Tip: You can use this column to identifiy whether the samples belong to the same source."""); + case CONDITION -> new Helper("Enumeration, Select a value from the dropdown", """ + A distinct value or condition of the independent variable at which the dependent variable is measured in order to carry out statistical analysis. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case SPECIES -> new Helper("Enumeration, Select a value from the dropdown", """ + Scientific name of the organism(s) from which the biological material is derived. E.g. Homo sapiens, Mus musculus. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case ANALYTE -> new Helper("Enumeration, Select a value from the dropdown", """ + The chemical substance extracted from the biological material that is identified and measured. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case SPECIMEN -> new Helper("Enumeration, Select a value from the dropdown", """ + Name of the biological material from which the analytes would be extracted. + Note: The values in the dropdown are the predefined values from the experimental design."""); + case COMMENT -> new Helper("Free text", "Notes about the sample. (Max 500 characters)"); + }; + } + throw new IllegalArgumentException( + "Column not of class " + RegisterColumn.class.getName() + " but is " + + column.getClass().getName()); + }; + private final String headerName; private final int columnIndex; private final boolean readOnly; @@ -60,4 +94,9 @@ public boolean isReadOnly() { public boolean isMandatory() { return mandatory; } + + @Override + public Optional getFillHelp() { + return Optional.of(exampleProvider.getHelper(this)); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/xlsx/XLSXParser.java b/user-interface/src/main/java/life/qbic/datamanager/parser/xlsx/XLSXParser.java index 6ec69b393..8b6794ed7 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/xlsx/XLSXParser.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/xlsx/XLSXParser.java @@ -64,7 +64,7 @@ private static String readCellAsString(Cell cell) { return switch (cell.getCellType()) { case _NONE, ERROR, FORMULA, BLANK -> ""; case BOOLEAN -> Boolean.toString(cell.getBooleanCellValue()); - case NUMERIC -> String.valueOf(cell.getNumericCellValue()); + case NUMERIC -> Double.toString(cell.getNumericCellValue()); case STRING -> cell.getStringCellValue(); }; } diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/TemplateDownloadFactory.java b/user-interface/src/main/java/life/qbic/datamanager/templates/TemplateDownloadFactory.java index 84491cdf6..5c7d3c485 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/TemplateDownloadFactory.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/TemplateDownloadFactory.java @@ -1,8 +1,8 @@ package life.qbic.datamanager.templates; import life.qbic.datamanager.download.DownloadContentProvider; -import life.qbic.datamanager.templates.measurement.NGSMeasurementTemplate; -import life.qbic.datamanager.templates.measurement.ProteomicsMeasurementTemplate; +import life.qbic.datamanager.templates.measurement.NGSMeasurementRegisterTemplate; +import life.qbic.datamanager.templates.measurement.ProteomicsMeasurementRegisterTemplate; /** * Template Download Factory @@ -17,8 +17,8 @@ public class TemplateDownloadFactory { public static Template provider(TemplateType templateType) { return switch (templateType) { - case MS_MEASUREMENT -> new ProteomicsMeasurementTemplate(); - case NGS_MEASUREMENT -> new NGSMeasurementTemplate(); + case MS_MEASUREMENT -> new ProteomicsMeasurementRegisterTemplate(); + case NGS_MEASUREMENT -> new NGSMeasurementRegisterTemplate(); }; } diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/XLSXTemplateHelper.java b/user-interface/src/main/java/life/qbic/datamanager/templates/XLSXTemplateHelper.java index b57e1eee2..08f333616 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/XLSXTemplateHelper.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/XLSXTemplateHelper.java @@ -3,6 +3,7 @@ import static java.util.Objects.isNull; import static java.util.Objects.nonNull; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -21,6 +22,7 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.SheetVisibility; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.usermodel.DefaultIndexedColorMap; @@ -40,7 +42,9 @@ public class XLSXTemplateHelper { private static final Random RANDOM = new Random(); private static final byte[] DARK_GREY = {(byte) 119, (byte) 119, (byte) 119}; private static final byte[] LIGHT_GREY = {(byte) 220, (byte) 220, (byte) 220}; + private static final byte[] LINK_BLUE = {(byte) 9, (byte) 105, (byte) 218}; private static final int COLUMN_MAX_WIDTH = 255; + private static final String PROPERTY_INFORMATION_SHEET_NAME = "Property Information"; protected XLSXTemplateHelper() { //hide constructor as static methods only are used @@ -157,17 +161,43 @@ public static CellStyle createBoldCellStyle(Workbook workbook) { CellStyle boldStyle = workbook.createCellStyle(); Font fontBold = workbook.createFont(); fontBold.setBold(true); + fontBold.setFontName("Open Sans"); + fontBold.setFontHeightInPoints((short) 12); + boldStyle.setFont(fontBold); + return boldStyle; + } + + public static CellStyle createDefaultCellStyle(Workbook workbook) { + CellStyle boldStyle = workbook.createCellStyle(); + Font fontBold = workbook.createFont(); + fontBold.setFontName("Open Sans"); + fontBold.setFontHeightInPoints((short) 12); + boldStyle.setFont(fontBold); return boldStyle; } + public static CellStyle createLinkHeaderCellStyle(Workbook workbook) { + CellStyle linkHeaderStyle = workbook.createCellStyle(); + XSSFFont linkFont = (XSSFFont) workbook.createFont(); + linkFont.setColor(new XSSFColor(LINK_BLUE, new DefaultIndexedColorMap())); + linkFont.setBold(true); + linkFont.setFontName("Open Sans"); + linkFont.setFontHeightInPoints((short) 12); + + linkHeaderStyle.setFont(linkFont); + return linkHeaderStyle; + } + public static CellStyle createReadOnlyCellStyle(Workbook workbook) { CellStyle readOnlyStyle = workbook.createCellStyle(); readOnlyStyle.setFillForegroundColor(new XSSFColor(LIGHT_GREY, new DefaultIndexedColorMap())); readOnlyStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); XSSFFont font = (XSSFFont) workbook.createFont(); font.setColor(new XSSFColor(DARK_GREY, new DefaultIndexedColorMap())); + font.setFontName("Open Sans"); + font.setFontHeightInPoints((short) 12); readOnlyStyle.setFont(font); return readOnlyStyle; } @@ -180,6 +210,8 @@ public static CellStyle createReadOnlyHeaderCellStyle(Workbook workbook) { XSSFFont fontHeader = (XSSFFont) workbook.createFont(); fontHeader.setBold(true); fontHeader.setColor(new XSSFColor(DARK_GREY, new DefaultIndexedColorMap())); + fontHeader.setFontName("Open Sans"); + fontHeader.setFontHeightInPoints((short) 12); readOnlyHeaderStyle.setFont(fontHeader); return readOnlyHeaderStyle; } @@ -262,6 +294,8 @@ protected static String toCamelCase(String input) { * Adds data validation to an area in the spreadsheet. Requires the valid options to be set * beforehand as a name. This can be done by using {@link #createOptionArea(Sheet, String, List)} * + * Please note: There must not exist any data validation for the cell area provided. + * * @param sheet the sheet in which the validation should be added * @param startColIdx the start column of the validated values >= 0 * @param startRowIdx the start row of the validated values >= 0 @@ -276,17 +310,157 @@ public static void addDataValidation(Sheet sheet, int startColIdx, int startRowI stopRowIdx, startColIdx, stopColIdx); + + if (hasAnyDataValidation(sheet, startRowIdx, startColIdx, stopRowIdx, stopColIdx)) { + throw new IllegalStateException( + "Cannot add data validation as there is already a data validation present at " + + validatedCells.getCellRangeAddress(0).formatAsString(sheet.getSheetName(), true)); + } + DataValidationHelper dataValidationHelper = sheet.getDataValidationHelper(); DataValidationConstraint formulaListConstraint = dataValidationHelper .createFormulaListConstraint(allowedValues.getNameName()); - DataValidation validation = dataValidationHelper.createValidation(formulaListConstraint, - validatedCells); + + var validation = dataValidationHelper.createValidation(formulaListConstraint, validatedCells); + validation.setSuppressDropDownArrow(true); // shows dropdown if true validation.setShowErrorBox(true); validation.createErrorBox("Invalid choice", "Please select a value from the dropdown list."); sheet.addValidationData(validation); } + /** + * Adds a property information description to the workbook. Creates a sheet called + * {@link #PROPERTY_INFORMATION_SHEET_NAME} if it does not exist yet. + * + * @param workbook the workbook to take + * @param columnName the column name / property name to add information to + * @param isMandatory is filling the column mandatory? + * @param descriptionTitle allowed value type and example + * @param description the description of the input + * @param headerStyle the style used for headers in the property information sheet + */ + public static void addPropertyInformation(Workbook workbook, + String columnName, + boolean isMandatory, + String descriptionTitle, + String description, + CellStyle defaultStyle, + CellStyle headerStyle) { + // add row with information + Sheet propertyInformationSheet = Optional + .ofNullable(workbook.getSheet(PROPERTY_INFORMATION_SHEET_NAME)) + .orElseGet(() -> workbook.createSheet(PROPERTY_INFORMATION_SHEET_NAME)); + int lastRowIdx = Math.max(propertyInformationSheet.getLastRowNum(), 0); + if (lastRowIdx == 0) { + //we do not have a header yet + Row row = getOrCreateRow(propertyInformationSheet, 0); + Cell propertyNameCell = getOrCreateCell(row, 0); + propertyNameCell.setCellStyle(headerStyle); + propertyNameCell.setCellValue("Property Name"); + + Cell provisionCell = getOrCreateCell(row, 1); + provisionCell.setCellStyle(headerStyle); + provisionCell.setCellValue("Provision"); + + Cell allowedValuesCell = getOrCreateCell(row, 2); + allowedValuesCell.setCellStyle(headerStyle); + allowedValuesCell.setCellValue("Allowed Values"); + + Cell descriptionCell = getOrCreateCell(row, 3); + descriptionCell.setCellStyle(headerStyle); + descriptionCell.setCellValue("Description"); + + } + lastRowIdx++; + Row row = getOrCreateRow(propertyInformationSheet, lastRowIdx); + Cell propertyNameCell = getOrCreateCell(row, 0); + propertyNameCell.setCellStyle(defaultStyle); + propertyNameCell.setCellValue(columnName); + + Cell provisionCell = getOrCreateCell(row, 1); + provisionCell.setCellStyle(defaultStyle); + provisionCell.setCellValue(isMandatory ? "mandatory" : "optional"); + + Cell allowedValuesCell = getOrCreateCell(row, 2); + allowedValuesCell.setCellStyle(defaultStyle); + allowedValuesCell.setCellValue(descriptionTitle); + + Cell descriptionCell = getOrCreateCell(row, 3); + descriptionCell.setCellStyle(defaultStyle); + descriptionCell.setCellValue(description); + + setColumnAutoWidth(propertyInformationSheet, 0, 3); + } + + + /** + * Adds an input prompt box to cells within the selected range. If there is already a validation + * for exactly those cells, the prompt box of the existing validation is overwritten. + * + * @param sheet the sheet in which the cells are + * @param startColIdx the index of the first column + * @param startRowIdx the index of the first row + * @param stopColIdx the index of the last column + * @param stopRowIdx the index of the last row + * @param title the title of the message in the prompt box + * @param content the content of the prompt box + */ + public static void addInputHelper(Sheet sheet, int startColIdx, int startRowIdx, + int stopColIdx, int stopRowIdx, String title, String content) { + CellRangeAddressList validatedCells = new CellRangeAddressList(startRowIdx, + stopRowIdx, + startColIdx, + stopColIdx); + + var validation = getValidationsExactlyCovering(sheet, validatedCells.getCellRangeAddresses()[0]) + .stream() + .findFirst() // the first is applied first + .orElse(createFakeValidation(sheet, validatedCells)); + + validation.setShowPromptBox(true); + validation.createPromptBox(title, content); + sheet.addValidationData(validation); + } + + private static DataValidation createFakeValidation(Sheet sheet, + CellRangeAddressList validatedCells) { + DataValidationHelper dataValidationHelper = sheet.getDataValidationHelper(); + DataValidationConstraint alwaysTrue = dataValidationHelper.createCustomConstraint("TRUE"); + return dataValidationHelper.createValidation(alwaysTrue, + validatedCells); + } + + private static List getValidationsExactlyCovering(Sheet sheet, + CellRangeAddress cellRangeAddress) { + List validations = new ArrayList<>(); + for (DataValidation dataValidation : sheet.getDataValidations()) { + for (CellRangeAddress rangeAddress : dataValidation.getRegions().getCellRangeAddresses()) { + if (rangeAddress.equals(cellRangeAddress)) { + validations.add(dataValidation); + } + } + } + return validations; + } + + private static boolean hasAnyDataValidation(Sheet sheet, int startRowIdx, int startColIdx, + int stopRowIdx, int stopColIdx) { + for (DataValidation dataValidation : sheet.getDataValidations()) { + CellRangeAddressList regions = dataValidation.getRegions(); + for (int i = 0; i < regions.getCellRangeAddresses().length; i++) { + CellRangeAddress cellRangeAddress = regions.getCellRangeAddress(i); + if (cellRangeAddress.intersects( + new CellRangeAddress(startRowIdx, stopRowIdx, startColIdx, stopColIdx))) { + return true; + } + } + } + return false; + + } + + public static void hideSheet(Workbook workbook, Sheet sheet) { workbook.setSheetVisibility(workbook.getSheetIndex(sheet), SheetVisibility.VERY_HIDDEN); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java index b75de213e..256f16104 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java @@ -1,6 +1,8 @@ package life.qbic.datamanager.templates.measurement; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createBoldCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createLinkHeaderCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyHeaderCellStyle; @@ -13,18 +15,23 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.ApplicationException.ErrorCode; import life.qbic.datamanager.download.DownloadContentProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; import life.qbic.datamanager.parser.measurement.NGSMeasurementEditColumn; import life.qbic.datamanager.templates.XLSXTemplateHelper; import life.qbic.datamanager.views.projects.project.measurements.NGSMeasurementEntry; import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.measurement.NGSMeasurementMetadata; import life.qbic.projectmanagement.domain.model.measurement.NGSMeasurement; +import org.apache.poi.common.usermodel.HyperlinkType; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Hyperlink; import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -47,20 +54,6 @@ public class NGSMeasurementEditTemplate implements DownloadContentProvider { private String fileNamePrefix = DEFAULT_FILE_NAME_PREFIX; private static final int DEFAULT_GENERATED_ROW_COUNT = 200; - private enum SequencingReadType { - SINGLE_END("single-end"), - PAIRED_END("paired-end"); - private final String presentationString; - - SequencingReadType(String presentationString) { - this.presentationString = presentationString; - } - - static List getOptions() { - return Arrays.stream(values()).map(it -> it.presentationString).toList(); - } - } - private static void setAutoWidth(Sheet sheet) { for (int col = 0; col <= NGSMeasurementEditColumn.values().length; col++) { sheet.autoSizeColumn(col); @@ -68,15 +61,14 @@ private static void setAutoWidth(Sheet sheet) { } private static void writeMeasurementIntoRow(NGSMeasurementEntry ngsMeasurementEntry, - Row entryRow, CellStyle readOnlyCellStyle) { - + Row entryRow, CellStyle defaultStyle, CellStyle readOnlyCellStyle) { for (NGSMeasurementEditColumn measurementColumn : NGSMeasurementEditColumn.values()) { var value = switch (measurementColumn) { case MEASUREMENT_ID -> ngsMeasurementEntry.measurementCode(); case SAMPLE_ID -> ngsMeasurementEntry.sampleInformation().sampleId(); case SAMPLE_NAME -> ngsMeasurementEntry.sampleInformation().sampleName(); case POOL_GROUP -> ngsMeasurementEntry.samplePoolGroup(); - case ORGANISATION_ID -> ngsMeasurementEntry.organisationId(); + case ORGANISATION_URL -> ngsMeasurementEntry.organisationId(); case ORGANISATION_NAME -> ngsMeasurementEntry.organisationName(); case FACILITY -> ngsMeasurementEntry.facility(); case INSTRUMENT -> ngsMeasurementEntry.instrumentCURI(); @@ -91,12 +83,13 @@ private static void writeMeasurementIntoRow(NGSMeasurementEntry ngsMeasurementEn }; var cell = getOrCreateCell(entryRow, measurementColumn.columnIndex()); cell.setCellValue(value); + cell.setCellStyle(defaultStyle); if (measurementColumn.isReadOnly()) { cell.setCellStyle(readOnlyCellStyle); } } - - + + } public void setMeasurements(List measurements, String fileNamePrefix) { @@ -117,28 +110,67 @@ public byte[] getContent() { CellStyle readOnlyCellStyle = createReadOnlyCellStyle(workbook); CellStyle readOnlyHeaderStyle = createReadOnlyHeaderCellStyle(workbook); CellStyle boldStyle = createBoldCellStyle(workbook); + CellStyle linkHeaderStyle = createLinkHeaderCellStyle(workbook); + CellStyle defaultStyle = createDefaultCellStyle(workbook); Sheet sheet = workbook.createSheet("NGS Measurement Metadata"); Row header = getOrCreateRow(sheet, 0); - for (NGSMeasurementEditColumn value : NGSMeasurementEditColumn.values()) { - var cell = getOrCreateCell(header, value.columnIndex()); - if (value.isMandatory()) { - cell.setCellValue(value.headerName() + "*"); + for (NGSMeasurementEditColumn column : NGSMeasurementEditColumn.values()) { + var cell = getOrCreateCell(header, column.columnIndex()); + if (column.isMandatory()) { + cell.setCellValue(column.headerName() + "*"); } else { - cell.setCellValue(value.headerName()); + cell.setCellValue(column.headerName()); } cell.setCellStyle(boldStyle); - if (value.isReadOnly()) { + if (column.isReadOnly()) { cell.setCellStyle(readOnlyHeaderStyle); + } else if (column.equals(NGSMeasurementEditColumn.ORGANISATION_URL)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://ror.org"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } else if (column.equals(NGSMeasurementEditColumn.INSTRUMENT)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://rdm.qbic.uni-tuebingen.de"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); } + //add helper to header + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + 0, + column.columnIndex(), + 0, + helper.exampleValue(), + helper.description())); + } + + // add property information order of columns matters!! + for (NGSMeasurementEditColumn column : Arrays.stream( + NGSMeasurementEditColumn.values()) + .sorted(Comparator.comparing(NGSMeasurementEditColumn::columnIndex)).toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultStyle, + boldStyle); } var startIndex = 1; // start in row number 2 with index 1 as the header row has number 1 index 0 int rowIndex = startIndex; for (NGSMeasurementEntry measurement : measurements) { Row row = getOrCreateRow(sheet, rowIndex); - writeMeasurementIntoRow(measurement, row, readOnlyCellStyle); + writeMeasurementIntoRow(measurement, row, defaultStyle, readOnlyCellStyle); rowIndex++; } @@ -157,6 +189,18 @@ public byte[] getContent() { DEFAULT_GENERATED_ROW_COUNT - 1, sequencingReadTypeArea); + for (NGSMeasurementEditColumn column : NGSMeasurementEditColumn.values()) { + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + helper.exampleValue(), + helper.description()) + ); + } + setAutoWidth(sheet); workbook.setActiveSheet(0); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java new file mode 100644 index 000000000..edc91ddb9 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java @@ -0,0 +1,171 @@ +package life.qbic.datamanager.templates.measurement; + +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createBoldCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createLinkHeaderCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyHeaderCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateCell; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateRow; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.hideSheet; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.lockSheet; +import static life.qbic.logging.service.LoggerFactory.logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import life.qbic.application.commons.ApplicationException; +import life.qbic.application.commons.ApplicationException.ErrorCode; +import life.qbic.datamanager.download.DownloadContentProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; +import life.qbic.datamanager.parser.measurement.NGSMeasurementRegisterColumn; +import life.qbic.datamanager.templates.Template; +import life.qbic.datamanager.templates.XLSXTemplateHelper; +import life.qbic.logging.api.Logger; +import life.qbic.projectmanagement.application.measurement.NGSMeasurementMetadata; +import life.qbic.projectmanagement.domain.model.measurement.NGSMeasurement; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Hyperlink; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * NGS Measurement Content Provider + *

+ * Implementation of the {@link DownloadContentProvider} providing the content and file name for any + * files created from {@link NGSMeasurement} and {@link NGSMeasurementMetadata} + *

+ */ +public class NGSMeasurementRegisterTemplate extends Template implements DownloadContentProvider { + + private static final String NGS_MEASUREMENT_TEMPLATE_FILENAME = "ngs_measurement_registration_sheet.xlsx"; + private static final String NGS_MEASUREMENT_TEMPLATE_DOMAIN_NAME = "Genomics Template"; + private static final Logger log = logger(NGSMeasurementRegisterTemplate.class); + private static final int DEFAULT_GENERATED_ROW_COUNT = 200; + + private static void setAutoWidth(Sheet sheet) { + for (int col = 0; col <= NGSMeasurementRegisterColumn.values().length; col++) { + sheet.autoSizeColumn(col); + } + } + + @Override + public String getDomainName() { + return NGS_MEASUREMENT_TEMPLATE_DOMAIN_NAME; + } + + @Override + public byte[] getContent() { + + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + CellStyle readOnlyHeaderStyle = createReadOnlyHeaderCellStyle(workbook); + CellStyle boldStyle = createBoldCellStyle(workbook); + CellStyle linkHeaderStyle = createLinkHeaderCellStyle(workbook); + CellStyle defaultCellStyle = createDefaultCellStyle(workbook); + + Sheet sheet = workbook.createSheet("NGS Measurement Metadata"); + + Row header = getOrCreateRow(sheet, 0); + for (NGSMeasurementRegisterColumn column : NGSMeasurementRegisterColumn.values()) { + var cell = getOrCreateCell(header, column.columnIndex()); + if (column.isMandatory()) { + cell.setCellValue(column.headerName() + "*"); + } else { + cell.setCellValue(column.headerName()); + } + cell.setCellStyle(boldStyle); + if (column.isReadOnly()) { + cell.setCellStyle(readOnlyHeaderStyle); + } else if (column.equals(NGSMeasurementRegisterColumn.ORGANISATION_URL)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://ror.org"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } else if (column.equals(NGSMeasurementRegisterColumn.INSTRUMENT)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://rdm.qbic.uni-tuebingen.de"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } + + //add helper to header + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + 0, + column.columnIndex(), + 0, + helper.exampleValue(), + helper.description()) + ); + } + + var startIndex = 1; // start in row number 2 with index 1 as the header row has number 1 index 0 + // make sure to create the visible sheet first + Sheet hiddenSheet = workbook.createSheet("hidden"); + Name sequencingReadTypeArea = createOptionArea(hiddenSheet, + "Sequencing read type", SequencingReadType.getOptions()); + + XLSXTemplateHelper.addDataValidation(sheet, + NGSMeasurementRegisterColumn.SEQUENCING_READ_TYPE.columnIndex(), + startIndex, + NGSMeasurementRegisterColumn.SEQUENCING_READ_TYPE.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + sequencingReadTypeArea); + + for (NGSMeasurementRegisterColumn column : NGSMeasurementRegisterColumn.values()) { + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + helper.exampleValue(), + helper.description())); + } + + // add property information order of columns matters!! + for (NGSMeasurementRegisterColumn column : Arrays.stream( + NGSMeasurementRegisterColumn.values()) + .sorted(Comparator.comparing(NGSMeasurementRegisterColumn::columnIndex)).toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultCellStyle, + boldStyle); + } + + setAutoWidth(sheet); + workbook.setActiveSheet(0); + + lockSheet(hiddenSheet); + hideSheet(workbook, hiddenSheet); + + workbook.write(byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new ApplicationException(ErrorCode.GENERAL, null); + } + } + + @Override + public String getFileName() { + return NGS_MEASUREMENT_TEMPLATE_FILENAME; + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementTemplate.java deleted file mode 100644 index 3e74ab6cf..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementTemplate.java +++ /dev/null @@ -1,44 +0,0 @@ -package life.qbic.datamanager.templates.measurement; - -import java.io.IOException; -import java.util.Objects; -import life.qbic.datamanager.templates.Template; - -/** - * NGS measurement template - * - *

The Excel spreadsheet containing the required information for measurement registration-

- * - * @since 1.0.0 - */ -public class NGSMeasurementTemplate extends Template { - - private static final String NGS_MEASUREMENT_TEMPLATE_PATH = "templates/ngs_measurement_registration_sheet.xlsx"; - - private static final String NGS_MEASUREMENT_TEMPLATE_FILENAME = "ngs_measurement_registration_sheet.xlsx"; - - private static final String NGS_MEASUREMENT_TEMPLATE_DOMAIN_NAME = "Genomics Template"; - - @Override - public byte[] getContent() { - try { - return Objects.requireNonNull( - getClass().getClassLoader().getResourceAsStream(NGS_MEASUREMENT_TEMPLATE_PATH)) - .readAllBytes(); - } catch (IOException e) { - throw new RuntimeException( - "Cannot get content for template: " + NGS_MEASUREMENT_TEMPLATE_PATH, - e); - } - } - - @Override - public String getFileName() { - return NGS_MEASUREMENT_TEMPLATE_FILENAME; - } - - @Override - public String getDomainName() { - return NGS_MEASUREMENT_TEMPLATE_DOMAIN_NAME; - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java index 7fce85779..10af56e8c 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java @@ -2,6 +2,8 @@ import static life.qbic.datamanager.templates.XLSXTemplateHelper.addDataValidation; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createBoldCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createLinkHeaderCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateCell; @@ -12,11 +14,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.ApplicationException.ErrorCode; import life.qbic.datamanager.download.DownloadContentProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; import life.qbic.datamanager.parser.measurement.ProteomicsMeasurementEditColumn; import life.qbic.datamanager.templates.XLSXTemplateHelper; import life.qbic.datamanager.views.projects.project.measurements.ProteomicsMeasurementEntry; @@ -24,7 +29,10 @@ import life.qbic.projectmanagement.application.measurement.ProteomicsMeasurementMetadata; import life.qbic.projectmanagement.application.measurement.validation.MeasurementProteomicsValidator.DigestionMethod; import life.qbic.projectmanagement.domain.model.measurement.ProteomicsMeasurement; +import org.apache.poi.common.usermodel.HyperlinkType; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Hyperlink; import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -55,7 +63,9 @@ private static void setAutoWidth(Sheet sheet) { } private static void createMeasurementEntry(ProteomicsMeasurementEntry pxpEntry, Row entryRow, + CellStyle defaultStyle, CellStyle readOnlyStyle) { + for (ProteomicsMeasurementEditColumn measurementColumn : ProteomicsMeasurementEditColumn.values()) { var value = switch (measurementColumn) { case MEASUREMENT_ID -> pxpEntry.measurementCode(); @@ -63,7 +73,7 @@ private static void createMeasurementEntry(ProteomicsMeasurementEntry pxpEntry, case SAMPLE_NAME -> pxpEntry.sampleInformation().sampleName(); case POOL_GROUP -> pxpEntry.samplePoolGroup(); case TECHNICAL_REPLICATE_NAME -> pxpEntry.technicalReplicateName(); - case ORGANISATION_ID -> pxpEntry.organisationId(); + case ORGANISATION_URL -> pxpEntry.organisationId(); case ORGANISATION_NAME -> pxpEntry.organisationName(); case FACILITY -> pxpEntry.facility(); case MS_DEVICE -> pxpEntry.msDeviceCURIE(); @@ -81,7 +91,7 @@ private static void createMeasurementEntry(ProteomicsMeasurementEntry pxpEntry, }; var cell = getOrCreateCell(entryRow, measurementColumn.columnIndex()); cell.setCellValue(value); - cell.setCellValue(value); + cell.setCellStyle(defaultStyle); if (measurementColumn.isReadOnly()) { cell.setCellStyle(readOnlyStyle); } @@ -106,6 +116,8 @@ public byte[] getContent() { CellStyle readOnlyHeaderStyle = XLSXTemplateHelper.createReadOnlyHeaderCellStyle(workbook); CellStyle boldStyle = createBoldCellStyle(workbook); CellStyle readOnlyStyle = createReadOnlyCellStyle(workbook); + CellStyle linkHeaderStyle = createLinkHeaderCellStyle(workbook); + CellStyle defaultStyle = createDefaultCellStyle(workbook); Sheet sheet = workbook.createSheet("Proteomics Measurement Metadata"); Row header = getOrCreateRow(sheet, 0); @@ -116,11 +128,47 @@ public byte[] getContent() { } else { cell.setCellValue(measurementColumn.headerName()); } + cell.setCellStyle(boldStyle); if (measurementColumn.isReadOnly()) { cell.setCellStyle(readOnlyHeaderStyle); - } else { - cell.setCellStyle(boldStyle); + } else if (measurementColumn.equals(ProteomicsMeasurementEditColumn.ORGANISATION_URL)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://ror.org"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } else if (measurementColumn.equals(ProteomicsMeasurementEditColumn.MS_DEVICE)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://rdm.qbic.uni-tuebingen.de"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); } + //add helper to header + measurementColumn.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + measurementColumn.columnIndex(), + 0, + measurementColumn.columnIndex(), + 0, + helper.exampleValue(), + helper.description())); + } + + // add property information order of columns matters!! + for (ProteomicsMeasurementEditColumn column : Arrays.stream( + ProteomicsMeasurementEditColumn.values()) + .sorted(Comparator.comparing(ProteomicsMeasurementEditColumn::columnIndex)).toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultStyle, + boldStyle); } var startIndex = 1; // start in row number 2 with index 1 skipping the header in the first row @@ -128,7 +176,7 @@ public byte[] getContent() { for (ProteomicsMeasurementEntry pxpEntry : measurements) { Row entry = getOrCreateRow(sheet, rowIndex); - createMeasurementEntry(pxpEntry, entry, readOnlyStyle); + createMeasurementEntry(pxpEntry, entry, defaultStyle, readOnlyStyle); rowIndex++; } var generatedRowCount = rowIndex - startIndex; @@ -145,6 +193,18 @@ public byte[] getContent() { DEFAULT_GENERATED_ROW_COUNT - 1, digestionMethodArea); + for (ProteomicsMeasurementEditColumn column : ProteomicsMeasurementEditColumn.values()) { + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + helper.exampleValue(), + helper.description()) + ); + } + setAutoWidth(sheet); workbook.setActiveSheet(0); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java new file mode 100644 index 000000000..5c3c4faa1 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java @@ -0,0 +1,173 @@ +package life.qbic.datamanager.templates.measurement; + +import static life.qbic.datamanager.templates.XLSXTemplateHelper.addDataValidation; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createBoldCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createLinkHeaderCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateCell; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateRow; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.hideSheet; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.lockSheet; +import static life.qbic.logging.service.LoggerFactory.logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import life.qbic.application.commons.ApplicationException; +import life.qbic.application.commons.ApplicationException.ErrorCode; +import life.qbic.datamanager.download.DownloadContentProvider; +import life.qbic.datamanager.parser.ExampleProvider.Helper; +import life.qbic.datamanager.parser.measurement.ProteomicsMeasurementRegisterColumn; +import life.qbic.datamanager.templates.Template; +import life.qbic.datamanager.templates.XLSXTemplateHelper; +import life.qbic.logging.api.Logger; +import life.qbic.projectmanagement.application.measurement.ProteomicsMeasurementMetadata; +import life.qbic.projectmanagement.application.measurement.validation.MeasurementProteomicsValidator.DigestionMethod; +import life.qbic.projectmanagement.domain.model.measurement.ProteomicsMeasurement; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Hyperlink; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** Proteomics Measurement Content Provider + *

+ * Implementation of the {@link DownloadContentProvider} providing the content and file name for any files created + * from {@link ProteomicsMeasurement} + * and {@link ProteomicsMeasurementMetadata} + *

+ */ +public class ProteomicsMeasurementRegisterTemplate extends Template { + + private static final String MS_MEASUREMENT_TEMPLATE_FILENAME = "proteomics_measurement_registration_sheet.xlsx"; + private static final String MS_MEASUREMENT_TEMPLATE_DOMAIN_NAME = "Proteomics Template"; + + + private static final Logger log = logger(ProteomicsMeasurementRegisterTemplate.class); + private static final int DEFAULT_GENERATED_ROW_COUNT = 200; + + + private static void setAutoWidth(Sheet sheet) { + for (int col = 0; col <= 18; col++) { + sheet.autoSizeColumn(col); + } + } + + @Override + public byte[] getContent() { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();) { + + CellStyle readOnlyHeaderStyle = XLSXTemplateHelper.createReadOnlyHeaderCellStyle(workbook); + CellStyle boldStyle = createBoldCellStyle(workbook); + CellStyle linkHeaderStyle = createLinkHeaderCellStyle(workbook); + CellStyle defaultStyle = createDefaultCellStyle(workbook); + + Sheet sheet = workbook.createSheet("Proteomics Measurement Metadata"); + Row header = getOrCreateRow(sheet, 0); + for (ProteomicsMeasurementRegisterColumn measurementColumn : ProteomicsMeasurementRegisterColumn.values()) { + var cell = getOrCreateCell(header, measurementColumn.columnIndex()); + if (measurementColumn.isMandatory()) { + cell.setCellValue(measurementColumn.headerName() + "*"); + } else { + cell.setCellValue(measurementColumn.headerName()); + } + cell.setCellStyle(boldStyle); + if (measurementColumn.isReadOnly()) { + cell.setCellStyle(readOnlyHeaderStyle); + } else if (measurementColumn.equals(ProteomicsMeasurementRegisterColumn.ORGANISATION_URL)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://ror.org"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } else if (measurementColumn.equals(ProteomicsMeasurementRegisterColumn.MS_DEVICE)) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress("https://rdm.qbic.uni-tuebingen.de"); + cell.setCellStyle(linkHeaderStyle); + cell.setHyperlink(hyperlink); + } + //add helper to header + measurementColumn.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + measurementColumn.columnIndex(), + 0, + measurementColumn.columnIndex(), + 0, + helper.exampleValue(), + helper.description())); + } + + // add property information order of columns matters!! + for (ProteomicsMeasurementRegisterColumn column : Arrays.stream( + ProteomicsMeasurementRegisterColumn.values()) + .sorted(Comparator.comparing(ProteomicsMeasurementRegisterColumn::columnIndex)) + .toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultStyle, + boldStyle); + } + + var startIndex = 1; // start in row number 2 with index 1 skipping the header in the first row + + // make sure to create the visible sheet first + Sheet hiddenSheet = workbook.createSheet("hidden"); + Name digestionMethodArea = createOptionArea(hiddenSheet, "Digestion Method", + DigestionMethod.getOptions()); + + addDataValidation(sheet, + ProteomicsMeasurementRegisterColumn.DIGESTION_METHOD.columnIndex(), startIndex, + ProteomicsMeasurementRegisterColumn.DIGESTION_METHOD.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + digestionMethodArea); + + for (ProteomicsMeasurementRegisterColumn column : ProteomicsMeasurementRegisterColumn.values()) { + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + helper.exampleValue(), + helper.description()) + ); + } + + setAutoWidth(sheet); + workbook.setActiveSheet(0); + + lockSheet(hiddenSheet); + hideSheet(workbook, hiddenSheet); + + workbook.write(byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new ApplicationException(ErrorCode.GENERAL, null); + } + } + + @Override + public String getFileName() { + return MS_MEASUREMENT_TEMPLATE_FILENAME; + } + + @Override + public String getDomainName() { + return MS_MEASUREMENT_TEMPLATE_DOMAIN_NAME; + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementTemplate.java deleted file mode 100644 index ca544df95..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementTemplate.java +++ /dev/null @@ -1,47 +0,0 @@ -package life.qbic.datamanager.templates.measurement; - -import java.io.IOException; -import java.util.Objects; -import life.qbic.datamanager.templates.Template; - -/** - * MS measurement template - * - *

The Excel spreadsheet containing the required information for mass spectrometry measurement - * registration-

- * - * @since 1.0.0 - */ -public class ProteomicsMeasurementTemplate extends Template { - - private static final String MS_MEASUREMENT_TEMPLATE_PATH = "templates/proteomics_measurement_registration_sheet.xlsx"; - - private static final String MS_MEASUREMENT_TEMPLATE_FILENAME = "proteomics_measurement_registration_sheet.xlsx"; - - private static final String MS_MEASUREMENT_TEMPLATE_DOMAIN_NAME = "Proteomics Template"; - - public ProteomicsMeasurementTemplate() { - } - - @Override - public byte[] getContent() { - try { - return Objects.requireNonNull( - getClass().getClassLoader().getResourceAsStream(MS_MEASUREMENT_TEMPLATE_PATH)) - .readAllBytes(); - } catch (IOException e) { - throw new RuntimeException("Cannot get content for template: " + MS_MEASUREMENT_TEMPLATE_PATH, - e); - } - } - - @Override - public String getFileName() { - return MS_MEASUREMENT_TEMPLATE_FILENAME; - } - - @Override - public String getDomainName() { - return MS_MEASUREMENT_TEMPLATE_DOMAIN_NAME; - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/SequencingReadType.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/SequencingReadType.java new file mode 100644 index 000000000..9ded7a11f --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/SequencingReadType.java @@ -0,0 +1,18 @@ +package life.qbic.datamanager.templates.measurement; + +import java.util.Arrays; +import java.util.List; + +enum SequencingReadType { + SINGLE_END("single-end"), + PAIRED_END("paired-end"); + private final String presentationString; + + SequencingReadType(String presentationString) { + this.presentationString = presentationString; + } + + static List getOptions() { + return Arrays.stream(values()).map(it -> it.presentationString).toList(); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java index 2b02f1ce9..68a1815c7 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java @@ -1,15 +1,22 @@ package life.qbic.datamanager.templates.sample; -import static life.qbic.datamanager.templates.XLSXTemplateHelper.*; import static life.qbic.datamanager.templates.XLSXTemplateHelper.addDataValidation; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createBoldCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyHeaderCellStyle; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateCell; import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateRow; import static life.qbic.datamanager.templates.XLSXTemplateHelper.hideSheet; import static life.qbic.datamanager.templates.XLSXTemplateHelper.lockSheet; import static life.qbic.datamanager.templates.XLSXTemplateHelper.setColumnAutoWidth; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.setColumnWidth; +import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.List; +import life.qbic.datamanager.parser.ExampleProvider.Helper; import life.qbic.datamanager.parser.sample.RegisterColumn; import life.qbic.datamanager.templates.XLSXTemplateHelper; import org.apache.poi.ss.usermodel.Name; @@ -66,6 +73,7 @@ public static XSSFWorkbook createRegistrationTemplate(List conditions, XSSFWorkbook workbook = new XSSFWorkbook(); var readOnlyHeaderStyle = createReadOnlyHeaderCellStyle(workbook); var boldCellStyle = createBoldCellStyle(workbook); + var defaultStyle = createDefaultCellStyle(workbook); var sheet = workbook.createSheet("Sample Metadata"); @@ -82,7 +90,34 @@ public static XSSFWorkbook createRegistrationTemplate(List conditions, if (column.isReadOnly()) { cell.setCellStyle(readOnlyHeaderStyle); } + + //add helper to header + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + 0, + column.columnIndex(), + 0, + helper.exampleValue(), + helper.description())); + } + + // add property information order of columns matters!! + for (RegisterColumn column : Arrays.stream( + RegisterColumn.values()) + .sorted(Comparator.comparing(RegisterColumn::columnIndex)).toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultStyle, + boldCellStyle); } + var startIndex = 1; //start in the second row with index 1. var hiddenSheet = workbook.createSheet("hidden"); @@ -124,6 +159,18 @@ public static XSSFWorkbook createRegistrationTemplate(List conditions, MAX_ROW_INDEX_TO, specimenOptions); + for (var column : RegisterColumn.values()) { + column.getFillHelp().ifPresent( + helper -> XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + MAX_ROW_INDEX_TO, + helper.exampleValue(), + helper.description()) + ); + } + setColumnAutoWidth(sheet, 0, RegisterColumn.maxColumnIndex()); // Auto width ignores cell validation values (e.g. a list of valid entries). So we need // to set them explicit diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java index 0fb98dc01..c865454e7 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java @@ -1,6 +1,7 @@ package life.qbic.datamanager.templates.sample; import static life.qbic.datamanager.templates.XLSXTemplateHelper.addDataValidation; +import static life.qbic.datamanager.templates.XLSXTemplateHelper.createDefaultCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createOptionArea; import static life.qbic.datamanager.templates.XLSXTemplateHelper.createReadOnlyCellStyle; import static life.qbic.datamanager.templates.XLSXTemplateHelper.getOrCreateCell; @@ -9,7 +10,10 @@ import static life.qbic.datamanager.templates.XLSXTemplateHelper.lockSheet; import static life.qbic.datamanager.templates.XLSXTemplateHelper.setColumnAutoWidth; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import life.qbic.datamanager.parser.ExampleProvider.Helper; import life.qbic.datamanager.parser.sample.EditColumn; import life.qbic.datamanager.templates.XLSXTemplateHelper; import life.qbic.projectmanagement.application.sample.PropertyConversion; @@ -57,6 +61,7 @@ public static XSSFWorkbook createUpdateTemplate(List samples, List samples, List XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + 0, + column.columnIndex(), + 0, + helper.exampleValue(), + helper.description())); } + + // add property information order of columns matters!! + for (EditColumn column : Arrays.stream( + EditColumn.values()) + .sorted(Comparator.comparing(EditColumn::columnIndex)).toList()) { + // add property information + var exampleValue = column.getFillHelp().map(Helper::exampleValue).orElse(""); + var description = column.getFillHelp().map(Helper::description).orElse(""); + XLSXTemplateHelper.addPropertyInformation(workbook, + column.headerName(), + column.isMandatory(), + exampleValue, + description, + defaultStyle, + boldCellStyle); + } + var startIndex = 1; //start in the second row with index 1. int rowIndex = startIndex; for (Sample sample : samples) { Row row = getOrCreateRow(sheet, rowIndex); var experimentalGroup = experimentalGroups.stream() .filter(group -> group.id() == sample.experimentalGroupId()).findFirst().orElseThrow(); - fillRowWithSampleMetadata(row, sample, experimentalGroup.condition(), readOnlyCellStyle); + fillRowWithSampleMetadata(row, sample, experimentalGroup.condition(), defaultStyle, + readOnlyCellStyle); rowIndex++; } + var hiddenSheet = workbook.createSheet("hidden"); Name analysisToBePerformedOptions = createOptionArea(hiddenSheet, "Analysis to be performed", analysisToPerform); @@ -122,6 +155,18 @@ public static XSSFWorkbook createUpdateTemplate(List samples, List XLSXTemplateHelper.addInputHelper(sheet, + column.columnIndex(), + startIndex, + column.columnIndex(), + MAX_ROW_INDEX_TO, + helper.exampleValue(), + helper.description()) + ); + } + setColumnAutoWidth(sheet, 0, EditColumn.maxColumnIndex()); workbook.setActiveSheet(0); lockSheet(hiddenSheet); @@ -131,7 +176,7 @@ public static XSSFWorkbook createUpdateTemplate(List samples, List sample.sampleCode().code(); @@ -146,6 +191,7 @@ private static void fillRowWithSampleMetadata(Row row, Sample sample, }; var cell = getOrCreateCell(row, column.columnIndex()); cell.setCellValue(value); + cell.setCellStyle(defaultStyle); if (column.isReadOnly()) { cell.setCellStyle(readOnlyCellStyle); } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/MeasurementType.java b/user-interface/src/main/java/life/qbic/datamanager/views/MeasurementType.java new file mode 100644 index 000000000..8fc5b1306 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/MeasurementType.java @@ -0,0 +1,24 @@ +package life.qbic.datamanager.views; + +/** + * Measurement Type + *

+ * Some controlled enum vocabulary for different measurement types to use in the frontend part of + * the application. + * + * @since 1.6.0 + */ +public enum MeasurementType { + PROTEOMICS("Proteomics"), + GENOMICS("Genomics"); + + private final String type; + + MeasurementType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/TagFactory.java b/user-interface/src/main/java/life/qbic/datamanager/views/TagFactory.java new file mode 100644 index 000000000..0fc3195c8 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/TagFactory.java @@ -0,0 +1,42 @@ +package life.qbic.datamanager.views; + +import life.qbic.datamanager.views.general.Tag; +import life.qbic.datamanager.views.general.Tag.TagColor; + +/** + * Tag Factory + * + *

Create display tags for all thinkable use cases.

+ * + * @since 1.6.0 + */ +public class TagFactory { + + private TagFactory() {} + + public static Tag forMeasurement(MeasurementType measurementType) { + return switch (measurementType) { + case GENOMICS -> pinkTag("Genomics"); + case PROTEOMICS -> violetTag("Proteomics"); + }; + } + + public static Tag forCustom(String label, TagColor tagColor) { + return tagWithColor(label, tagColor); + } + + private static Tag tagWithColor(String label, TagColor color) { + var tag = new Tag(label); + tag.setTagColor(color); + return tag; + } + + private static Tag violetTag(String label) { + return tagWithColor(label, TagColor.VIOLET); + } + + private static Tag pinkTag(String label) { + return tagWithColor(label, TagColor.PINK); + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/events/ContactUpdateEvent.java b/user-interface/src/main/java/life/qbic/datamanager/views/events/ContactUpdateEvent.java new file mode 100644 index 000000000..ed4e4afc8 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/events/ContactUpdateEvent.java @@ -0,0 +1,30 @@ +package life.qbic.datamanager.views.events; + +import com.vaadin.flow.component.ComponentEvent; +import java.util.Objects; +import java.util.Optional; +import life.qbic.datamanager.views.projects.ProjectInformation; +import life.qbic.datamanager.views.projects.edit.EditContactDialog; + + +public class ContactUpdateEvent extends ComponentEvent { + + private final ProjectInformation projectInfo; + + /** + * 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 ContactUpdateEvent(EditContactDialog source, boolean fromClient, ProjectInformation projectInformation) { + super(source, fromClient); + this.projectInfo = Objects.requireNonNull(projectInformation); + } + + public Optional content() { + return Optional.ofNullable(projectInfo); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/events/FundingInformationUpdateEvent.java b/user-interface/src/main/java/life/qbic/datamanager/views/events/FundingInformationUpdateEvent.java new file mode 100644 index 000000000..a828718f4 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/events/FundingInformationUpdateEvent.java @@ -0,0 +1,20 @@ +package life.qbic.datamanager.views.events; + +import com.vaadin.flow.component.ComponentEvent; +import java.util.Optional; +import life.qbic.datamanager.views.projects.ProjectInformation; +import life.qbic.datamanager.views.projects.edit.EditFundingInformationDialog; + +public class FundingInformationUpdateEvent extends ComponentEvent { + + private final ProjectInformation projectInformation; + + public FundingInformationUpdateEvent(EditFundingInformationDialog source, boolean fromClient, ProjectInformation projectInformation) { + super(source, fromClient); + this.projectInformation = projectInformation; + } + + public Optional content() { + return Optional.ofNullable(projectInformation); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/events/ProjectDesignUpdateEvent.java b/user-interface/src/main/java/life/qbic/datamanager/views/events/ProjectDesignUpdateEvent.java new file mode 100644 index 000000000..01b11ef9a --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/events/ProjectDesignUpdateEvent.java @@ -0,0 +1,21 @@ +package life.qbic.datamanager.views.events; + +import com.vaadin.flow.component.ComponentEvent; +import java.util.Optional; +import life.qbic.datamanager.views.projects.ProjectInformation; +import life.qbic.datamanager.views.projects.edit.EditProjectDesignDialog; + + +public class ProjectDesignUpdateEvent extends ComponentEvent { + + private final ProjectInformation projectInformation; + + public ProjectDesignUpdateEvent(EditProjectDesignDialog source, boolean fromClient, ProjectInformation projectInformation) { + super(source, fromClient); + this.projectInformation = projectInformation; + } + + public Optional content() { + return Optional.ofNullable(projectInformation); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/DetailBox.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/DetailBox.java new file mode 100644 index 000000000..cbba54bae --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/DetailBox.java @@ -0,0 +1,119 @@ +package life.qbic.datamanager.views.general; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.Icon; + +/** + * Detail Box + *

+ * A data manager detail box contains of the two main layout sections: + * + *

    + *
  • Header Section
  • + *
  • Content Section
  • + *
+ *

+ * Detail boxes are used to visually highlight some contextual information to the user, + * with a descriptive heading, icons and some border to separate it from the surrounding elements. + *

+ * Developer hint: the content section can be filled with any content, but the height is currently + * restricted to 10rem (css: detail-box). Then the overflow will trigger a scrollbar in the content section. + * + * @since 1.6.0 + */ +public class DetailBox extends Div { + + private Div headerSection; + + private Div contentSection; + + private Header header; + + private Component content; + + public DetailBox() { + addClassName("detail-box"); + headerSection = new Div(); + headerSection.addClassName("detail-box-child"); + contentSection = new Div(); + contentSection.addClassName("detail-box-child"); + contentSection.addClassName("overflow-scroll-height"); + add(headerSection, contentSection); + } + + public void setHeader(Header header) { + this.header = header; + rebuild(); + } + + public void setContent(Component content) { + this.content = content; + rebuild(); + } + + private void rebuild() { + headerSection.removeAll(); + contentSection.removeAll(); + if (header != null) { + headerSection.add(header); + } + if (content != null) { + add(content); + contentSection.add(content); + } + } + + + public static class Header extends Div { + + private boolean iconVisible = false; + + private Icon icon; + + private Div heading; + + public Header() { + addClassName("detail-box-header"); + heading = new Div(); + } + + public Header(Icon icon, String text) { + this(); + this.icon = icon; + heading.setText(text); + showIcon(); + rebuild(); + } + + public Header(String text) { + this(); + heading.setText(text); + rebuild(); + } + + private void setIconVisibility(boolean visible) { + iconVisible = visible; + } + + public void showIcon() { + setIconVisibility(true); + rebuild(); + } + + public void hideIcon() { + setIconVisibility(false); + rebuild(); + } + + private void rebuild() { + removeAll(); + if (iconVisible && icon != null) { + add(icon); + } + if (heading != null) { + add(heading); + } + } + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBoundField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBoundField.java new file mode 100644 index 000000000..0d0132833 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/HasBoundField.java @@ -0,0 +1,51 @@ +package life.qbic.datamanager.views.general; + +import com.vaadin.flow.data.binder.ValidationException; + +/** + * Bound Field + * + *

A bound field offers some common access and behaviour to the implemented bound field.

+ * + * @since 1.6.0 + */ +public interface HasBoundField { + + /** + * Returns the field with bindings + * + * @since 1.6.0 + */ + T getField(); + + /** + * Returns the bound value + * + * @throws ValidationException if any validation of the field fails + * @since 1.6.0 + */ + V getValue() throws ValidationException; + + /** + * Set the bound value for the field. This will also update the field content. + * + * @param value sets an original value + * @since 1.6.0 + */ + void setValue(V value); + + /** + * true, if the bound value is valid, else returns false + * + * @since 1.6.0 + */ + boolean isValid(); + + /** + * Indicates, if the original value has changed after being set via {@link #setValue(Object)} + * + * @return true, if the original value has changed, else false + * @since 1.6.0 + */ + boolean hasChanged(); +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/Heading.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/Heading.java new file mode 100644 index 000000000..13354bfcb --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/Heading.java @@ -0,0 +1,66 @@ +package life.qbic.datamanager.views.general; + +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; + +/** + * Heading (with icon support) + * + * @since 1.6.0 + */ +public class Heading extends Div { + + private Icon icon; + + private Span text; + + private Heading() { + addClassName("heading-with-icon"); + this.icon = VaadinIcon.VAADIN_H.create(); + this.text = new Span(); + rebuild(); + } + + public static Heading createEmpty() { + return new Heading(); + } + + public static Heading withIcon(Icon icon) { + var headerWithIcon = new Heading(); + headerWithIcon.setIcon(icon); + return headerWithIcon; + } + + public static Heading withText(String text) { + var headerWithText = new Heading(); + headerWithText.setText(text); + return headerWithText; + } + + public static Heading withIconAndText(Icon icon, String text) { + var headerWithIconAndText = new Heading(); + headerWithIconAndText.setIcon(icon); + headerWithIconAndText.setCustomText(text); + return headerWithIconAndText; + } + + private void setCustomText(String text) { + this.text = new Span(text); + rebuild(); + } + + public void setIcon(Icon icon) { + this.icon = icon; + rebuild(); + } + + private void rebuild() { + removeAll(); + add(icon); + add(text); + } + + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/IconLabel.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/IconLabel.java new file mode 100644 index 000000000..a53f34d1e --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/IconLabel.java @@ -0,0 +1,75 @@ +package life.qbic.datamanager.views.general; + +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; + +/** + * Icon label + * + *

Provides an icon and a label next to it.

+ * + * @since 1.6.0 + */ +public class IconLabel extends Div { + + private Icon icon; + + private String label; + + private final Icon toolTipIcon = VaadinIcon.INFO_CIRCLE.create(); + + private String toolTipText; + + private boolean showTooltip; + + private String information; + + private IconLabel() { + addClassName("icon-label"); + addClassName("horizontal-list"); + this.showTooltip = false; + } + + public IconLabel(Icon icon, String label) { + this(); + this.icon = icon; + this.label = label; + rebuild(); + } + + public void setTooltipText(String toolTipText) { + this.showTooltip = true; + this.toolTipText = toolTipText; + rebuild(); + } + + public void setInformation(String information) { + this.information = information; + rebuild(); + } + + private void rebuild() { + removeAll(); + var iconLabelContainer = new Div(); + iconLabelContainer.setClassName("icon-label-container"); + + if (icon != null) { + iconLabelContainer.add(icon); + } + iconLabelContainer.add(new Span(label)); + + if (showTooltip) { + toolTipIcon.setTooltipText(toolTipText); + iconLabelContainer.add(toolTipIcon); + } + add(iconLabelContainer); + + if (information != null) { + add(new Span(information)); + } + } + + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/OboIdFormatter.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/OboIdFormatter.java new file mode 100644 index 000000000..e2a1f3820 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/OboIdFormatter.java @@ -0,0 +1,25 @@ +package life.qbic.datamanager.views.general; + +import life.qbic.projectmanagement.domain.model.OntologyTerm; + +/** + * Enforce harmonised CURIE formatting + *

+ * Enforced format: ontology-name:id + * + * @since 1.6.0 + */ +public class OboIdFormatter { + + public static String render(OntologyTerm term) { + return enforceColonSeparator(term.getOboId()); + } + + public static String render(String oboId) { + return enforceColonSeparator(oboId); + } + + private static String enforceColonSeparator(String term) { + return term.replace("_", ":"); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyTermDisplay.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyTermDisplay.java new file mode 100644 index 000000000..99d48846b --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/OntologyTermDisplay.java @@ -0,0 +1,32 @@ +package life.qbic.datamanager.views.general; + +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.AnchorTarget; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; + +/** + * Ontology Term Display + *

+ * Renders an ontology term nicely as a badge with href. + * + * @since 1.6.0 + */ +public class OntologyTermDisplay extends Div { + + + public OntologyTermDisplay(String label, String curie, String reference) { + Div ontology = new Div(); + ontology.addClassName("vertical-list"); + Span ontologyLabel = new Span(label); + ontologyLabel.addClassName("overflow-hidden-ellipsis"); + Span ontologyLink = new Span(OboIdFormatter.render(curie)); + ontologyLink.addClassName("ontology-link"); + Anchor ontologyClassIri = new Anchor(reference, ontologyLink); + ontologyClassIri.setTarget(AnchorTarget.BLANK); + ontology.add(ontologyLabel, ontologyClassIri); + ontology.addClassNames("ontology-term", "gap-small"); + add(ontology); + } + +} 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 index 0fd51a582..39aeb0f74 100644 --- 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 @@ -14,10 +14,7 @@ import java.util.ArrayList; import java.util.List; import life.qbic.datamanager.views.general.HasBinderValidation; -import life.qbic.projectmanagement.application.authorization.QbicOidcUser; -import life.qbic.projectmanagement.application.authorization.QbicUserDetails; -import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; -import org.springframework.security.core.context.SecurityContextHolder; +import life.qbic.datamanager.views.general.utils.Utility; /** * A component for contact person input @@ -27,6 +24,7 @@ * * @since 1.0.0 */ +@Deprecated(since = "1.6.0") public class AutocompleteContactField extends CustomField implements HasBinderValidation { @@ -101,20 +99,8 @@ public AutocompleteContactField(String label, String shortName) { private void onSelfSelected( ComponentValueChangeEvent checkboxvalueChangeEvent) { if (Boolean.TRUE.equals(checkboxvalueChangeEvent.getValue())) { - var principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - String fullName; - String emailAddress; - if (principal instanceof QbicUserDetails qbicUserDetails) { - fullName = qbicUserDetails.fullName(); - emailAddress = qbicUserDetails.getEmailAddress(); - } else if (principal instanceof QbicOidcUser qbicOidcUser) { - fullName = qbicOidcUser.getFullName(); - emailAddress = qbicOidcUser.getEmail(); - } else { - throw new AuthenticationCredentialsNotFoundException("Unknown authentication principal"); - } - Contact userAsContact = new Contact(fullName, emailAddress); - setContact(userAsContact); + var userAsContact = Utility.tryToLoadFromPrincipal(); + userAsContact.ifPresent(this::setContact); } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/BoundContactField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/BoundContactField.java new file mode 100644 index 000000000..b1cae8af6 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/BoundContactField.java @@ -0,0 +1,186 @@ +package life.qbic.datamanager.views.general.contact; + +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.ValidationException; +import com.vaadin.flow.data.validator.EmailValidator; +import com.vaadin.flow.function.SerializablePredicate; +import java.util.Objects; +import life.qbic.datamanager.views.general.HasBoundField; + +/** + * Bound Contact Field + *

+ * Binds a {@link ContactField} to a {@link Contact}- + * + * @since 1.6.0 + */ +public class BoundContactField implements HasBoundField { + + private final ContactField contactField; + + private final Binder binder; + + private Contact originalValue; + + private BoundContactField(ContactField contactField, + SerializablePredicate predicate) { + this.contactField = contactField; + this.binder = createBinder(predicate, contactField); + binder.addStatusChangeListener( + event -> updateStatus(contactField, event.hasValidationErrors())); + this.originalValue = new Contact("", ""); + } + + private static void updateStatus(ContactField contactField, boolean isInvalid) { + contactField.getElement().setProperty("invalid", isInvalid); + updateStatus(contactField.getEmailTextField(), isInvalid); + updateStatus(contactField.getFullNameTextField(), isInvalid); + } + + private static void updateStatus(TextField textField, boolean isInvalid) { + textField.setInvalid(isInvalid); + } + + /** + * The contact field will only invalidate, if one of the fields is empty. Since it is optional, + * the contact field will not invalidate, if both inputs are empty. + * + * @param contactField + * @return + * @since + */ + public static BoundContactField createOptional(ContactField contactField) { + contactField.setOptional(true); + return new BoundContactField(contactField, isOptional()); + } + + /** + * The contact field will invalidate, if one of the fields is empty or both are empty, since it is + * mandatory to be filled with information. + * + * @param contactField + * @return + * @since + */ + public static BoundContactField createMandatory(ContactField contactField) { + return new BoundContactField(contactField, isMandatory()); + } + + /** + * This predicate will return true, if all fields are filled. + *

+ * If either is filled alone or none, the predicate will return false + * + * @return + * @since + */ + private static SerializablePredicate isMandatory() { + return contact -> { + var onlyEmailEmpty = contact.getEmail().isBlank() && !contact.getFullName().isBlank(); + var onlyNameEmpty = !contact.getEmail().isBlank() && contact.getFullName().isBlank(); + var bothEmpty = contact.getEmail().isBlank() && contact.getFullName().isBlank(); + return !(onlyEmailEmpty || onlyNameEmpty || bothEmpty); + }; + } + + /** + * This predicate will return true, if all fields are empty or both are filled. + *

+ * If either is filled alone, the predicate will return false + */ + private static SerializablePredicate isOptional() { + return contact -> { + var onlyEmailProvided = !contact.getEmail().isBlank() && contact.getFullName().isBlank(); + var onlyNameProvided = contact.getEmail().isBlank() && !contact.getFullName().isBlank(); + return !(onlyEmailProvided || onlyNameProvided); + }; + } + + private static Binder createBinder(SerializablePredicate predicate, ContactField contactField) { + Binder binder = new Binder<>(ContactContainer.class); + binder.setBean(new ContactContainer()); + binder.forField(contactField).withValidator(predicate, "There is still information missing") + .bind(ContactContainer::getContact, ContactContainer::setContact); + binder.forField(contactField.getEmailTextField()).withValidator( + new EmailValidator("Please provide a valid email address, e.g. my.name@example.com", true)) + .bind(ContactContainer::getEmail, ContactContainer::setEmail); + binder.forField(contactField.getFullNameTextField()) + .bind(ContactContainer::getFullName, ContactContainer::setFullName); + return binder; + } + + @Override + public ContactField getField() { + return contactField; + } + + @Override + public Contact getValue() throws ValidationException { + var container = new ContactContainer(); + binder.writeBean(container); + return container.getContact(); + } + + @Override + public void setValue(Contact value) { + var container = new ContactContainer(); + container.setContact(value); + binder.readBean(container); + originalValue = value; + } + + @Override + public boolean isValid() { + return binder.validate().isOk(); + } + + @Override + public boolean hasChanged() { + return binder.hasChanges() || isDifferent(originalValue, binder.getBean()); + } + + private boolean isDifferent(Contact originalValue, ContactContainer bean) { + var newValue = bean.getContact(); + return !Objects.equals(originalValue, newValue); + } + + + private static class ContactContainer { + + private Contact contact; + + public ContactContainer() { + contact = new Contact("", ""); + } + + public Contact getContact() { + return contact; + } + + public void setContact(Contact contact) { + this.contact = Objects.requireNonNull(contact); + } + + public String getEmail() { + return contact == null ? "" : contact.getEmail(); + } + + public void setEmail(String email) { + if (contact != null) { + contact.setEmail(email); + } + } + + public String getFullName() { + return contact == null ? "" : contact.getFullName(); + } + + public void setFullName(String fullName) { + if (contact != null) { + contact.setFullName(fullName); + } + } + + } +} 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 eccbb2df7..e7c4c4722 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 @@ -54,6 +54,8 @@ public life.qbic.projectmanagement.domain.model.project.Contact toDomainContact( } return new life.qbic.projectmanagement.domain.model.project.Contact(getFullName(), getEmail()); } + + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactField.java new file mode 100644 index 000000000..52254e5e8 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactField.java @@ -0,0 +1,152 @@ +package life.qbic.datamanager.views.general.contact; + +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.customfield.CustomField; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.shared.HasClientValidation; +import com.vaadin.flow.component.textfield.TextField; +import java.util.Objects; + + +public class ContactField extends CustomField implements HasClientValidation { + + public static final String GAP_M_CSS = "gap-m"; + private static final String FULL_WIDTH_CSS = "full-width"; + private final TextField fullName; + private final TextField email; + private final Checkbox setMyselfCheckBox; + private Contact myself; + private boolean isOptional = true; + + private ContactField(String label) { + this.fullName = withErrorMessage(withPlaceHolder(new TextField(), "Please provide a name"), + ""); + this.email = withErrorMessage(withPlaceHolder(new TextField(), "Please enter an email address"), + ""); + this.setMyselfCheckBox = new Checkbox(); + setLabel(label); + add(layoutFields(setMyselfCheckBox, layoutFields(fullName, email))); + hideCheckbox(); // default is to hide the set myself checkbox + setMyselfCheckBox.addValueChangeListener(listener -> { + if (isChecked(listener.getSource())) { + loadContact(this, myself); + } + }); + fullName.addValueChangeListener(listener -> { + updateValue(); + }); + email.addValueChangeListener(listener -> { + updateValue(); + }); + } + + private static void loadContact(ContactField field, Contact contact) { + field.setPresentationValue(contact); + } + + private static boolean isChecked(Checkbox checkbox) { + return checkbox.getValue(); + } + + public static ContactField createSimple(String label) { + return new ContactField(label); + } + + public static ContactField createWithMyselfOption(String label, Contact myself, String hint, + boolean setOptional) { + var contactField = createSimple(label); + contactField.setMyself(myself, hint); + contactField.setOptional(setOptional); + return contactField; + } + + private static TextField withPlaceHolder(TextField textField, String placeHolder) { + textField.setPlaceholder(placeHolder); + return textField; + } + + private static TextField withErrorMessage(TextField textField, String errorMessage) { + textField.setErrorMessage(errorMessage); + return textField; + } + + private static Div layoutFields(Checkbox box, Div fields) { + var layout = new Div(); + layout.addClassNames("flex-vertical", GAP_M_CSS); + layout.add(box); + layout.add(fields); + return layout; + } + + private static Div layoutFields(TextField fullName, TextField email) { + var layout = new Div(fullName, email); + layout.addClassNames("flex-horizontal", GAP_M_CSS, FULL_WIDTH_CSS); + fullName.addClassName(FULL_WIDTH_CSS); + email.addClassName(FULL_WIDTH_CSS); + return layout; + } + + public void setOptional(boolean optional) { + isOptional = optional; + } + + public void setMyself(Contact myself, String hint) { + this.myself = Objects.requireNonNull(myself); + this.setMyselfCheckBox.setLabel(hint); + showCheckbox(); + } + + private void showCheckbox() { + setMyselfCheckBox.setVisible(true); + } + + private void hideCheckbox() { + setMyselfCheckBox.setVisible(false); + } + + @Override + protected Contact generateModelValue() { + return new Contact(fullName.getValue(), email.getValue()); + } + + @Override + protected void setPresentationValue(Contact contact) { + fullName.setValue(contact.getFullName()); + email.setValue(contact.getEmail()); + } + + @Override + public void setInvalid(boolean value) { + if (value) { + invalidate(); + } else { + removeErrors(); + } + } + + public TextField getEmailTextField() { + return email; + } + + public TextField getFullNameTextField() { + return fullName; + } + + private void removeErrors() { + email.setInvalid(false); + fullName.setInvalid(false); + } + + private void invalidate() { + if (email.getValue().isEmpty() && fullName.getValue().isEmpty() && isOptional) { + return; + } + if (email.getValue().isBlank()) { + email.setInvalid(true); + } + if (fullName.getValue().isBlank()) { + fullName.setInvalid(true); + } + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactsForm.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactsForm.java new file mode 100644 index 000000000..c7b32163c --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/contact/ContactsForm.java @@ -0,0 +1,29 @@ +package life.qbic.datamanager.views.general.contact; + +import com.vaadin.flow.component.html.Div; +import java.util.Objects; + +/** + * Contacts Form + * + *

Provides fields for defining the principal investigator, project responsible + * and project manager<

+ * + * @since 1.6.0 + */ +public class ContactsForm extends Div { + + public ContactsForm( + ContactField principalInvestigator, + ContactField personResponsible, + ContactField projectManager) { + Objects.requireNonNull(principalInvestigator); + Objects.requireNonNull(personResponsible); + Objects.requireNonNull(projectManager); + + addClassNames("vertical-list", "gap-m"); + + add(principalInvestigator, personResponsible, projectManager); + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/BoundFundingField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/BoundFundingField.java new file mode 100644 index 000000000..577e61c42 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/BoundFundingField.java @@ -0,0 +1,100 @@ +package life.qbic.datamanager.views.general.funding; + +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.ValidationException; +import com.vaadin.flow.data.binder.Validator; +import java.util.Objects; +import life.qbic.datamanager.views.general.HasBoundField; + +/** + * Bound Funding Field + *

+ * Binds a {@link FundingField} to a {@link FundingEntry}- + * + * @since 1.6.0 + */ +public class BoundFundingField implements HasBoundField { + + private final FundingField fundingField; + + private final Binder binder; + + private FundingEntry initValue; + + public BoundFundingField(FundingField fundingField) { + this.fundingField = Objects.requireNonNull(fundingField); + this.binder = new Binder<>(FundingInformationContainer.class); + bindSimple(binder, fundingField); + } + + @SafeVarargs + public BoundFundingField(FundingField fundingField, Validator... validators) { + this.fundingField = Objects.requireNonNull(fundingField); + this.binder = new Binder<>(FundingInformationContainer.class); + bindWithValidators(binder, fundingField, validators); + } + + private static void bindSimple(Binder binder, + FundingField fundingField) { + binder.forField(fundingField) + .bind(FundingInformationContainer::get, FundingInformationContainer::set); + } + + @SafeVarargs + private static void bindWithValidators(Binder binder, + FundingField fundingField, Validator... validators) { + var binding = binder.forField(fundingField); + for (Validator validator : validators) { + binding.withValidator(validator); + } + binding.bind(FundingInformationContainer::get, FundingInformationContainer::set); + } + + @Override + public FundingField getField() { + return fundingField; + } + + @Override + public FundingEntry getValue() throws ValidationException { + var container = new FundingInformationContainer(); + binder.writeBean(container); + return container.get(); + } + + @Override + public void setValue(FundingEntry value) { + initValue = value; + var container = new FundingInformationContainer(); + container.set(value); + binder.setBean(container); + } + + @Override + public boolean isValid() { + return binder.isValid(); + } + + @Override + public boolean hasChanged() { + return binder.hasChanges() || hasChanged(initValue, binder.getBean().get()); + } + + private boolean hasChanged(FundingEntry oldValue, FundingEntry newValue) { + return !oldValue.equals(newValue); + } + + public static final class FundingInformationContainer { + + private FundingEntry fundingEntry; + + public FundingEntry get() { + return fundingEntry; + } + + public void set(FundingEntry fundingEntry) { + this.fundingEntry = fundingEntry; + } + + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingField.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingField.java index e70df236b..c8372c787 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingField.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingField.java @@ -20,11 +20,11 @@ public class FundingField extends CustomField implements HasClient private static final long serialVersionUID = 839203706554301417L; private final TextField label; private final TextField referenceId; + private final Div layoutFundingInput; public FundingField(String fieldLabel) { super(); addClassName("funding-field"); - setLabel(fieldLabel); this.label = new TextField("Grant", "e.g. SFB"); this.label.addClassName("grant-label-field"); this.referenceId = new TextField("Grant ID", "e.g. SFB 1101"); @@ -32,9 +32,21 @@ public FundingField(String fieldLabel) { // we need to override the text-fields internal default validation, since we do not directly add binders // with validators to the encapsulated fields, which results in removal of the invalid HTML property and disabling // us correctly display invalid element status + setLabel(fieldLabel); label.addValidationStatusChangeListener(e -> validate()); referenceId.addValidationStatusChangeListener(e -> validate()); - layoutComponent(); + layoutFundingInput = layoutFundingInput(label, referenceId); + add(layoutFundingInput); + } + + public static FundingField createVertical(String fieldLabel) { + return new FundingField(fieldLabel); + } + + public static FundingField createHorizontal(String fieldLabel) { + var field = new FundingField(fieldLabel); + field.layoutFundingInput.addClassNames("flex-horizontal", "gap-m"); + return field; } protected void validate() { @@ -79,8 +91,12 @@ public void setInvalid(boolean invalid) { referenceId.setInvalid(invalid); } + public void setLabel(String label) { + this.label.setValue(label); + } - - + public void setReferenceId(String referenceId) { + this.referenceId.setValue(referenceId); + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingInputForm.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingInputForm.java new file mode 100644 index 000000000..acd00b998 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/funding/FundingInputForm.java @@ -0,0 +1,40 @@ +package life.qbic.datamanager.views.general.funding; + +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.data.binder.ValidationException; +import java.util.Objects; +import life.qbic.datamanager.views.general.HasBoundField; + +/** + * Funding Input Form + *

+ * Form that can be used to request funding information about a project from a user. + * + * @since 1.6.0 + */ +public class FundingInputForm extends FormLayout { + + private transient final HasBoundField fundingField; + + private FundingInputForm(HasBoundField fundingField) { + this.fundingField = fundingField; + add(fundingField.getField()); + } + + public static FundingInputForm create(HasBoundField fundingField) { + Objects.requireNonNull(fundingField); + return new FundingInputForm(fundingField); + } + + public void setContent(FundingEntry fundingEntry) { + fundingField.setValue(fundingEntry); + } + + public FundingEntry fromUserInput() throws ValidationException { + return fundingField.getValue(); + } + + public boolean hasChanges() { + return fundingField.hasChanged(); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/section/ActionBar.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/section/ActionBar.java new file mode 100644 index 000000000..386c18976 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/section/ActionBar.java @@ -0,0 +1,106 @@ +package life.qbic.datamanager.views.general.section; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Action Bar + * + *

An actionbar offers the user the possibility to perform some action + * that is related to its placed context in the application.

+ *

+ * Action items can be activated or deactivated. Inactive control elements are disabled and hidden + * (default), active control elements are enabled and shown. + * + *

+ * Relevant CSS + *

+ * The relevant CSS classes for this component are: + * + *

    + *
  • actionbar
  • + *
+ * + * @since 1.6.0 + */ +public class ActionBar extends Div { + + private transient ControlStrategy controlStrategy; + + private List