From 21c226194ff15f9d0f390ff3dbb022dfdef81f85 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 9 Sep 2026 14:59:34 -0600 Subject: [PATCH 1/6] Show every project and protocol column in the ehr default views --- .../resources/queries/ehr/project/.qview.xml | 20 +++++++++++++++++-- .../resources/queries/ehr/protocol/.qview.xml | 17 +++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/nbri_ehr/resources/queries/ehr/project/.qview.xml b/nbri_ehr/resources/queries/ehr/project/.qview.xml index 2022e60..b13e8bf 100644 --- a/nbri_ehr/resources/queries/ehr/project/.qview.xml +++ b/nbri_ehr/resources/queries/ehr/project/.qview.xml @@ -1,8 +1,24 @@ - + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml index 62dfd8e..74d96bb 100644 --- a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml +++ b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml @@ -1,15 +1,26 @@ - + + + + + + + + + + - + + + + - From d3f449e84f1d134b4e83d30fdb20318cf7b558b8 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 9 Sep 2026 14:59:34 -0600 Subject: [PATCH 2/6] Add protocol description and project account to the assignment forms The protocol dropdown now shows the protocol display name with its investigator's last name, and both new columns are read-only echoes of the selected lookup row. --- .../resources/queries/ehr/activeProtocols.sql | 7 ++- .../web/nbri_ehr/model/sources/Assignment.js | 57 +++++++++++++++++-- .../NBRIProjectAssignmentFormSection.java | 14 +++++ .../NBRIProtocolAssignmentFormSection.java | 14 +++++ 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/nbri_ehr/resources/queries/ehr/activeProtocols.sql b/nbri_ehr/resources/queries/ehr/activeProtocols.sql index a33358e..790340b 100644 --- a/nbri_ehr/resources/queries/ehr/activeProtocols.sql +++ b/nbri_ehr/resources/queries/ehr/activeProtocols.sql @@ -3,5 +3,10 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -SELECT protocol, title FROM ehr.protocol pr +SELECT + pr.protocol, + pr.title, + pr.description, + CASE WHEN pr.investigatorId.lastName IS NULL THEN pr.displayName ELSE pr.displayName || ' - ' || pr.investigatorId.lastName END AS displayText +FROM ehr.protocol pr WHERE pr.inactiveDate IS NULL OR pr.inactiveDate > now() \ No newline at end of file diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js index 31645f0..2544175 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -18,10 +18,34 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { schemaName: 'ehr', queryName: 'project', keyColumn: 'project', - columns: 'project,name', + columns: 'project,name,account', filterArray: [ LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), ] + }, + editorConfig: { + listeners: { + select: function (combo, recs) { + if (!recs || recs.length !== 1) + return; + + EHR.DataEntryUtils.setSiblingFields(combo, {'project/account': recs[0].get('account')}); + }, + change: function (combo, newVal) { + if (Ext4.isEmpty(newVal)) + EHR.DataEntryUtils.setSiblingFields(combo, {'project/account': null}); + } + } + } + }, + // read-only echo of the selected project's account; the select listener above keeps it current + 'project/account': { + header: 'Project Account', + label: 'Project Account', + editable: false, + columnConfig: { + editable: false, + width: 200 } } }, @@ -37,15 +61,38 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { nullable: false, columnConfig: { fixed: true, - width: 150 + width: 250 }, - // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', - displayColumn: 'protocol', - columns: 'protocol,title' + displayColumn: 'displayText', + columns: 'protocol,title,description,displayText' + }, + editorConfig: { + listeners: { + select: function (combo, recs) { + if (!recs || recs.length !== 1) + return; + + EHR.DataEntryUtils.setSiblingFields(combo, {'protocol/description': recs[0].get('description')}); + }, + change: function (combo, newVal) { + if (Ext4.isEmpty(newVal)) + EHR.DataEntryUtils.setSiblingFields(combo, {'protocol/description': null}); + } + } + } + }, + // read-only echo of the selected protocol's description; the select listener above keeps it current + 'protocol/description': { + header: 'Protocol Description', + label: 'Protocol Description', + editable: false, + columnConfig: { + editable: false, + width: 300 } } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProjectAssignmentFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProjectAssignmentFormSection.java index bc00399..a883de1 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProjectAssignmentFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProjectAssignmentFormSection.java @@ -15,8 +15,13 @@ */ package org.labkey.nbri_ehr.dataentry.section; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.FieldKey; import org.labkey.api.view.template.ClientDependency; +import java.util.ArrayList; +import java.util.List; + public class NBRIProjectAssignmentFormSection extends BaseFormSection { public NBRIProjectAssignmentFormSection(boolean allowAnyId, boolean collapsible, boolean initCollapsed) @@ -34,4 +39,13 @@ public NBRIProjectAssignmentFormSection(boolean allowAnyId, boolean collapsible, setClientStoreClass("NBRI_EHR.data.AssignmentsClientStore"); } } + + @Override + protected List getFieldKeys(TableInfo ti) + { + List keys = new ArrayList<>(super.getFieldKeys(ti)); + keys.add(FieldKey.fromString("project/account")); + + return keys; + } } \ No newline at end of file diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProtocolAssignmentFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProtocolAssignmentFormSection.java index 4180bb2..3532cda 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProtocolAssignmentFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIProtocolAssignmentFormSection.java @@ -15,8 +15,13 @@ */ package org.labkey.nbri_ehr.dataentry.section; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.FieldKey; import org.labkey.api.view.template.ClientDependency; +import java.util.ArrayList; +import java.util.List; + public class NBRIProtocolAssignmentFormSection extends BaseFormSection { public NBRIProtocolAssignmentFormSection(boolean allowAnyId, boolean collapsible, boolean initCollapsed) @@ -34,4 +39,13 @@ public NBRIProtocolAssignmentFormSection(boolean allowAnyId, boolean collapsible setClientStoreClass("NBRI_EHR.data.AssignmentsClientStore"); } } + + @Override + protected List getFieldKeys(TableInfo ti) + { + List keys = new ArrayList<>(super.getFieldKeys(ti)); + keys.add(FieldKey.fromString("protocol/description")); + + return keys; + } } \ No newline at end of file From f894b7eb49694db2a953b4dfaa835cf95c3b5132 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 9 Sep 2026 15:11:32 -0600 Subject: [PATCH 3/6] Test the assignment form's lookup columns and seed the data they need The protocol fixtures now point at ehr.investigators rows rather than user ids, which is what the investigator lookup actually resolves against, and they carry descriptions so the new column has something to show. --- .../tests.nbri_ehr/NBRI_EHRTest.java | 89 ++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index 9861c69..8ed531b 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -139,6 +139,13 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly private static final String ROOMLESS_CAGE = cageLocation("R1", ROOMLESS_CAGE_NAME); private static final String[] ROOMLESS_ANIMALS = {"CAGE0001", "CAGE0002"}; + // protocol.investigatorId looks up ehr.investigators rather than the user table, so a protocol shows an + // investigator only when a row there carries its id. + private static final String INVES_LAST_NAME = "Marsh"; + private static final String DUMMY_INVES_LAST_NAME = "Okafor"; + private static final String PROTOCOL_DESCRIPTION = "Chronic implant study"; + private static final String DUMMY_PROTOCOL_DESCRIPTION = "Placeholder protocol for the arrival and assignment fixtures"; + private final String[] weightFields = {"Id", "date", "enddate", "project", "weight", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"}; private final Object[] weightData1 = {getExpectedAnimalIDCasing("TESTSUBJECT1"), EHRClientAPIHelper.DATE_SUBSTITUTION, null, null, "12", EHRQCState.IN_PROGRESS.label, null, null, "_recordID", 1004}; @@ -177,17 +184,28 @@ protected void populateProtocolRecords() throws Exception _permissionsHelper.addUserToProjGroup(inves1.getEmail(), getProjectName(), INVESTIGATOR.getGroup()); _permissionsHelper.addUserToProjGroup(inves2.getEmail(), getProjectName(), INVESTIGATOR.getGroup()); + InsertRowsCommand invesCmd = new InsertRowsCommand("ehr", "investigators"); + invesCmd.addRow(Map.of("lastName", INVES_LAST_NAME, "firstName", "Robin", "userid", inves1.getUserId())); + invesCmd.addRow(Map.of("lastName", DUMMY_INVES_LAST_NAME, "firstName", "Alex", "userid", inves2.getUserId())); + invesCmd.execute(createDefaultConnection(), getContainerPath()); + + Map investigatorIds = new HashMap<>(); + for (Map row : executeSelectRowCommand("ehr", "investigators", ContainerFilter.Current, "/" + getContainerPath(), List.of()).getRows()) + investigatorIds.put((String) row.get("lastName"), row.get("rowid")); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr", "protocol"); Map rowMap = new HashMap<>(); rowMap.put("protocol", PROTOCOL_ID); - rowMap.put("InvestigatorId", inves1.getUserId()); + rowMap.put("InvestigatorId", investigatorIds.get(INVES_LAST_NAME)); rowMap.put("title", PROTOCOL_ID); + rowMap.put("description", PROTOCOL_DESCRIPTION); insertCmd.addRow(rowMap); rowMap = new HashMap<>(); rowMap.put("protocol", DUMMY_PROTOCOL); - rowMap.put("InvestigatorId", inves2.getUserId()); + rowMap.put("InvestigatorId", investigatorIds.get(DUMMY_INVES_LAST_NAME)); rowMap.put("title", DUMMY_PROTOCOL); + rowMap.put("description", DUMMY_PROTOCOL_DESCRIPTION); insertCmd.addRow(rowMap); insertCmd.execute(createDefaultConnection(), getContainerPath()); @@ -771,6 +789,73 @@ public void testArrivalForm() throws IOException, CommandException arrivalBirthDay, getDatasetDay("demographics", arrivedAnimal, "birth")); } + @Test + public void testAssignmentFormLookupColumns() + { + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Assignment")); + lockForm(); + + Ext4GridRef protocols = _helper.getExt4GridForFormSection("Protocol Assignment"); + _helper.addRecordToGrid(protocols); + protocols.setGridCell(1, "Id", aliveAnimalId); + + log("Verifying the protocol dropdown lists each protocol with its investigator"); + // setGridCell clicks the list item whose text is exactly this, so selecting it is the check on the format + protocols.setGridCell(1, "protocol", DUMMY_PROTOCOL + " - " + DUMMY_INVES_LAST_NAME); + Assert.assertEquals("Protocol dropdown stored something other than the protocol id", + DUMMY_PROTOCOL, protocols.getFieldValue(1, "protocol")); + + log("Verifying the protocol description follows the selected protocol"); + Assert.assertEquals("Protocol description did not follow the selected protocol", + DUMMY_PROTOCOL_DESCRIPTION, protocols.getFieldValue(1, "protocol/description")); + protocols.setGridCell(1, "protocol", PROTOCOL_ID + " - " + INVES_LAST_NAME); + Assert.assertEquals("Protocol description did not follow a changed protocol", + PROTOCOL_DESCRIPTION, protocols.getFieldValue(1, "protocol/description")); + + Assert.assertEquals("Protocol Description is not the last column", + getVisibleColumnCount(protocols), protocols.getIndexOfColumn("protocol/description", true)); + Assert.assertEquals("Wrong header over the protocol description", + "Protocol Description", getColumnHeader(protocols, "protocol/description")); + Assert.assertFalse("Protocol Description should not be editable", + isColumnEditable(protocols, "protocol/description")); + + Ext4GridRef projects = _helper.getExt4GridForFormSection("Project Assignment"); + _helper.addRecordToGrid(projects); + projects.setGridCell(1, "Id", aliveAnimalId); + projects.setGridCell(1, "project", PROJECT_ID); + + log("Verifying the project account follows the selected project"); + Assert.assertEquals("Project account did not follow the selected project", + ACCOUNT_ID_2, projects.getFieldValue(1, "project/account")); + Assert.assertEquals("Project Account is not the last column", + getVisibleColumnCount(projects), projects.getIndexOfColumn("project/account", true)); + Assert.assertEquals("Wrong header over the project account", + "Project Account", getColumnHeader(projects, "project/account")); + Assert.assertFalse("Project Account should not be editable", + isColumnEditable(projects, "project/account")); + + // every check above reads the unsaved row, so discard rather than submit and leave the animal's own + // assignments alone + _helper.discardForm(); + } + + /** The header a data entry grid renders for a column, which only the column config carries. */ + private String getColumnHeader(Ext4GridRef grid, String dataIndex) + { + return (String) grid.getFnEval("for (var i=0;i Date: Wed, 9 Sep 2026 21:03:58 -0600 Subject: [PATCH 4/6] Align project and protocol column metadata with their default views ehr.xml hides name, investigatorId, alwaysavailable and both ibc_approval columns, so un-hiding those takes an explicit false rather than a dropped tag. inves and inves2 stay hidden and drop out of the views. --- .../resources/queries/ehr/project.query.xml | 49 +++++++------------ .../resources/queries/ehr/project/.qview.xml | 2 - .../resources/queries/ehr/protocol.query.xml | 8 +++ .../resources/queries/ehr/protocol/.qview.xml | 1 - 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/nbri_ehr/resources/queries/ehr/project.query.xml b/nbri_ehr/resources/queries/ehr/project.query.xml index 8ce9715..97ce065 100644 --- a/nbri_ehr/resources/queries/ehr/project.query.xml +++ b/nbri_ehr/resources/queries/ehr/project.query.xml @@ -11,43 +11,32 @@ Project + false NBRI Project Project Id - true - - - true - - - true - - - true + + + IACUC Protocol - true ehr protocol protocol - - true - - - true - - - true + + + + + false - true - + false ehr investigators @@ -55,26 +44,22 @@ lastName - - true - - - true - + Investigator true - - Requestor Name + true - + true - - true + + Requestor Name + + diff --git a/nbri_ehr/resources/queries/ehr/project/.qview.xml b/nbri_ehr/resources/queries/ehr/project/.qview.xml index b13e8bf..1a0f2b6 100644 --- a/nbri_ehr/resources/queries/ehr/project/.qview.xml +++ b/nbri_ehr/resources/queries/ehr/project/.qview.xml @@ -7,8 +7,6 @@ - - diff --git a/nbri_ehr/resources/queries/ehr/protocol.query.xml b/nbri_ehr/resources/queries/ehr/protocol.query.xml index edf95db..995a4ea 100644 --- a/nbri_ehr/resources/queries/ehr/protocol.query.xml +++ b/nbri_ehr/resources/queries/ehr/protocol.query.xml @@ -15,6 +15,14 @@ lastName + + + false + + + false + + true diff --git a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml index 74d96bb..9aff599 100644 --- a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml +++ b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml @@ -6,7 +6,6 @@ - From 4a7da57672bb624b462a04af424cde2f9a5fe770 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 9 Sep 2026 21:04:37 -0600 Subject: [PATCH 5/6] Show the investigator in every protocol dropdown The arrival and birth forms share ehr/activeProtocols with the assignment form, and the lookup store is keyed by display column alone, so all three must request the same columns or whichever loads first starves the others. --- .../resources/queries/ehr/activeProtocols.sql | 1 - .../web/nbri_ehr/model/sources/Arrival.js | 7 +-- .../web/nbri_ehr/model/sources/Assignment.js | 2 +- .../web/nbri_ehr/model/sources/Birth.js | 7 +-- .../tests.nbri_ehr/NBRI_EHRTest.java | 52 ++++++++++--------- 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/nbri_ehr/resources/queries/ehr/activeProtocols.sql b/nbri_ehr/resources/queries/ehr/activeProtocols.sql index 790340b..7c1a24a 100644 --- a/nbri_ehr/resources/queries/ehr/activeProtocols.sql +++ b/nbri_ehr/resources/queries/ehr/activeProtocols.sql @@ -5,7 +5,6 @@ */ SELECT pr.protocol, - pr.title, pr.description, CASE WHEN pr.investigatorId.lastName IS NULL THEN pr.displayName ELSE pr.displayName || ' - ' || pr.investigatorId.lastName END AS displayText FROM ehr.protocol pr diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js index af420c2..becc204 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -99,13 +99,14 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { fixed: true, width: 150 }, - // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', - displayColumn: 'protocol', - columns: 'protocol,title' + displayColumn: 'displayText', + // description is unused here, but the lookup store is shared by display column alone, so every + // consumer must request the same columns or whichever loads first starves the others + columns: 'protocol,description,displayText' } }, groupId: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js index 2544175..229b17f 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -68,7 +68,7 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { queryName: 'activeProtocols', keyColumn: 'protocol', displayColumn: 'displayText', - columns: 'protocol,title,description,displayText' + columns: 'protocol,description,displayText' }, editorConfig: { listeners: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index a961152..db26149 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -97,13 +97,14 @@ EHR.model.DataModelManager.registerMetadata('Birth', { columnConfig: { width: 150 }, - // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', - displayColumn: 'protocol', - columns: 'protocol,title' + displayColumn: 'displayText', + // description is unused here, but the lookup store is shared by display column alone, so every + // consumer must request the same columns or whichever loads first starves the others + columns: 'protocol,description,displayText' } }, groupId: { diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index 8ed531b..b05855b 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -187,12 +187,14 @@ protected void populateProtocolRecords() throws Exception InsertRowsCommand invesCmd = new InsertRowsCommand("ehr", "investigators"); invesCmd.addRow(Map.of("lastName", INVES_LAST_NAME, "firstName", "Robin", "userid", inves1.getUserId())); invesCmd.addRow(Map.of("lastName", DUMMY_INVES_LAST_NAME, "firstName", "Alex", "userid", inves2.getUserId())); - invesCmd.execute(createDefaultConnection(), getContainerPath()); + RowsResponse invesResponse = invesCmd.execute(createDefaultConnection(), getContainerPath()); Map investigatorIds = new HashMap<>(); - for (Map row : executeSelectRowCommand("ehr", "investigators", ContainerFilter.Current, "/" + getContainerPath(), List.of()).getRows()) + for (Map row : invesResponse.getRows()) investigatorIds.put((String) row.get("lastName"), row.get("rowid")); + assertEquals("Investigator insert did not return a row id per investigator", 2, investigatorIds.size()); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr", "protocol"); Map rowMap = new HashMap<>(); @@ -723,7 +725,7 @@ public void testArrivalForm() throws IOException, CommandException // the animal's opening project, protocol and group are entered on the arrival row itself; the trigger script // opens the matching assignment record for each one arrivals.setGridCell(1, "project", "640991"); - arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); + arrivals.setGridCell(1, "arrivalProtocol", protocolChoice(DUMMY_PROTOCOL, DUMMY_INVES_LAST_NAME)); arrivals.setGridCell(1, "groupId", animalGroup); log("Verifying the opening project, protocol and group are required"); @@ -733,7 +735,7 @@ public void testArrivalForm() throws IOException, CommandException arrivals.setGridCellJS(1, "arrivalProtocol", null); waitForFormError("The field: Protocol is required"); - arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); + arrivals.setGridCell(1, "arrivalProtocol", protocolChoice(DUMMY_PROTOCOL, DUMMY_INVES_LAST_NAME)); arrivals.setGridCellJS(1, "groupId", null); waitForFormError("The field: Group is required"); @@ -802,23 +804,23 @@ public void testAssignmentFormLookupColumns() log("Verifying the protocol dropdown lists each protocol with its investigator"); // setGridCell clicks the list item whose text is exactly this, so selecting it is the check on the format - protocols.setGridCell(1, "protocol", DUMMY_PROTOCOL + " - " + DUMMY_INVES_LAST_NAME); + protocols.setGridCell(1, "protocol", protocolChoice(DUMMY_PROTOCOL, DUMMY_INVES_LAST_NAME)); Assert.assertEquals("Protocol dropdown stored something other than the protocol id", DUMMY_PROTOCOL, protocols.getFieldValue(1, "protocol")); log("Verifying the protocol description follows the selected protocol"); Assert.assertEquals("Protocol description did not follow the selected protocol", DUMMY_PROTOCOL_DESCRIPTION, protocols.getFieldValue(1, "protocol/description")); - protocols.setGridCell(1, "protocol", PROTOCOL_ID + " - " + INVES_LAST_NAME); + protocols.setGridCell(1, "protocol", protocolChoice(PROTOCOL_ID, INVES_LAST_NAME)); Assert.assertEquals("Protocol description did not follow a changed protocol", PROTOCOL_DESCRIPTION, protocols.getFieldValue(1, "protocol/description")); Assert.assertEquals("Protocol Description is not the last column", - getVisibleColumnCount(protocols), protocols.getIndexOfColumn("protocol/description", true)); + "protocol/description", getLastVisibleColumn(protocols)); Assert.assertEquals("Wrong header over the protocol description", - "Protocol Description", getColumnHeader(protocols, "protocol/description")); - Assert.assertFalse("Protocol Description should not be editable", - isColumnEditable(protocols, "protocol/description")); + "Protocol Description", getColumnProperty(protocols, "protocol/description", "text")); + Assert.assertEquals("Protocol Description should not be editable", + false, getColumnProperty(protocols, "protocol/description", "editable")); Ext4GridRef projects = _helper.getExt4GridForFormSection("Project Assignment"); _helper.addRecordToGrid(projects); @@ -829,31 +831,33 @@ public void testAssignmentFormLookupColumns() Assert.assertEquals("Project account did not follow the selected project", ACCOUNT_ID_2, projects.getFieldValue(1, "project/account")); Assert.assertEquals("Project Account is not the last column", - getVisibleColumnCount(projects), projects.getIndexOfColumn("project/account", true)); + "project/account", getLastVisibleColumn(projects)); Assert.assertEquals("Wrong header over the project account", - "Project Account", getColumnHeader(projects, "project/account")); - Assert.assertFalse("Project Account should not be editable", - isColumnEditable(projects, "project/account")); + "Project Account", getColumnProperty(projects, "project/account", "text")); + Assert.assertEquals("Project Account should not be editable", + false, getColumnProperty(projects, "project/account", "editable")); // every check above reads the unsaved row, so discard rather than submit and leave the animal's own // assignments alone _helper.discardForm(); } - /** The header a data entry grid renders for a column, which only the column config carries. */ - private String getColumnHeader(Ext4GridRef grid, String dataIndex) + /** The text every protocol dropdown displays for a protocol, built by ehr/activeProtocols.sql. */ + private static String protocolChoice(String protocol, String investigatorLastName) { - return (String) grid.getFnEval("for (var i=0;i Date: Sat, 12 Sep 2026 15:45:20 -0600 Subject: [PATCH 6/6] Maintain the assignment echo columns from the client store The combo's select event only fires for a dropdown pick, so rows added through bulk add or carrying an auto-populated project left the protocol description and project account blank. Rearrival was still requesting the activeProtocols columns the query no longer returns, and every protocol dropdown now gets the width its longer display text needs. --- .../nbri_ehr/data/AssignmentsClientStore.js | 96 +++++++++++++++++++ .../web/nbri_ehr/model/sources/Arrival.js | 2 +- .../web/nbri_ehr/model/sources/Assignment.js | 32 +------ .../web/nbri_ehr/model/sources/Birth.js | 2 +- .../web/nbri_ehr/model/sources/Rearrival.js | 9 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 11 ++- 6 files changed, 114 insertions(+), 38 deletions(-) diff --git a/nbri_ehr/resources/web/nbri_ehr/data/AssignmentsClientStore.js b/nbri_ehr/resources/web/nbri_ehr/data/AssignmentsClientStore.js index bdd28b5..be4d68e 100644 --- a/nbri_ehr/resources/web/nbri_ehr/data/AssignmentsClientStore.js +++ b/nbri_ehr/resources/web/nbri_ehr/data/AssignmentsClientStore.js @@ -9,6 +9,102 @@ Ext4.define('NBRI_EHR.data.AssignmentsClientStore', { extend: 'EHR.data.DataEntryClientStore', + // Read-only columns echoing a value from the row's lookup target. Maintained here rather than from the + // combo's select event because nothing guarantees a combo: bulk add builds its rows through createModel(), + // and the parent store populates the project itself when an animal has a single active assignment. + lookupEchoes: [ + {keyField: 'protocol', echoField: 'protocol/description', valueColumn: 'description'}, + {keyField: 'project', echoField: 'project/account', valueColumn: 'account'} + ], + + constructor: function(){ + this.callParent(arguments); + + // One class serves both assignment sections, so keep only the pair this query actually carries + this.activeEchoes = Ext4.Array.filter(this.lookupEchoes, function(echo){ + return !!this.getFields().get(echo.keyField) && !!this.getFields().get(echo.echoField); + }, this); + }, + + onUpdate: function(record, operation, modified){ + this.callParent(arguments); + + this.syncLookupEchoes(record); + }, + + insert: function(index, records){ + // After the parent, which may set the project itself + var ret = this.callParent(arguments); + + Ext4.Array.forEach(Ext4.Array.from(records), function(record){ + this.syncLookupEchoes(record); + }, this); + + return ret; + }, + + syncLookupEchoes: function(record){ + // set() below re-enters this through onUpdate, and a field whose convert() rewrites the value would + // never satisfy the equality check that normally stops it, so stop on the way in instead. + if (this.syncingEchoes) + return; + + this.syncingEchoes = true; + try { + Ext4.Array.forEach(this.activeEchoes, function(echo){ + this.syncLookupEcho(record, echo); + }, this); + } + finally { + this.syncingEchoes = false; + } + }, + + syncLookupEcho: function(record, echo){ + var keyValue = record.get(echo.keyField); + if (Ext4.isEmpty(keyValue)){ + this.setEchoValue(record, echo.echoField, null); + return; + } + + var field = this.getFields().get(echo.keyField); + if (!field || !field.lookup || !field.lookup.keyColumn) + return; + + var lookupStore = Ext4.StoreMgr.get(LABKEY.ext4.Util.getLookupStoreId(field)); + if (!lookupStore) + return; + + // The lookup store autoLoads, so a row can arrive while it is still in flight. getCount() reads 0 both + // then and for a store that loaded nothing, and neither can resolve anything, so wait for the load + // rather than echoing the null an empty store would produce. + if (lookupStore.isLoading() || !lookupStore.getCount()){ + lookupStore.on('load', function(){ + this.syncLookupEcho(record, echo); + }, this, {single: true}); + + return; + } + + // findRecord(field, value, start, anyMatch, caseSensitive, exactMatch) defaults to a prefix match, + // which would resolve one protocol id to another that merely starts with it. + var match = lookupStore.findRecord(field.lookup.keyColumn, keyValue, 0, false, false, true); + + this.setEchoValue(record, echo.echoField, match ? match.get(echo.valueColumn) : null); + }, + + setEchoValue: function(record, fieldName, value){ + // A row loaded from the server already carries the right echo, so this keeps the sync from dirtying it. + // Empty-aware because a string field with no useNull converts a set null straight back to ''. + var current = record.get(fieldName); + if (current === value || (Ext4.isEmpty(current) && Ext4.isEmpty(value))) + return; + + record.suspendEvents(); + record.set(fieldName, value); + record.resumeEvents(); + }, + getExtraContext: function(){ var rows = []; var allRecords = this.getRange(); diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js index becc204..c6e6a94 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -97,7 +97,7 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { nullable: false, columnConfig: { fixed: true, - width: 150 + width: 250 }, lookup: { schemaName: 'ehr', diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js index 229b17f..6ba035b 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -22,23 +22,9 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { filterArray: [ LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), ] - }, - editorConfig: { - listeners: { - select: function (combo, recs) { - if (!recs || recs.length !== 1) - return; - - EHR.DataEntryUtils.setSiblingFields(combo, {'project/account': recs[0].get('account')}); - }, - change: function (combo, newVal) { - if (Ext4.isEmpty(newVal)) - EHR.DataEntryUtils.setSiblingFields(combo, {'project/account': null}); - } - } } }, - // read-only echo of the selected project's account; the select listener above keeps it current + // read-only echo of the selected project's account; NBRI_EHR.data.AssignmentsClientStore keeps it current 'project/account': { header: 'Project Account', label: 'Project Account', @@ -69,23 +55,9 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { keyColumn: 'protocol', displayColumn: 'displayText', columns: 'protocol,description,displayText' - }, - editorConfig: { - listeners: { - select: function (combo, recs) { - if (!recs || recs.length !== 1) - return; - - EHR.DataEntryUtils.setSiblingFields(combo, {'protocol/description': recs[0].get('description')}); - }, - change: function (combo, newVal) { - if (Ext4.isEmpty(newVal)) - EHR.DataEntryUtils.setSiblingFields(combo, {'protocol/description': null}); - } - } } }, - // read-only echo of the selected protocol's description; the select listener above keeps it current + // read-only echo of the selected protocol's description; NBRI_EHR.data.AssignmentsClientStore keeps it current 'protocol/description': { header: 'Protocol Description', label: 'Protocol Description', diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index db26149..fe89d2e 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -95,7 +95,7 @@ EHR.model.DataModelManager.registerMetadata('Birth', { allowBlank: false, nullable: false, columnConfig: { - width: 150 + width: 250 }, lookup: { schemaName: 'ehr', diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js index 3b3a575..d0dde4d 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js @@ -79,15 +79,16 @@ EHR.model.DataModelManager.registerMetadata('Rearrival', { nullable: false, columnConfig: { fixed: true, - width: 150 + width: 250 }, - // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', - displayColumn: 'protocol', - columns: 'protocol,title' + displayColumn: 'displayText', + // description is unused here, but the lookup store is shared by display column alone, so every + // consumer must request the same columns or whichever loads first starves the others + columns: 'protocol,description,displayText' } }, groupId: { diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index 2075dd6..1f5c610 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -85,6 +85,7 @@ import java.util.UUID; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.labkey.test.components.html.Input.Input; @Category({EHR.class}) @@ -195,9 +196,15 @@ protected void populateProtocolRecords() throws Exception Map investigatorIds = new HashMap<>(); for (Map row : invesResponse.getRows()) - investigatorIds.put((String) row.get("lastName"), row.get("rowid")); + { + Object rowId = row.get("rowid"); + // A null id here reaches the protocols below as a null investigator, which only surfaces much later + // as a protocol dropdown missing the investigator half of its text. + assertNotNull("Investigator insert did not return a row id for " + row.get("lastName"), rowId); + investigatorIds.put((String) row.get("lastName"), rowId); + } - assertEquals("Investigator insert did not return a row id per investigator", 2, investigatorIds.size()); + assertEquals("Investigator insert did not return a row per investigator", 2, investigatorIds.size()); InsertRowsCommand insertCmd = new InsertRowsCommand("ehr", "protocol");