diff --git a/ehr/resources/web/ehr/DataEntryUtils.js b/ehr/resources/web/ehr/DataEntryUtils.js
index e218991d3..3c3fa72cb 100644
--- a/ehr/resources/web/ehr/DataEntryUtils.js
+++ b/ehr/resources/web/ehr/DataEntryUtils.js
@@ -801,7 +801,17 @@ EHR.DataEntryUtils = new function(){
schemaName: 'ehr',
queryName: 'observation_types',
columns: 'value,editorconfig',
- autoLoad: true
+ autoLoad: true,
+ listeners: {
+ // unlike EHR.data.DataEntryClientStore, LABKEY.ext4.data.Store has no hasLoaded();
+ // consumers need to distinguish a pending initial load from one that returned no rows
+ load: {
+ single: true,
+ fn: function(store){
+ store.hasLoadedOnce = true;
+ }
+ }
+ }
});
return EHR._observationTypesStore;
diff --git a/ehr/resources/web/ehr/ehr_ext4_dataEntry.lib.xml b/ehr/resources/web/ehr/ehr_ext4_dataEntry.lib.xml
index 60557b71e..edaa925ac 100644
--- a/ehr/resources/web/ehr/ehr_ext4_dataEntry.lib.xml
+++ b/ehr/resources/web/ehr/ehr_ext4_dataEntry.lib.xml
@@ -21,6 +21,7 @@
+
diff --git a/ehr/resources/web/ehr/grid/ClinicalObservationGridPanel.js b/ehr/resources/web/ehr/grid/ClinicalObservationGridPanel.js
index 5ff31c467..ee28b6d05 100644
--- a/ehr/resources/web/ehr/grid/ClinicalObservationGridPanel.js
+++ b/ehr/resources/web/ehr/grid/ClinicalObservationGridPanel.js
@@ -13,6 +13,16 @@ Ext4.define('EHR.grid.ClinicalObservationGridPanel', {
initComponent: function(){
this.observationTypesStore = EHR.DataEntryUtils.getObservationTypesStore();
+ // Make the bulk edit panel for this grid offer the same category-dependent Observation/Score
+ // editor as the grid's cell editor. formConfig is threaded unchanged to EHR.panel.BulkEditPanel,
+ // which instantiates any plugins listed here.
+ if (this.formConfig){
+ this.formConfig.bulkEditPlugins = Ext4.Array.from(this.formConfig.bulkEditPlugins || []);
+ if (!Ext4.Array.contains(this.formConfig.bulkEditPlugins, 'clinicalobservationsbulkedit')){
+ this.formConfig.bulkEditPlugins.push('clinicalobservationsbulkedit');
+ }
+ }
+
this.callParent(arguments);
},
diff --git a/ehr/resources/web/ehr/panel/BulkEditPanel.js b/ehr/resources/web/ehr/panel/BulkEditPanel.js
index 6ac52fcac..ceb71473f 100644
--- a/ehr/resources/web/ehr/panel/BulkEditPanel.js
+++ b/ehr/resources/web/ehr/panel/BulkEditPanel.js
@@ -46,6 +46,20 @@ Ext4.define('EHR.panel.BulkEditPanel', {
this.callParent(arguments);
+ // Allow a form section to contribute panel plugins to its bulk edit panel via formConfig.bulkEditPlugins.
+ // For example, the clinical observations grid uses this to make the Observation/Score editor depend on
+ // the selected Category (see EHR.plugin.ClinicalObservationsBulkEdit), keeping this panel generic. This
+ // runs after callParent because EHR.form.Panel resets this.plugins for collapsible forms. By this point
+ // the panel's own plugins have already been constructed, so we construct ours explicitly and append them;
+ // the component constructor then calls init() on every entry in this.plugins.
+ var extraPlugins = this.formConfig && this.formConfig.bulkEditPlugins;
+ if (extraPlugins){
+ this.plugins = this.plugins || [];
+ Ext4.Array.forEach(Ext4.Array.from(extraPlugins), function(p){
+ this.plugins.push(this.constructPlugin(p));
+ }, this);
+ }
+
this.addEvents('bulkeditcomplete');
},
@@ -117,27 +131,7 @@ Ext4.define('EHR.panel.BulkEditPanel', {
item = Ext4.widget(item);
- item.on('render', function(field){
- if (field.labelEl){
- Ext4.QuickTips.register({
- target: field.labelEl,
- text: 'Click to toggle'
- });
-
- field.labelEl.on('click', function(){
- if (field.originalDisabled){
- Ext4.Msg.alert('Error', 'This field cannot be enabled');
- return;
- }
-
- field.setDisabled(!field.isDisabled());
- Ext4.defer(field.focus, 100, field);
- }, this);
- }
- else {
- console.log(field);
- }
- }, this);
+ this.addLabelToggle(item);
newItems.push(item);
}, this);
@@ -145,6 +139,30 @@ Ext4.define('EHR.panel.BulkEditPanel', {
return newItems;
},
+ addLabelToggle: function(field){
+ field.on('render', function(field){
+ if (field.labelEl){
+ Ext4.QuickTips.register({
+ target: field.labelEl,
+ text: 'Click to toggle'
+ });
+
+ field.labelEl.on('click', function(){
+ if (field.originalDisabled){
+ Ext4.Msg.alert('Error', 'This field cannot be enabled');
+ return;
+ }
+
+ field.setDisabled(!field.isDisabled());
+ Ext4.defer(field.focus, 100, field);
+ }, this);
+ }
+ else {
+ console.log(field);
+ }
+ }, this);
+ },
+
onSubmit: function(){
if (!this.suppressConfirmMsg){
var values = this.getForm().getFieldValues();
diff --git a/ehr/resources/web/ehr/plugin/ClinicalObservationsBulkEdit.js b/ehr/resources/web/ehr/plugin/ClinicalObservationsBulkEdit.js
new file mode 100644
index 000000000..1f35a824f
--- /dev/null
+++ b/ehr/resources/web/ehr/plugin/ClinicalObservationsBulkEdit.js
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+/**
+ * A plugin for EHR.panel.BulkEditPanel that makes the Observation/Score field's editor depend on the
+ * selected Category, mirroring EHR.grid.plugin.ClinicalObservationsCellEditing so that the data type
+ * and options offered in the bulk edit dialog match the clinical observations grid.
+ *
+ * It is attached to the bulk edit panel via the clinical observations grid's formConfig.bulkEditPlugins
+ * (see EHR.grid.ClinicalObservationGridPanel), so the generic bulk edit panel stays free of any
+ * observation-specific logic.
+ */
+Ext4.define('EHR.plugin.ClinicalObservationsBulkEdit', {
+ extend: 'Ext.AbstractPlugin',
+ alias: 'plugin.clinicalobservationsbulkedit',
+
+ init: function(panel){
+ this.panel = panel;
+ this.observationTypesStore = EHR.DataEntryUtils.getObservationTypesStore();
+ panel.on('afterrender', this.setupObservationDependency, this, {single: true});
+
+ this.callParent(arguments);
+ },
+
+ setupObservationDependency: function(){
+ var panel = this.panel;
+ var categoryField = panel.down('[name=category]');
+ var observationField = panel.down('[name=observation]');
+ if (!categoryField || !observationField){
+ return;
+ }
+
+ // Capture the base config of the original Observation/Score field so it can be rebuilt with a
+ // category-specific editor (see reconfigureObservationField).
+ this.observationFieldBaseCfg = {
+ name: observationField.name,
+ fieldLabel: observationField.fieldLabel,
+ labelWidth: observationField.labelWidth,
+ width: observationField.width,
+ // honor metadata that marks the field as never-enableable (see BulkEditPanel.getFieldConfigs)
+ originalDisabled: observationField.originalDisabled
+ };
+
+ categoryField.on('change', function(field, newValue){
+ this.reconfigureObservationField(newValue);
+ }, this);
+
+ // Initialize based on any pre-populated category value (e.g. when all selected records share a category)
+ var initialCategory = categoryField.getValue();
+ if (initialCategory){
+ this.reconfigureObservationField(initialCategory);
+ }
+ },
+
+ reconfigureObservationField: function(category){
+ var store = this.observationTypesStore;
+ // hasLoadedOnce (set in EHR.DataEntryUtils.getObservationTypesStore) distinguishes a pending
+ // initial load from one that already returned no rows; for the latter we fall through to the
+ // textfield default rather than waiting on a load event that will never fire
+ if (!store.hasLoadedOnce){
+ store.on('load', function(){
+ // the dialog may have been closed before the store finished loading
+ if (this.panel && !this.panel.isDestroyed){
+ this.reconfigureObservationField(category);
+ }
+ }, this, {single: true});
+ return;
+ }
+
+ var panel = this.panel;
+ var observationField = panel.down('[name=observation]');
+ if (!observationField){
+ return;
+ }
+
+ //note: we proceed even if the category cannot be found, to support records saved under a no-longer-supported category
+ var rec = category ? store.findRecord('value', category) : null;
+ var rawEditorConfig = (rec && rec.get('editorconfig')) || null;
+
+ // the category combo fires change on every keystroke, so skip the rebuild when the resolved
+ // editor is unchanged; this avoids churn and preserves any value the user already entered
+ if (this.appliedEditorConfig !== undefined && this.appliedEditorConfig === rawEditorConfig){
+ return;
+ }
+
+ var container = observationField.ownerCt;
+ var index = container.items.indexOf(observationField);
+ //preserve the user's enable/disable toggle state across category changes
+ var wasDisabled = observationField.isDisabled();
+
+ var editorConfig = rawEditorConfig ? Ext4.decode(rawEditorConfig) : null;
+ editorConfig = editorConfig || {
+ xtype: 'textfield'
+ };
+
+ var cfg = Ext4.apply({}, editorConfig);
+ Ext4.apply(cfg, this.observationFieldBaseCfg);
+ delete cfg.value;
+ delete cfg.defaultValue;
+ cfg.allowBlank = true;
+ cfg.disabled = wasDisabled;
+
+ cfg = EHR.DataEntryUtils.ensureLookupPlugin(cfg, false);
+
+ container.remove(observationField, true);
+ var newField = Ext4.widget(cfg);
+ panel.addLabelToggle(newField);
+ container.insert(index, newField);
+
+ // the databind plugin only registers listeners on the fields present at init, so register the
+ // replacement explicitly to keep record syncing and validation consistent with the original field
+ var databind = panel.getPlugin('ehr-databind');
+ if (databind){
+ databind.addFieldListener(newField);
+ }
+
+ this.appliedEditorConfig = rawEditorConfig;
+ }
+});