Merge "add errors and align style"
authorAvi Gaffa <avi.gaffa@amdocs.com>
Sun, 5 Aug 2018 08:03:40 +0000 (08:03 +0000)
committerGerrit Code Review <gerrit@onap.org>
Sun, 5 Aug 2018 08:03:40 +0000 (08:03 +0000)
50 files changed:
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/RestUtils.java [deleted file]
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/WorkflowController.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/WorkflowVersionController.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/types/Paging.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/types/Sorting.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/types/VersionStatesFormatter.java [new file with mode: 0644]
workflow-designer-be/src/main/java/org/onap/sdc/workflow/persistence/types/ParameterEntity.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/persistence/types/Workflow.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/persistence/types/WorkflowVersion.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/services/impl/WorkflowManagerImpl.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/services/impl/WorkflowVersionManagerImpl.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/services/types/PagingRequest.java
workflow-designer-be/src/main/java/org/onap/sdc/workflow/services/types/Sort.java
workflow-designer-be/src/test/java/org/onap/sdc/workflow/RestPath.java
workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/WorkflowControllerTest.java
workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/PagingTest.java [new file with mode: 0644]
workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/SortingTest.java [new file with mode: 0644]
workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/VersionStatesFormatterTest.java [new file with mode: 0644]
workflow-designer-be/src/test/java/org/onap/sdc/workflow/services/impl/WorkflowManagerImplTest.java
workflow-designer-be/src/test/java/org/onap/sdc/workflow/services/impl/WorkflowVersionManagerImplTest.java
workflow-designer-ui/src/main/frontend/.gitignore
workflow-designer-ui/src/main/frontend/index.html
workflow-designer-ui/src/main/frontend/package.json
workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss
workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss
workflow-designer-ui/src/main/frontend/resources/scss/style.scss
workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js
workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js
workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js
workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js
workflow-designer-ui/src/main/frontend/src/i18n/languages.json
workflow-designer-ui/src/main/frontend/src/rootReducers.js
workflow-designer-ui/src/main/frontend/src/routes.js
workflow-designer-ui/src/main/frontend/webpack.config.js
workflow-designer-ui/src/main/frontend/yarn.lock

diff --git a/workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/RestUtils.java b/workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/RestUtils.java
deleted file mode 100644 (file)
index d8577c1..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright © 2018 European Support Limited
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.onap.sdc.workflow.api;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.onap.sdc.workflow.persistence.types.WorkflowVersionState;
-import org.openecomp.sdc.logging.api.Logger;
-import org.openecomp.sdc.logging.api.LoggerFactory;
-
-public class RestUtils {
-
-    private RestUtils() {
-    }
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(RestUtils.class);
-
-    public static Set<WorkflowVersionState> formatVersionStates(String versionStateFilter) {
-        Set<WorkflowVersionState> filter;
-        try {
-            filter = versionStateFilter == null ? null :
-                             Arrays.stream(versionStateFilter.split(",")).map(WorkflowVersionState::valueOf)
-                                   .collect(Collectors.toSet());
-        } catch (Exception e) {
-            LOGGER.info(
-                    "version state filter value is invalid and cannot be formatted to a set of version states, therefore it is set to empty set");
-            filter = Collections.emptySet();
-        }
-        return filter;
-    }
-}
index 77f6e6d..690b207 100644 (file)
@@ -20,7 +20,6 @@ import static org.onap.sdc.workflow.api.RestParams.LIMIT;
 import static org.onap.sdc.workflow.api.RestParams.OFFSET;
 import static org.onap.sdc.workflow.api.RestParams.SORT;
 import static org.onap.sdc.workflow.api.RestParams.USER_ID_HEADER;
-import static org.onap.sdc.workflow.api.RestUtils.formatVersionStates;
 import static org.onap.sdc.workflow.services.types.PagingConstants.DEFAULT_LIMIT;
 import static org.onap.sdc.workflow.services.types.PagingConstants.DEFAULT_OFFSET;
 
@@ -31,6 +30,7 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import org.onap.sdc.workflow.api.types.Paging;
 import org.onap.sdc.workflow.api.types.Sorting;
+import org.onap.sdc.workflow.api.types.VersionStatesFormatter;
 import org.onap.sdc.workflow.persistence.types.Workflow;
 import org.onap.sdc.workflow.services.WorkflowManager;
 import org.onap.sdc.workflow.services.WorkflowVersionManager;
@@ -53,6 +53,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import springfox.documentation.annotations.ApiIgnore;
 
 @RequestMapping("/workflows")
 @Api("Workflows")
