diff --git a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java index 4a08a8a74..6f5f98f49 100644 --- a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java +++ b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java @@ -200,6 +200,11 @@ public final class Permission { public static String EditPrefillingSource= "EditPrefillingSource"; public static String DeletePrefillingSource = "DeletePrefillingSource"; + //NotificationTemplate + public static String BrowseStatus = "BrowseStatus"; + public static String EditStatus = "EditStatus"; + public static String DeleteStatus = "DeleteStatus"; + // UI Pages public static String ViewDescriptionTemplateTypePage = "ViewDescriptionTemplateTypePage"; @@ -224,5 +229,6 @@ public final class Permission { public static String ViewHomePage = "ViewHomePage"; public static String ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage"; public static String ViewTenantConfigurationPage = "ViewTenantConfigurationPage"; + public static String ViewStatusPage = "ViewStatusPage"; } diff --git a/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java index 8af2748ef..a3a268a07 100644 --- a/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java @@ -258,7 +258,7 @@ public class FileTransformerServiceImpl implements FileTransformerService { FileTransformerRepository repository = this.getRepository(repositoryId); if (repository == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{format, FileTransformerRepository.class.getSimpleName()}, LocaleContextHolder.getLocale())); //GK: Second get the Target Data Management Plan - DmpQuery query = this.queryFactory.query(DmpQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).ids(dmpId); + DmpQuery query = this.queryFactory.query(DmpQuery.class).disableTracking().authorize(AuthorizationFlags.All).ids(dmpId); DmpModel dmpFileTransformerModel = this.builderFactory.builder(DmpCommonModelBuilder.class).useSharedStorage(repository.getConfiguration().isUseSharedStorage()).setRepositoryId(repository.getConfiguration().getFileTransformerId()).authorize(AuthorizationFlags.AllExceptPublic).build(query.first()); if (dmpFileTransformerModel == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{dmpId, Dmp.class.getSimpleName()}, LocaleContextHolder.getLocale())); @@ -279,7 +279,7 @@ public class FileTransformerServiceImpl implements FileTransformerService { if (repository == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{format, FileTransformerRepository.class.getSimpleName()}, LocaleContextHolder.getLocale())); //GK: Second get the Target Data Management Plan - DescriptionQuery query = this.queryFactory.query(DescriptionQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).ids(descriptionId); + DescriptionQuery query = this.queryFactory.query(DescriptionQuery.class).disableTracking().authorize(AuthorizationFlags.All).ids(descriptionId); DescriptionModel descriptionFileTransformerModel = this.builderFactory.builder(DescriptionCommonModelBuilder.class).setRepositoryId(repository.getConfiguration().getFileTransformerId()).useSharedStorage(repository.getConfiguration().isUseSharedStorage()).authorize(AuthorizationFlags.AllExceptPublic).build(query.first()); if (descriptionFileTransformerModel == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{descriptionId, Description.class.getSimpleName()}, LocaleContextHolder.getLocale())); diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DepositController.java b/backend/web/src/main/java/org/opencdmp/controllers/DepositController.java index 2e2d20859..136e13ac2 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DepositController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DepositController.java @@ -7,7 +7,18 @@ import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.MapLogEntry; import gr.cite.tools.validation.ValidationFilterAnnotation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.opencdmp.audit.AuditableAction; +import org.opencdmp.controllers.swagger.SwaggerHelpers; +import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader; +import org.opencdmp.controllers.swagger.annotation.Swagger400; +import org.opencdmp.controllers.swagger.annotation.Swagger404; +import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; import org.opencdmp.model.EntityDoi; import org.opencdmp.model.censorship.EntityDoiCensor; import org.opencdmp.model.censorship.deposit.DepositConfigurationCensor; @@ -35,11 +46,12 @@ import java.util.Map; @RestController @CrossOrigin @RequestMapping("/api/deposit/") +@Tag(name = "Deposit", description = "Manage deposit repositories, make deposits") +@SwaggerCommonErrorResponses public class DepositController { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(DepositController.class)); - private final DepositService depositService; private final CensorFactory censorFactory; @@ -53,11 +65,18 @@ public class DepositController { this.depositService = depositService; this.censorFactory = censorFactory; this.auditService = auditService; - this.messageSource = messageSource; + this.messageSource = messageSource; } @GetMapping("/repositories/available") - public List getAvailableRepos(FieldSet fieldSet) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + @OperationWithTenantHeader(summary = "Fetch all repositories", description = SwaggerHelpers.Deposit.endpoint_get_available_repos, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = DepositConfiguration.class + ))) + )) + public List getAvailableRepos(FieldSet fieldSet) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { logger.debug(new MapLogEntry("retrieving" + DepositConfiguration.class.getSimpleName()).And("fields", fieldSet)); this.censorFactory.censor(DepositConfigurationCensor.class).censor(fieldSet, null); @@ -71,6 +90,13 @@ public class DepositController { } @PostMapping("/deposit") + @OperationWithTenantHeader(summary = "Make a plan deposit request", description = SwaggerHelpers.Deposit.endpoint_deposit, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = EntityDoi.class + )) + )) + @Swagger400 @Transactional @ValidationFilterAnnotation(validator = DepositRequest.DepositRequestValidator.ValidatorName, argumentName = "model") public EntityDoi deposit(@RequestBody DepositRequest model) throws Exception { @@ -86,15 +112,26 @@ public class DepositController { } @GetMapping("/repositories/{repositoryId}") - public org.opencdmp.model.deposit.DepositConfiguration getRepository(@PathVariable("repositoryId") String repositoryId, FieldSet fieldSet) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + @OperationWithTenantHeader(summary = "Fetch a specific deposit repository by id", description = SwaggerHelpers.Deposit.endpoint_get_repository, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DepositConfiguration.class + )) + )) + @Swagger404 + public DepositConfiguration getRepository( + @Parameter(name = "repositoryId", description = "The id of a repository to fetch", example = "zenodo", required = true) @PathVariable("repositoryId") String repositoryId, + @Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet + ) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { logger.debug(new MapLogEntry("retrieving" + DepositConfiguration.class.getSimpleName()).And("fields", fieldSet).And("repositoryId", repositoryId)); this.censorFactory.censor(DepositConfigurationCensor.class).censor(fieldSet, null); List models = this.depositService.getAvailableConfigurations(fieldSet); - org.opencdmp.model.deposit.DepositConfiguration model = models.stream().filter(x-> x.getRepositoryId().equals(repositoryId)).findFirst().orElse(null); - if (model == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{repositoryId, org.opencdmp.model.deposit.DepositConfiguration.class.getSimpleName()}, LocaleContextHolder.getLocale())); - + org.opencdmp.model.deposit.DepositConfiguration model = models.stream().filter(x -> x.getRepositoryId().equals(repositoryId)).findFirst().orElse(null); + if (model == null) + throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{repositoryId, org.opencdmp.model.deposit.DepositConfiguration.class.getSimpleName()}, LocaleContextHolder.getLocale())); + this.auditService.track(AuditableAction.Deposit_GetRepository, Map.ofEntries( new AbstractMap.SimpleEntry("repositoryId", repositoryId), new AbstractMap.SimpleEntry("fields", fieldSet) @@ -104,7 +141,16 @@ public class DepositController { } @GetMapping("/repositories/{repositoryId}/logo") - public String getLogo(@PathVariable("repositoryId") String repositoryId) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + @OperationWithTenantHeader(summary = "Fetch a specific deposit repository logo by id", description = SwaggerHelpers.Deposit.endpoint_get_logo, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = String.class + )) + )) + @Swagger404 + public String getLogo( + @Parameter(name = "repositoryId", description = "The id of a repository of which to fetch the logo", example = "zenodo", required = true) @PathVariable("repositoryId") String repositoryId + ) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { logger.debug(new MapLogEntry("get logo" + DepositConfiguration.class.getSimpleName()).And("repositoryId", repositoryId)); String logo = this.depositService.getLogo(repositoryId); diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java index 16ab7bbac..c6a25ae60 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java @@ -195,7 +195,12 @@ public class DescriptionController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific description by id") + @OperationWithTenantHeader(summary = "Fetch a specific description by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Description.class + )) + )) @Swagger404 public Description get( @Parameter(name = "id", description = "The id of a description to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -220,7 +225,12 @@ public class DescriptionController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing description") + @OperationWithTenantHeader(summary = "Create a new or update an existing description", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Description.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -243,7 +253,12 @@ public class DescriptionController { } @PostMapping("persist-status") - @OperationWithTenantHeader(summary = "Update the status of an existing description") + @OperationWithTenantHeader(summary = "Update the status of an existing description", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Description.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -298,7 +313,8 @@ public class DescriptionController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete a description by id") + @OperationWithTenantHeader(summary = "Delete a description by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void delete( @@ -312,7 +328,8 @@ public class DescriptionController { } @GetMapping("{id}/export/{type}") - @OperationWithTenantHeader(summary = "Export a description in various formats by id") + @OperationWithTenantHeader(summary = "Export a description in various formats by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public ResponseEntity export( @Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -324,7 +341,12 @@ public class DescriptionController { } @PostMapping("field-file/upload") - @OperationWithTenantHeader(summary = "Upload a file attachment on a field that supports it") + @OperationWithTenantHeader(summary = "Upload a file attachment on a field that supports it", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = StorageFile.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -348,7 +370,8 @@ public class DescriptionController { } @GetMapping("{id}/field-file/{fileId}") - @OperationWithTenantHeader(summary = "Fetch a field file attachment as byte array") + @OperationWithTenantHeader(summary = "Fetch a field file attachment as byte array", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public ResponseEntity getFieldFile( @Parameter(name = "id", description = "The id of a description", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -377,7 +400,8 @@ public class DescriptionController { } @PostMapping("update-description-template") - @OperationWithTenantHeader(summary = "Change the template of a description") + @OperationWithTenantHeader(summary = "Change the template of a description", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger400 @Swagger404 @Transactional @@ -394,7 +418,8 @@ public class DescriptionController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @OperationWithTenantHeader(summary = "Export a description in xml format by id") + @OperationWithTenantHeader(summary = "Export a description in xml format by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateController.java b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateController.java index 591988ef4..0dcc5e3c6 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateController.java @@ -134,7 +134,12 @@ public class DescriptionTemplateController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific description template by id") + @OperationWithTenantHeader(summary = "Fetch a specific description template by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplate.class + )) + )) @Swagger404 public DescriptionTemplate get( @Parameter(name = "id", description = "The id of a description template to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -159,7 +164,12 @@ public class DescriptionTemplateController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing description template") + @OperationWithTenantHeader(summary = "Create a new or update an existing description template", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplate.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -182,7 +192,8 @@ public class DescriptionTemplateController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete a description template by id") + @OperationWithTenantHeader(summary = "Delete a description template by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void delete( @@ -196,7 +207,12 @@ public class DescriptionTemplateController { } @GetMapping("clone/{id}") - @OperationWithTenantHeader(summary = "Clone a description template by id") + @OperationWithTenantHeader(summary = "Clone a description template by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplate.class + )) + )) @Swagger404 public DescriptionTemplate buildClone( @Parameter(name = "id", description = "The id of a description template to clone", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -218,7 +234,12 @@ public class DescriptionTemplateController { } @PostMapping("new-version") - @OperationWithTenantHeader(summary = "Create a new version of a description template") + @OperationWithTenantHeader(summary = "Create a new version of a description template", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplate.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -240,7 +261,8 @@ public class DescriptionTemplateController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @OperationWithTenantHeader(summary = "Export a description template in xml by id") + @OperationWithTenantHeader(summary = "Export a description template in xml by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a description template to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id @@ -257,7 +279,12 @@ public class DescriptionTemplateController { } @RequestMapping(method = RequestMethod.POST, value = {"/xml/import/{groupId}", "/xml/import"}) - @OperationWithTenantHeader(summary = "Import a description template from an xml file") + @OperationWithTenantHeader(summary = "Import a description template from an xml file", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplate.class + )) + )) @Transactional public DescriptionTemplate importXml( @RequestParam("file") MultipartFile file, diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateTypeController.java b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateTypeController.java index 49c404928..dabe332c1 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateTypeController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionTemplateTypeController.java @@ -116,7 +116,12 @@ public class DescriptionTemplateTypeController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific description template type by id") + @OperationWithTenantHeader(summary = "Fetch a specific description template type by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplateType.class + )) + )) @Swagger404 public DescriptionTemplateType Get( @Parameter(name = "id", description = "The id of a description template type to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -141,7 +146,12 @@ public class DescriptionTemplateTypeController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing description template type") + @OperationWithTenantHeader(summary = "Create a new or update an existing description template type", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DescriptionTemplateType.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -162,7 +172,8 @@ public class DescriptionTemplateTypeController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete a description template type by id") + @OperationWithTenantHeader(summary = "Delete a description template type by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void Delete( diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DmpBlueprintController.java b/backend/web/src/main/java/org/opencdmp/controllers/DmpBlueprintController.java index c58fc4b0a..bc344b6e5 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DmpBlueprintController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DmpBlueprintController.java @@ -29,7 +29,6 @@ import org.opencdmp.controllers.swagger.annotation.Swagger400; import org.opencdmp.controllers.swagger.annotation.Swagger404; import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; import org.opencdmp.data.DmpBlueprintEntity; -import org.opencdmp.model.DescriptionTemplateType; import org.opencdmp.model.builder.dmpblueprint.DmpBlueprintBuilder; import org.opencdmp.model.censorship.dmpblueprint.DmpBlueprintCensor; import org.opencdmp.model.dmpblueprint.DmpBlueprint; @@ -127,7 +126,12 @@ public class DmpBlueprintController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific plan blueprint by id") + @OperationWithTenantHeader(summary = "Fetch a specific plan blueprint by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DmpBlueprint.class + )) + )) @Swagger404 public DmpBlueprint get( @Parameter(name = "id", description = "The id of a plan blueprint to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -151,7 +155,12 @@ public class DmpBlueprintController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing plan blueprint") + @OperationWithTenantHeader(summary = "Create a new or update an existing plan blueprint", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DmpBlueprint.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -174,7 +183,8 @@ public class DmpBlueprintController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete a plan blueprint by id") + @OperationWithTenantHeader(summary = "Delete a plan blueprint by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void delete( @@ -188,7 +198,12 @@ public class DmpBlueprintController { } @GetMapping("clone/{id}") - @OperationWithTenantHeader(summary = "Clone a plan blueprint by id") + @OperationWithTenantHeader(summary = "Clone a plan blueprint by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DmpBlueprint.class + )) + )) @Swagger404 public DmpBlueprint buildClone( @Parameter(name = "id", description = "The id of a plan blueprint to clone", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -209,7 +224,12 @@ public class DmpBlueprintController { } @PostMapping("new-version") - @OperationWithTenantHeader(summary = "Create a new version of an existing plan blueprint") + @OperationWithTenantHeader(summary = "Create a new version of an existing plan blueprint", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DmpBlueprint.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -230,7 +250,8 @@ public class DmpBlueprintController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @OperationWithTenantHeader(summary = "Export a plan blueprint in xml format by id") + @OperationWithTenantHeader(summary = "Export a plan blueprint in xml format by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a plan blueprint to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id @@ -246,7 +267,12 @@ public class DmpBlueprintController { } @RequestMapping(method = RequestMethod.POST, value = {"/xml/import/{groupId}", "/xml/import"}) - @OperationWithTenantHeader(summary = "Import a plan blueprint from an xml file") + @OperationWithTenantHeader(summary = "Import a plan blueprint from an xml file", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = DmpBlueprint.class + )) + )) @Transactional public DmpBlueprint importXml( @RequestParam("file") MultipartFile file, diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java b/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java index ae1fef59a..51fde57a4 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java @@ -179,7 +179,12 @@ public class DmpController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific plan by id") + @OperationWithTenantHeader(summary = "Fetch a specific plan by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @Swagger404 public Dmp Get( @Parameter(name = "id", description = "The id of a plan to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -204,7 +209,12 @@ public class DmpController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing plan") + @OperationWithTenantHeader(summary = "Create a new or update an existing plan", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -226,7 +236,8 @@ public class DmpController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete a plan by id") + @OperationWithTenantHeader(summary = "Delete a plan by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void Delete( @@ -240,7 +251,8 @@ public class DmpController { } @PostMapping("finalize/{id}") - @OperationWithTenantHeader(summary = "Finalize a plan by id") + @OperationWithTenantHeader(summary = "Finalize a plan by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public boolean finalize( @@ -260,7 +272,8 @@ public class DmpController { } @GetMapping("undo-finalize/{id}") - @OperationWithTenantHeader(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)") + @OperationWithTenantHeader(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public boolean undoFinalize( @@ -298,7 +311,12 @@ public class DmpController { } @PostMapping("clone") - @OperationWithTenantHeader(summary = "Create a clone of an existing plan") + @OperationWithTenantHeader(summary = "Create a clone of an existing plan", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -322,7 +340,12 @@ public class DmpController { } @PostMapping("new-version") - @OperationWithTenantHeader(summary = "Create a new version of an existing plan") + @OperationWithTenantHeader(summary = "Create a new version of an existing plan", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -380,7 +403,8 @@ public class DmpController { } @GetMapping("{id}/export/{transformerId}/{type}") - @OperationWithTenantHeader(summary = "Export a plan in various formats by id") + @OperationWithTenantHeader(summary = "Export a plan in various formats by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public ResponseEntity export( @Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -432,7 +456,8 @@ public class DmpController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @OperationWithTenantHeader(summary = "Export a plan in xml format by id") + @OperationWithTenantHeader(summary = "Export a plan in xml format by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id @@ -448,7 +473,12 @@ public class DmpController { } @RequestMapping(method = RequestMethod.POST, value = "/xml/import") - @OperationWithTenantHeader(summary = "Import a plan from an xml file") + @OperationWithTenantHeader(summary = "Import a plan from an xml file", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @Transactional public Dmp importXml( @RequestParam("file") MultipartFile file, @@ -467,7 +497,12 @@ public class DmpController { } @PostMapping("json/preprocessing") - @OperationWithTenantHeader(summary = "preprocessing a plan from an json file") + @OperationWithTenantHeader(summary = "Preprocess a plan from a json file", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = PreprocessingDmpModel.class + )) + )) @Transactional public PreprocessingDmpModel preprocessing( @RequestParam("fileId") UUID fileId, @@ -486,7 +521,12 @@ public class DmpController { } @PostMapping("json/import") - @OperationWithTenantHeader(summary = "Import a plan from an json file") + @OperationWithTenantHeader(summary = "Import a plan from an json file", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = Dmp.class + )) + )) @ValidationFilterAnnotation(validator = DmpCommonModelConfig.DmpCommonModelConfigValidator.ValidatorName, argumentName = "model") @Transactional public Dmp importJson( diff --git a/backend/web/src/main/java/org/opencdmp/controllers/EntityDoiController.java b/backend/web/src/main/java/org/opencdmp/controllers/EntityDoiController.java index 58ba122aa..742e091d5 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/EntityDoiController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/EntityDoiController.java @@ -30,7 +30,6 @@ import org.opencdmp.model.DescriptionTemplateType; import org.opencdmp.model.EntityDoi; import org.opencdmp.model.builder.EntityDoiBuilder; import org.opencdmp.model.censorship.EntityDoiCensor; -import org.opencdmp.model.dmpblueprint.DmpBlueprint; import org.opencdmp.model.persist.EntityDoiPersist; import org.opencdmp.model.result.QueryResult; import org.opencdmp.query.EntityDoiQuery; @@ -116,7 +115,12 @@ public class EntityDoiController { } @GetMapping("{id}") - @OperationWithTenantHeader(summary = "Fetch a specific entity doi by id") + @OperationWithTenantHeader(summary = "Fetch a specific entity doi by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = EntityDoi.class + )) + )) @Swagger404 public EntityDoi get( @Parameter(name = "id", description = "The id of an entity doi to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -141,7 +145,12 @@ public class EntityDoiController { } @PostMapping("persist") - @OperationWithTenantHeader(summary = "Create a new or update an existing entity doi") + @OperationWithTenantHeader(summary = "Create a new or update an existing entity doi", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = EntityDoi.class + )) + )) @Swagger400 @Swagger404 @Transactional @@ -162,7 +171,8 @@ public class EntityDoiController { } @DeleteMapping("{id}") - @OperationWithTenantHeader(summary = "Delete an entity doi by id") + @OperationWithTenantHeader(summary = "Delete an entity doi by id", description = "", + responses = @ApiResponse(description = "OK", responseCode = "200")) @Swagger404 @Transactional public void delete( diff --git a/backend/web/src/main/java/org/opencdmp/controllers/FileTransformerController.java b/backend/web/src/main/java/org/opencdmp/controllers/FileTransformerController.java index 0846f2ea7..14fd21e11 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/FileTransformerController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/FileTransformerController.java @@ -1,6 +1,15 @@ package org.opencdmp.controllers; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.opencdmp.audit.AuditableAction; +import org.opencdmp.controllers.swagger.SwaggerHelpers; +import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader; +import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; +import org.opencdmp.model.deposit.DepositConfiguration; import org.opencdmp.model.file.ExportRequestModel; import org.opencdmp.model.file.FileEnvelope; import org.opencdmp.model.file.RepositoryFileFormat; @@ -29,6 +38,8 @@ import java.util.List; @RestController @CrossOrigin @RequestMapping(value = {"/api/file-transformer/"}) +@Tag(name = "File Transformers", description = "Manage file transformers, perform exports") +@SwaggerCommonErrorResponses public class FileTransformerController { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(FileTransformerController.class)); @@ -43,6 +54,13 @@ public class FileTransformerController { } @GetMapping("/available") + @OperationWithTenantHeader(summary = "Fetch all transformers", description = SwaggerHelpers.FileTransformer.endpoint_get_available_transformers, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = RepositoryFileFormat.class + ))) + )) public List getAvailableConfigurations() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { logger.debug(new MapLogEntry("getAvailableConfigurations")); @@ -54,8 +72,10 @@ public class FileTransformerController { } @PostMapping("/export-dmp") - public ResponseEntity export(@RequestBody ExportRequestModel requestModel) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { - logger.debug(new MapLogEntry("exporting dmp")); + @OperationWithTenantHeader(summary = "Export a plan", description = SwaggerHelpers.FileTransformer.endpoint_export_plans, + responses = @ApiResponse(description = "OK", responseCode = "200")) + public ResponseEntity exportPlan(@RequestBody ExportRequestModel requestModel) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + logger.debug(new MapLogEntry("exporting plan")); HttpHeaders headers = new HttpHeaders(); FileEnvelope fileEnvelope = this.fileTransformerService.exportDmp(requestModel.getId(), requestModel.getRepositoryId(), requestModel.getFormat()); @@ -66,8 +86,10 @@ public class FileTransformerController { } @PostMapping("/export-description") + @OperationWithTenantHeader(summary = "Export a description", description = SwaggerHelpers.FileTransformer.endpoint_export_descriptions, + responses = @ApiResponse(description = "OK", responseCode = "200")) public ResponseEntity exportDescription(@RequestBody ExportRequestModel requestModel) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { - logger.debug(new MapLogEntry("exporting dmp")); + logger.debug(new MapLogEntry("exporting description")); HttpHeaders headers = new HttpHeaders(); FileEnvelope fileEnvelope = this.fileTransformerService.exportDescription(requestModel.getId(), requestModel.getRepositoryId(), requestModel.getFormat()); diff --git a/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java b/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java index c7114e0fa..ee20f7032 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java @@ -2550,6 +2550,23 @@ public class SwaggerHelpers { public static class FileTransformer { + public static final String endpoint_get_available_transformers = + """ + This endpoint is used to fetch all the available file transformers.
+ """; + + public static final String endpoint_export_plans = + """ + This endpoint is used to export a plan using a specific file transformer.
+ Choosing the transformer will determine the format of the export. + """; + + public static final String endpoint_export_descriptions = + """ + This endpoint is used to export a plan using a specific file transformer. + Choosing the transformer will determine the format of the export. + """; + } public static class EntityDoi { @@ -2815,10 +2832,31 @@ public class SwaggerHelpers { "count":118 } """; + } public static class Deposit { + public static final String endpoint_get_available_repos = + """ + This endpoint is used to fetch all the available deposit repositories. + """; + + public static final String endpoint_deposit = + """ + This endpoint is used to deposit a plan in a repository. + """; + + public static final String endpoint_get_repository = + """ + This endpoint is used to get information about a specific repository. + """; + + public static final String endpoint_get_logo = + """ + This endpoint is used to fetch the logo url of a repository. + """; + } } diff --git a/backend/web/src/main/resources/config/permissions.yml b/backend/web/src/main/resources/config/permissions.yml index 2d9a29a9f..a0922bd7c 100644 --- a/backend/web/src/main/resources/config/permissions.yml +++ b/backend/web/src/main/resources/config/permissions.yml @@ -1060,6 +1060,29 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + # Status + BrowseStatus: + roles: + - Admin + - TenantAdmin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + EditStatus: + roles: + - Admin + - TenantAdmin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + DeleteStatus: + roles: + - Admin + - TenantAdmin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false # ViewPage Permissions ViewDescriptionTemplateTypePage: @@ -1206,6 +1229,13 @@ permissions: allowAnonymous: false allowAuthenticated: true ViewTenantConfigurationPage: + roles: + - Admin + - TenantAdmin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + ViewStatusPage: roles: - Admin - TenantAdmin diff --git a/backend/web/src/main/resources/config/swagger.yml b/backend/web/src/main/resources/config/swagger.yml index 0310255b0..72d70a5b0 100644 --- a/backend/web/src/main/resources/config/swagger.yml +++ b/backend/web/src/main/resources/config/swagger.yml @@ -11,7 +11,7 @@ springdoc: displayName: Current packagesToScan: org.opencdmp.controllers packagesToExclude: org.opencdmp.controllers.publicapi - pathsToMatch: "/api/dmp/**, /api/description/**, /api/description-template/**, /api/description-template-type/**, /api/dmp-blueprint/**, /api/entity-doi/**" + pathsToMatch: "/api/dmp/**, /api/description/**, /api/description-template/**, /api/description-template-type/**, /api/dmp-blueprint/**, /api/entity-doi/**, /api/deposit/**, /api/file-transformer/**" swaggerUi: enabled: true useRootPath: true diff --git a/dmp-frontend/src/annotation-service/core/enum/internal-status.enum.ts b/dmp-frontend/src/annotation-service/core/enum/internal-status.enum.ts new file mode 100644 index 000000000..37e9db80e --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/enum/internal-status.enum.ts @@ -0,0 +1,3 @@ +export enum InternalStatus { + Resolved = 0, +} diff --git a/dmp-frontend/src/annotation-service/core/enum/is-active.enum.ts b/dmp-frontend/src/annotation-service/core/enum/is-active.enum.ts new file mode 100644 index 000000000..cdadd23b4 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/enum/is-active.enum.ts @@ -0,0 +1,4 @@ +export enum IsActive { + Inactive = 0, + Active = 1 +} diff --git a/dmp-frontend/src/annotation-service/core/formatting/enum-utils.service.ts b/dmp-frontend/src/annotation-service/core/formatting/enum-utils.service.ts new file mode 100644 index 000000000..e43362098 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/formatting/enum-utils.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { BaseEnumUtilsService } from '@common/base/base-enum-utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { IsActive } from '../enum/is-active.enum'; +import { InternalStatus } from '../enum/internal-status.enum'; + +@Injectable() +export class AnnotationServiceEnumUtils extends BaseEnumUtilsService { + constructor(private language: TranslateService) { super(); } + + + toIsActiveString(value: IsActive): string { + switch (value) { + case IsActive.Active: return this.language.instant('ANNOTATION-SERVICE.TYPES.IS-ACTIVE.ACTIVE'); + case IsActive.Inactive: return this.language.instant('ANNOTATION-SERVICE.TYPES.IS-ACTIVE.INACTIVE'); + default: return ''; + } + } + + toInternalStatusString(status: InternalStatus): string { + switch (status) { + case InternalStatus.Resolved: return this.language.instant('ANNOTATION-SERVICE.TYPES.INTERNAL-STATUS.RESOLVED'); + } + } + +} diff --git a/dmp-frontend/src/annotation-service/core/formatting/formatting.module.ts b/dmp-frontend/src/annotation-service/core/formatting/formatting.module.ts new file mode 100644 index 000000000..e7f028491 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/formatting/formatting.module.ts @@ -0,0 +1,36 @@ + + +// +// +// This is shared module that provides all notification service formatting utils. Its imported only once. +// + +import { CommonFormattingModule } from "@common/formatting/common-formatting.module"; +import { IsActiveTypePipe } from "./pipes/is-active-type.pipe"; +import { InternalStatusTypePipe } from "./pipes/internal-status-type.pipe"; +import { PipeService } from "@common/formatting/pipe.service"; +import { AnnotationServiceEnumUtils } from "./enum-utils.service"; +import { NgModule } from "@angular/core"; + +// +@NgModule({ + imports: [ + CommonFormattingModule, + ], + declarations: [ + IsActiveTypePipe, + InternalStatusTypePipe + ], + exports: [ + CommonFormattingModule, + IsActiveTypePipe, + InternalStatusTypePipe, + ], + providers: [ + PipeService, + IsActiveTypePipe, + InternalStatusTypePipe, + AnnotationServiceEnumUtils + ] +}) +export class AnnotationServiceFormattingModule { } diff --git a/dmp-frontend/src/annotation-service/core/formatting/pipes/internal-status-type.pipe.ts b/dmp-frontend/src/annotation-service/core/formatting/pipes/internal-status-type.pipe.ts new file mode 100644 index 000000000..1a61c2ab1 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/formatting/pipes/internal-status-type.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { AnnotationServiceEnumUtils } from '../enum-utils.service'; + +@Pipe({ name: 'InternalStatusTypeFormat' }) +export class InternalStatusTypePipe implements PipeTransform { + constructor(private enumUtils: AnnotationServiceEnumUtils) { } + + public transform(value): any { + return this.enumUtils.toInternalStatusString(value); + } +} diff --git a/dmp-frontend/src/annotation-service/core/formatting/pipes/is-active-type.pipe.ts b/dmp-frontend/src/annotation-service/core/formatting/pipes/is-active-type.pipe.ts new file mode 100644 index 000000000..458a8b5c6 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/formatting/pipes/is-active-type.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { AnnotationServiceEnumUtils } from '../enum-utils.service'; + +@Pipe({ name: 'IsActiveTypeFormat' }) +export class IsActiveTypePipe implements PipeTransform { + constructor(private enumUtils: AnnotationServiceEnumUtils) { } + + public transform(value): any { + return this.enumUtils.toIsActiveString(value); + } +} diff --git a/dmp-frontend/src/annotation-service/core/model/annotation-status.model.ts b/dmp-frontend/src/annotation-service/core/model/annotation-status.model.ts new file mode 100644 index 000000000..7e2c651c4 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/model/annotation-status.model.ts @@ -0,0 +1,16 @@ +import { BaseEntity } from "@common/base/base-entity.model"; +import { Annotation } from "./annotation.model"; +import { Status } from "./status.model"; +import { Guid } from "@common/types/guid"; + +export interface AnnotationStatus extends BaseEntity { + annotation: Annotation; + status: Status; +} + +// persist + +export interface AnnotationStatusPersist extends BaseEntity { + annotationId: Guid; + statusId: Guid; +} \ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/core/model/annotation.model.ts b/dmp-frontend/src/annotation-service/core/model/annotation.model.ts index 8c868f66c..6a5cfa871 100644 --- a/dmp-frontend/src/annotation-service/core/model/annotation.model.ts +++ b/dmp-frontend/src/annotation-service/core/model/annotation.model.ts @@ -2,6 +2,7 @@ import { AnnotationProtectionType } from "@app/core/common/enum/annotation-prote import { User } from "@app/core/model/user/user"; import { BaseEntity, BaseEntityPersist } from "@common/base/base-entity.model"; import { Guid } from "@common/types/guid"; +import { AnnotationStatus } from "./annotation-status.model"; export interface Annotation extends BaseEntity { entityId: Guid; @@ -13,6 +14,7 @@ export interface Annotation extends BaseEntity { threadId: Guid; parent: Annotation; protectionType: AnnotationProtectionType; + annotationStatuses: AnnotationStatus[]; } export interface AnnotationPersist extends BaseEntityPersist { diff --git a/dmp-frontend/src/annotation-service/core/model/status.model.ts b/dmp-frontend/src/annotation-service/core/model/status.model.ts new file mode 100644 index 000000000..aa374663f --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/model/status.model.ts @@ -0,0 +1,12 @@ +import { BaseEntity, BaseEntityPersist } from "@common/base/base-entity.model"; +import { InternalStatus } from "../enum/internal-status.enum"; + +export interface Status extends BaseEntity { + label: string; + internalStatus: InternalStatus; +} + +export interface StatusPersist extends BaseEntityPersist { + label: string; + internalStatus: InternalStatus; +} \ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/core/query/status.lookup.ts b/dmp-frontend/src/annotation-service/core/query/status.lookup.ts new file mode 100644 index 000000000..3078f8703 --- /dev/null +++ b/dmp-frontend/src/annotation-service/core/query/status.lookup.ts @@ -0,0 +1,26 @@ +import { IsActive } from "@app/core/common/enum/is-active.enum"; +import { Lookup } from "@common/model/lookup"; +import { Guid } from "@common/types/guid"; +import { InternalStatus } from "../enum/internal-status.enum"; + +export class StatusLookup extends Lookup implements StatusFilter { + + like: string; + ids: Guid[]; + excludedIds: Guid[]; + isActive: IsActive[]; + internalStatuses: InternalStatus[]; + + constructor() { + super(); + } + +} + +export interface StatusFilter { + like: string; + ids: Guid[]; + excludedIds: Guid[]; + isActive: IsActive[]; + internalStatuses: InternalStatus[]; +} \ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/services/core-service.module.ts b/dmp-frontend/src/annotation-service/services/core-service.module.ts index 58e1e9189..579cd9ddb 100644 --- a/dmp-frontend/src/annotation-service/services/core-service.module.ts +++ b/dmp-frontend/src/annotation-service/services/core-service.module.ts @@ -11,6 +11,7 @@ import { FormService } from "@common/forms/form-service"; import { HttpErrorHandlingService } from "@common/modules/errors/error-handling/http-error-handling.service"; import { FilterService } from "@common/modules/text-filter/filter-service"; import { AnnotationService } from "@annotation-service/services/http/annotation.service"; +import { StatusService } from "./http/status.service"; @NgModule({}) export class CoreAnnotationServiceModule { @@ -30,7 +31,8 @@ export class CoreAnnotationServiceModule { FormService, LoggingService, PrincipalService, - AnnotationService + AnnotationService, + StatusService ], }; } diff --git a/dmp-frontend/src/annotation-service/services/http/annotation.service.ts b/dmp-frontend/src/annotation-service/services/http/annotation.service.ts index d89f5d059..44c683da1 100644 --- a/dmp-frontend/src/annotation-service/services/http/annotation.service.ts +++ b/dmp-frontend/src/annotation-service/services/http/annotation.service.ts @@ -1,4 +1,5 @@ import { Injectable } from "@angular/core"; +import { AnnotationStatus, AnnotationStatusPersist } from "@annotation-service/core/model/annotation-status.model"; import { Annotation, AnnotationPersist } from "@annotation-service/core/model/annotation.model"; import { AnnotationLookup } from "@annotation-service/core/query/annotation.lookup"; import { ConfigurationService } from "@app/core/services/configuration/configuration.service"; @@ -42,6 +43,14 @@ export class AnnotationService { catchError((error: any) => throwError(error))); } + persistStatus(item: AnnotationStatusPersist) { + const url = `${this.apiBase}/persist-status`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(error))); + } + delete(id: Guid, ): Observable { const url = `${this.apiBase}/${id}`; return this.http diff --git a/dmp-frontend/src/annotation-service/services/http/status.service.ts b/dmp-frontend/src/annotation-service/services/http/status.service.ts new file mode 100644 index 000000000..49a05b1d5 --- /dev/null +++ b/dmp-frontend/src/annotation-service/services/http/status.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from "@angular/core"; +import { Status, StatusPersist } from "@annotation-service/core/model/status.model"; +import { StatusLookup } from "@annotation-service/core/query/status.lookup"; +import { ConfigurationService } from "@app/core/services/configuration/configuration.service"; +import { BaseHttpV2Service } from "@app/core/services/http/base-http-v2.service"; +import { QueryResult } from "@common/model/query-result"; +import { FilterService } from "@common/modules/text-filter/filter-service"; +import { Guid } from "@common/types/guid"; +import { Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; + +@Injectable() +export class StatusService { + private get apiBase(): string { return `${this.installationConfiguration.annotationServiceAddress}api/status`; } + + constructor( + private installationConfiguration: ConfigurationService, + private http: BaseHttpV2Service, + private filterService: FilterService + ) { } + + query(q: StatusLookup): Observable> { + const url = `${this.apiBase}/query`; + + return this.http + .post>(url, q).pipe( + catchError((error: any) => throwError(error))); + } + + getSingle(id: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/${id}`; + const options = { params: { f: reqFields } }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); + } + + persist(item: StatusPersist) { + const url = `${this.apiBase}/persist`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(error))); + } + + delete(id: Guid, ): Observable { + const url = `${this.apiBase}/${id}`; + return this.http + .delete(url).pipe( + catchError((error: any) => throwError(error))); + } + +} \ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.html b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.html new file mode 100644 index 000000000..88c2d4216 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.html @@ -0,0 +1,71 @@ +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+
+
+
+ + {{'ANNOTATION-SERVICE.STATUS-EDITOR.FIELDS.LABEL' | translate}} + +
+
+
+
+
+
+
+
+ + + {{formGroup.get('label').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ + {{'ANNOTATION-SERVICE.STATUS-EDITOR.FIELDS.INTERNAL-STATUS' | translate}} + + {{enumUtils.toInternalStatusString(internalStatus)}} + + {{formGroup.get('internalStatus').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+
+
+
+
+
+
+
+
+
diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.scss b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.scss new file mode 100644 index 000000000..489faf25d --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.scss @@ -0,0 +1,26 @@ +.status-editor { + // padding-top: 1em; + + .editor-actions { + margin-top: 30px; + } +} + +.action-btn { + border-radius: 30px; + background-color: var(--secondary-color); + border: 1px solid transparent; + padding-left: 2em; + padding-right: 2em; + box-shadow: 0px 3px 6px #1E202029; + + transition-property: background-color, color; + transition-duration: 200ms; + transition-delay: 50ms; + transition-timing-function: ease-in-out; + &:disabled{ + background-color: #CBCBCB; + color: #FFF; + border: 0px; + } +} \ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.ts b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.ts new file mode 100644 index 000000000..e4d57f7fb --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.component.ts @@ -0,0 +1,177 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Title } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockService } from '@app/core/services/lock/lock.service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +import { BaseEditor } from '@common/base/base-editor'; +import { FormService } from '@common/forms/form-service'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { FilterService } from '@common/modules/text-filter/filter-service'; +import { Guid } from '@common/types/guid'; +import { TranslateService } from '@ngx-translate/core'; +import { map, takeUntil } from 'rxjs/operators'; +import { StatusEditorModel } from './status-editor.model'; +import { StatusEditorService } from './status-editor.service'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; +import { AnnotationServiceEnumUtils } from '@annotation-service/core/formatting/enum-utils.service'; +import { Status, StatusPersist } from '@annotation-service/core/model/status.model'; +import { StatusService } from '@annotation-service/services/http/status.service'; +import { StatusEditorResolver } from './status-editor.resolver'; +import { InternalStatus } from '@annotation-service/core/enum/internal-status.enum'; + +@Component({ + templateUrl: './status-editor.component.html', + styleUrls: ['./status-editor.component.scss'], + providers: [StatusEditorService] +}) +export class StatusEditorComponent extends BaseEditor implements OnInit { + + isNew = true; + isDeleted = false; + formGroup: UntypedFormGroup = null; + showInactiveDetails = false; + public internalStatusEnum = this.enumUtils.getEnumValues(InternalStatus); + + + protected get canDelete(): boolean { + return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteStatus) && this.editorModel.belongsToCurrentTenant != false; + } + + protected get canSave(): boolean { + return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditStatus) && this.editorModel.belongsToCurrentTenant != false; + } + + private hasPermission(permission: AppPermission): boolean { + return this.authService.hasPermission(permission) || this.editorModel?.permissions?.includes(permission); + } + + constructor( + // BaseFormEditor injected dependencies + protected dialog: MatDialog, + protected language: TranslateService, + protected formService: FormService, + protected router: Router, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected filterService: FilterService, + protected route: ActivatedRoute, + protected queryParamsService: QueryParamsService, + protected lockService: LockService, + protected authService: AuthService, + protected configurationService: ConfigurationService, + // Rest dependencies. Inject any other needed deps here: + public enumUtils: AnnotationServiceEnumUtils, + private statusService: StatusService, + private logger: LoggingService, + private statusEditorService: StatusEditorService, + public titleService: Title, + protected routerUtils: RouterUtilsService + ) { + const statusLabel: string = route.snapshot.data['entity']?.name; + if (statusLabel) { + titleService.setTitle(statusLabel); + } else { + titleService.setTitle('ANNOTATION-SERVICE.STATUS-EDITOR.TITLE-EDIT-ANNOTATION-STATUS'); + } + super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, route, queryParamsService, lockService, authService, configurationService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + getItem(itemId: Guid, successFunction: (item: Status) => void) { + this.statusService.getSingle(itemId, StatusEditorResolver.lookupFields()) + .pipe(map(data => data as Status), takeUntil(this._destroyed)) + .subscribe( + data => successFunction(data), + error => this.onCallbackError(error) + ); + } + + prepareForm(data: Status) { + try { + this.editorModel = data ? new StatusEditorModel().fromModel(data) : new StatusEditorModel(); + this.isDeleted = data ? data.isActive === IsActive.Inactive : false; + this.buildForm(); + } catch (error) { + this.logger.error('Could not parse Status item: ' + data + error); + this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); + } + } + + buildForm() { + this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.authService.hasPermission(AppPermission.EditStatus)); + this.statusEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); + } + + refreshData(): void { + this.getItem(this.editorModel.id, (data: Status) => this.prepareForm(data)); + } + + refreshOnNavigateToData(id?: Guid): void { + this.formGroup.markAsPristine(); + + this.router.navigate([this.routerUtils.generateUrl('/annotation-statuses')], { queryParams: { 'lookup': this.queryParamsService.serializeLookup(this.lookupParams), 'lv': ++this.lv }, replaceUrl: true, relativeTo: this.route }); + } + + persistEntity(onSuccess?: (response) => void): void { + const formData = this.formService.getValue(this.formGroup.value) as StatusPersist; + + this.statusService.persist(formData) + .pipe(takeUntil(this._destroyed)).subscribe( + // for each state navigate to listing page + complete => { + this.onCallbackSuccess(); + }, + error => this.onCallbackError(error) + ); + } + + formSubmit(): void { + this.formService.removeAllBackEndErrors(this.formGroup); + this.formService.touchAllFormFields(this.formGroup); + if (!this.isFormValid()) { + return; + } + + this.persistEntity(); + } + + public delete() { + const value = this.formGroup.value; + if (value.id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + maxWidth: '300px', + data: { + message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'), + confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), + cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL') + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + this.statusService.delete(value.id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackDeleteSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + clearErrorModel() { + this.editorModel.validationErrorModel.clear(); + this.formService.validateAllFormFields(this.formGroup); + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.model.ts b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.model.ts new file mode 100644 index 000000000..d860dc2ea --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.model.ts @@ -0,0 +1,50 @@ +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { InternalStatus } from '@annotation-service/core/enum/internal-status.enum'; +import { Status, StatusPersist } from '@annotation-service/core/model/status.model'; +import { BaseEditorModel } from '@common/base/base-form-editor-model'; +import { BackendErrorValidator } from '@common/forms/validation/custom-validator'; +import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model'; +import { Validation, ValidationContext } from '@common/forms/validation/validation-context'; + +export class StatusEditorModel extends BaseEditorModel implements StatusPersist { + label: string; + internalStatus: InternalStatus; + permissions: string[]; + + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel(); + protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder(); + + constructor() { super(); } + + public fromModel(item: Status): StatusEditorModel { + if (item) { + super.fromModel(item); + this.label = item.label; + this.internalStatus = item.internalStatus; + } + return this; + } + + buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup { + if (context == null) { context = this.createValidationContext(); } + + return this.formBuilder.group({ + id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators], + label: [{ value: this.label, disabled: disabled }, context.getValidation('label').validators], + internalStatus: [{ value: this.internalStatus, disabled: disabled }, context.getValidation('internalStatus').validators], + hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators] + }); + } + + createValidationContext(): ValidationContext { + const baseContext: ValidationContext = new ValidationContext(); + const baseValidationArray: Validation[] = new Array(); + baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] }); + baseValidationArray.push({ key: 'label', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'label')] }); + baseValidationArray.push({ key: 'internalStatus', validators: [BackendErrorValidator(this.validationErrorModel, 'internalStatus')] }); + baseValidationArray.push({ key: 'hash', validators: [] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.resolver.ts b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.resolver.ts new file mode 100644 index 000000000..54f9a1077 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.resolver.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Status } from '@annotation-service/core/model/status.model'; +import { StatusService } from '@annotation-service/services/http/status.service'; +import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; +import { BaseEditorResolver } from '@common/base/base-editor.resolver'; +import { Guid } from '@common/types/guid'; +import { takeUntil, tap } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; + +@Injectable() +export class StatusEditorResolver extends BaseEditorResolver { + + constructor(private statusService: StatusService, private breadcrumbService: BreadcrumbService) { + super(); + } + + public static lookupFields(): string[] { + return [ + ...BaseEditorResolver.lookupFields(), + nameof(x => x.id), + nameof(x => x.label), + nameof(x => x.internalStatus), + nameof(x => x.createdAt), + nameof(x => x.updatedAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ] + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + + const fields = [ + ...StatusEditorResolver.lookupFields() + ]; + const id = route.paramMap.get('id'); + + if (id != null) { + return this.statusService.getSingle(Guid.parse(id), fields).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed)); + } + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.service.ts b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.service.ts new file mode 100644 index 000000000..2bef89bd8 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/editor/status-editor.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from "@angular/core"; +import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model"; + +@Injectable() +export class StatusEditorService { + private validationErrorModel: ValidationErrorModel; + + public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void { + this.validationErrorModel = validationErrorModel; + } + + public getValidationErrorModel(): ValidationErrorModel { + return this.validationErrorModel; + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.html b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.html new file mode 100644 index 000000000..bc96f462b --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.html @@ -0,0 +1,59 @@ +
+ + + + + +
+
+
+

{{'ANNOTATION-SERVICE.STATUS-LISTING.FILTER.TITLE' | translate}}

+
+
+ +
+
+ +
+
+
+ + {{'ANNOTATION-SERVICE.STATUS-LISTING.FILTER.IS-ACTIVE' | translate}} + +
+
+
+ +
+
+ + {{'ANNOTATION-SERVICE.STATUS-LISTING.FILTER.INTERNAL-STATUS' | translate}} + + {{enumUtils.toInternalStatusString(type)}} + + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.scss b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.scss new file mode 100644 index 000000000..8e2ed08bf --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.scss @@ -0,0 +1,26 @@ + +::ng-deep.mat-mdc-menu-panel { + max-width: 100% !important; + height: 100% !important; +} + +:host::ng-deep.mat-mdc-menu-content:not(:empty) { + padding-top: 0 !important; +} + + +.filter-button{ + padding-top: .6rem; + padding-bottom: .6rem; + // .mat-icon{ + // font-size: 1.5em; + // width: 1.2em; + // height: 1.2em; + // } +} + +::ng-deep .mdc-form-field { + label { + margin: 0; + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.ts b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.ts new file mode 100644 index 000000000..9868cab51 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/filters/status-listing-filters.component.ts @@ -0,0 +1,100 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { BaseComponent } from '@common/base/base.component'; +import { IsActive } from '@notification-service/core/enum/is-active.enum'; +import { nameof } from 'ts-simple-nameof'; +import { StatusFilter } from '@annotation-service/core/query/status.lookup'; +import { InternalStatus } from '@annotation-service/core/enum/internal-status.enum'; +import { AnnotationServiceEnumUtils } from '@annotation-service/core/formatting/enum-utils.service'; + +@Component({ + selector: 'app-status-listing-filters', + templateUrl: './status-listing-filters.component.html', + styleUrls: ['./status-listing-filters.component.scss'] +}) +export class StatusListingFiltersComponent extends BaseComponent implements OnInit, OnChanges { + + @Input() readonly filter: StatusFilter; + @Output() filterChange = new EventEmitter(); + internalStatusEnumValues = this.enumUtils.getEnumValues(InternalStatus) + + // * State + internalFilters: StatusListingFilters = this._getEmptyFilters(); + + protected appliedFilterCount: number = 0; + constructor( + public enumUtils: AnnotationServiceEnumUtils, + ) { super(); } + + ngOnInit() { + } + + ngOnChanges(changes: SimpleChanges): void { + const filterChange = changes[nameof(x => x.filter)]?.currentValue as StatusFilter; + if (filterChange) { + this.updateFilters() + } + } + + + onSearchTermChange(searchTerm: string): void { + this.applyFilters() + } + + + protected updateFilters(): void { + this.internalFilters = this._parseToInternalFilters(this.filter); + this.appliedFilterCount = this._computeAppliedFilters(this.internalFilters); + } + + protected applyFilters(): void { + const { isActive, internalStatuses, like } = this.internalFilters ?? {} + this.filterChange.emit({ + ...this.filter, + like: like, + isActive: isActive ? [IsActive.Active] : [IsActive.Inactive], + internalStatuses + }) + } + + + private _parseToInternalFilters(inputFilter: StatusFilter): StatusListingFilters { + if (!inputFilter) { + return this._getEmptyFilters(); + } + + let { isActive, internalStatuses, like } = inputFilter; + + return { + isActive: (isActive ?? [])?.includes(IsActive.Active) || !isActive?.length, + internalStatuses: internalStatuses, + like: like + } + + } + + private _getEmptyFilters(): StatusListingFilters { + return { + isActive: true, + internalStatuses: null, + like: null, + } + } + + private _computeAppliedFilters(filters: StatusListingFilters): number { + let count = 0; + if (filters?.isActive) { + count++ + } + return count; + } + + clearFilters() { + this.internalFilters = this._getEmptyFilters(); + } +} + +interface StatusListingFilters { + like: string; + isActive: boolean; + internalStatuses: InternalStatus[]; +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.html b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.html new file mode 100644 index 000000000..adbed7be5 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.html @@ -0,0 +1,93 @@ +
+
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + {{item?.name | nullifyValue}} +
+
+ + + {{'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.INTERNAL-STATUS' | translate}}: + + {{enumUtils.toNotificationTypeString(item.notificationType) | nullifyValue}} + + +
+
+ + + {{'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.CREATED-AT' | translate}}: + + {{item?.createdAt | dateTimeFormatter : 'short' | nullifyValue}} + + + + + + {{'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.UPDATED-AT' | translate}}: + + {{item?.updatedAt | dateTimeFormatter : 'short' | nullifyValue}} + + + +
+
+
+ + +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.scss b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.scss new file mode 100644 index 000000000..248f88bac --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.scss @@ -0,0 +1,60 @@ +.status-listing { + // margin-top: 1.3rem; + // margin-left: 1rem; + // margin-right: 2rem; + + .mat-header-row{ + background: #f3f5f8; + } + .mat-card { + margin: 16px 0; + padding: 0px; + } + + .mat-row { + cursor: pointer; + min-height: 4.5em; + } + + mat-row:hover { + background-color: #eef5f6; + } + .mat-fab-bottom-right { + float: right; + z-index: 5; + } +} +.create-btn { + border-radius: 30px; + background-color: var(--secondary-color); + padding-left: 2em; + padding-right: 2em; + // color: #000; + + .button-text{ + display: inline-block; + } +} + +.dlt-btn { + color: rgba(0, 0, 0, 0.54); +} + +.status-chip{ + + border-radius: 20px; + padding-left: 1em; + padding-right: 1em; + padding-top: 0.2em; + font-size: .8em; +} + +.status-chip-finalized{ + color: #568b5a; + background: #9dd1a1 0% 0% no-repeat padding-box; +} + +.status-chip-draft{ + color: #00c4ff; + background: #d3f5ff 0% 0% no-repeat padding-box; +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.ts b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.ts new file mode 100644 index 000000000..50211dc9a --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/listing/status-listing.component.ts @@ -0,0 +1,180 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +import { BaseListingComponent } from '@common/base/base-listing-component'; +import { PipeService } from '@common/formatting/pipe.service'; +import { DataTableDateTimeFormatPipe } from '@app/core/pipes/date-time-format.pipe'; +import { QueryResult } from '@common/model/query-result'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { ColumnDefinition, ColumnsChangedEvent, HybridListingComponent, PageLoadEvent } from '@common/modules/hybrid-listing/hybrid-listing.component'; +import { Guid } from '@common/types/guid'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; +import { IsActive } from '@notification-service/core/enum/is-active.enum'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; +import { Status } from '@annotation-service/core/model/status.model'; +import { StatusLookup } from '@annotation-service/core/query/status.lookup'; +import { StatusService } from '@annotation-service/services/http/status.service'; +import { AnnotationServiceEnumUtils } from '@annotation-service/core/formatting/enum-utils.service'; +import { InternalStatusTypePipe } from '@annotation-service/core/formatting/pipes/internal-status-type.pipe'; +import { IsActiveTypePipe } from '@annotation-service/core/formatting/pipes/is-active-type.pipe'; + +@Component({ + templateUrl: './status-listing.component.html', + styleUrls: ['./status-listing.component.scss'] +}) +export class StatusListingComponent extends BaseListingComponent implements OnInit { + publish = false; + userSettingsKey = { key: 'StatusListingUserSettings' }; + propertiesAvailableForOrder: ColumnDefinition[]; + + @ViewChild('actions', { static: true }) actions?: TemplateRef; + @ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent; + + private readonly lookupFields: string[] = [ + nameof(x => x.id), + nameof(x => x.label), + nameof(x => x.internalStatus), + nameof(x => x.updatedAt), + nameof(x => x.createdAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ]; + + rowIdentity = x => x.id; + + constructor( + public routerUtils: RouterUtilsService, + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private statusService: StatusService, + public authService: AuthService, + private pipeService: PipeService, + public enumUtils: AnnotationServiceEnumUtils, + private language: TranslateService, + private dialog: MatDialog + ) { + super(router, route, uiNotificationService, httpErrorHandlingService, queryParamsService); + // Lookup setup + // Default lookup values are defined in the user settings class. + this.lookup = this.initializeLookup(); + } + + ngOnInit() { + super.ngOnInit(); + } + + protected initializeLookup(): StatusLookup { + const lookup = new StatusLookup(); + lookup.metadata = { countAll: true }; + lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE }; + lookup.isActive = [IsActive.Active]; + lookup.order = { items: [this.toDescSortField(nameof(x => x.createdAt))] }; + this.updateOrderUiFields(lookup.order); + + lookup.project = { + fields: this.lookupFields + }; + + return lookup; + } + + protected setupColumns() { + this.gridColumns.push(...[{ + prop: nameof(x => x.label), + sortable: true, + languageName: 'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.LABEL', + }, + { + prop: nameof(x => x.internalStatus), + sortable: true, + languageName: 'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.INTERNAL-STATUS', + pipe: this.pipeService.getPipe(InternalStatusTypePipe) + }, + { + prop: nameof(x => x.createdAt), + sortable: true, + languageName: 'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.CREATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.updatedAt), + sortable: true, + languageName: 'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.UPDATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.isActive), + sortable: true, + languageName: 'ANNOTATION-SERVICE.STATUS-LISTING.FIELDS.IS-ACTIVE', + pipe: this.pipeService.getPipe(IsActiveTypePipe) + }, + { + alwaysShown: true, + cellTemplate: this.actions, + maxWidth: 120 + } + ]); + this.propertiesAvailableForOrder = this.gridColumns.filter(x => x.sortable); + } + + // + // Listing Component functions + // + onColumnsChanged(event: ColumnsChangedEvent) { + super.onColumnsChanged(event); + this.onColumnsChangedInternal(event.properties.map(x => x.toString())); + } + + private onColumnsChangedInternal(columns: string[]) { + // Here are defined the projection fields that always requested from the api. + const fields = new Set(this.lookupFields); + this.gridColumns.map(x => x.prop) + .filter(x => !columns?.includes(x as string)) + .forEach(item => { + fields.delete(item as string) + }); + this.lookup.project = { fields: [...fields] }; + this.onPageLoad({ offset: 0 } as PageLoadEvent); + } + + protected loadListing(): Observable> { + return this.statusService.query(this.lookup); + } + + public deleteType(id: Guid) { + if (id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + isDeleteConfirmation: true, + message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'), + confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), + cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL') + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + this.statusService.delete(id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success); + this.refresh(); + } +} diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/status.module.ts b/dmp-frontend/src/annotation-service/ui/admin/status/status.module.ts new file mode 100644 index 000000000..e82942316 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/status.module.ts @@ -0,0 +1,45 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { NgModule } from "@angular/core"; +import { AutoCompleteModule } from "@app/library/auto-complete/auto-complete.module"; +import { CommonFormattingModule } from '@common/formatting/common-formatting.module'; +import { CommonFormsModule } from '@common/forms/common-forms.module'; +import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module'; +import { HybridListingModule } from "@common/modules/hybrid-listing/hybrid-listing.module"; +import { TextFilterModule } from "@common/modules/text-filter/text-filter.module"; +import { UserSettingsModule } from "@common/modules/user-settings/user-settings.module"; +import { CommonUiModule } from '@common/ui/common-ui.module'; +import { NgxDropzoneModule } from "ngx-dropzone"; +import { StatusListingComponent } from './listing/status-listing.component'; +import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module'; +import { MatIconModule } from '@angular/material/icon'; +import { EditorModule } from '@tinymce/tinymce-angular'; +import { StatusRoutingModule } from './status.routing'; +import { AnnotationServiceFormattingModule } from '@annotation-service/core/formatting/formatting.module'; +import { StatusListingFiltersComponent } from './listing/filters/status-listing-filters.component'; +import { StatusEditorComponent } from './editor/status-editor.component'; + +@NgModule({ + imports: [ + CommonUiModule, + CommonFormsModule, + ConfirmationDialogModule, + StatusRoutingModule, + NgxDropzoneModule, + DragDropModule, + AutoCompleteModule, + HybridListingModule, + TextFilterModule, + UserSettingsModule, + CommonFormattingModule, + RichTextEditorModule, + MatIconModule, + EditorModule, + AnnotationServiceFormattingModule + ], + declarations: [ + StatusEditorComponent, + StatusListingComponent, + StatusListingFiltersComponent, + ] +}) +export class StatusModule { } diff --git a/dmp-frontend/src/annotation-service/ui/admin/status/status.routing.ts b/dmp-frontend/src/annotation-service/ui/admin/status/status.routing.ts new file mode 100644 index 000000000..655503825 --- /dev/null +++ b/dmp-frontend/src/annotation-service/ui/admin/status/status.routing.ts @@ -0,0 +1,58 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { AuthGuard } from '@app/core/auth-guard.service'; +import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; +import { PendingChangesGuard } from '@common/forms/pending-form-changes/pending-form-changes-guard.service'; +import { StatusListingComponent } from './listing/status-listing.component'; +import { StatusEditorResolver } from './editor/status-editor.resolver'; +import { StatusEditorComponent } from './editor/status-editor.component'; + +const routes: Routes = [ + { + path: '', + component: StatusListingComponent, + canActivate: [AuthGuard] + }, + { + path: 'new', + canActivate: [AuthGuard], + component: StatusEditorComponent, + canDeactivate: [PendingChangesGuard], + data: { + authContext: { + permissions: [AppPermission.EditStatus] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.NEW-TENANT' + }), + getFromTitleService: true, + usePrefix: false + } + }, + { + path: ':id', + canActivate: [AuthGuard], + component: StatusEditorComponent, + canDeactivate: [PendingChangesGuard], + resolve: { + 'entity': StatusEditorResolver + }, + data: { + authContext: { + permissions: [AppPermission.EditStatus] + }, + getFromTitleService: true, + usePrefix: false + } + + }, + { path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [StatusEditorResolver] +}) +export class StatusRoutingModule { } diff --git a/dmp-frontend/src/app/app-routing.module.ts b/dmp-frontend/src/app/app-routing.module.ts index b5231d0ac..db44db47f 100644 --- a/dmp-frontend/src/app/app-routing.module.ts +++ b/dmp-frontend/src/app/app-routing.module.ts @@ -353,6 +353,19 @@ const appRoutes: Routes = [ title: 'GENERAL.TITLES.ENTITY-LOCKS' }, }, + { + path: 'annotation-statuses', + loadChildren: () => import('@annotation-service/ui/admin/status/status.module').then(m => m.StatusModule), + data: { + authContext: { + permissions: [AppPermission.ViewStatusPage] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.ANNOTATION-STATUSES' + }), + title: 'GENERAL.TITLES.ANNOTATION-STATUSES' + }, + }, { path: 'maintenance-tasks', loadChildren: () => import('./ui/admin/maintenance-tasks/maintenance-tasks.module').then(m => m.MaintenanceTasksModule), diff --git a/dmp-frontend/src/app/core/common/enum/permission.enum.ts b/dmp-frontend/src/app/core/common/enum/permission.enum.ts index 5c585f061..820fa5fc4 100644 --- a/dmp-frontend/src/app/core/common/enum/permission.enum.ts +++ b/dmp-frontend/src/app/core/common/enum/permission.enum.ts @@ -195,6 +195,11 @@ export enum AppPermission { EditPrefillingSource= "EditPrefillingSource", DeletePrefillingSource = "DeletePrefillingSource", + //Status + BrowseStatus = "BrowseStatus", + EditStatus = "EditStatus", + DeleteStatus = "DeleteStatus", + // UI Pages ViewDescriptionTemplateTypePage = "ViewDescriptionTemplateTypePage", @@ -219,5 +224,6 @@ export enum AppPermission { ViewHomePage = "ViewHomePage", ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage", ViewTenantConfigurationPage = "ViewTenantConfigurationPage", + ViewStatusPage = "ViewStatusPage", } diff --git a/dmp-frontend/src/app/core/services/auth/auth.service.ts b/dmp-frontend/src/app/core/services/auth/auth.service.ts index 3c222bd8b..e6458d6dd 100644 --- a/dmp-frontend/src/app/core/services/auth/auth.service.ts +++ b/dmp-frontend/src/app/core/services/auth/auth.service.ts @@ -171,18 +171,18 @@ export class AuthService extends BaseService { return observable.pipe( map((x) => this.currentAuthenticationToken(x)), concatMap(response => { - return this.accessToken ? this.ensureTenant(tenantCode ?? this.selectedTenant() ?? 'default') : of(false); + return this.ensureTenant(tenantCode ?? this.selectedTenant() ?? 'default'); }), concatMap(response => { - return this.accessToken ? this.principalService.me(httpParams) : of(null); + return this.principalService.me(httpParams); }), concatMap(response => { this.currentAccount(response); - return this.accessToken ? this.tenantHandlingService.loadTenantCssColors() : of(null); + return this.tenantHandlingService.loadTenantCssColors(); }), - map((item) => { - this.tenantHandlingService.applyTenantCssColors(item[2]?.cssColors); - return true; + concatMap(response => { + this.tenantHandlingService.applyTenantCssColors(response?.cssColors); + return of(true); }) ); } diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html index 606023bf3..bda190867 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html @@ -8,6 +8,11 @@
+
+ +
diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts index a42a2ada0..5626e9b58 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts @@ -217,4 +217,23 @@ export class AnnotationDialogComponent extends BaseComponent { enableReply(threadId: string): void { this.replyEnabledPerThread[threadId] = true; } + + copyLink() { + const el = document.createElement('textarea'); + let domain = `${window.location.protocol}//${window.location.hostname}`; + if (window.location.port && window.location.port != '') domain += `:${window.location.port}` + const descriptionSectionPath = this.routerUtils.generateUrl(['descriptions/edit', this.entityId, 'f', this.anchor, 'annotation'].join('/')); + el.value = domain + descriptionSectionPath; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + this.uiNotificationService.snackBarNotification( + this.language.instant('DESCRIPTION-EDITOR.QUESTION.EXTENDED-DESCRIPTION.COPY-LINK-SUCCESSFUL'), + SnackBarNotificationLevel.Success + ); + } } diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html index 2bf48ec73..6aa96e43a 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html @@ -107,7 +107,22 @@
- + +
diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts index f501902b1..752f7cb80 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, UntypedFormArray, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Title } from '@angular/platform-browser'; @@ -44,6 +44,7 @@ import { TableOfContentsService } from './table-of-contents/services/table-of-co import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component'; import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; import { DescriptionFormService } from './description-form/components/services/description-form.service'; +import { DescriptionFormAnnotationService } from './description-form/description-form-annotation.service'; @Component({ selector: 'app-description-editor-component', @@ -51,7 +52,7 @@ import { DescriptionFormService } from './description-form/components/services/d styleUrls: ['./description-editor.component.scss'], providers: [DescriptionEditorService] }) -export class DescriptionEditorComponent extends BaseEditor implements OnInit { +export class DescriptionEditorComponent extends BaseEditor implements OnInit, AfterViewInit { isNew = true; isDeleted = false; @@ -71,6 +72,10 @@ export class DescriptionEditorComponent extends BaseEditor = new Map(); @@ -105,6 +110,7 @@ export class DescriptionEditorComponent extends BaseEditor void) { this.descriptionService.getSingle(itemId, DescriptionEditorEntityResolver.lookupFields()) .pipe(map(data => data as Description), takeUntil(this._destroyed)) diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.routing.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.routing.ts index e4d9acc30..6a0e30f00 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.routing.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.routing.ts @@ -41,6 +41,47 @@ const routes: Routes = [ title: 'DESCRIPTION-EDITOR.TITLE-EDIT-DESCRIPTION', } }, + { + path: ':id/f/:fieldsetId', + canActivate: [AuthGuard], + component: DescriptionEditorComponent, + canDeactivate: [PendingChangesGuard], + resolve: { + 'entity': DescriptionEditorEntityResolver, + 'permissions': DescriptionEditorPermissionsResolver, + }, + data: { + breadcrumbs: true, + getFromTitleService: true, + usePrefix: false, + ...BreadcrumbService.generateRouteDataConfiguration({ + skipNavigation: true, + }), + title: 'DESCRIPTION-EDITOR.TITLE-EDIT-DESCRIPTION', + scrollToField: true, + } + }, + { + path: ':id/f/:fieldsetId/annotation', + canActivate: [AuthGuard], + component: DescriptionEditorComponent, + canDeactivate: [PendingChangesGuard], + resolve: { + 'entity': DescriptionEditorEntityResolver, + 'permissions': DescriptionEditorPermissionsResolver, + }, + data: { + breadcrumbs: true, + getFromTitleService: true, + usePrefix: false, + ...BreadcrumbService.generateRouteDataConfiguration({ + skipNavigation: true, + }), + title: 'DESCRIPTION-EDITOR.TITLE-EDIT-DESCRIPTION', + scrollToField: true, + openAnnotation: true, + } + }, { path: ':dmpId/:dmpSectionId', canActivate: [AuthGuard], diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.html b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.html index c16a7b36c..35771b003 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.html @@ -6,8 +6,13 @@
- +
+
+
diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts index 68f7c9deb..a6447b848 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts @@ -17,6 +17,9 @@ import { DescriptionFormAnnotationService } from '../../description-form-annotat import { DescriptionPropertyDefinitionFieldSet } from '@app/core/model/description/description'; import { DescriptionFormService } from '../services/description-form.service'; import { DescriptionTemplateFieldType } from '@app/core/common/enum/description-template-field-type'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-description-form-field-set', @@ -53,10 +56,13 @@ export class DescriptionFormFieldSetComponent extends BaseComponent { descriptionTemplateFieldType = DescriptionTemplateFieldType; constructor( + private routerUtils: RouterUtilsService, private dialog: MatDialog, private changeDetector: ChangeDetectorRef, private descriptionFormAnnotationService: DescriptionFormAnnotationService, private descriptionFormService: DescriptionFormService, + private uiNotificationService: UiNotificationService, + private language: TranslateService, ) { super(); } @@ -71,6 +77,10 @@ export class DescriptionFormFieldSetComponent extends BaseComponent { this.changeDetector.markForCheck(); } }); + + this.descriptionFormAnnotationService.getOpenAnnotationSubjectObservable().pipe(takeUntil(this._destroyed)).subscribe( (anchorFieldsetId: string) => { + if (anchorFieldsetId && anchorFieldsetId == this.fieldSet.id) this.showAnnotations(anchorFieldsetId); + }); } canAddMultiplicityField(): boolean{ @@ -135,6 +145,25 @@ export class DescriptionFormFieldSetComponent extends BaseComponent { }); } + copyLink(fieldsetId: string) { + const el = document.createElement('textarea'); + let domain = `${window.location.protocol}//${window.location.hostname}`; + if (window.location.port && window.location.port != '') domain += `:${window.location.port}` + const descriptionSectionPath = this.routerUtils.generateUrl(['descriptions/edit', this.descriptionId.toString(), 'f', fieldsetId].join('/')); + el.value = domain + descriptionSectionPath; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + this.uiNotificationService.snackBarNotification( + this.language.instant('DESCRIPTION-EDITOR.QUESTION.EXTENDED-DESCRIPTION.COPY-LINK-SUCCESSFUL'), + SnackBarNotificationLevel.Success + ); + } + // // // Annotations diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/description-form-annotation.service.ts b/dmp-frontend/src/app/ui/description/editor/description-form/description-form-annotation.service.ts index 0cdc17ff8..1fcfa1d86 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/description-form-annotation.service.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/description-form-annotation.service.ts @@ -7,7 +7,7 @@ import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/serv import { BaseService } from '@common/base/base.service'; import { Guid } from '@common/types/guid'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { nameof } from 'ts-simple-nameof'; @@ -20,6 +20,7 @@ export class DescriptionFormAnnotationService extends BaseService { private entityId: Guid; private annotationsPerAnchor: Map; private annotationCountSubject: BehaviorSubject> = new BehaviorSubject>(null); + private openAnnotationSubject: Subject = new Subject(); constructor( private annotationService: AnnotationService, @@ -37,6 +38,14 @@ export class DescriptionFormAnnotationService extends BaseService { public getAnnotationCountObservable(): Observable> { return this.annotationCountSubject.asObservable(); } + + public getOpenAnnotationSubjectObservable(): Observable { + return this.openAnnotationSubject.asObservable(); + } + + public οpenAnnotationDialog(next: any): void { + this.openAnnotationSubject.next(next); + } public refreshAnnotations() { const lookup: AnnotationLookup = new AnnotationLookup(); diff --git a/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts b/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts index d5dffa114..95096a3be 100644 --- a/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts +++ b/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts @@ -161,8 +161,13 @@ export class DescriptionEditorEntityResolver extends BaseEditorResolver { const dmpId = route.paramMap.get('dmpId'); const dmpSectionId = route.paramMap.get('dmpSectionId'); const copyDmpId = route.paramMap.get('copyDmpId'); - // const cloneid = route.paramMap.get('cloneid'); + const fieldsetId = route.paramMap.get('fieldsetId'); if (id != null && copyDmpId == null && dmpSectionId == null) { + if (fieldsetId != null) { + this.breadcrumbService.addExcludedParam(fieldsetId, true); + this.breadcrumbService.addExcludedParam('f', true); + this.breadcrumbService.addExcludedParam('annotation', true); + } return this.descriptionService.getSingle(Guid.parse(id), fields).pipe(tap(d => this.breadcrumbService.addIdResolvedValue(d.id.toString(), d.label))); } else if (dmpId != null && dmpSectionId != null && copyDmpId == null) { return this.dmpService.getSingle(Guid.parse(dmpId), DescriptionEditorEntityResolver.dmpLookupFields()) diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts index 5294c4d57..f4baaed6e 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts @@ -21,6 +21,7 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O @Input() descriptionTemplate: DescriptionTemplate; @Input() hasFocus: boolean = false; @Input() visibilityRulesService: VisibilityRulesService; + @Input() anchorFieldsetId: string; tocentries: ToCEntry[] = null; @@ -29,7 +30,6 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O private _tocentrySelected: ToCEntry = null; get tocentrySelected() { return this._tocentrySelected; - // return this.hasFocus ? this._tocentrySelected : null; } set tocentrySelected(value) { this._tocentrySelected = value; @@ -47,6 +47,10 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O if (this.descriptionTemplate) { this.tocentries = this.getTocEntries(this.descriptionTemplate); + if (this.anchorFieldsetId) { + const anchorTocentry = TableOfContentsComponent._findTocEntryById(this.anchorFieldsetId, this.tocentries); + if (anchorTocentry) setTimeout(() => { this.onToCentrySelected(anchorTocentry) }, 300); + } } this.tableOfContentsService.getNextClickedEventObservable().pipe(takeUntil(this._destroyed)).subscribe(x => { @@ -105,6 +109,10 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O if (changes['descriptionTemplate'] && changes.descriptionTemplate != null) { this.tocentries = this.getTocEntries(this.descriptionTemplate); } + if (changes['anchorFieldsetId'] && changes.anchorFieldsetId != null) { + const anchorTocentry = TableOfContentsComponent._findTocEntryById(this.anchorFieldsetId, this.tocentries); + if (anchorTocentry) setTimeout(() => { this.onToCentrySelected(anchorTocentry) }, 300); + } } private _resetObserver() { diff --git a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts index 367d37d19..e4ef8af3b 100644 --- a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts +++ b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts @@ -119,6 +119,7 @@ export class SidebarComponent implements OnInit { if (this.authentication.hasPermission(AppPermission.ViewSupportiveMaterialPage)) this.adminItems.routes.push({ path: '/supportive-material', title: 'SIDE-BAR.SUPPORTIVE-MATERIAL', icon: 'help_center' }); if (this.authentication.hasPermission(AppPermission.ViewNotificationTemplatePage)) this.adminItems.routes.push({ path: '/notification-templates', title: 'SIDE-BAR.NOTIFICATION-TEMPLATES', icon: 'grid_guides' }); if (this.authentication.hasPermission(AppPermission.ViewNotificationPage)) this.adminItems.routes.push({ path: '/notifications', title: 'SIDE-BAR.NOTIFICATIONS', icon: 'notifications' }); + if (this.authentication.hasPermission(AppPermission.ViewStatusPage)) this.adminItems.routes.push({ path: '/annotation-statuses', title: 'SIDE-BAR.ANNOTATION-STATUSES', icon: 'notifications' }); if (this.authentication.hasPermission(AppPermission.ViewMaintenancePage)) this.adminItems.routes.push({ path: '/maintenance-tasks', title: 'SIDE-BAR.MAINTENANCE', icon: 'build' }); this.groupMenuItems.push(this.adminItems); diff --git a/dmp-frontend/src/assets/i18n/baq.json b/dmp-frontend/src/assets/i18n/baq.json index 4885c8981..7a216f1d7 100644 --- a/dmp-frontend/src/assets/i18n/baq.json +++ b/dmp-frontend/src/assets/i18n/baq.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/de.json b/dmp-frontend/src/assets/i18n/de.json index 780fbe5ae..82ec675fc 100644 --- a/dmp-frontend/src/assets/i18n/de.json +++ b/dmp-frontend/src/assets/i18n/de.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index c87d9038e..dc04b8196 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/es.json b/dmp-frontend/src/assets/i18n/es.json index b28a97e95..0ea669ddc 100644 --- a/dmp-frontend/src/assets/i18n/es.json +++ b/dmp-frontend/src/assets/i18n/es.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/gr.json b/dmp-frontend/src/assets/i18n/gr.json index 56f36bd14..55d129236 100644 --- a/dmp-frontend/src/assets/i18n/gr.json +++ b/dmp-frontend/src/assets/i18n/gr.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/hr.json b/dmp-frontend/src/assets/i18n/hr.json index d165213e9..d052320a5 100644 --- a/dmp-frontend/src/assets/i18n/hr.json +++ b/dmp-frontend/src/assets/i18n/hr.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/pl.json b/dmp-frontend/src/assets/i18n/pl.json index ed97c6f68..0376cb063 100644 --- a/dmp-frontend/src/assets/i18n/pl.json +++ b/dmp-frontend/src/assets/i18n/pl.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/pt.json b/dmp-frontend/src/assets/i18n/pt.json index 595f7005d..696432124 100644 --- a/dmp-frontend/src/assets/i18n/pt.json +++ b/dmp-frontend/src/assets/i18n/pt.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/sk.json b/dmp-frontend/src/assets/i18n/sk.json index c948e97dc..1f0df25a5 100644 --- a/dmp-frontend/src/assets/i18n/sk.json +++ b/dmp-frontend/src/assets/i18n/sk.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/sr.json b/dmp-frontend/src/assets/i18n/sr.json index dd603d625..3d1332c83 100644 --- a/dmp-frontend/src/assets/i18n/sr.json +++ b/dmp-frontend/src/assets/i18n/sr.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": { diff --git a/dmp-frontend/src/assets/i18n/tr.json b/dmp-frontend/src/assets/i18n/tr.json index 71c2708f1..147b9b309 100644 --- a/dmp-frontend/src/assets/i18n/tr.json +++ b/dmp-frontend/src/assets/i18n/tr.json @@ -986,6 +986,13 @@ "CANCEL": "Cancel", "UPDATE": "Update" } + }, + "QUESTION": { + "EXTENDED-DESCRIPTION": { + "COPY-LINK": "Copy Link", + "COPY-LINK-SUCCESSFUL": "Link copied", + "ANNOTATIONS": "Comment" + } } }, "DESCRIPTION-COPY-DIALOG": {