TSK-743: Removed HATEOAS from classification- and workbasketdefinition exports

This commit is contained in:
julian.schallenmueller 2018-11-29 15:06:57 +01:00 committed by Holger Hagen
parent 55c7453034
commit 835148c608
45 changed files with 681 additions and 346 deletions

View File

@ -355,7 +355,7 @@ include::../../../{snippets}/DeleteClassificationDocTest/http-request.adoc[]
include::../../../{snippets}/DeleteClassificationDocTest/http-response.adoc[]
== Classificationdefinition-Resource
== Classification-definition-Resource
[[classification-definitions]]
=== Get all classification definitions
@ -382,15 +382,15 @@ A `POST` request is used to import new classification-definitions.
This minimal example shows only the required fields to import a single new classification-definition. The <<classification, classification structure>> shows all possible fields for importing (and therefore creating) new classification-definitions.
include::../../../{snippets}/ImportClassificationdefinitions/http-request.adoc[]
include::../../../{snippets}/ImportClassificationDefinitions/http-request.adoc[]
==== Request Structure
include::../../../{snippets}/ImportClassificationdefinitions/request-fields.adoc[]
include::../../../{snippets}/ImportClassificationDefinitions/request-fields.adoc[]
==== Example Response
include::../../../{snippets}/ImportClassificationdefinitions/http-response.adoc[]
include::../../../{snippets}/ImportClassificationDefinitions/http-response.adoc[]
== Workbaskets-Resource
@ -540,7 +540,7 @@ include::../../../{snippets}/CreateWorkbasketDocTest/http-response.adoc[]
The response-body is essentially the same as for getting a single classification. +
Therefore for the response fields you can refer to the structure of the <<classification, single classification>>.
== Workbasketdefinition-Resource
== Workbasket-definition-resource
=== Get all workbasket definitions
@ -548,16 +548,16 @@ A `GET` request is used to get all workbasket definitions.
==== Example Request
include::../../../{snippets}/ExportWorkbasktdefinitionsDocTest/http-request.adoc[]
include::../../../{snippets}/ExportWorkbasketdefinitionsDocTest/http-request.adoc[]
==== Example Response
include::../../../{snippets}/ExportWorkbasktdefinitionsDocTest/http-response.adoc[]
include::../../../{snippets}/ExportWorkbasketdefinitionsDocTest/http-response.adoc[]
[[workbasket-definitions]]
==== Response Structure
include::../../../{snippets}/ExportWorkbasktdefinitionsDocTest/response-fields.adoc[]
include::../../../{snippets}/ExportWorkbasketdefinitionsDocTest/response-fields.adoc[]
=== Import new workbasket-definitions
@ -567,15 +567,15 @@ A `POST` request is used to import new workbasket-definitions.
This minimal example shows only the required fields to import a single new workbasket-definition. The <<workbasket, workbasket structure>> shows all possible fields for importing (and therefore creating) new workbasket-definitions.
include::../../../{snippets}/ImportWorkbasketdefinitions/http-request.adoc[]
include::../../../{snippets}/ImportWorkbasketDefinitions/http-request.adoc[]
==== Request Structure
include::../../../{snippets}/ImportWorkbasketdefinitions/request-fields.adoc[]
include::../../../{snippets}/ImportWorkbasketDefinitions/request-fields.adoc[]
==== Response Structure
include::../../../{snippets}/ImportWorkbasketdefinitions/http-response.adoc[]
include::../../../{snippets}/ImportWorkbasketDefinitions/http-response.adoc[]
== WorkbasketAccessItems-Resource

View File

@ -8,7 +8,10 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import org.junit.Before;
import org.junit.Rule;
@ -33,59 +36,61 @@ import pro.taskana.rest.RestConfiguration;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class ClassificationDefinitionControllerRestDocumentation {
@LocalServerPort
int port;
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private FieldDescriptor[] classificationdefinitionsFieldDescriptors;
private FieldDescriptor[] classificationDefinitionsFieldDescriptors;
@Before
public void setUp() {
document("{methodName}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(springSecurity())
.apply(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withResponseDefaults(prettyPrint())
.withRequestDefaults(prettyPrint()))
.build();
classificationdefinitionsFieldDescriptors = new FieldDescriptor[] {
subsectionWithPath("[]").description("An array of <<classification, classifications>>")
.apply(springSecurity())
.apply(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withResponseDefaults(prettyPrint())
.withRequestDefaults(prettyPrint()))
.build();
classificationDefinitionsFieldDescriptors = new FieldDescriptor[] {
subsectionWithPath("[]").description("An array of <<ClassificationResource, classifications>>")
};
}
@Test
public void exportAllClassificationdefinitions() throws Exception {
public void exportAllClassificationDefinitions() throws Exception {
this.mockMvc.perform(RestDocumentationRequestBuilders
.get("http://127.0.0.1:" + port + "/v1/classificationdefinitions")
.accept("application/json")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ExportClassificationdefinitionsDocTest",
responseFields(classificationdefinitionsFieldDescriptors)));
.get("http://127.0.0.1:" + port + "/v1/classification-definitions")
.accept("application/json")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ExportClassificationDefinitionsDocTest",
responseFields(classificationDefinitionsFieldDescriptors)));
}
@Test
public void importClassificationdefinitions() throws Exception {
public void importClassificationDefinitions() throws Exception {
String definitionString = "[{\"key\":\"Key0815\", \"domain\":\"DOMAIN_B\"}]";
this.mockMvc.perform(RestDocumentationRequestBuilders
.post("http://127.0.0.1:" + port + "/v1/classificationdefinitions/")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x")
.contentType("application/json")
.content(definitionString))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ImportClassificationdefinitions",
requestFields(subsectionWithPath("[]").description("An array of <<classification-definitions, classifications>>"))));
this.mockMvc.perform(multipart("http://127.0.0.1:" + port + "/v1/classification-definitions")
.file("file",
definitionString.getBytes())
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(document("ImportClassificationDefinitions", requestParts(
partWithName("file").description("The file to upload"))
));
}
}

View File

