Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion ehr/resources/web/ehr/DataEntryUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ehr/resources/web/ehr/ehr_ext4_dataEntry.lib.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<script path="ehr/plugin/ResizableTextArea.js"/>
<script path="ehr/plugin/RowEditor.js"/>
<script path="ehr/plugin/CollapsibleDataEntryPanel.js"/>
<script path="ehr/plugin/ClinicalObservationsBulkEdit.js"/>

<!--fields-->
<script path="ehr/form/field/AnimalField.js"/>
Expand Down
10 changes: 10 additions & 0 deletions ehr/resources/web/ehr/grid/ClinicalObservationGridPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},

Expand Down
60 changes: 39 additions & 21 deletions ehr/resources/web/ehr/panel/BulkEditPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},

Expand Down Expand Up @@ -117,34 +131,38 @@ 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);

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();
Expand Down
121 changes: 121 additions & 0 deletions ehr/resources/web/ehr/plugin/ClinicalObservationsBulkEdit.js
Original file line number Diff line number Diff line change
@@ -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;
}
});