From 87fa742f628931eb3fd6717596d813d3f367b519 Mon Sep 17 00:00:00 2001 From: Susan Hert Date: Fri, 17 Apr 2026 09:24:05 -0700 Subject: [PATCH 1/5] Issue 1062: Fix export of job references in notebooks (#7587) --- api/src/org/labkey/api/workflow/Action.java | 130 +++++++ api/src/org/labkey/api/workflow/Job.java | 365 ++++++++++++++++++ api/src/org/labkey/api/workflow/Task.java | 274 +++++++++++++ .../org/labkey/api/workflow/WorkEntity.java | 199 ++++++++++ .../labkey/api/workflow/WorkflowService.java | 11 +- 5 files changed, 978 insertions(+), 1 deletion(-) create mode 100644 api/src/org/labkey/api/workflow/Action.java create mode 100644 api/src/org/labkey/api/workflow/Job.java create mode 100644 api/src/org/labkey/api/workflow/Task.java create mode 100644 api/src/org/labkey/api/workflow/WorkEntity.java diff --git a/api/src/org/labkey/api/workflow/Action.java b/api/src/org/labkey/api/workflow/Action.java new file mode 100644 index 00000000000..204d8356115 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Action.java @@ -0,0 +1,130 @@ +package org.labkey.api.workflow; + +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.util.GUID; + +import java.util.LinkedHashMap; +import java.util.Map; + +public abstract class Action extends CreatedModified +{ + public static final String ASSAY_TYPES_KEY = "assayTypes"; + protected Long _rowId; + protected GUID _containerId; + protected String _name; + protected boolean _isUpdatable = false; + protected Long _taskId; + protected WorkflowService.ActionType _type; + protected JSONObject _inputParameters; + + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public boolean getIsUpdatable() + { + return _isUpdatable; + } + + public void setIsUpdatable(boolean updatable) + { + _isUpdatable = updatable; + } + + public Long getTaskId() + { + return _taskId; + } + + public void setTaskId(Long taskId) + { + _taskId = taskId; + } + + public WorkflowService.ActionType getType() + { + return _type; + } + + public void setType(WorkflowService.ActionType type) + { + _type = type; + } + + public JSONObject getInputParameters() + { + return _inputParameters; + } + + public void setInputParameters(JSONObject inputParameters) + { + _inputParameters = inputParameters; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Action action = (Action) o; + + // GitHub Issue 799: Workflow Automation: Attempting to add a sample filter on an existing template errors + // Migration script generated action.name, but they are currently not used. Allow name to be changed to null or empty string. + if (!java.util.Objects.equals(_name, action._name) && !StringUtils.isEmpty(action.getName())) + return false; + + return _isUpdatable == action._isUpdatable && + java.util.Objects.equals(_rowId, action._rowId) && + java.util.Objects.equals(_taskId, action._taskId) && + java.util.Objects.equals(_type, action._type) && + java.util.Objects.equals( + _inputParameters == null ? null : _inputParameters.toString(), + action._inputParameters == null ? null : action._inputParameters.toString() + ); + } + + public Map toAuditDetailMap() + { + Map map = new LinkedHashMap<>(); + map.put("rowId", _rowId); + map.put("name", _name); + map.put("isUpdatable", _isUpdatable); + map.put("taskId", _taskId); + if (_type != null) + map.put("type", _type.name()); + if (_inputParameters != null) + map.put("inputParameters", _inputParameters.toString()); + return map; + } + + public abstract Task getTask(); +} diff --git a/api/src/org/labkey/api/workflow/Job.java b/api/src/org/labkey/api/workflow/Job.java new file mode 100644 index 00000000000..7765baf45d0 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Job.java @@ -0,0 +1,365 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.attachments.AttachmentFile; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.exp.Identifiable; +import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.api.ExpMaterial; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.Group; +import org.labkey.api.security.SecurityManager; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.GUID; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public abstract class Job extends CreatedModified implements Identifiable +{ + protected Long _rowId; + protected GUID _containerId; + protected Container _container; + protected GUID _entityId; + + protected String _name; + protected String _id; + protected String _description; + protected Date _startDate; + protected Date _dueDate; + protected Integer _priority; + protected Long _templateId; + protected Integer _assignee; + protected List _notifyList; + protected boolean _isTemplate; + protected Job _template; + protected Integer _jobCount; // only applies to templates + protected Integer _domainId; + protected String _lsid; // needed for attaching domain properties + protected List _tasks; // ordered by ordinal value + protected List _attachments; + protected List> _attachmentData = new ArrayList<>(); // ?? used for template instead of attachments? + protected List _entities; + protected final Map> _entityTypes = new HashMap<>(); + protected Map _domainProperties; + protected Map _domainPropertyValues; + protected boolean _hasMedia = false; + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonIgnore + public Container getContainer() + { + if (_containerId == null) + return null; + if (_container == null) + _container = ContainerManager.getForId(_containerId); + return _container; + } + + public GUID getEntityId() + { + return _entityId; + } + + public void setEntityId(GUID entityId) + { + _entityId = entityId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getId() + { + return _id; + } + + public void setId(String id) + { + _id = id; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getDueDate() + { + return _dueDate; + } + + public void setDueDate(Date dueDate) + { + _dueDate = dueDate; + } + + public Integer getPriority() + { + return _priority; + } + + public void setPriority(Integer priority) + { + _priority = priority; + } + + public Long getTemplateId() + { + return _templateId; + } + + public void setTemplateId(Long templateId) + { + _templateId = templateId; + } + + @JsonProperty("template") + public abstract Job getTemplate(Container container, User user); + + public static JSONObject getAssigneeJSON(Integer assigneeId) + { + if (assigneeId == null) + return null; + User user = UserManager.getUser(assigneeId); + if (user != null) + return user.getUserProps(); + + Group group = SecurityManager.getGroup(assigneeId); + if (group != null) + { + JSONObject props = new JSONObject(); + props.put("id", group.getUserId()); + props.put("displayName", group.getName()); + return props; + } + + return null; + } + + @JsonProperty("assignee") + public void setAssignee(Integer assignee) + { + _assignee = assignee; + } + + public Integer getAssignee() + { + return _assignee; + } + + @JsonProperty("assignee") + public JSONObject getAssigneeJSON() + { + return getAssigneeJSON(_assignee); + } + + public abstract List getNotifyList(); + + public void setNotifyList(List notifyList) + { + _notifyList = notifyList; + } + + @JsonProperty("notifyList") + public List getNotifyListJSON() + { + if (getNotifyList() == null) + return null; + + return getNotifyList().stream() + .map(Job::getAssigneeJSON) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + public boolean getIsTemplate() + { + return _isTemplate; + } + + public void setIsTemplate(boolean template) + { + _isTemplate = template; + } + + public Integer getJobCount() + { + return _jobCount; + } + + public void setJobCount(Integer jobCount) + { + _jobCount = jobCount; + } + + @Override + public String getLSID() + { + return _lsid; + } + + public void setLSID(String lsid) + { + _lsid = lsid; + } + + public void setLSID(Lsid lsid) + { + _lsid = lsid.toString(); + } + + public Integer getDomainId() + { + return _domainId; + } + + public void setDomainId(Integer domainId) + { + _domainId = domainId; + } + + public abstract List getTasks(); + + public void setTasks(List tasks) + { + _tasks = tasks; + } + + @JsonIgnore + public List getAttachments() + { + return _attachments; + } + + public void setAttachments(List attachments) + { + _attachments = attachments; + } + + public abstract List getEntities(); + + @JsonIgnore + public abstract @NotNull List getSamples(); + + public void setEntities(List entities) + { + _entities = entities; + } + + @JsonProperty("containerPath") + public String getContainerPath() + { + Container container = getContainer(); + return container == null ? null : container.getPath(); + } + + @JsonProperty("hasMedia") + public boolean isHasMedia() + { + return _hasMedia; + } + + public void setHasMedia(boolean hasMedia) + { + _hasMedia = hasMedia; + } + + public abstract Map toMap(); + + @JsonIgnore + public List getSubsequentTasks(long taskId) + { + List orderedTasks = getTasks().stream().sorted().toList(); + int index = orderedTasks.stream().map(Task::getRowId).toList().indexOf(taskId); + if (index == -1 || index == orderedTasks.size() - 1) + return Collections.emptyList(); + return orderedTasks.subList(index+1, orderedTasks.size()); + } + + public Task getNextTask(long taskId) + { + List subsequent = getSubsequentTasks(taskId); + if (subsequent.isEmpty()) + return null; + return subsequent.get(0); + } + + public boolean isComplete() + { + if (getTasks().isEmpty()) + return true; + return getTasks().stream().allMatch(Task::isCompleted); + } + + public List> getAttachmentData() + { + return _attachmentData; + } + + public void setAttachmentData(List> attachmentData) + { + _attachmentData = attachmentData; + } + + public abstract Map toAuditDetailMap(); + + public abstract Object getDomainProperty(PropertyDescriptor prop); + + public abstract void setProperty(User user, PropertyDescriptor pd, Object value) throws ValidationException; +} diff --git a/api/src/org/labkey/api/workflow/Task.java b/api/src/org/labkey/api/workflow/Task.java new file mode 100644 index 00000000000..627fec251f0 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Task.java @@ -0,0 +1,274 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.util.GUID; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Task extends CreatedModified implements Comparable +{ + protected Long _rowId; + protected GUID _containerId; + protected Container _container; + protected GUID _entityId; + + protected String _name; + + protected String _description; + protected String _entityFilter; + protected Integer _status; + protected Date _startDate; + protected Date _endDate; + protected Date _dueDate; + protected int _ordinal; + protected Integer _assignee; + protected Long _jobId; + protected Job _job; + protected List _actions = null; + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public String getEntityFilter() + { + return _entityFilter; + } + + public void setEntityFilter(String entityFilter) + { + _entityFilter = entityFilter; + } + + @JsonProperty("assignee") + public JSONObject getAssigneeJSON() + { + return Job.getAssigneeJSON(_assignee); + } + + public Integer getAssignee() + { + return _assignee; + } + + @JsonProperty("assignee") + public void setAssignee(Integer assignee) + { + _assignee = assignee; + } + + public Integer getStatus() + { + return _status; + } + + public void setStatus(Integer status) + { + _status = status; + } + + public int getOrdinal() + { + return _ordinal; + } + + public void setOrdinal(int ordinal) + { + _ordinal = ordinal; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getEndDate() + { + return _endDate; + } + + public void setEndDate(Date endDate) + { + _endDate = endDate; + } + + public Date getDueDate() + { + return _dueDate; + } + + public void setDueDate(Date dueDate) + { + _dueDate = dueDate; + } + + public GUID getEntityId() + { + return _entityId; + } + + public void setEntityId(GUID entityId) + { + _entityId = entityId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonIgnore + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public abstract boolean isCompleted(); + + public abstract boolean isActive(); + + public abstract boolean isPending(); + + public abstract List getActions(); + + public void setActions(List actions) + { + _actions = actions; + } + + public Long getJobId() + { + return _jobId; + } + + public void setJobId(Long jobId) + { + _jobId = jobId; + } + + public abstract Job getJob(); + + public abstract Map toMap(); + + // Determine if the template task with existing jobs can be updated to the new task definition + // Only entityFilter field is allowed to be changed for a referenced template task + public boolean canUpdateUsedTemplateTask(Task task) + { + if (this == task) return true; + if (task == null || getClass() != task.getClass()) return false; + + if (!Objects.equals(_name, task._name) || + !Objects.equals(_description, task._description)) + return false; + + List existingActions = this.getActions(); + List newActions = task.getActions(); + if (existingActions.size() != newActions.size()) + return false; + + for (int i = 0; i < existingActions.size(); i++) + { + Action existingAction = existingActions.get(i); + Action newAction = newActions.get(i); + if (!existingAction.equals(newAction)) + { + return false; + } + } + + return true; + } + + @JsonIgnore + public boolean hasUpdatableAssaysAction() + { + return getActions().stream().anyMatch(action -> action.getType() == WorkflowService.ActionType.AssayImport && action.getIsUpdatable()); + } + + public Map toAuditDetailMap() + { + Map map = new LinkedHashMap<>(); + map.put("rowId", getRowId()); + map.put("name", getName()); + map.put("description", getDescription()); + map.put("entityFilter", getEntityFilter()); + map.put("status", getStatus()); + if (getStartDate() != null) + map.put("startDate", getStartDate()); + if (getEndDate() != null) + map.put("endDate", getEndDate()); + if (getDueDate() != null) + map.put("dueDate", getDueDate()); + if (getEntityId() != null) + map.put("entityId", getEntityId().toString()); + map.put("ordinal", getOrdinal()); + int actionIndex = 1; + for (Action action : getActions()) + { + Map actionMap = action.toAuditDetailMap(); + for (Map.Entry entry : actionMap.entrySet()) + map.put("action" + actionIndex + "." + entry.getKey(), entry.getValue()); + actionIndex++; + } + return map; + } + + @Override + public int compareTo(@NotNull Task o) + { + return Integer.compare(getOrdinal(), o.getOrdinal()); + } + +} diff --git a/api/src/org/labkey/api/workflow/WorkEntity.java b/api/src/org/labkey/api/workflow/WorkEntity.java new file mode 100644 index 00000000000..629ce889eb9 --- /dev/null +++ b/api/src/org/labkey/api/workflow/WorkEntity.java @@ -0,0 +1,199 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.labkey.api.exp.api.ExpMaterial; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.GUID; + +import java.util.Date; +import java.util.Map; + +public class WorkEntity +{ + public enum WorkType + { + Job, + Task, + Action + } + + public enum EntityType + { + Sample, + Source, + Plate, + } + + protected Long _rowId; + protected GUID _containerId; + protected WorkType _workType; + protected Long _workRowId; + protected EntityType _entityType; + protected Long _entityValue; + protected Long _created; + protected User _createdBy; + + public WorkEntity() + {} + + public WorkEntity(Map map) + { + _rowId = MapUtils.getLong(map, "rowId"); + if (map.get("workType") instanceof String) + _workType = WorkType.valueOf((String) map.get("workType")); + else + _workType = (WorkType) map.get("workType"); + _workRowId = MapUtils.getLong(map, "workRowId"); + if (map.get("entityType") instanceof String) + _entityType = EntityType.valueOf((String) map.get("entityType")); + else + _entityType = (EntityType) map.get("entityType"); + _entityValue = MapUtils.getLong(map, "entityValue"); + if (map.get("Container") != null) + this.setContainerId(new GUID((String) map.get("Container"))); + } + + public WorkEntity(ExpMaterial sample) + { + _entityType = EntityType.Sample; + _entityValue = sample.getRowId(); + } + + public WorkEntity(ExpMaterial sample, WorkType workType, Long workRowId) + { + this(sample); + _workType = workType; + _workRowId = workRowId; + } + + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonProperty("created") + public Long getCreated() + { + return _created; + } + + @JsonIgnore + public Date getCreatedDate() + { + return _created == null ? null : new Date(_created); + } + + public void setCreated(Long created) + { + _created = created; + } + + @JsonIgnore // created is serialized as Long + public void setCreated(Date created) + { + if (created != null) + setCreated(created.getTime()); + } + + @JsonProperty("createdBy") + public JSONObject getCreatedBy() + { + if (_createdBy == null) + return null; + return _createdBy.getUserProps(); + } + + @JsonIgnore + public User getCreatedByUser() + { + return _createdBy; + } + + public void setCreatedBy(User createdBy) + { + _createdBy = createdBy; + } + + public void setCreatedBy(Integer createdById) + { + if (createdById != null) + _createdBy = UserManager.getUser(createdById); + } + + public WorkType getWorkType() + { + return _workType; + } + + public void setWorkType(WorkType workType) + { + _workType = workType; + } + + public void setWorkType(String workTypeStr) + { + _workType = StringUtils.isEmpty(StringUtils.trimToEmpty(workTypeStr)) ? null : WorkType.valueOf(workTypeStr.trim()); + } + + public Long getWorkRowId() + { + return _workRowId; + } + + public void setWorkRowId(Long workRowId) + { + _workRowId = workRowId; + } + + public EntityType getEntityType() + { + return _entityType; + } + + public void setEntityType(EntityType entityType) + { + _entityType = entityType; + } + + public void setValueType(String valueTypeStr) + { + _entityType = EntityType.valueOf(valueTypeStr); + } + + public Long getEntityValue() + { + return _entityValue; + } + + public void setEntityValue(Long entityValue) + { + _entityValue = entityValue; + } + + @JsonIgnore + public String getKey() + { + return _entityType + ":" + _entityValue; + } +} diff --git a/api/src/org/labkey/api/workflow/WorkflowService.java b/api/src/org/labkey/api/workflow/WorkflowService.java index 55e22cb6602..6c830e01625 100644 --- a/api/src/org/labkey/api/workflow/WorkflowService.java +++ b/api/src/org/labkey/api/workflow/WorkflowService.java @@ -1,7 +1,10 @@ package org.labkey.api.workflow; import org.jetbrains.annotations.NotNull; -import org.labkey.api.data.Container;import org.labkey.api.security.User;import org.labkey.api.services.ServiceRegistry; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.security.User; +import org.labkey.api.services.ServiceRegistry; public interface WorkflowService { @@ -41,4 +44,10 @@ static WorkflowService get() void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long actionId); void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long taskId, @NotNull ActionType actionType); + + @Nullable + Job getJob(Long jobId); + + @Nullable + Job getELNReferencePlaceholderJob(Container container); } From e82c821825b11c49c8888074c0999a8cb581cb65 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Fri, 17 Apr 2026 16:15:48 -0500 Subject: [PATCH 2/5] GitHub Issue #1061: Error loading assay import page when Participant/Visit resolver field is set to 'fixed value' default (#7592) - ParticipantVisitResolverChooser disabled state fix to use getHtmlString() isntead of appendTo() --- .../api/study/actions/ParticipantVisitResolverChooser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/study/actions/ParticipantVisitResolverChooser.java b/api/src/org/labkey/api/study/actions/ParticipantVisitResolverChooser.java index c9a6d34f1ef..d0b733ea6b6 100644 --- a/api/src/org/labkey/api/study/actions/ParticipantVisitResolverChooser.java +++ b/api/src/org/labkey/api/study/actions/ParticipantVisitResolverChooser.java @@ -168,7 +168,7 @@ public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) return ret2; } ), - disabledInput ? InputBuilder.hidden().name(_typeInputName).value(finalSelected.getName()).appendTo(out) : null + disabledInput ? InputBuilder.hidden().name(_typeInputName).value(finalSelected.getName()).getHtmlString() : null ) ).appendTo(out); } From 530877914feb8d01c78e6e99297ffc897d9a829a Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Fri, 17 Apr 2026 16:49:17 -0700 Subject: [PATCH 3/5] Fix McpServerTest (#7598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Rationale Fix McpServerTest also switch default back to the default of gemini-flash #### Related Pull Requests - #### Changes - --- core/src/org/labkey/core/CoreMcp.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/org/labkey/core/CoreMcp.java b/core/src/org/labkey/core/CoreMcp.java index fecba61406d..fa18a2a55c1 100644 --- a/core/src/org/labkey/core/CoreMcp.java +++ b/core/src/org/labkey/core/CoreMcp.java @@ -103,14 +103,14 @@ String setContainer(ToolContext context, @ToolParam(description = "Container pat Container container = ContainerManager.getForPath(containerPath); - if (null == container) + if (null == container && (containerPath.startsWith("http://") || containerPath.startsWith("https://"))) { try { var url = new ActionURL(containerPath); container = ContainerManager.getForURL(url); } - catch (IllegalArgumentException x) + catch (IllegalArgumentException _) { } } From 8e584547eb93c507360f61c28518546ecfb40d13 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 20 Apr 2026 14:27:43 -0500 Subject: [PATCH 4/5] fixes post merge conflicts for classes moved to api for workflow --- api/src/org/labkey/api/workflow/Action.java | 130 ++++++++++++++++++ api/src/org/labkey/api/workflow/Task.java | 10 ++ .../org/labkey/api/workflow/WorkEntity.java | 16 ++- 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/workflow/Action.java b/api/src/org/labkey/api/workflow/Action.java index 204d8356115..ac58813c1f9 100644 --- a/api/src/org/labkey/api/workflow/Action.java +++ b/api/src/org/labkey/api/workflow/Action.java @@ -1,17 +1,29 @@ package org.labkey.api.workflow; +import com.fasterxml.jackson.annotation.JsonIgnore; import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.data.CreatedModified; +import org.labkey.api.exp.api.ExpProtocol; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.exp.api.SampleTypeService; import org.labkey.api.util.GUID; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Set; public abstract class Action extends CreatedModified { public static final String ASSAY_TYPES_KEY = "assayTypes"; + public static final String NUM_PER_PARENT_KEY = "numPerParent"; protected Long _rowId; + protected int _ordinal; protected GUID _containerId; protected String _name; protected boolean _isUpdatable = false; @@ -50,6 +62,16 @@ public void setName(String name) _name = name; } + public int getOrdinal() + { + return _ordinal; + } + + public void setOrdinal(int ordinal) + { + _ordinal = ordinal; + } + public boolean getIsUpdatable() { return _isUpdatable; @@ -90,6 +112,112 @@ public void setInputParameters(JSONObject inputParameters) _inputParameters = inputParameters; } + @JsonIgnore + public List validateInputParameters(int ordinal) + { + String prefix = "Action #" + ordinal + ": "; + if (_type == WorkflowService.ActionType.AssayImport) + { + if (_inputParameters != null && _inputParameters.has(ASSAY_TYPES_KEY)) + { + try + { + JSONArray assayTypes = _inputParameters.getJSONArray(ASSAY_TYPES_KEY); + // When assay types are updatable, there may be none provided. Usually that means the inputParameters will + // be empty or null, but it also works if the assay types array is empty. + if (assayTypes.isEmpty()) + return Collections.emptyList(); + + Set invalidAssayIds = new HashSet<>(); + assayTypes.toList().forEach(assayId -> + { + try + { + int protocolId = (assayId instanceof String) ? Integer.valueOf((String) assayId) : (Integer) assayId; + ExpProtocol assay = ExperimentService.get().getExpProtocol(protocolId); + if (null == assay) + invalidAssayIds.add(protocolId); + } + catch (Exception e) + { + invalidAssayIds.add(assayId); + } + }); + if (!invalidAssayIds.isEmpty()) + return List.of(prefix + "invalid assay IDs " + invalidAssayIds + "."); + } + catch (Exception e) + { + return List.of(prefix + ASSAY_TYPES_KEY + " must be an array."); + } + } + + } + else if (_type == WorkflowService.ActionType.AliquotSamples) + { + if (_inputParameters == null || !_inputParameters.has(NUM_PER_PARENT_KEY)) + return List.of(prefix + NUM_PER_PARENT_KEY + " is required for action of type " + _type + "."); + else + { + try { + int numPerParent = _inputParameters.getInt(NUM_PER_PARENT_KEY); + if (numPerParent < 0) + return List.of(prefix + NUM_PER_PARENT_KEY + " cannot be negative."); + } + catch (Exception e) { + return List.of(prefix + NUM_PER_PARENT_KEY + " must be an integer."); + } + } + } + else + { + if (_inputParameters == null || _inputParameters.isEmpty()) + return List.of(prefix + "data about sample types and sample counts per parent is required for action of type " + _type + "."); + if (_type == WorkflowService.ActionType.PoolSamples && _inputParameters.length() > 1) + return List.of(prefix + "only one sample type can be specified for action of type " + _type + "."); + SampleTypeService sampleTypeService = SampleTypeService.get(); + Set invalidSampleTypeIds = new HashSet<>(); + List invalidCounts = new ArrayList<>(); + + _inputParameters.keys().forEachRemaining(id -> { + try + { + if (sampleTypeService.getSampleType(Long.valueOf(id)) == null) + invalidSampleTypeIds.add(id); + } + catch (NumberFormatException e) + { + invalidSampleTypeIds.add(id); + } + Object countObj = _inputParameters.get(id); + if ((countObj instanceof String)) + try + { + if (Integer.parseInt((String) countObj) < 0) + invalidCounts.add(countObj); + } + catch (NumberFormatException e) + { + invalidCounts.add(countObj); + } + else if (countObj instanceof Integer) + { + if (((Integer) countObj) < 0) + invalidCounts.add(countObj); + } + else + invalidCounts.add(countObj); + }); + List messages = new ArrayList<>(); + if (!invalidSampleTypeIds.isEmpty()) + messages.add(prefix + "invalid sample type IDs " + invalidSampleTypeIds + "."); + if (!invalidCounts.isEmpty()) + messages.add(prefix + "invalid sample count values " + invalidCounts + "."); + return messages; + } + return Collections.emptyList(); + } + @Override public boolean equals(Object o) { @@ -103,6 +231,7 @@ public boolean equals(Object o) return false; return _isUpdatable == action._isUpdatable && + _ordinal == action._ordinal && java.util.Objects.equals(_rowId, action._rowId) && java.util.Objects.equals(_taskId, action._taskId) && java.util.Objects.equals(_type, action._type) && @@ -117,6 +246,7 @@ public Map toAuditDetailMap() Map map = new LinkedHashMap<>(); map.put("rowId", _rowId); map.put("name", _name); + map.put("ordinal", _ordinal); map.put("isUpdatable", _isUpdatable); map.put("taskId", _taskId); if (_type != null) diff --git a/api/src/org/labkey/api/workflow/Task.java b/api/src/org/labkey/api/workflow/Task.java index 627fec251f0..10b2c402fdc 100644 --- a/api/src/org/labkey/api/workflow/Task.java +++ b/api/src/org/labkey/api/workflow/Task.java @@ -10,6 +10,7 @@ import org.labkey.api.util.GUID; import java.util.Date; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -183,6 +184,15 @@ public void setContainer(Container container) public abstract List getActions(); + @JsonIgnore + public Map getActionsByRowId() + { + Map map = new HashMap<>(); + for (Action action : getActions()) + map.put(action.getRowId(), action); + return map; + } + public void setActions(List actions) { _actions = actions; diff --git a/api/src/org/labkey/api/workflow/WorkEntity.java b/api/src/org/labkey/api/workflow/WorkEntity.java index 629ce889eb9..f01cc08c70b 100644 --- a/api/src/org/labkey/api/workflow/WorkEntity.java +++ b/api/src/org/labkey/api/workflow/WorkEntity.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; import org.json.JSONObject; import org.labkey.api.exp.api.ExpMaterial; import org.labkey.api.security.User; @@ -35,6 +36,7 @@ public enum EntityType protected Long _workRowId; protected EntityType _entityType; protected Long _entityValue; + protected Long _actionId; protected Long _created; protected User _createdBy; @@ -54,6 +56,7 @@ public WorkEntity(Map map) else _entityType = (EntityType) map.get("entityType"); _entityValue = MapUtils.getLong(map, "entityValue"); + _actionId = MapUtils.getLong(map, "actionId"); if (map.get("Container") != null) this.setContainerId(new GUID((String) map.get("Container"))); } @@ -64,11 +67,12 @@ public WorkEntity(ExpMaterial sample) _entityValue = sample.getRowId(); } - public WorkEntity(ExpMaterial sample, WorkType workType, Long workRowId) + public WorkEntity(ExpMaterial sample, WorkType workType, Long workRowId, @Nullable Long actionId) { this(sample); _workType = workType; _workRowId = workRowId; + _actionId = actionId; } @@ -92,6 +96,16 @@ public void setContainerId(GUID containerId) _containerId = containerId; } + public Long getActionId() + { + return _actionId; + } + + public void setActionId(Long actionId) + { + _actionId = actionId; + } + @JsonProperty("created") public Long getCreated() { From 5de2cfbb2818d9a0b8ee42ba7d3449b904123ea4 Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 22 Apr 2026 09:08:11 -0500 Subject: [PATCH 5/5] Reapply changes to Action.java (from "Add workflow storage actions" #2104) after merge conflict --- api/src/org/labkey/api/workflow/Action.java | 45 ++++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/api/src/org/labkey/api/workflow/Action.java b/api/src/org/labkey/api/workflow/Action.java index ac58813c1f9..6ba2c550783 100644 --- a/api/src/org/labkey/api/workflow/Action.java +++ b/api/src/org/labkey/api/workflow/Action.java @@ -4,10 +4,13 @@ import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; import org.labkey.api.data.CreatedModified; import org.labkey.api.exp.api.ExpProtocol; import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.api.SampleTypeService; +import org.labkey.api.qc.SampleStatusService; import org.labkey.api.util.GUID; import java.util.ArrayList; @@ -22,9 +25,12 @@ public abstract class Action extends CreatedModified { public static final String ASSAY_TYPES_KEY = "assayTypes"; public static final String NUM_PER_PARENT_KEY = "numPerParent"; + public static final String UPDATE_STATUS_KEY = "updateStatus"; + public static final String STATUS_KEY = "sampleStatus"; protected Long _rowId; protected int _ordinal; protected GUID _containerId; + private Container _container; protected String _name; protected boolean _isUpdatable = false; protected Long _taskId; @@ -52,6 +58,14 @@ public void setContainerId(GUID containerId) _containerId = containerId; } + @JsonIgnore + public Container getContainer() + { + if (_container == null && _containerId != null) + _container = ContainerManager.getForId(_containerId); + return _container; + } + public String getName() { return _name; @@ -113,7 +127,7 @@ public void setInputParameters(JSONObject inputParameters) } @JsonIgnore - public List validateInputParameters(int ordinal) + public List validateInputParameters(int ordinal, Container container) { String prefix = "Action #" + ordinal + ": "; if (_type == WorkflowService.ActionType.AssayImport) @@ -169,7 +183,7 @@ else if (_type == WorkflowService.ActionType.AliquotSamples) } } } - else + else if (_type == WorkflowService.ActionType.DeriveSamples || _type == WorkflowService.ActionType.PoolSamples) { if (_inputParameters == null || _inputParameters.isEmpty()) return List.of(prefix + "data about sample types and sample counts per parent is required for action of type " + _type + "."); @@ -215,6 +229,33 @@ else if (countObj instanceof Integer) messages.add(prefix + "invalid sample count values " + invalidCounts + "."); return messages; } + else if (_type == WorkflowService.ActionType.RemoveFromStorage) + { + if (_inputParameters == null || _inputParameters.isEmpty()) + return Collections.emptyList(); + boolean updateStatus = _inputParameters.getBoolean(UPDATE_STATUS_KEY); + if (updateStatus && !_inputParameters.has(STATUS_KEY)) + return List.of(prefix + STATUS_KEY + " is required for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is true."); + if (!updateStatus && _inputParameters.has(STATUS_KEY)) + return List.of(prefix + STATUS_KEY + " is not allowed for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is false."); + if (updateStatus && container != null) + { + try + { + long statusId = _inputParameters.getLong(STATUS_KEY); + SampleStatusService sampleStatusService = SampleStatusService.get(); + if (sampleStatusService.getStateForRowId(container, statusId) == null) + return List.of(prefix + "Invalid " + STATUS_KEY + " (" + statusId + ")."); + } + catch (Exception e) + { + return List.of(prefix + "Invalid " + STATUS_KEY + "."); + } + } + } else { + if (_inputParameters != null && !_inputParameters.isEmpty()) + return List.of(prefix + "input parameters are not allowed for action of type " + _type + "."); + } return Collections.emptyList(); }