@ -8,7 +8,10 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import org.junit.Before;
import org.junit.Rule;
@ -33,65 +36,67 @@ import pro.taskana.rest.RestConfiguration;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class WorkbasketDefinitionControllerRestDocumentation {
@LocalServerPort
int port;
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private FieldDescriptor[] workbasketdefinitionsFieldDescriptors;
private FieldDescriptor[] workbasketDefinitionsFieldDescriptors;
@Before
public void setUp() {
document("{methodName}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(springSecurity())
.apply(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withResponseDefaults(prettyPrint())
.withRequestDefaults(prettyPrint()))
.build();
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
workbasketdefinitionsFieldDescriptors = new FieldDescriptor[] {
subsectionWithPath("[]").description("An array of <<workbasket, workbaskets>>")
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(springSecurity())
.apply(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withResponseDefaults(prettyPrint())
.withRequestDefaults(prettyPrint()))
.build();
workbasketDefinitionsFieldDescriptors = new FieldDescriptor[] {
subsectionWithPath("[]").description("An array of <<WorkbasketDefinitions, workbasketsDefinitions>>")
};
}
@Test
public void exportAllWorkbasketDefinitions() throws Exception {
this.mockMvc.perform(RestDocumentationRequestBuilders
.get("http://127.0.0.1:" + port + "/v1/workbasketdefinitions")
.accept("application/json")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ExportWorkbasktdefinitionsDocTest",
responseFields(workbasketdefinitionsFieldDescriptors)));
.get("http://127.0.0.1:" + port + "/v1/workbasket-definitions")
.accept("application/json")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ExportWorkbasketdefinitionsDocTest",
responseFields(workbasketDefinitionsFieldDescriptors)));
}
@Test
public void importWorkbasketdefinition() throws Exception {
this.mockMvc.perform(RestDocumentationRequestBuilders
.post("http://127.0.0.1:" + port + "/v1/workbasketdefinitions")
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x")
.contentType("application/json")
.content("["
+ "{"
+ "\"distributionTargets\":[], "
+ "\"authorizations\":[], "
+ "\"workbasket\": {\"name\":\"wbblabla\", \"key\":\"neuerKeyXy\", \"domain\": \"DOMAIN_A\", \"type\":\"GROUP\"}"
+ "}"
+ "]"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("ImportWorkbasketdefinitions",
requestFields(subsectionWithPath("[]").description("An array of <<workbasket, workbaskets>>"))));
public void importWorkbasketDefinition() throws Exception {
String definitionString = "["
+ "{"
+ "\"distributionTargets\":[], "
+ "\"authorizations\":[], "
+ "\"workbasket\": {\"name\":\"wbblabla\", \"key\":\"neuerKeyXy\", \"domain\": \"DOMAIN_A\", \"type\":\"GROUP\" , \"workbasketId\":\"gibtsNed\"}"
+ "}"
+ "]";
this.mockMvc.perform(multipart("http://127.0.0.1:" + port + "/v1/workbasket-definitions")
.file("file",
definitionString.getBytes())
.header("Authorization", "Basic dGVhbWxlYWRfMTp0ZWFtbGVhZF8x"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(document("ImportWorkbasketDefinitions", requestParts(
partWithName("file").description("The file to upload"))
));
}
}
}

View File

@ -1,5 +1,6 @@
package pro.taskana.rest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -9,13 +10,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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 pro.taskana.Classification;
import pro.taskana.ClassificationQuery;
@ -27,54 +27,72 @@ import pro.taskana.exceptions.ConcurrencyException;
import pro.taskana.exceptions.DomainNotFoundException;
import pro.taskana.exceptions.InvalidArgumentException;
import pro.taskana.exceptions.NotAuthorizedException;
import pro.taskana.rest.resource.ClassificationResource;
import pro.taskana.rest.resource.ClassificationResourceAssembler;
import java.io.IOException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Controller for Importing / Exporting classifications.
*/
@RestController
@RequestMapping(path = "/v1/classificationdefinitions", produces = {MediaType.APPLICATION_JSON_VALUE})
@RequestMapping(path = "/v1/classification-definitions", produces = {MediaType.APPLICATION_JSON_VALUE})
public class ClassificationDefinitionController {
@Autowired
private ClassificationService classificationService;
@Autowired
private ClassificationResourceAssembler classificationResourceAssembler;
@GetMapping
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<List<ClassificationSummary>> exportClassifications(
@RequestParam(required = false) String domain) {
public ResponseEntity<List<ClassificationResource>> exportClassifications(
@RequestParam(required = false) String domain)
throws ClassificationNotFoundException, DomainNotFoundException, ConcurrencyException, InvalidArgumentException,
NotAuthorizedException, ClassificationAlreadyExistException {
ClassificationQuery query = classificationService.createClassificationQuery();
List<ClassificationSummary> summaries = domain != null ? query.domainIn(domain).list() : query.list();
return new ResponseEntity<>(summaries, HttpStatus.OK);
List<ClassificationSummary> summaries = domain != null ? query.domainIn(domain).list() : query.list();
List<ClassificationResource> export = new ArrayList<>();
for (ClassificationSummary summary : summaries) {
Classification classification = classificationService.getClassification(summary.getKey(),
summary.getDomain());
export.add(classificationResourceAssembler.toDefinition(classification));
}
return new ResponseEntity<>(export, HttpStatus.OK);
}
@PostMapping
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<String> importClassifications(
@RequestBody List<Classification> classifications) throws InvalidArgumentException {
@RequestParam("file") MultipartFile file)
throws InvalidArgumentException, NotAuthorizedException, ConcurrencyException, ClassificationNotFoundException,
ClassificationAlreadyExistException, DomainNotFoundException, IOException {
Map<String, String> systemIds = classificationService.createClassificationQuery()
.list()
.stream()
.collect(Collectors.toMap(i -> i.getKey() + "|" + i.getDomain(), ClassificationSummary::getId));
try {
for (Classification classification : classifications) {
if (systemIds.containsKey(classification.getKey() + "|" + classification.getDomain())) {
classificationService.updateClassification(classification);
} else {
classificationService.createClassification(classification);
}
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<ClassificationResource> classificationsDefinitions = mapper.readValue(file.getInputStream(),
new TypeReference<List<ClassificationResource>>() {
});
for (ClassificationResource classification : classificationsDefinitions) {
if (systemIds.containsKey(classification.getKey() + "|" + classification.getDomain())) {
classificationService.updateClassification(classificationResourceAssembler.toModel(classification));
} else {
classificationService.createClassification(classificationResourceAssembler.toModel(classification));
}
} catch (NotAuthorizedException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
} catch (ClassificationNotFoundException | DomainNotFoundException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (ClassificationAlreadyExistException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.CONFLICT);
// TODO why is this occuring???
} catch (ConcurrencyException e) {
}
return new ResponseEntity<>(HttpStatus.OK);

View File

@ -11,13 +11,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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 pro.taskana.Workbasket;
import pro.taskana.WorkbasketAccessItem;
@ -31,25 +30,43 @@ import pro.taskana.exceptions.NotAuthorizedException;
import pro.taskana.exceptions.WorkbasketAlreadyExistException;
import pro.taskana.exceptions.WorkbasketNotFoundException;
import pro.taskana.rest.resource.WorkbasketDefinition;
import pro.taskana.rest.resource.WorkbasketDefinitionAssembler;
import java.io.IOException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Controller for all {@link WorkbasketDefinition} related endpoints.
*/
@RestController
@RequestMapping(path = "/v1/workbasketdefinitions", produces = {MediaType.APPLICATION_JSON_VALUE})
@RequestMapping(path = "/v1/workbasket-definitions", produces = {MediaType.APPLICATION_JSON_VALUE})
public class WorkbasketDefinitionController {
@Autowired
private WorkbasketService workbasketService;
@Autowired
private WorkbasketDefinitionAssembler workbasketDefinitionAssembler;
@GetMapping
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<List<WorkbasketSummary>> exportWorkbaskets(@RequestParam(required = false) String domain) {
public ResponseEntity<List<WorkbasketDefinition>> exportWorkbaskets(@RequestParam(required = false) String domain)
throws NotAuthorizedException, WorkbasketNotFoundException {
WorkbasketQuery workbasketQuery = workbasketService.createWorkbasketQuery();
List<WorkbasketSummary> workbasketSummaryList = domain != null
? workbasketQuery.domainIn(domain).list()
: workbasketQuery.list();
return new ResponseEntity<>(workbasketSummaryList, HttpStatus.OK);
List<WorkbasketDefinition> basketExports = new ArrayList<>();
for (WorkbasketSummary summary : workbasketSummaryList) {
Workbasket workbasket = workbasketService.getWorkbasket(summary.getId());
basketExports.add(workbasketDefinitionAssembler.toDefinition(workbasket));
}
return new ResponseEntity<>(basketExports, HttpStatus.OK);
}
/**
@ -57,85 +74,76 @@ public class WorkbasketDefinitionController {
* we want to have an option to import all settings at once. When a logical equal (key and domain are equal)
* workbasket already exists an update will be executed. Otherwise a new workbasket will be created.
*
* @param definitions the list of workbasket definitions which will be imported to the current system.
* @param file the list of workbasket definitions which will be imported to the current system.
* @return Return answer is determined by the status code: 200 - all good 400 - list state error (referring to non
* existing id's) 401 - not authorized
*/
@PostMapping
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<String> importWorkbaskets(@RequestBody List<WorkbasketDefinition> definitions) {
try {
// key: logical ID
// value: system ID (in database)
Map<String, String> systemIds = workbasketService.createWorkbasketQuery()
.list()
.stream()
.collect(Collectors.toMap(this::logicalId, WorkbasketSummary::getId));
public ResponseEntity<String> importWorkbaskets(@RequestParam("file") MultipartFile file)
throws IOException, NotAuthorizedException, DomainNotFoundException, InvalidWorkbasketException,
WorkbasketAlreadyExistException, WorkbasketNotFoundException, InvalidArgumentException {
// key: old system ID
// value: system ID
Map<String, String> idConversion = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<WorkbasketDefinition> definitions = mapper.readValue(file.getInputStream(),
new TypeReference<List<WorkbasketDefinition>>() {
// STEP 1: update or create workbaskets from the import
for (WorkbasketDefinition definition : definitions) {
Workbasket importedWb = definition.workbasket;
Workbasket workbasket;
if (systemIds.containsKey(logicalId(importedWb))) {
workbasket = workbasketService.updateWorkbasket(importedWb);
} else {
workbasket = workbasketService.createWorkbasket(importedWb);
}
});
// Since we would have a n² runtime when doing a lookup and updating the access items we decided to
// simply delete all existing accessItems and create new ones.
for (WorkbasketAccessItem accessItem : workbasketService.getWorkbasketAccessItems(workbasket.getId())) {
workbasketService.deleteWorkbasketAccessItem(accessItem.getId());
}
for (WorkbasketAccessItem authorization : definition.authorizations) {
workbasketService.createWorkbasketAccessItem(authorization);
}
idConversion.put(definition.workbasket.getId(), workbasket.getId());
// key: logical ID
// value: system ID (in database)
Map<String, String> systemIds = workbasketService.createWorkbasketQuery()
.list()
.stream()
.collect(Collectors.toMap(this::logicalId, WorkbasketSummary::getId));
// key: old system ID
// value: system ID
Map<String, String> idConversion = new HashMap<>();
// STEP 1: update or create workbaskets from the import
for (WorkbasketDefinition definition : definitions) {
Workbasket importedWb = workbasketDefinitionAssembler.toModel(definition.workbasket);
Workbasket workbasket;
if (systemIds.containsKey(logicalId(importedWb))) {
workbasket = workbasketService.updateWorkbasket(importedWb);
} else {
workbasket = workbasketService.createWorkbasket(importedWb);
}
// STEP 2: update distribution targets
// This can not be done in step 1 because the system IDs are only known after step 1
for (WorkbasketDefinition definition : definitions) {
List<String> distributionTargets = new ArrayList<>();
for (String oldId : definition.distributionTargets) {
if (idConversion.containsKey(oldId)) {
distributionTargets.add(idConversion.get(oldId));
} else {
throw new InvalidWorkbasketException(
String.format(
"invalid import state: Workbasket '%s' does not exist in the given import list",
oldId));
}
}
workbasketService.setDistributionTargets(
// no verification necessary since the workbasket was already imported in step 1.
idConversion.get(definition.workbasket.getId()), distributionTargets);
// Since we would have a n² runtime when doing a lookup and updating the access items we decided to
// simply delete all existing accessItems and create new ones.
for (WorkbasketAccessItem accessItem : workbasketService.getWorkbasketAccessItems(workbasket.getId())) {
workbasketService.deleteWorkbasketAccessItem(accessItem.getId());
}
return new ResponseEntity<>(HttpStatus.OK);
} catch (WorkbasketNotFoundException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (InvalidWorkbasketException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} catch (NotAuthorizedException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
} catch (InvalidArgumentException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
} catch (WorkbasketAlreadyExistException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.CONFLICT);
} catch (DomainNotFoundException e) {
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
for (WorkbasketAccessItem authorization : definition.authorizations) {
workbasketService.createWorkbasketAccessItem(authorization);
}
idConversion.put(importedWb.getId(), workbasket.getId());
}
// STEP 2: update distribution targets
// This can not be done in step 1 because the system IDs are only known after step 1
for (WorkbasketDefinition definition : definitions) {
List<String> distributionTargets = new ArrayList<>();
for (String oldId : definition.distributionTargets) {
if (idConversion.containsKey(oldId)) {
distributionTargets.add(idConversion.get(oldId));
} else {
throw new InvalidWorkbasketException(
String.format(
"invalid import state: Workbasket '%s' does not exist in the given import list",
oldId));
}
}
workbasketService.setDistributionTargets(
// no verification necessary since the workbasket was already imported in step 1.
idConversion.get(definition.workbasket.getWorkbasketId()), distributionTargets);
}
return new ResponseEntity<>(HttpStatus.OK);
}
private String logicalId(WorkbasketSummary workbasket) {

View File

@ -29,16 +29,18 @@ public class ClassificationResourceAssembler {
@Autowired
ClassificationService classificationService;
public ClassificationResource toResource(Classification classification) throws ClassificationNotFoundException,
NotAuthorizedException, ClassificationAlreadyExistException, ConcurrencyException, DomainNotFoundException,
InvalidArgumentException {
ClassificationResource resource = new ClassificationResource();
BeanUtils.copyProperties(classification, resource);
// need to be set by hand, because they are named different, or have different types
resource.setClassificationId(classification.getId());
resource.setCreated(classification.getCreated().toString());
resource.setModified(classification.getModified().toString());
return addLinks(resource, classification);
public ClassificationResource toResource(Classification classification)
throws InvalidArgumentException, ConcurrencyException, ClassificationNotFoundException, DomainNotFoundException,
ClassificationAlreadyExistException, NotAuthorizedException {
return this.createResource(classification);
}
public ClassificationResource toDefinition(Classification classification)
throws InvalidArgumentException, ConcurrencyException, ClassificationNotFoundException, DomainNotFoundException,
ClassificationAlreadyExistException, NotAuthorizedException {
ClassificationResource resource = this.createResource(classification);
resource.removeLinks();
return resource;
}
public Classification toModel(ClassificationResource classificationResource) {
@ -56,6 +58,22 @@ public class ClassificationResourceAssembler {
return classification;
}
private ClassificationResource createResource(Classification classification)
throws NotAuthorizedException, ConcurrencyException, InvalidArgumentException, DomainNotFoundException,
ClassificationAlreadyExistException, ClassificationNotFoundException {
ClassificationResource resource = new ClassificationResource();
BeanUtils.copyProperties(classification, resource);
// need to be set by hand, because they are named different, or have different types
resource.setClassificationId(classification.getId());
if(classification.getCreated() != null){
resource.setCreated(classification.getCreated().toString());
}
if(classification.getModified() != null){
resource.setModified(classification.getModified().toString());
}
return addLinks(resource, classification);
}
private ClassificationResource addLinks(ClassificationResource resource, Classification classification)
throws ClassificationNotFoundException, NotAuthorizedException, ClassificationAlreadyExistException,
ConcurrencyException, DomainNotFoundException, InvalidArgumentException {

View File

@ -13,13 +13,13 @@ public class WorkbasketDefinition {
public Set<String> distributionTargets;
public List<WorkbasketAccessItem> authorizations;
public Workbasket workbasket;
public WorkbasketResource workbasket;
public WorkbasketDefinition() {
// necessary for de-serializing
}
public WorkbasketDefinition(Workbasket workbasket,
public WorkbasketDefinition(WorkbasketResource workbasket,
Set<String> distributionTargets,
List<WorkbasketAccessItem> authorizations) {
super();

View File

@ -1,19 +1,28 @@
package pro.taskana.rest.resource;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import pro.taskana.Workbasket;
import pro.taskana.WorkbasketAccessItem;
import pro.taskana.WorkbasketService;
import pro.taskana.WorkbasketSummary;
import pro.taskana.exceptions.DomainNotFoundException;
import pro.taskana.exceptions.InvalidArgumentException;
import pro.taskana.exceptions.InvalidWorkbasketException;
import pro.taskana.exceptions.NotAuthorizedException;
import pro.taskana.exceptions.WorkbasketAlreadyExistException;
import pro.taskana.exceptions.WorkbasketNotFoundException;
import pro.taskana.impl.WorkbasketImpl;
/**
* Transforms {@link Workbasket} into a {@link WorkbasketDefinition}
@ -28,7 +37,7 @@ public class WorkbasketDefinitionAssembler {
/**
* maps the distro targets to their id to remove overhead.
*
* @param basket
* @param workbasket
* {@link Workbasket} which will be converted
* @return a {@link WorkbasketDefinition}, containing the {@code basket}, its distribution targets and its
* authorizations
@ -37,16 +46,37 @@ public class WorkbasketDefinitionAssembler {
* @throws WorkbasketNotFoundException
* if {@code basket} is an unknown workbasket
*/
public WorkbasketDefinition toDefinition(Workbasket basket)
public WorkbasketDefinition toDefinition(Workbasket workbasket)
throws NotAuthorizedException, WorkbasketNotFoundException {
WorkbasketResource basket = new WorkbasketResource();
BeanUtils.copyProperties(workbasket, basket);
basket.setWorkbasketId(workbasket.getId());
basket.setModified(workbasket.getModified().toString());
basket.setCreated(workbasket.getCreated().toString());
List<WorkbasketAccessItem> authorizations = new ArrayList<>();
for (WorkbasketAccessItem accessItem : workbasketService.getWorkbasketAccessItems(basket.getKey())) {
authorizations.add(accessItem);
}
Set<String> distroTargets = workbasketService.getDistributionTargets(basket.getId())
Set<String> distroTargets = workbasketService.getDistributionTargets(workbasket.getId())
.stream()
.map(WorkbasketSummary::getId)
.collect(Collectors.toSet());
return new WorkbasketDefinition(basket, distroTargets, authorizations);
}
public Workbasket toModel(WorkbasketResource wbResource) {
WorkbasketImpl workbasket = (WorkbasketImpl) workbasketService.newWorkbasket(wbResource.key, wbResource.domain);
BeanUtils.copyProperties(wbResource, workbasket);
workbasket.setId(wbResource.workbasketId);
if (wbResource.getModified() != null) {
workbasket.setModified(Instant.parse(wbResource.modified));
}
if (wbResource.getCreated() != null) {
workbasket.setCreated(Instant.parse(wbResource.created));
}
return workbasket;
}
}

View File

@ -31,6 +31,7 @@ import {WorkbasketDefinitionService} from './services/workbasket-definition/work
import {ClassificationsService} from '../services/classifications/classifications.service';
import {ClassificationCategoriesService} from 'app/services/classifications/classification-categories.service';
import { AccessItemsManagementComponent } from 'app/administration/access-items-management/access-items-management.component';
import { ImportExportService } from './services/import-export/import-export.service';
const MODULES = [
CommonModule,
@ -69,6 +70,7 @@ const DECLARATIONS = [
SavingWorkbasketService,
ClassificationsService,
ClassificationCategoriesService,
ImportExportService,
]
})
export class AdministrationModule {

View File

@ -26,6 +26,7 @@ import { AlertService } from 'app/services/alert/alert.service';
import { TreeService } from 'app/services/tree/tree.service';
import { CustomFieldsService } from 'app/services/custom-fields/custom-fields.service';
import { RemoveConfirmationService } from 'app/services/remove-confirmation/remove-confirmation.service';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-dummy-detail',
@ -52,8 +53,7 @@ describe('ClassificationDetailsComponent', () => {
imports: [FormsModule, HttpClientModule, RouterTestingModule.withRoutes(routes), AngularSvgIconModule],
declarations: [ClassificationDetailsComponent, DummyDetailComponent],
providers: [MasterAndDetailService, RequestInProgressService, ClassificationsService, HttpClient, ErrorModalService, AlertService,
TreeService, ClassificationCategoriesService,
CustomFieldsService]
TreeService, ClassificationCategoriesService, CustomFieldsService, ImportExportService]
})
};
configureTests(configure).then(testBed => {

View File

@ -26,6 +26,7 @@ import { CustomFieldsService } from '../../../services/custom-fields/custom-fiel
import { Pair } from 'app/models/pair';
import { NgForm } from '@angular/forms';
import { FormsValidatorService } from 'app/shared/services/forms/forms-validator.service';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-classification-details',
@ -37,7 +38,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
classification: ClassificationDefinition;
classificationClone: ClassificationDefinition;
selectedClassification: ClassificationDefinition = undefined;
showDetail = false;
classificationTypes: Array<string> = [];
badgeMessage = '';
@ -65,6 +65,7 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
private selectedClassificationTypeSubscription: Subscription;
private categoriesSubscription: Subscription;
private domainSubscription: Subscription;
private importingExportingSubscription: Subscription;
@ViewChild('ClassificationForm') classificationForm: NgForm;
toogleValidationMap = new Map<string, boolean>();
@ -81,7 +82,8 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
private domainService: DomainService,
private customFieldsService: CustomFieldsService,
private removeConfirmationService: RemoveConfirmationService,
private formsValidatorService: FormsValidatorService) {
private formsValidatorService: FormsValidatorService,
private importExportService: ImportExportService) {
}
ngOnInit() {
@ -107,7 +109,7 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
if (id === 'undefined') {
id = undefined;
}
this.fillClassificationInformation(this.selectedClassification ? this.selectedClassification : new ClassificationDefinition())
this.fillClassificationInformation(this.classification ? this.classification : new ClassificationDefinition())
}
if (!this.classification || this.classification.classificationId !== id && id && id !== '') {
@ -125,6 +127,10 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
this.classification.category = categories[0];
}
});
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe((value: Boolean) => {
if (this.classification.classificationId) { this.selectClassification(this.classification.classificationId); }
})
}
backClicked(): void {
@ -216,14 +222,14 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
}
this.requestInProgress = true;
this.selectedClassificationSubscription = this.classificationsService.getClassification(id).subscribe(classification => {
this.selectedClassification = classification;
this.fillClassificationInformation(classification)
this.classificationsService.selectClassification(classification);
this.requestInProgress = false;
});
}
private classificationIsAlreadySelected(): boolean {
if (this.action === ACTION.CREATE && this.selectedClassification) { return true }
if (this.action === ACTION.CREATE && this.classification) { return true }
}
private fillClassificationInformation(classificationSelected: ClassificationDefinition) {
@ -287,7 +293,7 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
})
}
private cloneClassification (classification: ClassificationDefinition) {
private cloneClassification(classification: ClassificationDefinition) {
this.classificationClone = { ...classification };
}
@ -311,5 +317,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
if (this.selectedClassificationTypeSubscription) { this.selectedClassificationTypeSubscription.unsubscribe(); }
if (this.categoriesSubscription) { this.categoriesSubscription.unsubscribe(); }
if (this.domainSubscription) { this.domainSubscription.unsubscribe(); }
if (this.importingExportingSubscription) { this.importingExportingSubscription.unsubscribe(); }
}
}

View File

@ -5,7 +5,7 @@
<button type="button" (click)="addClassification()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="material-icons md-20 green-blue">add_circle_outline</span>
</button>
<taskana-import-export-component class ="btn-group" [currentSelection]="selectionToImport" (importSucessful)="refreshClassificationList()">
<taskana-import-export-component class ="btn-group" [currentSelection]="selectionToImport">
</taskana-import-export-component>
</div>
<div class="col-xs-6">

View File

@ -26,6 +26,7 @@ import {
} from 'app/services/classifications/classification-categories.service';
import { Pair } from 'app/models/pair';
import { TreeService } from 'app/services/tree/tree.service';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-dummy-detail',
@ -54,7 +55,7 @@ describe('ClassificationListComponent', () => {
imports: [HttpClientModule, RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule],
providers: [
HttpClient, WorkbasketDefinitionService, AlertService, ClassificationsService, DomainService, ClassificationDefinitionService,
ErrorModalService, RequestInProgressService, ClassificationCategoriesService, TreeService
ErrorModalService, RequestInProgressService, ClassificationCategoriesService, TreeService, ImportExportService
]
})
};

View File

@ -12,6 +12,7 @@ import {
} from 'app/services/classifications/classification-categories.service';
import { Pair } from 'app/models/pair';
import { ClassificationDefinition } from '../../../../models/classification-definition';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-classification-list',
@ -37,12 +38,14 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
classificationSavedSubscription: Subscription;
selectedClassificationSubscription: Subscription;
categoriesSubscription: Subscription;
importingExportingSubscription: Subscription;
constructor(
private classificationService: ClassificationsService,
private router: Router,
private route: ActivatedRoute,
private categoryService: ClassificationCategoriesService) {
private categoryService: ClassificationCategoriesService,
private importExportService: ImportExportService) {
}
ngOnInit() {
@ -52,14 +55,17 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
this.performRequest(true);
});
this.selectedClassificationSubscription = this.categoryService.getSelectedClassificationType().subscribe(value => {
this.classificationTypeSelected = value;
this.classificationTypeSelected = value;
this.performRequest();
});
this.categoriesSubscription =
this.categoryService.getCategories(this.classificationTypeSelected).subscribe((categories: Array<string>) => {
this.categories = categories;
});
this.categoriesSubscription =
this.categoryService.getCategories(this.classificationTypeSelected).subscribe((categories: Array<string>) => {
this.categories = categories;
});
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe((value: Boolean) => {
this.performRequest(true);
})
}
selectClassificationType(classificationTypeSelected: string) {
@ -70,8 +76,8 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
.subscribe((classifications: Array<TreeNodeModel>) => {
this.classifications = classifications;
this.requestInProgress = false;
});
this.selectClassification(undefined);
});
this.selectClassification(undefined);
}
selectClassification(id: string) {
@ -123,16 +129,11 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
this.initialized = true;
}
refreshClassificationList() {
this.performRequest(true);
}
ngOnDestroy(): void {
if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); }
if (this.classificationTypeServiceSubscription) { this.classificationTypeServiceSubscription.unsubscribe(); }
if (this.classificationSelectedSubscription) { this.classificationSelectedSubscription.unsubscribe(); }
if (this.classificationSavedSubscription) { this.classificationSavedSubscription.unsubscribe(); }
if (this.importingExportingSubscription) { this.importingExportingSubscription.unsubscribe(); }
}
}

View File

@ -1,20 +1,23 @@
<input #selectedFile type="file" accept=".json" (change)="onSelectFile($event)" class="hide" />
<button type="button" (click)="selectedFile.click()" data-toggle="tooltip" title="Import" class="btn btn-default">
<button type="button" [ngClass]="{disabled: uploadservice?.isInUse}" (click)="selectedFile.click()" data-toggle="tooltip" title="Import" class="btn btn-default">
<span class="material-icons md-20 green-blue">cloud_upload</span>
</button>
<form class="hidden" id="upload_form" enctype="multipart/form-data" method="post">
<input #selectedFile type="file" accept=".json" (change)="uploadFile()" class="hide" />
</form>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" class="dropdown-item" (click)="export()">
<label>All Domains</label>
</a>
</li>
<div role="separator" class="divider"></div>
<li *ngFor="let domain of domains">
<a href="javascript:void(0)" class="dropdown-item" (click)="export(domain)">
<label>{{domain === '' ? 'Master' : domain}}</label>
</a>
</li>
</ul>
<button type="button" data-toggle="dropdown" title="Export" class="btn btn-default">
<li>
<a href="javascript:void(0)" class="dropdown-item" (click)="export()">
<label>All Domains</label>
</a>
</li>
<div role="separator" class="divider"></div>
<li *ngFor="let domain of domains">
<a href="javascript:void(0)" class="dropdown-item" (click)="export(domain)">
<label>{{domain === '' ? 'Master' : domain}}</label>
</a>
</li>
</ul>
<button [ngClass]="{disabled: uploadservice?.isInUse}" type="button" data-toggle="dropdown" title="Export" class="btn btn-default">
<span class="material-icons md-20 red">cloud_download</span>
</button>

View File

@ -9,6 +9,7 @@ import { HttpClientModule } from '@angular/common/http';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { configureTests } from 'app/app.test.configuration';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
describe('ImportExportComponent', () => {
let component: ImportExportComponent;
@ -21,7 +22,7 @@ describe('ImportExportComponent', () => {
declarations: [ImportExportComponent],
imports: [HttpClientModule, AngularSvgIconModule],
providers: [WorkbasketService, ClassificationDefinitionService, WorkbasketDefinitionService, AlertService,
ErrorModalService]
ErrorModalService, ImportExportService]
})
};
configureTests(configure).then(testBed => {

View File

@ -1,10 +1,15 @@
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { ClassificationDefinitionService } from 'app/administration/services/classification-definition/classification-definition.service';
import { WorkbasketDefinitionService } from 'app/administration/services/workbasket-definition/workbasket-definition.service';
import { DomainService } from 'app/services/domain/domain.service';
import { TaskanaType } from 'app/models/taskana-type';
import { ErrorModel } from 'app/models/modal-error';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { environment } from 'environments/environment';
import { AlertService } from 'app/services/alert/alert.service';
import { AlertModel, AlertType } from 'app/models/alert';
import { UploadService } from 'app/shared/services/upload/upload.service';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-import-export-component',
@ -15,13 +20,20 @@ export class ImportExportComponent implements OnInit {
@Input() currentSelection: TaskanaType;
@Output() importSucessful = new EventEmitter();
@ViewChild('selectedFile')
selectedFileInput;
domains: string[] = [];
errorWhileUploadingText: string;
constructor(private domainService: DomainService, private workbasketDefinitionService: WorkbasketDefinitionService,
private classificationDefinitionService: ClassificationDefinitionService, private errorModalService: ErrorModalService) {
constructor(
private domainService: DomainService,
private workbasketDefinitionService: WorkbasketDefinitionService,
private classificationDefinitionService: ClassificationDefinitionService,
private errorModalService: ErrorModalService,
private alertService: AlertService,
public uploadservice: UploadService,
private importExportService: ImportExportService) {
}
ngOnInit() {
@ -30,30 +42,6 @@ export class ImportExportComponent implements OnInit {
);
}
onSelectFile(event) {
const file = event.target.files[0];
const ending = file.name.match(/\.([^\.]+)$/)[1];
switch (ending) {
case 'json':
break;
default:
file.value = '';
this.errorModalService.triggerError(new ErrorModel(undefined,
`This file format is not allowed! Please use a .json file.`));
}
const reader = new FileReader();
if (this.currentSelection === TaskanaType.WORKBASKETS) {
reader.onload = <Event>(e) => this.workbasketDefinitionService.importWorkbasketDefinitions(e.target.result);
this.importSucessful.emit();
} else {
reader.onload = <Event>(e) => this.classificationDefinitionService.importClassifications(e.target.result);
this.importSucessful.emit();
}
reader.readAsText(file);
}
export(domain = '') {
if (this.currentSelection === TaskanaType.WORKBASKETS) {
this.workbasketDefinitionService.exportWorkbaskets(domain);
@ -61,4 +49,81 @@ export class ImportExportComponent implements OnInit {
this.classificationDefinitionService.exportClassifications(domain);
}
}
uploadFile() {
const file = this.selectedFileInput.nativeElement.files[0],
formdata = new FormData(),
ajax = new XMLHttpRequest();
if (!this.checkFormatFile(file)) { return false }
formdata.append('file', file);
ajax.upload.addEventListener('progress', this.progressHandler.bind(this), false);
ajax.addEventListener('load', this.resetProgress.bind(this), false);
ajax.addEventListener('error', this.errorHandler.bind(this), false);
ajax.onreadystatechange = this.onReadyStateChangeHandler.bind(this, ajax);
if (this.currentSelection === TaskanaType.WORKBASKETS) {
ajax.open('POST', environment.taskanaRestUrl + '/v1/workbasket-definitions');
} else {
ajax.open('POST', environment.taskanaRestUrl + '/v1/classification-definitions');
}
ajax.send(formdata);
this.uploadservice.isInUse = true;
this.uploadservice.setCurrentProgressValue(1)
}
progressHandler(event) {
const percent = (event.loaded / event.total) * 100;
this.uploadservice.setCurrentProgressValue(Math.round(percent))
}
private checkFormatFile(file): boolean {
const ending = file.name.match(/\.([^\.]+)$/)[1];
let check = false;
switch (ending) {
case 'json':
check = true;
break;
default:
file.value = '';
this.errorModalService.triggerError(new ErrorModel(undefined,
`This file format is not allowed! Please use a .json file.`));
}
return check;
}
private resetProgress() {
this.uploadservice.setCurrentProgressValue(0)
this.uploadservice.isInUse = false;
this.selectedFileInput.nativeElement.value = '';
}
private onReadyStateChangeHandler(event) {
if (event.readyState === 4 && event.status >= 400) {
let title;
if (event.status === 401) {
title = 'Import was not successful, you have no access to apply this operation.';
} else if (event.status === 404) {
title = 'Import was not successful, operation was not found.';
} else if (event.status === 409) {
title = 'Import was not successful, operation has some conflicts.';
}
this.errorHandler(title, JSON.parse(event.responseText).message);
} else if (event.readyState === 4 && event.status === 200) {
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, 'Import was successful'))
this.importExportService.setImportingFinished(true);
this.resetProgress();
}
}
private errorHandler(title = 'Import was not successful', message) {
this.errorModalService.triggerError(
new ErrorModel(
title,
message
)
);
this.selectedFileInput.files = undefined;
this.resetProgress();
}
}

View File

@ -1,19 +1,16 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'app/../environments/environment';
import { AlertService } from 'app/services/alert/alert.service';
import { ClassificationDefinition } from 'app/models/classification-definition';
import { AlertModel, AlertType } from 'app/models/alert';
import { saveAs } from 'file-saver/FileSaver';
import { TaskanaDate } from 'app/shared/util/taskana.date';
@Injectable()
export class ClassificationDefinitionService {
url = environment.taskanaRestUrl + '/v1/classificationdefinitions';
url = environment.taskanaRestUrl + '/v1/classification-definitions';
constructor(
private httpClient: HttpClient,
private alertService: AlertService
private httpClient: HttpClient
) {
}
@ -26,14 +23,4 @@ export class ClassificationDefinitionService {
'Classifications_' + TaskanaDate.getDate() + '.json')
);
}
// POST
// TODO handle error
importClassifications(classifications: any) {
this.httpClient.post(this.url,
JSON.parse(classifications)).subscribe(
classificationsUpdated => this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, 'Import was successful')),
error => this.alertService.triggerAlert(new AlertModel(AlertType.DANGER, 'Import was not successful'))
);
}
}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
@Injectable()
export class ImportExportService {
public importingFinished = new Subject<boolean>();
constructor() { }
setImportingFinished(value: boolean) {
this.importingFinished.next(value);
}
getImportingFinished(): Observable<boolean> {
return this.importingFinished.asObservable();
}
}

View File

@ -1,21 +1,15 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { environment } from 'app/../environments/environment';
import { saveAs } from 'file-saver/FileSaver';
import { AlertService } from 'app/services/alert/alert.service';
import { WorkbasketDefinition } from 'app/models/workbasket-definition';
import { AlertModel, AlertType } from 'app/models/alert';
import { TaskanaDate } from 'app/shared/util/taskana.date';
import { ErrorModel } from 'app/models/modal-error';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
@Injectable()
export class WorkbasketDefinitionService {
url: string = environment.taskanaRestUrl + '/v1/workbasketdefinitions';
url: string = environment.taskanaRestUrl + '/v1/workbasket-definitions';
constructor(private httpClient: HttpClient, private alertService: AlertService,
private errorModalService: ErrorModalService) {
constructor(private httpClient: HttpClient) {
}
// GET
@ -28,14 +22,4 @@ export class WorkbasketDefinitionService {
}
);
}
// POST
importWorkbasketDefinitions(workbasketDefinitions: any) {
this.httpClient.post(environment.taskanaRestUrl + '/v1/workbasketdefinitions',
JSON.parse(workbasketDefinitions)).subscribe(
workbasketsUpdated => this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, 'Import was successful')),
error => this.errorModalService.triggerError(new ErrorModel(
`There was an error importing workbaskets`, error.message))
);
}
}

