From e65b9836077ff28d6e261e671ad7baa952f2f404 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Thu, 27 Jul 2023 14:35:58 +0200 Subject: [PATCH 1/9] set duration to 3sec and change position (#333) --- .../experiments/experiment/ExperimentDetailsComponent.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java index a54ea052e..f8c708587 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java @@ -117,7 +117,8 @@ private Notification createSampleRegistrationPossibleNotification(String project Component layout = new HorizontalLayout(text, closeButton); layout.addClassName("content"); - notification.setPosition(Position.MIDDLE); + notification.setPosition(Position.BOTTOM_START); + notification.setDuration(3_000); notification.add(layout); return notification; } From db2c46e36533e32f22d5d616b48ca4f7c2e2a086 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Thu, 27 Jul 2023 17:33:20 +0200 Subject: [PATCH 2/9] Introduce editor functionality to show a custom component within the spreadsheet only if user selects the cell. (#335) * WIP increase performance * Fix blinking issue for comboboxes * Add javadoc and fix header row * Fix Dialog loading * Fix rowcount accession and prefilling of cells for validation * Remove unnecessary duplicate unused checks since we already define the value during row addition * WIP fix dialog reload everything else works * Finally fixed! * Remove unused method * Remove unused method * replaces red validation borders in spreadsheet by cell highlighting comparable to usual style * Update Javadocs * rename conversion methods * Fix SonarCloud Code Smell * unlock cells after validation * Fix copy past JD fail --------- Co-authored-by: wow-such-code --- .../batch/BatchRegistrationDialog.java | 31 +- .../registration/batch/DropdownColumn.java | 113 ---- .../batch/SampleRegistrationSpreadsheet.java | 610 +++++++++--------- .../batch/SampleSpreadsheetLayout.java | 33 +- .../batch/SpreadsheetDropdownFactory.java | 136 ++-- .../batch/SpreadsheetMethods.java | 32 + 6 files changed, 415 insertions(+), 540 deletions(-) delete mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/DropdownColumn.java diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/BatchRegistrationDialog.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/BatchRegistrationDialog.java index 0cd59eeb1..97d4ab542 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/BatchRegistrationDialog.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/BatchRegistrationDialog.java @@ -29,14 +29,13 @@ public class BatchRegistrationDialog extends DialogWindow { private final Tab batchInformationTab = createTabStep("1", "Batch Information"); private final Tab sampleInformationTab = createTabStep("2", "Register Samples"); private final BatchInformationLayout batchInformationLayout = new BatchInformationLayout(); - private SampleSpreadsheetLayout sampleSpreadsheetLayout; + private final SampleSpreadsheetLayout sampleSpreadsheetLayout = new SampleSpreadsheetLayout(); private final transient RegisterBatchDialogHandler registerBatchDialogHandler; public BatchRegistrationDialog() { addClassName("batch-registration-dialog"); setResizable(true); setHeaderTitle(TITLE); - initSampleRegistrationLayout(); initTabStepper(); registerBatchDialogHandler = new RegisterBatchDialogHandler(); } @@ -58,10 +57,6 @@ private Tab createTabStep(String avatarLabel, String tabLabel) { return tabStep; } - private void initSampleRegistrationLayout() { - sampleSpreadsheetLayout = new SampleSpreadsheetLayout(); - } - /** * Adds the provided {@link ComponentEventListener} to the list of listeners that will be notified * if an {@link BatchRegistrationEvent} occurs within this Dialog @@ -144,16 +139,22 @@ private void setTabSelectionListener() { } private void generateSampleRegistrationLayout() { + //We only reload the spreadsheet if the user selects an experiment and switches back in the tabSheet to select a different one + if (hasExperimentInformationChanged()) { + sampleSpreadsheetLayout.resetLayout(); + } sampleSpreadsheetLayout.setBatchName(batchInformationLayout.batchNameField.getValue()); - ExperimentId selectedExperimentId = batchInformationLayout.experimentSelect.getValue() + sampleSpreadsheetLayout.setExperiment(batchInformationLayout.experimentSelect.getValue()); + sampleSpreadsheetLayout.generateSampleRegistrationSheet( + batchInformationLayout.dataTypeSelection.getValue()); + } + + private boolean hasExperimentInformationChanged() { + ExperimentId previouslySelectedExperiment = sampleSpreadsheetLayout.getExperiment(); + ExperimentId selectedExperiment = batchInformationLayout.experimentSelect.getValue() .experimentId(); - //We only reload the spreadsheet if the selected experiment was changed (or dialog closed) - if (sampleSpreadsheetLayout.getExperiment() == null || !selectedExperimentId.equals( - sampleSpreadsheetLayout.getExperiment())) { - sampleSpreadsheetLayout.setExperiment(batchInformationLayout.experimentSelect.getValue()); - sampleSpreadsheetLayout.generateSampleRegistrationSheet( - batchInformationLayout.dataTypeSelection.getValue()); - } + return previouslySelectedExperiment != null && !previouslySelectedExperiment.equals( + selectedExperiment); } private void setSubmissionListeners() { @@ -200,7 +201,7 @@ public void resetAndClose() { private void reset() { batchInformationLayout.reset(); - sampleSpreadsheetLayout.reset(); + sampleSpreadsheetLayout.resetLayout(); tabStepper.setSelectedTab(batchInformationTab); } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/DropdownColumn.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/DropdownColumn.java deleted file mode 100644 index c39ff971e..000000000 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/DropdownColumn.java +++ /dev/null @@ -1,113 +0,0 @@ -package life.qbic.datamanager.views.projects.project.samples.registration.batch; - -import java.util.ArrayList; -import java.util.List; - -/** - * Describes a table column (without header) that contains a dropdown menu with items and optional - * label to style a spreadsheet - * @since 1.0.0 - */ -public class DropdownColumn { - - private String dropdownLabel = ""; - private List items = new ArrayList<>(); - private int fromRowIndex = 1; - private int toRowIndex = 1000; - private int colIndex = 0; - - /** - * Adds an item to the dropdown menu of this DropDownColumn - * @param item String denoting the item - * @return this DropdownColumn, now with one more item - */ - public DropdownColumn addItem(String item) { - items.add(item+" "); - return this; - } - - /** - * Sets items to display in the dropdown menu of this DropDownColumn - * @param items List of Strings denoting the items - * @return this DropdownColumn, now with the provided items - */ - public DropdownColumn withItems(List items) { - this.items = new ArrayList<>(); - for(String item : items) { - this.addItem(item); - } - return this; - } - - /** - * Sets the minimum row index from which on the dropdown menu should be displayed - * @param i the first row index - * @return this DropdownColumn - */ - public DropdownColumn fromRowIndex(int i) { - this.fromRowIndex = i; - return this; - } - - /** - * Sets the maximum row index until which the dropdown menu should be displayed - * @param i the last row index - * @return this DropdownColumn - */ - public DropdownColumn toRowIndex(int i) { - this.toRowIndex = i; - return this; - } - - /** - * Sets the column index of the column in which the dropdown menu should be displayed - * @param i the column index - * @return this DropdownColumn - */ - public DropdownColumn atColIndex(int i) { - this.colIndex = i; - return this; - } - - /** - * Tests if this DropDownColumn has been defined for a provided column index and if it includes a - * provided row, that is, if a cell is to be rendered with this dropdown. - * @param row the row index of the spreadsheet cell to test - * @param col the column index of the spreadsheet cell to test - * @return true, if the cell coordinates are within range of this DropDownColumn, false otherwise - */ - public boolean isWithinRange(int row, int col) { - return fromRowIndex <= row && toRowIndex >= row && col == colIndex; - } - - /** - * Returns the label of this DropDownColumn, if it was set - * @return the label, which might be an empty String - */ - public String getLabel() { - return dropdownLabel; - } - - /** - * Returns the items of this DropDownColumn, if they were set - * @return the list of items to display in the dropdown, if there are any - */ - public List getItems() { - return items; - } - - public boolean isInColumn(int columnIndex) { - return columnIndex == colIndex; - } - - /** - * Increases the row range of this DropDownColumn to include the specified row - * No change is made if the row index was already in that range. - * @param rowIndex the row index of the spreadsheet cell - */ - public void increaseToRow(int rowIndex) { - if(this.toRowIndex < rowIndex) { - this.toRowIndex = rowIndex; - } - } -} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java index 6277dc01e..31e38d49c 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java @@ -1,6 +1,7 @@ package life.qbic.datamanager.views.projects.project.samples.registration.batch; import com.vaadin.flow.component.spreadsheet.Spreadsheet; +import java.awt.Color; import java.io.Serial; import java.io.Serializable; import java.util.ArrayList; @@ -18,47 +19,47 @@ import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.stream.StreamSupport; import life.qbic.application.commons.Result; import life.qbic.projectmanagement.domain.project.experiment.BiologicalReplicate; import life.qbic.projectmanagement.domain.project.experiment.BiologicalReplicateId; +import life.qbic.projectmanagement.domain.project.experiment.Condition; import life.qbic.projectmanagement.domain.project.experiment.Experiment; import life.qbic.projectmanagement.domain.project.experiment.ExperimentalGroup; import life.qbic.projectmanagement.domain.project.experiment.VariableLevel; +import life.qbic.projectmanagement.domain.project.experiment.VariableName; import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Analyte; import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Species; import life.qbic.projectmanagement.domain.project.experiment.vocabulary.Specimen; -import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.ExtendedColor; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.HorizontalAlignment; -import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; /** - * + * The SampleRegistrationSpreadSheet is a {@link Spreadsheet} based component which enables the + * registration of {@link MetadataType} specific Sample information. *

- * - * - * @since + * The spreadsheet enables the user to provide and change the sample information of Samples for the + * provided {@link Experiment} */ public class SampleRegistrationSpreadsheet extends Spreadsheet implements Serializable { @Serial private static final long serialVersionUID = 573778360298068552L; - private SpreadsheetDropdownFactory dropdownCellFactory; + private final SpreadsheetDropdownFactory dropdownCellFactory = new SpreadsheetDropdownFactory(); private List header; - private static List species; - private static List specimens; - private static List analytes; + private List analysisTypes; + private List species; + private List specimens; + private List analytes; - //Spreadsheet component only allows retrieval of strings so we have to store the experimentalGroupId separately - private static Map experimentalGroupToConditionString; - private static Map> conditionsToReplicates; - private static int numberOfSamples; - private transient Sheet sampleRegistrationSheet; + //Spreadsheet component only allows retrieval of strings, so we have to store the experimentalGroupId separately + private Map experimentalGroupToConditionString; + private Map> conditionsToReplicates; + private int numberOfSamples; public SampleRegistrationSpreadsheet() { this.addClassName("sample-spreadsheet"); @@ -72,7 +73,7 @@ public SampleRegistrationSpreadsheet() { * * @param experiment An Experiment object, most likely the active one */ - public static void setExperimentMetadata(Experiment experiment) { + public void setExperimentMetadata(Experiment experiment) { species = experiment.getSpecies().stream().map(Species::label).toList(); specimens = experiment.getSpecimens().stream() .map(Specimen::label).toList(); @@ -84,36 +85,57 @@ public static void setExperimentMetadata(Experiment experiment) { prepareConditionItems(groups); } + /** + * Fills in the default active sheet of the spreadsheet component dependent on the provided + * {@link MetadataType} + * + * @param metaDataType the data type dependent on the chosen facility for which a sheet should be + * generated + */ public void addSheetToSpreadsheet(MetadataType metaDataType) { + generateSheetDependentOnDataType(metaDataType); + setRowColHeadingsVisible(false); + setActiveSheetProtected("password-needed-to-lock"); + setSpreadsheetComponentFactory(dropdownCellFactory); + //initialise first rows based on known sample size + addRowsForInitialSamples(numberOfSamples); + refreshAllCellValues(); + //Only reloads based on first row and first column with index = 1, meaning row and column style has to be refreshed manually + reloadVisibleCellContents(); + } - dropdownCellFactory = new SpreadsheetDropdownFactory(); - this.createNewSheet("SampleRegistrationSheet", 1, 1); - this.deleteSheet(0); - sampleRegistrationSheet = this.getActiveSheet(); + private void generateSheetDependentOnDataType(MetadataType metaDataType) { switch (metaDataType) { - case PROTEOMICS -> addProteomicsSheet(retrieveProteomics()); - case LIGANDOMICS -> addLigandomicsSheet(retrieveLigandomics()); - case TRANSCRIPTOMICS_GENOMICS -> addGenomicsSheet(retrieveGenomics()); - case METABOLOMICS -> addMetabolomicsSheet(retrieveMetabolomics()); + case PROTEOMICS -> generateProteomicsSheet(); + case LIGANDOMICS -> generateLigandomicsSheet(); + case TRANSCRIPTOMICS_GENOMICS -> generateGenomicsSheet(); + case METABOLOMICS -> generateMetabolomicsSheet(); } - this.setRowColHeadingsVisible(false); - - this.setActiveSheetProtected("password-needed-to-lock"); - this.setSpreadsheetComponentFactory(dropdownCellFactory); - //initialise first rows based on known sample size - addRowsForInitialSamples(numberOfSamples); } + /** + * Generates and adds Rows for the aggregated number of {@link BiologicalReplicate} within the + * {@link ExperimentalGroup} of an Experiment + * + * @param numberOfSamples the count of already defined samples within the setExperiment of this + * spreadsheet + */ private void addRowsForInitialSamples(int numberOfSamples) { // + 1 header row setMaxRows(1); for (int currentRow = 1; currentRow <= numberOfSamples; currentRow++) { addRow(); } - setMaxRows(numberOfSamples+1); } - private static void prepareConditionItems(List groups) { + /** + * Extracts and aggregates the String values into the selectable CellValueOptions within the + * ConditionColumn of all {@link VariableName} and {@link VariableLevel} for each + * {@link Condition} of each {@link ExperimentalGroup} within the provided experiment + * + * @param groups List of all experimental groups defined within the set Experiment + */ + private void prepareConditionItems(List groups) { // create condition items for dropdown and fix cell width. Remember replicates for each condition conditionsToReplicates = new HashMap<>(); experimentalGroupToConditionString = new HashMap<>(); @@ -134,6 +156,11 @@ private static void prepareConditionItems(List groups) { } } + /** + * Extracts and aggregates the String based label values to the selectable CellValueOptions within + * the ConditionColumn for all {@link BiologicalReplicate} within the {@link ExperimentalGroup} of + * the provided experiment + */ private List getReplicateLabels() { Set replicateLabels = new TreeSet<>(); for (List replicates : conditionsToReplicates.values()) { @@ -149,42 +176,31 @@ private List getReplicateLabels() { public void addRow() { int lastRowIndex = getRows() - 1; // zero-based index int increasedRowIndex = lastRowIndex + 1; - for (int columnIndex = 0; columnIndex < header.size(); columnIndex++) { SamplesheetHeaderName colHeader = header.get(columnIndex); switch (colHeader) { - case SPECIES -> prefillCell(columnIndex, increasedRowIndex, species); - case SPECIMEN -> prefillCell(columnIndex, increasedRowIndex, specimens); - case ANALYTE -> prefillCell(columnIndex, increasedRowIndex, analytes); - case CONDITION -> prefillCell(columnIndex, increasedRowIndex, - conditionsToReplicates.keySet().stream().toList()); + case ROW -> generateRowHeaderCell(columnIndex, increasedRowIndex); + case SPECIES -> generatePrefilledCell(columnIndex, increasedRowIndex, species); + case SEQ_ANALYSIS_TYPE -> + generatePrefilledCell(columnIndex, increasedRowIndex, analysisTypes); + case SAMPLE_LABEL, CUSTOMER_COMMENT -> + generatePrefilledCell(columnIndex, increasedRowIndex, new ArrayList<>()); case BIOLOGICAL_REPLICATE_ID -> - prefillCell(columnIndex, increasedRowIndex, getReplicateLabels()); - case ROW -> fillEnumerationCell(columnIndex, increasedRowIndex); - default -> { - DropdownColumn column = dropdownCellFactory.getColumn(columnIndex); - if (column != null) { - column.increaseToRow(increasedRowIndex); - } - } + generatePrefilledCell(columnIndex, increasedRowIndex, getReplicateLabels()); + case SPECIMEN -> generatePrefilledCell(columnIndex, increasedRowIndex, specimens); + case ANALYTE -> generatePrefilledCell(columnIndex, increasedRowIndex, analytes); + case CONDITION -> generatePrefilledCell(columnIndex, increasedRowIndex, + conditionsToReplicates.keySet().stream().toList()); } - //cells need to be unlocked if they are not prefilled in any way - enableEditableCellsOfColumnUntilRow(increasedRowIndex, columnIndex); } setMaxRows(increasedRowIndex + 1); // 1-based } - private boolean isPrefilledColumn(int columnIndex) { - SamplesheetHeaderName colHeader = header.get(columnIndex); - switch (colHeader) { - case ROW, SEQ_ANALYSIS_TYPE, BIOLOGICAL_REPLICATE_ID, SPECIES, SPECIMEN, ANALYTE, CONDITION: - return true; - default: - return false; - } - } - - private void fillEnumerationCell(int colIndex, int rowIndex) { + /** + * Generates and fills the cells in the first column with the current RowIndex and Header specific + * style functioning so the column functions a row header + */ + private void generateRowHeaderCell(int colIndex, int rowIndex) { CellStyle boldStyle = this.getWorkbook().createCellStyle(); Font font = this.getWorkbook().createFont(); font.setBold(true); @@ -193,7 +209,8 @@ private void fillEnumerationCell(int colIndex, int rowIndex) { boldStyle.setAlignment(HorizontalAlignment.CENTER); Cell cell = this.createCell(rowIndex, colIndex, rowIndex); cell.setCellStyle(boldStyle); - this.refreshCells(cell); + //This is a bottleneck which can impact performance but is necessary, because we allow users to add rows manually + refreshCells(cell); } /** @@ -217,212 +234,178 @@ public void deleteRow(int index) { /** - * Generates and prefills the correct cell components dependent on already specified values. + * Generates a new cell with a prefilled value and style if only one value was provided, otherwise + * the cell is filled with an empty string. * - * @param colIndex - * @param rowIndex - * @param items + * @param colIndex columnIndex of the cell to be generated and prefilled + * @param rowIndex rowIndex of the cell to be generated and prefilled + * @param items list of items which should be selectable within the cell */ - private void prefillCell(int colIndex, int rowIndex, List items) { + private void generatePrefilledCell(int colIndex, int rowIndex, List items) { + Cell cell = this.createCell(rowIndex, colIndex, ""); if (items.size() == 1) { - Cell cell = this.createCell(rowIndex, colIndex, items.get(0)); + cell.setCellValue(items.stream().findFirst().orElseThrow()); CellStyle lockedStyle = this.getWorkbook().createCellStyle(); lockedStyle.setLocked(true); cell.setCellStyle(lockedStyle); - this.refreshCells(cell); - } else { - dropdownCellFactory.addDropDownCell(rowIndex, colIndex); } } - private void setupCommonDropDownColumns() { - initDropDownColumn(header.indexOf(SamplesheetHeaderName.SPECIES), species); - initDropDownColumn(header.indexOf(SamplesheetHeaderName.SPECIMEN), specimens); - initDropDownColumn(header.indexOf(SamplesheetHeaderName.ANALYTE), analytes); - initDropDownColumn(header.indexOf(SamplesheetHeaderName.CONDITION), - conditionsToReplicates.keySet().stream().toList()); - initDropDownColumn(header.indexOf(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID), - getReplicateLabels()); - } - - private void initDropDownColumn(int colIndex, List items) { - if (items.size() > 1) { - DropdownColumn itemDropDown = new DropdownColumn(); - itemDropDown.withItems(items); - itemDropDown.toRowIndex(0).atColIndex(colIndex); - dropdownCellFactory.addDropdownColumn(itemDropDown); + /** + * Generates the columnHeaders from the options provided in the {@link SamplesheetHeaderName} and + * specifies the number of columns. Finally, it also specifies the width of a column dependent on + * the selectable items within the cells of the column + * + * @param cellValueOptionsMap maps the selectable Items within the cells of a column with the + * columnHeader + */ + private void generateColumnsHeaders(LinkedHashMap> cellValueOptionsMap) { + List headerCells = new ArrayList<>(); + setMaxColumns(SamplesheetHeaderName.values().length); + for (SamplesheetHeaderName columnHeader : SamplesheetHeaderName.values()) { + String columnLabel = columnHeader.label; + int currentColumnIndex = columnHeader.ordinal(); + Cell cell = this.createCell(0, currentColumnIndex, columnLabel); + headerCells.add(cell); + List cellValueOptions = cellValueOptionsMap.get(columnHeader); + defineColumnWidthDependentOnLengthOfCellValueOptions(currentColumnIndex, columnLabel, + Objects.requireNonNullElseGet(cellValueOptions, ArrayList::new)); } + styleColumnHeaderCells(headerCells); } - private void prepareColumnHeaderAndWidth(LinkedHashMap> headerToPresets) { + + /** + * Updates the style of the header cells to be set to bold and locked. + * + * @param headerCells List containing the cells functioning as a header in the sheet which should + * be assigned a bold and locked style + */ + private void styleColumnHeaderCells(List headerCells) { CellStyle boldHeaderStyle = this.getWorkbook().createCellStyle(); Font font = this.getWorkbook().createFont(); font.setBold(true); boldHeaderStyle.setFont(font); - List updatedCells = new ArrayList<>(); - int columnIndex = 0; - for (SamplesheetHeaderName columnHeader : headerToPresets.keySet()) { - List presets = headerToPresets.get(columnHeader); - String columnLabel = columnHeader.label; - this.fixColumnWidth(columnIndex, columnLabel, - Objects.requireNonNullElseGet(presets, ArrayList::new)); - Cell cell = this.createCell(0, columnIndex, columnLabel); - cell.setCellStyle(boldHeaderStyle); - updatedCells.add(cell); - columnIndex++; - } - this.getColumns(); - this.refreshCells(updatedCells); - } - - private void prepareCommonSheetTasks() { - LinkedHashMap> headerToPresets = new LinkedHashMap<>(); - for (SamplesheetHeaderName label : header) { - headerToPresets.put(label, new ArrayList<>()); - } - headerToPresets.put(SamplesheetHeaderName.SPECIES, species); - headerToPresets.put(SamplesheetHeaderName.SPECIMEN, specimens); - headerToPresets.put(SamplesheetHeaderName.ANALYTE, analytes); - headerToPresets.put(SamplesheetHeaderName.CONDITION, - conditionsToReplicates.keySet().stream().toList()); - headerToPresets.put(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID, getReplicateLabels()); - prepareColumnHeaderAndWidth(headerToPresets); - this.reloadVisibleCellContents(); - setupCommonDropDownColumns(); - } - - void enableEditableCellsOfColumnUntilRow(int maxRow, int column) { - Set cells = new HashSet<>(); - for (int row = 1; row <= maxRow; row++) { - if (isCellUnused(this.getCell(row, column))) { - Cell cell = this.createCell(row, column, ""); - cells.add(cell); - } - } - defaultStyleAndUnlockEditableCells(cells); - } - - void defaultStyleAndUnlockEditableCells(Set cells) { - CellStyle unLockedStyle = this.getWorkbook().createCellStyle(); - unLockedStyle.setLocked(false); - for(Cell cell : cells) { - if(!isPrefilledColumn(cell.getColumnIndex())) { - cell.setCellStyle(unLockedStyle); - } - } - this.refreshCells(cells); + headerCells.forEach(cell -> cell.setCellStyle(boldHeaderStyle)); + //This has to be called separately since the reloadVisibleCellContent method starts the refresh at index 1 + refreshCells(headerCells); } - //an unused cell is either null or empty (not blank) - private boolean isCellUnused(Cell cell) { - return cell == null || SpreadsheetMethods.cellToStringOrNull(cell).isEmpty(); - } - - /* - * Changes width of a spreadsheet column based on header element and potential known entries. + /** + * Defines the allocated width of a column based on the length of its label and the possible + * cellValueOptions within a cell. Necessary since natively the spreadsheet.autofit() column + * method does not account for the width of items within components within a cell + * + * @param colIndex columnIndex for which the columnWidth should be calculated + * @param colLabel columnLabel equal to the one defined in the label of + * {@link SamplesheetHeaderName} + * @param cellValueOptions list of String values which are selectable within a cell. */ - void fixColumnWidth(int colIndex, String colLabel, - List entries) { - final String COL_SPACER = "___"; - List stringList = new ArrayList<>(Collections.singletonList(colLabel)); - stringList.addAll(entries); - String longestString = stringList.stream().max(Comparator.comparingInt(String::length)) - .orElseThrow(); - String spacingValue = longestString + COL_SPACER; + private void defineColumnWidthDependentOnLengthOfCellValueOptions(int colIndex, String colLabel, + List cellValueOptions) { + String longestColumnString = findLongestStringWithinColumn(colLabel, cellValueOptions); + //Since all cells within a column have the same set of items we only need to set the value for one cell to allow autofit to find the correct width. Cell cell = this.getCell(0, colIndex); String oldValue = ""; if (cell == null) { - this.createCell(0, colIndex, spacingValue); + this.createCell(0, colIndex, longestColumnString); } else { oldValue = SpreadsheetMethods.cellToStringOrNull(cell); - this.getCell(0, colIndex).setCellValue(spacingValue); + this.getCell(0, colIndex).setCellValue(longestColumnString); } - //Todo Find out why switching from a sheet with less columns to a sheet with more columns breaks the sheet(e.g. lipidomics to genomics) try { this.autofitColumn(colIndex); - this.getActiveSheet(); } catch (IndexOutOfBoundsException exception) { - throw new RuntimeException("Something went wrong when switching sheets."); + throw new RuntimeException("Can't autofit column width due to" + exception.getMessage()); } - this.getCell(0, colIndex).setCellValue(oldValue); } - private void addProteomicsSheet(List header) { - this.header = header; - prepareCommonSheetTasks(); - setDefaultColumnCount(header.size()); + /** + * Finds and returns the longest String within the label and cellValueOptions within a column, + * concatenated with a minimum spacer + * + * @param colLabel columnLabel equal to the one defined in the label of * + * {@link SamplesheetHeaderName} + * @param cellValueOptions list of String values which are selectable within a cell. + * @return Concatenation of the longest String within a column and a predefined spacer string. + */ + private String findLongestStringWithinColumn(String colLabel, List cellValueOptions) { + //We need to ensure that there is a minimum Width for columns without cellValueOptions + final String COL_SPACER = "___"; + List stringList = new ArrayList<>(Collections.singletonList(colLabel)); + stringList.addAll(cellValueOptions); + String longestString = stringList.stream().max(Comparator.comparingInt(String::length)) + .orElseThrow(); + return longestString + COL_SPACER; } - private void addMetabolomicsSheet(List header) { - this.header = header; - prepareCommonSheetTasks(); - setDefaultColumnCount(header.size()); + + private void generateProteomicsSheet() { + this.header = retrieveProteomics(); } - private void addLigandomicsSheet(List header) { - this.header = header; - prepareCommonSheetTasks(); - setDefaultColumnCount(header.size()); + private void generateMetabolomicsSheet() { + this.header = retrieveMetabolomics(); } - private void addGenomicsSheet(List header) { - this.header = header; - LinkedHashMap> headerToPresets = new LinkedHashMap<>(); - setDefaultColumnCount(header.size()); - for (SamplesheetHeaderName head : header) { - headerToPresets.put(head, new ArrayList<>()); - } - headerToPresets.put(SamplesheetHeaderName.SPECIES, species); - headerToPresets.put(SamplesheetHeaderName.SPECIMEN, specimens); - headerToPresets.put(SamplesheetHeaderName.ANALYTE, analytes); - headerToPresets.put(SamplesheetHeaderName.CONDITION, - conditionsToReplicates.keySet().stream().toList()); - headerToPresets.put(SamplesheetHeaderName.SEQ_ANALYSIS_TYPE, - Arrays.stream(SequenceAnalysisType - .values()) - .map(e -> e.label) - .collect(Collectors.toList())); - headerToPresets.put(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID, getReplicateLabels()); - prepareColumnHeaderAndWidth(headerToPresets); - this.reloadVisibleCellContents(); - DropdownColumn analysisTypeColumn = new DropdownColumn().withItems( - Arrays.stream(SequenceAnalysisType - .values()) - .map(e -> e.label) - .collect(Collectors.toList())); - analysisTypeColumn.toRowIndex(0) - .atColIndex(header.indexOf(SamplesheetHeaderName.SEQ_ANALYSIS_TYPE)); + private void generateLigandomicsSheet() { + this.header = retrieveLigandomics(); + } - dropdownCellFactory.addDropdownColumn(analysisTypeColumn); - setupCommonDropDownColumns(); + /** + * Triggers the generation of the columnHeaders and cellValueOptions with componentRendering + * specifically defined for the {@link MetadataType} of the genomics facility + */ + private void generateGenomicsSheet() { + this.header = retrieveGenomics(); + LinkedHashMap> cellValueOptionsMap = cellValueOptionsForCellWithinColumn( + header); + generateColumnsHeaders(cellValueOptionsMap); + dropdownCellFactory.setColumnValues(cellValueOptionsMap); } - /** - * The SamplesheetHeaderName enum contains the labels which are used to refer to the headers - * employed during sample batch registration for different data technologies + * Generates a LinkedHashMap containing an ordered collection of all possible cellValueOptions for + * each Column of the {@link MetadataType} specific sheet. Necessary so the + * {@link SpreadsheetDropdownFactory} knows which cells within a column should be rendered as + * dropdown components and which should be default cells * - * @since 1.0.0 + * @param headerNames List of headerNames dependent on the selected {@link MetadataType} + * @return cellValueOptionsForColumnMap map grouping the selectable cell values within the cells + * of a column with the {@link SamplesheetHeaderName} of the colum */ - public enum SamplesheetHeaderName { - ROW("#", false), SEQ_ANALYSIS_TYPE("Analysis to be performed", - true), SAMPLE_LABEL("Sample label", true), - BIOLOGICAL_REPLICATE_ID("Biological replicate id", true), - CONDITION("Condition", true), SPECIES("Species", true), - SPECIMEN("Specimen",true), ANALYTE("Analyte", true), - CUSTOMER_COMMENT("Customer comment", false); - - public final String label; - public final boolean isMandatory; - - SamplesheetHeaderName(String label, boolean isMandatory) { - this.label = label; - this.isMandatory = isMandatory; + private LinkedHashMap> cellValueOptionsForCellWithinColumn( + List headerNames) { + LinkedHashMap> cellValueOptionsForColumnMap = new LinkedHashMap<>(); + analysisTypes = generateGenomicsAnalysisTypes(); + for (SamplesheetHeaderName head : headerNames) { + cellValueOptionsForColumnMap.put(head, new ArrayList<>()); } + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.SPECIES, species); + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.SPECIMEN, specimens); + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.ANALYTE, analytes); + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.CONDITION, + conditionsToReplicates.keySet().stream().toList()); + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.SEQ_ANALYSIS_TYPE, analysisTypes); + cellValueOptionsForColumnMap.put(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID, + getReplicateLabels()); + return cellValueOptionsForColumnMap; + } + + /** + * Collects all {@link SequenceAnalysisType} specific for the genomic {@link MetadataType} + * + * @return List of String labels for all genomic analysis types. + */ + private List generateGenomicsAnalysisTypes() { + return Arrays.stream(SequenceAnalysisType + .values()) + .map(e -> e.label) + .collect(Collectors.toList()); } public List retrieveProteomics() { @@ -449,98 +432,110 @@ public List retrieveMetabolomics() { SamplesheetHeaderName.CUSTOMER_COMMENT); } + /** + * Collects the {@link SamplesheetHeaderName} specifically for the genomic {@link MetadataType} + * + * @return List of header names for the genomic {@link MetadataType} specific sheet + */ public List retrieveGenomics() { - return List.of(SamplesheetHeaderName.ROW, SamplesheetHeaderName.SEQ_ANALYSIS_TYPE, SamplesheetHeaderName.SAMPLE_LABEL, + return List.of(SamplesheetHeaderName.ROW, SamplesheetHeaderName.SEQ_ANALYSIS_TYPE, + SamplesheetHeaderName.SAMPLE_LABEL, SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID, SamplesheetHeaderName.CONDITION, SamplesheetHeaderName.SPECIES, SamplesheetHeaderName.SPECIMEN, SamplesheetHeaderName.ANALYTE, SamplesheetHeaderName.CUSTOMER_COMMENT); } + /** + * Validates if the provided Input for each cell within the spreadsheet is valid Validation is + * based on if the {@link SamplesheetHeaderName} mandatory attribute is set to true for the + * selected cell. If that is the case, then the validation will trigger an error if the cell was + * left blank + * + * @return {@link Result} containing nothing if the inputs are valid or + * {@link InvalidSpreadsheetInput} if invalid input was detected. + */ public Result areInputsValid() { Set invalidCells = new HashSet<>(); Set validCells = new HashSet<>(); - for (int rowId = 1; rowId <= sampleRegistrationSheet.getLastRowNum(); rowId++) { - Row row = sampleRegistrationSheet.getRow(rowId); + for (int rowId = 1; rowId < getRows(); rowId++) { + Row row = getActiveSheet().getRow(rowId); // needed to highlight cells with missing values List mandatoryInputCols = new ArrayList<>(); - // needed to find which cells have missing values - List mandatoryInputs = new ArrayList<>(); for (SamplesheetHeaderName name : SamplesheetHeaderName.values()) { if (name.isMandatory) { - mandatoryInputs.add(SpreadsheetMethods.cellToStringOrNull(row.getCell( - header.indexOf(name)))); mandatoryInputCols.add(header.indexOf(name)); } } - // break when cells in row are undefined - if (mandatoryInputs.stream().anyMatch(Objects::isNull)) { - break; + // Throw exception if null values in row. + if (areNullCellsInRow(row)) { + throw new IllegalArgumentException("null value provided in row" + row.getRowNum()); } - // mandatory not filled in --> invalid - for(int colId : mandatoryInputCols) { + for (int colId : mandatoryInputCols) { Cell cell = row.getCell(colId); - if(SpreadsheetMethods.cellToStringOrNull(cell).isBlank()) { + if (SpreadsheetMethods.cellToStringOrNull(cell).isBlank()) { invalidCells.add(cell); } else { validCells.add(cell); } } - - String replicateIDInput = SpreadsheetMethods.cellToStringOrNull(row.getCell( - header.indexOf(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID))).trim(); } - defaultStyleAndUnlockEditableCells(validCells); - if(!invalidCells.isEmpty()) { + //We need to reset the style for cells with valid content if they were previously invalid + CellStyle defaultStyle = getDefaultStyle(); + validCells.forEach(cell -> cell.setCellStyle(defaultStyle)); + refreshCells(validCells); + if (!invalidCells.isEmpty()) { highlightInvalidCells(invalidCells); - return Result.fromError(new InvalidSpreadsheetInput( SpreadsheetInvalidationReason.MISSING_INPUT)); } return Result.fromValue(null); } - private void highlightInvalidCells(Collection cells) { + private CellStyle getDefaultStyle() { + CellStyle defaultStyle = getWorkbook().createCellStyle(); + defaultStyle.setLocked(false); + return defaultStyle; + } + + + private boolean areNullCellsInRow(Row row) { + return StreamSupport.stream(row.spliterator(), false).anyMatch(Objects::isNull); + } + + /** + * Sets the cell Style of the provided cells to a predefined invalid style + * + * @param invalidCells cells in which the value provided did not pass the validation step + */ + private void highlightInvalidCells(Collection invalidCells) { CellStyle invalidStyle = this.getWorkbook().createCellStyle(); invalidStyle.setLocked(false); - invalidStyle.setBorderTop(BorderStyle.THIN); - invalidStyle.setBorderLeft(BorderStyle.THIN); - invalidStyle.setBorderRight(BorderStyle.THIN); - invalidStyle.setBorderBottom(BorderStyle.THIN); - - short redIndex = IndexedColors.RED.getIndex(); - invalidStyle.setBottomBorderColor(redIndex); - invalidStyle.setTopBorderColor(redIndex); - invalidStyle.setLeftBorderColor(redIndex); - invalidStyle.setRightBorderColor(redIndex); - - for(Cell cell : cells) { + + ExtendedColor redErrorHue = SpreadsheetMethods.convertRGBToSpreadsheetColor(Color.red, 0.1); + + invalidStyle.setFillBackgroundColor(redErrorHue); + + for (Cell cell : invalidCells) { cell.setCellStyle(invalidStyle); } - this.refreshCells(cells); - } - - private boolean isUniqueSampleRow(Collection knownIDs, Row row) { - String replicateIDInput = SpreadsheetMethods.cellToStringOrNull(row.getCell( - header.indexOf(SamplesheetHeaderName.BIOLOGICAL_REPLICATE_ID))).trim(); - String conditionInput = SpreadsheetMethods.cellToStringOrNull(row.getCell( - header.indexOf(SamplesheetHeaderName.CONDITION))).trim(); - // Sample uniqueness needs to be guaranteed by condition and replicate ID - String concatenatedSampleID = replicateIDInput + conditionInput; - if(knownIDs.contains(concatenatedSampleID)) { - return false; - } else { - knownIDs.add(concatenatedSampleID); - return true; - } + //We need to refresh the cells so the style change takes effect. + this.refreshCells(invalidCells); } + /** + * Returns a List of {@link NGSRowDTO} for each row for which the least mandatory information was + * provided by the user + * + * @return {@link NGSRowDTO} containing the provided mandatory specific information for the + * genomic {@link MetadataType} sheet + */ public List getFilledRows() { List rows = new ArrayList<>(); - for (int rowId = 1; rowId <= sampleRegistrationSheet.getLastRowNum(); rowId++) { - Row row = sampleRegistrationSheet.getRow(rowId); + for (int rowId = 1; rowId < getRows(); rowId++) { + Row row = getActiveSheet().getRow(rowId); String analysisTypeInput = SpreadsheetMethods.cellToStringOrNull(row.getCell( header.indexOf(SamplesheetHeaderName.SEQ_ANALYSIS_TYPE))); @@ -560,9 +555,8 @@ public List getFilledRows() { header.indexOf(SamplesheetHeaderName.CUSTOMER_COMMENT))); // break when cells in row are undefined - if (Stream.of(analysisTypeInput, sampleLabelInput, - replicateIDInput, conditionInput, speciesInput, specimenInput, analyteInput).anyMatch(Objects::isNull)) { - break; + if (areNullCellsInRow(row)) { + throw new IllegalArgumentException("null value provided in row" + row.getRowNum()); } String conditionString = conditionInput.trim(); @@ -572,9 +566,10 @@ public List getFilledRows() { Long experimentalGroupId = experimentalGroup.id(); BiologicalReplicateId biologicalReplicateId = retrieveBiologicalReplicateId(replicateIDString, conditionString); - rows.add(new NGSRowDTO(analysisTypeInput.trim(), sampleLabelInput.trim(), biologicalReplicateId, - experimentalGroupId, speciesInput.trim(), specimenInput.trim(), analyteInput.trim(), - commentInput.trim())); + rows.add( + new NGSRowDTO(analysisTypeInput.trim(), sampleLabelInput.trim(), biologicalReplicateId, + experimentalGroupId, speciesInput.trim(), specimenInput.trim(), analyteInput.trim(), + commentInput.trim())); } return rows; } @@ -593,6 +588,11 @@ private BiologicalReplicateId retrieveBiologicalReplicateId(String replicateLabe return biologicalReplicateId; } + /** + * Record containing the provided mandatory specific information for the genomic + * {@link MetadataType} sheet + */ + public record NGSRowDTO(String analysisType, String sampleLabel, BiologicalReplicateId bioReplicateID, Long experimentalGroupId, String species, String specimen, String analyte, @@ -600,6 +600,29 @@ public record NGSRowDTO(String analysisType, String sampleLabel, } + /** + * The SamplesheetHeaderName enum contains the labels which are used to refer to the headers + * employed during sample batch registration for different data technologies + * + * @since 1.0.0 + */ + public enum SamplesheetHeaderName { + ROW("#", false), SEQ_ANALYSIS_TYPE("Analysis to be performed", + true), SAMPLE_LABEL("Sample label", true), + BIOLOGICAL_REPLICATE_ID("Biological replicate id", true), + CONDITION("Condition", true), SPECIES("Species", true), + SPECIMEN("Specimen", true), ANALYTE("Analyte", true), + CUSTOMER_COMMENT("Customer comment", false); + + public final String label; + public final boolean isMandatory; + + SamplesheetHeaderName(String label, boolean isMandatory) { + this.label = label; + this.isMandatory = isMandatory; + } + } + /** * SequenceAnalysisType enums are used in {@link SampleSpreadsheetLayout}, to indicate which type * of Analysis will be performed. @@ -615,13 +638,23 @@ enum SequenceAnalysisType { } } + /** + * The InvalidSpreadsheetInput class is employed within the spreadsheet validation and contains + * the information of the row for which the validation has failed + * {@link SpreadsheetInvalidationReason} outlining why the validation has failed and additional + * information if necessary + * + * @since 1.0.0 + */ + public static class InvalidSpreadsheetInput { private final int invalidRow; private final SpreadsheetInvalidationReason reason; private final String additionalInfo; - InvalidSpreadsheetInput(SpreadsheetInvalidationReason reason, int invalidRow, String additionalInfo) { + InvalidSpreadsheetInput(SpreadsheetInvalidationReason reason, int invalidRow, + String additionalInfo) { this.reason = reason; this.invalidRow = invalidRow; this.additionalInfo = additionalInfo; @@ -631,25 +664,22 @@ public static class InvalidSpreadsheetInput { this(reason, 0, ""); } - InvalidSpreadsheetInput(SpreadsheetInvalidationReason reason, int invalidRow) { - this(reason, invalidRow, ""); - } - /** - * Returns a String mentioning the invalid row of the spreadsheet and the reason - * why it is invalid. If this object was created with additional information on - * the reason, it is added. + * Returns a String mentioning the invalid row of the spreadsheet and the reason why it is + * invalid. If this object was created with additional information on the reason, it is added. * * @return String stating row and reason for the row being invalid */ public String getInvalidationReason() { String message = switch (reason) { - case MISSING_INPUT: yield "Please complete the missing mandatory information."; - case DUPLICATE_ID: yield "Biological replicate Id was used multiple times for the " - + "same condition in row "+invalidRow+"."; + case MISSING_INPUT: + yield "Please complete the missing mandatory information."; + case DUPLICATE_ID: + yield "Biological replicate Id was used multiple times for the " + + "same condition in row " + invalidRow + "."; }; - if(!additionalInfo.isEmpty()) { - message += ": "+additionalInfo; + if (!additionalInfo.isEmpty()) { + message += ": " + additionalInfo; } return message; diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java index 23ad92865..bc90132c0 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java @@ -1,9 +1,7 @@ package life.qbic.datamanager.views.projects.project.samples.registration.batch; -import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.ComponentEventListener; -import com.vaadin.flow.component.DetachEvent; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; @@ -12,10 +10,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import life.qbic.application.commons.Result; -import life.qbic.datamanager.views.notifications.ErrorMessage; -import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.projects.project.samples.registration.batch.SampleRegistrationSpreadsheet.InvalidSpreadsheetInput; import life.qbic.datamanager.views.projects.project.samples.registration.batch.SampleRegistrationSpreadsheet.NGSRowDTO; import life.qbic.projectmanagement.domain.project.experiment.Experiment; @@ -35,10 +30,9 @@ class SampleSpreadsheetLayout extends Div { private final Span errorInstructionSpan = new Span(); private final Span batchName = new Span(); private final Span experimentName = new Span(); - public final transient SampleRegistrationSpreadsheet sampleRegistrationSpreadsheet = new SampleRegistrationSpreadsheet(); + private final SampleRegistrationSpreadsheet sampleRegistrationSpreadsheet = new SampleRegistrationSpreadsheet(); public final Button cancelButton = new Button("Cancel"); public final Button addRowButton = new Button("Add Row"); - public final Button deleteRowButton = new Button("Delete Row"); public final Button backButton = new Button("Back"); public final Button registerButton = new Button("Register"); @@ -96,14 +90,10 @@ private void styleSampleRegistrationSpreadSheet() { } public void generateSampleRegistrationSheet(MetadataType metaDataType) { - sampleRegistrationSpreadsheet.reset(); sampleRegistrationSpreadsheet.addSheetToSpreadsheet(metaDataType); - sampleRegistrationSpreadsheet.reloadVisibleCellContents(); } - public void reset() { - //this needs to be reset when dialog is closed, as the sheet will not be recreated for set experiments - experiment = null; + public void resetLayout() { sampleInformationLayoutHandler.reset(); } @@ -118,7 +108,7 @@ public boolean isInputValid() { public void setExperiment(Experiment experiment) { this.experiment = experiment.experimentId(); experimentName.setText(experiment.getName()); - SampleRegistrationSpreadsheet.setExperimentMetadata(experiment); + sampleRegistrationSpreadsheet.setExperimentMetadata(experiment); } public List getContent() { @@ -150,12 +140,11 @@ private void resetInstructions() { private void resetSpreadSheet() { sampleRegistrationSpreadsheet.reset(); - sampleRegistrationSpreadsheet.reload(); } private boolean isInputValid() { Result content = sampleRegistrationSpreadsheet.areInputsValid(); - if(content.isValue()) { + if (content.isValue()) { hideErrorInstructions(); } return content.onError(error -> displayErrorInstructions(error.getInvalidationReason())) @@ -174,20 +163,6 @@ private void hideErrorInstructions() { errorInstructionSpan.removeAll(); } - private void displayInputInvalidMessage(String invalidationReason) { - ErrorMessage infoMessage = new ErrorMessage( - "Incomplete or erroneous metadata found", - invalidationReason); - StyledNotification notification = new StyledNotification(infoMessage); - // we need to reload the sheet as the notification popup and removal destroys the spreadsheet UI for some reason... - notification.addAttachListener( - (ComponentEventListener) attachEvent -> sampleRegistrationSpreadsheet.reload()); - notification.addDetachListener( - (ComponentEventListener) detachEvent -> sampleRegistrationSpreadsheet.reload()); - - notification.open(); - } - private List getContent() { List filledRows = sampleRegistrationSpreadsheet.getFilledRows(); List samplesToRegister = new ArrayList<>(); diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetDropdownFactory.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetDropdownFactory.java index 93863bbca..74c2911a1 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetDropdownFactory.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetDropdownFactory.java @@ -4,136 +4,86 @@ import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.spreadsheet.Spreadsheet; import com.vaadin.flow.component.spreadsheet.SpreadsheetComponentFactory; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import life.qbic.datamanager.views.projects.project.samples.registration.batch.SampleRegistrationSpreadsheet.SamplesheetHeaderName; import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Sheet; /** - * SpreadsheetDropdownFactory implements the SpreadsheetComponentFactory in order to style Spreadsheets to - * contain cells with dropdown components. Information about dropdown values and which cells of the - * spreadsheet should be styled that way must be provided. + * SpreadsheetDropdownFactory implements the SpreadsheetComponentFactory in order to style + * Spreadsheets to contain cells with dropdown components. Information about dropdown values and + * which cells of the spreadsheet should be styled that way must be provided. * * @since 1.0.0 */ public class SpreadsheetDropdownFactory implements SpreadsheetComponentFactory { - private List dropdownColumns = new ArrayList<>(); - - /** - * Initialises the dropdown factory to display a dropdown menu (ComboBox) in a specific column - * @param column a DropDownColumn object specifying column index and items to be displayed - */ - public void addDropdownColumn(DropdownColumn column) { - this.dropdownColumns.add(column); - } + private final HashMap> columnIndexToCellValueOptions = new HashMap<>(); + // this method is not used atm, as we only want to show a component if a cell is selected for performance reasons @Override public Component getCustomComponentForCell(Cell cell, int rowIndex, int columnIndex, Spreadsheet spreadsheet, Sheet sheet) { - DropdownColumn dropDownColumn = findColumnInRange(rowIndex, columnIndex); - if (spreadsheet.getActiveSheetIndex() == 0 && dropDownColumn!=null) { - if (cell == null) { - cell = spreadsheet.createCell(rowIndex, columnIndex, ""); - } - if (cell.getCellStyle().getLocked()) { - CellStyle unLockedStyle = spreadsheet.getWorkbook().createCellStyle(); - unLockedStyle.setLocked(false); - cell.setCellStyle(unLockedStyle); - spreadsheet.refreshCells(cell); - } - // if this cell contains a valid value, but no combobox (set via copying from other cells) - // we create a dropdown with that value selected - String value = cell.getStringCellValue(); - ComboBox comboBox = initCustomComboBox(dropDownColumn, rowIndex, columnIndex, - spreadsheet); - if(dropDownColumn.getItems().contains(value)) { - comboBox.setValue(value); - } - return comboBox; - } return null; } - // note: we need to unlock cells in addition to returning null in getCustomComponentForCell, - // otherwise "getCustomEditorForCell" is not called - // this method is not used atm, as it only shows the component when the cell is selected @Override public Component getCustomEditorForCell(Cell cell, int rowIndex, int columnIndex, Spreadsheet spreadsheet, Sheet sheet) { - + //We only want to have a combobox if more than one value is selectable for the user and it's not the header row. + if (hasMoreThanOneValue(columnIndex) && !isHeaderRow(rowIndex)) { + ComboBox editorCombobox = createEditorCombobox(spreadsheet, cell.getColumnIndex(), + cell.getRowIndex()); + if (!cell.getStringCellValue().isEmpty()) { + editorCombobox.setValue(cell.getStringCellValue()); + } + return editorCombobox; + } return null; } - private ComboBox initCustomComboBox(DropdownColumn dropDownColumn, int rowIndex, int columnIndex, - Spreadsheet spreadsheet) { - List items = dropDownColumn.getItems(); - ComboBox comboBox = new ComboBox<>(dropDownColumn.getLabel(), items); - comboBox.addValueChangeListener(e -> { - //when a selection is made, the value is set to the cell (in addition to the component) - //this is needed for copying of inputs to other cells works - Cell cell = spreadsheet.getCell(rowIndex, columnIndex); - cell.setCellValue(e.getValue()); + private ComboBox createEditorCombobox(Spreadsheet spreadsheet, + int selectedCellColumnIndex, int selectedCellRowIndex) { + ComboBox editorComboBox = new ComboBox<>(); + List editorItems = getColumnValues(selectedCellColumnIndex); + editorComboBox.setItems(editorItems); + editorComboBox.addValueChangeListener(e -> { + if (e.isFromClient()) { + //We add a whitespace so the value is not auto incremented when the user drags a value + Cell createdCell = spreadsheet.createCell(selectedCellRowIndex, selectedCellColumnIndex, + e.getValue() + " "); + spreadsheet.refreshCells(createdCell); + } }); - // allows copying of Comboboxes before a selection was made - Cell cell = spreadsheet.getCell(rowIndex, columnIndex); - cell.setCellValue(""); - - return comboBox; + return editorComboBox; } @Override public void onCustomEditorDisplayed(Cell cell, int rowIndex, int columnIndex, Spreadsheet spreadsheet, Sheet sheet, Component editor) { - /* not implemented since no custom editor is currently used */ } - /** - * Tests if a DropDownColumn has been defined for a provided column index and if it includes a - * provided row, that is, if a cell is to be rendered as a dropdown. If yes, the DropDownColumn - * object is returned, null otherwise. - * @param rowIndex the row index of the spreadsheet cell to test - * @param columnIndex the column index of the spreadsheet cell to test - * @return the DropDownColumn object if it has been defined for the cell, null otherwise - */ - public DropdownColumn findColumnInRange(int rowIndex, int columnIndex) { - for(DropdownColumn dropDown : dropdownColumns) { - if(dropDown.isWithinRange(rowIndex, columnIndex)) { - return dropDown; - } - } - return null; + private boolean isHeaderRow(int rowIndex) { + return rowIndex == 0; } - /** - * Increases rendering of a DropDownColumn in the specified column to include the specified row - * Nothing happens if no DropDownColumn is defined for this column - * @param rowIndex the row index of the spreadsheet cell - * @param columnIndex the column index of the spreadsheet cell - */ - public void addDropDownCell(int rowIndex, int columnIndex) { - for(DropdownColumn dropDown : dropdownColumns) { - if(dropDown.isInColumn(columnIndex)) { - dropDown.increaseToRow(rowIndex); - } + private boolean hasMoreThanOneValue(int columnIndex) { + if (columnIndexToCellValueOptions.get(columnIndex) == null) { + return false; } + return columnIndexToCellValueOptions.get(columnIndex).size() > 1; } - /** - * Returns a DropDownColumn defined for a specific column, irrespective of its row range. Returns - * null if no DropDownColumn was defined. - * @param columnIndex the spreadsheet column of the DropDownColumn - * @return the DropDownColumn object if it has been defined at this index, null otherwise - */ - public DropdownColumn getColumn(int columnIndex) { - for(DropdownColumn dropDown : dropdownColumns) { - if(dropDown.isInColumn(columnIndex)) { - return dropDown; - } - } - return null; + private List getColumnValues(int columnIndex) { + return columnIndexToCellValueOptions.get(columnIndex); + } + + //We are only interested in the columnIndex to know which options should be shown in the editor component. + public void setColumnValues(HashMap> columnValues) { + columnValues.forEach((samplesheetHeaderName, strings) -> columnIndexToCellValueOptions.put( + samplesheetHeaderName.ordinal(), strings)); } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetMethods.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetMethods.java index a47d2c338..765cfb91c 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetMethods.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SpreadsheetMethods.java @@ -1,8 +1,11 @@ package life.qbic.datamanager.views.projects.project.samples.registration.batch; +import java.awt.Color; import life.qbic.logging.api.Logger; import life.qbic.logging.service.LoggerFactory; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.ExtendedColor; +import org.apache.poi.xssf.usermodel.XSSFColor; final class SpreadsheetMethods { @@ -53,4 +56,33 @@ public static String cellToStringOrNull(Cell cell) { } } + /** + * Converts a color with alpha channel information to a color object usable in the Spreadsheet. + * Assumes white background. + * @param foreground - the foreground color as defined in Java awt + * @param alpha - the transparency as a double alpha value between 0 and 1 + * @return an ExtendedColor object to be used in a Spreadsheet + */ + public static ExtendedColor convertRGBToSpreadsheetColor(Color foreground, double alpha) { + return SpreadsheetMethods.convertRBGToSpreadsheetColor(foreground, alpha, Color.white); + } + + /** + * Converts a color with alpha channel information to a color object usable in the Spreadsheet. + * The result is influenced by the background color + * @param foreground - the foreground color as defined in Java awt + * @param alpha - the transparency as a double alpha value between 0 and 1 + * @param background - the background color as defined in Java awt + * @return an ExtendedColor object to be used in a Spreadsheet + */ + public static ExtendedColor convertRBGToSpreadsheetColor(Color foreground, double alpha, Color background) { + double red = ((1 - alpha) * background.getRed()) + (alpha * foreground.getRed()); + double green = ((1 - alpha) * background.getGreen()) + (alpha * foreground.getGreen()); + double blue = ((1 - alpha) * background.getBlue()) + (alpha * foreground.getBlue()); + + Color awtColor = new Color((int) red, (int) green, (int) blue); + + return new XSSFColor(awtColor, null); + } + } From 4c6710de358399f664f27a78e7987b382901c247 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Fri, 28 Jul 2023 10:28:34 +0200 Subject: [PATCH 3/9] Confirm experimental group deletion with the user when editing experimental variables (#334) * clean up code * add double-check confirmation dialog * address inspection warning * extract confirm dialog into new method * Fix typo Co-authored-by: Steffengreiner * Fix switched JavaDocs Co-authored-by: steffengreiner --------- Co-authored-by: Steffengreiner --- .../application/DeletionService.java | 5 +- .../ExperimentDetailsComponent.java | 37 ++- .../ExperimentalVariableContent.java | 19 ++ .../ExperimentalVariableRowLayout.java | 91 +++++++ .../ExperimentalVariablesDialog.java | 244 +++++++----------- 5 files changed, 230 insertions(+), 166 deletions(-) create mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableContent.java create mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableRowLayout.java diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java index 15e7ac53f..808ec4bf0 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java @@ -1,8 +1,7 @@ package life.qbic.projectmanagement.application; -import static java.util.Objects.*; +import static java.util.Objects.requireNonNull; -import java.util.Objects; import life.qbic.application.commons.Result; import life.qbic.projectmanagement.application.sample.SampleInformationService; import life.qbic.projectmanagement.domain.project.experiment.ExperimentId; @@ -47,7 +46,7 @@ public Result deleteAllExperimentalVariables(Experim if (queryResult.isError()) { return Result.fromError(ResponseCode.QUERY_FAILED); } - if (queryResult.isValue() && queryResult.getValue().size() > 0) { + if (queryResult.isValue() && !queryResult.getValue().isEmpty()) { return Result.fromError(ResponseCode.SAMPLES_STILL_ATTACHED_TO_EXPERIMENT); } experimentInformationService.deleteAllExperimentalVariables(id); diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java index f8c708587..0cb326708 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java @@ -6,6 +6,7 @@ import com.vaadin.flow.component.Text; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; @@ -155,27 +156,43 @@ private void configureComponent() { configureExperimentalGroupCreation(); addCancelListenerForAddVariableDialog(); addConfirmListenerForAddVariableDialog(); + addConfirmListenerForEditVariableDialog(); addListenerForNewVariableEvent(); - addEditListenerForExperimentalVariables(); } - private void addEditListenerForExperimentalVariables() { + private void addConfirmListenerForEditVariableDialog() { experimentalVariablesComponent.subscribeToEditEvent(experimentalVariablesEditEvent -> { ExperimentId experimentId = context.experimentId().orElseThrow(); var editDialog = ExperimentalVariablesDialog.prefilled( experimentInformationService.getVariablesOfExperiment(experimentId)); - editDialog.subscribeToCancelEvent( + editDialog.addCancelEventListener( experimentalVariablesDialogCancelEvent -> editDialog.close()); - editDialog.subscribeToConfirmEvent(experimentalVariablesDialogConfirmEvent -> { - deleteExistingExperimentalVariables(experimentId); - registerExperimentalVariables(experimentalVariablesDialogConfirmEvent.getSource()); - editDialog.close(); - reloadExperimentalVariables(); + editDialog.addConfirmEventListener(experimentalVariablesDialogConfirmEvent -> { + var confirmDialog = experimentalGroupDeletionConfirmDialog(); + confirmDialog.addConfirmListener(confirmDeletionEvent -> { + deleteExistingExperimentalVariables(experimentId); + registerExperimentalVariables(experimentalVariablesDialogConfirmEvent.getSource()); + editDialog.close(); + reloadExperimentalVariables(); + }); + confirmDialog.open(); }); editDialog.open(); }); } + private static ConfirmDialog experimentalGroupDeletionConfirmDialog() { + var confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Your experimental groups will be deleted"); + confirmDialog.setText( + "Editing experimental variables requires all experimental groups to be deleted. Are you sure you want to delete them?"); + confirmDialog.setConfirmText("Delete experimental groups"); + confirmDialog.setCancelable(true); + confirmDialog.setCancelText("Abort"); + confirmDialog.setRejectable(false); + return confirmDialog; + } + private void reloadExperimentalVariables() { loadExperiment(context.experimentId().orElseThrow()); } @@ -233,7 +250,7 @@ private void configureExperimentalGroupCreation() { } private void addCancelListenerForAddVariableDialog() { - addExperimentalVariablesDialog.subscribeToCancelEvent(it -> it.getSource().close()); + addExperimentalVariablesDialog.addCancelEventListener(it -> it.getSource().close()); } private void handleGroupSubmittedSuccess() { @@ -293,7 +310,7 @@ private void handleDeletionClickedEvent( } private void addConfirmListenerForAddVariableDialog() { - addExperimentalVariablesDialog.subscribeToConfirmEvent(it -> { + addExperimentalVariablesDialog.addConfirmEventListener(it -> { try { registerExperimentalVariables(it.getSource()); it.getSource().close(); diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableContent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableContent.java new file mode 100644 index 000000000..16f37450d --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableContent.java @@ -0,0 +1,19 @@ +package life.qbic.datamanager.views.projects.project.experiments.experiment.components; + +import java.util.List; + +/** + * @param name the name of the variable + * @param unit the unit of the variable levels + * @param levels the variable levels that are assigned to the variable + */ +public record ExperimentalVariableContent(String name, String unit, List levels) { + + static ExperimentalVariableContent from(final ExperimentalVariableRowLayout layout) { + final String variableName = layout.getVariableName(); + final String unit = layout.getUnit(); + final List levels = layout.getLevels(); + return new ExperimentalVariableContent(variableName, unit, levels); + } + +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableRowLayout.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableRowLayout.java new file mode 100644 index 000000000..7c639a833 --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariableRowLayout.java @@ -0,0 +1,91 @@ +package life.qbic.datamanager.views.projects.project.experiments.experiment.components; + +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.shared.Registration; +import java.io.Serial; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import life.qbic.projectmanagement.domain.project.experiment.ExperimentalVariable; + +/** + * A layout containing rows for experimental variable input + */ +final class ExperimentalVariableRowLayout extends Span { + + @Serial + private static final long serialVersionUID = -1126299161780107501L; + private final TextField nameField = new TextField("Experimental Variable"); + private final TextField unitField = new TextField("Unit"); + private final TextArea levelArea = new TextArea("Levels"); + private final Icon deleteIcon = new Icon(VaadinIcon.CLOSE_SMALL); + private Registration clickListener; + + ExperimentalVariableRowLayout() { + init(); + } + + static ExperimentalVariableRowLayout from( + final ExperimentalVariable experimentalVariable) { + final ExperimentalVariableRowLayout rowLayout = new ExperimentalVariableRowLayout(); + rowLayout.nameField.setValue(experimentalVariable.name().value()); + rowLayout.unitField.setValue( + experimentalVariable.levels().get(0).experimentalValue().unit().orElse("")); + rowLayout.levelArea.setValue( + experimentalVariable.levels().stream().map(it -> it.experimentalValue().value()) + .collect(Collectors.joining("\n"))); + return rowLayout; + } + + private void init() { + addClassName("row"); + FormLayout experimentalVariableFieldsLayout = new FormLayout(); + experimentalVariableFieldsLayout.add(nameField, unitField, levelArea); + experimentalVariableFieldsLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 3)); + nameField.setRequired(true); + levelArea.setRequired(true); + add(experimentalVariableFieldsLayout, deleteIcon); + } + + public String getVariableName() { + return nameField.getValue(); + } + + public String getUnit() { + return unitField.getValue(); + } + + public List getLevels() { + return levelArea.getValue().lines().filter(it -> !it.isBlank()).toList(); + } + + public void setCloseListener( + Consumer closeListener) { + if (Objects.nonNull(clickListener)) { + clickListener.remove(); + } + clickListener = deleteIcon.addClickListener(it -> closeListener.accept( + new CloseEvent(this))); + } + + public boolean isValid() { + boolean isNameFieldValid = !nameField.isInvalid() && !nameField.isEmpty(); + boolean isLevelFieldValid = !levelArea.isInvalid() && !levelArea.isEmpty(); + return isNameFieldValid && isLevelFieldValid; + } + + public boolean isEmpty() { + return nameField.isEmpty() && unitField.isEmpty() && levelArea.isEmpty(); + } + + public record CloseEvent(ExperimentalVariableRowLayout origin) { + + } + +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariablesDialog.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariablesDialog.java index 9ed31d2f4..949efa219 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariablesDialog.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalVariablesDialog.java @@ -2,20 +2,16 @@ import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.formlayout.FormLayout.ResponsiveStep; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.shared.Registration; import java.io.Serial; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.stream.Collectors; import life.qbic.datamanager.views.general.CancelEvent; import life.qbic.datamanager.views.general.ConfirmEvent; import life.qbic.datamanager.views.general.DialogWindow; @@ -32,12 +28,12 @@ public class ExperimentalVariablesDialog extends DialogWindow { @Serial private static final long serialVersionUID = 5296014328282974007L; - private final List experimentalVariablesLayoutRows = new ArrayList<>(); - private final List>> listenersCancellation = new ArrayList<>(); + private final List experimentalVariablesLayoutRows = new ArrayList<>(); + private final List>> cancelEventListeners = new ArrayList<>(); private final Div dialogueContentLayout = new Div(); private final Div experimentalVariableRowsContainerLayout = new Div(); private final Span addExperimentalVariableLayoutRow = new Span(); - private final List>> listenersConfirmation = new ArrayList<>(); + private final List>> confirmEventListeners = new ArrayList<>(); private final MODE mode; public ExperimentalVariablesDialog() { @@ -55,34 +51,39 @@ private ExperimentalVariablesDialog(boolean editMode) { configureComponent(); } + /** + * Creates a new dialog prefilled with experimental variables. + * + * @param experimentalVariables the variables to fill the dialog with + * @return a new instance of the dialog + */ public static ExperimentalVariablesDialog prefilled( List experimentalVariables) { return editDialog(experimentalVariables); } private static ExperimentalVariablesDialog editDialog( - List experimentalVariables) { - ExperimentalVariablesDialog experimentalVariablesDialog = new ExperimentalVariablesDialog(true); - var rowLayouts = experimentalVariablesDialog.convertVariables(experimentalVariables); + final Collection experimentalVariables) { + final ExperimentalVariablesDialog experimentalVariablesDialog = new ExperimentalVariablesDialog( + true); + final var rowLayouts = convertVariables(experimentalVariables); rowLayouts.forEach(experimentalVariablesDialog::prefill); return experimentalVariablesDialog; } - private void prefill(ExperimentalVariableRowLayout rowLayout) { + private void prefill(final ExperimentalVariableRowLayout rowLayout) { appendRow(rowLayout); } - private List convertVariables( - List variables) { - return variables.stream().map(this::convert).toList(); - } - - private ExperimentalVariableRowLayout convert(ExperimentalVariable experimentalVariable) { - return ExperimentalVariableRowLayout.from(experimentalVariable); + private static List convertVariables( + final Collection variables) { + return variables.stream() + .map(ExperimentalVariableRowLayout::from) + .toList(); } private String confirmActionLabel() { - return mode.equals(MODE.EDIT) ? "Save" : "Add"; + return isEditing() ? "Save" : "Add"; } private void configureComponent() { @@ -92,15 +93,21 @@ private void configureComponent() { } private void configureConfirmation() { - ConfirmEvent confirmEvent = new ConfirmEvent<>(this, true); - confirmButton.addClickListener(confirmListener -> listenersConfirmation.forEach( - listener -> listener.onComponentEvent(confirmEvent))); + this.confirmButton.addClickListener(event -> fireConfirmEvent()); } private void configureCancelling() { - CancelEvent cancelEvent = new CancelEvent<>(this, true); - cancelButton.addClickListener(cancelListener -> listenersCancellation.forEach( - listener -> listener.onComponentEvent(cancelEvent))); + this.cancelButton.addClickListener(cancelListener -> fireCancelEvent()); + } + + private void fireConfirmEvent() { + this.confirmEventListeners.forEach( + listener -> listener.onComponentEvent(new ConfirmEvent<>(this, true))); + } + + private void fireCancelEvent() { + this.cancelEventListeners.forEach( + listener -> listener.onComponentEvent(new CancelEvent<>(this, true))); } private void resetDialogUponClosure() { @@ -108,14 +115,24 @@ private void resetDialogUponClosure() { addDialogCloseActionListener(closeActionEvent -> resetAndClose()); } - public void subscribeToConfirmEvent( - ComponentEventListener> listener) { - listenersConfirmation.add(listener); + /** + * Adds a listener for {@link ConfirmEvent}s + * + * @param listener the listener to add + */ + public void addConfirmEventListener( + final ComponentEventListener> listener) { + this.confirmEventListeners.add(listener); } - public void subscribeToCancelEvent( - ComponentEventListener> listener) { - listenersCancellation.add(listener); + /** + * Adds a listener for {@link CancelEvent}s + * + * @param listener the listener to add + */ + public void addCancelEventListener( + final ComponentEventListener> listener) { + this.cancelEventListeners.add(listener); } /** @@ -132,86 +149,84 @@ private void resetAndClose() { } private void reset() { - experimentalVariablesLayoutRows.clear(); - experimentalVariableRowsContainerLayout.removeAll(); + this.experimentalVariablesLayoutRows.clear(); + this.experimentalVariableRowsContainerLayout.removeAll(); initDefineExperimentalVariableLayout(); } private void initDefineExperimentalVariableLayout() { - Span experimentalDesignHeader = new Span("Define Experimental Variable"); + final Span experimentalDesignHeader = new Span("Define Experimental Variable"); experimentalDesignHeader.addClassName("header"); - experimentalVariableRowsContainerLayout.add(experimentalDesignHeader); - if (mode.equals(MODE.ADD)) { + this.experimentalVariableRowsContainerLayout.add(experimentalDesignHeader); + if (isAdding()) { appendEmptyRow(); } } + private boolean isAdding() { + return MODE.ADD == this.mode; + } + + private boolean isEditing() { + return MODE.EDIT == this.mode; + } + private void appendEmptyRow() { - appendRow(new ExperimentalVariablesDialog.ExperimentalVariableRowLayout()); + appendRow(new ExperimentalVariableRowLayout()); } - private void appendRow(ExperimentalVariablesDialog.ExperimentalVariableRowLayout component) { - component.setCloseListener(it -> removeRow(it.origin())); - this.experimentalVariablesLayoutRows.add(component); - experimentalVariableRowsContainerLayout.add(component); + private void appendRow(final ExperimentalVariableRowLayout experimentalVariableRowLayout) { + experimentalVariableRowLayout.setCloseListener(it -> removeRow(it.origin())); + this.experimentalVariablesLayoutRows.add(experimentalVariableRowLayout); + this.experimentalVariableRowsContainerLayout.add(experimentalVariableRowLayout); } - private void removeRow(ExperimentalVariablesDialog.ExperimentalVariableRowLayout component) { - boolean wasRemoved = this.experimentalVariablesLayoutRows.remove(component); + private void removeRow(final ExperimentalVariableRowLayout experimentalVariableRowLayout) { + final boolean wasRemoved = this.experimentalVariablesLayoutRows.remove( + experimentalVariableRowLayout); if (wasRemoved) { - experimentalVariableRowsContainerLayout.remove(component); + this.experimentalVariableRowsContainerLayout.remove(experimentalVariableRowLayout); } } private void layoutComponent() { setHeaderTitle("Experimental Design"); - getFooter().add(cancelButton, confirmButton); + final DialogFooter footer = getFooter(); + footer.add(this.cancelButton, this.confirmButton); } private void initDialogueContent() { initDefineExperimentalVariableLayout(); initDesignVariableTemplate(); - dialogueContentLayout.addClassName("content"); - experimentalVariableRowsContainerLayout.addClassName("variables"); - dialogueContentLayout.add(experimentalVariableRowsContainerLayout); - dialogueContentLayout.add(addExperimentalVariableLayoutRow); - add(dialogueContentLayout); + this.dialogueContentLayout.addClassName("content"); + this.experimentalVariableRowsContainerLayout.addClassName("variables"); + this.dialogueContentLayout.add(this.experimentalVariableRowsContainerLayout); + this.dialogueContentLayout.add(this.addExperimentalVariableLayoutRow); + add(this.dialogueContentLayout); } private void initDesignVariableTemplate() { - TextField experimentalVariableField = new TextField("Experimental Variable"); - TextField unitField = new TextField("Unit"); - TextArea levelField = new TextArea("Levels"); + final TextField experimentalVariableField = new TextField("Experimental Variable"); + final TextField unitField = new TextField("Unit"); + final TextArea levelField = new TextArea("Levels"); experimentalVariableField.setEnabled(false); unitField.setEnabled(false); levelField.setEnabled(false); - Icon plusIcon = new Icon(VaadinIcon.PLUS); + final Icon plusIcon = new Icon(VaadinIcon.PLUS); plusIcon.addClickListener(iconClickEvent -> appendEmptyRow()); - FormLayout experimentalVariableFieldsLayout = new FormLayout(); + final FormLayout experimentalVariableFieldsLayout = new FormLayout(); experimentalVariableFieldsLayout.add(experimentalVariableField, unitField, levelField); - experimentalVariableFieldsLayout.setResponsiveSteps(new ResponsiveStep("0", 3)); - addExperimentalVariableLayoutRow.addClassName("row"); - addExperimentalVariableLayoutRow.add(plusIcon, experimentalVariableFieldsLayout); - } - - private void dropEmptyRows() { - experimentalVariablesLayoutRows.removeIf( - ExperimentalVariablesDialog.ExperimentalVariableRowLayout::isEmpty); - } - - private void closeDialogueIfValid() { - if (experimentalVariablesLayoutRows.stream() - .allMatch(ExperimentalVariablesDialog.ExperimentalVariableRowLayout::isValid)) { - resetAndClose(); - } - //ToDo what should happen if invalid information is provided in rows + experimentalVariableFieldsLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 3)); + this.addExperimentalVariableLayoutRow.addClassName("row"); + this.addExperimentalVariableLayoutRow.add(plusIcon, experimentalVariableFieldsLayout); } + /** + * @return a list of experimental variables defined by this dialog + */ public List definedVariables() { - return experimentalVariablesLayoutRows.stream().map( - experimentalVariableRowLayout -> new ExperimentalVariableContent( - experimentalVariableRowLayout.getVariableName(), - experimentalVariableRowLayout.getUnit(), experimentalVariableRowLayout.getLevels())) + return this.experimentalVariablesLayoutRows.stream() + .map(ExperimentalVariableContent::from) .toList(); } @@ -219,82 +234,5 @@ private enum MODE { ADD, EDIT } - public record ExperimentalVariableContent(String name, String unit, List levels) { - - } - - static class ExperimentalVariableRowLayout extends Span { - - @Serial - private static final long serialVersionUID = -1126299161780107501L; - private final TextField nameField = new TextField("Experimental Variable"); - private final TextField unitField = new TextField("Unit"); - private final TextArea levelArea = new TextArea("Levels"); - private final Icon deleteIcon = new Icon(VaadinIcon.CLOSE_SMALL); - private Registration clickListener; - - private ExperimentalVariableRowLayout() { - init(); - } - - protected static ExperimentalVariableRowLayout from(ExperimentalVariable experimentalVariable) { - ExperimentalVariableRowLayout rowLayout = new ExperimentalVariableRowLayout(); - rowLayout.nameField.setValue(experimentalVariable.name().value()); - rowLayout.unitField.setValue( - experimentalVariable.levels().get(0).experimentalValue().unit().orElse("")); - rowLayout.levelArea.setValue( - experimentalVariable.levels().stream().map(it -> it.experimentalValue().value()) - .collect(Collectors.joining("\n"))); - return rowLayout; - } - - private void init() { - addClassName("row"); - FormLayout experimentalVariableFieldsLayout = new FormLayout(); - experimentalVariableFieldsLayout.add(nameField, unitField, levelArea); - experimentalVariableFieldsLayout.setResponsiveSteps(new ResponsiveStep("0", 3)); - nameField.setRequired(true); - levelArea.setRequired(true); - add(experimentalVariableFieldsLayout, deleteIcon); - } - - public String getVariableName() { - return nameField.getValue(); - } - - public String getUnit() { - return unitField.getValue(); - } - - public List getLevels() { - return levelArea.getValue().lines().filter(it -> !it.isBlank()).toList(); - } - - public void setCloseListener( - Consumer closeListener) { - if (Objects.nonNull(clickListener)) { - clickListener.remove(); - } - clickListener = deleteIcon.addClickListener(it -> closeListener.accept( - new ExperimentalVariablesDialog.ExperimentalVariableRowLayout.CloseEvent(this))); - } - - public boolean isValid() { - boolean isNameFieldValid = !nameField.isInvalid() && !nameField.isEmpty(); - boolean isLevelFieldValid = !levelArea.isInvalid() && !levelArea.isEmpty(); - return isNameFieldValid && isLevelFieldValid; - } - - //We need to make sure that the service is only called if valid input is provided - - public boolean isEmpty() { - return nameField.isEmpty() && unitField.isEmpty() && levelArea.isEmpty(); - } - - private record CloseEvent(ExperimentalVariablesDialog.ExperimentalVariableRowLayout origin) { - - } - - } } From 8c521f4587323c9d995d3964ac6f073883633969 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 28 Jul 2023 12:42:50 +0200 Subject: [PATCH 4/9] Inform User that row deletion is not possible if only one row remains in the spreadsheet. (#336) * Ensure user can't delete the last row * Inline method and ensure that the header row is never deleted * Fix typo in notification --- .../batch/SampleRegistrationSpreadsheet.java | 2 +- .../batch/SampleSpreadsheetLayout.java | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java index 31e38d49c..730877e97 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleRegistrationSpreadsheet.java @@ -219,11 +219,11 @@ private void generateRowHeaderCell(int colIndex, int rowIndex) { * @param index 0-based row index of the row to remove */ public void deleteRow(int index) { + //delete row if (getRows() == 1) { // only one row remaining -> the header row return; } - //delete row deleteRows(index, index); //move other rows up if (index + 1 < getRows()) { diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java index bc90132c0..7addacb74 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleSpreadsheetLayout.java @@ -11,6 +11,8 @@ import java.util.ArrayList; import java.util.List; import life.qbic.application.commons.Result; +import life.qbic.datamanager.views.notifications.ErrorMessage; +import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.projects.project.samples.registration.batch.SampleRegistrationSpreadsheet.InvalidSpreadsheetInput; import life.qbic.datamanager.views.projects.project.samples.registration.batch.SampleRegistrationSpreadsheet.NGSRowDTO; import life.qbic.projectmanagement.domain.project.experiment.Experiment; @@ -39,6 +41,11 @@ class SampleSpreadsheetLayout extends Div { private final SampleInformationLayoutHandler sampleInformationLayoutHandler; private ExperimentId experiment; + //The spreadsheet breaks if the Notification is generated via an ApplicationException + private final StyledNotification lastRowDeletionNotification = new StyledNotification( + new ErrorMessage("Can't delete last row", + "At least one row has to remain in the spreadsheet")); + SampleSpreadsheetLayout() { initContent(); this.addClassName("batch-content"); @@ -53,6 +60,7 @@ private void initContent() { add(sampleSpreadSheetContainer); styleSampleRegistrationSpreadSheet(); initButtonLayout(); + addComponentAsFirst(lastRowDeletionNotification); } private void initHeaderAndInstruction() { @@ -76,14 +84,24 @@ private void initButtonLayout() { addRowButton.addClickListener( (ComponentEventListener>) buttonClickEvent -> sampleRegistrationSpreadsheet.addRow()); deleteRowButton.addClickListener( - event -> sampleRegistrationSpreadsheet.deleteRow( - sampleRegistrationSpreadsheet.getRows() - 1)); + event -> { + if (!isLastRow()) { + sampleRegistrationSpreadsheet.deleteRow( + sampleRegistrationSpreadsheet.getRows() - 1); + } else { + lastRowDeletionNotification.open(); + } + }); registerButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); sampleInformationButtons.add(backButton, addRowButton, deleteRowButton, cancelButton, registerButton); add(sampleInformationButtons); } + private boolean isLastRow() { + return sampleRegistrationSpreadsheet.getRows() <= 2; + } + private void styleSampleRegistrationSpreadSheet() { sampleRegistrationSpreadsheet.setSheetSelectionBarVisible(false); sampleRegistrationSpreadsheet.setFunctionBarVisible(false); From bb938930797490b112d5ab664d94f22dd37f4d94 Mon Sep 17 00:00:00 2001 From: Sven F <9976560+sven1103@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:22:16 +0200 Subject: [PATCH 5/9] Enables multiple experimental group addition (#328) * Save current work progress * Save work * Save work * Enable addition of multiple experimental groups * Reload experimental groups after registration * Enable experimental group edit * Save current work * Save the day * Finish styling * Clean up * More cleanup work * rename methods, extract methods Co-authored-by: steffengreiner * Add all experimental groups or none Co-authored-by: steffengreiner * rename method Co-authored-by: steffengreiner * Exchange group input by existing component * address code review Co-authored-by: steffengreiner * Add JavaDoc Co-authored-by: steffengreiner * rename method Co-authored-by: steffengreiner * address review * address review * remove duplicate css, group css selectors Co-authored-by: steffengreiner * remove unused css Co-authored-by: steffengreiner --------- Co-authored-by: Tobias Koch Co-authored-by: steffengreiner --- .../application/DeletionService.java | 17 ++ .../ExperimentInformationService.java | 62 +++-- .../experiment/ExperimentalDesign.java | 4 +- .../themes/datamanager/components/card.css | 21 +- .../themes/datamanager/components/custom.css | 8 + .../themes/datamanager/components/dialog.css | 66 +++-- .../datamanager/views/general/AddEvent.java | 30 +++ .../datamanager/views/general/EditEvent.java | 31 +++ .../AddExperimentalGroupsDialog.java | 100 -------- .../ExperimentDetailsComponent.java | 233 ++++++++++-------- .../experiment/ExperimentalGroupCard.java | 33 --- .../experiment/ExperimentalGroupInput.java | 93 +++++-- .../ExperimentalGroupCardCollection.java | 85 +++++-- .../components/ExperimentalGroupsDialog.java | 188 ++++++++++++++ 14 files changed, 659 insertions(+), 312 deletions(-) create mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java create mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/EditEvent.java delete mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java create mode 100644 vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupsDialog.java diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java index 808ec4bf0..1c3abac88 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/DeletionService.java @@ -1,8 +1,10 @@ package life.qbic.projectmanagement.application; import static java.util.Objects.requireNonNull; +import static life.qbic.logging.service.LoggerFactory.logger; import life.qbic.application.commons.Result; +import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.sample.SampleInformationService; import life.qbic.projectmanagement.domain.project.experiment.ExperimentId; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +20,7 @@ @Service public class DeletionService { + private static final Logger log = logger(DeletionService.class); private final ExperimentInformationService experimentInformationService; private final SampleInformationService sampleInformationService; @@ -53,6 +56,20 @@ public Result deleteAllExperimentalVariables(Experim return Result.fromValue(id); } + public Result deleteAllExperimentalGroups(ExperimentId id) { + var queryResult = sampleInformationService.retrieveSamplesForExperiment(id); + if (queryResult.isError()) { + log.debug("experiment (%s) converting %s to %s".formatted(id, queryResult.getError(), + ResponseCode.QUERY_FAILED)); + return Result.fromError(ResponseCode.QUERY_FAILED); + } + if (queryResult.isValue() && !queryResult.getValue().isEmpty()) { + return Result.fromError(ResponseCode.SAMPLES_STILL_ATTACHED_TO_EXPERIMENT); + } + experimentInformationService.deleteAllExperimentalGroups(id); + return Result.fromValue(id); + } + public enum ResponseCode { SAMPLES_STILL_ATTACHED_TO_EXPERIMENT, QUERY_FAILED } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java index d8fcfe72e..67650ad18 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/application/ExperimentInformationService.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.Result; import life.qbic.logging.api.Logger; @@ -74,7 +73,7 @@ public Result addExperimentalGroupToExperiment( Experiment activeExperiment = loadExperimentById(experimentId); Result result = activeExperiment.addExperimentalGroup( - experimentalGroup.levels(), experimentalGroup.sampleSize()); + experimentalGroup.levels(), experimentalGroup.replicateCount()); if (result.isValue()) { experimentRepository.update(activeExperiment); } @@ -100,15 +99,9 @@ public List experimentalGroupsFor(ExperimentId experimentId) return experiment.getExperimentalGroups().stream().toList(); } - public void deleteExperimentGroup(ExperimentId experimentId, long groupId) { - Experiment experiment = loadExperimentById(experimentId); - experiment.removeExperimentGroup(groupId); - experimentRepository.update(experiment); - } - /** - * ATTENTION! This will remove all existing experimental variables and all defined experimental - * groups in a give experiment! + * ATTENTION! This will remove all existing experimental variables and all defined + * experimental groups in a give experiment! * * @param experimentId the experiment reference to delete the experimental variables from * @since 1.0.0 @@ -261,19 +254,52 @@ public List getVariablesOfExperiment(ExperimentId experime } /** - * Checks if the provided ExperimentId contains an experimental Group + * Deletes all experimental groups in a given experiment. + *

+ * This method does not check if samples are already. + * + * @param id the experiment identifier of the experiment the experimental groups are going to be + * deleted. + * @since 1.0.0 + */ + public void deleteAllExperimentalGroups(ExperimentId id) { + Experiment experiment = loadExperimentById(id); + experiment.removeAllExperimentalGroups(); + experimentRepository.update(experiment); + } + + /** + * Adds experimental groups to an experiment * - * @param experimentId the {@link ExperimentId} of the {@link Experiment} which should be checked - * if it contains an {@link ExperimentalGroup} - * @return a boolean indicating if the experiment contains an {@link ExperimentalGroup} + * @param experimentId the experiment to add the groups to + * @param experimentalGroupDTOS the group information + * @return either the collection of added groups or an appropriate response code */ - public boolean hasExperimentalGroup(ExperimentId experimentId) { + public Result, ResponseCode> addExperimentalGroupsToExperiment( + ExperimentId experimentId, List experimentalGroupDTOS) { Experiment experiment = loadExperimentById(experimentId); - return !experiment.getExperimentalGroups().isEmpty(); + List addedGroups = new ArrayList<>(); + for (ExperimentalGroupDTO experimentalGroupDTO : experimentalGroupDTOS) { + Result result = experiment.addExperimentalGroup( + experimentalGroupDTO.levels(), + experimentalGroupDTO.replicateCount()); + if (result.isError()) { + return Result.fromError(result.getError()); + } else { + addedGroups.add(result.getValue()); + } + } + experimentRepository.update(experiment); + return Result.fromValue(addedGroups); } - public record ExperimentalGroupDTO(Set levels, int sampleSize) { + /** + * Information about an experimental group + * + * @param levels the levels in the condition of the group + * @param replicateCount the number of biological replicates + */ + public record ExperimentalGroupDTO(Collection levels, int replicateCount) { } - } diff --git a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java index 6279fa5ec..bb9de9f74 100644 --- a/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java +++ b/projectmanagement/src/main/java/life/qbic/projectmanagement/domain/project/experiment/ExperimentalDesign.java @@ -177,7 +177,7 @@ boolean isConditionDefined(Condition condition) { } Result addVariable(String variableName, List levels) { - if (levels.size() < 1) { + if (levels.isEmpty()) { return Result.fromError(new IllegalArgumentException( "No levels were defined for " + variableName)); } @@ -197,7 +197,7 @@ Result addVariable(String variableName, List 0) { + if (!experimentalGroups.isEmpty()) { throw new IllegalStateException("Cannot delete experimental variables referenced by an experimental group."); } this.variables.clear(); diff --git a/vaadinfrontend/frontend/themes/datamanager/components/card.css b/vaadinfrontend/frontend/themes/datamanager/components/card.css index 33d0b2a18..510b7bdde 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/card.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/card.css @@ -85,13 +85,32 @@ max-width: 300px; } -.experimental-group-card-collection { +.experimental-group-card-collection .header { + justify-content: space-between; + display: flex; +} + +.experimental-group-card-collection .content { display: flex; align-content: space-evenly; flex-flow: row wrap; gap: 1rem; } +.experimental-group-card-collection .controls { + display: flex; + align-content: space-evenly; + flex-flow: row wrap; + gap: 0.5rem; +} + +.experimental-group-card-collection { + display: block; + align-content: space-evenly; + flex-flow: row wrap; + gap: 1rem; +} + .experimental-group .header { display: flex; justify-content: space-between; diff --git a/vaadinfrontend/frontend/themes/datamanager/components/custom.css b/vaadinfrontend/frontend/themes/datamanager/components/custom.css index 3659f15c4..54d75cdff 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/custom.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/custom.css @@ -33,3 +33,11 @@ .error-text { color: var(--lumo-error-text-color); } + +vaadin-multi-select-combo-box::part(toggle-button)::before { + color: var(--lumo-primary-color); +} + +vaadin-number-field::part(decrease-button), vaadin-number-field::part(increase-button) { + color: var(--lumo-primary-color); +} diff --git a/vaadinfrontend/frontend/themes/datamanager/components/dialog.css b/vaadinfrontend/frontend/themes/datamanager/components/dialog.css index 62a0cd323..6ff5f49ce 100644 --- a/vaadinfrontend/frontend/themes/datamanager/components/dialog.css +++ b/vaadinfrontend/frontend/themes/datamanager/components/dialog.css @@ -2,6 +2,22 @@ opacity: 0.05; } +vaadin-dialog-overlay::part(content) { + padding: 1rem 4rem 3rem 4rem; +} + +vaadin-dialog-overlay::part(footer) { + padding: 1rem 4rem 1rem 0; +} + +vaadin-dialog-overlay::part(header) { + padding: 2rem 1rem 1rem 4rem; +} + +vaadin-dialog-overlay::part(title) { + margin-inline-start: 0; +} + .batch-registration-dialog .stepper { width: 100%; height: 100%; @@ -146,36 +162,55 @@ Used by both project creation and experiment creation dialog, thus the unique na width: 66%; } -.experiment-group-dialog .group-input { - width: 100%; +.experiment-group-dialog .number-field { + min-width: 175px; } -.experiment-group-dialog .group-input .layout { - display: flex; - flex-direction: row; - gap: var(--lumo-space-m); +.experiment-group-dialog .experimental-group-entry { + display: grid; + grid: auto auto auto / 1fr 175px auto; + column-gap: 1rem; + align-items: baseline; } -.experiment-group-dialog .group-input .layout .combo-box { - width: 100%; +.experiment-group-dialog vaadin-icon{ + cursor: pointer; + color: var(--lumo-primary-color); } -.experiment-group-dialog .group-input .layout .number-field { - width: 150px; +.experiment-group-dialog .header{ + font-weight: bold; } .experiment-variable-dialog .content { - padding: var(--lumo-space-m); - gap: var(--lumo-space-m); display: flex; flex-direction: column; } +.experiment-group-dialog .add-new-group-action { + display: flex; + column-gap: 1rem; + color: var(--lumo-primary-color) +} + +.experiment-group-dialog .content { + display: grid; + gap: 1.5rem; +} + +.experiment-group-dialog .content .group-collection { + display: grid; + gap: 1rem; +} + + +.experiment-group-dialog .add-new-group-action span{ + cursor: pointer; +} + .experiment-variable-dialog .content .variables { display: flex; flex-direction: column; - gap: var(--lumo-space-m); - padding: var(--lumo-space-m); } .experiment-variable-dialog .content .variables .header { @@ -185,9 +220,10 @@ Used by both project creation and experiment creation dialog, thus the unique na .experiment-variable-dialog .content .row { display: flex; align-items: center; - gap: var(--lumo-space-m); + gap: var(--lumo-space-l); } .experiment-variable-dialog .content .row vaadin-icon { cursor: pointer; + color: var(--lumo-primary-color); } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java new file mode 100644 index 000000000..94c145d5d --- /dev/null +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/general/AddEvent.java @@ -0,0 +1,30 @@ +package life.qbic.datamanager.views.general; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEvent; +import java.io.Serial; + +/** + * Add event + *

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

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

+ * + * @since 1.0.0 + */ +public class EditEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = -1033061739347133861L; + + /** + * Creates a new event using the given source and indicator whether the event originated from the + * client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public EditEvent(T source, boolean fromClient) { + super(source, fromClient); + } +} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java deleted file mode 100644 index 8b10b23a4..000000000 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/AddExperimentalGroupsDialog.java +++ /dev/null @@ -1,100 +0,0 @@ -package life.qbic.datamanager.views.projects.project.experiments.experiment; - -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.data.binder.Binder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import life.qbic.datamanager.views.general.DialogWindow; -import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentalGroupInput.ExperimentalGroupBean; -import life.qbic.projectmanagement.domain.project.experiment.VariableLevel; - -/** - * Dialog allowing the addition of experimental groups. - * - *

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

- * - * @since 1.0.0 - */ -public class AddExperimentalGroupsDialog extends DialogWindow { - - private Collection levels; - - private ExperimentalGroupInput experimentalGroupInput; - - private final List> binders = new ArrayList<>(); - - private final List submitListeners = new ArrayList<>(); - - public record ExperimentalGroupSubmitEvent(Dialog eventSourceDialog, Set variableLevels, - int sampleSize) { - - } - - @FunctionalInterface - public interface ExperimentalGroupSubmitListener { - - void handle(ExperimentalGroupSubmitEvent event); - } - - private void fireExperimentalGroupSubmitEvent(ExperimentalGroupSubmitEvent event) { - submitListeners.forEach(it -> it.handle(event)); - } - - public void addExperimentalGroupSubmitListener( - ExperimentalGroupSubmitListener experimentalGroupSubmitListener) { - this.submitListeners.add(experimentalGroupSubmitListener); - } - - - public AddExperimentalGroupsDialog() { - super(); - setConfirmButtonLabel("Add"); - setCancelButtonLabel("Cancel"); - addClassName("experiment-group-dialog"); - setHeaderTitle("Please enter group information"); - levels = Collections.emptySet(); - - confirmButton.addClickListener(event -> submit()); - cancelButton.addClickListener(event -> close()); - getFooter().add(cancelButton, confirmButton); - } - - private void submit() { - //TODO validate all fields - binders.forEach(Binder::validate); - if (experimentalGroupInput.isInvalid()) { - return; - } - ExperimentalGroupBean value = experimentalGroupInput.getValue(); - ExperimentalGroupSubmitEvent event = new ExperimentalGroupSubmitEvent( - this, - new HashSet<>(value.getLevels()), - value.getSampleSize()); - fireExperimentalGroupSubmitEvent(event); - } - - public void setLevels(Collection levels) { - if (isOpened()) { - return; - } - this.levels = levels; - } - - @Override - public void open() { - if (Objects.nonNull(experimentalGroupInput)) { - remove(experimentalGroupInput); - } - experimentalGroupInput = new ExperimentalGroupInput(levels); - experimentalGroupInput.setRequiredIndicatorVisible(true); - add(experimentalGroupInput); - super.open(); - } - -} diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java index 0cb326708..a79a7c6d4 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentDetailsComponent.java @@ -21,6 +21,8 @@ import com.vaadin.flow.theme.lumo.LumoUtility.Display; import java.io.Serial; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; import life.qbic.application.commons.ApplicationException; @@ -29,16 +31,16 @@ import life.qbic.application.commons.Result; import life.qbic.datamanager.views.AppRoutes.Projects; import life.qbic.datamanager.views.Context; -import life.qbic.datamanager.views.general.CreationCard; +import life.qbic.datamanager.views.general.ConfirmEvent; import life.qbic.datamanager.views.general.DisclaimerCard; import life.qbic.datamanager.views.general.PageArea; import life.qbic.datamanager.views.general.ToggleDisplayEditComponent; -import life.qbic.datamanager.views.notifications.InformationMessage; -import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.projects.project.experiments.ExperimentInformationMain; -import life.qbic.datamanager.views.projects.project.experiments.experiment.AddExperimentalGroupsDialog.ExperimentalGroupSubmitEvent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentInfoComponent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupCardCollection; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupsDialog; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalGroupsDialog.ExperimentalGroupContent; +import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariableContent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariablesComponent; import life.qbic.datamanager.views.projects.project.experiments.experiment.components.ExperimentalVariablesDialog; import life.qbic.logging.api.Logger; @@ -80,11 +82,7 @@ public class ExperimentDetailsComponent extends PageArea { private final Div experimentSummary = new Div(); private final ExperimentalGroupCardCollection experimentalGroupsCollection = new ExperimentalGroupCardCollection(); private final ExperimentalVariablesDialog addExperimentalVariablesDialog; - private final AddExperimentalGroupsDialog experimentalGroupsDialog; private final DisclaimerCard noExperimentalVariablesDefined; - - private final CreationCard experimentalGroupCreationCard = CreationCard.create( - "Add experimental groups"); private final DisclaimerCard addExperimentalVariablesNote; private Context context; private boolean hasExperimentalGroups; @@ -99,7 +97,6 @@ public ExperimentDetailsComponent( this.addExperimentalVariablesDialog = new ExperimentalVariablesDialog(); this.noExperimentalVariablesDefined = createNoVariableDisclaimer(); this.addExperimentalVariablesNote = createNoVariableDisclaimer(); - this.experimentalGroupsDialog = createExperimentalGroupDialog(); this.addClassName("experiment-details-component"); layoutComponent(); configureComponent(); @@ -131,12 +128,6 @@ private DisclaimerCard createNoVariableDisclaimer() { return disclaimer; } - private AddExperimentalGroupsDialog createExperimentalGroupDialog() { - AddExperimentalGroupsDialog dialog = new AddExperimentalGroupsDialog(); - dialog.addExperimentalGroupSubmitListener(this::onGroupSubmitted); - return dialog; - } - private void layoutComponent() { this.add(content); content.addClassName("details-content"); @@ -154,6 +145,7 @@ private void setTitle() { private void configureComponent() { configureExperimentalGroupCreation(); + configureExperimentalGroupsEdit(); addCancelListenerForAddVariableDialog(); addConfirmListenerForAddVariableDialog(); addConfirmListenerForEditVariableDialog(); @@ -171,7 +163,8 @@ private void addConfirmListenerForEditVariableDialog() { var confirmDialog = experimentalGroupDeletionConfirmDialog(); confirmDialog.addConfirmListener(confirmDeletionEvent -> { deleteExistingExperimentalVariables(experimentId); - registerExperimentalVariables(experimentalVariablesDialogConfirmEvent.getSource()); + addExperimentalVariables( + experimentalVariablesDialogConfirmEvent.getSource().definedVariables()); editDialog.close(); reloadExperimentalVariables(); }); @@ -194,7 +187,8 @@ private static ConfirmDialog experimentalGroupDeletionConfirmDialog() { } private void reloadExperimentalVariables() { - loadExperiment(context.experimentId().orElseThrow()); + experimentInformationService.find(context.experimentId().orElseThrow()) + .ifPresent(this::loadExperimentInformation); } private void deleteExistingExperimentalVariables(ExperimentId experimentId) { @@ -214,17 +208,6 @@ private void displayAddExperimentalVariablesDialog() { this.addExperimentalVariablesDialog.open(); } - public void onGroupSubmitted(ExperimentalGroupSubmitEvent groupSubmitted) { - Result response = experimentInformationService.addExperimentalGroupToExperiment( - context.experimentId().orElseThrow(), - new ExperimentalGroupDTO(groupSubmitted.variableLevels(), groupSubmitted.sampleSize())); - if (response.isValue()) { - handleGroupSubmittedSuccess(); - } else { - handleDuplicateConditionInput(); - } - } - private void addTagCollectionToContent() { tagCollection.addClassName("tag-collection"); content.add(tagCollection); @@ -246,19 +229,95 @@ private void layoutTabSheet() { } private void configureExperimentalGroupCreation() { - experimentalGroupCreationCard.addListener(event -> experimentalGroupsDialog.open()); + experimentalGroupsCollection.addAddEventListener(listener -> openExperimentalGroupAddDialog()); } - private void addCancelListenerForAddVariableDialog() { - addExperimentalVariablesDialog.addCancelEventListener(it -> it.getSource().close()); + private void openExperimentalGroupAddDialog() { + ExperimentId experimentId = context.experimentId().orElseThrow(); + List variables = experimentInformationService.getVariablesOfExperiment( + experimentId); + List levels = variables.stream() + .flatMap(variable -> variable.levels().stream()) + .toList(); + var dialog = getExperimentalGroupsDialogForAdding(levels); + dialog.open(); + } + + private ExperimentalGroupsDialog getExperimentalGroupsDialogForAdding( + List levels) { + var dialog = ExperimentalGroupsDialog.empty(levels); + dialog.addCancelEventListener(cancelEvent -> cancelEvent.getSource().close()); + dialog.addConfirmEventListener(this::onAddExperimentalGroupDialogConfirmed); + return dialog; } - private void handleGroupSubmittedSuccess() { + private void onAddExperimentalGroupDialogConfirmed( + ConfirmEvent confirmEvent) { + ExperimentalGroupsDialog dialog = confirmEvent.getSource(); + addExperimentalGroups(dialog.experimentalGroups()); reloadExperimentalGroups(); - if (hasExperimentalGroups) { - showSampleRegistrationPossibleNotification(); - } - experimentalGroupsDialog.close(); + dialog.close(); + } + + + private void configureExperimentalGroupsEdit() { + experimentalGroupsCollection.addEditEventListener(listener -> { + ExperimentId experimentId = context.experimentId().orElseThrow(); + List variables = experimentInformationService.getVariablesOfExperiment( + experimentId); + List levels = variables.stream() + .flatMap(variable -> variable.levels().stream()).toList(); + var experimentalGroups = experimentInformationService.getExperimentalGroups(experimentId) + .stream().map(this::toContent).toList(); + var dialog = ExperimentalGroupsDialog.prefilled(levels, experimentalGroups); + dialog.addCancelEventListener(cancelEvent -> cancelEvent.getSource().close()); + dialog.addConfirmEventListener( + confirmEvent -> { + editExperimentalGroups(confirmEvent.getSource().experimentalGroups()); + reloadExperimentalGroups(); + dialog.close(); + }); + dialog.open(); + }); + } + + private void editExperimentalGroups( + Collection experimentalGroupContents) { + ExperimentId experimentId = context.experimentId().orElseThrow(); + deletionService.deleteAllExperimentalGroups(experimentId).onError(error -> { + throw new ApplicationException("Could not edit experiments because samples are already registered."); + }); + addExperimentalGroups(experimentalGroupContents); + } + + private void addExperimentalGroups( + Collection experimentalGroupContents) { + List experimentalGroupDTOS = experimentalGroupContents.stream() + .map(this::toExperimentalGroupDTO).toList(); + ExperimentId experimentId = context.experimentId().orElseThrow(); + Result, ResponseCode> result = experimentInformationService.addExperimentalGroupsToExperiment( + experimentId, experimentalGroupDTOS); + result.onError(error -> { + throw new ApplicationException( + "Could not save one or more experimental groups %s %nReason: %s".formatted( + Arrays.toString( + experimentalGroupContents.toArray()), error)); + }); + } + + private ExperimentalGroupDTO toExperimentalGroupDTO( + ExperimentalGroupContent experimentalGroupContent) { + return new ExperimentalGroupDTO(experimentalGroupContent.variableLevels(), + experimentalGroupContent.size()); + } + + private ExperimentalGroupContent toContent(ExperimentalGroupDTO experimentalGroupDTO) { + return new ExperimentalGroupContent(experimentalGroupDTO.replicateCount(), + experimentalGroupDTO.levels()); + } + + private void addCancelListenerForAddVariableDialog() { + addExperimentalVariablesDialog.addCancelEventListener(it -> it.getSource().close()); } private void showSampleRegistrationPossibleNotification() { @@ -267,17 +326,12 @@ private void showSampleRegistrationPossibleNotification() { notification.open(); } - private void handleDuplicateConditionInput() { - InformationMessage infoMessage = new InformationMessage( - "A group with the same condition exists already.", ""); - StyledNotification notification = new StyledNotification(infoMessage); - notification.open(); - } - private void reloadExperimentalGroups() { loadExperimentalGroups(); - addCreationCardToExperimentalGroupCollection(); + if (hasExperimentalGroups) { + showSampleRegistrationPossibleNotification(); + } } private void loadExperimentalGroups() { @@ -287,32 +341,14 @@ private void loadExperimentalGroups() { List experimentalGroupsCards = experimentalGroups.stream() .map(ExperimentalGroupCard::new).toList(); - // We register the experimental details component as listener for group deletion events - experimentalGroupsCards.forEach(this::subscribeToDeletionClickEvent); - experimentalGroupsCollection.setComponents(experimentalGroupsCards); + experimentalGroupsCollection.setContent(experimentalGroupsCards); this.hasExperimentalGroups = !experimentalGroupsCards.isEmpty(); } - private void addCreationCardToExperimentalGroupCollection() { - experimentalGroupsCollection.addComponentAsLast(experimentalGroupCreationCard); - } - - private void subscribeToDeletionClickEvent(ExperimentalGroupCard experimentalGroupCard) { - experimentalGroupCard.addDeletionEventListener( - ExperimentDetailsComponent.this::handleDeletionClickedEvent); - } - - private void handleDeletionClickedEvent( - ExperimentalGroupDeletionEvent experimentalGroupDeletionEvent) { - experimentInformationService.deleteExperimentGroup(context.experimentId().orElseThrow(), - experimentalGroupDeletionEvent.getSource().groupId()); - reloadExperimentalGroups(); - } - private void addConfirmListenerForAddVariableDialog() { addExperimentalVariablesDialog.addConfirmEventListener(it -> { try { - registerExperimentalVariables(it.getSource()); + addExperimentalVariables(it.getSource().definedVariables()); it.getSource().close(); setContext(this.context); if (hasExperimentalGroups) { @@ -324,25 +360,21 @@ private void addConfirmListenerForAddVariableDialog() { }); } - private void registerExperimentalVariables( - ExperimentalVariablesDialog experimentalVariablesDialog) { - experimentalVariablesDialog.definedVariables().forEach( + private void addExperimentalVariables( + List experimentalVariableContents) { + experimentalVariableContents.forEach( experimentalVariableContent -> experimentInformationService.addVariableToExperiment( context.experimentId().orElseThrow(), - experimentalVariableContent.name(), experimentalVariableContent.unit(), - experimentalVariableContent.levels())); + experimentalVariableContent.name(), experimentalVariableContent.unit(), + experimentalVariableContent.levels())); } public void setContext(Context context) { ExperimentId experimentId = context.experimentId() .orElseThrow(() -> new ApplicationException("no experiment id in context " + context)); - ProjectId projectId = context.projectId() + context.projectId() .orElseThrow(() -> new ApplicationException("no project id in context " + context)); this.context = context; - loadExperiment(experimentId); - } - - private void loadExperiment(ExperimentId experimentId) { experimentInformationService.find(experimentId).ifPresent(this::loadExperimentInformation); } @@ -350,16 +382,16 @@ private void loadExperimentInformation(Experiment experiment) { title.setText(experiment.getName()); loadTagInformation(experiment); loadExperimentInfo(experiment); - fillExperimentalGroupDialog(); - reloadExperimentalGroups(); + loadExperimentalGroups(); if (experiment.variables().isEmpty()) { - useCaseNoVariablesYet(); + onNoVariablesDefined(); } else { - removeDisclaimer(); - displayExperimentalGroupsCollection(); + removeNoExperimentalVariablesDefinedDisclaimer(); + contentExperimentalGroupsTab.add(experimentalGroupsCollection); } } + private void loadTagInformation(Experiment experiment) { tagCollection.removeAll(); List tags = new ArrayList<>(); @@ -372,44 +404,27 @@ private void loadTagInformation(Experiment experiment) { private void loadExperimentInfo(Experiment experiment) { ExperimentInfoComponent factSheet = ExperimentInfoComponent.create(experiment.getSpecies(), experiment.getSpecimens(), experiment.getAnalytes()); + this.experimentSummary.removeAll(); + this.experimentSummary.add(factSheet); + factSheet.showMenu(); + reloadExperimentalVariables(experiment); + } + + private void reloadExperimentalVariables(Experiment experiment) { this.experimentalVariablesComponent.setExperimentalVariables(experiment.variables()); - ExperimentDetailsComponent.this.experimentSummary.removeAll(); - ExperimentDetailsComponent.this.experimentSummary.add(factSheet); if (experiment.variables().isEmpty()) { - ExperimentDetailsComponent.this.experimentSummary.add(addExperimentalVariablesNote); + this.experimentSummary.add(addExperimentalVariablesNote); } else { - ExperimentDetailsComponent.this.experimentSummary.add(experimentalVariablesComponent); + this.experimentSummary.add(experimentalVariablesComponent); } - factSheet.showMenu(); - } - - private void fillExperimentalGroupDialog() { - List variables = experimentInformationService.getVariablesOfExperiment( - context.experimentId().orElseThrow()); - List levels = variables.stream() - .flatMap(variable -> variable.levels().stream()).toList(); - experimentalGroupsDialog.setLevels(levels); - } - - private void useCaseNoVariablesYet() { - displayDisclaimer(); - hideExperimentalGroupsCollection(); } - private void removeDisclaimer() { - contentExperimentalGroupsTab.remove(noExperimentalVariablesDefined); - } - - private void displayExperimentalGroupsCollection() { - contentExperimentalGroupsTab.add(experimentalGroupsCollection); - } - - private void displayDisclaimer() { + private void onNoVariablesDefined() { contentExperimentalGroupsTab.add(noExperimentalVariablesDefined); - } - - private void hideExperimentalGroupsCollection() { contentExperimentalGroupsTab.remove(experimentalGroupsCollection); } + private void removeNoExperimentalVariablesDefinedDisclaimer() { + contentExperimentalGroupsTab.remove(noExperimentalVariablesDefined); + } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java index 983f93573..8be5613d9 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupCard.java @@ -27,29 +27,20 @@ public class ExperimentalGroupCard extends Card { @Serial private static final long serialVersionUID = -8400631799486647200L; private final transient ExperimentalGroup experimentalGroup; - private final List> listenersDeletionEvent; public ExperimentalGroupCard(ExperimentalGroup experimentalGroup) { super(); this.experimentalGroup = experimentalGroup; - this.listenersDeletionEvent = new ArrayList<>(); layoutComponent(); } private void layoutComponent() { addClassName("experimental-group"); - MenuBar menuBar = createMenuBar(); - Div cardHeader = new Div(); cardHeader.addClassName("header"); - Div controls = new Div(); - controls.addClassName("controls"); - controls.add(menuBar); - cardHeader.add(title("Experimental Group")); - cardHeader.add(controls); this.add(cardHeader); Div cardContent = new Div(); @@ -59,16 +50,6 @@ private void layoutComponent() { this.add(cardContent); } - private MenuBar createMenuBar() { - MenuBar menuBar = new MenuBar(); - menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY); - MenuItem menuItem = menuBar.addItem("•••"); - SubMenu subMenu = menuItem.getSubMenu(); - subMenu.addItem("Edit", event -> { - }); - subMenu.addItem("Delete", event -> fireDeletionEvent()); - return menuBar; - } private Span title(String value) { Span cardTitle = new Span(); @@ -97,15 +78,6 @@ private Span sampleSize() { return span; } - public void fireDeletionEvent() { - var deletionEvent = new ExperimentalGroupDeletionEvent(ExperimentalGroupCard.this, true); - listenersDeletionEvent.forEach(listener -> listener.onComponentEvent(deletionEvent)); - } - - public void addDeletionEventListener( - ComponentEventListener listener) { - this.listenersDeletionEvent.add(listener); - } public long groupId() { return this.experimentalGroup.id(); @@ -115,9 +87,4 @@ public ExperimentalGroup experimentalGroup() { return this.experimentalGroup; } - public void subscribeToDeletionEvent( - ComponentEventListener subscriber) { - this.listenersDeletionEvent.add(subscriber); - } - } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java index 74fb80c09..6fe3a5c68 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/ExperimentalGroupInput.java @@ -1,19 +1,23 @@ package life.qbic.datamanager.views.projects.project.experiments.experiment; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.ItemLabelGenerator; +import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.combobox.MultiSelectComboBox; import com.vaadin.flow.component.combobox.dataview.ComboBoxListDataView; import com.vaadin.flow.component.customfield.CustomField; -import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.data.binder.Binder; import jakarta.validation.constraints.Min; +import java.io.Serial; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import life.qbic.datamanager.views.general.Container; @@ -32,6 +36,7 @@ * {@link ExperimentalGroup} by defining the {@link Condition} and number of * {@link BiologicalReplicate} associated with the {@link ExperimentalGroup} */ +@Tag(Tag.DIV) public class ExperimentalGroupInput extends CustomField { private static final Comparator VARIABLE_LEVEL_COMPARATOR = Comparator @@ -41,6 +46,7 @@ public class ExperimentalGroupInput extends CustomField { "%s: %s", it.variableName().value(), ExperimentValueFormatter.format(it.experimentalValue())); + private final List> removeEventListeners; private final MultiSelectComboBox variableLevelSelect; private final NumberField replicateCountField; int variableCount = 0; @@ -54,14 +60,16 @@ public class ExperimentalGroupInput extends CustomField { * @param availableLevels Collection of {@link VariableLevel} defined for an {@link Experiment} */ public ExperimentalGroupInput(Collection availableLevels) { - addClassName("group-input"); + addClassName("experimental-group-entry"); + removeEventListeners = new ArrayList<>(); variableLevelSelect = generateVariableLevelSelect(); - replicateCountField = generateSampleSizeField(); + replicateCountField = generateBiologicalReplicateField(); - Span layout = new Span(variableLevelSelect, replicateCountField); - layout.addClassName("layout"); - add(layout); + var deleteIcon = new Icon(VaadinIcon.CLOSE_SMALL); + deleteIcon.addClickListener( + event -> fireRemoveEvent(new RemoveEvent(this, event.isFromClient()))); + add(variableLevelSelect, replicateCountField, deleteIcon); setLevels(availableLevels); addValidationForVariableCount(); variableLevelSelect.addValueChangeListener( @@ -70,6 +78,22 @@ public ExperimentalGroupInput(Collection availableLevels) { event -> setInvalid(variableLevelSelect.isInvalid() || replicateCountField.isInvalid())); } + public void setCondition(Collection levels) { + this.variableLevelSelect.setValue(levels); + } + + public void setReplicateCount(int numberOfReplicates) { + this.replicateCountField.setValue((double) numberOfReplicates); + } + + private void fireRemoveEvent(RemoveEvent event) { + removeEventListeners.forEach(listener -> listener.onComponentEvent(event)); + } + + public void addRemoveEventListener(ComponentEventListener listener) { + removeEventListeners.add(listener); + } + private static void overwriteSelectionOfSameVariable( MultiSelectComboBox selectComboBox) { selectComboBox.addSelectionListener(event -> { @@ -103,18 +127,30 @@ private void setLevels(Collection availableLevels) { @Override protected ExperimentalGroupBean generateModelValue() { - var levels = variableLevelSelect.getValue().stream().sorted(VARIABLE_LEVEL_COMPARATOR) - .toList(); - var sampleSize = Optional - .ofNullable(replicateCountField.getValue()).map(Double::intValue) - .orElse(0); + var levels = getCondition(); + var sampleSize = getReplicateCount(); return new ExperimentalGroupBean(sampleSize, levels); } + public int getReplicateCount() { + return replicateCountField.getOptionalValue().map(Double::intValue).orElse(0); + } + + public List getCondition() { + return variableLevelSelect.getValue().stream() + .sorted(VARIABLE_LEVEL_COMPARATOR) + .toList(); + } + + @Override + public ExperimentalGroupBean getEmptyValue() { + return generateModelValue(); + } + @Override protected void setPresentationValue(ExperimentalGroupBean newPresentationValue) { variableLevelSelect.setValue(newPresentationValue.levels); - replicateCountField.setValue((double) newPresentationValue.sampleSize); + replicateCountField.setValue((double) newPresentationValue.replicateCount); } @Override @@ -138,11 +174,12 @@ private MultiSelectComboBox generateVariableLevelSelect() { selectComboBox.addClassName("chip-badge"); selectComboBox.setAllowCustomValue(false); selectComboBox.setItemLabelGenerator(VARIABLE_LEVEL_ITEM_LABEL_GENERATOR); + selectComboBox.setWidthFull(); overwriteSelectionOfSameVariable(selectComboBox); return selectComboBox; } - private NumberField generateSampleSizeField() { + private NumberField generateBiologicalReplicateField() { NumberField numberField = new NumberField(); numberField.addClassName("number-field"); numberField.setLabel("Biological Replicates"); @@ -176,10 +213,10 @@ public static class ExperimentalGroupBean { private final List levels = new ArrayList<>(); @Min(1) - private final int sampleSize; + private final int replicateCount; - public ExperimentalGroupBean(int sampleSize, List levels) { - this.sampleSize = sampleSize; + public ExperimentalGroupBean(int replicateCount, List levels) { + this.replicateCount = replicateCount; this.levels.addAll(levels); } @@ -187,8 +224,26 @@ public List getLevels() { return Collections.unmodifiableList(levels); } - public int getSampleSize() { - return sampleSize; + public int getReplicateCount() { + return replicateCount; + } + } + + public static class RemoveEvent extends ComponentEvent { + + @Serial + private static final long serialVersionUID = 2934203596212238997L; + + /** + * Creates a new event using the given source and indicator whether the event originated from + * the client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public RemoveEvent(ExperimentalGroupInput source, boolean fromClient) { + super(source, fromClient); } } } diff --git a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java index 2365e0e2f..60ed8af2e 100644 --- a/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java +++ b/vaadinfrontend/src/main/java/life/qbic/datamanager/views/projects/project/experiments/experiment/components/ExperimentalGroupCardCollection.java @@ -1,11 +1,17 @@ package life.qbic.datamanager.views.projects.project.experiments.experiment.components; +import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.HasComponents; -import com.vaadin.flow.component.HasSize; -import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; import java.io.Serial; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import life.qbic.datamanager.views.general.AddEvent; +import life.qbic.datamanager.views.general.EditEvent; import life.qbic.datamanager.views.projects.project.experiments.experiment.ExperimentalGroupCard; /** @@ -15,33 +21,82 @@ * * @since 1.0.0 */ -@Tag(Tag.DIV) -public class ExperimentalGroupCardCollection extends Component implements HasComponents, HasSize { + +public class ExperimentalGroupCardCollection extends Div { @Serial private static final long serialVersionUID = -5835580091959912561L; + private final Div content = new Div(); + private final List>> addListeners = new ArrayList<>(); + private final List>> editListeners = new ArrayList<>(); + + public ExperimentalGroupCardCollection() { addClassName("experimental-group-card-collection"); + Span title = new Span("Groups"); + title.setClassName("title"); + Div header = new Div(); + header.setClassName("header"); + Div controlItems = new Div(); + controlItems.setClassName("controls"); + content.setClassName("content"); + header.add(title, controlItems); + Button addButton = new Button("Add"); + Button editButton = new Button("Edit"); + controlItems.add(editButton, addButton); + + addButton.addClassName("primary"); + add(header, content); + + addButton.addClickListener(this::emitAddEvent); + editButton.addClickListener(this::emitEditEvent); } - public void setComponents(Collection experimentalGroupComponents) { - removeAll(); - addComponents(experimentalGroupComponents); + private void emitEditEvent(ClickEvent