Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1060e0b
Add new source derivation action types and add inputEntityType to wor…
labkey-susanh Jul 28, 2026
bc0ec96
Update data iterators to add sources to jobs based on actions
labkey-susanh Jul 29, 2026
2a9ccd0
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Jul 30, 2026
9640846
Add inputEntityType to audit log map
labkey-susanh Jul 30, 2026
4ecce28
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Jul 30, 2026
b452832
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Aug 3, 2026
4a40f09
No need to check for adding samples when deriving data class objects
labkey-susanh Aug 3, 2026
af106a6
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Aug 3, 2026
e499159
Add validation for actions not matching task inputs
labkey-susanh Aug 4, 2026
bb85a2d
Various bits of cleanup
labkey-susanh Aug 4, 2026
19fd2a8
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Aug 4, 2026
a239d4a
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Aug 5, 2026
3beb913
Check insert option is not an update before adding workflow data iter…
labkey-susanh Aug 5, 2026
4068cb7
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
XingY Aug 10, 2026
65e61fb
Fix update sample status action audits
XingY Aug 10, 2026
26f99ea
Merge remote-tracking branch 'origin/develop' into fb_jobSourceActions
labkey-susanh Aug 11, 2026
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
74 changes: 71 additions & 3 deletions api/src/org/labkey/api/workflow/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ else if (_type == WorkflowService.ActionType.AliquotSamples)