View File

@ -31,6 +31,7 @@ import { AccessItemsComponent } from './access-items/access-items.component';
import { DistributionTargetsComponent } from './distribution-targets/distribution-targets.component';
import { DualListComponent } from './distribution-targets//dual-list/dual-list.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-dummy-detail',
@ -56,14 +57,13 @@ describe('WorkbasketDetailsComponent', () => {
beforeEach(done => {
const configure = (testBed: TestBed) => {
testBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule,
InfiniteScrollModule],
imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule,
InfiniteScrollModule],
declarations: [WorkbasketDetailsComponent, WorkbasketInformationComponent,
AccessItemsComponent,
DistributionTargetsComponent, DualListComponent, DummyDetailComponent],
providers: [WorkbasketService, MasterAndDetailService, ErrorModalService, RequestInProgressService,
AlertService, SavingWorkbasketService,
CustomFieldsService]
AlertService, SavingWorkbasketService, CustomFieldsService, ImportExportService]
})
};
configureTests(configure).then(testBed => {

View File

@ -10,7 +10,7 @@ import { WorkbasketService } from 'app/services/workbasket/workbasket.service'
import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service'
import { DomainService } from 'app/services/domain/domain.service';
import { ErrorModalService } from '../../../services/errorModal/error-modal.service';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-workbasket-details',
@ -18,7 +18,6 @@ import { ErrorModalService } from '../../../services/errorModal/error-modal.serv
})
export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
workbasket: Workbasket;
workbasketCopy: Workbasket;
selectedId: string = undefined;
@ -33,13 +32,15 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
private masterAndDetailSubscription: Subscription;
private permissionSubscription: Subscription;
private domainSubscription: Subscription;
private importingExportingSubscription: Subscription;
constructor(private service: WorkbasketService,
private route: ActivatedRoute,
private router: Router,
private masterAndDetailService: MasterAndDetailService,
private domainService: DomainService,
private errorModalService: ErrorModalService) { }
private errorModalService: ErrorModalService,
private importExportService: ImportExportService) { }
@ -77,6 +78,10 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
this.masterAndDetailSubscription = this.masterAndDetailService.getShowDetail().subscribe(showDetail => {
this.showDetail = showDetail;
});
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe((value: Boolean) => {
if (this.workbasket) { this.getWorkbasketInformation(this.workbasket.workbasketId); }
})
}
backClicked(): void {
@ -113,9 +118,9 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
this.requestInProgress = false;
this.checkDomainAndRedirect();
}, err => {
this.errorModalService.triggerError(
new ErrorModel('An error occurred while fetching the workbasket', err));
});
this.errorModalService.triggerError(
new ErrorModel('An error occurred while fetching the workbasket', err));
});
}
}
@ -134,5 +139,6 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
if (this.masterAndDetailSubscription) { this.masterAndDetailSubscription.unsubscribe(); }
if (this.permissionSubscription) { this.permissionSubscription.unsubscribe(); }
if (this.domainSubscription) { this.domainSubscription.unsubscribe(); }
if (this.importingExportingSubscription) { this.importingExportingSubscription.unsubscribe(); }
}
}

