Skip to content

Commit 7f59542

Browse files
labkey-martypclaude
andcommitted
Add 'Delete Empty Tasks' admin action to ehr.tasks grid
Adds a More Actions menu item, gated on EHRDataAdminPermission, that deletes the selected ehr.tasks rows after verifying none of them have referencing rows in study.studydata. Tasks with associated study dataset data are rejected with an error; data in other schemas is not checked. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 25f55cf commit 7f59542

4 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2026 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.api.ehr.buttons;
17+
18+
import org.labkey.api.data.TableInfo;
19+
import org.labkey.api.ehr.security.EHRDataAdminPermission;
20+
import org.labkey.api.ldk.table.SimpleButtonConfigFactory;
21+
import org.labkey.api.module.Module;
22+
23+
public class DiscardEmptyTasksButton extends SimpleButtonConfigFactory
24+
{
25+
public DiscardEmptyTasksButton(Module owner)
26+
{
27+
super(owner, "Delete Empty Tasks", "EHR.DatasetButtons.discardEmptyTasks(dataRegionName);");
28+
}
29+
30+
@Override
31+
public boolean isAvailable(TableInfo ti)
32+
{
33+
return super.isAvailable(ti) && ti.getUserSchema().getContainer().hasPermission(ti.getUserSchema().getUser(), EHRDataAdminPermission.class);
34+
}
35+
}

ehr/resources/web/ehr/studyButtons.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,34 @@ EHR.DatasetButtons = new function () {
266266
}, this);
267267
},
268268

269+
discardEmptyTasks: function (dataRegionName) {
270+
var dataRegion = LABKEY.DataRegions[dataRegionName];
271+
var checked = dataRegion.getChecked();
272+
if (!checked || !checked.length) {
273+
Ext4.Msg.alert('Error', 'No records selected');
274+
return;
275+
}
276+
277+
Ext4.Msg.confirm('Delete Empty Tasks',
278+
'Permanently delete the selected task(s)? This will fail if any selected task has related data in a study dataset. Note: Task related data in other schemas is not checked.',
279+
function (val) {
280+
if (val !== 'yes') return;
281+
Ext4.Msg.wait('Deleting...');
282+
LABKEY.Ajax.request({
283+
url: LABKEY.ActionURL.buildURL('ehr', 'discardEmptyTasks', null, {taskIds: checked}),
284+
method: 'POST',
285+
success: function (response) {
286+
Ext4.Msg.hide();
287+
var json = LABKEY.Utils.decode(response.responseText) || {};
288+
Ext4.Msg.alert('Success', 'Deleted ' + (json.deletedCount || 0) + ' task(s).');
289+
dataRegion.refresh();
290+
},
291+
failure: LDK.Utils.getErrorCallback(),
292+
scope: this
293+
});
294+
}, this);
295+
},
296+
269297
limitKinshipSelection: function (dataRegionName) {
270298
var dataRegion = LABKEY.DataRegions[dataRegionName];
271299

ehr/src/org/labkey/ehr/EHRController.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.labkey.api.ehr.dataentry.DataEntryForm;
5050
import org.labkey.api.ehr.demographics.AnimalRecord;
5151
import org.labkey.api.ehr.history.HistoryRow;
52+
import org.labkey.api.ehr.security.EHRDataAdminPermission;
5253
import org.labkey.api.ehr.security.EHRDataEntryPermission;
5354
import org.labkey.api.exp.api.ExperimentService;
5455
import org.labkey.api.gwt.client.AuditBehaviorType;
@@ -60,6 +61,7 @@
6061
import org.labkey.api.query.BatchValidationException;
6162
import org.labkey.api.query.DetailsURL;
6263
import org.labkey.api.query.FieldKey;
64+
import org.labkey.api.query.InvalidKeyException;
6365
import org.labkey.api.query.QueryAction;
6466
import org.labkey.api.query.QueryForm;
6567
import org.labkey.api.query.QueryParseException;
@@ -331,6 +333,137 @@ public ApiResponse execute(DiscardFormForm form, BindException errors)
331333
}
332334
}
333335