return messages;
}
else if (_type == WorkflowService.ActionType.DeriveSamples || _type == WorkflowService.ActionType.PoolSamples)
else if (_type == WorkflowService.ActionType.DeriveSamples
|| _type == WorkflowService.ActionType.PoolSamples
|| _type == WorkflowService.ActionType.DeriveSamplesFromSources)
{
String emptyMessage = prefix + "data about sample types and sample counts per parent is required for action of type " + _type + ".";

Expand Down Expand Up @@ -358,9 +360,14 @@ else if (countObj instanceof Integer count)

if (hasAnySampleStatusKey())
{
String statusMessage = validateStatus(container, prefix, true);
if (_type == WorkflowService.ActionType.DeriveSamplesFromSources)
messages.add(prefix + "data about updating parent status not allowed for action of type " + _type + ".");
else
{
String statusMessage = validateStatus(container, prefix, true);

if (statusMessage != null) messages.add(statusMessage);
if (statusMessage != null) messages.add(statusMessage);
}
}

return messages;
Expand All @@ -377,6 +384,67 @@ else if (_type == WorkflowService.ActionType.RemoveFromStorage || _type == Workf

return Collections.emptyList();
}
else if (_type == WorkflowService.ActionType.DeriveSources)
{
String emptyMessage = prefix + "data about source types and source counts per parent is required for action of type " + _type + ".";

if (_inputParameters == null) return List.of(emptyMessage);

// We can't just check _inputParameters size because it may include sample status keys, so we extract the
// source type IDs and validate against those.
List<String> sourceTypeIds = new ArrayList<>();
_inputParameters.keys().forEachRemaining(id -> {
if (isSampleStatusKey(id)) return;
sourceTypeIds.add(id);
});

if (sourceTypeIds.isEmpty())
return List.of(emptyMessage);

// don't allow more than one target source type
if (sourceTypeIds.size() > 1)
return List.of(prefix + "only one source type can be specified for action of type " + _type + ".");

List<String> messages = new ArrayList<>();
String sourceTypeId = sourceTypeIds.get(0);
boolean invalidId;
try
{
invalidId = ExperimentService.get().getDataClass(container, Long.parseLong(sourceTypeId), true) == null;
}
catch (NumberFormatException e)
{
invalidId = true;
}
if (invalidId)
messages.add(prefix + "invalid source type ID " + sourceTypeId + ".");

Object countObj = _inputParameters.get(sourceTypeId);
boolean invalidCount;
if (countObj instanceof String countStr)
{
try
{
invalidCount = Integer.parseInt(countStr) < 0;
}
catch (NumberFormatException e)
{
invalidCount = true;
}
}
else if (countObj instanceof Integer count)
invalidCount = count < 0;
else
invalidCount = true;

if (invalidCount)
messages.add(prefix + "invalid source count value " + countObj + ".");

if (hasAnySampleStatusKey())
messages.add(prefix + "data about updating parent status not allowed for action of type " + _type + ".");

return messages;
}
else
{
if (_inputParameters != null && !_inputParameters.isEmpty())
Expand Down
13 changes: 13 additions & 0 deletions api/src/org/labkey/api/workflow/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public abstract class Task extends CreatedModified implements Comparable<Task>

protected String _description;
protected String _entityFilter;
protected WorkEntity.EntityType _inputEntityType;
protected Integer _status;
protected Date _startDate;
protected Date _endDate;
Expand Down Expand Up @@ -93,6 +94,16 @@ public void setEntityFilter(String entityFilter)
_entityFilter = entityFilter;
}

public WorkEntity.EntityType getInputEntityType()
{
return _inputEntityType;
}

public void setInputEntityType(WorkEntity.EntityType inputEntityType)
{
_inputEntityType = inputEntityType;
}

@JsonProperty("assignee")
public JSONObject getAssigneeJSON()
{
Expand Down Expand Up @@ -278,6 +289,8 @@ public Map<String, Object> toAuditDetailMap()
map.put("dueDate", getDueDate());
if (getEntityId() != null)
map.put("entityId", getEntityId().toString());
if (getInputEntityType() != null)
map.put("inputEntityType", getInputEntityType());
map.put("ordinal", getOrdinal());
int actionIndex = 1;
for (Action action : getActions())
Expand Down
34 changes: 23 additions & 11 deletions api/src/org/labkey/api/workflow/WorkflowService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,28 @@ enum WorkflowConfigs

enum ActionType
{
AssayImport("assay types", "Imported assay data"),
DeriveSamples("derivation sample type parameters", "Derived samples"),
AliquotSamples("aliquot sample type parameters", "Aliquot samples"),
PoolSamples("pooling sample type parameters", "Pooled samples"),
AddToStorage("input parameters", "Added samples to storage"),
MoveInStorage("input parameters", "Moved samples in storage"),
CheckOut("input parameters", "Checked out samples"),
CheckIn("input parameters", "Checked in samples"),
RemoveFromStorage("sample status value", "Removed samples from storage"),
UpdateSampleStatus("sample status value", "Updated sample status");
AssayImport("assay types", "Imported assay data", WorkEntity.EntityType.Sample),
DeriveSamples("derivation sample type parameters", "Derived samples", WorkEntity.EntityType.Sample),
AliquotSamples("aliquot sample type parameters", "Aliquot samples", WorkEntity.EntityType.Sample),
PoolSamples("pooling sample type parameters", "Pooled samples", WorkEntity.EntityType.Sample),
AddToStorage("input parameters", "Added samples to storage", WorkEntity.EntityType.Sample),
MoveInStorage("input parameters", "Moved samples in storage", WorkEntity.EntityType.Sample),
CheckOut("input parameters", "Checked out samples", WorkEntity.EntityType.Sample),
CheckIn("input parameters", "Checked in samples", WorkEntity.EntityType.Sample),
RemoveFromStorage("sample status value", "Removed samples from storage", WorkEntity.EntityType.Sample),
UpdateSampleStatus("sample status value", "Updated sample status", WorkEntity.EntityType.Sample),
DeriveSamplesFromSources("derivation source type parameters", "Derived samples from sources", WorkEntity.EntityType.Source),
DeriveSources("derivation source type parameters", "Derived sources", WorkEntity.EntityType.Source);

private final String _inputDescription;
private final String _auditMessage;
private final WorkEntity.EntityType _inputEntityType;

ActionType(String inputDescription, String auditMessage)
ActionType(String inputDescription, String auditMessage, WorkEntity.EntityType inputEntityType)
{
_inputDescription = inputDescription;
_auditMessage = auditMessage;
_inputEntityType = inputEntityType;
}

public String getInputDescription()
Expand All @@ -65,6 +69,11 @@ public String getAuditMessage()
{
return _auditMessage;
}

public WorkEntity.EntityType getInputEntityType()
{
return _inputEntityType;
}
}

static void setInstance(WorkflowService impl)
Expand All @@ -84,9 +93,12 @@ static WorkflowService get()
void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long actionId, @Nullable String userAuditComment);
void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long taskId, @NotNull ActionType actionType);
boolean actionWillAddSamples(Long actionId);
boolean actionWillAddSources(Long actionId);