View File

@ -1,10 +1,10 @@
<li id="wb-action-toolbar" class="list-group-item tab-align">
<div class="row">
<div class="col-xs-9 btn-group">
<div class="col-xs-7 btn-group">
<button type="button" (click)="addWorkbasket()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="material-icons md-20 green-blue">add_circle_outline</span>
</button>
<taskana-import-export-component class="btn-group" (importSucessful)="importEvent()" [currentSelection]="selectionToImport"></taskana-import-export-component>
<taskana-import-export-component class="btn-group" [currentSelection]="selectionToImport"></taskana-import-export-component>
</div>
<div class="margin-right pull-right btn-group">
<taskana-sort [sortingFields]="sortingFields" (performSorting)="sorting($event)" class="btn-group"></taskana-sort>

View File

@ -22,6 +22,7 @@ import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { ClassificationDefinitionService } from 'app/administration/services/classification-definition/classification-definition.service';
import { WorkbasketDefinitionService } from 'app/administration/services/workbasket-definition/workbasket-definition.service';
import { configureTests } from 'app/app.test.configuration';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-dummy-detail',
@ -50,6 +51,7 @@ describe('WorkbasketListToolbarComponent', () => {
WorkbasketService,
ClassificationDefinitionService,
WorkbasketDefinitionService,
ImportExportService
]
})
};

