Merge branch 'dmp-refactoring' of code-repo.d4science.org:MaDgiK-CITE/argos into dmp-refactoring

This commit is contained in:
Efstratios Giannopoulos 2024-06-07 12:03:34 +03:00
commit e322b6adbc
68 changed files with 1994 additions and 897 deletions

View File

@ -308,4 +308,14 @@ public class ErrorThesaurusProperties {
public void setImportDescriptionWithoutDmpDescriptionTemplate(ErrorDescription importDescriptionWithoutDmpDescriptionTemplate) {
this.importDescriptionWithoutDmpDescriptionTemplate = importDescriptionWithoutDmpDescriptionTemplate;
}
private ErrorDescription duplicateDmpUser;
public ErrorDescription getDuplicateDmpUser() {
return duplicateDmpUser;
}
public void setDuplicateDmpUser(ErrorDescription duplicateDmpUser) {
this.duplicateDmpUser = duplicateDmpUser;
}
}

View File

@ -81,12 +81,6 @@ public class DescriptionTemplateDeleter implements Deleter {
UserDescriptionTemplateDeleter deleter = this.deleterFactory.deleter(UserDescriptionTemplateDeleter.class);
deleter.delete(items);
}
{
logger.debug("checking related - {}", DmpDescriptionTemplateEntity.class.getSimpleName());
List<DmpDescriptionTemplateEntity> items = this.queryFactory.query(DmpDescriptionTemplateQuery.class).descriptionTemplateGroupIds(groupIds).collect();
DmpDescriptionTemplateDeleter deleter = this.deleterFactory.deleter(DmpDescriptionTemplateDeleter.class);
deleter.delete(items);
}
//TODO can not delete profile if has Datasets
@ -94,7 +88,6 @@ public class DescriptionTemplateDeleter implements Deleter {
for (DescriptionTemplateEntity item : data) {
logger.trace("deleting item {}", item.getId());
if(item.getVersionStatus().equals(DescriptionTemplateVersionStatus.Current)) throw new MyApplicationException("Description is current can not deleted");
item.setIsActive(IsActive.Inactive);
item.setUpdatedAt(now);
logger.trace("updating item");

View File

@ -1145,7 +1145,7 @@ public class DescriptionServiceImpl implements DescriptionService {
List<TagEntity> tagsEntities = this.queryFactory.query(TagQuery.class).disableTracking().descriptionTagSubQuery(descriptionTagQuery).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).isActive(IsActive.Active).collect();
if (!this.conventionService.isListNullOrEmpty(tagsEntities)) xml.setTags(tagsEntities.stream().map(TagEntity::getLabel).collect(Collectors.toList()));
DescriptionTemplateEntity descriptionTemplateEntity = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(data.getDescriptionTemplateId()).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).isActive(IsActive.Active).first();
DescriptionTemplateEntity descriptionTemplateEntity = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(data.getDescriptionTemplateId()).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).first();
if (descriptionTemplateEntity != null) {
xml.setDescriptionTemplate(this.descriptionTemplateService.exportXmlEntity(descriptionTemplateEntity.getId(), true));
}

View File

@ -572,8 +572,8 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
if (previousFinalized != null){
previousFinalized.setVersionStatus(DescriptionTemplateVersionStatus.Current);
this.entityManager.merge(previousFinalized);
data.setVersionStatus(DescriptionTemplateVersionStatus.NotFinalized);
}
data.setVersionStatus(DescriptionTemplateVersionStatus.NotFinalized);
this.entityManager.merge(data);
this.entityManager.flush();
}
@ -908,7 +908,7 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
logger.debug(new MapLogEntry("exportXml").And("id", id));
if (!ignoreAuthorize) this.authorizationService.authorizeForce(Permission.ExportDescriptionTemplate);
DescriptionTemplateEntity data = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(id).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).isActive(IsActive.Active).first();
DescriptionTemplateEntity data = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(id).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).first();
if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, DescriptionTemplate.class.getSimpleName()}, LocaleContextHolder.getLocale()));
DefinitionEntity definition = this.xmlHandlingService.fromXml(DefinitionEntity.class, data.getDefinition());
@ -921,7 +921,7 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
logger.debug(new MapLogEntry("exportXml").And("id", id));
this.authorizationService.authorizeForce(Permission.ExportDescriptionTemplate);
DescriptionTemplateEntity data = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(id).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).isActive(IsActive.Active).first();
DescriptionTemplateEntity data = this.queryFactory.query(DescriptionTemplateQuery.class).disableTracking().ids(id).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).first();
if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, DescriptionTemplate.class.getSimpleName()}, LocaleContextHolder.getLocale()));
String xml = this.xmlHandlingService.toXml(this.exportXmlEntity(id, false));

View File

@ -768,7 +768,9 @@ public class DmpServiceImpl implements DmpService {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.dmpAffiliation(dmpId)), Permission.AssignDmpUsers);
if (!disableDelete && (model == null || model.stream().noneMatch(x-> x.getUser() != null && DmpUserRole.Owner.equals(x.getRole())))) throw new MyApplicationException("At least one owner required");
this.checkDuplicateDmpUser(model);
DmpEntity dmpEntity = this.entityManager.find(DmpEntity.class, dmpId, true);
if (dmpEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{dmpId, Dmp.class.getSimpleName()}, LocaleContextHolder.getLocale()));
@ -812,6 +814,20 @@ public class DmpServiceImpl implements DmpService {
return this.builderFactory.builder(DmpUserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fieldSet, DmpUser._id, DmpUser._hash), persisted);
}
private void checkDuplicateDmpUser(List<DmpUserPersist> model){
for (DmpUserPersist user: model) {
List<DmpUserPersist> duplicateUser = null;
if (user.getUser() != null){
duplicateUser = model.stream().filter(x -> x.getUser().equals(user.getUser()) && x.getRole().equals(user.getRole()) && Objects.equals(user.getSectionId(), x.getSectionId())).collect(Collectors.toList());
} else {
duplicateUser = model.stream().filter(x -> x.getEmail().equals(user.getEmail()) && x.getRole().equals(user.getRole()) && Objects.equals(user.getSectionId(), x.getSectionId())).collect(Collectors.toList());
}
if (duplicateUser.size() > 1) {
throw new MyValidationException(this.errors.getDuplicateDmpUser().getCode(), this.errors.getDuplicateDmpUser().getMessage());
}
}
}
@Override
public Dmp removeUser(DmpUserRemovePersist model, FieldSet fields) throws InvalidApplicationException, IOException {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.dmpAffiliation(model.getDmpId())), Permission.AssignDmpUsers);

View File

@ -37,6 +37,8 @@ public interface UserService {
void sendRemoveCredentialConfirmation(RemoveCredentialRequestPersist model) throws InvalidApplicationException, JAXBException;
boolean doesTokenBelongToLoggedInUser(String token) throws InvalidApplicationException, IOException;
void confirmMergeAccount(String token) throws InvalidApplicationException, IOException;
void confirmRemoveCredential(String token) throws InvalidApplicationException;

View File

@ -601,6 +601,12 @@ public class UserServiceImpl implements UserService {
return (String.format("%02d", hour) + ":" + String.format("%02d", min) + ":" + String.format("%02d", sec));
}
public boolean doesTokenBelongToLoggedInUser(String token) throws IOException, InvalidApplicationException {
UserEntity userToBeMerge = this.getUserEntityFromToken(token);
return this.userScope.getUserIdSafe().equals(userToBeMerge.getId());
}
public void confirmMergeAccount(String token) throws IOException, InvalidApplicationException {
ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.MergeAccount).isActive(IsActive.Active).first();
if (action == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
@ -842,4 +848,22 @@ public class UserServiceImpl implements UserService {
}
}
private UserEntity getUserEntityFromToken(String token) throws MyForbiddenException, MyNotFoundException {
ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.MergeAccount).isActive(IsActive.Active).first();
if (action == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
this.checkActionState(action);
MergeAccountConfirmationEntity mergeAccountConfirmationEntity = this.xmlHandlingService.fromXmlSafe(MergeAccountConfirmationEntity.class, action.getData());
if (mergeAccountConfirmationEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), MergeAccountConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
UserContactInfoEntity userContactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).values(mergeAccountConfirmationEntity.getEmail()).types(ContactInfoType.Email).first();
if (userContactInfoEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{mergeAccountConfirmationEntity.getEmail(), User.class.getSimpleName()}, LocaleContextHolder.getLocale()));
UserEntity userToBeMerge = this.queryFactory.query(UserQuery.class).ids(userContactInfoEntity.getUserId()).isActive(IsActive.Active).first();
if (userToBeMerge == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{userContactInfoEntity.getUserId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale()));
return userToBeMerge;
}
}

View File