DataIteratorBuilder getSampleCreationDataIteratorBuilder(DataIteratorBuilder data, Container container, User user);

DataIteratorBuilder getSourceCreationDataIteratorBuilder(DataIteratorBuilder data, Container container, User user);

DataIteratorBuilder getActionAuditDataIteratorBuilder(DataIteratorBuilder data, Container container, User user);

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.UnauthorizedException;
import org.labkey.api.view.ViewContext;
import org.labkey.api.workflow.WorkflowService;
import org.labkey.data.xml.TableType;
import org.labkey.experiment.ExpDataIterators;
import org.labkey.experiment.ExpDataIterators.AliasDataIteratorBuilder;
Expand Down Expand Up @@ -942,8 +943,24 @@ public DataIteratorBuilder persistRows(DataIteratorBuilder data, DataIteratorCon

}, DbScope.CommitTaskOption.POSTCOMMIT));


DataIteratorBuilder builder = LoggingDataIterator.wrap(step0);
return LoggingDataIterator.wrap(new AliasDataIteratorBuilder(builder, getUserSchema().getContainer(), getUserSchema().getUser(), ExperimentService.get().getTinfoDataAliasMap(), _dataClass, false));
UserSchema userSchema = getUserSchema();
builder = LoggingDataIterator.wrap(new AliasDataIteratorBuilder(builder, userSchema.getContainer(), userSchema.getUser(), ExperimentService.get().getTinfoDataAliasMap(), _dataClass, false));
WorkflowService workService = WorkflowService.get();
if (workService != null && !context.getInsertOption().allowUpdate)
{
if (context.getConfigParameter(WorkflowService.WorkflowConfigs.ActionId) != null)
{
Long actionId = (Long) context.getConfigParameter(WorkflowService.WorkflowConfigs.ActionId);

if (workService.actionWillAddSources(actionId))
builder = workService.getSourceCreationDataIteratorBuilder(builder, userSchema.getContainer(), userSchema.getUser());

builder = workService.getActionAuditDataIteratorBuilder(builder, userSchema.getContainer(), userSchema.getUser());
}
}
return builder;
}
catch (IOException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI
{
Long actionId = (Long) context.getConfigParameter(WorkflowService.WorkflowConfigs.ActionId);

if (WorkflowService.get().actionWillAddSamples(actionId))
if (workService.actionWillAddSamples(actionId) && !context.getInsertOption().allowUpdate)
{
dib = workService.getSampleCreationDataIteratorBuilder(dib, userSchema.getContainer(), userSchema.getUser());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4426,17 +4426,6 @@ protected int importData(
tInfo = ExperimentService.get().createMaterialTable(new SamplesSchema(getUser(), getContainer()), ContainerFilter.current(this), null);
updateService = tInfo.getUpdateService();
}
if (WorkflowService.get() != null)
{
try
{
WorkflowService.get().populateConfigParams(getViewContext().getRequest(), _context.getConfigParameters());
}
catch (ValidationException e)
{
errors.addRowError(e);
}
}

int count = importData(dl, tInfo, updateService, _context, auditEvent, getUser(), getContainer());

Expand Down Expand Up @@ -4551,6 +4540,21 @@ protected Set<String> getLineageImportAliases() throws IOException
protected void initContext(DataLoader dl, BatchValidationException errors, @Nullable AuditBehaviorType auditBehaviorType, @Nullable String auditUserComment)
{
_context = createDataIteratorContext(_insertOption, getOptionParamsMap(), getLookupResolutionType(), auditBehaviorType, auditUserComment, errors, null, getContainer());

// Both samples and data classes can be created via a workflow job action, so the action and job ids need to
// be available to the update service for either type. The background import path populates these separately
// via AbstractQueryImportAction.getImportContextBuilder().
if (WorkflowService.get() != null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about biologics-import.api? Certain registry sources use that instead of experiment-importData

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently not allowing you to create sources for the built-in registry types from the job actions, so this doesn't apply (yet).

{
try
{
WorkflowService.get().populateConfigParams(getViewContext().getRequest(), _context.getConfigParameters());
}
catch (ValidationException e)
{
errors.addRowError(e);
}
}
}

@Override
Expand Down