View File

@ -22,7 +22,6 @@ export class WorkbasketListToolbarComponent implements OnInit {
@Input() workbaskets: Array<WorkbasketSummary>;
@Output() performSorting = new EventEmitter<SortingModel>();
@Output() performFilter = new EventEmitter<FilterModel>();
@Output() importSucessful = new EventEmitter();
workbasketServiceSubscription: Subscription;
selectionToImport = TaskanaType.WORKBASKETS;
sortingFields = new Map([['name', 'Name'], ['key', 'Key'], ['description', 'Description'], ['owner', 'Owner'], ['type', 'Type']]);
@ -54,7 +53,4 @@ export class WorkbasketListToolbarComponent implements OnInit {
this.router.navigate([{ outlets: { detail: ['new-workbasket'] } }], { relativeTo: this.route });
}
importEvent() {
this.importSucessful.emit();
}
}

View File

@ -22,6 +22,7 @@ import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { OrientationService } from 'app/services/orientation/orientation.service';
import { configureTests } from 'app/app.test.configuration';
import { Page } from 'app/models/page';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-dummy-detail',
@ -80,7 +81,8 @@ describe('WorkbasketListComponent', () => {
WorkbasketService,
WorkbasketDefinitionService,
ClassificationDefinitionService,
OrientationService
OrientationService,
ImportExportService
]
});
};

