From 158c454eb2896562df7c0976217aacfc58657a9e Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 10 Dec 2025 11:13:30 -0800 Subject: [PATCH 01/15] Add '+' to show when the users mouses over the part of the row --- webapp/TargetedMS/js/QCTrendPlotPanel.js | 47 ++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index f22fbb0b9..b359f3d27 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1844,6 +1844,23 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }); } + let nonAnnotationsData = []; + Ext4.each(precursorInfo.data, function (row) { + let obj = {}; + obj['Date'] = row.fullDate; + obj['yStepIndex'] = 0; + nonAnnotationsData.push(obj); + }); + + // Remove objects from nonAnnotationsData where date matches in annotationsData + let annotationDates = Ext4.Array.pluck(this.annotationData, 'Date').map(function (d) { + return me.formatDate(new Date(d), !me.groupedX); + }); + nonAnnotationsData = nonAnnotationsData.filter(function (obj) { + var objDate = me.formatDate(new Date(obj['Date']), !me.groupedX); + return annotationDates.indexOf(objDate) === -1; + }); + // use direct D3 code to inject the annotation icons to the rendered SVG var xAcc = function(d) { var annotationDate = me.formatDate(new Date(d['Date']), !me.groupedX); @@ -1858,10 +1875,11 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { var colorAcc = function(d) { return '#' + d['Color']; }; - var annotations = this.getSvgElForPlot(plot).selectAll("path.annotation").data(this.annotationData) - .enter().append("path").attr("class", "annotation") - .attr("d", this.annotationShape(5)).attr('transform', transformAcc) - .style("fill", colorAcc).style("stroke", colorAcc); + + let annotations = this.getSvgElForPlot(plot).selectAll("path.annotation").data(this.annotationData) + .enter().append("path").attr("class", "annotation") + .attr("d", this.annotationShape(4)).attr('transform', transformAcc) + .style("fill", colorAcc).style("stroke", colorAcc); // add hover text for the annotation details annotations.append("title") @@ -1881,6 +1899,27 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }; annotations.on("mouseover", function(){ return mouseOn(this, 3); }); annotations.on("mouseout", function(){ return mouseOff(this); }); + + // Add non-annotation markers with '+' shape + let addShape = function (size) { + var s = size / 2; + return 'M' + (-s) + ',0 L' + s + ',0 M0,' + (-s) + ' L0,' + s; + }; + + let nonAnnotations = this.getSvgElForPlot(plot).selectAll("path.non-annotation").data(nonAnnotationsData) + .enter().append("path").attr("class", "non-annotation") + .attr("d", addShape(20)).attr('transform', transformAcc) + .style("fill", 'none').style("stroke", '#000000') + .style("stroke-width", 2) + .style("opacity", 0.05); + + // Add mouseover effects for non-annotations + nonAnnotations.on("mouseover", function () { + d3.select(this).transition().duration(300).style("opacity", 1).style("cursor", "pointer"); + }); + nonAnnotations.on("mouseout", function () { + d3.select(this).transition().duration(300).style("opacity", 0.05).style("cursor", "default"); + }); }, formatDate: function(d, includeTime) { From d45ec17ffa31914accdd018050e37f4934d8c2b1 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 10 Dec 2025 11:43:20 -0800 Subject: [PATCH 02/15] improve non-annotation rendering --- webapp/TargetedMS/js/QCTrendPlotPanel.js | 44 ++++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index b359f3d27..82b550ec7 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1906,19 +1906,49 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { return 'M' + (-s) + ',0 L' + s + ',0 M0,' + (-s) + ' L0,' + s; }; - let nonAnnotations = this.getSvgElForPlot(plot).selectAll("path.non-annotation").data(nonAnnotationsData) - .enter().append("path").attr("class", "non-annotation") - .attr("d", addShape(20)).attr('transform', transformAcc) + + let nonAnnotationGroups = this.getSvgElForPlot(plot).selectAll("g.non-annotation-group").data(nonAnnotationsData) + .enter().append("g").attr("class", "non-annotation-group") + .attr('transform', transformAcc); + + // Add background-rectangle (initially hidden) + nonAnnotationGroups.append("rect") + .attr("class", "non-annotation-background") + .attr("x", -10).attr("y", -10) + .attr("width", 20).attr("height", 20) + .attr("rx", 2).attr("ry", 2) + .style("fill", '#000000') + .style("opacity", 0); + + // Add the plus shape + nonAnnotationGroups.append("path") + .attr("class", "non-annotation") + .attr("d", addShape(15)) .style("fill", 'none').style("stroke", '#000000') .style("stroke-width", 2) .style("opacity", 0.05); // Add mouseover effects for non-annotations - nonAnnotations.on("mouseover", function () { - d3.select(this).transition().duration(300).style("opacity", 1).style("cursor", "pointer"); + nonAnnotationGroups.append("title") + .text("Add annotation"); + + nonAnnotationGroups.on("mouseover", function () { + d3.select(this).select(".non-annotation-background") + .transition().duration(300) + .style("opacity", 0.2); + d3.select(this).select(".non-annotation") + .transition().duration(300) + .style("opacity", 1) + .style("cursor", "pointer"); }); - nonAnnotations.on("mouseout", function () { - d3.select(this).transition().duration(300).style("opacity", 0.05).style("cursor", "default"); + nonAnnotationGroups.on("mouseout", function () { + d3.select(this).select(".non-annotation-background") + .transition().duration(300) + .style("opacity", 0); + d3.select(this).select(".non-annotation") + .transition().duration(300) + .style("opacity", 0.05) + .style("cursor", "default"); }); }, From 835be5c1b850e0681824ad5ec1add3fa6c32fc4f Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Fri, 12 Dec 2025 10:47:19 -0800 Subject: [PATCH 03/15] Create dialog to crud annotations in qcplots --- webapp/TargetedMS/js/QCTrendPlotPanel.js | 185 ++++++++++++++++++++++- 1 file changed, 179 insertions(+), 6 deletions(-) diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 82b550ec7..28046cb0f 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1304,7 +1304,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { var config = this.getReportConfig(); - var annotationSql = "SELECT qca.Date, qca.Description, qca.Created, qca.CreatedBy.DisplayName, qcat.Name, qcat.Color FROM qcannotation qca JOIN qcannotationtype qcat ON qcat.Id = qca.QCAnnotationTypeId"; + var annotationSql = "SELECT qca.Id AS qcAnnotationId, qca.Date, qca.Description, qca.Created, qca.CreatedBy.DisplayName, qcat.Id AS qcAnnotationTypeId, qcat.Name, qcat.Color FROM qcannotation qca JOIN qcannotationtype qcat ON qcat.Id = qca.QCAnnotationTypeId"; // Filter on start/end dates var separator = " WHERE "; @@ -1900,8 +1900,12 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { annotations.on("mouseover", function(){ return mouseOn(this, 3); }); annotations.on("mouseout", function(){ return mouseOff(this); }); + annotations.on("click", function (d) { + me.openAnnotationDialog(false, d).show(); + }); + // Add non-annotation markers with '+' shape - let addShape = function (size) { + const addShape = function (size) { var s = size / 2; return 'M' + (-s) + ',0 L' + s + ',0 M0,' + (-s) + ' L0,' + s; }; @@ -1926,7 +1930,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { .attr("d", addShape(15)) .style("fill", 'none').style("stroke", '#000000') .style("stroke-width", 2) - .style("opacity", 0.05); + .style("opacity", 0.03); // Add mouseover effects for non-annotations nonAnnotationGroups.append("title") @@ -1935,7 +1939,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { nonAnnotationGroups.on("mouseover", function () { d3.select(this).select(".non-annotation-background") .transition().duration(300) - .style("opacity", 0.2); + .style("opacity", 0.03) + .style("cursor", "pointer"); d3.select(this).select(".non-annotation") .transition().duration(300) .style("opacity", 1) @@ -1944,15 +1949,183 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { nonAnnotationGroups.on("mouseout", function () { d3.select(this).select(".non-annotation-background") .transition().duration(300) - .style("opacity", 0); + .style("opacity", 0) + .style("cursor", "default"); d3.select(this).select(".non-annotation") .transition().duration(300) .style("opacity", 0.05) .style("cursor", "default"); }); + + nonAnnotationGroups.on("click", function (d) { + me.openAnnotationDialog(true, d).show(); + }); + }, + + openAnnotationDialog: function (addNew, data) { + const date = this.formatDate(new Date(data['Date']), false); + const title = addNew ? 'Add Annotation' : 'Edit Annotation'; + const me = this; + + return Ext4.create('Ext.window.Window', { + title: title, + width: 400, + height: 200, + modal: true, + items: [{ + xtype: 'labkey-combo', + fieldLabel: 'Annotation Type', + name: 'annotationType', + labelWidth: 150, + width: 350, + margin: '10 0 10 0', + store: Ext4.create('LABKEY.ext4.data.Store', { + schemaName: 'targetedms', + queryName: 'QCAnnotationType', + columns: 'Id,Name', + autoLoad: true + }), + displayField: 'Name', + valueField: 'Id', + editable: false, + allowBlank: false, + value: addNew ? null : data['qcAnnotationTypeId'], + + }, { + xtype: 'textarea', + labelWidth: 150, + width: 350, + fieldLabel: 'Comment', + height: 40, + name: 'comment', + value: addNew ? '' : data['Description'] + }, { + xtype: 'datefield', + labelWidth: 150, + width: 350, + fieldLabel: 'Date', + name: 'annotationDate', + format: 'Y-m-d', + allowBlank: false, + value: date + }], + + buttons: [{ + text: 'Save', + hidden: !addNew, + handler: function () { + const win = this.up('window'); + const form = win.down('form') || win; + const annotationType = form.down('[name=annotationType]').getValue(); + const comment = form.down('[name=comment]').getValue(); + const annotationDate = form.down('[name=annotationDate]').getValue(); + + if (!annotationType || !annotationDate) { + Ext4.Msg.alert('Error', 'Please fill in all required fields.'); + return; + } + + me.saveAnnotation(annotationType, comment, annotationDate, win); + } + }, { + text: 'Update', + hidden: addNew, + handler: function () { + const win = this.up('window'); + const form = win.down('form') || win; + const annotationType = form.down('[name=annotationType]').getValue(); + const comment = form.down('[name=comment]').getValue(); + const annotationDate = form.down('[name=annotationDate]').getValue(); + + if (!annotationType || !annotationDate) { + Ext4.Msg.alert('Error', 'Please fill in all required fields.'); + return; + } + + me.updateAnnotation(data['qcAnnotationId'], annotationType, comment, annotationDate, win); + } + }, { + text: 'Delete', + hidden: addNew, + handler: function () { + const win = this.up('window'); + Ext4.Msg.confirm('Confirm Delete', 'Are you sure you want to delete this annotation?', function (btn) { + if (btn === 'yes') { + me.deleteAnnotation(data['qcAnnotationId'], win); + } + }); + } + }, { + text: 'Cancel', + handler: function () { + this.up('window').close(); + } + }] + + }); + }, + + saveAnnotation: function (annotationType, comment, annotationDate, win) { + LABKEY.Query.insertRows({ + schemaName: 'targetedms', + queryName: 'QCAnnotation', + rows: [{ + QCAnnotationTypeId: annotationType, + Description: comment, + Date: annotationDate + }], + success: function () { + Ext4.Msg.alert('Success', 'Annotation saved successfully.'); + win.close(); + this.displayTrendPlot(); + }, + failure: function (response) { + Ext4.Msg.alert('Error', 'Failed to save annotation: ' + response.exception); + }, + scope: this + }); + }, + + updateAnnotation: function (annotationId, annotationType, comment, annotationDate, win) { + LABKEY.Query.updateRows({ + schemaName: 'targetedms', + queryName: 'QCAnnotation', + rows: [{ + Id: annotationId, + QCAnnotationTypeId: annotationType, + Description: comment, + Date: annotationDate + }], + success: function () { + Ext4.Msg.alert('Success', 'Annotation updated successfully.'); + win.close(); + this.displayTrendPlot(); + }, + failure: function (response) { + Ext4.Msg.alert('Error', 'Failed to update annotation: ' + response.exception); + }, + scope: this + }); + }, + + deleteAnnotation: function (annotationId, win) { + LABKEY.Query.deleteRows({ + schemaName: 'targetedms', + queryName: 'QCAnnotation', + rows: [{ Id: annotationId }], + success: function () { + Ext4.Msg.alert('Success', 'Annotation deleted successfully.'); + win.close(); + this.displayTrendPlot(); + }, + failure: function (response) { + Ext4.Msg.alert('Error', 'Failed to delete annotation: ' + response.exception); + }, + scope: this + }); }, - formatDate: function(d, includeTime) { + formatDate: function (d, includeTime) { if (d instanceof Date) { if (includeTime) { return Ext4.util.Format.date(d, 'Y-m-d H:i:s'); From 5c6988e0f6fa67836759704c0c3696013523e74e Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Sun, 14 Dec 2025 11:43:51 -0800 Subject: [PATCH 04/15] Add test to create annotation from qc plot --- .../tests/targetedms/TargetedMSQCTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index d1e38fbbd..5553f8d95 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -255,6 +255,7 @@ public void testQCAnnotations() QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true); checkForCorrectAnnotations("Individual Plots", qcPlotsWebPart); + verifyAddAnnotationsFromQCPlots(); } @Test @@ -1056,4 +1057,60 @@ private void checkForCorrectAnnotations(String plotType, QCPlotsWebPart qcPlotsW assertEquals("Wrong annotations in " + plotType + ":" + plot.getPrecursor(), expectedAnnotations, plotAnnotations); } } + + private void verifyAddAnnotationsFromQCPlots() + { + log("Testing add annotation from QC plots"); + PanoramaDashboard qcDashboard = new PanoramaDashboard(this); + QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true); + + // Hover over the add annotation button and verify tooltip + Locator addAnnotationButton = Locator.tagWithClass("path", "non-annotation"); + mouseOver(addAnnotationButton); + waitForElement(Locator.tagWithText("title", "Add annotation")); + + // Click the add annotation button + click(addAnnotationButton); + + // Wait for the annotation window to appear + _ext4Helper.waitForMaskToDisappear(); + waitForElement(Locator.xpath("//div[contains(@class, 'x4-window')]//span[text()='Add Annotation']")); + + // Select annotation type + _ext4Helper.selectComboBoxItem(Locator.xpath("//input[@name='annotationType']"), instrumentChange.getType()); + + // Enter comment + String testComment = "Test annotation from QC plot"; + setFormElement(Locator.xpath("//textarea[@name='description']"), testComment); + + // Click save button + clickButton("Save", 0); + _ext4Helper.waitForMaskToDisappear(); + + // Wait for the plots to refresh + qcPlotsWebPart.waitForPlots(PRECURSORS.length); + + // Verify the annotation appears in the QC plots + QCHelper.Annotation testAnnotation = new QCHelper.Annotation(instrumentChange.getType(), testComment); + List qcPlots = qcPlotsWebPart.getPlots(); + boolean annotationFound = false; + for (QCPlot plot : qcPlots) + { + List annotations = plot.getAnnotations(); + for (QCHelper.Annotation annotation : annotations) + { + if (annotation.getType().equals(testAnnotation.getType()) && + annotation.getDescription().equals(testAnnotation.getDescription())) + { + annotationFound = true; + break; + } + } + if (annotationFound) + break; + } + assertTrue("Newly added annotation should appear in QC plots", annotationFound); + } + } From 1e015fbb752f675bfd5a66421c767cf8c9cbf8a1 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 15 Dec 2025 11:01:01 -0800 Subject: [PATCH 05/15] code change in test --- test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 5553f8d95..dd6c8b507 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1063,7 +1063,6 @@ private void verifyAddAnnotationsFromQCPlots() log("Testing add annotation from QC plots"); PanoramaDashboard qcDashboard = new PanoramaDashboard(this); QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); - qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true); // Hover over the add annotation button and verify tooltip Locator addAnnotationButton = Locator.tagWithClass("path", "non-annotation"); From be41c11889dbb929827be1525af78357b5c6c935 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 15 Dec 2025 12:10:34 -0800 Subject: [PATCH 06/15] code change in test --- test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index dd6c8b507..1374098eb 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1067,7 +1067,7 @@ private void verifyAddAnnotationsFromQCPlots() // Hover over the add annotation button and verify tooltip Locator addAnnotationButton = Locator.tagWithClass("path", "non-annotation"); mouseOver(addAnnotationButton); - waitForElement(Locator.tagWithText("title", "Add annotation")); + waitForElement(Locator.tagWithText("title", "Add Annotation")); // Click the add annotation button click(addAnnotationButton); From a9d069b56135e963a4da463bdaa8794014f2577e Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 15 Dec 2025 16:38:54 -0800 Subject: [PATCH 07/15] code change in test --- .../test/tests/targetedms/TargetedMSQCTest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 1374098eb..3e05b6a2d 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1066,25 +1066,24 @@ private void verifyAddAnnotationsFromQCPlots() // Hover over the add annotation button and verify tooltip Locator addAnnotationButton = Locator.tagWithClass("path", "non-annotation"); + scrollIntoView(addAnnotationButton); mouseOver(addAnnotationButton); - waitForElement(Locator.tagWithText("title", "Add Annotation")); // Click the add annotation button click(addAnnotationButton); - - // Wait for the annotation window to appear - _ext4Helper.waitForMaskToDisappear(); waitForElement(Locator.xpath("//div[contains(@class, 'x4-window')]//span[text()='Add Annotation']")); - // Select annotation type - _ext4Helper.selectComboBoxItem(Locator.xpath("//input[@name='annotationType']"), instrumentChange.getType()); + // Select an annotation type + _ext4Helper.selectComboBoxItem(Ext4Helper.Locators.formItemWithInputNamed("annotationType"), Ext4Helper.TextMatchTechnique.CONTAINS, instrumentChange.getType()); // Enter comment String testComment = "Test annotation from QC plot"; - setFormElement(Locator.xpath("//textarea[@name='description']"), testComment); + setFormElement(Locator.name("comment"), testComment); - // Click save button + // Click the save button clickButton("Save", 0); + waitForText("Annotation saved successfully"); + clickButton("OK", 0); _ext4Helper.waitForMaskToDisappear(); // Wait for the plots to refresh From d8fbbdcf2f326eca12f37d60db99e23425a6984d Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 16 Dec 2025 10:06:34 -0800 Subject: [PATCH 08/15] code change in test --- test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 3e05b6a2d..2969c281f 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1088,6 +1088,8 @@ private void verifyAddAnnotationsFromQCPlots() // Wait for the plots to refresh qcPlotsWebPart.waitForPlots(PRECURSORS.length); + refresh(); + qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); // Verify the annotation appears in the QC plots QCHelper.Annotation testAnnotation = new QCHelper.Annotation(instrumentChange.getType(), testComment); From c05857c128b8d86bbf663b02684ac3a74cb7543b Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 16 Dec 2025 12:04:22 -0800 Subject: [PATCH 09/15] code change in test --- test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 2969c281f..c77516895 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1047,6 +1047,8 @@ private void checkForCorrectAnnotations(String plotType, QCPlotsWebPart qcPlotsW { List qcPlots = qcPlotsWebPart.getPlots(); Bag expectedAnnotations = new HashBag<>(); + // instrumentChange is added twice, once using traditional insert and then via qc plots + expectedAnnotations.add(instrumentChange); expectedAnnotations.add(instrumentChange); expectedAnnotations.add(reagentChange); expectedAnnotations.add(technicianChange); From c72fbcbd8a896b99d97aa74b272a3500ab77c4df Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 16 Dec 2025 15:16:58 -0800 Subject: [PATCH 10/15] code change in test --- .../tests/targetedms/TargetedMSQCTest.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index c77516895..158f16d9a 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1047,8 +1047,6 @@ private void checkForCorrectAnnotations(String plotType, QCPlotsWebPart qcPlotsW { List qcPlots = qcPlotsWebPart.getPlots(); Bag expectedAnnotations = new HashBag<>(); - // instrumentChange is added twice, once using traditional insert and then via qc plots - expectedAnnotations.add(instrumentChange); expectedAnnotations.add(instrumentChange); expectedAnnotations.add(reagentChange); expectedAnnotations.add(technicianChange); @@ -1113,6 +1111,36 @@ private void verifyAddAnnotationsFromQCPlots() break; } assertTrue("Newly added annotation should appear in QC plots", annotationFound); + + Locator deleteAnnotation = Locator.tagWithClass("path", "annotation"); + mouseOver(deleteAnnotation); + click(deleteAnnotation); + waitForElement(Locator.xpath("//div[contains(@class, 'x4-window')]//span[text()='Edit Annotation']")); + clickButton("Delete", 0); + waitForText("Are you sure you want to delete this annotation?"); + clickButton("Yes", 0); + waitForText("Annotation deleted successfully"); + clickButton("OK", 0); + _ext4Helper.waitForMaskToDisappear(); + + refresh(); + qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + annotationFound = false; + qcPlots = qcPlotsWebPart.getPlots(); + for (QCPlot plot : qcPlots) + { + List annotations = plot.getAnnotations(); + for (QCHelper.Annotation annotation : annotations) + { + if (annotation.getType().equals(testAnnotation.getType()) && + annotation.getDescription().equals(testAnnotation.getDescription())) + { + annotationFound = true; + break; + } + } + } + assertFalse("Newly deleted annotation should not appear in QC plots", annotationFound); } } From fda8e9d74b6775af1bb539fd0568a766e19a5467 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Fri, 19 Dec 2025 11:52:44 -0800 Subject: [PATCH 11/15] manual testing and code review suggestions --- .../tests/targetedms/TargetedMSQCTest.java | 15 ++-- webapp/TargetedMS/js/QCTrendPlotPanel.js | 85 +++++++++++++------ 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 158f16d9a..520eb5855 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -26,6 +26,7 @@ import org.labkey.test.SortDirection; import org.labkey.test.TestFileUtils; import org.labkey.test.components.ext4.RadioButton; +import org.labkey.test.components.ext4.Window; import org.labkey.test.components.html.SiteNavBar; import org.labkey.test.components.targetedms.GuideSet; import org.labkey.test.components.targetedms.QCAnnotationTypeWebPart; @@ -1065,13 +1066,13 @@ private void verifyAddAnnotationsFromQCPlots() QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); // Hover over the add annotation button and verify tooltip - Locator addAnnotationButton = Locator.tagWithClass("path", "non-annotation"); + Locator addAnnotationButton = Locator.tagWithClass("path", "add-annotation"); scrollIntoView(addAnnotationButton); mouseOver(addAnnotationButton); // Click the add annotation button click(addAnnotationButton); - waitForElement(Locator.xpath("//div[contains(@class, 'x4-window')]//span[text()='Add Annotation']")); + Window addAnnotationDialog = new Window.WindowFinder(getDriver()).withTitle("Add Annotation").waitFor(); // Select an annotation type _ext4Helper.selectComboBoxItem(Ext4Helper.Locators.formItemWithInputNamed("annotationType"), Ext4Helper.TextMatchTechnique.CONTAINS, instrumentChange.getType()); @@ -1081,9 +1082,7 @@ private void verifyAddAnnotationsFromQCPlots() setFormElement(Locator.name("comment"), testComment); // Click the save button - clickButton("Save", 0); - waitForText("Annotation saved successfully"); - clickButton("OK", 0); + addAnnotationDialog.clickButton("Save", 0); _ext4Helper.waitForMaskToDisappear(); // Wait for the plots to refresh @@ -1115,12 +1114,10 @@ private void verifyAddAnnotationsFromQCPlots() Locator deleteAnnotation = Locator.tagWithClass("path", "annotation"); mouseOver(deleteAnnotation); click(deleteAnnotation); - waitForElement(Locator.xpath("//div[contains(@class, 'x4-window')]//span[text()='Edit Annotation']")); - clickButton("Delete", 0); + addAnnotationDialog = new Window.WindowFinder(getDriver()).withTitle("Edit Annotation").waitFor(); + addAnnotationDialog.clickButton("Delete", 0); waitForText("Are you sure you want to delete this annotation?"); clickButton("Yes", 0); - waitForText("Annotation deleted successfully"); - clickButton("OK", 0); _ext4Helper.waitForMaskToDisappear(); refresh(); diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 28046cb0f..9047abd9f 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1900,24 +1900,25 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { annotations.on("mouseover", function(){ return mouseOn(this, 3); }); annotations.on("mouseout", function(){ return mouseOff(this); }); - annotations.on("click", function (d) { - me.openAnnotationDialog(false, d).show(); - }); + if (this.canUserEdit()) { + annotations.on("click", function (d) { + me.openAnnotationDialog(false, d).show(); + }); + } - // Add non-annotation markers with '+' shape + // Add add-annotation markers with '+' shape const addShape = function (size) { var s = size / 2; return 'M' + (-s) + ',0 L' + s + ',0 M0,' + (-s) + ' L0,' + s; }; - - let nonAnnotationGroups = this.getSvgElForPlot(plot).selectAll("g.non-annotation-group").data(nonAnnotationsData) - .enter().append("g").attr("class", "non-annotation-group") + let nonAnnotationGroups = this.getSvgElForPlot(plot).selectAll("g.add-annotation-group").data(nonAnnotationsData) + .enter().append("g").attr("class", "add-annotation-group") .attr('transform', transformAcc); // Add background-rectangle (initially hidden) nonAnnotationGroups.append("rect") - .attr("class", "non-annotation-background") + .attr("class", "add-annotation-background") .attr("x", -10).attr("y", -10) .attr("width", 20).attr("height", 20) .attr("rx", 2).attr("ry", 2) @@ -1926,40 +1927,47 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { // Add the plus shape nonAnnotationGroups.append("path") - .attr("class", "non-annotation") + .attr("class", "add-annotation") .attr("d", addShape(15)) .style("fill", 'none').style("stroke", '#000000') .style("stroke-width", 2) - .style("opacity", 0.03); + .style("opacity", 0); - // Add mouseover effects for non-annotations + // Add mouseover effects for add-annotations nonAnnotationGroups.append("title") .text("Add annotation"); nonAnnotationGroups.on("mouseover", function () { - d3.select(this).select(".non-annotation-background") + d3.select(this).select(".add-annotation-background") .transition().duration(300) - .style("opacity", 0.03) + .style("opacity", 0) .style("cursor", "pointer"); - d3.select(this).select(".non-annotation") + d3.select(this).select(".add-annotation") .transition().duration(300) .style("opacity", 1) .style("cursor", "pointer"); }); nonAnnotationGroups.on("mouseout", function () { - d3.select(this).select(".non-annotation-background") + d3.select(this).select(".add-annotation-background") .transition().duration(300) .style("opacity", 0) .style("cursor", "default"); - d3.select(this).select(".non-annotation") + d3.select(this).select(".add-annotation") .transition().duration(300) - .style("opacity", 0.05) + .style("opacity", 0) .style("cursor", "default"); }); nonAnnotationGroups.on("click", function (d) { - me.openAnnotationDialog(true, d).show(); + if (me.canUserEdit()) { + me.openAnnotationDialog(true, d).show(); + } }); + + // Hide add-annotation markers if the user cannot modify annotations + if (!this.canUserEdit()) { + nonAnnotationGroups.style("display", "none"); + } }, openAnnotationDialog: function (addNew, data) { @@ -1978,7 +1986,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { name: 'annotationType', labelWidth: 150, width: 350, - margin: '10 0 10 0', + margin: '10 10 10 10', store: Ext4.create('LABKEY.ext4.data.Store', { schemaName: 'targetedms', queryName: 'QCAnnotationType', @@ -1997,12 +2005,15 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { width: 350, fieldLabel: 'Comment', height: 40, + margin: '10 10 10 10', name: 'comment', - value: addNew ? '' : data['Description'] + allowBlank: false, + value: addNew ? null : data['Description'] }, { xtype: 'datefield', labelWidth: 150, width: 350, + margin: '10 10 10 10', fieldLabel: 'Date', name: 'annotationDate', format: 'Y-m-d', @@ -2013,6 +2024,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { buttons: [{ text: 'Save', hidden: !addNew, + disabled: !me.canUserEdit(), handler: function () { const win = this.up('window'); const form = win.down('form') || win; @@ -2030,6 +2042,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }, { text: 'Update', hidden: addNew, + disabled: !me.canUserEdit(), handler: function () { const win = this.up('window'); const form = win.down('form') || win; @@ -2047,6 +2060,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }, { text: 'Delete', hidden: addNew, + disabled: !me.canUserEdit(), handler: function () { const win = this.up('window'); Ext4.Msg.confirm('Confirm Delete', 'Are you sure you want to delete this annotation?', function (btn) { @@ -2075,12 +2089,19 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { Date: annotationDate }], success: function () { - Ext4.Msg.alert('Success', 'Annotation saved successfully.'); win.close(); this.displayTrendPlot(); }, failure: function (response) { - Ext4.Msg.alert('Error', 'Failed to save annotation: ' + response.exception); + + Ext4.Msg.show({ + title: 'Error', + msg: 'Failed to save annotation: ' + response.exception, + buttons: Ext4.Msg.OK, + icon: Ext4.MessageBox.ERROR, + minWidth: 300, + maxWidth: 600 + }); }, scope: this }); @@ -2097,12 +2118,18 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { Date: annotationDate }], success: function () { - Ext4.Msg.alert('Success', 'Annotation updated successfully.'); win.close(); this.displayTrendPlot(); }, failure: function (response) { - Ext4.Msg.alert('Error', 'Failed to update annotation: ' + response.exception); + Ext4.Msg.show({ + title: 'Error', + msg: 'Failed to update annotation: ' + response.exception, + buttons: Ext4.Msg.OK, + icon: Ext4.MessageBox.ERROR, + minWidth: 300, + maxWidth: 600 + }); }, scope: this }); @@ -2114,12 +2141,18 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { queryName: 'QCAnnotation', rows: [{ Id: annotationId }], success: function () { - Ext4.Msg.alert('Success', 'Annotation deleted successfully.'); win.close(); this.displayTrendPlot(); }, failure: function (response) { - Ext4.Msg.alert('Error', 'Failed to delete annotation: ' + response.exception); + Ext4.Msg.show({ + title: 'Error', + msg: 'Failed to delete annotation: ' + response.exception, + buttons: Ext4.Msg.OK, + icon: Ext4.MessageBox.ERROR, + minWidth: 300, + maxWidth: 600 + }); }, scope: this }); From cd5969e906db132649b5c493ba2376f7c9f35be7 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Fri, 19 Dec 2025 12:00:44 -0800 Subject: [PATCH 12/15] manual testing and code review suggestions --- .../org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 2 +- webapp/TargetedMS/js/QCTrendPlotPanel.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 520eb5855..fa1dc92ec 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1079,7 +1079,7 @@ private void verifyAddAnnotationsFromQCPlots() // Enter comment String testComment = "Test annotation from QC plot"; - setFormElement(Locator.name("comment"), testComment); + setFormElement(Locator.name("description"), testComment); // Click the save button addAnnotationDialog.clickButton("Save", 0); diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 9047abd9f..033484df9 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -2003,10 +2003,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { xtype: 'textarea', labelWidth: 150, width: 350, - fieldLabel: 'Comment', + fieldLabel: 'Description', height: 40, margin: '10 10 10 10', - name: 'comment', + name: 'description', allowBlank: false, value: addNew ? null : data['Description'] }, { From 9f4b19c6d698a42b8db895fd5d586097c05fc756 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Fri, 19 Dec 2025 13:24:56 -0800 Subject: [PATCH 13/15] fix test --- webapp/TargetedMS/js/QCTrendPlotPanel.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 033484df9..16b5e3aae 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -2029,7 +2029,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { const win = this.up('window'); const form = win.down('form') || win; const annotationType = form.down('[name=annotationType]').getValue(); - const comment = form.down('[name=comment]').getValue(); + const description = form.down('[name=description]').getValue(); const annotationDate = form.down('[name=annotationDate]').getValue(); if (!annotationType || !annotationDate) { @@ -2037,7 +2037,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { return; } - me.saveAnnotation(annotationType, comment, annotationDate, win); + me.saveAnnotation(annotationType, description, annotationDate, win); } }, { text: 'Update', @@ -2047,7 +2047,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { const win = this.up('window'); const form = win.down('form') || win; const annotationType = form.down('[name=annotationType]').getValue(); - const comment = form.down('[name=comment]').getValue(); + const description = form.down('[name=description]').getValue(); const annotationDate = form.down('[name=annotationDate]').getValue(); if (!annotationType || !annotationDate) { @@ -2055,7 +2055,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { return; } - me.updateAnnotation(data['qcAnnotationId'], annotationType, comment, annotationDate, win); + me.updateAnnotation(data['qcAnnotationId'], annotationType, description, annotationDate, win); } }, { text: 'Delete', @@ -2079,13 +2079,13 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }); }, - saveAnnotation: function (annotationType, comment, annotationDate, win) { + saveAnnotation: function (annotationType, description, annotationDate, win) { LABKEY.Query.insertRows({ schemaName: 'targetedms', queryName: 'QCAnnotation', rows: [{ QCAnnotationTypeId: annotationType, - Description: comment, + Description: description, Date: annotationDate }], success: function () { @@ -2107,14 +2107,14 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }); }, - updateAnnotation: function (annotationId, annotationType, comment, annotationDate, win) { + updateAnnotation: function (annotationId, annotationType, description, annotationDate, win) { LABKEY.Query.updateRows({ schemaName: 'targetedms', queryName: 'QCAnnotation', rows: [{ Id: annotationId, QCAnnotationTypeId: annotationType, - Description: comment, + Description: description, Date: annotationDate }], success: function () { From 4922c1cbcbfa5d9777105a4a3fa21044ff03909b Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Fri, 19 Dec 2025 17:31:13 -0800 Subject: [PATCH 14/15] code change in test --- .../org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index fa1dc92ec..b71f5891c 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1082,13 +1082,13 @@ private void verifyAddAnnotationsFromQCPlots() setFormElement(Locator.name("description"), testComment); // Click the save button - addAnnotationDialog.clickButton("Save", 0); + addAnnotationDialog.clickButton("Save", true); _ext4Helper.waitForMaskToDisappear(); // Wait for the plots to refresh - qcPlotsWebPart.waitForPlots(PRECURSORS.length); refresh(); qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + qcPlotsWebPart.waitForReady(); // Verify the annotation appears in the QC plots QCHelper.Annotation testAnnotation = new QCHelper.Annotation(instrumentChange.getType(), testComment); From 6d26a40fc1757327cb0d504d9d533a0c86a922a6 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 22 Dec 2025 14:51:18 -0800 Subject: [PATCH 15/15] code change in test --- test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index b71f5891c..64ad440d2 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1087,6 +1087,7 @@ private void verifyAddAnnotationsFromQCPlots() // Wait for the plots to refresh refresh(); + qcDashboard = new PanoramaDashboard(this); qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); qcPlotsWebPart.waitForReady();