From db2c46e36533e32f22d5d616b48ca4f7c2e2a086 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Thu, 27 Jul 2023 17:33:20 +0200 Subject: [PATCH] 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); + } + }