View File

@ -11,6 +11,7 @@ import {Orientation} from 'app/models/orientation';
import {WorkbasketService} from 'app/services/workbasket/workbasket.service'
import {OrientationService} from 'app/services/orientation/orientation.service';
import {TaskanaQueryParameters} from 'app/shared/util/query-parameters';
import { ImportExportService } from 'app/administration/services/import-export/import-export.service';
@Component({
selector: 'taskana-workbasket-list',
@ -38,12 +39,14 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
private workbasketServiceSubscription: Subscription;
private workbasketServiceSavedSubscription: Subscription;
private orientationSubscription: Subscription;
private importingExportingSubscription: Subscription;
constructor(
private workbasketService: WorkbasketService,
private router: Router,
private route: ActivatedRoute,
private orientationService: OrientationService) {
private orientationService: OrientationService,
private importExportService: ImportExportService) {
}
ngOnInit() {
@ -64,6 +67,10 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => {
this.refreshWorkbasketList();
});
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe((value: Boolean) => {
this.refreshWorkbasketList();
})
}
selectWorkbasket(id: string) {
@ -121,5 +128,8 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
this.orientationSubscription.unsubscribe();
}
if (this.importingExportingSubscription) {
this.importingExportingSubscription.unsubscribe();
}
}
}

