diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index bdfbbbf90dd..c97e5e0546e 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -68,6 +68,8 @@ public class DataService implements IDataService { public static final int MONGO_ID_LENGTH = 24; + private static final Set setDataForbiddenFieldTypes = Set.of(FieldType.TASK_REF, FieldType.CASE_REF); + @Autowired protected ApplicationEventPublisher publisher; @@ -219,6 +221,22 @@ public SetDataEventOutcome setData(Task task, ObjectNode values) { @Override public SetDataEventOutcome setData(Task task, ObjectNode values, Map params) { + return setData(task, values, params, false); + } + + /** + * Updates the data field's attributes of the provided task. + * + * @param task the task object of which the data are updated + * @param values information about how to update the data fields + * @param params additional information to be injected to the action delegate context + * @param runStrict if set to true, additional validations are going to be applied when updating the data fields. If + * set to false, minimal restrictions are considered. + * + * @return outcome containing Case, Task and changes that have been made. + * */ + @Override + public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runStrict) { Case useCase = workflowService.findOne(task.getCaseId()); IUser user = userService.getLoggedOrSystem(); @@ -230,8 +248,20 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map { String fieldId = entry.getKey(); + if (runStrict) { + Field field = useCase.getField(fieldId); + if (field == null) { + throw new IllegalArgumentException("Such field with id [" + fieldId + "] does not exist in petri net [" + useCase.getPetriNetId() + "]"); + } + if (setDataForbiddenFieldTypes.contains(field.getType())) { + return; + } + } DataField dataField = useCase.getDataSet().get(fieldId); if (dataField != null) { + if (runStrict && !isDataFieldEditable(dataField, task.getTransitionId())) { + throw new IllegalArgumentException("Cannot edit data field [" + fieldId + "], which is not editable on transition [" + task.getTransitionId() + "]."); + } Field field = useCase.getPetriNet().getField(fieldId).get(); outcome.addOutcomes(resolveDataEvents(field, DataEventType.SET, EventPhase.PRE, useCase, task, params)); if (outcome.getMessage() == null) { @@ -303,6 +333,15 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map> behaviorMap = dataField.getBehavior(); + if (behaviorMap == null) { + return false; + } + Set behaviorSet = behaviorMap.get(transId); + return behaviorSet != null && behaviorSet.contains(FieldBehavior.EDITABLE); + } + @Override public GetDataGroupsEventOutcome getDataGroups(String taskId, Locale locale) { return getDataGroups(taskId, locale, new HashSet<>(), 0, null); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index bde3ec1fad7..58c7db6f9d9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -925,6 +925,9 @@ public SetDataEventOutcome getMainOutcome(Map outco } } mainOutcome = outcomes.remove(key); + if (mainOutcome == null) { + return null; + } mainOutcome.addOutcomes(new ArrayList<>(outcomes.values())); return mainOutcome; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java index 48dc9244a9d..a2cb4211f8b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java @@ -36,6 +36,8 @@ public interface IDataService { SetDataEventOutcome setData(Task task, ObjectNode values, Map params); + SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runStrict); + FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview, Map params) throws FileNotFoundException; diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index 890e053fcde..0e295542652 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -5,6 +5,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; import com.netgrif.application.engine.eventoutcomes.LocalisedEventOutcomeFactory; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; import com.netgrif.application.engine.petrinet.domain.throwable.TransitionNotExecutableException; import com.netgrif.application.engine.workflow.domain.IllegalArgumentWithChangedFieldsException; import com.netgrif.application.engine.workflow.domain.MergeFilterOperation; @@ -16,6 +17,7 @@ import com.netgrif.application.engine.workflow.service.FileFieldInputStream; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.*; @@ -38,10 +40,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public abstract class AbstractTaskController { @@ -51,11 +51,15 @@ public abstract class AbstractTaskController { private final IDataService dataService; + private final IWorkflowService workflowService; + private final IElasticTaskService searchService; - public AbstractTaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { + public AbstractTaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { this.taskService = taskService; this.dataService = dataService; + this.workflowService = workflowService; this.searchService = searchService; } @@ -210,9 +214,31 @@ public EntityModel getData(String taskId, Locale locale public EntityModel setData(String taskId, ObjectNode dataBody, Locale locale) { try { + List dataGroups = dataService.getDataGroups(taskId, locale).getData(); + Set referencedTaskIds = new HashSet<>(); + referencedTaskIds.add(taskId); + for (com.netgrif.application.engine.petrinet.domain.DataGroup dataGroup : dataGroups) { + Set referencedTaskIdsByDataGroup = dataGroup.getFields().getContent().stream() + .filter(localisedField -> localisedField.getType() == FieldType.TASK_REF + && localisedField.getValue() instanceof List + && !((List) localisedField.getValue()).isEmpty()) + .map(localisedField -> (List) localisedField.getValue()) + .flatMap(List::stream) + .collect(Collectors.toSet()); + referencedTaskIds.addAll(referencedTaskIdsByDataGroup); + } Map outcomes = new HashMap<>(); - dataBody.fields().forEachRemaining(it -> outcomes.put(it.getKey(), dataService.setData(it.getKey(), it.getValue().deepCopy()))); + dataBody.fields().forEachRemaining(fieldChangesEntry -> { + String taskIdToChangeWith = fieldChangesEntry.getKey(); + if (!referencedTaskIds.contains(taskIdToChangeWith)) { + return; + } + Task taskToChangeWith = taskService.findOne(taskIdToChangeWith); + outcomes.put(taskIdToChangeWith, dataService.setData(taskToChangeWith, + fieldChangesEntry.getValue().deepCopy(), new HashMap<>(), true)); + }); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -230,6 +256,7 @@ public EntityModel saveFile(String taskId, MultipartFil Map outcomes = new HashMap<>(); outcomes.put(dataBody.getParentTaskId(), dataService.saveFile(dataBody.getParentTaskId(), dataBody.getFieldId(), multipartFile)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -262,7 +289,8 @@ public EntityModel deleteFile(String taskId, String fie Map outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFile(taskId, fieldId)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -270,7 +298,8 @@ public EntityModel saveFiles(String taskId, MultipartFi Map outcomes = new HashMap<>(); outcomes.put(requestBody.getParentTaskId(), dataService.saveFiles(requestBody.getParentTaskId(), requestBody.getFieldId(), multipartFiles)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -294,7 +323,8 @@ public EntityModel deleteNamedFile(String taskId, Strin Map outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFileByName(taskId, fieldId, name)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -310,4 +340,13 @@ public ResponseEntity getFilePreview(String taskId, String fieldId) th .headers(headers) .body(fileFieldInputStream != null ? new InputStreamResource(fileFieldInputStream.getInputStream()) : null); } + + protected SetDataEventOutcome handleMainSetDataEventOutcome(SetDataEventOutcome mainOutcome, String taskId) { + if (mainOutcome == null) { + Task task = taskService.findOne(taskId); + return new SetDataEventOutcome(workflowService.findOne(task.getCaseId()), task); + } else { + return mainOutcome; + } + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java index 30294e760c9..02818262191 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java @@ -7,6 +7,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.LocalisedTaskResource; @@ -49,8 +50,9 @@ public class PublicTaskController extends AbstractTaskController { private final ITaskService taskService; private final IDataService dataService; - public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService) { - super(taskService, dataService, null); + public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService, + IWorkflowService workflowService) { + super(taskService, dataService, workflowService, null); this.taskService = taskService; this.dataService = dataService; this.userService = userService; diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java index b9690306c3f..593f37ce4ee 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java @@ -9,6 +9,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.CountResponse; @@ -51,8 +52,9 @@ public class TaskController extends AbstractTaskController { public static final Logger log = LoggerFactory.getLogger(TaskController.class); - public TaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { - super(taskService, dataService, searchService); + public TaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { + super(taskService, dataService, workflowService, searchService); } @Override diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy index ce970f467d6..e30f8af7f85 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy @@ -1,5 +1,7 @@ package com.netgrif.application.engine.workflow +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.domain.Authority import com.netgrif.application.engine.auth.domain.User @@ -9,7 +11,6 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.VersionType -import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue import com.netgrif.application.engine.petrinet.domain.dataset.FileListFieldValue import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.ProcessRoleService @@ -18,12 +19,12 @@ import com.netgrif.application.engine.startup.ImportHelper import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.utils.FullPageRequest import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.DataField import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.service.TaskSearchService import com.netgrif.application.engine.workflow.service.TaskService import com.netgrif.application.engine.workflow.service.interfaces.IDataService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService -import com.netgrif.application.engine.workflow.web.PublicTaskController import com.netgrif.application.engine.workflow.web.TaskController import com.netgrif.application.engine.workflow.web.WorkflowController import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest @@ -87,7 +88,9 @@ class TaskControllerTest { @Autowired private TaskController taskController - private PetriNet net + private PetriNet allDataNet + + private PetriNet setDataNet private Case useCase @@ -106,7 +109,7 @@ class TaskControllerTest { state: UserState.ACTIVE, authorities: [authorityService.getOrCreate(Authority.user)] as Set, processRoles: [] as Set)) - importNet() + importNets() } @Test @@ -118,7 +121,7 @@ class TaskControllerTest { @Test void testDeleteFile() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFile(taskId, "file", new MockMultipartFile("test", new byte[] {})) @@ -132,7 +135,7 @@ class TaskControllerTest { @Test void testDeleteFileByName() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFiles(taskId, "fileList", new MockMultipartFile[] {new MockMultipartFile("test", "test", null, new byte[] {})}) @@ -144,6 +147,79 @@ class TaskControllerTest { assert ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths == null || ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths.size() == 0 } + @Test + void testSetDataFieldTypeRestriction() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataFieldTypeRestriction" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["taskRef_0": ["type": "taskRef", "value": [taskId]]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("taskRef_0").getValue()).isEmpty() + + dataSet = populateNestedDataset([(taskId):["caseRef_0": ["type": "caseRef", "value": [testCase.stringId]]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("caseRef_0").getValue()).isEmpty() + } + + @Test + void testSetDataVisibleField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "data" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome == null + assert response.content.error != null + + // todo: test visible behavior based on parent taskRef behavior + } + + @Test + void testSetDataNestedTaskRefRestrictions() { + Case testCase1 = helper.createCase("testCase1", setDataNet) + String taskId = testCase1.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task + Case testCase2 = helper.createCase("testCase2", setDataNet) + Case testCase3 = helper.createCase("testCase3", setDataNet) + + DataField case1DataField = testCase1.getDataField("taskRef_0") + case1DataField.setValue(List.of(testCase2.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task)) + workflowService.save(testCase1) + + DataField case2DataField = testCase2.getDataField("taskRef_0") + case2DataField.setValue(List.of(testCase3.tasks.find { it.transition == "data" }.task)) + workflowService.save(testCase2) + + String nestedOtherTaskId = testCase2.tasks.find { it.transition == "data" }.task + ObjectNode dataSet = populateNestedDataset([(nestedOtherTaskId):["text_0": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase2.stringId).getDataField("text_0").getValue() == null + + String nestedTaskId = testCase3.tasks.find { it.transition == "data" }.task + dataSet = populateNestedDataset([(nestedTaskId):["text_0": ["type": "text", "value": "awd"]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert !response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase3.stringId).getDataField("text_0").getValue() == "awd" + } + + @Test + void testSetDataNonReferencedField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataNonReferencedField" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + + assert response != null && response.content.outcome == null + assert response.content.error != null + } + void testWithRoleAndUserref() { createCase() @@ -167,15 +243,19 @@ class TaskControllerTest { assert !findTasksByMongo().empty } - void importNet() { - PetriNet netOptional = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() - assert netOptional != null - net = netOptional + void importNets() { + PetriNet allDataNet = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() + assert allDataNet != null + this.allDataNet = allDataNet + + PetriNet setDataNet = helper.createNet("task_controller_set_data.xml", VersionType.MAJOR).get() + assert setDataNet != null + this.setDataNet = setDataNet } void createCase() { useCase = null - useCase = helper.createCase("My case", net) + useCase = helper.createCase("My case", allDataNet) assert useCase != null } @@ -203,7 +283,7 @@ class TaskControllerTest { } void setUserRole() { - List roles = processRoleService.findAll(net.stringId) + List roles = processRoleService.findAll(allDataNet.stringId) for (ProcessRole role : roles) { if (role.importId == "process_role") { @@ -219,4 +299,10 @@ class TaskControllerTest { Page tasks = taskService.search(taskSearchRequestList, new FullPageRequest(), userService.findByEmail(DUMMY_USER_MAIL, false).transformToLoggedUser(), new Locale("en"), false) return tasks } + + static ObjectNode populateNestedDataset(Map>> data) { + ObjectMapper mapper = new ObjectMapper() + String json = mapper.writeValueAsString(data) + return mapper.readTree(json) as ObjectNode + } } diff --git a/src/test/resources/petriNets/task_controller_set_data.xml b/src/test/resources/petriNets/task_controller_set_data.xml new file mode 100644 index 00000000000..60743a813ad --- /dev/null +++ b/src/test/resources/petriNets/task_controller_set_data.xml @@ -0,0 +1,154 @@ + + task_controller_set_data + 1.0.0 + TCS + task_controller_set_data + device_hub + true + true + false + + taskRef_0 + + </data> + <data type="caseRef"> + <id>caseRef_0</id> + <title/> + </data> + <data type="text"> + <id>text_0</id> + <title/> + </data> + <data type="text"> + <id>text_1</id> + <title/> + </data> + <transition> + <id>testSetDataFieldTypeRestriction</id> + <x>720</x> + <y>336</y> + <label>testSetDataFieldTypeRestriction</label> + <dataGroup> + <id>testSetDataFieldTypeRestriction_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>caseRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNestedTaskRefRestrictions</id> + <x>720</x> + <y>436</y> + <label>testSetDataNestedTaskRefRestrictions</label> + <dataGroup> + <id>testSetDataNestedTaskRefRestrictions_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>data</id> + <x>720</x> + <y>564</y> + <label>data</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNonReferencedField</id> + <x>720</x> + <y>664</y> + <label>testSetDataNonReferencedField</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> +</document> \ No newline at end of file