336+
public static class DiscardEmptyTasksForm
337+
{
338+
private String[] taskIds;
339+
340+
public String[] getTaskIds()
341+
{
342+
return taskIds;
343+
}
344+
345+
public void setTaskIds(String[] taskIds)
346+
{
347+
this.taskIds = taskIds;
348+
}
349+
}
350+
351+
@RequiresPermission(EHRDataAdminPermission.class)
352+
public static class DiscardEmptyTasksAction extends MutatingApiAction<DiscardEmptyTasksForm>
353+
{
354+
private List<String> _selectedTaskIds;
355+
private TableInfo _tasksTable;
356+
357+
@Override
358+
public void validateForm(DiscardEmptyTasksForm form, Errors errors)
359+
{
360+
super.validateForm(form, errors);
361+
362+
if (form.getTaskIds() == null || form.getTaskIds().length == 0)
363+
{
364+
errors.reject(ERROR_MSG, "No tasks selected.");
365+
return;
366+
}
367+
_selectedTaskIds = Arrays.asList(form.getTaskIds());
368+
369+
UserSchema ehrSchema = QueryService.get().getUserSchema(getUser(), getContainer(), EHRSchema.EHR_SCHEMANAME);
370+
if (ehrSchema == null)
371+
{
372+
errors.reject(ERROR_MSG, "EHR schema is not available in this container.");
373+
return;
374+
}
375+
376+
_tasksTable = ehrSchema.getTable(EHRSchema.TABLE_TASKS);
377+
if (_tasksTable == null)
378+
{
379+
errors.reject(ERROR_MSG, "ehr.tasks table is not available in this container.");
380+
return;
381+
}
382+
383+
Set<String> nonEmpty = new LinkedHashSet<>();
384+
UserSchema studySchema = QueryService.get().getUserSchema(getUser(), getContainer(), "study");
385+
if (studySchema != null)
386+
{
387+
TableInfo studyData = studySchema.getTable("StudyData");
388+
if (studyData != null && studyData.getColumn("taskid") != null)
389+
{
390+
SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), _selectedTaskIds, CompareType.IN);
391+
String[] ids = new TableSelector(studyData, Collections.singleton("taskid"), filter, null).getArray(String.class);
392+
if (ids != null)
393+
{
394+
for (String id : ids)
395+
{
396+
if (id != null)
397+
nonEmpty.add(id);
398+
}
399+
}
400+
}
401+
}
402+
403+
if (!nonEmpty.isEmpty())
404+
{
405+
errors.reject(ERROR_MSG, "Cannot delete: " + nonEmpty.size() + " of the selected task(s) have associated dataset records. Task ID(s): " + String.join(", ", nonEmpty));
406+
}
407+
}
408+
409+
@Override
410+
public ApiResponse execute(DiscardEmptyTasksForm form, BindException errors)
411+
{
412+
int deleted;
413+
try (DbScope.Transaction transaction = ExperimentService.get().ensureTransaction())
414+
{
415+
deleted = deleteRowsByTaskIds(_tasksTable, _selectedTaskIds);
416+
transaction.commit();
417+
}
418+
catch (SQLException e)
419+
{
420+
throw new RuntimeSQLException(e);
421+
}
422+
423+
Map<String, Object> resultProperties = new HashMap<>();
424+
resultProperties.put("success", true);
425+
resultProperties.put("deletedCount", deleted);
426+
return new ApiSimpleResponse(resultProperties);
427+
}
428+
429+
private int deleteRowsByTaskIds(TableInfo ti, List<String> taskIds) throws SQLException
430+
{
431+
QueryUpdateService qus = ti.getUpdateService();
432+
if (qus == null)
433+
return 0;
434+
435+
int total = 0;
436+
final int chunkSize = 1000;
437+
for (int start = 0; start < taskIds.size(); start += chunkSize)
438+
{
439+
List<String> chunk = taskIds.subList(start, Math.min(start + chunkSize, taskIds.size()));
440+
SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), chunk, CompareType.IN);
441+
String[] pkColumns = ti.getPkColumnNames().toArray(new String[0]);
442+
Set<String> selectCols = new LinkedHashSet<>(Arrays.asList(pkColumns));
443+
selectCols.add("taskid");
444+
445+
Map<String, Object>[] rows = new TableSelector(ti, selectCols, filter, null).getMapArray();
446+
if (rows == null || rows.length == 0)
447+
continue;
448+
449+
List<Map<String, Object>> keys = new ArrayList<>(rows.length);
450+
for (Map<String, Object> row : rows)
451+
keys.add(new HashMap<>(row));
452+
453+
try
454+
{
455+
qus.deleteRows(getUser(), getContainer(), keys, null, new HashMap<>());
456+
}
457+
catch (InvalidKeyException | QueryUpdateServiceException | BatchValidationException e)
458+
{
459+
throw new RuntimeException(e);
460+
}
461+
total += rows.length;
462+
}
463+
return total;
464+
}
465+
}
466+
334467
public static class EHRQueryForm extends QueryForm
335468
{
336469
private boolean _showImport = false;

ehr/src/org/labkey/ehr/EHRModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.labkey.api.data.UpgradeCode;
2626
import org.labkey.api.ehr.EHRDemographicsService;
2727
import org.labkey.api.ehr.EHRService;
28+
import org.labkey.api.ehr.buttons.DiscardEmptyTasksButton;
2829
import org.labkey.api.ehr.buttons.EHRShowEditUIButton;
2930
import org.labkey.api.ehr.buttons.MarkCompletedButton;
3031
import org.labkey.api.ehr.demographics.ActiveAssignmentsDemographicsProvider;
@@ -249,6 +250,7 @@ public void moduleStartupComplete(ServletContext servletContext)
249250
EHRService.get().registerMoreActionsButton(new CompareWeightsButton(this), "study", "weight");
250251
EHRService.get().registerMoreActionsButton(new TaskAssignButton(this), "ehr", "my_tasks");
251252
EHRService.get().registerMoreActionsButton(new TaskAssignButton(this), "ehr", "tasks");
253+
EHRService.get().registerMoreActionsButton(new DiscardEmptyTasksButton(this), "ehr", "tasks");
252254
EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "treatment_order", "Set End Date"), "study", "treatment_order");
253255
EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "problem", "End Problem(s)", true), "study", "problem");
254256
EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "feeding"), "study", "feeding");

0 commit comments

Comments
 (0)