View File

@ -6,5 +6,6 @@
<taskana-spinner [isRunning]="requestInProgress" isModal=true></taskana-spinner>
<taskana-alert></taskana-alert>
<taskana-remove-confirmation></taskana-remove-confirmation>
<taskana-progress-bar [ngClass]="{hidden: currentProgressValue === 0}" currentValue={{currentProgressValue}}></taskana-progress-bar>
</div>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { RequestInProgressService } from './services/requestInProgress/request-i
import { OrientationService } from './services/orientation/orientation.service';
import { SelectedRouteService } from './services/selected-route/selected-route';
import { FormsValidatorService } from 'app/shared/services/forms/forms-validator.service';
import { UploadService } from './shared/services/upload/upload.service';
@Component({
selector: 'taskana-root',
@ -24,11 +25,13 @@ export class AppComponent implements OnInit, OnDestroy {
selectedRoute = '';
requestInProgress = false;
currentProgressValue = 0;
errorModalSubscription: Subscription;
requestInProgressSubscription: Subscription;
selectedRouteSubscription: Subscription;
routerSubscription: Subscription;
uploadingFileSubscription: Subscription;
@HostListener('window:resize', ['$event'])
onResize(event) {
@ -41,7 +44,8 @@ export class AppComponent implements OnInit, OnDestroy {
private requestInProgressService: RequestInProgressService,
private orientationService: OrientationService,
private selectedRouteService: SelectedRouteService,
private formsValidatorService: FormsValidatorService) {
private formsValidatorService: FormsValidatorService,
public uploadService: UploadService) {
}
ngOnInit() {
@ -73,6 +77,9 @@ export class AppComponent implements OnInit, OnDestroy {
}
this.selectedRoute = value;
})
this.uploadingFileSubscription = this.uploadService.getCurrentProgressValue().subscribe(value => {
this.currentProgressValue = value;
})
}
ngOnDestroy() {
@ -80,5 +87,6 @@ export class AppComponent implements OnInit, OnDestroy {
if (this.errorModalSubscription) { this.errorModalSubscription.unsubscribe(); }
if (this.requestInProgressSubscription) { this.requestInProgressSubscription.unsubscribe(); }
if (this.selectedRouteSubscription) { this.selectedRouteSubscription.unsubscribe(); }
if (this.uploadingFileSubscription) { this.uploadingFileSubscription.unsubscribe(); }
}
}

View File

@ -14,7 +14,6 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeModule } from 'angular-tree-component';
import { SharedModule } from 'app/shared/shared.module';
/**
* Services
*/
@ -34,6 +33,7 @@ import { WindowRefService } from 'app/services/window/window.service';
import { TaskanaEngineService } from 'app/services/taskana-engine/taskana-engine.service';
import { RemoveConfirmationService } from './services/remove-confirmation/remove-confirmation.service';
import { FormsValidatorService } from './shared/services/forms/forms-validator.service';
import { UploadService } from './shared/services/upload/upload.service';
/**
* Components
@ -110,7 +110,8 @@ export function startupServiceFactory(startupService: StartupService): () => Pro
CustomFieldsService,
TaskanaEngineService,
RemoveConfirmationService,
FormsValidatorService
FormsValidatorService,
UploadService
],
bootstrap: [AppComponent]
})

View File

@ -71,5 +71,5 @@
<p id="taskana-version"> Taskana version: {{version}} </p>
</div>
</div>
<div *ngIf="showNavbar" class="navbar-backdrop" (click)="toogleNavBar()"></div>
</nav>
<div *ngIf="showNavbar" class="backdrop" (click)="toogleNavBar()"></div>
</nav>

View File

@ -48,17 +48,6 @@ h2.navbar-brand{
font-size: 20px;
}
.navbar-backdrop{
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 990;
background-color: grey;
opacity: 0.5;
}
.domain-form {
margin: 13px;
color: white;

View File

@ -1,8 +1,6 @@
import { Injectable } from '@angular/core';
import { Subject , Observable } from 'rxjs';
@Injectable()
export class RequestInProgressService {

View File

@ -31,6 +31,6 @@ ul.pagination{
}
.footer{
margin: 0 5px 0 0;
margin: 0px 5px 0 0;
color: $blue;
}

View File

@ -0,0 +1,7 @@
<div class="upload-file-container col-xs-12 col-md-4">
<div class="item progress-{{currentValue}}">
<div class="radial-inner-bg"><h1 class="blue"> {{currentValue}}%</h1></div>
</div>
<h4 class="blue">Uploading file</h4>
</div>
<div *ngIf="currentValue" class="mask"></div>

View File

@ -0,0 +1,11 @@
.upload-file-container{
z-index: 3000;
width: 170px;
position: absolute;
left: 50%;
top: 33%;
margin-left: -85px;
> .item{
margin-left: 10px;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProgressBarComponent } from './progress-bar.component';
describe('ProgressBarComponent', () => {
let component: ProgressBarComponent;
let fixture: ComponentFixture<ProgressBarComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProgressBarComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProgressBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core';
@Component({
selector: 'taskana-progress-bar',
templateUrl: './progress-bar.component.html',
styleUrls: ['./progress-bar.component.scss']
})
export class ProgressBarComponent implements OnInit, OnChanges {
@Input()
currentValue = 0;
@Input()
min = 0;
@Input()
max = 100;
inProgress = false;
constructor() { }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (!this.inProgress && changes.currentValue.currentValue > this.min) {
this.inProgress = true;
}
if (this.inProgress && changes.currentValue.currentValue >= this.max) {
this.inProgress = false;
}
}
}

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class UploadService {
private currentProgressValue = new Subject<number>();
public isInUse = false;
constructor() { }
setCurrentProgressValue(value: number) {
this.currentProgressValue.next(value);
}
getCurrentProgressValue() {
return this.currentProgressValue.asObservable();
}
}

View File

@ -23,6 +23,7 @@ import { FilterComponent } from 'app/shared/filter/filter.component';
import { IconTypeComponent } from 'app/administration/components/type-icon/icon-type.component';
import { FieldErrorDisplayComponent } from 'app/shared/field-error-display/field-error-display.component';
import { PaginationComponent } from './pagination/pagination.component';
import { ProgressBarComponent } from './progress-bar/progress-bar.component';
/**
* Pipes
@ -73,6 +74,7 @@ const DECLARATIONS = [
RemoveConfirmationComponent,
FieldErrorDisplayComponent,
PaginationComponent,
ProgressBarComponent,
];
@NgModule({

View File

@ -1,7 +1,7 @@
<div class="footer-space-pagination-list">
<div #wbToolbar>
<taskana-tasklist-toolbar (performSorting)="performSorting($event)" (performFilter)="performFilter($event)"
(selectSearchType)="selectSearchType($event)" (importSucessful)="refreshWorkbasketList()">
(selectSearchType)="selectSearchType($event)">
</taskana-tasklist-toolbar>
</div>
<div *ngIf="!requestInProgress">

View File

@ -4,4 +4,8 @@
}
.btn-group ul + .btn, {
margin-left: -1px;
}
}
.btn-group > div.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
border-radius: 0;
}

View File

@ -11,3 +11,4 @@
@import 'tabs';
@import 'bootstrap-3-backward-compatibility';
@import 'mixin/colors';
@import 'progress-bar';

View File

@ -0,0 +1,42 @@
@import '_colors';
.item {
margin-bottom: 20px;
position: relative;
width: 113px;
height: 113px;
border-radius: 50%;
border: 1px solid #fff;
background-color: #FFF;
float: left;
margin-right: 20px;
}
.radial-inner-bg {
border-radius: 50%;
width: 100px;
height: 100px;
top: 5px;
left: 5px;
background: #FFF;
position: absolute;
>h1 {
margin: 30px 0 0 5px;
}
}
$step: 1; // step of % for created classes
$loops: 100;
$increment: (360 / $loops);
$half: round($loops / 2);
@for $i from 0 through $loops {
.progress-#{$i*$step} {
@if $i < 50 {
$nextdeg: 90deg + ( $increment * $i );
background-image: linear-gradient(90deg, #fff 50%, transparent 50%, transparent), linear-gradient($nextdeg, $pallete-green 51%, #fff 50%, #fff);
}
@else {
$nextdeg: -90deg + ( $increment * ( $i - $half ) );
background-image: linear-gradient($nextdeg, $pallete-green 51%, transparent 50%, transparent), linear-gradient(270deg, $pallete-green 50%, #fff 50%, #fff);
}
}
}

View File

@ -395,3 +395,25 @@ li.list-group-item:hover {
.padding-right.pull-right {
padding-right: 15px;
}
.backdrop{
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 990;
background-color: white;
opacity: 0.8;
}
.mask {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 2000;
background-color: white;
opacity: 0.8;
}