@@ -71,6 +72,8 @@ public class WorkflowController {
     @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation("List workflows")
     @ApiImplicitParams({
+            @ApiImplicitParam(name = "versionState", dataType = "string", paramType = "query",
+                    allowableValues = "DRAFT,CERTIFIED", value = "Filter by version state"),
             @ApiImplicitParam(name = OFFSET, dataType = "string", paramType = "query", defaultValue = "0",
                     value = "Index of the starting item"),
             @ApiImplicitParam(name = LIMIT, dataType = "string", paramType = "query", defaultValue = "200",
@@ -78,12 +81,11 @@ public class WorkflowController {
             @ApiImplicitParam(name = SORT, dataType = "string", paramType = "query", defaultValue = "name:asc",
                     value = "Sorting criteria in the format: property:(asc|desc). Default sort order is ascending.",
                     allowableValues = "name:asc,name:desc")})
-    public Page<Workflow> list(@ApiParam(value = "Filter by version state", allowableValues = "DRAFT,CERTIFIED")
-            @RequestParam(name = "versionState", required = false) String versionStateFilter,
-            @ApiParam(hidden = true) Paging paging,
-            @ApiParam(hidden = true) Sorting sorting,
+    public Page<Workflow> list(@ApiIgnore VersionStatesFormatter versionStateFilter,
+            @ApiIgnore Paging paging,
+            @ApiIgnore Sorting sorting,
             @RequestHeader(USER_ID_HEADER) String user) {
-        return workflowManager.list(formatVersionStates(versionStateFilter), initRequestSpec(paging, sorting));
+        return workflowManager.list(versionStateFilter.getVersionStates(), initRequestSpec(paging, sorting));
     }
 
     @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
index ba15f9f..df4c9fe 100644 (file)
 
 package org.onap.sdc.workflow.api;
 
-import static org.onap.sdc.workflow.api.RestUtils.formatVersionStates;
 import static org.onap.sdc.workflow.api.RestParams.USER_ID_HEADER;
 
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
 import org.onap.sdc.workflow.api.types.CollectionResponse;
 import org.onap.sdc.workflow.api.types.VersionStateDto;
+import org.onap.sdc.workflow.api.types.VersionStatesFormatter;
 import org.onap.sdc.workflow.persistence.types.ArtifactEntity;
 import org.onap.sdc.workflow.persistence.types.WorkflowVersion;
 import org.onap.sdc.workflow.services.WorkflowVersionManager;
@@ -50,6 +50,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
+import springfox.documentation.annotations.ApiIgnore;
 
 @RequestMapping("/workflows/{workflowId}/versions")
 @Api("Workflow versions")
@@ -71,13 +72,14 @@ public class WorkflowVersionController {
         this.validator = validator;
     }
 
+    @ApiImplicitParam(name = "state", dataType = "string", paramType = "query",
+            allowableValues = "DRAFT,CERTIFIED", value = "Filter by state")
     @GetMapping
     @ApiOperation("List workflow versions")
     public CollectionResponse<WorkflowVersion> list(@PathVariable("workflowId") String workflowId,
-            @ApiParam(value = "Filter by state", allowableValues = "DRAFT,CERTIFIED")
-            @RequestParam(value = "state", required = false) String stateFilter,
+            @ApiIgnore VersionStatesFormatter stateFilter,
             @RequestHeader(USER_ID_HEADER) String user) {
-        return new CollectionResponse<>(workflowVersionManager.list(workflowId, formatVersionStates(stateFilter)));
+        return new CollectionResponse<>(workflowVersionManager.list(workflowId, stateFilter.getVersionStates()));
     }
 
     @PostMapping
index 3d91c8e..0a27e1e 100644 (file)
@@ -16,14 +16,16 @@ public class Paging {
     }
 
     public void setLimit(String limit) {
-        getIntValue(limit).map(integer -> integer > MAX_LIMIT ? MAX_LIMIT : integer)
-                          .ifPresent(integer -> this.limit = integer);
+        getIntValue(limit).map(integer -> integer > MAX_LIMIT ? MAX_LIMIT : integer).ifPresent(integer -> {
+            if (integer != 0) {
+                this.limit = integer;
+            }
+        });
     }
 
-    private Optional<Integer> getIntValue(String value) {
-        int intValue;
+    private static Optional<Integer> getIntValue(String value) {
         try {
-            intValue = Integer.parseInt(value);
+            int intValue = Integer.parseInt(value);
             return intValue < 0 ? Optional.empty() : Optional.of(intValue);
         } catch (NumberFormatException e) {
             return Optional.empty();
index 7bb43d0..f02a05f 100644 (file)
@@ -18,11 +18,11 @@ public class Sorting {
     private List<Sort> sorts = Collections.emptyList();
 
     public void setSort(String sortString) {
-        this.sorts = Arrays.stream(sortString.split(SORTS_DELIMITER)).map(this::formatSort).filter(Objects::nonNull)
+        this.sorts = Arrays.stream(sortString.split(SORTS_DELIMITER)).map(Sorting::formatSort).filter(Objects::nonNull)
                            .collect(Collectors.toList());
     }
 
-    private Sort formatSort(String sort) {
+    private static Sort formatSort(String sort) {
         String[] tokens = sort.split(DIRECTION_DELIMITER);
         try {
             return new Sort(tokens[0], ASCENDING_ORDER.equalsIgnoreCase(tokens[1]));
diff --git a/workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/types/VersionStatesFormatter.java b/workflow-designer-be/src/main/java/org/onap/sdc/workflow/api/types/VersionStatesFormatter.java
new file mode 100644 (file)
index 0000000..5467dee
--- /dev/null
@@ -0,0 +1,36 @@
+package org.onap.sdc.workflow.api.types;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import org.onap.sdc.workflow.persistence.types.WorkflowVersionState;
+import org.openecomp.sdc.logging.api.Logger;
+import org.openecomp.sdc.logging.api.LoggerFactory;
+
+@Getter
+public class VersionStatesFormatter {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(VersionStatesFormatter.class);
+
+    private Set<WorkflowVersionState> versionStates = null;
+
+    public void setVersionState(String value) {
+        this.versionStates = formatString(value);
+    }
+
+    public void setState(String value) {
+        this.versionStates = formatString(value);
+    }
+
+    private static Set<WorkflowVersionState> formatString(String value) {
+        try {
+            return value == null ? null : Arrays.stream(value.split(",")).map(WorkflowVersionState::valueOf)
+                                                .collect(Collectors.toSet());
+        } catch (Exception ignore) {
+            LOGGER.info(
+                    "value is invalid and cannot be formatted to a set of version states, therefore it set to null");
+            return null;
+        }
+    }
+}
index 7c957d8..5bf3077 100644 (file)
@@ -25,7 +25,7 @@ public class ParameterEntity {
 
     private String id;
     @NotNull(message = "Parameter name may not be null")
-    @Pattern(regexp = "[A-Za-z0-9_]*", message = "The field must contain only letters, digits and underscores")
+    @Pattern(regexp = "[A-Za-z0-9_ ]*", message = "Parameter name must contain only letters, digits and underscores")
     private String name;
     @NotNull
     private ParameterType type;
index b2fc6f5..4b9f344 100644 (file)
@@ -31,7 +31,7 @@ public class Workflow {
     private String id;
     @NotNull(message = "Workflow name may not be null")
     @Size(max = 80, message = "Workflow name must be less than 80 characters")
-    @Pattern(regexp = "[A-Za-z0-9_]*", message = "Workflow name must contain only letters, digits and underscores")
+    @Pattern(regexp = "[A-Za-z0-9_ ]*", message = "Workflow name must contain only letters, digits and underscores")
     private String name;
     private String description;
     private Set<WorkflowVersionState> versionStates;
index 17f95ae..e3bbd64 100644 (file)
@@ -19,6 +19,7 @@ package org.onap.sdc.workflow.persistence.types;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import javax.validation.Valid;
 import lombok.Data;
 
 
@@ -31,7 +32,9 @@ public class WorkflowVersion {
     private String baseId;
     private WorkflowVersionState state;
     private boolean hasArtifact;
+    @Valid
     private Collection<ParameterEntity> inputs = Collections.emptyList();
+    @Valid
     private Collection<ParameterEntity> outputs = Collections.emptyList();
     private Date creationTime;
     private Date modificationTime;
index 23979a5..a309204 100644 (file)
@@ -135,14 +135,14 @@ public class WorkflowManagerImpl implements WorkflowManager {
         itemManager.update(item);
     }
 
-    private RequestSpec getRequestSpec(RequestSpec requestSpec) {
+    private static RequestSpec getRequestSpec(RequestSpec requestSpec) {
         if (requestSpec == null) {
             return WORKSPACES_DEFAULT_REQUEST_SPEC;
         }
         if (requestSpec.getPaging() == null) {
             requestSpec.setPaging(WORKSPACES_DEFAULT_REQUEST_SPEC.getPaging());
-        } else if (requestSpec.getPaging().getLimit() > MAX_LIMIT) {
-            requestSpec.getPaging().setLimit(MAX_LIMIT);
+        } else {
+            handlePagingRequestValues(requestSpec.getPaging());
         }
         if (requestSpec.getSorting() == null) {
             requestSpec.setSorting(WORKSPACES_DEFAULT_REQUEST_SPEC.getSorting());
@@ -150,7 +150,18 @@ public class WorkflowManagerImpl implements WorkflowManager {
         return requestSpec;
     }
 
-    private Comparator<Workflow> getWorkflowComparator(SortingRequest sorting) {
+    private static void handlePagingRequestValues(PagingRequest paging) {
+        if (paging.getOffset() == null) {
+            paging.setOffset(DEFAULT_OFFSET);
+        }
+        if (paging.getLimit() == null) {
+            paging.setLimit(DEFAULT_LIMIT);
+        } else if (paging.getLimit() > MAX_LIMIT) {
+            paging.setLimit(MAX_LIMIT);
+        }
+    }
+
+    private static Comparator<Workflow> getWorkflowComparator(SortingRequest sorting) {
         Boolean byNameAscending = sorting.getSorts().stream()
                                   .filter(sort -> WORKSPACES_SORT_PROPERTY.equalsIgnoreCase(sort.getProperty()))
                                   .findFirst().map(Sort::isAscendingOrder).orElse(true);
index 56ba7ac..14e2874 100644 (file)
@@ -18,7 +18,6 @@ package org.onap.sdc.workflow.services.impl;
 
 import static org.onap.sdc.workflow.persistence.types.WorkflowVersionState.CERTIFIED;
 
-import com.amdocs.zusammen.datatypes.response.ErrorCode;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
@@ -45,7 +44,6 @@ import org.onap.sdc.workflow.services.exceptions.VersionModificationException;
 import org.onap.sdc.workflow.services.exceptions.VersionStateModificationException;
 import org.onap.sdc.workflow.services.impl.mappers.VersionMapper;
 import org.onap.sdc.workflow.services.impl.mappers.VersionStateMapper;
-import org.openecomp.sdc.common.errors.SdcRuntimeException;
 import org.openecomp.sdc.logging.api.Logger;
 import org.openecomp.sdc.logging.api.LoggerFactory;
 import org.openecomp.sdc.versioning.VersioningManager;
@@ -186,7 +184,10 @@ public class WorkflowVersionManagerImpl implements WorkflowVersionManager {
             ArtifactEntity artifactEntity =
                     new ArtifactEntity(StringUtils.cleanPath(artifact.getOriginalFilename()), artifactData);
             artifactRepository.update(workflowId, versionId, artifactEntity);
-            versioningManager.publish(workflowId, new Version(versionId), "Update Artifact");
+            Version updatedVersion = versioningManager.get(workflowId, new Version(versionId));
+            if(updatedVersion.getState().isDirty()) {
+                versioningManager.publish(workflowId, updatedVersion, "Update artifact");
+            }
 
         } catch (IOException e) {
             LOGGER.error(String.format("Upload Artifact failed for workflow id %s and version id %s",
index f06822f..da46cc2 100644 (file)
@@ -1,15 +1,23 @@
 package org.onap.sdc.workflow.services.types;
 
-import lombok.Data;
+import lombok.Getter;
 
-@Data
+@Getter
 public class PagingRequest {
 
-    private int offset;
-    private int limit;
+    private Integer offset;
+    private Integer limit;
 
     public PagingRequest(int offset, int limit) {
-        this.offset = offset;
-        this.limit = limit;
+        setOffset(offset);
+        setLimit(limit);
+    }
+
+    public void setOffset(int offset) {
+        this.offset = offset < 0 ? null : offset;
+    }
+
+    public void setLimit(int limit) {
+        this.limit = limit <= 0 ? null : limit;
     }
 }
index 563c9ad..88151a0 100644 (file)
@@ -1,8 +1,10 @@
 package org.onap.sdc.workflow.services.types;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 
 @Getter
+@EqualsAndHashCode
 public class Sort {
 
     private String property;
index a9408f1..4301feb 100644 (file)
@@ -48,10 +48,6 @@ public class RestPath {
         return WORKFLOWS_URL;
     }
 
-    public static String getWorkflowsWithVersionStateFilterPath(String versionState) {
-        return String.format(WORKFLOWS_WITH_VERSION_STATE_FILTER_URL, versionState);
-    }
-
     public static String getWorkflowPath(String workflowId) {
         return String.format(WORKFLOW_URL_FORMATTER, workflowId);
     }
index 95f7fff..f8d2aec 100644 (file)
@@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 import static org.onap.sdc.workflow.TestUtil.createWorkflow;
@@ -21,25 +20,29 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 import com.amdocs.zusammen.datatypes.Id;
 import com.amdocs.zusammen.datatypes.item.Item;
 import com.google.gson.Gson;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 import org.onap.sdc.workflow.RestPath;
 import org.onap.sdc.workflow.api.exceptionshandlers.CustomizedResponseEntityExceptionHandler;
 import org.onap.sdc.workflow.persistence.types.Workflow;
-import org.onap.sdc.workflow.persistence.types.WorkflowVersionState;
 import org.onap.sdc.workflow.services.WorkflowManager;
 import org.onap.sdc.workflow.services.types.Page;
 import org.onap.sdc.workflow.services.types.PagingRequest;
+import org.onap.sdc.workflow.services.types.RequestSpec;
+import org.onap.sdc.workflow.services.types.Sort;
 import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -55,11 +58,13 @@ public class WorkflowControllerTest {
     private MockMvc mockMvc;
 
 
+    @Mock
+    private WorkflowManager workflowManagerMock;
+    @Captor
+    private ArgumentCaptor<RequestSpec> requestSpecArg;
     @InjectMocks
     private WorkflowController workflowController;
 
-    @Mock
-    private WorkflowManager workflowManagerMock;
 
     @Before
     public void setUp() {
@@ -99,109 +104,92 @@ public class WorkflowControllerTest {
     }
 
     @Test
-    public void shouldReturn5WorkflowWhen5WorkflowsExists() throws Exception {
-        int numOfWorkflows = 5;
-        Page<Workflow> workflowMocks = createWorkflows(numOfWorkflows);
-        doReturn(workflowMocks).when(workflowManagerMock).list(any(), any());
-        mockMvc.perform(get(RestPath.getWorkflowsPath()).header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON))
-               .andDo(print()).andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(numOfWorkflows)));
-    }
-
-    @Test
-    public void listWithValidVersionStateFilter() throws Exception {
-        int numOfWorkflows = 3;
-        Page<Workflow> workflows = createWorkflows(numOfWorkflows);
-        doReturn(workflows).when(workflowManagerMock)
-                           .list(eq(Collections.singleton(WorkflowVersionState.CERTIFIED)), any());
-        mockMvc.perform(
-                get(RestPath.getWorkflowsWithVersionStateFilterPath("CERTIFIED")).header(USER_ID_HEADER, USER_ID)
-                                                                                 .contentType(APPLICATION_JSON))
-               .andDo(print()).andExpect(status().isOk()).andExpect(jsonPath("$.paging.total", is(numOfWorkflows)))
-               .andExpect(jsonPath("$.items", hasSize(numOfWorkflows)));
-    }
-
-    @Test
-    public void listWithInvalidVersionStateFilter() throws Exception {
-        int numOfWorkflows = 0;
-        Page<Workflow> workflows = createWorkflows(numOfWorkflows);
-        doReturn(workflows).when(workflowManagerMock).list(eq(Collections.emptySet()), any());
+    public void listWhenExist() throws Exception {
+        mockManagerList3();
+        ResultActions result = mockMvc.perform(
+                get(RestPath.getWorkflowsPath()).header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON))
+                                      .andDo(print()).andExpect(status().isOk())
+                                      .andExpect(jsonPath("$.items", hasSize(3)));
+        for (int i = 0; i < 3; i++) {
+            result.andExpect(jsonPath(String.format("$.items[%s].id", i), is(String.valueOf(i + 1))));
+        }
 
-        mockMvc.perform(
-                get(RestPath.getWorkflowsWithVersionStateFilterPath("gibberish")).header(USER_ID_HEADER, USER_ID)
-                                                                                 .contentType(APPLICATION_JSON))
-               .andDo(print()).andExpect(status().isOk()).andExpect(jsonPath("$.paging.total", is(numOfWorkflows)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), DEFAULT_OFFSET, DEFAULT_LIMIT, Collections.emptyList());
     }
 
     @Test
-    public void shouldReturnSortedLimitOffsetAppliedWorkflows() throws Exception {
-        Page<Workflow> workflowMocks = createLimit2AndOffset1For5WorkflowList();
-        doReturn(workflowMocks).when(workflowManagerMock).list(any(), any());
+    public void listWhenPagingAndSortingAreSet() throws Exception {
+        mockManagerList3();
         mockMvc.perform(get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "2", "1"))
                                 .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
-               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(2)));
+               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), 1, 2, Collections.singletonList(new Sort("name", true)));
     }
 
-/*    @Test
+    @Test
     public void shouldReturnResultsWithDefaultWhenLimitIsNegative() throws Exception {
-        Page<Workflow> workflowMocks = createLimit2AndOffset1For5WorkflowList();
-        doReturn(workflowMocks).when(workflowManagerMock).list(any(), any());
+        mockManagerList3();
         mockMvc.perform(get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "-2", "1"))
                                 .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
-               .andExpect(status().isOk())
-               .andExpect(jsonPath("$.paging.offset", is(1)))
-               .andExpect(jsonPath("$.paging.limit", is(DEFAULT_LIMIT)))
-               .andExpect(jsonPath("$.paging.total", is(2)));
-    }*/
+               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), 1, DEFAULT_LIMIT,
+                Collections.singletonList(new Sort("name", true)));
+    }
 
-/*    @Test
+    @Test
     public void shouldFallbackOnDefaultOffsetWhenOffsetIsNegative() throws Exception {
-        mockMvc.perform(
-                get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "2", "-1"))
-                        .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
-                                                  .andExpect(status().isOk())
-               .andExpect(jsonPath("$.paging.offset", is(DEFAULT_OFFSET)))
-               .andExpect(jsonPath("$.paging.limit", is(2)))
-               .andExpect(jsonPath("$.paging.total", is(0)));
-    }*/
-
-/*    @Test
+        mockManagerList3();
+        mockMvc.perform(get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "2", "-1"))
+                                .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
+               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), DEFAULT_OFFSET, 2,
+                Collections.singletonList(new Sort("name", true)));
+    }
+
+    @Test
     public void shouldFallbackOnDefaultLimitWhenLimitIsNotAnInteger() throws Exception {
-        mockMvc.perform(
-                get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "abc", "0"))
-                        .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
-                                                  .andExpect(status().isOk())
-                                                  .andExpect(jsonPath("$.paging.offset", is(0)))
-                                                  .andExpect(jsonPath("$.paging.limit", is(DEFAULT_LIMIT)))
-                                                  .andExpect(jsonPath("$.paging.total", is(0)));
-    }*/
-
-/*    @Test
+        mockManagerList3();
+        mockMvc.perform(get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "abc", "0"))
+                                .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
+               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), 0, DEFAULT_LIMIT,
+                Collections.singletonList(new Sort("name", true)));
+    }
+
+    @Test
     public void shouldFallbackOnDefaultOffsetWhenOffsetIsNotAnInteger() throws Exception {
-        mockMvc.perform(
-                get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "2", "abc"))
-                        .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
-                                                  .andExpect(status().isOk())
-                                                  .andExpect(jsonPath("$.paging.offset", is(DEFAULT_OFFSET)))
-                                                  .andExpect(jsonPath("$.paging.limit", is(2)))
-                                                  .andExpect(jsonPath("$.paging.total", is(0)));
-    }*/
+        mockManagerList3();
+        mockMvc.perform(get(RestPath.getWorkflowsPathAllQueryParams(DEFAULT_SORT_VALUE, "2", "abc"))
+                                .header(USER_ID_HEADER, USER_ID).contentType(APPLICATION_JSON)).andDo(print())
+               .andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), DEFAULT_OFFSET, 2,
+                Collections.singletonList(new Sort("name", true)));
+    }
 
     @Test
     public void shouldReturnDefaultLimitOffsetAppliedWorkflowsWhenLimitIsNotSpecified() throws Exception {
-        Page<Workflow> workflowMocks = createLimit2AndOffset1For5WorkflowList();
-        doReturn(workflowMocks).when(workflowManagerMock).list(any(), any());
+        mockManagerList3();
         mockMvc.perform(get(RestPath.getWorkflowsPathNoSortAndLimit("1")).header(USER_ID_HEADER, USER_ID)
                                                                          .contentType(APPLICATION_JSON)).andDo(print())
-               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(2)));
+               .andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), 1, DEFAULT_LIMIT, Collections.emptyList());
     }
 
     @Test
     public void shouldReturnDefaultOffsetAppliedWorkflowsWhenOffsetIsNotSpecified() throws Exception {
-        Page<Workflow> workflowMocks = createLimit1WorkflowList();
-        doReturn(workflowMocks).when(workflowManagerMock).list(any(), any());
+        mockManagerList3();
         mockMvc.perform(get(RestPath.getWorkflowsPathNoSortAndOffset("1")).header(USER_ID_HEADER, USER_ID)
                                                                           .contentType(APPLICATION_JSON)).andDo(print())
-               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(1)));
+               .andExpect(status().isOk()).andExpect(jsonPath("$.items", hasSize(3)));
+        verify(workflowManagerMock).list(any(), requestSpecArg.capture());
+        assertRequestSpec(requestSpecArg.getValue(), DEFAULT_OFFSET, 1, Collections.emptyList());
     }
 
     @Test
@@ -227,24 +215,25 @@ public class WorkflowControllerTest {
         assertEquals("Workflow name must contain only letters, digits and underscores", response.getContentAsString());
     }
 
-    private Page<Workflow> createWorkflows(int numOfWorkflows) {
-        List<Workflow> workflows = new ArrayList<>(numOfWorkflows);
-        for (int i = 0; i < numOfWorkflows; i++) {
-            workflows.add(createWorkflow(i, true));
-        }
-        return new Page<>(workflows, new PagingRequest(0, 200), numOfWorkflows);
+    private void mockManagerList3() {
+        doReturn(new Page<>(Arrays.asList(
+                createWorkflow(1, true),
+                createWorkflow(2, true),
+                createWorkflow(3, true)),
+                new PagingRequest(DEFAULT_OFFSET, DEFAULT_LIMIT), 3))
+                .when(workflowManagerMock).list(any(), any());
     }
 
-    private Page<Workflow> createLimit2AndOffset1For5WorkflowList() {
-        List<Workflow> workflows = new ArrayList<>();
-        workflows.add(createWorkflow(2, true));
-        workflows.add(createWorkflow(3, true));
-        return new Page<>(workflows, new PagingRequest(1, 200), 5);
-    }
-
-    private Page<Workflow> createLimit1WorkflowList() {
-        List<Workflow> workflows = new ArrayList<>();
-        workflows.add(createWorkflow(0, true));
-        return new Page<>(workflows, new PagingRequest(0, 1), 1);
+    private static void assertRequestSpec(RequestSpec actual, int expectedOffset, int expectedLimit,
+            List<Sort> expectedSorts) {
+        assertEquals(Integer.valueOf(expectedOffset), actual.getPaging().getOffset());
+        assertEquals(Integer.valueOf(expectedLimit), actual.getPaging().getLimit());
+        if (expectedSorts.isEmpty()) {
+            assertEquals(expectedSorts, actual.getSorting().getSorts());
+        } else {
+            for (int i = 0; i < expectedSorts.size(); i++) {
+                assertEquals(expectedSorts.get(i), actual.getSorting().getSorts().get(i));
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/PagingTest.java b/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/PagingTest.java
new file mode 100644 (file)
index 0000000..c2b6cef
--- /dev/null
@@ -0,0 +1,71 @@
+package org.onap.sdc.workflow.api.types;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.onap.sdc.workflow.services.types.PagingConstants.MAX_LIMIT;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+public class PagingTest {
+
+    @InjectMocks
+    private Paging paging;
+
+    @Test
+    public void setOffsetNotNumber() {
+        paging.setOffset("aaa");
+        assertNull(paging.getOffset());
+    }
+
+    @Test
+    public void setOffsetNegative() {
+        paging.setOffset("-5");
+        assertNull(paging.getOffset());
+    }
+
+    @Test
+    public void setOffsetZero() {
+        paging.setOffset("0");
+        assertEquals(Integer.valueOf(0), paging.getOffset());
+    }
+
+    @Test
+    public void setOffsetPositive() {
+        paging.setOffset("8");
+        assertEquals(Integer.valueOf(8), paging.getOffset());
+    }
+
+    @Test
+    public void setLimitNotNumber() {
+        paging.setLimit("aaa");
+        assertNull(paging.getLimit());
+    }
+
+    @Test
+    public void setLimitNegative() {
+        paging.setLimit("-5");
+        assertNull(paging.getLimit());
+    }
+
+    @Test
+    public void setLimitZero() {
+        paging.setLimit("0");
+        assertNull(paging.getLimit());
+    }
+
+    @Test
+    public void setLimitPositive() {
+        paging.setLimit("8");
+        assertEquals(Integer.valueOf(8), paging.getLimit());
+    }
+
+    @Test
+    public void setLimitGreaterThanMax() {
+        paging.setLimit("7000");
+        assertEquals(Integer.valueOf(MAX_LIMIT), paging.getLimit());
+    }
+}
\ No newline at end of file
diff --git a/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/SortingTest.java b/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/SortingTest.java
new file mode 100644 (file)
index 0000000..2943644
--- /dev/null
@@ -0,0 +1,42 @@
+package org.onap.sdc.workflow.api.types;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.onap.sdc.workflow.services.types.Sort;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+public class SortingTest {
+
+    @InjectMocks
+    private Sorting sorting;
+
+    @Test
+    public void setSortInvalid() {
+        sorting.setSort("a");
+        assertEquals(Collections.emptyList(), sorting.getSorts());
+    }
+
+    @Test
+    public void setSortAsc() {
+        sorting.setSort("name:asc");
+        assertEquals(Collections.singletonList(new Sort("name", true)), sorting.getSorts());
+    }
+
+    @Test
+    public void setSortDesc() {
+        sorting.setSort("name:desc");
+        assertEquals(Collections.singletonList(new Sort("name", false)), sorting.getSorts());
+    }
+
+    @Test
+    public void setSortMoreThanOne() {
+        sorting.setSort("name:asc,date:desc");
+        assertEquals(Arrays.asList(new Sort("name", true), new Sort("date", false)), sorting.getSorts());
+    }
+}
\ No newline at end of file
diff --git a/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/VersionStatesFormatterTest.java b/workflow-designer-be/src/test/java/org/onap/sdc/workflow/api/types/VersionStatesFormatterTest.java
new file mode 100644 (file)
index 0000000..c57bd9c
--- /dev/null
@@ -0,0 +1,45 @@
+package org.onap.sdc.workflow.api.types;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.onap.sdc.workflow.persistence.types.WorkflowVersionState.CERTIFIED;
+import static org.onap.sdc.workflow.persistence.types.WorkflowVersionState.DRAFT;
+
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+public class VersionStatesFormatterTest {
+
+    @InjectMocks
+    private VersionStatesFormatter versionStateSet;
+
+    @Test
+    public void setVersionStateInvalid() {
+        versionStateSet.setVersionState("aaa");
+        assertNull(versionStateSet.getVersionStates());
+    }
+
+    @Test
+    public void setVersionStateDraft() {
+        versionStateSet.setVersionState("DRAFT");
+        assertEquals(Collections.singleton(DRAFT), versionStateSet.getVersionStates());
+    }
+
+    @Test
+    public void setVersionStateCertified() {
+        versionStateSet.setVersionState("CERTIFIED");
+        assertEquals(Collections.singleton(CERTIFIED), versionStateSet.getVersionStates());
+    }
+
+    @Test
+    public void setVersionStateBoth() {
+        versionStateSet.setVersionState("DRAFT,CERTIFIED");
+        assertEquals(Stream.of(DRAFT, CERTIFIED).collect(Collectors.toSet()), versionStateSet.getVersionStates());
+    }
+}
\ No newline at end of file
index 0797b64..de17a04 100644 (file)
@@ -8,6 +8,9 @@ import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.onap.sdc.workflow.TestUtil.createItem;
 import static org.onap.sdc.workflow.TestUtil.createWorkflow;
+import static org.onap.sdc.workflow.services.types.PagingConstants.DEFAULT_LIMIT;
+import static org.onap.sdc.workflow.services.types.PagingConstants.DEFAULT_OFFSET;
+import static org.onap.sdc.workflow.services.types.PagingConstants.MAX_LIMIT;
 import static org.openecomp.sdc.versioning.dao.types.VersionStatus.Certified;
 
 import java.util.ArrayList;
@@ -18,7 +21,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
@@ -30,6 +32,7 @@ import org.onap.sdc.workflow.services.exceptions.EntityNotFoundException;
 import org.onap.sdc.workflow.services.impl.mappers.VersionStateMapper;
 import org.onap.sdc.workflow.services.impl.mappers.WorkflowMapper;
 import org.onap.sdc.workflow.services.types.Page;
+import org.onap.sdc.workflow.services.types.Paging;
 import org.onap.sdc.workflow.services.types.PagingRequest;
 import org.onap.sdc.workflow.services.types.RequestSpec;
 import org.onap.sdc.workflow.services.types.Sort;
@@ -53,7 +56,7 @@ public class WorkflowManagerImplTest {
     static {
         List<Item> items = new ArrayList<>();
         List<Workflow> mappedWorkflows = new ArrayList<>();
-        for (int i = 1; i < 6; i++) {
+        for (int i = 0; i < 5; i++) {
             items.add(createItem(i, true, true));
             mappedWorkflows.add(createWorkflow(i, true));
         }
@@ -75,17 +78,18 @@ public class WorkflowManagerImplTest {
     @Test
     public void shouldReturnWorkflowVersionList() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Page<Workflow> workflows = workflowManager.list(null, createRequestSpec(20, 0, true, SORT_FIELD_NAME));
+        mockItemToWorkflowMaps();
+        RequestSpec requestSpec = createRequestSpec(0, 20, true);
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
 
         Map<String, Workflow> workflowById =
                 workflows.getItems().stream().collect(Collectors.toMap(Workflow::getId, Function.identity()));
         assertEquals(ITEMS.size(), workflows.getItems().size());
-        for (int i = 1; i < ITEMS.size() + 1; i++) {
+        for (int i = 0; i < ITEMS.size(); i++) {
             assertTrue(workflowById.containsKey(String.valueOf(i)));
         }
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
     }
 
     @Test
@@ -96,14 +100,17 @@ public class WorkflowManagerImplTest {
         doReturn(MAPPED_WORKFLOWS.get(0)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(0));
         doReturn(MAPPED_WORKFLOWS.get(2)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(2));
 
-        Page<Workflow> workflows = workflowManager.list(Collections.singleton(WorkflowVersionState.CERTIFIED),
-                createRequestSpec(20, 0, true, SORT_FIELD_NAME));
+        RequestSpec requestSpec = createRequestSpec(0, 20, true);
+        Page<Workflow> workflows =
+                workflowManager.list(Collections.singleton(WorkflowVersionState.CERTIFIED), requestSpec);
 
         Map<String, Workflow> workflowById =
                 workflows.getItems().stream().collect(Collectors.toMap(Workflow::getId, Function.identity()));
         assertEquals(2, workflows.getItems().size());
-        assertTrue(workflowById.containsKey("1"));
-        assertTrue(workflowById.containsKey("3"));
+        assertTrue(workflowById.containsKey("0"));
+        assertTrue(workflowById.containsKey("2"));
+
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(), 2);
     }
 
     @Test(expected = EntityNotFoundException.class)
@@ -162,81 +169,183 @@ public class WorkflowManagerImplTest {
     }
 
     @Test
-    public void shouldListAllWorkflowsWhenLimitAndOffsetAreValid() {
-        RequestSpec requestSpec = createRequestSpec(5, 0, true, SORT_FIELD_NAME);
+    public void listWhenRequestSpecIsNull() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, null);
+
+        assertEquals(ITEMS.size(), workflows.getItems().size());
+        assertPaging(workflows.getPaging(), DEFAULT_OFFSET, DEFAULT_LIMIT, ITEMS.size());
+
+        // verify sorted ascending by name
+        for (int i = DEFAULT_OFFSET; i < ITEMS.size(); i++) {
+            assertEquals("Workflow_" + i, workflows.getItems().get(i).getName());
         }
-        Assert.assertEquals(5, workflowManager.list(null, requestSpec).getItems().size());
     }
 
     @Test
-    public void shouldListLimitFilteredWorkflowsInFirstOffsetRange() {
-        RequestSpec requestSpec = createRequestSpec(3, 0, true, SORT_FIELD_NAME);
+    public void listWhenPagingIsNull() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Assert.assertEquals(3, workflowManager.list(null, requestSpec).getItems().size());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, new RequestSpec(null,
+                SortingRequest.builder().sort(new Sort(SORT_FIELD_NAME, true)).build()));
+
+        assertEquals(ITEMS.size(), workflows.getItems().size());
+        assertPaging(workflows.getPaging(), DEFAULT_OFFSET, DEFAULT_LIMIT, ITEMS.size());
     }
 
-/*    @Test
-    public void shouldListLimitFilteredWorkflowsInSecondOffsetRange() {
-        RequestSpec requestSpec = createRequestSpec(3, 1, true, SORT_FIELD_NAME);
+    @Test
+    public void listWhenOffsetAndLimitAreNull() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Assert.assertEquals(2, workflowManager.list(null, requestSpec).getItems().size());
-    }*/
+        mockItemToWorkflowMaps();
+        RequestSpec requestSpec = new RequestSpec(new PagingRequest(-2, -8),
+                SortingRequest.builder().sort(new Sort(SORT_FIELD_NAME, true)).build());
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+
+        assertEquals(ITEMS.size(), workflows.getItems().size());
+        assertPaging(workflows.getPaging(), DEFAULT_OFFSET, DEFAULT_LIMIT, ITEMS.size());
+    }
 
     @Test
-    public void shouldListAllWorkflowsWhenLimitGreaterThanTotalRecordsAndOffsetInRange() {
-        RequestSpec requestSpec = createRequestSpec(10, 0, true, SORT_FIELD_NAME);
+    public void listWhenSortingIsNull() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Assert.assertEquals(5, workflowManager.list(null, requestSpec).getItems().size());
+        mockItemToWorkflowMaps();
+        RequestSpec requestSpec = new RequestSpec(new PagingRequest(2, 8), null);
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+
+        assertEquals(3, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+
+        // verify sorted ascending by name
+        assertEquals("Workflow_2", workflows.getItems().get(0).getName());
+        assertEquals("Workflow_3", workflows.getItems().get(1).getName());
+        assertEquals("Workflow_4", workflows.getItems().get(2).getName());
     }
 
     @Test
-    public void shouldNotListWorkflowsIfOffsetGreaterThanTotalRecords() {
-        RequestSpec requestSpec = createRequestSpec(3, 6, true, SORT_FIELD_NAME);
+    public void listWhenSortingIsEmpty() {
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Assert.assertEquals(0, workflowManager.list(null, requestSpec).getItems().size());
+        mockItemToWorkflowMaps();
+        RequestSpec requestSpec = new RequestSpec(new PagingRequest(2, 8), SortingRequest.builder().build());
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+
+        assertEquals(3, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+
+        // verify sorted ascending by name
+        assertEquals("Workflow_2", workflows.getItems().get(0).getName());
+        assertEquals("Workflow_3", workflows.getItems().get(1).getName());
+        assertEquals("Workflow_4", workflows.getItems().get(2).getName());
     }
 
     @Test
-    public void shouldNotListWorkflowsBothLimitAndOffsetGreaterThanTotalRecords() {
-        RequestSpec requestSpec = createRequestSpec(10, 10, true, SORT_FIELD_NAME);
+    public void listWhenRequestSpecIsValid() {
+        RequestSpec requestSpec = createRequestSpec(0, 5, true);
         doReturn(ITEMS).when(itemManagerMock).list(any());
-        for (int i = 0; i < ITEMS.size(); i++) {
-            doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
-        }
-        Assert.assertEquals(0, workflowManager.list(null, requestSpec).getItems().size());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+
+        assertEquals(5, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+    @Test
+    public void listWhenLimitIsLessThanTotal() {
+        RequestSpec requestSpec = createRequestSpec(0, 3, true);
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(3, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+
+    @Test
+    public void listWhenOffsetIsNotFirst() {
+        RequestSpec requestSpec = createRequestSpec(3, 1, true);
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(1, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+    @Test
+    public void listWhenLimitIsMoreThanTotal() {
+        RequestSpec requestSpec = createRequestSpec(0, 10, true);
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(5, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+    @Test
+    public void listWhenOffsetIsMoreThanTotal() {
+        RequestSpec requestSpec = createRequestSpec(6, 3, true);
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(0, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+    @Test
+    public void listWhenOffsetIsMoreThanMax() {
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        RequestSpec requestSpec = createRequestSpec(0, 5555, true);
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+
+        assertEquals(ITEMS.size(), workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), MAX_LIMIT, ITEMS.size());
     }
 
-/*    @Test
-    public void shouldListLimitOffsetAppliedWorkflowsSortedInDescOrder() {
-        RequestSpec requestSpec = createRequestSpec(2, 1, false, SORT_FIELD_NAME);
+    @Test
+    public void listWhenOffsetAndLimitAreMoreThanTotal() {
+        RequestSpec requestSpec = createRequestSpec(10, 10, true);
         doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(0, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+    }
+
+    @Test
+    public void listWhenSortedDesc() {
+        RequestSpec requestSpec = createRequestSpec(2, 1, false);
+        doReturn(ITEMS).when(itemManagerMock).list(any());
+        mockItemToWorkflowMaps();
+        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
+        assertEquals(1, workflows.getItems().size());
+        assertPaging(workflows.getPaging(), requestSpec.getPaging().getOffset(), requestSpec.getPaging().getLimit(),
+                ITEMS.size());
+        Iterator<Workflow> workflowIterator = workflows.getItems().iterator();
+        assertEquals("Workflow_2", workflowIterator.next().getName());
+    }
+
+    private void mockItemToWorkflowMaps() {
         for (int i = 0; i < ITEMS.size(); i++) {
             doReturn(MAPPED_WORKFLOWS.get(i)).when(workflowMapperMock).itemToWorkflow(ITEMS.get(i));
         }
-        Page<Workflow> workflows = workflowManager.list(null, requestSpec);
-        Assert.assertEquals(2, workflows.getItems().size());
-        Iterator<Workflow> workflowIterator = workflows.getItems().iterator();
-        Assert.assertEquals("Workflow_3", workflowIterator.next().getName());
-        Assert.assertEquals("Workflow_2", workflowIterator.next().getName());
-    }*/
+    }
 
-    private RequestSpec createRequestSpec(int limit, int offset, boolean isAscending, String sortField) {
+    private static RequestSpec createRequestSpec(int offset, int limit, boolean isAscending) {
         return new RequestSpec(new PagingRequest(offset, limit),
-                SortingRequest.builder().sort(new Sort(sortField, isAscending)).build());
+                SortingRequest.builder().sort(new Sort(SORT_FIELD_NAME, isAscending)).build());
+    }
+
+    private static void assertPaging(Paging paging, int expectedOffset, int expectedLimit, int expectedTotal) {
+        assertEquals(expectedOffset, paging.getOffset());
+        assertEquals(expectedLimit, paging.getLimit());
+        assertEquals(expectedTotal, paging.getTotal());
     }
 }
\ No newline at end of file
index 38ab8a4..be6fe04 100644 (file)
@@ -219,6 +219,9 @@ public class WorkflowVersionManagerImplTest {
     public void shouldUploadArtifact() {
         Version version = new Version(VERSION1_ID);
         version.setStatus(VersionStatus.Draft);
+        VersionState versionState = new VersionState();
+        versionState.setDirty(false);
+        version.setState(versionState);
         doReturn(version).when(versioningManagerMock).get(eq(ITEM1_ID), eqVersion(VERSION1_ID));
         doReturn(DRAFT).when(versionStateMapperMock).versionStatusToWorkflowVersionState(version.getStatus());
 
index c3a6f3b..09d2d0a 100644 (file)
@@ -2,9 +2,7 @@
 <html>
 <head>
     <base href="/">
-    <meta charset="utf-8">
-       <link rel="stylesheet" href="https://unpkg.com/bpmn-js@2.1.0/dist/assets/diagram-js.css" />
-       <link rel="stylesheet" href="https://unpkg.com/bpmn-js@2.1.0/dist/assets/bpmn-font/css/bpmn.css" />
+    <meta charset="utf-8">     
     <title>SDC Workflow App</title>
 </head>
 <body>
index 529d909..bd224ba 100644 (file)
@@ -23,6 +23,7 @@
                "dateformat": "^3.0.3",
                "enzyme": "^3.3.0",
                "enzyme-adapter-react-16": "^1.1.1",
+               "file-saver": "^1.3.8",
                "http-proxy-middleware": "^0.17.4",
                "lodash": "^3.0.1",
                "md5": "^2.2.1",
index 7ab294a..a159a4b 100644 (file)
@@ -4,12 +4,43 @@
 
        .bpmn-container {
                flex-basis: 100%;
-               height: 100%;
+               flex-grow: 1
        }
-
-       .properties-panel {
-               &, .bpp-properties-panel {
-                       height: 100%;
+       .bpmn-sidebar {
+               height: 100%;
+               .properties-panel {
+                       &, .bpp-properties-panel {
+                               height: 100%;
+                       }
+               }
+               .composition-buttons {
+                       position: fixed;
+                       background-color: #fafafa;
+                       left: 265px;
+                       bottom: 46px;
+                       border: 1px solid lightgray;
+                       width: 189px;
+                       display: flex;
+                       flex-direction: row;
+                       justify-content: space-around;
+                       height: 57px;
+                       align-items: center;
+                       padding: 10px;
+                       .divider {
+                               height: 35px;
+                       border: 1px solid $silver;
+                       }
+                       .diagram-btn {
+                               
+                               &:hover {
+                                       fill: $blue;
+                                       cursor: pointer;
+                               }
+                               .svg-icon {
+                                       width: 25px;
+                                   height: 23px;
+                               }
+                       }
                }
        }
 }
index 95828ae..4927856 100644 (file)
@@ -1,3 +1,5 @@
+@import '../../node_modules/bpmn-js/dist/assets/diagram-js.css';
+@import '../../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
 @import 'common';
 @import '../../node_modules/sdc-ui/lib/css/style.css';
 @import 'components';
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js
new file mode 100644 (file)
index 0000000..d2c273c
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { connect } from 'react-redux';
+import { I18n } from 'react-redux-i18n';
+import { updateComposition } from './compositionActions';
+import CompositionView from './CompositionView';
+import { showErrorModalAction } from '../../../shared/modal/modalWrapperActions';
+import { getComposition } from './compositionSelectors';
+import { getWorkflowName } from '../../workflow/workflowSelectors';
+
+function mapStateToProps(state) {
+    return {
+        composition: getComposition(state),
+        name: getWorkflowName(state)
+    };
+}
+
+function mapDispatchToProps(dispatch) {
+    return {
+        compositionUpdate: composition =>
+            dispatch(updateComposition(composition)),
+        showErrorModal: msg =>
+            dispatch(
+                showErrorModalAction({
+                    title: I18n.t('workflow.composition.bpmnError'),
+                    body: msg,
+                    withButtons: true,
+                    closeButtonText: 'Ok'
+                })
+            )
+    };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CompositionView);
index ba0351b..d549456 100644 (file)
@@ -5,7 +5,7 @@
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
- *      http://www.apache.org/licenses/LICENSE-2.0
+*http://www.apache.org/licenses/LICENSE-2.0
 *
  * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * limitations under the License.
 */
 import React, { Component } from 'react';
-import { connect } from 'react-redux';
-
-import BpmnModeler from 'bpmn-js/lib/Modeler';
-// import propertiesPanelModule from 'bpmn-js-properties-panel';
+import fileSaver from 'file-saver';
+import CustomModeler from './custom-modeler';
+import propertiesPanelModule from 'bpmn-js-properties-panel';
 import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
-import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda';
-
+import camundaModuleDescriptor from 'camunda-bpmn-moddle/resources/camunda';
 import newDiagramXML from './newDiagram.bpmn';
+import PropTypes from 'prop-types';
+import CompositionButtons from './components/CompositionButtonsPanel';
 
 class CompositionView extends Component {
+    static propTypes = {
+        compositionUpdate: PropTypes.func,
+        showErrorModal: PropTypes.func,
+        composition: PropTypes.string,
+        name: PropTypes.string
+    };
     constructor() {
         super();
         this.generatedId = 'bpmn-container' + Date.now();
+        this.fileInput = React.createRef();
+        this.state = {
+            diagram: false
+        };
     }
 
     componentDidMount() {
-        this.modeler = new BpmnModeler({
+        const { composition } = this.props;
+
+        this.modeler = new CustomModeler({
             propertiesPanel: {
-                parent: '#js-properties-navigationSideBar'
+                parent: '#js-properties-panel'
             },
             additionalModules: [
-                //TODO:: need to fix
-                // propertiesPanelModule,
+                propertiesPanelModule,
                 propertiesProviderModule
             ],
             moddleExtensions: {
-                camunda: camundaModdleDescriptor
+                camunda: camundaModuleDescriptor
             }
         });
         window.modeler = this.modeler;
         this.modeler.attachTo('#' + this.generatedId);
-        // let diagramXML =
-        //     '<?xml version="1.0" encoding="UTF-8"?>\r\n<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"\r\n             xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"\r\n             xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"\r\n             xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"\r\n             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\r\n             expressionLanguage="http://www.w3.org/1999/XPath"\r\n             typeLanguage="http://www.w3.org/2001/XMLSchema"\r\n             targetNamespace=""\r\n             xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">\r\n<collaboration id="sid-c0e745ff-361e-4afb-8c8d-2a1fc32b1424">\r\n    <participant id="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F" name="Customer" processRef="sid-C3803939-0872-457F-8336-EAE484DC4A04">\r\n    </participant>\r\n</collaboration>\r\n<process id="sid-C3803939-0872-457F-8336-EAE484DC4A04" isClosed="false" isExecutable="false" name="Customer" processType="None">\r\n    <extensionElements/>\r\n    <laneSet id="sid-b167d0d7-e761-4636-9200-76b7f0e8e83a">\r\n        <lane id="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254">\r\n            <flowNodeRef>sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138</flowNodeRef>\r\n            <flowNodeRef>sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26</flowNodeRef>\r\n            <flowNodeRef>SCAN_OK</flowNodeRef>\r\n            <flowNodeRef>sid-E49425CF-8287-4798-B622-D2A7D78EF00B</flowNodeRef>\r\n            <flowNodeRef>sid-E433566C-2289-4BEB-A19C-1697048900D2</flowNodeRef>\r\n            <flowNodeRef>sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9</flowNodeRef>\r\n        </lane>\r\n    </laneSet>\r\n    <startEvent id="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" name="Notices&#10;QR code">\r\n        <outgoing>sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD</outgoing>\r\n    </startEvent>\r\n    <task completionQuantity="1" id="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" isForCompensation="false" name="Scan QR code" startQuantity="1">\r\n        <incoming>sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D</incoming>\r\n        <outgoing>sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A</outgoing>\r\n    </task>\r\n    <exclusiveGateway gatewayDirection="Diverging" id="SCAN_OK" name="Scan successful?&#10;">\r\n        <incoming>sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A</incoming>\r\n        <outgoing>sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB</outgoing>\r\n        <outgoing>sid-337A23B9-A923-4CCE-B613-3E247B773CCE</outgoing>\r\n    </exclusiveGateway>\r\n    <task completionQuantity="1" id="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" isForCompensation="false" name="Open product information in mobile  app" startQuantity="1">\r\n        <incoming>sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB</incoming>\r\n        <outgoing>sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C</outgoing>\r\n    </task>\r\n    <endEvent id="sid-E433566C-2289-4BEB-A19C-1697048900D2" name="Is informed">\r\n        <incoming>sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C</incoming>\r\n    </endEvent>\r\n    <exclusiveGateway gatewayDirection="Converging" id="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9">\r\n        <incoming>sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD</incoming>\r\n        <incoming>sid-337A23B9-A923-4CCE-B613-3E247B773CCE</incoming>\r\n        <outgoing>sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D</outgoing>\r\n    </exclusiveGateway>\r\n    <sequenceFlow id="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD" sourceRef="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" targetRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9"/>\r\n    <sequenceFlow id="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A" sourceRef="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" targetRef="SCAN_OK"/>\r\n    <sequenceFlow id="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C" sourceRef="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" targetRef="sid-E433566C-2289-4BEB-A19C-1697048900D2"/>\r\n    <sequenceFlow id="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB" name="No" sourceRef="SCAN_OK" targetRef="sid-E49425CF-8287-4798-B622-D2A7D78EF00B"/>\r\n    <sequenceFlow id="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D" sourceRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9" targetRef="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26"/>\r\n    <sequenceFlow id="sid-337A23B9-A923-4CCE-B613-3E247B773CCE" name="Yes" sourceRef="SCAN_OK" targetRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9"/>\r\n</process>\r\n<bpmndi:BPMNDiagram id="sid-74620812-92c4-44e5-949c-aa47393d3830">\r\n    <bpmndi:BPMNPlane bpmnElement="sid-c0e745ff-361e-4afb-8c8d-2a1fc32b1424" id="sid-cdcae759-2af7-4a6d-bd02-53f3352a731d">\r\n        <bpmndi:BPMNShape bpmnElement="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F" id="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F_gui" isHorizontal="true">\r\n            <omgdc:Bounds height="500.0" width="933.0" x="42.5" y="75.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n                <omgdc:Bounds height="59.142852783203125" width="12.000000000000014" x="47.49999999999999" y="170.42857360839844"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254" id="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254_gui" isHorizontal="true">\r\n            <omgdc:Bounds height="250.0" width="903.0" x="72.5" y="75.0"/>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" id="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138_gui">\r\n            <omgdc:Bounds height="30.0" width="30.0" x="150.0" y="165.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n                <omgdc:Bounds height="22.0" width="46.35714340209961" x="141.8214282989502" y="197.0"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" id="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26_gui">\r\n            <omgdc:Bounds height="80.0" width="100.0" x="352.5" y="140.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n                <omgdc:Bounds height="12.0" width="84.0" x="360.5" y="172.0"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="SCAN_OK" id="SCAN_OK_gui" isMarkerVisible="true">\r\n            <omgdc:Bounds height="40.0" width="40.0" x="550.0" y="160.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n                <omgdc:Bounds height="12.0" width="102.0" x="521.0" y="127.0"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" id="sid-E49425CF-8287-4798-B622-D2A7D78EF00B_gui">\r\n            <omgdc:Bounds height="80.0" width="100.0" x="687.5" y="140.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n                <omgdc:Bounds height="36.0" width="83.14285278320312" x="695.9285736083984" y="162.0"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-E433566C-2289-4BEB-A19C-1697048900D2" id="sid-E433566C-2289-4BEB-A19C-1697048900D2_gui">\r\n            <omgdc:Bounds height="28.0" width="28.0" x="865.0" y="166.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n                <omgdc:Bounds height="11.0" width="62.857147216796875" x="847.5714263916016" y="196.0"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNShape bpmnElement="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9" id="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9_gui" isMarkerVisible="true">\r\n            <omgdc:Bounds height="40.0" width="40.0" x="240.0" y="160.0"/>\r\n        </bpmndi:BPMNShape>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A" id="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A_gui">\r\n            <omgdi:waypoint x="452.5" y="180"/>\r\n            <omgdi:waypoint x="550.0" y="180"/>\r\n        </bpmndi:BPMNEdge>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB" id="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB_gui">\r\n            <omgdi:waypoint x="590.0" y="180"/>\r\n            <omgdi:waypoint x="687.5" y="180"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n                <omgdc:Bounds height="12.048704338048935" width="16.32155963195521" x="597.8850936986571" y="155"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNEdge>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD" id="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD_gui">\r\n            <omgdi:waypoint x="180.0" y="180"/>\r\n            <omgdi:waypoint x="240.0" y="180"/>\r\n        </bpmndi:BPMNEdge>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D" id="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D_gui">\r\n            <omgdi:waypoint x="280.0" y="180"/>\r\n            <omgdi:waypoint x="352.5" y="180"/>\r\n        </bpmndi:BPMNEdge>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C" id="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C_gui">\r\n            <omgdi:waypoint x="787.5" y="180.0"/>\r\n            <omgdi:waypoint x="865.0" y="180.0"/>\r\n        </bpmndi:BPMNEdge>\r\n        <bpmndi:BPMNEdge bpmnElement="sid-337A23B9-A923-4CCE-B613-3E247B773CCE" id="sid-337A23B9-A923-4CCE-B613-3E247B773CCE_gui">\r\n            <omgdi:waypoint x="570.5" y="200.0"/>\r\n            <omgdi:waypoint x="570.5" y="269.0"/>\r\n            <omgdi:waypoint x="260.5" y="269.0"/>\r\n            <omgdi:waypoint x="260.5" y="200.0"/>\r\n            <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n                <omgdc:Bounds height="21.4285888671875" width="12.0" x="550" y="205"/>\r\n            </bpmndi:BPMNLabel>\r\n        </bpmndi:BPMNEdge>\r\n    </bpmndi:BPMNPlane>\r\n    <bpmndi:BPMNLabelStyle id="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n        <omgdc:Font isBold="false" isItalic="false" isStrikeThrough="false" isUnderline="false" name="Arial" size="11.0"/>\r\n    </bpmndi:BPMNLabelStyle>\r\n    <bpmndi:BPMNLabelStyle id="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n        <omgdc:Font isBold="false" isItalic="false" isStrikeThrough="false" isUnderline="false" name="Arial" size="12.0"/>\r\n    </bpmndi:BPMNLabelStyle>\r\n</bpmndi:BPMNDiagram>\r\n</definitions>\r\n\r\n';
-        this.importXML(newDiagramXML);
+        this.setDiagram(composition ? composition : newDiagramXML);
+        var eventBus = this.modeler.get('eventBus');
+        eventBus.on('element.out', () => {
+            this.exportDiagramToStore();
+        });
     }
 
-    importXML(xml) {
+    setDiagram = diagram => {
+        this.setState(
+            {
+                diagram
+            },
+            this.importXML
+        );
+    };
+
+    importXML = () => {
+        const { diagram } = this.state;
         let modeler = this.modeler;
-        this.modeler.importXML(xml, function(err) {
+        this.modeler.importXML(diagram, err => {
             if (err) {
-                return console.error('could not import BPMN file');
+                //TDOD add i18n
+                return this.props.showErrorModal('could not import diagram');
             }
             let canvas = modeler.get('canvas');
             canvas.zoom('fit-viewport');
         });
-    }
+    };
 
-    exportDiagram() {
+    exportDiagramToStore = () => {
         this.modeler.saveXML({ format: true }, (err, xml) => {
             if (err) {
-                return console.error('could not save diagram');
+                //TODO   add i18n
+                return this.props.showErrorModal('could not save diagram');
             }
-            console.log('Exported diagram: ', xml);
+            return this.props.compositionUpdate(xml);
         });
-    }
+    };
+
+    exportDiagram = () => {
+        const { name, showErrorModal } = this.props;
+        this.modeler.saveXML({ format: true }, (err, xml) => {
+            if (err) {
+                //TODO add i18n
+                return showErrorModal('could not save diagram');
+            }
+            const blob = new Blob([xml], { type: 'text/html;charset=utf-8' });
+            fileSaver.saveAs(blob, `${name}-diagram.bpmn`);
+        });
+    };
+
+    loadNewDiagram = () => {
+        this.setDiagram(newDiagramXML);
+    };
+
+    uploadDiagram = () => {
+        this.fileInput.current.click();
+    };
+
+    handleFileInputChange = filesList => {
+        const file = filesList[0];
+        const reader = new FileReader();
+        reader.onloadend = event => {
+            var xml = event.target.result;
+            this.setDiagram(xml);
+            this.fileInput.value = '';
+        };
+        reader.readAsText(file);
+    };
 
     render() {
         return (
             <div className="composition-view content">
-                <div className="bpmn-container" id={this.generatedId} />
-                <div className="properties-panel" id="js-properties-panel" />
+                <input
+                    ref={this.fileInput}
+                    onChange={e => this.handleFileInputChange(e.target.files)}
+                    id="file-input"
+                    accept=".bpmn, .xml"
+                    type="file"
+                    name="file-input"
+                    style={{ display: 'none' }}
+                />
+                <div
+                    onBlur={() => {
+                        this.exportDiagramToStore();
+                    }}
+                    className="bpmn-container"
+                    id={this.generatedId}
+                />
+                <div className="bpmn-sidebar">
+                    <div
+                        className="properties-panel"
+                        id="js-properties-panel"
+                    />
+                    <CompositionButtons
+                        onClean={this.loadNewDiagram}
+                        onDownload={this.exportDiagram}
+                        onUpload={this.uploadDiagram}
+                    />
+                </div>
             </div>
         );
     }
 }
 
-function mapStateToProps() {
-    return {};
-}
-
-function mapDispatchToProps() {
-    return {};
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(CompositionView);
+export default CompositionView;
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js
new file mode 100644 (file)
index 0000000..2fc6618
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import React from 'react';
+import PropTypes from 'prop-types';
+import SVGIcon from 'sdc-ui/lib/react/SVGIcon';
+
+const CompositionButton = ({ onClick, name, title }) => (
+    <div onClick={onClick} className="diagram-btn">
+        <SVGIcon title={title} name={name} />
+    </div>
+);
+
+CompositionButton.propTypes = {
+    onClick: PropTypes.func,
+    className: PropTypes.string,
+    name: PropTypes.string,
+    title: PropTypes.string
+};
+
+export default CompositionButton;
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js
new file mode 100644 (file)
index 0000000..add6490
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import React from 'react';
+import PropTypes from 'prop-types';
+import CompositionButton from './CompositionButton';
+
+const Divider = () => <div className="divider" />;
+
+const CompositionButtons = ({ onClean, onUpload, onDownload }) => (
+    <div className="composition-buttons">
+        <CompositionButton
+            data-test-id="composition-clear-btn"
+            onClick={onClean}
+            name="trashO"
+            title="clear"
+        />
+        <Divider />
+        <CompositionButton
+            data-test-id="composition-download-btn"
+            onClick={onDownload}
+            name="download"
+            title="download"
+        />
+        <Divider />
+        <CompositionButton
+            data-test-id="composition-download-upload"
+            onClick={onUpload}
+            name="upload"
+            title="upload"
+        />
+    </div>
+);
+
+CompositionButtons.propTypes = {
+    onClean: PropTypes.func,
+    onUpload: PropTypes.func,
+    onDownload: PropTypes.func
+};
+export default CompositionButtons;
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js
new file mode 100644 (file)
index 0000000..3f1755d
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*http://www.apache.org/licenses/LICENSE-2.0
+*
+ * Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { SET_COMPOSITION } from './compositionConstants';
+
+export const updateComposition = payload => ({
+    type: SET_COMPOSITION,
+    payload
+});
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js
new file mode 100644 (file)
index 0000000..74cab0c
--- /dev/null
@@ -0,0 +1,16 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*http://www.apache.org/licenses/LICENSE-2.0
+*
+ * Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+export const SET_COMPOSITION = 'composition/SET_COMPOSITION';
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js
new file mode 100644 (file)
index 0000000..9c70736
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*http://www.apache.org/licenses/LICENSE-2.0
+*
+ * Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { SET_COMPOSITION } from './compositionConstants';
+
+export default (state = {}, action) => {
+    switch (action.type) {
+        case SET_COMPOSITION:
+            return {
+                diagram: action.payload
+            };
+        default:
+            return state;
+    }
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js
new file mode 100644 (file)
index 0000000..7e28ca6
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+* Copyright © 2018 European Support Limited
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*http://www.apache.org/licenses/LICENSE-2.0
+*
+ * Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+export const getComposition = state =>
+    state && state.currentVersion && state.currentVersion.composition.diagram;
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js
new file mode 100644 (file)
index 0000000..0f2ba52
--- /dev/null
@@ -0,0 +1,43 @@
+import inherits from 'inherits';\r
+\r
+import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';\r
+\r
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';\r
+\r
+import { assign, bind } from 'min-dash';\r
+\r
+export default function CustomContextPadProvider(injector, connect, translate) {\r
+    injector.invoke(ContextPadProvider, this);\r
+\r
+    var cached = bind(this.getContextPadEntries, this);\r
+\r
+    this.getContextPadEntries = function(element) {\r
+        var actions = cached(element);\r
+\r
+        var businessObject = element.businessObject;\r
+\r
+        function startConnect(event, element, autoActivate) {\r
+            connect.start(event, element, autoActivate);\r
+        }\r
+\r
+        if (isAny(businessObject, ['custom:triangle', 'custom:circle'])) {\r
+            assign(actions, {\r
+                connect: {\r
+                    group: 'connect',\r
+                    className: 'bpmn-icon-connection-multi',\r
+                    title: translate('Connect using custom connection'),\r
+                    action: {\r
+                        click: startConnect,\r
+                        dragstart: startConnect\r
+                    }\r
+                }\r
+            });\r
+        }\r
+\r
+        return actions;\r
+    };\r
+}\r
+\r
+inherits(CustomContextPadProvider, ContextPadProvider);\r
+\r
+CustomContextPadProvider.$inject = ['injector', 'connect', 'translate'];\r
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js
new file mode 100644 (file)
index 0000000..01d4d27
--- /dev/null
@@ -0,0 +1,101 @@
+import { assign } from 'min-dash';
+
+import inherits from 'inherits';
+
+import BpmnElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory';
+import { DEFAULT_LABEL_SIZE } from 'bpmn-js/lib/util/LabelUtil';
+
+/**
+ * A custom factory that knows how to create BPMN _and_ custom elements.
+ */
+export default function CustomElementFactory(bpmnFactory, moddle) {
+    BpmnElementFactory.call(this, bpmnFactory, moddle);
+
+    var self = this;
+
+    /**
+     * Create a diagram-js element with the given type (any of shape, connection, label).
+     *
+     * @param  {String} elementType
+     * @param  {Object} attrs
+     *
+     * @return {djs.model.Base}
+     */
+    this.create = function(elementType, attrs) {
+        var type = attrs.type;
+
+        if (elementType === 'label') {
+            return self.baseCreate(
+                elementType,
+                assign({ type: 'label' }, DEFAULT_LABEL_SIZE, attrs)
+            );
+        }
+
+        // add type to businessObject if custom
+        if (/^custom:/.test(type)) {
+            if (!attrs.businessObject) {
+                attrs.businessObject = {
+                    type: type
+                };
+
+                if (attrs.id) {
+                    assign(attrs.businessObject, {
+                        id: attrs.id
+                    });
+                }
+            }
+
+            // add width and height if shape
+            if (!/:connection$/.test(type)) {
+                assign(attrs, self._getCustomElementSize(type));
+            }
+
+            if (!('$instanceOf' in attrs.businessObject)) {
+                // ensure we can use ModelUtil#is for type checks
+                Object.defineProperty(attrs.businessObject, '$instanceOf', {
+                    value: function(type) {
+                        return this.type === type;
+                    }
+                });
+            }
+
+            return self.baseCreate(elementType, attrs);
+        }
+
+        return self.createBpmnElement(elementType, attrs);
+    };
+}
+
+inherits(CustomElementFactory, BpmnElementFactory);
+
+CustomElementFactory.$inject = ['bpmnFactory', 'moddle'];
+
+/**
+ * Returns the default size of custom shapes.
+ *
+ * The following example shows an interface on how
+ * to setup the custom shapes's dimensions.
+ *
+ * @example
+ *
+ * var shapes = {
+ *   triangle: { width: 40, height: 40 },
+ *   rectangle: { width: 100, height: 20 }
+ * };
+ *
+ * return shapes[type];
+ *
+ *
+ * @param {String} type
+ *
+ * @return {Dimensions} a {width, height} object representing the size of the element
+ */
+CustomElementFactory.prototype._getCustomElementSize = function(type) {
+    var shapes = {
+        __default: { width: 100, height: 80 },
+        'custom:triangle': { width: 40, height: 40 },
+        'custom:circle': { width: 140, height: 140 }
+    };
+
+    return shapes[type] || shapes.__default;
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js
new file mode 100644 (file)
index 0000000..a8adb2f
--- /dev/null
@@ -0,0 +1,151 @@
+import { assign } from 'min-dash';
+
+/**
+ * A palette that allows you to create BPMN _and_ custom elements.
+ */
+export default function PaletteProvider(
+    palette,
+    create,
+    elementFactory,
+    spaceTool,
+    lassoTool,
+    handTool,
+    globalConnect,
+    translate
+) {
+    this._create = create;
+    this._elementFactory = elementFactory;
+    this._spaceTool = spaceTool;
+    this._lassoTool = lassoTool;
+    this._handTool = handTool;
+    this._globalConnect = globalConnect;
+    this._translate = translate;
+
+    palette.registerProvider(this);
+}
+
+PaletteProvider.$inject = [
+    'palette',
+    'create',
+    'elementFactory',
+    'spaceTool',
+    'lassoTool',
+    'handTool',
+    'globalConnect',
+    'translate'
+];
+
+PaletteProvider.prototype.getPaletteEntries = function() {
+    var actions = {},
+        create = this._create,
+        elementFactory = this._elementFactory,
+        spaceTool = this._spaceTool,
+        lassoTool = this._lassoTool,
+        handTool = this._handTool,
+        globalConnect = this._globalConnect,
+        translate = this._translate;
+
+    function createAction(type, group, className, title, options) {
+        function createListener(event) {
+            var shape = elementFactory.createShape(
+                assign({ type: type }, options)
+            );
+
+            if (options) {
+                shape.businessObject.di.isExpanded = options.isExpanded;
+            }
+
+            create.start(event, shape);
+        }
+
+        var shortType = type.replace(/^bpmn:/, '');
+
+        return {
+            group: group,
+            className: className,
+            title: title || 'Create ' + shortType,
+            action: {
+                dragstart: createListener,
+                click: createListener
+            }
+        };
+    }
+
+    assign(actions, {
+        'hand-tool': {
+            group: 'tools',
+            className: 'bpmn-icon-hand-tool',
+            title: translate('Activate the hand tool'),
+            action: {
+                click: function(event) {
+                    handTool.activateHand(event);
+                }
+            }
+        },
+        'lasso-tool': {
+            group: 'tools',
+            className: 'bpmn-icon-lasso-tool',
+            title: translate('Activate the lasso tool'),
+            action: {
+                click: function(event) {
+                    lassoTool.activateSelection(event);
+                }
+            }
+        },
+        'space-tool': {
+            group: 'tools',
+            className: 'bpmn-icon-space-tool',
+            title: translate('Activate the create/remove space tool'),
+            action: {
+                click: function(event) {
+                    spaceTool.activateSelection(event);
+                }
+            }
+        },
+        'global-connect-tool': {
+            group: 'tools',
+            className: 'bpmn-icon-connection-multi',
+            title: translate('Activate the global connect tool'),
+            action: {
+                click: function(event) {
+                    globalConnect.toggle(event);
+                }
+            }
+        },
+        'tool-separator': {
+            group: 'tools',
+            separator: true
+        },
+        'create.start-event': createAction(
+            'bpmn:StartEvent',
+            'event',
+            'bpmn-icon-start-event-none'
+        ),
+        'create.intermediate-event': createAction(
+            'bpmn:IntermediateThrowEvent',
+            'event',
+            'bpmn-icon-intermediate-event-none',
+            translate('Create Intermediate/Boundary Event')
+        ),
+        'create.end-event': createAction(
+            'bpmn:EndEvent',
+            'event',
+            'bpmn-icon-end-event-none'
+        ),
+        'create.exclusive-gateway': createAction(
+            'bpmn:ExclusiveGateway',
+            'gateway',
+            'bpmn-icon-gateway-none',
+            translate('Create Gateway')
+        ),
+        'create.task': createAction('bpmn:Task', 'activity', 'bpmn-icon-task'),
+        'create.subprocess-expanded': createAction(
+            'bpmn:SubProcess',
+            'activity',
+            'bpmn-icon-subprocess-expanded',
+            translate('Create expanded SubProcess'),
+            { isExpanded: true }
+        )
+    });
+    return actions;
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js
new file mode 100644 (file)
index 0000000..f397fed
--- /dev/null
@@ -0,0 +1,176 @@
+import inherits from 'inherits';
+
+import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
+
+import { componentsToPath, createLine } from 'diagram-js/lib/util/RenderUtil';
+
+import {
+    append as svgAppend,
+    attr as svgAttr,
+    create as svgCreate
+} from 'tiny-svg';
+
+/**
+ * A renderer that knows how to render custom elements.
+ */
+export default function CustomRenderer(eventBus, styles) {
+    BaseRenderer.call(this, eventBus, 2000);
+
+    var computeStyle = styles.computeStyle;
+
+    this.drawTriangle = function(p, side) {
+        var halfSide = side / 2,
+            points,
+            attrs;
+
+        points = [halfSide, 0, side, side, 0, side];
+
+        attrs = computeStyle(attrs, {
+            stroke: '#3CAA82',
+            strokeWidth: 2,
+            fill: '#3CAA82'
+        });
+
+        var polygon = svgCreate('polygon');
+
+        svgAttr(polygon, {
+            points: points
+        });
+
+        svgAttr(polygon, attrs);
+
+        svgAppend(p, polygon);
+
+        return polygon;
+    };
+
+    this.getTrianglePath = function(element) {
+        var x = element.x,
+            y = element.y,
+            width = element.width,
+            height = element.height;
+
+        var trianglePath = [
+            ['M', x + width / 2, y],
+            ['l', width / 2, height],
+            ['l', -width, 0],
+            ['z']
+        ];
+
+        return componentsToPath(trianglePath);
+    };
+
+    this.drawCircle = function(p, width, height) {
+        var cx = width / 2,
+            cy = height / 2;
+
+        var attrs = computeStyle(attrs, {
+            stroke: '#4488aa',
+            strokeWidth: 4,
+            fill: 'white'
+        });
+
+        var circle = svgCreate('circle');
+
+        svgAttr(circle, {
+            cx: cx,
+            cy: cy,
+            r: Math.round((width + height) / 4)
+        });
+
+        svgAttr(circle, attrs);
+
+        svgAppend(p, circle);
+
+        return circle;
+    };
+
+    this.getCirclePath = function(shape) {
+        var cx = shape.x + shape.width / 2,
+            cy = shape.y + shape.height / 2,
+            radius = shape.width / 2;
+
+        var circlePath = [
+            ['M', cx, cy],
+            ['m', 0, -radius],
+            ['a', radius, radius, 0, 1, 1, 0, 2 * radius],
+            ['a', radius, radius, 0, 1, 1, 0, -2 * radius],
+            ['z']
+        ];
+
+        return componentsToPath(circlePath);
+    };
+
+    this.drawCustomConnection = function(p, element) {
+        var attrs = computeStyle(attrs, {
+            stroke: '#ff471a',
+            strokeWidth: 2
+        });
+
+        return svgAppend(p, createLine(element.waypoints, attrs));
+    };
+
+    this.getCustomConnectionPath = function(connection) {
+        var waypoints = connection.waypoints.map(function(p) {
+            return p.original || p;
+        });
+
+        var connectionPath = [['M', waypoints[0].x, waypoints[0].y]];
+
+        waypoints.forEach(function(waypoint, index) {
+            if (index !== 0) {
+                connectionPath.push(['L', waypoint.x, waypoint.y]);
+            }
+        });
+
+        return componentsToPath(connectionPath);
+    };
+}
+
+inherits(CustomRenderer, BaseRenderer);
+
+CustomRenderer.$inject = ['eventBus', 'styles'];
+
+CustomRenderer.prototype.canRender = function(element) {
+    return /^custom:/.test(element.type);
+};
+
+CustomRenderer.prototype.drawShape = function(p, element) {
+    var type = element.type;
+
+    if (type === 'custom:triangle') {
+        return this.drawTriangle(p, element.width);
+    }
+
+    if (type === 'custom:circle') {
+        return this.drawCircle(p, element.width, element.height);
+    }
+};
+
+CustomRenderer.prototype.getShapePath = function(shape) {
+    var type = shape.type;
+
+    if (type === 'custom:triangle') {
+        return this.getTrianglePath(shape);
+    }
+
+    if (type === 'custom:circle') {
+        return this.getCirclePath(shape);
+    }
+};
+
+CustomRenderer.prototype.drawConnection = function(p, element) {
+    var type = element.type;
+
+    if (type === 'custom:connection') {
+        return this.drawCustomConnection(p, element);
+    }
+};
+
+CustomRenderer.prototype.getConnectionPath = function(connection) {
+    var type = connection.type;
+
+    if (type === 'custom:connection') {
+        return this.getCustomConnectionPath(connection);
+    }
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js
new file mode 100644 (file)
index 0000000..1dce143
--- /dev/null
@@ -0,0 +1,136 @@
+import { reduce } from 'min-dash';
+
+import inherits from 'inherits';
+
+import { is } from 'bpmn-js/lib/util/ModelUtil';
+
+import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';
+
+var HIGH_PRIORITY = 1500;
+
+function isCustom(element) {
+    return element && /^custom:/.test(element.type);
+}
+
+/**
+ * Specific rules for custom elements
+ */
+export default function CustomRules(eventBus) {
+    RuleProvider.call(this, eventBus);
+}
+
+inherits(CustomRules, RuleProvider);
+
+CustomRules.$inject = ['eventBus'];
+
+CustomRules.prototype.init = function() {
+    /**
+     * Can shape be created on target container?
+     */
+    function canCreate(shape, target) {
+        // only judge about custom elements
+        if (!isCustom(shape)) {
+            return;
+        }
+
+        // allow creation on processes
+        return (
+            is(target, 'bpmn:Process') ||
+            is(target, 'bpmn:Participant') ||
+            is(target, 'bpmn:Collaboration')
+        );
+    }
+
+    /**
+     * Can source and target be connected?
+     */
+    function canConnect(source, target) {
+        // only judge about custom elements
+        if (!isCustom(source) && !isCustom(target)) {
+            return;
+        }
+
+        // allow connection between custom shape and task
+        if (isCustom(source)) {
+            if (is(target, 'bpmn:Task')) {
+                return { type: 'custom:connection' };
+            } else {
+                return false;
+            }
+        } else if (isCustom(target)) {
+            if (is(source, 'bpmn:Task')) {
+                return { type: 'custom:connection' };
+            } else {
+                return false;
+            }
+        }
+    }
+
+    this.addRule('elements.move', HIGH_PRIORITY, function(context) {
+        var target = context.target,
+            shapes = context.shapes;
+
+        var type;
+
+        // do not allow mixed movements of custom / BPMN shapes
+        // if any shape cannot be moved, the group cannot be moved, too
+        var allowed = reduce(
+            shapes,
+            function(result, s) {
+                if (type === undefined) {
+                    type = isCustom(s);
+                }
+
+                if (type !== isCustom(s) || result === false) {
+                    return false;
+                }
+
+                return canCreate(s, target);
+            },
+            undefined
+        );
+
+        // reject, if we have at least one
+        // custom element that cannot be moved
+        return allowed;
+    });
+
+    this.addRule('shape.create', HIGH_PRIORITY, function(context) {
+        var target = context.target,
+            shape = context.shape;
+
+        return canCreate(shape, target);
+    });
+
+    this.addRule('shape.resize', HIGH_PRIORITY, function(context) {
+        var shape = context.shape;
+
+        if (isCustom(shape)) {
+            // cannot resize custom elements
+            return false;
+        }
+    });
+
+    this.addRule('connection.create', HIGH_PRIORITY, function(context) {
+        var source = context.source,
+            target = context.target;
+
+        return canConnect(source, target);
+    });
+
+    this.addRule('connection.reconnectStart', HIGH_PRIORITY, function(context) {
+        var connection = context.connection,
+            source = context.hover || context.source,
+            target = connection.target;
+
+        return canConnect(source, target, connection);
+    });
+
+    this.addRule('connection.reconnectEnd', HIGH_PRIORITY, function(context) {
+        var connection = context.connection,
+            source = connection.source,
+            target = context.hover || context.target;
+
+        return canConnect(source, target, connection);
+    });
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js
new file mode 100644 (file)
index 0000000..532c24f
--- /dev/null
@@ -0,0 +1,136 @@
+import inherits from 'inherits';
+
+import { pick, assign } from 'min-dash';
+
+import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
+
+import {
+    add as collectionAdd,
+    remove as collectionRemove
+} from 'diagram-js/lib/util/Collections';
+
+/**
+ * A handler responsible for updating the custom element's businessObject
+ * once changes on the diagram happen.
+ */
+export default function CustomUpdater(eventBus, bpmnjs) {
+    CommandInterceptor.call(this, eventBus);
+
+    function updateCustomElement(e) {
+        var context = e.context,
+            shape = context.shape,
+            businessObject = shape.businessObject;
+
+        if (!isCustom(shape)) {
+            return;
+        }
+
+        var parent = shape.parent;
+
+        var customElements = bpmnjs._customElements;
+
+        // make sure element is added / removed from bpmnjs.customElements
+        if (!parent) {
+            collectionRemove(customElements, businessObject);
+        } else {
+            collectionAdd(customElements, businessObject);
+        }
+
+        // save custom element position
+        assign(businessObject, pick(shape, ['x', 'y']));
+    }
+
+    function updateCustomConnection(e) {
+        var context = e.context,
+            connection = context.connection,
+            source = connection.source,
+            target = connection.target,
+            businessObject = connection.businessObject;
+
+        var parent = connection.parent;
+
+        var customElements = bpmnjs._customElements;
+
+        // make sure element is added / removed from bpmnjs.customElements
+        if (!parent) {
+            collectionRemove(customElements, businessObject);
+        } else {
+            collectionAdd(customElements, businessObject);
+        }
+
+        // update waypoints
+        assign(businessObject, {
+            waypoints: copyWaypoints(connection)
+        });
+
+        if (source && target) {
+            assign(businessObject, {
+                source: source.id,
+                target: target.id
+            });
+        }
+    }
+
+    this.executed(
+        ['shape.create', 'shape.move', 'shape.delete'],
+        ifCustomElement(updateCustomElement)
+    );
+
+    this.reverted(
+        ['shape.create', 'shape.move', 'shape.delete'],
+        ifCustomElement(updateCustomElement)
+    );
+
+    this.executed(
+        [
+            'connection.create',
+            'connection.reconnectStart',
+            'connection.reconnectEnd',
+            'connection.updateWaypoints',
+            'connection.delete',
+            'connection.layout',
+            'connection.move'
+        ],
+        ifCustomElement(updateCustomConnection)
+    );
+
+    this.reverted(
+        [
+            'connection.create',
+            'connection.reconnectStart',
+            'connection.reconnectEnd',
+            'connection.updateWaypoints',
+            'connection.delete',
+            'connection.layout',
+            'connection.move'
+        ],
+        ifCustomElement(updateCustomConnection)
+    );
+}
+
+inherits(CustomUpdater, CommandInterceptor);
+
+CustomUpdater.$inject = ['eventBus', 'bpmnjs'];
+
+/////// helpers ///////////////////////////////////
+
+function copyWaypoints(connection) {
+    return connection.waypoints.map(function(p) {
+        return { x: p.x, y: p.y };
+    });
+}
+
+function isCustom(element) {
+    return element && /custom:/.test(element.type);
+}
+
+function ifCustomElement(fn) {
+    return function(event) {
+        var context = event.context,
+            element = context.shape || context.connection;
+
+        if (isCustom(element)) {
+            fn(event);
+        }
+    };
+}
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js
new file mode 100644 (file)
index 0000000..f108539
--- /dev/null
@@ -0,0 +1,22 @@
+import CustomElementFactory from './CustomElementFactory';
+import CustomRenderer from './CustomRenderer';
+import CustomPalette from './CustomPalette';
+import CustomRules from './CustomRules';
+import CustomUpdater from './CustomUpdater';
+import CustomContextPadProvider from './CustomContextPadProvider';
+
+export default {
+    __init__: [
+        'customRenderer',
+        'paletteProvider',
+        'customRules',
+        'customUpdater',
+        'contextPadProvider'
+    ],
+    elementFactory: ['type', CustomElementFactory],
+    customRenderer: ['type', CustomRenderer],
+    paletteProvider: ['type', CustomPalette],
+    customRules: ['type', CustomRules],
+    customUpdater: ['type', CustomUpdater],
+    contextPadProvider: ['type', CustomContextPadProvider]
+};
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js
new file mode 100644 (file)
index 0000000..86fbff6
--- /dev/null
@@ -0,0 +1,99 @@
+import Modeler from 'bpmn-js/lib/Modeler';
+
+import { assign, isArray } from 'min-dash';
+
+import inherits from 'inherits';
+
+import CustomModule from './custom';
+
+export default function CustomModeler(options) {
+    Modeler.call(this, options);
+
+    this._customElements = [];
+}
+
+inherits(CustomModeler, Modeler);
+
+CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [
+    CustomModule
+]);
+
+/**
+ * Add a single custom element to the underlying diagram
+ *
+ * @param {Object} customElement
+ */
+CustomModeler.prototype._addCustomShape = function(customElement) {
+    this._customElements.push(customElement);
+
+    var canvas = this.get('canvas'),
+        elementFactory = this.get('elementFactory');
+
+    var customAttrs = assign({ businessObject: customElement }, customElement);
+
+    var customShape = elementFactory.create('shape', customAttrs);
+
+    return canvas.addShape(customShape);
+};
+
+CustomModeler.prototype._addCustomConnection = function(customElement) {
+    this._customElements.push(customElement);
+
+    var canvas = this.get('canvas'),
+        elementFactory = this.get('elementFactory'),
+        elementRegistry = this.get('elementRegistry');
+
+    var customAttrs = assign({ businessObject: customElement }, customElement);
+
+    var connection = elementFactory.create(
+        'connection',
+        assign(customAttrs, {
+            source: elementRegistry.get(customElement.source),
+            target: elementRegistry.get(customElement.target)
+        }),
+        elementRegistry.get(customElement.source).parent
+    );
+
+    return canvas.addConnection(connection);
+};
+
+/**
+ * Add a number of custom elements and connections to the underlying diagram.
+ *
+ * @param {Array<Object>} customElements
+ */
+CustomModeler.prototype.addCustomElements = function(customElements) {
+    if (!isArray(customElements)) {
+        throw new Error('argument must be an array');
+    }
+
+    var shapes = [],
+        connections = [];
+
+    customElements.forEach(function(customElement) {
+        if (isCustomConnection(customElement)) {
+            connections.push(customElement);
+        } else {
+            shapes.push(customElement);
+        }
+    });
+
+    // add shapes before connections so that connections
+    // can already rely on the shapes being part of the diagram
+    shapes.forEach(this._addCustomShape, this);
+
+    connections.forEach(this._addCustomConnection, this);
+};
+
+/**
+ * Get custom elements with their current status.
+ *
+ * @return {Array<Object>} custom elements on the diagram
+ */
+CustomModeler.prototype.getCustomElements = function() {
+    return this._customElements;
+};
+
+function isCustomConnection(element) {
+    return element.type === 'custom:connection';
+}
index 7848b1f..4e591c9 100644 (file)
@@ -41,6 +41,21 @@ const Api = {
             }
         );
     },
+    fetchVersionArtifact: ({ workflowId, versionId }) => {
+        return RestfulAPIUtil.fetch(
+            `${baseUrl(workflowId)}/${versionId}/artifact`
+        );
+    },
+    updateVersionArtifact: ({ workflowId, versionId, payload }) => {
+        let formData = new FormData();
+        var blob = new Blob([payload], { type: 'text/xml' });
+        formData.append('fileToUpload', blob);
+
+        return RestfulAPIUtil.put(
+            `${baseUrl(workflowId)}/${versionId}/artifact`,
+            formData
+        );
+    },
     certifyVersion: ({ workflowId, versionId }) => {
         return RestfulAPIUtil.post(
             `${baseUrl(workflowId)}/${versionId}/state`,
index 9010ed1..19c8bdc 100644 (file)
@@ -20,14 +20,17 @@ import {
     getOutputs
 } from 'features/version/inputOutput/inputOutputSelectors';
 import { getVersionInfo } from 'features/version/general/generalSelectors';
+import { getComposition } from 'features/version/composition/compositionSelectors';
 
 export const getSavedObjParams = createSelector(
     getOutputs,
     getInputs,
+    getComposition,
     getVersionInfo,
-    (outputs, inputs, general) => ({
+    (outputs, inputs, composition, general) => ({
         outputs,
         inputs,
+        composition,
         ...general
     })
 );
index 9ef88f9..78b82ab 100644 (file)
@@ -32,14 +32,24 @@ import { notificationActions } from 'shared/notifications/notificationsActions';
 import { versionState } from 'features/version/versionConstants';
 import overviewApi from '../workflow/overview/overviewApi';
 import { versionListFetchAction } from '../workflow/overview/overviewConstansts';
+import { updateComposition } from 'features/version/composition/compositionActions';
 
 function* fetchVersion(action) {
     try {
         const data = yield call(versionApi.fetchVersion, action.payload);
         const { inputs, outputs, ...rest } = data;
+        let composition = false;
+
+        if (rest.hasArtifact) {
+            composition = yield call(
+                versionApi.fetchVersionArtifact,
+                action.payload
+            );
+        }
         yield all([
             put(setWorkflowVersionAction(rest)),
-            put(setInputsOutputs({ inputs, outputs }))
+            put(setInputsOutputs({ inputs, outputs })),
+            put(updateComposition(composition))
         ]);
     } catch (error) {
         yield put(genericNetworkErrorAction(error));
@@ -62,7 +72,22 @@ function* watchSubmitVersion(action) {
 
 function* watchUpdateVersion(action) {
     try {
-        yield call(versionApi.updateVersion, action.payload);
+        //const { composition, ...versionData } = action.payload;
+        const {
+            workflowId,
+            params: { composition, ...versionData }
+        } = action.payload;
+        yield call(versionApi.updateVersion, {
+            workflowId,
+            params: versionData
+        });
+        if (composition) {
+            yield call(versionApi.updateVersionArtifact, {
+                workflowId,
+                versionId: versionData.id,
+                payload: composition
+            });
+        }
         yield put(
             notificationActions.showSuccess({
                 title: 'Update Workflow Version',
index 986cf12..5fd444a 100644 (file)
@@ -59,6 +59,9 @@
                 "confirmDlete": "Are you sure you want to delete \"%{name}\"?",
                 "alreadyExists": "Already exists",
                 "invalidCharacters": "Alphanumeric and underscore only"
+            },
+            "composition": {
+                "bpmnError" : "BPMN.IO Error"
             }
         },
         "version": {
index 9dbef26..9fab814 100644 (file)
@@ -25,14 +25,15 @@ import loader from 'shared/loader/LoaderReducer';
 import modal from 'shared/modal/modalWrapperReducer';
 import overviewReducer from 'features/workflow/overview/overviewReducer';
 import workflowReducer from 'features/workflow/workflowReducer';
-
+import compositionReducer from 'features/version/composition/compositionReducer';
 export default combineReducers({
     i18n: i18nReducer,
     catalog,
     notifications: notificationsReducer,
     currentVersion: combineReducers({
         general: versionReducer,
-        inputOutput
+        inputOutput,
+        composition: compositionReducer
     }),
     workflow: combineReducers({
         data: workflowReducer,
index 79abe30..ea433cc 100644 (file)
@@ -19,7 +19,7 @@ import Version from 'features/version/Version';
 import GeneralView from 'features/version/general/General';
 import OverviewView from 'features/workflow/overview/Overview';
 import InputOutput from 'features/version/inputOutput/InputOutput';
-import CompositionView from 'features/version/composition/CompositionView';
+import Composition from 'features/version/composition/Composition';
 
 export const routes = [
     {
@@ -41,7 +41,7 @@ export const routes = [
             },
             {
                 path: '/composition',
-                component: CompositionView,
+                component: Composition,
                 i18nName: 'workflow.sideBar.composition',
                 id: 'COMPOSITION'
             }
index 23f5334..fa4e5d8 100644 (file)
@@ -127,8 +127,13 @@ module.exports = (env, argv) => {
                     include: srcPath
                 },
                 {
-                    test: /\.woff|\.woff2$/,
-                    loader: 'file-loader'
+                    test: /\.(eot|svg|ttf|woff|woff2)(\?.*)?$/,
+                    use: [
+                        {
+                            loader: 'file-loader',
+                            options: {}
+                        }
+                    ]
                 }
             ]
         },
index 7e31f62..dbfc8bb 100644 (file)
@@ -4450,6 +4450,10 @@ file-loader@^1.1.11:
     loader-utils "^1.0.2"
     schema-utils "^0.4.5"
 
+file-saver@^1.3.8:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
+
 filename-regex@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"