diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java index 2c8f7455de..93e40ab5cd 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java @@ -118,8 +118,8 @@ private static void evaluateMandatoryMetadata(NGSMethodMetadata method) } /** - * Creates a new pooled {@link NGSMeasurement} object instance, that describes an NGS measurement entity - * with many describing properties about provenance and instrumentation. + * Creates a new pooled {@link NGSMeasurement} object instance, that describes an NGS measurement + * entity with many describing properties about provenance and instrumentation. * * @param projectId the project id the measurement belongs to * @param samplePool the sample pool label the measurement represents @@ -201,6 +201,17 @@ public void updateMethod(NGSMethodMetadata methodMetadata) { setMethod(methodMetadata); } + /** + * Convenience method to query if the measurement was derived from a single sample. + * + * @return true, if the measurement was performed on a single sample, else returns false if the + * measurement was derived from pooled samples + * @since 1.0.0 + */ + public boolean isSingleSampleMeasurement() { + return specificMetadata.size() <= 1; + } + public MeasurementCode measurementCode() { return this.measurementCode; @@ -209,6 +220,7 @@ public MeasurementCode measurementCode() { public MeasurementId measurementId() { return measurementId; } + public ProjectId projectId() { return projectId; } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java index eefc2a1fc4..9a0e465ea1 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java @@ -192,13 +192,14 @@ public void addSpecificMetadata(ProteomicsSpecificMeasurementMetadata specificMe } /** - * Convenience method to query if the measurement was derived from a pooled sample. + * Convenience method to query if the measurement was derived from a single Sample. * - * @return true, if the measurement was performed on a pooled sample, else returns false + * @return true, if the measurement was performed on a single Sample, else returns false if the + * measurement was derived from pooled samples * @since 1.0.0 */ - public boolean isPooledSampleMeasurement() { - return specificMetadata.size() > 1; + public boolean isSingleSampleMeasurement() { + return specificMetadata.size() <= 1; } public MeasurementCode measurementCode() { @@ -253,7 +254,7 @@ public String lcmsMethod() { public Instant registrationDate() { return registration; } - + public void updateMethod(ProteomicsMethodMetadata method) { setMethodMetadata(method); emitUpdatedEvent(); @@ -320,10 +321,6 @@ public int hashCode() { return measurementId != null ? measurementId.hashCode() : 0; } - public Optional comment() { - return Optional.empty(); - } - public Optional technicalReplicateName() { return Optional.ofNullable(technicalReplicateName); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsSpecificMeasurementMetadata.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsSpecificMeasurementMetadata.java index 65aa9a4232..38a645b651 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsSpecificMeasurementMetadata.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsSpecificMeasurementMetadata.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.Objects; +import java.util.Optional; import life.qbic.projectmanagement.domain.model.sample.SampleId; /** @@ -80,8 +81,8 @@ public String fractionName() { return fractionName; } - public String comment() { - return comment; + public Optional comment() { + return comment.isBlank() ? Optional.empty() : Optional.of(comment); } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java index 256f16104a..300fc77480 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementEditTemplate.java @@ -167,6 +167,7 @@ public byte[] getContent() { } var startIndex = 1; // start in row number 2 with index 1 as the header row has number 1 index 0 + var helperStopIndex = 1; //stop in row number 2 with index 1 as the header row has number 1 index 0 int rowIndex = startIndex; for (NGSMeasurementEntry measurement : measurements) { Row row = getOrCreateRow(sheet, rowIndex); @@ -195,7 +196,7 @@ public byte[] getContent() { column.columnIndex(), startIndex, column.columnIndex(), - DEFAULT_GENERATED_ROW_COUNT - 1, + helperStopIndex, helper.exampleValue(), helper.description()) ); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java index edc91ddb94..f160f820e3 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/NGSMeasurementRegisterTemplate.java @@ -110,6 +110,7 @@ public byte[] getContent() { } var startIndex = 1; // start in row number 2 with index 1 as the header row has number 1 index 0 + var helperStopIndex = 1; //stop in row number 2 with index 1 as the header row has number 1 index 0 // make sure to create the visible sheet first Sheet hiddenSheet = workbook.createSheet("hidden"); Name sequencingReadTypeArea = createOptionArea(hiddenSheet, @@ -128,7 +129,7 @@ public byte[] getContent() { column.columnIndex(), startIndex, column.columnIndex(), - DEFAULT_GENERATED_ROW_COUNT - 1, + helperStopIndex, helper.exampleValue(), helper.description())); } diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java index 10af56e8c8..e1391964b4 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementEditTemplate.java @@ -172,6 +172,7 @@ public byte[] getContent() { } var startIndex = 1; // start in row number 2 with index 1 skipping the header in the first row + var helperStopIndex = 1; //stop in row number 2 with index 1 as the header row has number 1 index 0 var rowIndex = startIndex; for (ProteomicsMeasurementEntry pxpEntry : measurements) { @@ -199,7 +200,7 @@ public byte[] getContent() { column.columnIndex(), startIndex, column.columnIndex(), - DEFAULT_GENERATED_ROW_COUNT - 1, + helperStopIndex, helper.exampleValue(), helper.description()) ); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java index 7517e20a36..861e177fac 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/measurement/ProteomicsMeasurementRegisterTemplate.java @@ -123,7 +123,7 @@ public byte[] getContent() { } var startIndex = 1; // start in row number 2 with index 1 skipping the header in the first row - + var helperStopIndex = 1; //stop in row number 2 with index 1 as the header row has number 1 index 0 // make sure to create the visible sheet first Sheet hiddenSheet = workbook.createSheet("hidden"); Name digestionMethodArea = createOptionArea(hiddenSheet, "Digestion Method", @@ -141,7 +141,7 @@ public byte[] getContent() { column.columnIndex(), startIndex, column.columnIndex(), - DEFAULT_GENERATED_ROW_COUNT - 1, + helperStopIndex, helper.exampleValue(), helper.description()) ); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java index 89ab37434f..acf1f9ce39 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchRegistrationTemplate.java @@ -129,6 +129,7 @@ public static XSSFWorkbook createRegistrationTemplate(List conditions, } var startIndex = 1; //start in the second row with index 1. + var helperStopIndex = 1; //stop in the second row with index 1 var hiddenSheet = workbook.createSheet("hidden"); Name analysisToBePerformedOptions = createOptionArea(hiddenSheet, "Analysis to be performed", @@ -175,7 +176,7 @@ public static XSSFWorkbook createRegistrationTemplate(List conditions, column.columnIndex(), startIndex, column.columnIndex(), - MAX_ROW_INDEX_TO, + helperStopIndex, helper.exampleValue(), helper.description()) ); diff --git a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java index d71342091f..25801db9fe 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java +++ b/user-interface/src/main/java/life/qbic/datamanager/templates/sample/SampleBatchUpdateTemplate.java @@ -131,6 +131,7 @@ public static XSSFWorkbook createUpdateTemplate(List samples, List samples, List + { + var progressBar = new ProgressBar(); + progressBar.setIndeterminate(true); + var toast = messageFactory.pendingTaskToast("task.in-progress", new Object[]{"Doing something really heavy here"}, getLocale()); + var succeededToast = messageFactory.toast("task.finished", new Object[]{"Heavy Task #1"}, getLocale()); + toast.open(); + var ui = UI.getCurrent(); + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(5000); + + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + }).thenRunAsync(() -> { + ui.access(() -> { + toast.close(); + succeededToast.open(); + } + ); + }); + }); + content.add(button); + return content; + } + private static Div detailBoxShowCase() { Div container = new Div(); container.add(createHeading2("Detail Box")); @@ -337,7 +390,7 @@ private static Div cardShowCase() { private static class BodyFontStyles { static String[] fontStyles = new String[]{ - "normal-body-text", + NORMAL_BODY_TEXT, "small-body-text", "extra-small-body-text", "field-label-text", @@ -354,7 +407,8 @@ private static class ExampleUserInput extends Div implements UserInput { Binder binder; ExampleUserInput(String prefill) { - var dialogSection = DialogSection.with("User Input Validation", "Try correct and incorrect input values in the following field."); + var dialogSection = DialogSection.with("User Input Validation", + "Try correct and incorrect input values in the following field."); originalValue = prefill; var textField = new TextField(); textField.setLabel("Correct input is 'Riddikulus'"); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java b/user-interface/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java index 933dd796ce..e29baf3e49 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java @@ -6,6 +6,7 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Html; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.progressbar.ProgressBar; import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.spring.annotation.SpringComponent; import java.time.Duration; @@ -17,8 +18,8 @@ import org.springframework.context.NoSuchMessageException; /** - * Notifications created by this factory can be shown to the user. - * There are multiply types of notifications. + * Notifications created by this factory can be shown to the user. There are multiply types of + * notifications. *
    *
  • Toast notification
  • *
  • Notification dialog
  • @@ -33,8 +34,8 @@ @SpringComponent public class MessageSourceNotificationFactory { - private static final Logger log = logger(MessageSourceNotificationFactory.class); public static final Object[] EMPTY_PARAMETERS = new Object[]{}; + private static final Logger log = logger(MessageSourceNotificationFactory.class); private static final String DEFAULT_CONFIRM_TEXT = "Okay"; private static final NotificationLevel DEFAULT_LEVEL = NotificationLevel.INFO; private static final MessageType DEFAULT_MESSAGE_TYPE = MessageType.HTML; @@ -109,6 +110,34 @@ public Toast routingToast(String key, Object[] messageArgs, Object[] routeArgs, return toast.withRouting(linkText, navigationTarget, routeParameters); } + /** + * Creates a toast that indicates a pending task. The display duration is set to + * {@link Duration#ZERO}, since it is the client's job to close the toast explicitly after the + * pending task has finished. + *

    + * The following message keys have to be present: + *

      + *
    • {@code .message.type} + *
    • {@code .message.text} + *
    • {@code .routing.link.text} + *
    + *

    + * For more information please see toast-notifications.properties + * + * @param key the key for the messages + * @param messageArgs the parameters shown in the message + * @param locale the locale for which to load the message + * @return a Toast with loaded content + * @see #toast(String, Object[], Locale) + */ + public Toast pendingTaskToast(String key, Object[] messageArgs, Locale locale) { + var toast = toast(key, messageArgs, locale); + var progressBar = new ProgressBar(); + progressBar.setIndeterminate(true); + toast.setDuration(Duration.ZERO); + return toast.add(progressBar); + } + /** * Creates a dialog notification with the contents found for the message key. This method produces * a notification dialog with the link text read from the message properties file. diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/notifications/Toast.java b/user-interface/src/main/java/life/qbic/datamanager/views/notifications/Toast.java index 3e96054025..6688b3819e 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/notifications/Toast.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/notifications/Toast.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import life.qbic.datamanager.views.general.ComponentFunctions; /** @@ -32,10 +33,9 @@ */ public final class Toast extends Notification { - private static final Position DEFAULT_POSITION = Position.BOTTOM_START; static final boolean DEFAULT_CLOSE_ON_NAVIGATION = true; static final Duration DEFAULT_OPEN_DURATION = Duration.ofSeconds(5); - + private static final Position DEFAULT_POSITION = Position.BOTTOM_START; private final List closeOnNavigationListeners = new ArrayList<>(); private final Button closeButton; @@ -48,7 +48,7 @@ public final class Toast extends Notification { addClassName(switch (level) { case SUCCESS -> "success-toast"; case INFO -> "info-toast"; - case WARNING, ERROR -> ""; + case WARNING, ERROR -> "error-toast"; }); setPosition(DEFAULT_POSITION); @@ -67,6 +67,22 @@ private static Button buttonClosing(Toast toast) { return button; } + /** + * Creates a matching toast content containing routing components with the correct css classes. + * + * @param content + * @param routingComponent + * @return + */ + private static Component createRoutingContent(Component content, Component routingComponent) { + var container = new Div(); + container.addClassName("routing-container"); + content.addClassName("routing-content"); + routingComponent.addClassName("routing-link"); + container.add(content, routingComponent); + return container; + } + /** * Sets whether toasts are closed after navigation. By default, Toasts do not stay open after * navigation. @@ -104,7 +120,6 @@ public void setDuration(Duration duration) { super.setDuration((int) duration.toMillis()); } - /** * Expands the toast and includes a link to a specific route target. * @@ -143,23 +158,6 @@ private void refresh() { add(this.content, this.closeButton); } - - /** - * Creates a matching toast content containing routing components with the correct css classes. - * - * @param content - * @param routingComponent - * @return - */ - private static Component createRoutingContent(Component content, Component routingComponent) { - var container = new Div(); - container.addClassName("routing-container"); - content.addClassName("routing-content"); - routingComponent.addClassName("routing-link"); - container.add(content, routingComponent); - return container; - } - /** * Creates a routing component with correct css classes and layout. * @@ -178,4 +176,28 @@ private Component createRoutingComponent(String text, routerLink.add(button); return routerLink; } + + /** + * Adds a component to the {@link Toast}. If content already exists, the existing component is + * taken and wrapped together with the new component in a {@link Div}, without extra formatting. + *

    + * If no content yet exists, the passed component is taken. + * + * @param component the component to add to the toast + * @return the toast + * @since 1.8.0 + */ + Toast add(Component component) { + Objects.requireNonNull(component); + if (nonNull(this.content)) { + var copy = this.content; + var newContent = new Div(); + newContent.add(copy, component); + this.content = newContent; + } else { + this.content = component; + } + refresh(); + return this; + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java index d4c46a4385..c92265119d 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java @@ -70,10 +70,10 @@ @PermitAll public class MeasurementDetailsComponent extends PageArea implements Serializable { + public static final String CLICKABLE = "clickable"; @Serial private static final long serialVersionUID = 5086686432247130622L; private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; - public static final String CLICKABLE = "clickable"; private final TabSheet registeredMeasurementsTabSheet = new TabSheet(); private final MultiSelectLazyLoadingGrid ngsMeasurementGrid = new MultiSelectLazyLoadingGrid<>(); private final MultiSelectLazyLoadingGrid proteomicsMeasurementGrid = new MultiSelectLazyLoadingGrid<>(); @@ -156,7 +156,7 @@ private void resetTabsInTabsheet() { } private void addMeasurementTab(GridLazyDataView gridLazyDataView) { - if(gridLazyDataView.getItems().findAny().isEmpty()) { + if (gridLazyDataView.getItems().findAny().isEmpty()) { return; } if (gridLazyDataView.getItem(0) instanceof ProteomicsMeasurement) { @@ -186,18 +186,18 @@ private void createNGSMeasurementGrid() { .setAutoWidth(true) .setFlexGrow(0); ngsMeasurementGrid.addComponentColumn(measurement -> { - if (measurement.samplePoolGroup().isEmpty()) { + if (measurement.isSingleSampleMeasurement()) { return new Span( String.join(" ", groupSampleInfoIntoCodeAndLabel(measurement.measuredSamples()))); } - MeasurementPooledSamplesDialog measurementPooledSamplesDialog = new MeasurementPooledSamplesDialog( - measurement); - Icon expandIcon = VaadinIcon.EXPAND_SQUARE.create(); - expandIcon.addClassName("expand-icon"); - Span expandSpan = new Span(new Span("Pooled sample"), expandIcon); - expandSpan.addClassNames("sample-column-cell", CLICKABLE); - expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); - return expandSpan; + return createNGSPooledSampleComponent(measurement); + }) + .setTooltipGenerator(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + return String.join(" ", groupSampleInfoIntoCodeAndLabel(measurement.measuredSamples())); + } else { + return ""; + } }) .setHeader("Samples") .setAutoWidth(true); @@ -242,6 +242,27 @@ private void createNGSMeasurementGrid() { ngsMeasurement -> asClientLocalDateTime(ngsMeasurement.registrationDate()) .format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))) .setAutoWidth(true); + ngsMeasurementGrid.addComponentColumn(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + Span singularComment = new Span(); + var optMetadata = measurement.specificMeasurementMetadata().stream().findFirst(); + optMetadata.ifPresent(metadata -> singularComment.setText(metadata.comment().orElse(""))); + return singularComment; + } else { + return createNGSPooledSampleComponent(measurement); + } + }) + .setHeader("Comment") + .setTooltipGenerator(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + var optMetadata = measurement.specificMeasurementMetadata().stream().findFirst(); + if (optMetadata.isPresent()) { + return optMetadata.get().comment().orElse(""); + } + } + return ""; + }) + .setAutoWidth(true); GridLazyDataView ngsGridDataView = ngsMeasurementGrid.setItems(query -> { List sortOrders = query.getSortOrders().stream().map( it -> new SortOrder(it.getSorted(), it.getDirection().equals(SortDirection.ASCENDING))) @@ -277,20 +298,19 @@ private void createProteomicsGrid() { .setAutoWidth(true) .setFlexGrow(0); proteomicsMeasurementGrid.addComponentColumn(measurement -> { - if (!measurement.isPooledSampleMeasurement()) { + if (measurement.isSingleSampleMeasurement()) { return new Span( String.join(" ", groupSampleInfoIntoCodeAndLabel(measurement.measuredSamples()))); } - MeasurementPooledSamplesDialog measurementPooledSamplesDialog = new MeasurementPooledSamplesDialog( - measurement); - Icon expandIcon = VaadinIcon.EXPAND_SQUARE.create(); - expandIcon.addClassName("expand-icon"); - Span expandSpan = new Span(new Span("Pooled sample"), expandIcon); - expandSpan.addClassNames("sample-column-cell", CLICKABLE); - expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); - return expandSpan; + return createProteomicsPooledSampleComponent(measurement); }) .setHeader("Samples") + .setTooltipGenerator(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + return String.join(" ", groupSampleInfoIntoCodeAndLabel(measurement.measuredSamples())); + } + return ""; + }) .setAutoWidth(true); proteomicsMeasurementGrid.addComponentColumn( proteomicsMeasurement -> renderOrganisation(proteomicsMeasurement.organisation())) @@ -339,9 +359,26 @@ private void createProteomicsGrid() { .setTooltipGenerator(measurement -> asClientLocalDateTime(measurement.registrationDate()) .format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))) .setAutoWidth(true); - proteomicsMeasurementGrid.addColumn(measurement -> measurement.comment().orElse("")) + proteomicsMeasurementGrid.addComponentColumn(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + Span singularComment = new Span(); + var optMetadata = measurement.specificMetadata().stream().findFirst(); + optMetadata.ifPresent(metadata -> singularComment.setText(metadata.comment().orElse(""))); + return singularComment; + } else { + return createProteomicsPooledSampleComponent(measurement); + } + }) .setHeader("Comment") - .setTooltipGenerator(measurement -> measurement.comment().orElse("")) + .setTooltipGenerator(measurement -> { + if (measurement.isSingleSampleMeasurement()) { + var optMetadata = measurement.specificMetadata().stream().findFirst(); + if (optMetadata.isPresent()) { + return optMetadata.get().comment().orElse(""); + } + } + return ""; + }) .setAutoWidth(true); GridLazyDataView proteomicsGridDataView = proteomicsMeasurementGrid.setItems( query -> { @@ -349,11 +386,11 @@ private void createProteomicsGrid() { it -> new SortOrder(it.getSorted(), it.getDirection().equals(SortDirection.ASCENDING))) .collect(Collectors.toList()); - sortOrders.add(SortOrder.of("measurementCode").ascending()); - return measurementService.findProteomicsMeasurements(searchTerm, - context.experimentId().orElseThrow(), - query.getOffset(), query.getLimit(), sortOrders, context.projectId().orElseThrow()) - .stream(); + sortOrders.add(SortOrder.of("measurementCode").ascending()); + return measurementService.findProteomicsMeasurements(searchTerm, + context.experimentId().orElseThrow(), + query.getOffset(), query.getLimit(), sortOrders, context.projectId().orElseThrow()) + .stream(); }); proteomicsGridDataView @@ -365,6 +402,28 @@ private void createProteomicsGrid() { measurementsGridDataViews.add(proteomicsGridDataView); } + private Span createProteomicsPooledSampleComponent(ProteomicsMeasurement measurement) { + MeasurementPooledSamplesDialog measurementPooledSamplesDialog = new MeasurementPooledSamplesDialog( + measurement); + Icon expandIcon = VaadinIcon.EXPAND_SQUARE.create(); + expandIcon.addClassName("expand-icon"); + Span expandSpan = new Span(new Span("Pooled sample"), expandIcon); + expandSpan.addClassNames("sample-column-cell", CLICKABLE); + expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); + return expandSpan; + } + + private Span createNGSPooledSampleComponent(NGSMeasurement measurement) { + MeasurementPooledSamplesDialog measurementPooledSamplesDialog = new MeasurementPooledSamplesDialog( + measurement); + Icon expandIcon = VaadinIcon.EXPAND_SQUARE.create(); + expandIcon.addClassName("expand-icon"); + Span expandSpan = new Span(new Span("Pooled sample"), expandIcon); + expandSpan.addClassNames("sample-column-cell", CLICKABLE); + expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); + return expandSpan; + } + private void updateSelectedMeasurementsInfo(boolean isFromClient) { listeners.forEach(listener -> listener.onComponentEvent( new MeasurementSelectionChangedEvent(this, isFromClient))); @@ -473,6 +532,54 @@ public MeasurementSelectionChangedEvent(MeasurementDetailsComponent source, } } + public static class MeasurementTechnologyTab extends Tab { + + private final Span countBadge; + private final Span technologyNameComponent; + private final String technology; + + public MeasurementTechnologyTab(String technology, int measurementCount) { + this.technology = technology; + technologyNameComponent = new Span(); + this.countBadge = createBadge(); + Span sampleCountComponent = new Span(); + sampleCountComponent.add(countBadge); + this.add(technologyNameComponent, sampleCountComponent); + setTechnologyName(technology); + setMeasurementCount(measurementCount); + addClassName("tab-with-count"); + } + + /** + * Helper method for creating a badge. + */ + private static Span createBadge() { + Tag tag = new Tag(String.valueOf(0)); + tag.setTagColor(TagColor.CONTRAST); + return tag; + } + + public String getTabLabel() { + return technology; + } + + /** + * Setter method for specifying the number of measurements of the technology type shown in this + * component + * + * @param measurementCount number of samples associated with the experiment shown in this + * component + */ + public void setMeasurementCount(int measurementCount) { + countBadge.setText(String.valueOf(measurementCount)); + } + + public void setTechnologyName(String technologyName) { + this.technologyNameComponent.setText(technologyName); + } + + } + public class MeasurementPooledSamplesDialog extends Dialog { /** @@ -542,6 +649,10 @@ private void setPooledProteomicSampleDetails( .setHeader("Measurement Label") .setTooltipGenerator(ProteomicsSpecificMeasurementMetadata::label) .setAutoWidth(true); + sampleDetailsGrid.addColumn(metadata -> metadata.comment().orElse("")) + .setHeader("comment") + .setTooltipGenerator(metadata -> metadata.comment().orElse("")) + .setAutoWidth(true); sampleDetailsGrid.setItems(proteomicsSpecificMeasurementMetadata); add(sampleDetailsGrid); } @@ -611,51 +722,4 @@ private Span pooledMeasurementEntry(String propertyLabel, String propertyValue) } } - public static class MeasurementTechnologyTab extends Tab { - - private final Span countBadge; - private final Span technologyNameComponent; - private final String technology; - - public MeasurementTechnologyTab(String technology, int measurementCount) { - this.technology = technology; - technologyNameComponent = new Span(); - this.countBadge = createBadge(); - Span sampleCountComponent = new Span(); - sampleCountComponent.add(countBadge); - this.add(technologyNameComponent, sampleCountComponent); - setTechnologyName(technology); - setMeasurementCount(measurementCount); - addClassName("tab-with-count"); - } - - public String getTabLabel() { - return technology; - } - - /** - * Helper method for creating a badge. - */ - private static Span createBadge() { - Tag tag = new Tag(String.valueOf(0)); - tag.setTagColor(TagColor.CONTRAST); - return tag; - } - - /** - * Setter method for specifying the number of measurements of the technology type shown in - * this component - * - * @param measurementCount number of samples associated with the experiment shown in this component - */ - public void setMeasurementCount(int measurementCount) { - countBadge.setText(String.valueOf(measurementCount)); - } - - public void setTechnologyName(String technologyName) { - this.technologyNameComponent.setText(technologyName); - } - - } - } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java index 7dde0ddfd3..4adeeff73c 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java @@ -18,6 +18,7 @@ import com.vaadin.flow.spring.annotation.UIScope; import jakarta.annotation.security.PermitAll; import java.io.Serial; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; @@ -39,15 +40,16 @@ import life.qbic.datamanager.views.general.download.MeasurementTemplateDownload; import life.qbic.datamanager.views.notifications.CancelConfirmationDialogFactory; import life.qbic.datamanager.views.notifications.ErrorMessage; +import life.qbic.datamanager.views.notifications.MessageSourceNotificationFactory; import life.qbic.datamanager.views.notifications.StyledNotification; +import life.qbic.datamanager.views.notifications.Toast; import life.qbic.datamanager.views.projects.project.experiments.ExperimentMainLayout; +import life.qbic.datamanager.views.projects.project.measurements.MeasurementMetadataUploadDialog.ConfirmEvent; import life.qbic.datamanager.views.projects.project.measurements.MeasurementMetadataUploadDialog.MODE; -import life.qbic.datamanager.views.projects.project.measurements.MeasurementMetadataUploadDialog.MeasurementMetadataUpload; import life.qbic.datamanager.views.projects.project.measurements.MeasurementTemplateListComponent.DownloadMeasurementTemplateEvent; import life.qbic.logging.api.Logger; import life.qbic.logging.service.LoggerFactory; import life.qbic.projectmanagement.application.ProjectInformationService; -import life.qbic.projectmanagement.application.measurement.MeasurementMetadata; import life.qbic.projectmanagement.application.measurement.MeasurementService; import life.qbic.projectmanagement.application.measurement.MeasurementService.MeasurementDeletionException; import life.qbic.projectmanagement.application.measurement.validation.MeasurementValidationService; @@ -60,7 +62,6 @@ import life.qbic.projectmanagement.domain.model.project.Project; import life.qbic.projectmanagement.domain.model.project.ProjectId; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; /** @@ -101,6 +102,7 @@ public class MeasurementMain extends Main implements BeforeEnterObserver { private final DownloadProvider proteomicsDownloadProvider; private final transient ProjectInformationService projectInformationService; private final transient CancelConfirmationDialogFactory cancelConfirmationDialogFactory; + private final transient MessageSourceNotificationFactory messageFactory; private transient Context context; public MeasurementMain( @@ -111,11 +113,13 @@ public MeasurementMain( @Autowired MeasurementPresenter measurementPresenter, @Autowired MeasurementValidationService measurementValidationService, ProjectInformationService projectInformationService, - CancelConfirmationDialogFactory cancelConfirmationDialogFactory) { + CancelConfirmationDialogFactory cancelConfirmationDialogFactory, + MessageSourceNotificationFactory messageFactory) { Objects.requireNonNull(measurementTemplateListComponent); Objects.requireNonNull(measurementDetailsComponent); Objects.requireNonNull(measurementService); Objects.requireNonNull(measurementValidationService); + this.messageFactory = Objects.requireNonNull(messageFactory); this.measurementDetailsComponent = measurementDetailsComponent; this.measurementTemplateListComponent = measurementTemplateListComponent; this.measurementService = measurementService; @@ -281,52 +285,76 @@ private void handleDeletionResults(Result re private Dialog setupDialog(MeasurementMetadataUploadDialog dialog) { dialog.addCancelListener(cancelEvent -> cancelEvent.getSource().close()); - dialog.addConfirmListener(confirmEvent -> - { - triggerMeasurementRegistration(confirmEvent.uploads(), - confirmEvent.getSource()); - setMeasurementInformation(); - }); + dialog.addConfirmListener(this::triggerMeasurementRegistration); return dialog; } private void triggerMeasurementRegistration( - List> measurementMetadataUploads, - MeasurementMetadataUploadDialog measurementMetadataUploadDialog) { - String process = - measurementMetadataUploadDialog.getMode() == MODE.EDIT ? "update" : "registration"; - for (var upload : measurementMetadataUploads) { + ConfirmEvent event) { + var uploads = new ArrayList<>(event.uploads()); + var dialog = event.getSource(); + var mode = dialog.getMode(); + UI ui = event.getSource().getUI().orElseThrow(); + for (var upload : uploads) { var measurementData = upload.measurementMetadata(); - measurementMetadataUploadDialog.taskInProgress( - "%s of %s measurements ...".formatted(StringUtils.capitalize(process), - measurementData.size()), - "This might take a minute"); + //Necessary so the dialog window switches to show the upload progress - UI.getCurrent().push(); + ui.push(); CompletableFuture>> completableFuture; - if (measurementMetadataUploadDialog.getMode().equals(MODE.EDIT)) { - completableFuture = measurementService.updateAll(upload.measurementMetadata(), + Toast pendingToast; + + // we can close the dialog, everything that comes now is executed concurrently and there is + // no need the user is blocked + event.getSource().close(); + + if (mode.equals(MODE.EDIT)) { + completableFuture = measurementService.updateAll(measurementData, context.projectId().orElseThrow()); + pendingToast = messageFactory.pendingTaskToast("task.in-progress", new Object[]{ + "Update of #%d measurements".formatted(measurementData.stream() + .map(measurementMetadata -> measurementMetadata.measurementIdentifier().orElse("")) + .distinct().toList().size())}, + getLocale()); } else { completableFuture = measurementService.registerAll(upload.measurementMetadata(), context.projectId().orElseThrow()); + pendingToast = messageFactory.pendingTaskToast("task.in-progress", new Object[]{ + "Registration of #%d measurements".formatted(upload.measurementMetadata().size())}, + getLocale()); + } + ui.access(pendingToast::open); + try { + completableFuture.exceptionally(e -> { + log.error(e.getMessage(), e); + ui.access(() -> { + pendingToast.close(); + messageFactory.toast("task.failed", new Object[]{"Registration failed"}, getLocale()) + .open(); + }); + throw new HandledException(e); + }) + .thenAccept(results -> { + var errorResult = results.stream().filter(Result::isError).findAny(); + if (errorResult.isPresent()) { + String detailedMessage = convertErrorCodeToMessage(errorResult.get().getError()); + ui.access(() -> { + pendingToast.close(); + messageFactory.toast("task.failed", new Object[]{detailedMessage}, getLocale()); + }); + } else { + ui.access(() -> { + pendingToast.close(); + messageFactory.toast("task.finished", new Object[]{"Measurement update"}, + getLocale()).open(); + setMeasurementInformation(); + }); + } + }).exceptionally(e -> { + throw new HandledException(e); + }); + } catch (HandledException e) { + log.error(e.getMessage(), e); } - completableFuture.thenAccept(results -> { - var errorResult = results.stream().filter(Result::isError).findAny(); - if (errorResult.isPresent()) { - String detailedMessage = convertErrorCodeToMessage(errorResult.get().getError()); - measurementMetadataUploadDialog.getUI().ifPresent(ui -> ui.access( - () -> measurementMetadataUploadDialog.taskFailed( - "Measurement %s could not be completed".formatted(process), - detailedMessage))); - } else { - measurementMetadataUploadDialog.getUI().ifPresent(ui -> ui.access( - () -> measurementMetadataUploadDialog.taskSucceeded( - "Measurement %s is complete".formatted(process), - "Measurement %s for %s measurements was successful".formatted(process, - measurementData.size())))); - } - }).join(); // we wait for the update to finish } } @@ -565,4 +593,12 @@ private void setSelectedMeasurementsInfo(int selectedMeasurements) { } measurementsSelectedInfoBox.setText(text); } + + static class HandledException extends RuntimeException { + + HandledException(Throwable cause) { + super(cause); + } + + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMetadataUploadDialog.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMetadataUploadDialog.java index e95d25acd2..b001177cee 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMetadataUploadDialog.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMetadataUploadDialog.java @@ -337,7 +337,6 @@ protected void onConfirmClicked(ClickEvent