@ -23,6 +23,7 @@ import org.opencdmp.authorization.AuthorizationFlags;
import org.opencdmp.commons.enums.DmpAccessType;
import org.opencdmp.commons.enums.DmpStatus;
import org.opencdmp.commons.enums.IsActive;
import org.opencdmp.controllers.swagger.SwaggerHelpers;
import org.opencdmp.convention.ConventionService;
import org.opencdmp.data.StorageFileEntity;
import org.opencdmp.model.DescriptionValidationResult;
@ -151,16 +152,16 @@ public class DescriptionController {
@PostMapping("query")
@Operation(
summary = "Query all descriptions",
description = SwaggerHelpers.endpoint_query,
description = SwaggerHelpers.Description.endpoint_query,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = SwaggerHelpers.endpoint_query_request_body,
description = SwaggerHelpers.Description.endpoint_query_request_body,
content = {
@Content(
examples = {
@ExampleObject(
name = "Pagination and projection",
description = "Simple paginated request using a property projection list and pagination info",
value = SwaggerHelpers.endpoint_query_request_body_example
value = SwaggerHelpers.Description.endpoint_query_request_body_example
)
}
)
@ -184,8 +185,8 @@ public class DescriptionController {
@Operation(summary = "Fetch a specific description by id")
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,
@Parameter(name = "fieldSet", description = "This is an object containing a list of the properties you wish to include in the response, similar to the 'project' attribute on queries.", required = true) FieldSet fieldSet)
throws MyApplicationException, MyForbiddenException, MyNotFoundException {
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("retrieving" + Description.class.getSimpleName()).And("id", id).And("fields", fieldSet));
fieldSet = this.fieldSetExpanderService.expand(fieldSet);
@ -208,7 +209,10 @@ public class DescriptionController {
@Operation(summary = "Create a new or update an existing description")
@Transactional
@ValidationFilterAnnotation(validator = DescriptionPersist.DescriptionPersistValidator.ValidatorName, argumentName = "model")
public Description persist(@RequestBody DescriptionPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException {
public Description persist(
@RequestBody DescriptionPersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("persisting" + Description.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
fieldSet = this.fieldSetExpanderService.expand(fieldSet);
@ -226,7 +230,10 @@ public class DescriptionController {
@Operation(summary = "Update the status of an existing description")
@Transactional
@ValidationFilterAnnotation(validator = DescriptionStatusPersist.DescriptionStatusPersistValidator.ValidatorName, argumentName = "model")
public Description persistStatus(@RequestBody DescriptionStatusPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
public Description persistStatus(
@RequestBody DescriptionStatusPersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
logger.debug(new MapLogEntry("persisting" + Description.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
fieldSet = this.fieldSetExpanderService.expand(fieldSet);
@ -275,7 +282,9 @@ public class DescriptionController {
@DeleteMapping("{id}")
@Operation(summary = "Delete a description by id")
@Transactional
public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException, IOException {
public void delete(
@Parameter(name = "id", description = "The id of a description to delete", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id
) throws MyForbiddenException, InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("retrieving" + Description.class.getSimpleName()).And("id", id));
this.descriptionService.deleteAndSave(id);
@ -285,7 +294,10 @@ public class DescriptionController {
@GetMapping("{id}/export/{type}")
@Operation(summary = "Export a description in various formats by id")
public ResponseEntity<byte[]> export(@PathVariable("id") UUID id, @PathVariable("type") String exportType) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
public ResponseEntity<byte[]> export(
@Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@Parameter(name = "type", description = "The type of the export", example = "rda", required = true) @PathVariable("type") String exportType
) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
logger.debug(new MapLogEntry("exporting description"));
return this.descriptionService.export(id, exportType);
@ -295,7 +307,11 @@ public class DescriptionController {
@Operation(summary = "Upload a file attachment on a field that supports it")
@Transactional
@ValidationFilterAnnotation(validator = DescriptionFieldFilePersist.PersistValidator.ValidatorName, argumentName = "model")
public StorageFile uploadFieldFiles(@RequestParam("file") MultipartFile file, @RequestParam("model") DescriptionFieldFilePersist model, FieldSet fieldSet) throws IOException {
public StorageFile uploadFieldFiles(
@RequestParam("file") MultipartFile file,
@RequestParam("model") DescriptionFieldFilePersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws IOException {
logger.debug(new MapLogEntry("uploadFieldFiles" + Description.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
fieldSet = this.fieldSetExpanderService.expand(fieldSet);
@ -311,7 +327,10 @@ public class DescriptionController {
@GetMapping("{id}/field-file/{fileId}")
@Operation(summary = "Fetch a field file attachment as byte array")
public ResponseEntity<ByteArrayResource> getFieldFile(@PathVariable("id") UUID id, @PathVariable("fileId") UUID fileId) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
public ResponseEntity<ByteArrayResource> getFieldFile(
@Parameter(name = "id", description = "The id of a description", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@Parameter(name = "fileIid", description = "The id of the file we want to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("fileId") UUID fileId
) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("get Field File" + Description.class.getSimpleName()).And("id", id).And("fileId", fileId));
StorageFileEntity storageFile = this.descriptionService.getFieldFile(id, fileId);
@ -349,7 +368,9 @@ public class DescriptionController {
@RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml")
@Operation(summary = "Export a description in xml format by id")
public @ResponseBody ResponseEntity<byte[]> getXml(@PathVariable UUID id) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException {
public @ResponseBody ResponseEntity<byte[]> getXml(
@Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id
) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException {
logger.debug(new MapLogEntry("export" + DmpBlueprint.class.getSimpleName()).And("id", id));
ResponseEntity<byte[]> response = this.descriptionService.exportXml(id);
@ -360,181 +381,4 @@ public class DescriptionController {
return response;
}
protected static class SwaggerHelpers {
static final String endpoint_query =
"""
This endpoint is used to fetch all the available descriptions.<br/>
It also allows to restrict the results using a query object passed in the request body.<br/>
""";
static final String endpoint_query_request_body =
"""
Let's explore the options this object gives us.
### <u>General query parameters:</u>
<ul>
<li><b>page:</b>
This is an object controlling the pagination of the results. It contains two properties.
</li>
<ul>
<li><b>offset:</b>
How many records to omit.
</li>
<li><b>size:</b>
How many records to include in each page.
</li>
</ul>
</ul>
For example, if we want the third page, and our pages to contain 15 elements, we would pass the following object:
```JSON
{
"offset": 30,
"size": 15
}
```
<ul>
<li><b>order:</b>
This is an object controlling the ordering of the results.
It contains a list of strings called <i>items</i> with the names of the properties to use.
<br/>If the name of the property is prefixed with a <b>'-'</b>, the ordering direction is <b>DESC</b>. Otherwise, it is <b>ASC</b>.
</li>
</ul>
For example, if we wanted to order based on the field 'createdAt' in descending order, we would pass the following object:
```JSON
{
"items": [
"-createdAt"
],
}
```
<ul>
<li><b>metadata:</b>
This is an object containing metadata for the request. There is only one available option.
<ul>
<li><b>countAll:</b>
If this is set to true, the count property included in the response will account for all the records regardless the pagination,
with all the rest of filtering options applied of course.
Otherwise, if it is set to false or not present, only the returned results will be counted.
<br/>The first option is useful for the UI clients to calculate how many result pages are available.
</li>
</ul>
</li>
<li><b>project:</b>
This is an object controlling the data projection of the results.
It contains a list of strings called <i>fields</i> with the names of the properties to project.
<br/>You can also include properties that are deeper in the object tree by prefixing them with dots.
</li>
</ul>
### <u>Description specific query parameters:</u>
<ul>
<li><b>like:</b>
If there is a like parameter present in the query, only the description entities that include the contents of the parameter either in their labels or the descriptions will be in the response.
</li>
<li><b>ids:</b>
This is a list and contains the ids we want to include in the response. <br/>If empty, every record is included.
</li>
<li><b>excludedIds:</b>
This is a list and contains the ids we want to exclude from the response. <br/>If empty, no record gets excluded.
</li>
<li><b>isActive:</b>
This is a list and determines which records we want to include in the response, based on if they are deleted or not.
This filter works like this. If we want to view only the active records we pass [1] and for only the deleted records we pass [0].
<br/>If not present or if we pass [0,1], every record is included.
</li>
<li><b>statuses:</b>
This is a list and determines which records we want to include in the response, based on their status.
The status can be <i>Draft</i>, <i>Finalized</i> or <i>Canceled</i>. We add 0, 1 or 2 to the list respectively.
<br/>If not present, every record is included.
</li>
<li><b>createdAfter:</b>
This is a date and determines which records we want to include in the response, based on their creation date.
Specifically, only the records created after the given date are included.
<br/>If not present, every record is included.
</li>
<li><b>createdBefore:</b>
This is a date and determines which records we want to include in the response, based on their creation date.
Specifically, only the records created before the given date are included.
<br/>If not present, every record is included.
</li>
<li><b>finalizedAfter:</b>
This is a date and determines which records we want to include in the response, based on their finalization date.
Specifically, only the records finalized after the given date are included.
<br/>If not present, every record is included.
</li>
<li><b>finalizedBefore:</b>
This is a date and determines which records we want to include in the response, based on their finalization date.
Specifically, only the records finalized before the given date are included.
<br/>If not present, every record is included.
</li>
</ul>
""";
static final String endpoint_query_request_body_example =
"""
{
"project":{
"fields":[
"id",
"label",
"status",
"updatedAt",
"belongsToCurrentTenant",
"finalizedAt",
"descriptionTemplate.id",
"descriptionTemplate.label",
"descriptionTemplate.groupId",
"dmp.id",
"dmp.label",
"dmp.status",
"dmp.accessType",
"dmp.blueprint.id",
"dmp.blueprint.label",
"dmp.blueprint.definition.sections.id",
"dmp.blueprint.definition.sections.label",
"dmp.blueprint.definition.sections.hasTemplates",
"dmp.dmpReferences.id",
"dmp.dmpReferences.reference.id",
"dmp.dmpReferences.reference.label",
"dmp.dmpReferences.reference.type.id",
"dmp.dmpReferences.reference.reference",
"dmp.dmpReferences.isActive",
"dmpDescriptionTemplate.id",
"dmpDescriptionTemplate.dmp.id",
"dmpDescriptionTemplate.descriptionTemplateGroupId",
"dmpDescriptionTemplate.sectionId"
]
},
"page":{
"size":5,
"offset":0
},
"order":{
"items":[
"-updatedAt"
]
},
"metadata":{
"countAll":true
},
"isActive":[
1
]
}
""";
static final String endpoint_get =
"""
""";
}
}

View File

@ -11,9 +11,12 @@ 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.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.xml.bind.JAXBException;
import org.opencdmp.audit.AuditableAction;
@ -21,6 +24,7 @@ import org.opencdmp.authorization.AuthorizationFlags;
import org.opencdmp.commons.enums.DmpAccessType;
import org.opencdmp.commons.enums.DmpStatus;
import org.opencdmp.commons.enums.IsActive;
import org.opencdmp.controllers.swagger.SwaggerHelpers;
import org.opencdmp.model.DescriptionsToBeFinalized;
import org.opencdmp.model.DmpUser;
import org.opencdmp.model.DmpValidationResult;
@ -99,6 +103,7 @@ public class DmpController {
@PostMapping("public/query")
@Operation(summary = "Query public published plans")
@Hidden
public QueryResult<PublicDmp> publicQuery(@RequestBody DmpLookup lookup) throws MyApplicationException, MyForbiddenException {
logger.debug("querying {}", Dmp.class.getSimpleName());
@ -114,6 +119,7 @@ public class DmpController {
@GetMapping("public/{id}")
@Operation(summary = "Fetch a specific public published plan by id")
@Hidden
public PublicDmp publicGet(@PathVariable("id") UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("retrieving" + Dmp.class.getSimpleName()).And("id", id).And("fields", fieldSet));
@ -136,21 +142,38 @@ public class DmpController {
@PostMapping("query")
@Operation(
summary = "Query all plans",
description = SwaggerHelpers.endpoint_query,
description = SwaggerHelpers.Dmp.endpoint_query,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = SwaggerHelpers.endpoint_query_request_body,
description = SwaggerHelpers.Dmp.endpoint_query_request_body,
content = {
@Content(
examples = {
@ExampleObject(
name = "Pagination and projection",
description = "Simple paginated request using a property projection list and pagination info",
value = SwaggerHelpers.endpoint_query_request_body_example
value = SwaggerHelpers.Dmp.endpoint_query_request_body_example
)
}
)
}
)
),
responses = {
@ApiResponse(
description = "OK",
responseCode = "200",
content = {
@Content(
examples = {
@ExampleObject(
name = "First page",
description = "Example with the first page of paginated results",
value = SwaggerHelpers.Dmp.endpoint_query_response_example
)
}
)
}
)
}
)
public QueryResult<Dmp> Query(@RequestBody DmpLookup lookup) throws MyApplicationException, MyForbiddenException {
logger.debug("querying {}", Dmp.class.getSimpleName());
@ -166,7 +189,11 @@ public class DmpController {
@GetMapping("{id}")
@Operation(summary = "Fetch a specific plan by id")
public Dmp Get(@PathVariable("id") UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
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,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet,
Locale locale
) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("retrieving" + Dmp.class.getSimpleName()).And("id", id).And("fields", fieldSet));
this.censorFactory.censor(DmpCensor.class).censor(fieldSet, null);
@ -188,7 +215,10 @@ public class DmpController {
@Operation(summary = "Create a new or update an existing plan")
@Transactional
@ValidationFilterAnnotation(validator = DmpPersist.DmpPersistValidator.ValidatorName, argumentName = "model")
public Dmp Persist(@RequestBody DmpPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException, JAXBException {
public Dmp Persist(
@RequestBody DmpPersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException, JAXBException {
logger.debug(new MapLogEntry("persisting" + Dmp.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
Dmp persisted = this.dmpService.persist(model, fieldSet);
@ -204,7 +234,9 @@ public class DmpController {
@DeleteMapping("{id}")
@Operation(summary = "Delete a plan by id")
@Transactional
public void Delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException, IOException {
public void Delete(
@Parameter(name = "id", description = "The id of a plan to delete", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id
) throws MyForbiddenException, InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("retrieving" + Dmp.class.getSimpleName()).And("id", id));
this.dmpService.deleteAndSave(id);
@ -215,7 +247,10 @@ public class DmpController {
@PostMapping("finalize/{id}")
@Operation(summary = "Finalize a plan by id")
@Transactional
public boolean finalize(@PathVariable("id") UUID id, @RequestBody DescriptionsToBeFinalized descriptions) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException {
public boolean finalize(
@Parameter(name = "id", description = "The id of a plan to finalize", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@RequestBody DescriptionsToBeFinalized descriptions
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("finalizing" + Dmp.class.getSimpleName()).And("id", id).And("descriptionIds", descriptions.getDescriptionIds()));
this.dmpService.finalize(id, descriptions.getDescriptionIds());
@ -231,7 +266,10 @@ public class DmpController {
@GetMapping("undo-finalize/{id}")
@Operation(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)")
@Transactional
public boolean undoFinalize(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException, JAXBException {
public boolean undoFinalize(
@Parameter(name = "id", description = "The id of a plan to revert the finalization", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException, JAXBException {
logger.debug(new MapLogEntry("undo-finalizing" + Dmp.class.getSimpleName()).And("id", id));
this.censorFactory.censor(DmpCensor.class).censor(fieldSet, null);
@ -247,6 +285,7 @@ public class DmpController {
@GetMapping("validate/{id}")
@Operation(summary = "Validate if a plan is ready for finalization by id")
@Hidden
public DmpValidationResult validate(@PathVariable("id") UUID id) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
logger.debug(new MapLogEntry("validating" + Dmp.class.getSimpleName()).And("id", id));
@ -265,7 +304,10 @@ public class DmpController {
@Operation(summary = "Create a clone of an existing plan")
@Transactional
@ValidationFilterAnnotation(validator = CloneDmpPersist.CloneDmpPersistValidator.ValidatorName, argumentName = "model")
public Dmp buildClone(@RequestBody CloneDmpPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
public Dmp buildClone(
@RequestBody CloneDmpPersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
logger.debug(new MapLogEntry("clone" + Dmp.class.getSimpleName()).And("model", model).And("fields", fieldSet));
this.censorFactory.censor(DmpCensor.class).censor(fieldSet, null);
@ -284,7 +326,10 @@ public class DmpController {
@Operation(summary = "Create a new version of an existing plan")
@Transactional
@ValidationFilterAnnotation(validator = NewVersionDmpPersist.NewVersionDmpPersistValidator.ValidatorName, argumentName = "model")
public Dmp createNewVersion(@RequestBody NewVersionDmpPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, JAXBException, IOException, TransformerException, InvalidApplicationException, ParserConfigurationException {
public Dmp createNewVersion(
@RequestBody NewVersionDmpPersist model,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, JAXBException, IOException, TransformerException, InvalidApplicationException, ParserConfigurationException {
logger.debug(new MapLogEntry("persisting" + NewVersionDmpPersist.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
Dmp persisted = this.dmpService.createNewVersion(model, fieldSet);
@ -301,6 +346,7 @@ public class DmpController {
@Operation(summary = "Assign users to the plan by id")
@Transactional
@ValidationFilterAnnotation(validator = DmpUserPersist.DmpUserPersistValidator.ValidatorName, argumentName = "model")
@Hidden
public QueryResult<DmpUser> assignUsers(@PathVariable("id") UUID id, @RequestBody List<DmpUserPersist> model, FieldSet fieldSet) throws InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("assigning users to dmp").And("model", model).And("fieldSet", fieldSet));
@ -318,6 +364,7 @@ public class DmpController {
@Operation(summary = "Remove a user association with the plan")
@Transactional
@ValidationFilterAnnotation(validator = DmpUserRemovePersist.DmpUserRemovePersistValidator.ValidatorName, argumentName = "model")
@Hidden
public QueryResult<Dmp> removeUser(@RequestBody DmpUserRemovePersist model, FieldSet fieldSet) throws InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("remove user from dmp").And("model", model).And("fieldSet", fieldSet));
@ -333,7 +380,11 @@ public class DmpController {
@GetMapping("{id}/export/{transformerId}/{type}")
@Operation(summary = "Export a plan in various formats by id")
public ResponseEntity<byte[]> export(@PathVariable("id") UUID id, @PathVariable("transformerId") String transformerId, @PathVariable("type") String exportType) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
public ResponseEntity<byte[]> export(
@Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@PathVariable("transformerId") String transformerId,
@PathVariable("type") String exportType
) throws InvalidApplicationException, IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
logger.debug(new MapLogEntry("exporting dmp").And("id", id).And("transformerId", transformerId).And("exportType", exportType));
ResponseEntity<byte[]> bytes = this.dmpService.export(id, transformerId, exportType);
@ -349,6 +400,7 @@ public class DmpController {
@Operation(summary = "Send user invitations for the plan by id")
@Transactional
@ValidationFilterAnnotation(validator = DmpUserInvitePersist.DmpUserInvitePersistValidator.ValidatorName, argumentName = "model")
@Hidden
public boolean inviteUsers(@PathVariable("id") UUID id, @RequestBody DmpUserInvitePersist model) throws InvalidApplicationException, JAXBException, IOException {
logger.debug(new MapLogEntry("inviting users to dmp").And("model", model));
@ -364,6 +416,7 @@ public class DmpController {
@GetMapping("{id}/token/{token}/invite-accept")
@Operation(summary = "Accept an invitation token for a plan by id")
@Transactional
@Hidden
public boolean acceptInvitation(@PathVariable("id") UUID id, @PathVariable("token") String token) throws InvalidApplicationException, JAXBException, IOException {
logger.debug(new MapLogEntry("inviting users to dmp").And("id", id));
@ -378,7 +431,9 @@ public class DmpController {
@RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml")
@Operation(summary = "Export a plan in xml format by id")
public @ResponseBody ResponseEntity<byte[]> getXml(@PathVariable UUID id) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException {
public @ResponseBody ResponseEntity<byte[]> getXml(
@Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id
) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException {
logger.debug(new MapLogEntry("export" + Dmp.class.getSimpleName()).And("id", id));
ResponseEntity<byte[]> response = this.dmpService.exportXml(id);
@ -392,7 +447,11 @@ public class DmpController {
@RequestMapping(method = RequestMethod.POST, value = "/xml/import")
@Operation(summary = "Import a plan from an xml file")
@Transactional
public Dmp importXml(@RequestParam("file") MultipartFile file, @RequestParam("label") String label, FieldSet fields) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException, TransformerException {
public Dmp importXml(
@RequestParam("file") MultipartFile file,
@RequestParam("label") String label,
@Parameter(name = "fields", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fields
) throws JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException, TransformerException {
logger.debug(new MapLogEntry("import xml" + Dmp.class.getSimpleName()).And("file", file).And("label", label));
Dmp model = this.dmpService.importXml(file.getBytes(), label, fields);
@ -407,7 +466,13 @@ public class DmpController {
@PostMapping("json/import")
@Operation(summary = "Import a plan from an json file")
@Transactional
public Dmp importJson(@RequestParam("file") MultipartFile file, @RequestParam("label") String label, @RequestParam("repositoryId") String repositoryId, @RequestParam("format") String format, FieldSet fields) throws InvalidAlgorithmParameterException, JAXBException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, IOException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
public Dmp importJson(
@RequestParam("file") MultipartFile file,
@RequestParam("label") String label,
@RequestParam("repositoryId") String repositoryId,
@RequestParam("format") String format,
@Parameter(name = "fields", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fields
) throws InvalidAlgorithmParameterException, JAXBException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, IOException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
logger.debug(new MapLogEntry("import json" + Dmp.class.getSimpleName()).And("transformerId", repositoryId).And("file", file).And("label", label));
Dmp model = this.dmpService.importJson(file, label, repositoryId, format, fields);
@ -421,171 +486,4 @@ public class DmpController {
return model;
}
protected static class SwaggerHelpers {
static final String endpoint_query =
"""
This endpoint is used to fetch all the available plans.<br/>
It also allows to restrict the results using a query object passed in the request body.<br/>
""";
static final String endpoint_query_request_body =
"""
Let's explore the options this object gives us.
### <u>General query parameters:</u>
<ul>
<li><b>page:</b>
This is an object controlling the pagination of the results. It contains two properties.
</li>
<ul>
<li><b>offset:</b>
How many records to omit.
</li>
<li><b>size:</b>
How many records to include in each page.
</li>
</ul>
</ul>
For example, if we want the third page, and our pages to contain 15 elements, we would pass the following object:
```JSON
{
"offset": 30,
"size": 15
}
```
<ul>
<li><b>order:</b>
This is an object controlling the ordering of the results.
It contains a list of strings called <i>items</i> with the names of the properties to use.
<br/>If the name of the property is prefixed with a <b>'-'</b>, the ordering direction is <b>DESC</b>. Otherwise, it is <b>ASC</b>.
</li>
</ul>
For example, if we wanted to order based on the field 'createdAt' in descending order, we would pass the following object:
```JSON
{
"items": [
"-createdAt"
],
}
```
<ul>
<li><b>metadata:</b>
This is an object containing metadata for the request. There is only one available option.
<ul>
<li><b>countAll:</b>
If this is set to true, the count property included in the response will account for all the records regardless the pagination,
with all the rest of filtering options applied of course.
Otherwise, if it is set to false or not present, only the returned results will be counted.
<br/>The first option is useful for the UI clients to calculate how many result pages are available.
</li>
</ul>
</li>
<li><b>project:</b>
This is an object controlling the data projection of the results.
It contains a list of strings called <i>fields</i> with the names of the properties to project.
<br/>You can also include properties that are deeper in the object tree by prefixing them with dots.
</li>
</ul>
### <u>Plan specific query parameters:</u>
<ul>
<li><b>like:</b>
If there is a like parameter present in the query, only the description entities that include the contents of the parameter either in their labels or the descriptions will be in the response.
</li>
<li><b>ids:</b>
This is a list and contains the ids we want to include in the response. <br/>If empty, every record is included.
</li>
<li><b>excludedIds:</b>
This is a list and contains the ids we want to exclude from the response. <br/>If empty, no record gets excluded.
</li>
<li><b>groupIds:</b>
This is a list and contains the group ids we want the plans to have. Every plan and all its versions, have the same groupId. <br/>If empty, every record is included.
</li>
<li><b>isActive:</b>
This is a list and determines which records we want to include in the response, based on if they are deleted or not.
This filter works like this. If we want to view only the active records we pass [1] and for only the deleted records we pass [0].
<br/>If not present or if we pass [0,1], every record is included.
</li>
<li><b>statuses:</b>
This is a list and determines which records we want to include in the response, based on their status.
The status can be <i>Draft</i> or <i>Finalized</i>. We add 0 or 1 to the list respectively.
<br/>If not present, every record is included.
</li>
<li><b>versionStatuses:</b>
This is a list and determines which records we want to include in the response, based on their version status.
The status can be <i>Current</i>, <i>Previous</i> or <i>NotFinalized</i>. We add 0, 1 or 2 to the list respectively.
<br/>If not present, every record is included.
</li>
<li><b>accessTypes:</b>
This is a list and determines which records we want to include in the response, based on their access type.
The access type can be <i>Public</i> or <i>Restricted</i>. We add 0 or 1 to the list respectively.
<br/>If not present, every record is included.
</li>
</ul>
""";
static final String endpoint_query_request_body_example =
"""
{
"project":{
"fields":[
"id",
"label",
"description",
"status",
"accessType",
"version",
"versionStatus",
"groupId",
"updatedAt",
"belongsToCurrentTenant",
"finalizedAt",
"hash",
"descriptions.id",
"descriptions.label",
"descriptions.status",
"descriptions.descriptionTemplate.groupId",
"descriptions.isActive",
"blueprint.id",
"blueprint.label",
"blueprint.definition.sections.id"
]
},
"page":{
"size":5,
"offset":0
},
"order":{
"items":[
"-updatedAt"
]
},
"metadata":{
"countAll":true
},
"isActive":[
1
],
"versionStatuses":[
0,
2
]
}
""";
static final String endpoint_ =
"""
""";
}
}

View File

@ -297,6 +297,14 @@ public class UserController {
return true;
}
@GetMapping("mine/get-permission/token/{token}")
@Transactional
public Boolean getUserTokenPermission(@PathVariable("token") String token) throws InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("confirm merge account to user").And("token", token));
return this.userTypeService.doesTokenBelongToLoggedInUser(token);
}
@PostMapping("mine/remove-credential-request")
@Transactional
@ValidationFilterAnnotation(validator = RemoveCredentialRequestPersist.RemoveCredentialRequestPersistValidator.ValidatorName, argumentName = "model")

File diff suppressed because it is too large Load Diff

View File

@ -97,4 +97,7 @@ error-thesaurus:
message: missing contact info for this user
import-description-without-dmp-description-template:
code: 136
message: Error creating description without dmp description template
message: Error creating description without dmp description template
duplicate-dmp-user:
code: 137
message: Duplicate Dmp User not allowed

View File

@ -958,6 +958,7 @@ permissions:
DeleteLock:
roles:
- Admin
- TenantAdmin
dmp:
roles:
- Owner

View File

@ -3,12 +3,11 @@ import { RouterModule, Routes } from '@angular/router';
import { AppPermission } from './core/common/enum/permission.enum';
import { BreadcrumbService } from './ui/misc/breadcrumb/breadcrumb.service';
import { ReloadHelperComponent } from './ui/misc/reload-helper/reload-helper.component';
import { DepositOauth2DialogComponent } from './ui/misc/deposit-oauth2-dialog/deposit-oauth2-dialog.component';
const appRoutes: Routes = [
{
path: '',
redirectTo:'home',
redirectTo: 'home',
pathMatch: 'full'
},
{
@ -35,7 +34,7 @@ const appRoutes: Routes = [
},
{
path: 'explore-descriptions',
loadChildren: () => import('./ui/description/description.module').then(m => m.DescriptionModule),
loadChildren: () => import('./ui/description/description.module').then(m => m.PublicDescriptionModule),
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -60,7 +59,7 @@ const appRoutes: Routes = [
},
{
path: 'explore-plans',
loadChildren: () => import('./ui/dmp/dmp.module').then(m => m.DmpModule),
loadChildren: () => import('./ui/dmp/dmp.module').then(m => m.PublicDmpModule),
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -183,14 +182,6 @@ const appRoutes: Routes = [
title: 'GENERAL.TITLES.COOKIES-POLICY'
}
},
// {
// path: 'splash',
// loadChildren: () => import('./ui/splash/splash.module').then(m => m.SplashModule),
// data: {
// breadcrumb: true
// }
// },
{
path: 'unauthorized',
loadChildren: () => import('./ui/misc/unauthorized/unauthorized.module').then(m => m.UnauthorizedModule),
@ -387,8 +378,22 @@ const appRoutes: Routes = [
}
];
const tenantEnrichedRoutes: Routes = [
{
path: 't/:tenant_code',
data: {
breadcrumb: true,
hideItem: true
},
children: [
...appRoutes
]
},
...appRoutes
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes, {})],
imports: [RouterModule.forRoot(tenantEnrichedRoutes, {})],
exports: [RouterModule],
})
export class AppRoutingModule { }

View File

@ -3,7 +3,7 @@ import { of as observableOf, Subscription } from 'rxjs';
import { switchMap, filter, map, takeUntil } from 'rxjs/operators';
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { AuthService, LoginStatus } from './core/services/auth/auth.service';
import { CultureService } from './core/services/culture/culture-service';
@ -23,6 +23,7 @@ import { TenantConfigurationService } from './core/services/tenant-configuration
import { TenantConfigurationType } from './core/common/enum/tenant-configuration-type';
import { CssColorsTenantConfiguration, TenantConfiguration } from './core/model/tenant-configuaration/tenant-configuration';
import { nameof } from 'ts-simple-nameof';
import { TenantHandlingService } from './core/services/tenant/tenant-handling.service';
declare const gapi: any;
@ -43,7 +44,7 @@ export class AppComponent implements OnInit, AfterViewInit {
onlySplash = true;
showOnlyRouterOutlet = false;
@ViewChild('sidenav') sidenav:MatSidenav;
@ViewChild('sidenav') sidenav: MatSidenav;
constructor(
private router: Router,
@ -61,7 +62,8 @@ export class AppComponent implements OnInit, AfterViewInit {
private location: Location,
private matomoService: MatomoService,
private tenantConfigurationService: TenantConfigurationService,
private sidenavService: SideNavService
private sidenavService: SideNavService,
private tenantHandlingService: TenantHandlingService
) {
this.initializeServices();
this.matomoService.init();
@ -69,30 +71,30 @@ export class AppComponent implements OnInit, AfterViewInit {
}
ngAfterViewInit(): void {
setTimeout(() => {
this.sideNavSubscription = this.sidenavService.status().subscribe(isopen=>{
this.sideNavSubscription = this.sidenavService.status().subscribe(isopen => {
const hamburger = document.getElementById('hamburger');
if(isopen){
if (isopen) {
//update value of hamburfer
if(!hamburger){//try later
if (!hamburger) {//try later
setTimeout(() => {
const hamburger = document.getElementById('hamburger');
if(hamburger){
const hamburger = document.getElementById('hamburger');
if (hamburger) {
hamburger.classList.add('change');
}
}, 300);
}else{
} else {
hamburger.classList.add('change');
}
this.sidenav.open()
}else{//closed
if(!hamburger){//try later
} else {//closed
if (!hamburger) {//try later
setTimeout(() => {
const hamburger = document.getElementById('hamburger');
if(hamburger){
const hamburger = document.getElementById('hamburger');
if (hamburger) {
hamburger.classList.remove('change');
}
}, 300);
}else{
} else {
hamburger.classList.remove('change');
}
this.sidenav.close();
@ -114,7 +116,7 @@ export class AppComponent implements OnInit, AfterViewInit {
if (this.location.path() === '') {
if (!this.configurationService.useSplash) {
this.onlySplash = false;
this.router.navigate(['/reload']).then(() => this.router.navigate(['/home']));
this.router.navigate(['/home']);
} else {
this.onlySplash = true;
this.router.navigate(['/reload']).then(() => this.router.navigate(['/splash']));
@ -126,7 +128,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
if (!this.cookieService.check("cookiesConsent")) {
// this.cookieService.set("cookiesConsent", "false", 356);
this.cookieService.set("cookiesConsent", "false", 356,null,null,false, 'Lax');
this.cookieService.set("cookiesConsent", "false", 356, null, null, false, 'Lax');
}
@ -171,17 +173,29 @@ export class AppComponent implements OnInit, AfterViewInit {
return { title: child.snapshot.data['title'], usePrefix: usePrefix };
}
}
return { title: appTitle, usePrefix: true};
return { title: appTitle, usePrefix: true };
})
).subscribe((titleOptions: { title: string, usePrefix: boolean}) => {
).subscribe((titleOptions: { title: string, usePrefix: boolean }) => {
this.translateTitle(titleOptions.title, titleOptions.usePrefix);
this.translate.onLangChange.subscribe(() => this.translateTitle(titleOptions.title, titleOptions.usePrefix));
});
this.router
.events.pipe(
filter(event => event instanceof NavigationEnd)
)
.subscribe((event: NavigationStart) => {
const enrichedUrl = this.tenantHandlingService.getUrlEnrichedWithTenantCode(event.url, this.authentication.selectedTenant() ?? 'default');
if (event.url != enrichedUrl) {
this.router.navigate([enrichedUrl]);
}
});
this.statusChangeSubscription = this.ccService.statusChange$.subscribe((event: NgcStatusChangeEvent) => {
if (event.status == "dismiss") {
// this.cookieService.set("cookiesConsent", "true", 365);
this.cookieService.set("cookiesConsent", "true", 356,null,null,false, 'Lax');
this.cookieService.set("cookiesConsent", "true", 356, null, null, false, 'Lax');
}
});
@ -203,7 +217,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
this.ccService.destroy();
this.ccService.init(this.ccService.getConfig());
});
});
}
translateTitle(ttl: string, usePrefix: boolean) {
@ -226,7 +240,7 @@ export class AppComponent implements OnInit, AfterViewInit {
ngOnDestroy() {
this.statusChangeSubscription.unsubscribe();
if(this.sideNavSubscription){
if (this.sideNavSubscription) {
this.sideNavSubscription.unsubscribe();
}
}
@ -269,24 +283,24 @@ export class AppComponent implements OnInit, AfterViewInit {
private loadCssColors() {
if (this.authentication.currentAccountIsAuthenticated() && this.authentication.selectedTenant()) {
this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.CssColors, [
this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.CssColors, [
nameof<TenantConfiguration>(x => x.type),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor2)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor3)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.secondaryColor)].join('.'),
])
.pipe(map(data => data as TenantConfiguration))
.subscribe(
data => {
if (data?.cssColors) {
if (data.cssColors.primaryColor) document.documentElement.style.setProperty(`--primary-color`, data.cssColors.primaryColor);
if (data.cssColors.primaryColor2) document.documentElement.style.setProperty(`--primary-color-2`, data.cssColors.primaryColor2);
if (data.cssColors.primaryColor3) document.documentElement.style.setProperty(`--primary-color-3`, data.cssColors.primaryColor3);
if (data.cssColors.secondaryColor) document.documentElement.style.setProperty(`--secondary-color`, data.cssColors.secondaryColor);
}
},
);
.pipe(map(data => data as TenantConfiguration))
.subscribe(
data => {
if (data?.cssColors) {
if (data.cssColors.primaryColor) document.documentElement.style.setProperty(`--primary-color`, data.cssColors.primaryColor);
if (data.cssColors.primaryColor2) document.documentElement.style.setProperty(`--primary-color-2`, data.cssColors.primaryColor2);
if (data.cssColors.primaryColor3) document.documentElement.style.setProperty(`--primary-color-3`, data.cssColors.primaryColor3);
if (data.cssColors.secondaryColor) document.documentElement.style.setProperty(`--secondary-color`, data.cssColors.secondaryColor);
}
},
);
}
}

View File

@ -41,6 +41,7 @@ import { CoreAnnotationServiceModule } from 'annotation-service/services/core-se
import { CoreNotificationServiceModule } from '@notification-service/services/core-service.module';
import { DepositOauth2DialogModule } from './ui/misc/deposit-oauth2-dialog/deposit-oauth2-dialog.module';
import { AnalyticsService } from './core/services/matomo/analytics-service';
import { TenantHandlingService } from './core/services/tenant/tenant-handling.service';
// AoT requires an exported function for factories
export function HttpLoaderFactory(languageHttpService: LanguageHttpService) {
@ -82,7 +83,7 @@ const appearance: MatFormFieldDefaultOptions = {
// appearance: 'standard'
};
export function InstallationConfigurationFactory(appConfig: ConfigurationService, keycloak: KeycloakService, authService: AuthService, languageService: LanguageService) {
export function InstallationConfigurationFactory(appConfig: ConfigurationService, keycloak: KeycloakService, authService: AuthService, languageService: LanguageService, tenantHandlingService:TenantHandlingService) {
return () => appConfig.loadConfiguration().then(() => {
return languageService.loadAvailableLanguages().toPromise();
}).then(x => keycloak.init({
@ -109,77 +110,81 @@ export function InstallationConfigurationFactory(appConfig: ConfigurationService
InterceptorType.UnauthorizedResponse,
]
};
const tenantCode = tenantHandlingService.extractTenantCodeFromUrlPath(window.location.pathname) ?? authService.selectedTenant() ?? 'default';
const tokenPromise = keycloak.getToken();
return authService.prepareAuthRequest(from(tokenPromise), { params }).toPromise().catch(error => authService.onAuthenticateError(error));
return authService.prepareAuthRequest(from(tokenPromise), tenantCode, { params }).toPromise().catch(error => authService.onAuthenticateError(error));
}));
}
@NgModule({ declarations: [
AppComponent,
ReloadHelperComponent
],
bootstrap: [AppComponent], imports: [BrowserModule,
BrowserAnimationsModule,
KeycloakAngularModule,
CoreServiceModule.forRoot(),
CoreAnnotationServiceModule.forRoot(),
CoreNotificationServiceModule.forRoot(),
AppRoutingModule,
CommonUiModule,
TranslateModule.forRoot({
compiler: { provide: TranslateCompiler, useClass: OpenDMPCustomTranslationCompiler },
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [LanguageHttpService]
}
}),
OverlayModule,
CommonHttpModule,
MatMomentDateModule,
LoginModule,
//Ui
NotificationModule,
// BreadcrumbModule,
ReactiveFormsModule,
FormsModule,
NavbarModule,
SidebarModule,
NgcCookieConsentModule.forRoot(cookieConfig),
DepositOauth2DialogModule,
GuidedTourModule.forRoot(),
DragulaModule.forRoot(),
NgxMatomoModule.forRoot({
mode: MatomoInitializationMode.AUTO_DEFERRED,
})], providers: [
ConfigurationService,
{
provide: APP_INITIALIZER,
useFactory: InstallationConfigurationFactory,
deps: [ConfigurationService, KeycloakService, AuthService, LanguageService],
multi: true
},
{
provide: MAT_DATE_LOCALE,
deps: [CultureService],
useFactory: (cultureService) => cultureService.getCurrentCulture().name
},
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: DateAdapter, useClass: MomentUtcDateAdapter },
{
provide: LOCALE_ID,
deps: [CultureService],
useFactory: (cultureService) => cultureService.getCurrentCulture().name
},
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: appearance
},
Title,
CookieService,
MatomoService,
AnalyticsService,
provideHttpClient(withInterceptorsFromDi())
] })
@NgModule({
declarations: [
AppComponent,
ReloadHelperComponent
],
bootstrap: [AppComponent], imports: [BrowserModule,
BrowserAnimationsModule,
KeycloakAngularModule,
CoreServiceModule.forRoot(),
CoreAnnotationServiceModule.forRoot(),
CoreNotificationServiceModule.forRoot(),
AppRoutingModule,
CommonUiModule,
TranslateModule.forRoot({
compiler: { provide: TranslateCompiler, useClass: OpenDMPCustomTranslationCompiler },
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [LanguageHttpService]
}
}),
OverlayModule,
CommonHttpModule,
MatMomentDateModule,
LoginModule,
//Ui
NotificationModule,
// BreadcrumbModule,
ReactiveFormsModule,
FormsModule,
NavbarModule,
SidebarModule,
NgcCookieConsentModule.forRoot(cookieConfig),
DepositOauth2DialogModule,
GuidedTourModule.forRoot(),
DragulaModule.forRoot(),
NgxMatomoModule.forRoot({
mode: MatomoInitializationMode.AUTO_DEFERRED,
})], providers: [
ConfigurationService,
{
provide: APP_INITIALIZER,
useFactory: InstallationConfigurationFactory,
deps: [ConfigurationService, KeycloakService, AuthService, LanguageService, TenantHandlingService],
multi: true
},
{
provide: MAT_DATE_LOCALE,
deps: [CultureService],
useFactory: (cultureService) => cultureService.getCurrentCulture().name
},
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: DateAdapter, useClass: MomentUtcDateAdapter },
{
provide: LOCALE_ID,
deps: [CultureService],
useFactory: (cultureService) => cultureService.getCurrentCulture().name
},
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: appearance
},
Title,
CookieService,
MatomoService,
AnalyticsService,
provideHttpClient(withInterceptorsFromDi())
]
})
export class AppModule { }

View File

@ -34,6 +34,7 @@ export enum ResponseErrorCode {
DmpInactiveUser = 134,
DmpMissingUserContactInfo = 135,
ImportDescriptionWithoutDmpDescriptionTemplate = 136,
DuplicateDmpUser = 137,
// Notification & Annotation Errors
InvalidApiKey = 200,
@ -116,6 +117,8 @@ export class ResponseErrorCodeHelper {
return language.instant("GENERAL.BACKEND-ERRORS.IMPORT-DESCRIPTION-WITHOUT-DMP-DESCRIPTION-TEMPLATE");
case ResponseErrorCode.InvalidApiKey:
return language.instant("GENERAL.BACKEND-ERRORS.INVALID-API-KEY");
case ResponseErrorCode.DuplicateDmpUser:
return language.instant("GENERAL.BACKEND-ERRORS.DUPLICATE-DMP-USER");
case ResponseErrorCode.StaleApiKey:
return language.instant("GENERAL.BACKEND-ERRORS.STALE-API-KEY");
case ResponseErrorCode.SensitiveInfo:

View File

@ -45,6 +45,7 @@ import { VisibilityRulesService } from '@app/ui/description/editor/description-f
import { StorageFileService } from './services/storage-file/storage-file.service';
import { TenantConfigurationService } from './services/tenant-configuration/tenant-configuration.service';
import { DefaultUserLocaleService } from './services/default-user-locale/default-user-locale.service';
import { TenantHandlingService } from './services/tenant/tenant-handling.service';
//
//
// This is shared module that provides all the services. Its imported only once on the AppModule.
@ -109,7 +110,8 @@ export class CoreServiceModule {
PrefillingSourceService,
VisibilityRulesService,
TenantConfigurationService,
StorageFileService
StorageFileService,
TenantHandlingService
],
};
}

View File

@ -154,11 +154,11 @@ export class AuthService extends BaseService {
public isLoggedIn(): boolean {
return this.authState();
}
public prepareAuthRequest(observable: Observable<string>, httpParams?: Object): Observable<boolean> {
public prepareAuthRequest(observable: Observable<string>, tenantCode: string, httpParams?: Object): Observable<boolean> {
return observable.pipe(
map((x) => this.currentAuthenticationToken(x)),
exhaustMap(() => forkJoin([
this.accessToken ? this.ensureTenant() : of(false),
this.accessToken ? this.ensureTenant(tenantCode ?? this.selectedTenant() ?? 'default') : of(false),
this.accessToken ? this.principalService.me(httpParams) : of(null),
])),
map((item) => {
@ -176,10 +176,7 @@ export class AuthService extends BaseService {
);
}
public ensureTenant(): Observable<string> {
if (!this.selectedTenant()) {
this.selectedTenant('default');
}
private ensureTenant(tenantCode: string): Observable<string> {
const params = new BaseHttpParams();
params.interceptorContext = {
excludedInterceptors: [InterceptorType.TenantHeaderInterceptor]
@ -188,10 +185,10 @@ export class AuthService extends BaseService {
map(
(myTenants) => {
if (myTenants) {
if (this.selectedTenant()) {
if (myTenants.findIndex(x => x.code.toLocaleLowerCase() == this.selectedTenant().toLocaleLowerCase()) < 0) {
this.selectedTenant(null);
}
if (myTenants.some(x => x.code.toLocaleLowerCase() == tenantCode.toLocaleLowerCase())) {
this.selectedTenant(tenantCode);
} else {
this.selectedTenant(null);
}
if (!this.selectedTenant()) {
if (myTenants.length > 0) {
@ -326,6 +323,7 @@ export class AuthService extends BaseService {
return this.prepareAuthRequest(
from(this.keycloakService.getToken()),
this.selectedTenant(),
httpParams
)
.pipe(takeUntil(this._destroyed))

View File

@ -57,21 +57,34 @@ export class CultureService {
// Set angular locale based on user selection.
// This is a very hacky way to map cultures with angular cultures, since there is no mapping. We first try to
// use the culture with the specialization (ex en-US), and if not exists we import the base culture (first part).
let locale = newCulture.name;
import(`/node_modules/@angular/common/locales/${locale}.mjs`).catch(reason => {
this.logger.error('Could not load locale: ' + locale);
}).then(selectedLocale => {
if (selectedLocale) {
registerLocaleData(selectedLocale.default);
} else {
locale = newCulture.name.split('-')[0];
import(`/node_modules/@angular/common/locales/${locale}.mjs`).catch(reason => {
this.logger.error('Could not load locale: ' + locale);
}).then(selectedDefaultLocale => {
registerLocaleData(selectedDefaultLocale.default);
});
}
});
// let locale = newCulture.name;
// const base = import(
// /* webpackExclude: /\.d\.ts$/ */
// /* webpackMode: "lazy-once" */
// /* webpackChunkName: "i18n-base" */
// `@angular/common/locales/${locale}.mjs`)//.then(m => m[basePkg]);
// const extra = import(
// /* webpackExclude: /\.d\.ts$/ */
// /* webpackMode: "lazy-once" */
// /* webpackChunkName: "i18n-extra" */
// `@angular/common/locales/extra/${locale.split('-')[0]}.mjs`)//.then(m => m[extraPkg]);
// import(`/node_modules/ @angular/common/locales/${locale}.mjs`).catch(reason => {
// this.logger.error('Could not load locale: ' + locale);
// }).then(selectedLocale => {
// if (selectedLocale) {
// registerLocaleData(selectedLocale.default);
// } else {
// locale = newCulture.name.split('-')[0];
// import(`/node_modules/@angular/common/locales/${locale}.mjs`).catch(reason => {
// this.logger.error('Could not load locale: ' + locale);
// }).then(selectedDefaultLocale => {
// registerLocaleData(selectedDefaultLocale.default);
// });
// }
// });
}
getCultureChangeObservable(): Observable<CultureInfo> {

View File

@ -148,9 +148,9 @@ export class DescriptionTemplateService {
//
// tslint:disable-next-line: member-ordering
descriptionTempalteGroupSingleAutocompleteConfiguration: SingleAutoCompleteConfiguration = {
initialItems: (data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup()).pipe(map(x => x.items)),
filterFn: (searchQuery: string, data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup(searchQuery)).pipe(map(x => x.items)),
getSelectedItem: (selectedItem: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup(null, null, [selectedItem])).pipe(map(x => x.items[0])),
initialItems: (data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active])).pipe(map(x => x.items)),
filterFn: (searchQuery: string, data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], searchQuery)).pipe(map(x => x.items)),
getSelectedItem: (selectedItem: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active, IsActive.Inactive], null, null, [selectedItem])).pipe(map(x => x.items[0])),
displayFn: (item: DescriptionTemplate) => item.label,
titleFn: (item: DescriptionTemplate) => item.label,
subtitleFn: (item: DescriptionTemplate) => item.description,
@ -160,9 +160,9 @@ export class DescriptionTemplateService {
// tslint:disable-next-line: member-ordering
descriptionTempalteGroupMultipleAutocompleteConfiguration: MultipleAutoCompleteConfiguration = {
initialItems: (excludedItems: any[], data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup(null, excludedItems ? excludedItems : null)).pipe(map(x => x.items)),
filterFn: (searchQuery: string, excludedItems: any[]) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup(searchQuery, excludedItems)).pipe(map(x => x.items)),
getSelectedItems: (selectedItems: any[]) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup(null, null, selectedItems)).pipe(map(x => x.items)),
initialItems: (excludedItems: any[], data?: any) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], null, excludedItems ? excludedItems : null)).pipe(map(x => x.items)),
filterFn: (searchQuery: string, excludedItems: any[]) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], searchQuery, excludedItems)).pipe(map(x => x.items)),
getSelectedItems: (selectedItems: any[]) => this.query(this.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active, IsActive.Inactive], null, null, selectedItems)).pipe(map(x => x.items)),
displayFn: (item: DescriptionTemplate) => item.label,
titleFn: (item: DescriptionTemplate) => item.label,
subtitleFn: (item: DescriptionTemplate) => item.description,
@ -170,14 +170,14 @@ export class DescriptionTemplateService {
popupItemActionIcon: 'visibility'
};
public buildDescriptionTempalteGroupAutocompleteLookup(like?: string, excludedIds?: Guid[], groupIds?: Guid[], excludedGroupIds?: Guid[]): DescriptionTemplateLookup {
public buildDescriptionTempalteGroupAutocompleteLookup(isActive: IsActive[], like?: string, excludedIds?: Guid[], groupIds?: Guid[], excludedGroupIds?: Guid[]): DescriptionTemplateLookup {
const lookup: DescriptionTemplateLookup = new DescriptionTemplateLookup();
lookup.page = { size: 100, offset: 0 };
if (excludedIds && excludedIds.length > 0) { lookup.excludedIds = excludedIds; }
if (groupIds && groupIds.length > 0) { lookup.groupIds = groupIds; }
if (excludedGroupIds && excludedGroupIds.length > 0) { lookup.excludedGroupIds = excludedGroupIds; }
lookup.isActive = [IsActive.Active];
lookup.isActive = isActive;
lookup.versionStatuses = [DescriptionTemplateVersionStatus.Current, DescriptionTemplateVersionStatus.NotFinalized];
lookup.statuses = [DescriptionTemplateStatus.Finalized];
lookup.project = {

View File

@ -0,0 +1,58 @@
import { DOCUMENT, LocationStrategy } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
@Injectable()
export class TenantHandlingService {
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly locationStrategy: LocationStrategy,
private readonly router: Router,
) {
}
extractTenantCodeFromUrlPath(path: string): string {
//Searches for "/t/<tenant_code>/" in a url;
const tenantRegex = new RegExp("\/t\/([^\/]+)");
const regexResult = tenantRegex.exec(path);
let tenantCode = null;
if (Array.isArray(regexResult) && regexResult.length > 0) {
tenantCode = regexResult[1];
}
return tenantCode;
}
getCurrentUrlEnrichedWithTenantCode(tenantCode: string, withOrigin: boolean) {
const path = this.getUrlEnrichedWithTenantCode(this.router.routerState.snapshot.url, tenantCode)
return withOrigin ? this.getBaseUrl() + path.substring(1) : path;
}
getUrlEnrichedWithTenantCode(url: string, tenantCode: string) {
const urlTree: UrlTree = this.router.parseUrl(url);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
const tenantParamIndex = urlSegments.findIndex(x => x.path == 't');
if (tenantParamIndex >= 0) {
if (tenantCode == 'default') {
urlSegments.splice(tenantParamIndex, 2);
} else {
const tenantCodeSegment = urlSegments.at(tenantParamIndex + 1);
tenantCodeSegment.path = tenantCode;
}
} else {
if (tenantCode != 'default') {
urlTree.root.children[PRIMARY_OUTLET].segments = [new UrlSegment('t', {}), new UrlSegment(tenantCode, {}), ...urlSegments];
}
}
return urlTree.toString();
}
getBaseUrl(): string {
return this.document.location.origin + this.locationStrategy.getBaseHref();
}
}

View File

@ -117,6 +117,14 @@ export class UserService {
catchError((error: any) => throwError(error)));
}
getUserTokenPermission(token: Guid): Observable<boolean> {
const url = `${this.apiBase}/mine/get-permission/token/${token}`;
return this.http
.get<boolean>(url).pipe(
catchError((error: any) => throwError(error)));
}
confirmMergeAccount(token: Guid): Observable<boolean> {
const url = `${this.apiBase}/mine/confirm-merge-account/token/${token}`;

View File

@ -76,7 +76,7 @@ export class DescriptionTemplatePreviewDialogComponent extends BaseComponent imp
}
buildForm() {
this.formGroup = this.editorModel.buildForm(null, true);
this.formGroup = this.editorModel.buildForm(null, true, this.visibilityRulesService);
this.previewPropertiesFormGroup = this.editorModel.properties.buildForm() as UntypedFormGroup;
this.visibilityRulesService.setContext(this.descriptionTemplate.definition, this.previewPropertiesFormGroup);
}

View File

@ -31,13 +31,13 @@
<div style="position: relative;" class="col-12" *ngIf="hasFocus" [@fade-in]>
<div *ngIf="showDescription" class="mb-4">
<h5 style="font-weight: bold" class="row">{{'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.DESCRIPTION' | translate}}</h5>
<rich-text-editor-component [form]="form.get('description')" [id]="'editor1'" [placeholder]="'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.DESCRIPTION'" [wrapperClasses]="'row'" [editable]="!viewOnly">
<rich-text-editor-component [form]="form.get('description')" [placeholder]="'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.DESCRIPTION'" [wrapperClasses]="'row'" [editable]="!viewOnly">
</rich-text-editor-component>
<mat-error *ngIf="this.form.get('description').hasError('backendError')">{{form.get('description').getError('backendError').message}}</mat-error>
</div>
<div *ngIf="showExtendedDescription" class="mb-4">
<h5 style="font-weight: bold" class="row">{{'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.EXTENDED-DESCRIPTION' | translate}}</h5>
<rich-text-editor-component [form]="form.get('extendedDescription')" [id]="'editor2'" [placeholder]="'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.EXTENDED-DESCRIPTION'" [wrapperClasses]="'row'" [editable]="!viewOnly">
<rich-text-editor-component [form]="form.get('extendedDescription')" [placeholder]="'DESCRIPTION-TEMPLATE-EDITOR.STEPS.FORM.COMPOSITE-FIELD.FIELDS.EXTENDED-DESCRIPTION'" [wrapperClasses]="'row'" [editable]="!viewOnly">
</rich-text-editor-component>
<mat-error *ngIf="this.form.get('extendedDescription').hasError('backendError')">{{form.get('extendedDescription').getError('backendError').message}}</mat-error>
</div>

View File

@ -72,9 +72,9 @@ export class DmpBlueprintEditorComponent extends BaseEditor<DmpBlueprintEditorMo
public dmpBlueprintExtraFieldDataTypeEnum = this.enumUtils.getEnumValues<DmpBlueprintExtraFieldDataType>(DmpBlueprintExtraFieldDataType);
public dmpBlueprintFieldCategoryEnum = this.enumUtils.getEnumValues<DmpBlueprintFieldCategory>(DmpBlueprintFieldCategory);
descriptionTempalteGroupSingleAutocompleteConfiguration: SingleAutoCompleteConfiguration = {
initialItems: (data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, null, null, this.getUsedDescriptionTemplateGroupIds())).pipe(map(x => x.items)),
filterFn: (searchQuery: string, data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(searchQuery, null, null, this.getUsedDescriptionTemplateGroupIds() ? this.getUsedDescriptionTemplateGroupIds() : null)).pipe(map(x => x.items)),
getSelectedItem: (selectedItem: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, null, [selectedItem])).pipe(map(x => x.items[0])),
initialItems: (data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], null, null, null, this.getUsedDescriptionTemplateGroupIds())).pipe(map(x => x.items)),
filterFn: (searchQuery: string, data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], searchQuery, null, null, this.getUsedDescriptionTemplateGroupIds() ? this.getUsedDescriptionTemplateGroupIds() : null)).pipe(map(x => x.items)),
getSelectedItem: (selectedItem: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active, IsActive.Inactive], null, null, [selectedItem])).pipe(map(x => x.items[0])),
displayFn: (item: DescriptionTemplate) => item.label,
titleFn: (item: DescriptionTemplate) => item.label,
subtitleFn: (item: DescriptionTemplate) => item.description,

View File

@ -88,7 +88,7 @@
<mat-icon>more_horiz</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
<button mat-menu-item [routerLink]="['/dmp-blueprints/', row.id]">
<button *ngIf="(row.status != null && row.status === dmpBlueprintStatuses.Draft)" mat-menu-item [routerLink]="['/dmp-blueprints/', row.id]">
<mat-icon>edit</mat-icon>{{'DMP-BLUEPRINT-LISTING.ACTIONS.EDIT' | translate}}
</button>
<button *ngIf="row.belongsToCurrentTenant != false && (row.status === dmpBlueprintStatuses.Finalized || row.status == null)" mat-menu-item [routerLink]="['/dmp-blueprints/new-version' , row.id]">

View File

@ -1,10 +1,8 @@
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '@app/core/services/auth/auth.service';
import { PrincipalService } from '@app/core/services/http/principal.service';
import { TenantHandlingService } from '@app/core/services/tenant/tenant-handling.service';
import { BaseComponent } from '@common/base/base.component';
import { BaseHttpParams } from '@common/http/base-http-params';
import { InterceptorType } from '@common/http/interceptors/interceptor-type';
import { KeycloakService } from 'keycloak-angular';
import { from } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -27,7 +25,7 @@ export class LoginComponent extends BaseComponent implements OnInit {
private router: Router,
private authService: AuthService,
private route: ActivatedRoute,
private principalService: PrincipalService,
private tenantHandlingService: TenantHandlingService,
private keycloakService: KeycloakService
) { super(); }
@ -36,7 +34,8 @@ export class LoginComponent extends BaseComponent implements OnInit {
if (!this.keycloakService.isLoggedIn()) {
this.authService.authenticate(this.returnUrl);
} else {
this.authService.prepareAuthRequest(from(this.keycloakService.getToken())).pipe(takeUntil(this._destroyed)).subscribe(
const tenantCode = this.tenantHandlingService.extractTenantCodeFromUrlPath(this.returnUrl) ?? this.authService.selectedTenant() ?? 'default';
this.authService.prepareAuthRequest(from(this.keycloakService.getToken()), tenantCode).pipe(takeUntil(this._destroyed)).subscribe(
() => {
let returnUrL = this.returnUrl;
this.zone.run(() => this.router.navigateByUrl(returnUrL));

View File

@ -4,7 +4,7 @@
<div class="col merge-account-title">{{'MERGE-ACCOUNT.TITLE' | translate}}</div>
</div>
<div *ngIf="showForm" class="row merge-account-content">
<div class="col">
<div *ngIf="isTokenValid" class="col">
<div class="row justify-content-center">
<div class="col-auto">
<span>
@ -20,6 +20,9 @@
</div>
</div>
</div>
<div *ngIf="!isTokenValid" class="col">
<span>{{'MERGE-ACCOUNT.MESSAGES.INVALID-TOKEN' | translate}}</span>
</div>
</div>
<ng-template #loading>
</ng-template>

View File

@ -16,6 +16,8 @@ import { takeUntil } from "rxjs/operators";
})
export class MergeEmailConfirmation extends BaseComponent implements OnInit {
isTokenValid: boolean = false;
private token: Guid;
get showForm(): boolean {
@ -37,9 +39,19 @@ export class MergeEmailConfirmation extends BaseComponent implements OnInit {
.subscribe(params => {
const token = params['token']
if (token != null) {
this.token = token;
this.userService.getUserTokenPermission(token)
.subscribe(result => {
this.isTokenValid = result
this.token = token;
});
}
},
error => {
this.isTokenValid = false;
this.token = Guid.createEmpty();
this.onCallbackError(error);
});
}
onConfirm(): void {
@ -64,7 +76,7 @@ export class MergeEmailConfirmation extends BaseComponent implements OnInit {
onCallbackError(errorResponse: HttpErrorResponse) {
const errorOverrides = new Map<number, string>();
errorOverrides.set(302, this.language.instant('EMAIL-CONFIRMATION.EMAIL-FOUND'));
errorOverrides.set(-1, this.language.instant('EMAIL-CONFIRMATION.EXPIRED-EMAIL'));
errorOverrides.set(403, this.language.instant('EMAIL-CONFIRMATION.EXPIRED-EMAIL'));
this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error)
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);

View File

@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '@app/core/services/auth/auth.service';
import { KeycloakService } from 'keycloak-angular';
@ -11,7 +10,7 @@ export class LogoutComponent implements OnInit {
constructor(
private keycloak: KeycloakService,
private authService: AuthService,
) {}
) { }
ngOnInit() {
this.authService.clear();
@ -19,9 +18,5 @@ export class LogoutComponent implements OnInit {
localStorage.clear();
// this.router.navigate(['./'], { replaceUrl: true });
});
// this.tokenService.logout(() => {
// localStorage.clear();
// this.router.navigate(["./"], { replaceUrl: true });
// });
}
}

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { FormattingModule } from '@app/core/formatting.module';
import { DescriptionRoutingModule } from '@app/ui/description/description.routing';
import { DescriptionRoutingModule, PublicDescriptionRoutingModule } from '@app/ui/description/description.routing';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
@ -17,3 +17,17 @@ import { CommonUiModule } from '@common/ui/common-ui.module';
]
})
export class DescriptionModule { }
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
FormattingModule,
PublicDescriptionRoutingModule,
],
declarations: [
],
exports: [
]
})
export class PublicDescriptionModule { }

View File

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@app/core/auth-guard.service';
import { BreadcrumbService } from '../misc/breadcrumb/breadcrumb.service';
// import { DescriptionWizardComponent } from './description-wizard/description-wizard.component';
// import { DescriptionOverviewComponent } from './overview/description-overview.component';
const routes: Routes = [
{
path: 'overview',
loadChildren: () => import('./overview/description-overview.module').then(m => m.DescriptionOverviewModule),
canActivate: [AuthGuard],
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -18,6 +18,28 @@ const routes: Routes = [
{
path: 'edit',
loadChildren: () => import('./editor/description-editor.module').then(m => m.DescriptionEditorModule),
canActivate: [AuthGuard],
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
hideNavigationItem: true
}),
}
},
{
path: '',
canActivate: [AuthGuard],
loadChildren: () => import('./listing/description-listing.module').then(m => m.DescriptionListingModule),
data: {
breadcrumb: true
},
},
];
const publicRoutes: Routes = [
{
path: 'overview',
loadChildren: () => import('./overview/description-overview.module').then(m => m.DescriptionOverviewModule),
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -40,3 +62,10 @@ const routes: Routes = [
providers: []
})
export class DescriptionRoutingModule { }
@NgModule({
imports: [RouterModule.forChild(publicRoutes)],
exports: [RouterModule],
providers: []
})
export class PublicDescriptionRoutingModule { }

View File

@ -220,7 +220,7 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
permissionPerSection => {
this.canEdit = permissionPerSection && permissionPerSection[this.item.dmpDescriptionTemplate.sectionId.toString()] && permissionPerSection[this.item.dmpDescriptionTemplate.sectionId.toString()].some(x => x === AppPermission.EditDescription);
this.canReview = permissionPerSection && permissionPerSection[this.item.dmpDescriptionTemplate.sectionId.toString()] && permissionPerSection[this.item.dmpDescriptionTemplate.sectionId.toString()].some(x => x === AppPermission.ReviewDescription);
this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.canEdit);
this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.canEdit, this.visibilityRulesService);
if (this.item.descriptionTemplate?.definition) this.visibilityRulesService.setContext(this.item.descriptionTemplate.definition, this.formGroup.get('properties'));
if (this.item.descriptionTemplate?.definition) this.pageToFieldSetMap = this.mapPageToFieldSet(this.item.descriptionTemplate);;
@ -681,7 +681,7 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
this.initialTemplateId = descriptionTemplateId.toString();
this.editorModel.properties = new DescriptionPropertyDefinitionEditorModel(this.editorModel.validationErrorModel).fromModel(null, descriptionTemplate, null);
this.formGroup.setControl('properties', this.editorModel.buildProperties());
this.formGroup.setControl('properties', this.editorModel.buildProperties(this.visibilityRulesService));
this.item.descriptionTemplate = descriptionTemplate;
const sectionId = this.item.dmpDescriptionTemplate.sectionId;
@ -755,6 +755,8 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
finalize() {
this.formService.removeAllBackEndErrors(this.formGroup);
this.formService.touchAllFormFields(this.formGroup);
this.formService.validateAllFormFields(this.formGroup);
this.tocValidationService.validateForm();
if (!this.isFormValid()) {
this.dialog.open(FormValidationErrorsDialogComponent, {

View File

@ -1,15 +1,17 @@
import { FormControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { DescriptionStatus } from "@app/core/common/enum/description-status";
import { DescriptionTemplateFieldType } from "@app/core/common/enum/description-template-field-type";
import { DescriptionTemplateFieldValidationType } from "@app/core/common/enum/description-template-field-validation-type";
import { IsActive } from "@app/core/common/enum/is-active.enum";
import { DescriptionTemplate, DescriptionTemplateField, DescriptionTemplateFieldSet, DescriptionTemplateSection } from "@app/core/model/description-template/description-template";
import { Description, DescriptionExternalIdentifier, DescriptionExternalIdentifierPersist, DescriptionField, DescriptionFieldPersist, DescriptionPersist, DescriptionPropertyDefinition, DescriptionPropertyDefinitionFieldSet, DescriptionPropertyDefinitionFieldSetItem, DescriptionPropertyDefinitionFieldSetItemPersist, DescriptionPropertyDefinitionFieldSetPersist, DescriptionPropertyDefinitionPersist, DescriptionReference, DescriptionReferencePersist } from "@app/core/model/description/description";
import { ReferencePersist } from "@app/core/model/reference/reference";
import { BaseEditorModel } from "@common/base/base-form-editor-model";
import { BackendErrorValidator } from '@common/forms/validation/custom-validator';
import { BackendErrorValidator, MinMaxValidator, RequiredWithVisibilityRulesValidator, UrlValidator } 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';
import { Guid } from "@common/types/guid";
import { VisibilityRulesService } from "./description-form/visibility-rules/visibility-rules.service";
export class DescriptionEditorModel extends BaseEditorModel implements DescriptionPersist {
label: string;
@ -42,7 +44,7 @@ export class DescriptionEditorModel extends BaseEditorModel implements Descripti
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
buildForm(context: ValidationContext = null, disabled: boolean = false, visibilityRulesService: VisibilityRulesService): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
@ -54,14 +56,15 @@ export class DescriptionEditorModel extends BaseEditorModel implements Descripti
status: [{ value: this.status, disabled: disabled }, context.getValidation('status').validators],
description: [{ value: this.description, disabled: disabled }, context.getValidation('description').validators],
tags: [{ value: this.tags, disabled: disabled }, context.getValidation('tags').validators],
properties: this.buildProperties(),
properties: this.buildProperties(visibilityRulesService),
hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators]
});
}
buildProperties() {
buildProperties(visibilityRulesService: VisibilityRulesService) {
return this.properties.buildForm({
rootPath: `properties.`
rootPath: `properties.`,
visibilityRulesService: visibilityRulesService
});
}
@ -82,18 +85,37 @@ export class DescriptionEditorModel extends BaseEditorModel implements Descripti
return baseContext;
}
static reApplyPropertiesValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
}): void {
const { formGroup, validationErrorModel } = params;
const control = formGroup?.get('properties');
DescriptionPropertyDefinitionEditorModel.reapplyValidators({
formGroup: control.get('fieldSets') as UntypedFormGroup,
rootPath: `properties.`,
validationErrorModel: validationErrorModel
});
static getFieldValueControlName(fieldType: DescriptionTemplateFieldType, multipleSelect: boolean): string {
switch (fieldType) {
case DescriptionTemplateFieldType.FREE_TEXT:
case DescriptionTemplateFieldType.TEXT_AREA:
case DescriptionTemplateFieldType.UPLOAD:
case DescriptionTemplateFieldType.RICH_TEXT_AREA:
case DescriptionTemplateFieldType.RADIO_BOX:
return 'textValue';
case DescriptionTemplateFieldType.DATASET_IDENTIFIER:
case DescriptionTemplateFieldType.VALIDATION:
return 'externalIdentifier';
case DescriptionTemplateFieldType.DATE_PICKER:
return 'dateValue';
case DescriptionTemplateFieldType.CHECK_BOX:
case DescriptionTemplateFieldType.BOOLEAN_DECISION:
return 'booleanValue';
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DESCRIPTIONS:
if (multipleSelect) return 'textListValue';
else return 'textValue';
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DMPS:
if (multipleSelect) return 'textListValue';
else return 'textValue';
case DescriptionTemplateFieldType.REFERENCE_TYPES:
if (multipleSelect) return 'references';
else return 'reference';
case DescriptionTemplateFieldType.SELECT:
if (multipleSelect) return 'textListValue';
else return 'textValue';
case DescriptionTemplateFieldType.TAGS:
return 'tags';
}
}
}
@ -113,7 +135,8 @@ export class DescriptionPropertyDefinitionEditorModel implements DescriptionProp
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
rootPath?: string,
visibilityRulesService: VisibilityRulesService
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
@ -128,7 +151,8 @@ export class DescriptionPropertyDefinitionEditorModel implements DescriptionProp
const fieldSetsFormGroup = this.formBuilder.group({});
if (this.fieldSets.size > 0) {
this.fieldSets.forEach((value, key) => fieldSetsFormGroup.addControl(key.toString(), value.buildForm({
rootPath: `${rootPath}fieldSets[${key}].`
rootPath: `${rootPath}fieldSets[${key}].`,
visibilityRulesService: params.visibilityRulesService
})), context.getValidation('fieldSets'));
formGroup.addControl('fieldSets', fieldSetsFormGroup);
}
@ -149,25 +173,6 @@ export class DescriptionPropertyDefinitionEditorModel implements DescriptionProp
return baseContext;
}
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const keys = Object.keys(formGroup.value as Object);
keys.forEach((key) => {
const formArray = formGroup?.get(key);
DescriptionPropertyDefinitionFieldSetEditorModel.reapplyValidators({
formArray: formArray.get('items') as UntypedFormArray,
rootPath: `${rootPath}fieldSets[${key}].`,
validationErrorModel: validationErrorModel
})
});
}
private calculateProperties(item: DescriptionPropertyDefinition, descriptionTemplate: DescriptionTemplate, descriptionReferences: DescriptionReference[]): Map<string, DescriptionPropertyDefinitionFieldSetEditorModel> {
let result: Map<string, DescriptionPropertyDefinitionFieldSetEditorModel> = new Map<string, DescriptionPropertyDefinitionFieldSetEditorModel>();
if (descriptionTemplate) (
@ -265,11 +270,14 @@ export class DescriptionPropertyDefinitionFieldSetEditorModel implements Descrip
items?: DescriptionPropertyDefinitionFieldSetItemEditorModel[] = [];
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
fieldSetDefinition: DescriptionTemplateFieldSet;
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: DescriptionPropertyDefinitionFieldSet, descriptionReferences: DescriptionReference[], definitionFieldSet: DescriptionTemplateFieldSet): DescriptionPropertyDefinitionFieldSetEditorModel {
this.fieldSetDefinition = definitionFieldSet;
if (item) {
if (item.items) { item.items.map(x => this.items.push(new DescriptionPropertyDefinitionFieldSetItemEditorModel(this.validationErrorModel).fromModel(x, descriptionReferences, definitionFieldSet))); }
}
@ -279,13 +287,15 @@ export class DescriptionPropertyDefinitionFieldSetEditorModel implements Descrip
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
rootPath?: string,
visibilityRulesService: VisibilityRulesService
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = DescriptionPropertyDefinitionFieldSetEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
rootPath,
fieldSetDefinition: this.fieldSetDefinition
});
}
@ -293,7 +303,8 @@ export class DescriptionPropertyDefinitionFieldSetEditorModel implements Descrip
items: this.formBuilder.array(
(this.items ?? []).map(
(item, index) => item.buildForm({
rootPath: `${rootPath}items[${index}].`
rootPath: `${rootPath}items[${index}].`,
visibilityRulesService: params.visibilityRulesService
})
), context.getValidation('items').validators
)
@ -302,14 +313,25 @@ export class DescriptionPropertyDefinitionFieldSetEditorModel implements Descrip
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
validationErrorModel: ValidationErrorModel,
fieldSetDefinition: DescriptionTemplateFieldSet
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'items', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}items`)] });
const validators = [];
validators.push(BackendErrorValidator(validationErrorModel, `${rootPath}items`));
if (params.fieldSetDefinition?.multiplicity?.min >= 0 && params.fieldSetDefinition?.multiplicity?.max >= 0) {
validators.push(MinMaxValidator(params.fieldSetDefinition.multiplicity.min, params.fieldSetDefinition.multiplicity.min));
} else if (params.fieldSetDefinition?.multiplicity?.min >= 0) {
validators.push(Validators.min(params.fieldSetDefinition.multiplicity.min));
}
else if (params.fieldSetDefinition?.multiplicity?.max >= 0) {
validators.push(Validators.max(params.fieldSetDefinition.multiplicity.max));
}
baseValidationArray.push({ key: 'items', validators: validators });
baseContext.validation = baseValidationArray;
return baseContext;
}
@ -317,14 +339,18 @@ export class DescriptionPropertyDefinitionFieldSetEditorModel implements Descrip
static reapplyValidators(params: {
formArray: UntypedFormArray,
validationErrorModel: ValidationErrorModel,
rootPath: string
rootPath: string,
fieldSetDefinition: DescriptionTemplateFieldSet,
visibilityRulesService: VisibilityRulesService
}): void {
const { validationErrorModel, rootPath, formArray } = params;
const { validationErrorModel, rootPath, formArray, fieldSetDefinition } = params;
formArray?.controls?.forEach(
(control, index) => DescriptionPropertyDefinitionFieldSetItemEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `${rootPath}items[${index}].`,
validationErrorModel: validationErrorModel
validationErrorModel: validationErrorModel,
fieldSetDefinition: fieldSetDefinition,
visibilityRulesService: params.visibilityRulesService
})
);
}
@ -359,7 +385,8 @@ export class DescriptionPropertyDefinitionFieldSetItemEditorModel implements Des
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
rootPath?: string,
visibilityRulesService: VisibilityRulesService
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
@ -376,7 +403,9 @@ export class DescriptionPropertyDefinitionFieldSetItemEditorModel implements Des
const fieldsFormGroup = this.formBuilder.group({});
this.fields.forEach((value, key) => fieldsFormGroup.addControl(key.toString(), value.buildForm({
rootPath: `${rootPath}fields[${key}].`
rootPath: `${rootPath}fields[${key}].`,
visibilityRulesService: params.visibilityRulesService,
visibilityRulesKey: key + '_' + formGroup.get('ordinal').value
})), context.getValidation('fields')
)
formGroup.addControl('fields', fieldsFormGroup);
@ -403,10 +432,12 @@ export class DescriptionPropertyDefinitionFieldSetItemEditorModel implements Des
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
rootPath: string,
fieldSetDefinition: DescriptionTemplateFieldSet,
visibilityRulesService: VisibilityRulesService
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const { formGroup, rootPath, validationErrorModel, fieldSetDefinition } = params;
const context = DescriptionPropertyDefinitionFieldSetItemEditorModel.createValidationContext({
rootPath,
validationErrorModel
@ -419,7 +450,10 @@ export class DescriptionPropertyDefinitionFieldSetItemEditorModel implements Des
DescriptionFieldEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `${rootPath}fields[${key}].`,
validationErrorModel: validationErrorModel
validationErrorModel: validationErrorModel,
fieldDefinition: fieldSetDefinition.fields.find(x => x.id == key),
visibilityRulesService: params.visibilityRulesService,
visibilityRulesKey: key + '_' + formGroup.get('ordinal').value
})
});
@ -442,6 +476,8 @@ export class DescriptionFieldEditorModel implements DescriptionFieldPersist {
tags: string[] = [];
externalIdentifier?: DescriptionExternalIdentifierEditorModel = new DescriptionExternalIdentifierEditorModel(this.validationErrorModel);
fieldDefinition: DescriptionTemplateField;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
@ -449,6 +485,7 @@ export class DescriptionFieldEditorModel implements DescriptionFieldPersist {
) { }
public fromModel(item: DescriptionField, descriptionTemplateField: DescriptionTemplateField, descriptionReferences: DescriptionReference[]): DescriptionFieldEditorModel {
this.fieldDefinition = descriptionTemplateField;
if (item) {
this.textValue = item.textValue;
this.textListValue = item.textListValue;
@ -488,46 +525,89 @@ export class DescriptionFieldEditorModel implements DescriptionFieldPersist {
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
rootPath?: string,
visibilityRulesService: VisibilityRulesService,
visibilityRulesKey: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = DescriptionFieldEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
rootPath,
fieldDefinition: this.fieldDefinition,
visibilityRulesService: params.visibilityRulesService,
visibilityRulesKey: params.visibilityRulesKey
});
}
return this.formBuilder.group({
textValue: [{ value: this.textValue, disabled: disabled }, context.getValidation('textValue').validators],
textListValue: [{ value: this.textListValue, disabled: disabled }, context.getValidation('textListValue').validators],
dateValue: [{ value: this.dateValue, disabled: disabled }, context.getValidation('dateValue').validators],
booleanValue: [{ value: this.booleanValue, disabled: disabled }, context.getValidation('booleanValue').validators],
references: [{ value: this.references, disabled: disabled }, context.getValidation('references').validators],
reference: [{ value: this.reference, disabled: disabled }, context.getValidation('reference').validators],
tags: [{ value: this.tags, disabled: disabled }, context.getValidation('tags').validators],
externalIdentifier: this.externalIdentifier.buildForm({
rootPath: `${rootPath}externalIdentifier.`
}),
});
const fieldType = this.fieldDefinition.data.fieldType;
const multipleSelect = this.fieldDefinition.data.multipleSelect;
const fieldValueControlName = DescriptionEditorModel.getFieldValueControlName(fieldType, multipleSelect);
const formGroup = this.formBuilder.group({});
switch (fieldType) {
case DescriptionTemplateFieldType.FREE_TEXT:
case DescriptionTemplateFieldType.TEXT_AREA:
case DescriptionTemplateFieldType.UPLOAD:
case DescriptionTemplateFieldType.RICH_TEXT_AREA:
case DescriptionTemplateFieldType.RADIO_BOX:
formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.DATASET_IDENTIFIER:
case DescriptionTemplateFieldType.VALIDATION:
formGroup.addControl(fieldValueControlName, this.externalIdentifier.buildForm({
rootPath: `${rootPath}externalIdentifier.`
}));
case DescriptionTemplateFieldType.DATE_PICKER:
formGroup.addControl(fieldValueControlName, new FormControl({ value: this.dateValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.CHECK_BOX:
case DescriptionTemplateFieldType.BOOLEAN_DECISION:
formGroup.addControl(fieldValueControlName, new FormControl({ value: this.booleanValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DESCRIPTIONS:
if (multipleSelect) formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textListValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
else formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DMPS:
if (multipleSelect) formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textListValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
else formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.REFERENCE_TYPES:
if (multipleSelect) formGroup.addControl(fieldValueControlName, new FormControl({ value: this.references, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
else formGroup.addControl(fieldValueControlName, new FormControl({ value: this.reference, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.SELECT:
if (multipleSelect) formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textListValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
else formGroup.addControl(fieldValueControlName, new FormControl({ value: this.textValue, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
case DescriptionTemplateFieldType.TAGS:
formGroup.addControl(fieldValueControlName, new FormControl({ value: this.tags, disabled: disabled }, context.getValidation(fieldValueControlName).validators));
}
return formGroup;
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
validationErrorModel: ValidationErrorModel,
fieldDefinition: DescriptionTemplateField,
visibilityRulesService: VisibilityRulesService,
visibilityRulesKey: string
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'textValue', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}textValue`)] });
baseValidationArray.push({ key: 'textListValue', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}textListValue`)] });
baseValidationArray.push({ key: 'dateValue', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}dateValue`)] });
baseValidationArray.push({ key: 'booleanValue', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}booleanValue`)] });
baseValidationArray.push({ key: 'references', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}references`)] });
baseValidationArray.push({ key: 'reference', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}references`)] });
baseValidationArray.push({ key: 'tags', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}tags`)] });
baseValidationArray.push({ key: 'externalIdentifier', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}externalIdentifier`)] });
const fieldValueControlName = DescriptionEditorModel.getFieldValueControlName(params.fieldDefinition.data.fieldType, params.fieldDefinition.data.multipleSelect);
const validators = [];
validators.push(BackendErrorValidator(validationErrorModel, `${rootPath}${fieldValueControlName}`));
params.fieldDefinition.validations.forEach(validation => {
switch (validation) {
case DescriptionTemplateFieldValidationType.Required:
validators.push(RequiredWithVisibilityRulesValidator(params.visibilityRulesService, params.visibilityRulesKey));
break;
case DescriptionTemplateFieldValidationType.Url:
validators.push(UrlValidator());
break;
}
});
baseValidationArray.push({ key: fieldValueControlName, validators: validators });
baseContext.validation = baseValidationArray;
return baseContext;
}
@ -535,13 +615,20 @@ export class DescriptionFieldEditorModel implements DescriptionFieldPersist {
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
rootPath: string,
fieldDefinition: DescriptionTemplateField,
visibilityRulesService: VisibilityRulesService,
visibilityRulesKey: string
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const { formGroup, rootPath, validationErrorModel, fieldDefinition } = params;
const context = DescriptionFieldEditorModel.createValidationContext({
rootPath,
validationErrorModel
validationErrorModel,
fieldDefinition: fieldDefinition,
visibilityRulesService: params.visibilityRulesService,
visibilityRulesKey: params.visibilityRulesKey
});
['textValue', 'textListValue', 'dateValue', 'booleanValue'].forEach(keyField => {
@ -717,45 +804,6 @@ export class DescriptionFieldIndicator {
this.sectionIds = sectionIds;
this.fieldSetId = fieldSetId;
this.fieldId = fieldId;
switch (type) {
case DescriptionTemplateFieldType.FREE_TEXT:
case DescriptionTemplateFieldType.TEXT_AREA:
case DescriptionTemplateFieldType.UPLOAD:
case DescriptionTemplateFieldType.RICH_TEXT_AREA:
case DescriptionTemplateFieldType.RADIO_BOX:
this.type = "textValue";
break;
case DescriptionTemplateFieldType.DATASET_IDENTIFIER:
case DescriptionTemplateFieldType.VALIDATION:
this.type = "externalIdentifier";
break;
case DescriptionTemplateFieldType.DATE_PICKER:
this.type = "dateValue";
break;
case DescriptionTemplateFieldType.CHECK_BOX:
case DescriptionTemplateFieldType.BOOLEAN_DECISION:
this.type = "booleanValue";
break;
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DESCRIPTIONS:
if (multipleSelect) this.type = "textListValue";
else this.type = "textValue"
break;
case DescriptionTemplateFieldType.INTERNAL_ENTRIES_DMPS:
if (multipleSelect) this.type = "textListValue";
else this.type = "textValue";
break;
case DescriptionTemplateFieldType.REFERENCE_TYPES:
if (multipleSelect) this.type = "references";
else this.type = "reference";
break;
case DescriptionTemplateFieldType.SELECT:
if (multipleSelect) this.type = "textListValue";
else this.type = "textValue";
break;
case DescriptionTemplateFieldType.TAGS:
this.type = "tags";
break;
}
this.type = DescriptionEditorModel.getFieldValueControlName(type, multipleSelect);
}
}

View File

@ -82,11 +82,11 @@ export class DescriptionFormFieldSetComponent extends BaseComponent {
}
const properties: DescriptionPropertyDefinitionFieldSet = this.propertiesFormGroup.value;
let ordinal = 0;
if (properties?.items && properties.items.map(x => x.ordinal).filter(val => !isNaN(val)).length > 0) {
if (properties?.items && properties.items.map(x => x.ordinal).filter(val => !isNaN(val)).length > 0) {
ordinal = Math.max(...properties.items.map(x => x.ordinal).filter(val => !isNaN(val))) + 1;
}
const item: DescriptionPropertyDefinitionFieldSetEditorModel = new DescriptionPropertyDefinitionEditorModel(this.validationErrorModel).calculateFieldSetProperties(this.fieldSet, ordinal, null, null);
formArray.push((item.buildForm({ rootPath: `properties.fieldSets[${this.fieldSet.id}].` }).get('items') as UntypedFormArray).at(0));
formArray.push((item.buildForm({ rootPath: `properties.fieldSets[${this.fieldSet.id}].`, visibilityRulesService: this.visibilityRulesService }).get('items') as UntypedFormArray).at(0));
this.visibilityRulesService.reloadVisibility();
}
@ -102,7 +102,9 @@ export class DescriptionFormFieldSetComponent extends BaseComponent {
{
formArray: formArray,
validationErrorModel: this.validationErrorModel,
rootPath: `properties.fieldSets[${this.fieldSet.id}].`
rootPath: `properties.fieldSets[${this.fieldSet.id}].`,
fieldSetDefinition: this.fieldSet,
visibilityRulesService: this.visibilityRulesService
}
);
formArray.markAsDirty();

View File

@ -11,7 +11,7 @@
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{ field.data.label }}</mat-label>
<input matInput [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}" [required]="isRequired">
<input matInput [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}">
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('pattern')">{{'GENERAL.VALIDATION.URL.MESSAGE' | translate}}</mat-error>
@ -31,14 +31,14 @@
<div class="row">
<mat-form-field class="col-md-12">
<ng-container *ngIf="field.data.multipleSelect">
<mat-select [formControl]="propertiesFormGroup?.get(field.id).get('textListValue')" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [required]="isRequired" [multiple]="field.data.multipleSelect">
<mat-select [formControl]="propertiesFormGroup?.get(field.id).get('textListValue')" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [multiple]="field.data.multipleSelect">
<mat-option *ngFor="let opt of field.data.options" [value]="opt.value">{{opt.label}}</mat-option>
</mat-select>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textListValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</ng-container>
<ng-container *ngIf="!(field.data.multipleSelect)">
<mat-select [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [required]="isRequired" [multiple]="field.data.multipleSelect">
<mat-select [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [multiple]="field.data.multipleSelect">
<mat-option *ngFor="let opt of field.data.options" [value]="opt.value">{{opt.label}}
</mat-option>
</mat-select>
@ -51,25 +51,25 @@
<div *ngSwitchCase="descriptionTemplateFieldTypeEnum.INTERNAL_ENTRIES_DESCRIPTIONS" class="col-12">
<div class="row">
<ng-container *ngIf="field.data.multipleSelect">
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<app-multiple-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textListValue')" [configuration]="descriptionService.multipleAutocompleteConfiguration">
</app-multiple-auto-complete>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textListValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-hint>{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.EXTERNAL-SOURCE-HINT" | translate }}</mat-hint>
</mat-form-field>
</ng-container>
<ng-container *ngIf="!(field.data.multipleSelect)">
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<app-single-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" [configuration]="descriptionService.singleAutocompleteConfiguration" [required]="isRequired">
</app-single-auto-complete>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-hint>{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.EXTERNAL-SOURCE-HINT" | translate }}</mat-hint>
</mat-form-field>
</ng-container>
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<app-multiple-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textListValue')" [configuration]="descriptionService.multipleAutocompleteConfiguration">
</app-multiple-auto-complete>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textListValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textListValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-hint>{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.EXTERNAL-SOURCE-HINT" | translate }}</mat-hint>
</mat-form-field>
</ng-container>
<ng-container *ngIf="!(field.data.multipleSelect)">
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<app-single-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" [configuration]="descriptionService.singleAutocompleteConfiguration">
</app-single-auto-complete>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-hint>{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.EXTERNAL-SOURCE-HINT" | translate }}</mat-hint>
</mat-form-field>
</ng-container>
</div>
</div>
<div *ngSwitchCase="descriptionTemplateFieldTypeEnum.INTERNAL_ENTRIES_DMPS" class="col-12">
@ -87,7 +87,7 @@
<ng-container *ngIf="!(field.data.multipleSelect)">
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<app-single-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" [configuration]="dmpService.singleAutocompleteConfiguration" [required]="isRequired">
<app-single-auto-complete placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" [configuration]="dmpService.singleAutocompleteConfiguration">
</app-single-auto-complete>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textValue').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
@ -105,7 +105,7 @@
<div class="col-12">
<mat-form-field *ngSwitchCase="descriptionTemplateFieldTypeEnum.TEXT_AREA" class="w-100">
<mat-label>{{ field.data.label }}</mat-label>
<textarea matInput class="text-area" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" matTextareaAutosize matAutosizeMinRows="3" matAutosizeMaxRows="15" [required]="isRequired" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}"></textarea>
<textarea matInput class="text-area" [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" matTextareaAutosize matAutosizeMinRows="3" matAutosizeMaxRows="15" placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}"></textarea>
<button mat-icon-button type="button" *ngIf="!propertiesFormGroup?.get(field.id).get('textValue').disabled && propertiesFormGroup?.get(field.id).get('textValue').value" matSuffix aria-label="Clear" (click)="this.propertiesFormGroup?.get(field.id).get('textValue').patchValue('')">
<mat-icon>close</mat-icon>
</button>
@ -116,7 +116,7 @@
<ng-container *ngSwitchCase="descriptionTemplateFieldTypeEnum.RICH_TEXT_AREA">
<div class="col-12">
<rich-text-editor-component [form]="propertiesFormGroup?.get(field.id).get('textValue')" [placeholder]="field.data.label" [required]="isRequired" [wrapperClasses]="'full-width editor ' +
<rich-text-editor-component [form]="propertiesFormGroup?.get(field.id).get('textValue')" [placeholder]="field.data.label" [wrapperClasses]="'full-width editor ' +
((isRequired && propertiesFormGroup?.get(field.id).get('textValue').touched && propertiesFormGroup?.get(field.id).get('textValue').hasError('required')) ? 'required' : '')" [editable]="!propertiesFormGroup?.get(field.id).get('textValue').disabled">
</rich-text-editor-component>
</div>
@ -148,7 +148,7 @@
</div>
</ng-container>
<div *ngSwitchCase="descriptionTemplateFieldTypeEnum.BOOLEAN_DECISION" class="col-12">
<mat-radio-group [formControl]="propertiesFormGroup?.get(field.id).get('booleanValue')" [required]="isRequired">
<mat-radio-group [formControl]="propertiesFormGroup?.get(field.id).get('booleanValue')">
<mat-radio-button class="radio-button-item" [value]="true">{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.ACTIONS.YES" | translate }}</mat-radio-button>
<mat-radio-button class="radio-button-item" [value]="false">{{ "TYPES.DATASET-PROFILE-COMBO-BOX-TYPE.ACTIONS.NO" | translate }}</mat-radio-button>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('booleanValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('booleanValue').getError('backendError').message}}</mat-error>
@ -159,7 +159,7 @@
</div>
<div *ngSwitchCase="descriptionTemplateFieldTypeEnum.RADIO_BOX" class="col-12">
<mat-radio-group [formControl]="propertiesFormGroup?.get(field.id).get('textValue')" [required]="isRequired">
<mat-radio-group [formControl]="propertiesFormGroup?.get(field.id).get('textValue')">
<mat-radio-button *ngFor="let option of field.data.options let index = index" class="radio-button-item" [value]="option.value">{{option.label}}</mat-radio-button>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('textValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('textValue').getError('backendError').message}}</mat-error>
</mat-radio-group>
@ -170,7 +170,7 @@
<mat-form-field *ngSwitchCase="descriptionTemplateFieldTypeEnum.DATE_PICKER" class="col-12">
<mat-label>{{ field.data.label }}</mat-label>
<input matInput placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" class="table-input" [matDatepicker]="date" [required]="isRequired" [formControl]="propertiesFormGroup?.get(field.id).get('dateValue')">
<input matInput placeholder="{{ (field.data.label | translate) + (isRequired? ' *': '') }}" class="table-input" [matDatepicker]="date" [formControl]="propertiesFormGroup?.get(field.id).get('dateValue')">
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('dateValue').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('dateValue').getError('backendError').message}}</mat-error>
@ -185,13 +185,13 @@
<div class="row" *ngIf="datasetIdInitialized">
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<input matInput class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}" [required]="isRequired" [disabled]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').disabled">
<input matInput class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}" [disabled]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').disabled">
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
<mat-form-field class="col-md-12">
<mat-label>{{ field.data.label }}</mat-label>
<mat-select class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('type')" [placeholder]="('TYPES.DATASET-PROFILE-IDENTIFIER.IDENTIFIER-TYPE' | translate) + (isRequired? ' *': '')" [required]="isRequired" [disabled]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('type').disabled">
<mat-select class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('type')" [placeholder]="('TYPES.DATASET-PROFILE-IDENTIFIER.IDENTIFIER-TYPE' | translate) + (isRequired? ' *': '')" [disabled]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('type').disabled">
<mat-option *ngFor="let type of datasetIdTypes" [value]="type.value">
{{ type.name }}
</mat-option>
@ -206,7 +206,7 @@
<div class="row align-items-baseline">
<mat-form-field class="col-md-4">
<mat-label>{{ field.data.label }}</mat-label>
<input matInput class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}" [required]="isRequired">
<input matInput class="col-md-12" [formControl]="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier')" placeholder="{{(field.data.label) + (isRequired? ' *': '')}}">
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').hasError('backendError')">{{propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').getError('backendError').message}}</mat-error>
<mat-error *ngIf="propertiesFormGroup?.get(field.id).get('externalIdentifier')?.get('identifier').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
@ -231,4 +231,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@ -80,11 +80,12 @@ export class FormProgressIndicationComponent extends BaseComponent implements On
countRequiredFieldsByFieldset(ordinal: number, fieldsFormGroup: UntypedFormGroup, filterValid: boolean = false): number {
let fieldsCount: number = 0;
const fieldNames = Object.keys(fieldsFormGroup.controls);
for(let item of fieldNames) {
const fieldSetNames = Object.keys(fieldsFormGroup.controls);
for(let item of fieldSetNames) {
if (!this.checkVisibility || this.visibilityRulesService.isVisible(item, ordinal)) {
const fieldControl = fieldsFormGroup.get(item);
for (let fieldType of this.fieldTypes) {
const fieldNames = Object.keys((fieldControl as UntypedFormGroup).controls);
for (let fieldType of fieldNames) {
const typedControl = fieldControl.get(fieldType);
let controlFilter: boolean = this.controlRequired(typedControl) && this.controlEnabled(typedControl);
if (filterValid) controlFilter = controlFilter && typedControl.valid;

View File

@ -1,6 +1,6 @@
<div class="main-content pl-5 pr-5">
<div class="container-fluid pl-0 pr-0">
<div *ngIf="description">
<div *ngIf="description && userName">
<div class="row">
<div class="col-12 pl-2 mb-3">
<app-navigation-breadcrumb />
@ -197,12 +197,20 @@
</button>
</div>
<div class="col pl-0" style="min-width: 0;">
<p class="authors-label">{{ dmpUser.user?.name }} <span *ngIf="isUserAuthor(dmpUser.user?.id)">({{ 'DESCRIPTION-OVERVIEW.YOU' | translate }})</span></p>
<p class="authors-role">
<span>{{ enumUtils.toDmpUserRoleString(dmpUser.role) }} - </span>
<span *ngIf="!dmpUser.sectionId">{{ 'DESCRIPTION-OVERVIEW.ROLES.ALL-SECTIONS' | translate}}</span>
<span *ngIf="dmpUser.sectionId">{{ getSectionNameById(dmpUser.sectionId) }}</span>
<ng-container *ngIf="!isUserAuthor(dmpUser.user?.id); else you">
<p class="authors-label">{{ dmpUser.user?.name }}</p>
</ng-container>
<ng-template #you>
<p class="authors-label">{{ userName }}
<span>({{ 'DESCRIPTION-OVERVIEW.YOU' | translate }})</span>
</p>
</ng-template>
<p class="authors-role">
<span>{{ enumUtils.toDmpUserRoleString(dmpUser.role) }} - </span>
<span *ngIf="!dmpUser.sectionId">{{ 'DESCRIPTION-OVERVIEW.ROLES.ALL-SECTIONS' | translate}}</span>
<span *ngIf="dmpUser.sectionId">{{ getSectionNameById(dmpUser.sectionId) }}</span>
</p>
</div>
<div class="col-auto" *ngIf="canInviteDmpUsers && description.dmp?.status === dmpStatusEnum.Draft && dmpUser.role != dmpUserRoleEnum.Owner">
<button (click)="removeUserFromDmp(dmpUser)" mat-mini-fab matTooltip="{{ 'DESCRIPTION-OVERVIEW.ACTIONS.REMOVE-AUTHOR' | translate}}" matTooltipPosition="above">

View File

@ -36,11 +36,14 @@ import { DmpInvitationDialogComponent } from '@app/ui/dmp/invitation/dialog/dmp-
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { takeUntil } from 'rxjs/operators';
import { map, takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { DescriptionCopyDialogComponent } from '../description-copy-dialog/description-copy-dialog.component';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { Observable, of } from 'rxjs';
import { UserService } from '@app/core/services/user/user.service';
import { User } from '@app/core/model/user/user';
@Component({
@ -72,6 +75,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
canInviteDmpUsers = false;
authorFocus: string;
userName: string;
constructor(
private route: ActivatedRoute,
@ -94,7 +98,8 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
private lockService: LockService,
private analyticsService: AnalyticsService,
private breadcrumbService: BreadcrumbService,
private httpErrorHandlingService: HttpErrorHandlingService
private httpErrorHandlingService: HttpErrorHandlingService,
private userService: UserService,
) {
super();
}
@ -183,6 +188,15 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
});
}
});
if (this.isAuthenticated()) {
this.userService.getSingle(this.authentication.userId(), [
nameof<User>(x => x.id),
nameof<User>(x => x.name)])
.pipe(map(u => u.name)).subscribe(name => this.userName = name);
} else {
this.userName = '';
}
}
get unauthorizedTootipText(): string {
@ -229,7 +243,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
isUserAuthor(userId: Guid): boolean {
if (this.isAuthenticated()) {
const principalId: Guid = this.authentication.userId();
return userId === principalId;
return this.userName && (userId === principalId);
} else return false;
}

View File

@ -96,9 +96,9 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
getDescriptionTemplateMultipleAutoCompleteConfiguration(sectionId: Guid): MultipleAutoCompleteConfiguration {
return {
initialItems: (excludedItems: any[], data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, excludedItems ? excludedItems : null)).pipe(map(x => x.items)),
filterFn: (searchQuery: string, excludedItems: any[]) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(searchQuery, excludedItems)).pipe(map(x => x.items)),
getSelectedItems: (selectedItems: any[]) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, null, selectedItems)).pipe(map(x => x.items)),
initialItems: (excludedItems: any[], data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], null, excludedItems ? excludedItems : null)).pipe(map(x => x.items)),
filterFn: (searchQuery: string, excludedItems: any[]) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active], searchQuery, excludedItems)).pipe(map(x => x.items)),
getSelectedItems: (selectedItems: any[]) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup([IsActive.Active, IsActive.Inactive], null, null, selectedItems)).pipe(map(x => x.items)),
displayFn: (item: DescriptionTemplate) => item.label,
titleFn: (item: DescriptionTemplate) => item.label,
subtitleFn: (item: DescriptionTemplate) => item.description,
@ -358,7 +358,8 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
setTimeout(x => {
this.step = this.step > 0 ? this.step - 1 : 0;
if (this.isNew) this.step = 0;
else this.step = this.step > 0 ? this.step - 1 : 0;
this.ngOnInit();
});
}

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { FormattingModule } from '@app/core/formatting.module';
import { DmpRoutingModule } from '@app/ui/dmp/dmp.routing';
import { DmpRoutingModule, PublicDmpRoutingModule } from '@app/ui/dmp/dmp.routing';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
@ -17,3 +17,17 @@ import { CommonUiModule } from '@common/ui/common-ui.module';
]
})
export class DmpModule { }
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
FormattingModule,
PublicDmpRoutingModule,
],
declarations: [
],
exports: [
]
})
export class PublicDmpModule { }

View File

@ -1,11 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BreadcrumbService } from '../misc/breadcrumb/breadcrumb.service';
import { AuthGuard } from '@app/core/auth-guard.service';
const routes: Routes = [
{
path: 'overview',
loadChildren: () => import('./overview/dmp-overview.module').then(m => m.DmpOverviewModule),
canActivate:[AuthGuard],
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -16,6 +18,7 @@ const routes: Routes = [
{
path: 'new',
loadChildren: () => import('./dmp-editor-blueprint/dmp-editor.module').then(m => m.DmpEditorModule),
canActivate:[AuthGuard],
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -27,6 +30,7 @@ const routes: Routes = [
{
path: 'edit',
loadChildren: () => import('./dmp-editor-blueprint/dmp-editor.module').then(m => m.DmpEditorModule),
canActivate:[AuthGuard],
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
@ -37,80 +41,32 @@ const routes: Routes = [
},
{
path: '',
canActivate:[AuthGuard],
loadChildren: () => import('./listing/dmp-listing.module').then(m => m.DmpListingModule),
data: {
breadcrumb: true
},
},
];
// {
// path: 'publicEdit/:publicId',
// component: DmpEditorComponent,
// data: {
// breadcrumb: true,
// title: 'GENERAL.TITLES.DMP-PUBLIC-EDIT'
// },
// canDeactivate: [CanDeactivateGuard]
// },
// {
// path: 'publicOverview/:publicId',
// component: DmpOverviewComponent,
// data: {
// breadcrumb: true,
// title: 'GENERAL.TITLES.DMP-OVERVIEW'
// },
// },
// {
// path: 'new/dataset',
// component: DmpEditorComponent,
// canActivate: [AuthGuard],
// data: {
// breadcrumbs: 'new/dataset',
// title: 'GENERAL.TITLES.DATASET-NEW'
// }
// },
// {
// path: 'new/dataset/:dmpId',
// component: DmpEditorComponent,
// canActivate: [AuthGuard],
// data: {
// breadcrumbs: 'new/dataset',
// title: 'GENERAL.TITLES.DATASET-NEW'
// }
// },
// {
// path: 'new_version/:id',
// // component: DmpWizardComponent,
// component: DmpCloneComponent,
// data: {
// clone: false,
// breadcrumb: true,
// title: 'GENERAL.TITLES.DMP-NEW-VERSION'
// },
// },
// {
// path: 'clone/:id',
// component: DmpCloneComponent,
// data: {
// clone: false,
// breadcrumb: true,
// title: 'GENERAL.TITLES.DMP-CLONE'
// },
// },
// {
// path: 'invitation/:id',
// component: InvitationAcceptedComponent,
// data: {
// breadcrumb: true
// },
// }
const publicRoutes: Routes = [
{
path: 'overview',
loadChildren: () => import('./overview/dmp-overview.module').then(m => m.DmpOverviewModule),
data: {
breadcrumb: true,
...BreadcrumbService.generateRouteDataConfiguration({
hideNavigationItem: true
}),
}
},
{
path: '',
loadChildren: () => import('./listing/dmp-listing.module').then(m => m.DmpListingModule),
data: {
breadcrumb: true
},
},
];
@NgModule({
@ -118,3 +74,9 @@ const routes: Routes = [
exports: [RouterModule]
})
export class DmpRoutingModule { }
@NgModule({
imports: [RouterModule.forChild(publicRoutes)],
exports: [RouterModule]
})
export class PublicDmpRoutingModule { }

View File

@ -12,7 +12,7 @@
<app-dmp-user-field-component [form]="formGroup" [validationErrorModel]="editorModel.validationErrorModel" [sections]="selectedBlueprint.definition.sections" [viewOnly]="false" [initializeUsers]="true" [enableSorting]="false"></app-dmp-user-field-component>
</div>
<div class="col mt-2">
<button mat-raised-button *ngIf="hasValue()" (click)="send()" type="button" class="invite-btn">{{'DMP-USER-INVITATION-DIALOG.ACTIONS.INVITE' | translate}}</button>
<button mat-raised-button *ngIf="hasValue()" [disabled]="inProgressSendButton" (click)="send()" type="button" class="invite-btn">{{'DMP-USER-INVITATION-DIALOG.ACTIONS.INVITE' | translate}}</button>
<mat-error *ngIf="formGroup.get('users').hasError('backendError')">{{formGroup.get('users').getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('users').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</div>

View File

@ -1,29 +1,31 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DmpUserRole } from '@app/core/common/enum/dmp-user-role';
import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint';
import { DmpUserPersist } from '@app/core/model/dmp/dmp';
import { DmpService } from '@app/core/services/dmp/dmp.service';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { UserService } from '@app/core/services/user/user.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { FilterService } from '@common/modules/text-filter/filter-service';
import { FormService } from '@common/forms/form-service';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { DmpEditorModel } from '../../dmp-editor-blueprint/dmp-editor.model';
import { takeUntil } from 'rxjs/operators';
import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { HttpErrorResponse } from '@angular/common/http';
import { DmpEditorModel } from '../../dmp-editor-blueprint/dmp-editor.model';
import { DmpEditorService } from '../../dmp-editor-blueprint/dmp-editor.service';
import { ResponseErrorCode } from '@app/core/common/enum/respone-error-code';
@Component({
selector: 'app-invitation-dialog-component',
templateUrl: 'dmp-invitation-dialog.component.html',
styleUrls: ['./dmp-invitation-dialog.component.scss'],
providers: [DmpEditorService]
})
export class DmpInvitationDialogComponent extends BaseComponent implements OnInit {
@ -31,7 +33,8 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni
editorModel: DmpEditorModel;
formGroup: UntypedFormGroup;
dmpUserRoleEnum = DmpUserRole;
selectedBlueprint: DmpBlueprint;
selectedBlueprint: DmpBlueprint;
inProgressSendButton = false;
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(
@ -43,8 +46,8 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni
private uiNotificationService: UiNotificationService,
private httpErrorHandlingService: HttpErrorHandlingService,
private dmpService: DmpService,
private userService: UserService,
private filterService: FilterService,
private formService: FormService,
private dmpEditorService: DmpEditorService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
super();
@ -58,10 +61,14 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni
}
send() {
this.formService.removeAllBackEndErrors(this.formGroup.get("users"));
this.formService.touchAllFormFields(this.formGroup.get("users"));
if (!this.formGroup.get("users").valid) { return; }
this.inProgressSendButton = true;
const userFormData = this.formGroup.get("users").value as DmpUserPersist[];
this.dmpService.inviteUsers(this.dmpId, {users: userFormData})
this.dmpService.inviteUsers(this.dmpId, { users: userFormData })
.pipe(takeUntil(this._destroyed))
.subscribe(
complete => {
@ -84,9 +91,21 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni
this.uiNotificationService.snackBarNotification(this.language.instant('DMP-USER-INVITATION-DIALOG.SUCCESS'), SnackBarNotificationLevel.Success);
}
// onCallbackError(errorResponse: HttpErrorResponse) {
// this.inProgressSendButton = false;
// let errorOverrides = new Map<number, string>();
// errorOverrides.set(-1, this.language.instant('DMP-USER-INVITATION-DIALOG.ERROR'));
// this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error);
// }
onCallbackError(errorResponse: HttpErrorResponse) {
let errorOverrides = new Map<number, string>();
errorOverrides.set(-1, this.language.instant('DMP-USER-INVITATION-DIALOG.ERROR'));
this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error);
this.inProgressSendButton = false;
this.httpErrorHandlingService.handleBackedRequestError(errorResponse);
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
if (error.statusCode === 400) {
this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error);
this.formService.validateAllFormFields(this.formGroup);
}
}
}

View File

@ -1,6 +1,6 @@
<div class="main-content dmp-overview pl-5 pr-5">
<div class="container-fluid pl-0 pr-0">
<div *ngIf="dmp">
<div *ngIf="dmp && userName">
<div class="row">
<div class="col-12 pl-2 mb-3">
<app-navigation-breadcrumb />
@ -258,11 +258,15 @@
</button>
</div>
<div class="col pl-0" style="min-width: 0;">
<!-- <div class="mytext">{{ dmpUser.user?.name }}</div> -->
<p class="authors-label">{{ dmpUser.user?.name }}
<span *ngIf="isUserAuthor(dmpUser.user?.id)">
({{ 'DMP-OVERVIEW.YOU' | translate }})</span>
</p>
<ng-container *ngIf="!isUserAuthor(dmpUser.user?.id); else you">
<p class="authors-label">{{ dmpUser.user?.name }}</p>
</ng-container>
<ng-template #you>
<p class="authors-label"> {{ userName }}
<span >
({{ 'DMP-OVERVIEW.YOU' | translate }})</span>
</p>
</ng-template>
<p class="authors-role">
<span>{{ enumUtils.toDmpUserRoleString(dmpUser.role) }} - </span>
<span *ngIf="!dmpUser.sectionId">{{ 'DMP-OVERVIEW.ROLES.ALL-SECTIONS' | translate}}</span>

View File

@ -41,7 +41,7 @@ import { PopupNotificationDialogComponent } from '@app/library/notification/popu
import { BaseComponent } from '@common/base/base.component';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { takeUntil } from 'rxjs/operators';
import { map, takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { CloneDmpDialogComponent } from '../clone-dialog/dmp-clone-dialog.component';
import { DmpDeleteDialogComponent } from '../dmp-delete-dialog/dmp-delete-dialog.component';
@ -51,6 +51,9 @@ import { DmpInvitationDialogComponent } from '../invitation/dialog/dmp-invitatio
import { NewVersionDmpDialogComponent } from '../new-version-dialog/dmp-new-version-dialog.component';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { User } from '@app/core/model/user/user';
import { UserService } from '@app/core/services/user/user.service';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-dmp-overview',
@ -85,6 +88,7 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
dmpUserRoleEnum = DmpUserRole;
authorFocus: string;
userName: string;
constructor(
private route: ActivatedRoute,
@ -108,6 +112,7 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
private analyticsService: AnalyticsService,
private breadcrumbService: BreadcrumbService,
private httpErrorHandlingService: HttpErrorHandlingService,
private userService: UserService,
) {
super();
}
@ -205,6 +210,13 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
this.depositRepos = repos;
},
error => this.depositRepos = []);
this.userService.getSingle(this.authentication.userId(), [
nameof<User>(x => x.id),
nameof<User>(x => x.name)])
.pipe(map(u => u.name)).subscribe(name => this.userName = name);
} else {
this.userName = '';
}
}
@ -223,7 +235,7 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
isUserAuthor(userId: Guid): boolean {
if (this.isAuthenticated()) {
const principalId: Guid = this.authentication.userId();
return userId === principalId;
return this.userName && userId === principalId;
} else return false;
}

View File

@ -90,14 +90,20 @@ export class NavbarComponent extends BaseComponent implements OnInit {
this.authentication.getAuthenticationStateObservable().subscribe(authenticationState => {
if (authenticationState.loginStatus === LoginStatus.LoggedIn) {
this.loadLogo();
this.loadUser();
}
});
this.loadLogo();
this.loadUser();
}
this.userService.getSingle(this.authentication.userId(), [
nameof<User>(x => x.id),
nameof<User>(x => x.name)
]).subscribe(u => this.userName = u.name); //TODO HANDLE-ERRORS
private loadUser() {
if (this.authentication.currentAccountIsAuthenticated() && this.authentication.userId()) {
this.userService.getSingle(this.authentication.userId(), [
nameof<User>(x => x.id),
nameof<User>(x => x.name)
]).subscribe(u => this.userName = u.name); //TODO HANDLE-ERRORS
}
}
private loadLogo() {

View File

@ -4,7 +4,6 @@
<div *ngIf="showItem(groupMenuItem);">
<hr *ngIf="!firstGroup">
<mat-list-item routerLinkActive="active" [isActiveMatchOptions]="{ paths: 'exact', queryParams: 'ignored' }" *ngFor="let groupMenuRoute of groupMenuItem.routes; let first = first" class="nav-item" [ngClass]="{'mt-4': first && firstGroup}">
<!-- {{ groupMenuRoute |json }} -->
<a class="new-dmp nav-link nav-row" *ngIf="groupMenuRoute.path !== '/contact-support' && groupMenuRoute.path !== '/co-branding' && groupMenuRoute.path !== '/feedback' && groupMenuRoute.path !== '/descriptions'" [routerLink]="[groupMenuRoute.path]" [ngClass]="{'dmp-tour': groupMenuRoute.path == '/plans'}">
<i class="material-symbols-outlined icon">{{ groupMenuRoute.icon }}</i>
<i *ngIf="groupMenuRoute.path == '/plans'" class="material-symbols-outlined icon-mask">person</i>

View File

@ -1,15 +1,13 @@
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { MatButtonToggleChange } from "@angular/material/button-toggle";
import { Router } from "@angular/router";
import { Tenant } from "@app/core/model/tenant/tenant";
import { AuthService } from "@app/core/services/auth/auth.service";
import { PrincipalService } from "@app/core/services/http/principal.service";
import { TenantHandlingService } from "@app/core/services/tenant/tenant-handling.service";
import { BaseComponent } from "@common/base/base.component";
import { BaseHttpParams } from "@common/http/base-http-params";
import { InterceptorType } from "@common/http/interceptors/interceptor-type";
import { KeycloakService } from "keycloak-angular";
import { Observable, from } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { Observable } from "rxjs";
@Component({
selector: 'app-tenant-switch',
@ -20,10 +18,9 @@ export class TenantSwitchComponent extends BaseComponent implements OnInit {
tenants: Observable<Array<Tenant>>;
constructor(
private router: Router,
private keycloakService: KeycloakService,
private principalService: PrincipalService,
private authService: AuthService,
private tenantHandlingService: TenantHandlingService
) {
super();
}
@ -34,7 +31,6 @@ export class TenantSwitchComponent extends BaseComponent implements OnInit {
ngOnInit() {
this.tenants = this.loadUserTenants(); //TODO
//this.tenantChange.emit(this.getCurrentLanguage())
}
loadUserTenants(): Observable<Array<Tenant>> {
@ -48,22 +44,7 @@ export class TenantSwitchComponent extends BaseComponent implements OnInit {
onTenantSelected(selectedTenant: MatButtonToggleChange) {
if (selectedTenant.value === undefined || selectedTenant.value === '') return;
this.formSubmit(selectedTenant.value);
this.loadUser();
}
formSubmit(selectedTenant: string): void {
this.authService.selectedTenant(selectedTenant);
}
loadUser(): void {
this.authService.prepareAuthRequest(from(this.keycloakService.getToken()), {})
.pipe(takeUntil(this._destroyed))
.subscribe(
() => {
this.authService.onAuthenticateSuccessReload();
},
(error) => this.authService.onAuthenticateError(error)
);
this.authService.selectedTenant(selectedTenant.value);
window.location.href = this.tenantHandlingService.getCurrentUrlEnrichedWithTenantCode(selectedTenant.value, true);
}
}

View File

@ -1,5 +1,3 @@
<!-- {{ userCredentials | async | json }} -->
<div class="profile">
<div class="container-fluid">
<div *ngIf="user | async as userProfile; else loading" class="user-profile">

View File

@ -1,4 +1,3 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
@ -18,6 +17,7 @@ import { AnalyticsService } from '@app/core/services/matomo/analytics-service';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service';
import { ReferenceService } from '@app/core/services/reference/reference.service';
import { TenantHandlingService } from '@app/core/services/tenant/tenant-handling.service';
import { UserService } from '@app/core/services/user/user.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration';
@ -28,16 +28,15 @@ import { FormValidationErrorsDialogComponent } from '@common/forms/form-validati
import { BaseHttpParams } from '@common/http/base-http-params';
import { InterceptorType } from '@common/http/interceptors/interceptor-type';
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { KeycloakService } from 'keycloak-angular';
import * as moment from 'moment-timezone';
import { Observable, from, of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { AddAccountDialogComponent } from './add-account/add-account-dialog.component';
import { UserProfileEditorModel } from './user-profile-editor.model';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
@Component({
selector: 'app-user-profile',
@ -81,7 +80,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes
private dialog: MatDialog,
public enumUtils: EnumUtils,
private formBuilder: UntypedFormBuilder,
private keycloakService: KeycloakService,
private tenantHandlingService: TenantHandlingService,
private principalService: PrincipalService,
private formService: FormService,
private referenceService: ReferenceService,
@ -281,7 +280,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes
}, maxWidth: '30em'
});
},
error => this.httpErrorHandlingService.handleBackedRequestError(error));
error => this.httpErrorHandlingService.handleBackedRequestError(error));
}
});
}
@ -309,7 +308,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes
});
}
},
error => this.httpErrorHandlingService.handleBackedRequestError(error)); //TODO how to handle this
error => this.httpErrorHandlingService.handleBackedRequestError(error)); //TODO how to handle this
}
});
}
@ -367,17 +366,9 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes
if (this.tenantFormGroup.valid === false) return;
const selectedTenant = this.tenantFormGroup.get('tenantCode').value;
this.formSubmit(selectedTenant);
this.loadUser();
}
formSubmit(selectedTenant: string): void {
this.authService.selectedTenant(selectedTenant);
}
loadUser(): void {
const returnUrl = '/profile';
this.authService.prepareAuthRequest(from(this.keycloakService.getToken()), {}).pipe(takeUntil(this._destroyed)).subscribe(() => this.authService.onAuthenticateSuccess(returnUrl), (error) => this.authService.onAuthenticateError(error));
this.authService.selectedTenant(selectedTenant.value);
window.location.href = this.tenantHandlingService.getCurrentUrlEnrichedWithTenantCode(selectedTenant.value, true);
}
//Preferences

View File

@ -69,7 +69,8 @@
"DESCRIPTION-TEMPLATE-INACTIVE-USER": "This description template contains users that are not exist",
"DESCRIPTION-TEMPLATE-MISSING-USER-CONTACT-INFO": "This description template contains users that don't have contact info",
"DMP-INACTIVE-USER": "This plan contains users that are not exist",
"DMP-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info"
"DMP-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info",
"DUPLICATE-DMP-USER": "You can't invite authors with same role and plan section more than once"
},
"FORM-VALIDATION-DISPLAY-DIALOG": {
"WARNING": "Warning!",
@ -257,7 +258,7 @@
"POLICY": "Cookies Policy"
},
"EMAIL-CONFIRMATION": {
"EXPIRED-EMAIL": "Mail invitation expired",
"EXPIRED-EMAIL": "Your mail invitation has expired, or you are not logged in with the correct account.",
"EMAIL-FOUND": "Email is already confirmed"
},
"HOME": {
@ -2221,7 +2222,8 @@
"MERGE-ACCOUNT": {
"TITLE": "Merge Your Account",
"MESSAGES": {
"CONFIRMATION": "Are you sure that you want to merge this account?"
"CONFIRMATION": "Are you sure that you want to merge this account?",
"INVALID-TOKEN": "Looks like your mail invitation has expired, or you are not logged in with the correct account. Please try logging in again or have the invitation re-sent."
},
"ACTIONS": {
"CONFIRM": "Confirm"

View File

@ -2,6 +2,7 @@ import { AbstractControl, UntypedFormArray, UntypedFormGroup, ValidatorFn, Valid
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { isNullOrUndefined } from '@app/utilities/enhancers/utils';
import { DmpBlueprintSystemFieldType } from '@app/core/common/enum/dmp-blueprint-system-field-type';
import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service';
export function BackendErrorValidator(errorModel: ValidationErrorModel, propertyName: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
@ -29,6 +30,34 @@ export function CustomErrorValidator(errorModel: ValidationErrorModel, propertyN
};
}
export function RequiredWithVisibilityRulesValidator(visibilityRulesService: VisibilityRulesService, visibilityRulesKey: string) {
return (control: AbstractControl): { [key: string]: any } => {
if (visibilityRulesService.isVisibleMap[visibilityRulesKey] ?? true) {
return Validators.required(control);
}
control.setErrors(null);
return null;
};
}
export function UrlValidator() {
return (control: AbstractControl): { [key: string]: any } => {
const urlRegex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
return Validators.pattern(urlRegex);
};
}
export function MinMaxValidator(min: number, max: number) {
return (control: AbstractControl): { [key: string]: any } => {
return null;
};
}
export function DateValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
if (control.value) {
@ -105,28 +134,28 @@ export function DmpBlueprintSystemFieldRequiredValidator(): ValidatorFn {
let foundTitle = false;
let foundDescription = false;
let foundLanguage = false;
let foundAccess = false;
let foundAccess = false;
const sectionsFormArray = (control as UntypedFormArray);
if (sectionsFormArray.controls != null && sectionsFormArray.controls.length > 0 ){
if (sectionsFormArray.controls != null && sectionsFormArray.controls.length > 0) {
sectionsFormArray.controls.forEach((section, index) => {
const fieldsFormArray = section.get('fields') as UntypedFormArray;
if (fieldsFormArray && fieldsFormArray.length > 0){
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Title)){
const fieldsFormArray = section.get('fields') as UntypedFormArray;
if (fieldsFormArray && fieldsFormArray.length > 0) {
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Title)) {
foundTitle = true;
}
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Description)){
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Description)) {
foundDescription = true;
}
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Language)){
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.Language)) {
foundLanguage = true;
}
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.AccessRights)){
if (fieldsFormArray.controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === DmpBlueprintSystemFieldType.AccessRights)) {
foundAccess = true;
}
}
});
}
}
return foundTitle && foundDescription && foundAccess && foundLanguage ? null : { 'dmpBlueprintSystemFieldRequired': true };
};

View File

@ -43,13 +43,13 @@ export class UnauthorizedResponseInterceptor extends BaseInterceptor {
this.authService.refreshToken().then((isRefreshed) => {
this.accountRefresh$ = null;
if (!isRefreshed) {
this.logoutUser();
this.handleUnauthorized();
return false;
}
return true;
}).catch(x => {
this.logoutUser();
this.handleUnauthorized();
return false;
})
).pipe(filter((x) => x));
@ -67,10 +67,10 @@ export class UnauthorizedResponseInterceptor extends BaseInterceptor {
return next.handle(newRequest);
}
private logoutUser() {
private handleUnauthorized() {
if (!this.isLoginRoute() && !this.isSignupRoute()) {
this.authService.clear();
this.router.navigate(['/unauthorized']);
this.router.navigate(['/unauthorized', { queryParams: { returnUrl: this.router.url } }]);
}
}

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
</head>
<body class="">
{description}

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
</head>
<body class="">
<p>Dear {recipient},</p>

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
</head>
<body class="">
{description}

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<title>OpenCDMP Notification</title>
<style>
/* -------------------------------------
GLOBAL RESETS