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

# Conflicts:
#	dmp-frontend/src/app/core/common/enum/app-role.ts
#	dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts
#	dmp-frontend/src/app/ui/admin/user/listing/role-editor/user-role-editor.component.html
#	dmp-frontend/src/app/ui/admin/user/listing/role-editor/user-role-editor.component.scss
#	dmp-frontend/src/assets/i18n/en.json
This commit is contained in:
Efstratios Giannopoulos 2024-04-16 11:12:49 +03:00
commit 48352f0461
91 changed files with 1111 additions and 3172 deletions

View File

@ -4,10 +4,7 @@ import eu.eudat.authorization.AuthorizationFlags;
import eu.eudat.authorization.Permission;
import eu.eudat.commons.enums.*;
import eu.eudat.commons.scope.user.UserScope;
import eu.eudat.data.DmpDescriptionTemplateEntity;
import eu.eudat.data.DmpEntity;
import eu.eudat.data.DmpReferenceEntity;
import eu.eudat.data.DmpUserEntity;
import eu.eudat.data.*;
import eu.eudat.model.Dmp;
import eu.eudat.model.PublicDmp;
import eu.eudat.query.utils.QueryUtilsService;
@ -364,6 +361,11 @@ public class DmpQuery extends QueryBase<DmpEntity> {
predicates.add(queryContext.CriteriaBuilder.in(queryContext.Root.get(DmpEntity._id)).value(subQuery.Query));
}
if (this.entityDoiQuery != null) {
QueryContext<EntityDoiEntity, UUID> subQuery = this.applySubQuery(this.entityDoiQuery, queryContext, UUID.class, entityDoiEntityRoot -> entityDoiEntityRoot.get(EntityDoiEntity._entityId));
predicates.add(queryContext.CriteriaBuilder.in(queryContext.Root.get(DmpEntity._id)).value(subQuery.Query));
}
if (!predicates.isEmpty()) {
Predicate[] predicatesArray = predicates.toArray(new Predicate[0]);
return queryContext.CriteriaBuilder.and(predicatesArray);

View File

@ -5,13 +5,24 @@ import eu.eudat.authorization.Permission;
import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver;
import eu.eudat.commonmodels.models.FileEnvelopeModel;
import eu.eudat.commonmodels.models.dmp.DmpModel;
import eu.eudat.commons.JsonHandlingService;
import eu.eudat.commons.enums.ContactInfoType;
import eu.eudat.commons.enums.IsActive;
import eu.eudat.commons.enums.StorageType;
import eu.eudat.commons.notification.NotificationProperties;
import eu.eudat.commons.scope.user.UserScope;
import eu.eudat.commons.types.notification.*;
import eu.eudat.convention.ConventionService;
import eu.eudat.data.DmpEntity;
import eu.eudat.data.DmpUserEntity;
import eu.eudat.data.UserEntity;
import eu.eudat.depositinterface.repository.DepositClient;
import eu.eudat.depositinterface.repository.DepositConfiguration;
import eu.eudat.integrationevent.outbox.notification.NotifyIntegrationEvent;
import eu.eudat.integrationevent.outbox.notification.NotifyIntegrationEventHandler;
import eu.eudat.model.EntityDoi;
import eu.eudat.model.StorageFile;
import eu.eudat.model.UserContactInfo;
import eu.eudat.model.builder.commonmodels.DepositConfigurationBuilder;
import eu.eudat.model.builder.commonmodels.dmp.DmpCommonModelBuilder;
import eu.eudat.model.persist.StorageFilePersist;
@ -19,6 +30,9 @@ import eu.eudat.model.persist.deposit.DepositAuthenticateRequest;
import eu.eudat.model.persist.deposit.DepositRequest;
import eu.eudat.model.persist.EntityDoiPersist;
import eu.eudat.query.DmpQuery;
import eu.eudat.query.DmpUserQuery;
import eu.eudat.query.UserContactInfoQuery;
import eu.eudat.query.UserQuery;
import eu.eudat.service.entitydoi.EntityDoiService;
import eu.eudat.service.storage.StorageFileProperties;
import eu.eudat.service.storage.StorageFileService;
@ -28,6 +42,7 @@ import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.commons.web.oidc.filter.webflux.TokenExchangeFilterFunction;
import gr.cite.commons.web.oidc.filter.webflux.TokenExchangeModel;
import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.query.Ordering;
import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyNotFoundException;
import gr.cite.tools.fieldset.BaseFieldSet;
@ -42,11 +57,13 @@ import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import javax.management.InvalidApplicationException;
import java.io.IOException;
import java.net.URI;
import java.net.URLConnection;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class DepositServiceImpl implements DepositService {
@ -67,6 +84,10 @@ public class DepositServiceImpl implements DepositService {
private final ValidatorFactory validatorFactory;
private final StorageFileProperties storageFileProperties;
private final AuthorizationContentResolver authorizationContentResolver;
private final ConventionService conventionService;
private final JsonHandlingService jsonHandlingService;
private final NotificationProperties notificationProperties;
private final NotifyIntegrationEventHandler eventHandler;
@Autowired
public DepositServiceImpl(DepositProperties depositProperties,
TokenExchangeCacheService tokenExchangeCacheService,
@ -74,7 +95,7 @@ public class DepositServiceImpl implements DepositService {
EntityDoiService doiService,
QueryFactory queryFactory,
MessageSource messageSource,
BuilderFactory builderFactory, DepositConfigurationCacheService depositConfigurationCacheService, FileTransformerService fileTransformerService, StorageFileService storageFileService, UserScope userScope, ValidatorFactory validatorFactory, StorageFileProperties storageFileProperties, AuthorizationContentResolver authorizationContentResolver) {
BuilderFactory builderFactory, DepositConfigurationCacheService depositConfigurationCacheService, FileTransformerService fileTransformerService, StorageFileService storageFileService, UserScope userScope, ValidatorFactory validatorFactory, StorageFileProperties storageFileProperties, AuthorizationContentResolver authorizationContentResolver, ConventionService conventionService, JsonHandlingService jsonHandlingService, NotificationProperties notificationProperties, NotifyIntegrationEventHandler eventHandler) {
this.depositProperties = depositProperties;
this.tokenExchangeCacheService = tokenExchangeCacheService;
this.authorizationService = authorizationService;
@ -89,6 +110,10 @@ public class DepositServiceImpl implements DepositService {
this.validatorFactory = validatorFactory;
this.storageFileProperties = storageFileProperties;
this.authorizationContentResolver = authorizationContentResolver;
this.conventionService = conventionService;
this.jsonHandlingService = jsonHandlingService;
this.notificationProperties = notificationProperties;
this.eventHandler = eventHandler;
this.clients = new HashMap<>();
}
@ -181,9 +206,48 @@ public class DepositServiceImpl implements DepositService {
doiPersist.setRepositoryId(dmpDepositModel.getRepositoryId());
doiPersist.setDoi(doi);
doiPersist.setEntityId(dmpEntity.getId());
this.sendNotification(dmpEntity);
return doiService.persist(doiPersist, dmpDepositModel.getProject());
}
private void sendNotification(DmpEntity dmpEntity) throws InvalidApplicationException {
List<DmpUserEntity> dmpUsers = this.queryFactory.query(DmpUserQuery.class).ids(dmpEntity.getId()).isActives(IsActive.Active).collect();
if (this.conventionService.isListNullOrEmpty(dmpUsers)){
throw new MyNotFoundException("Dmp does not have Users");
}
List<UserEntity> users = this.queryFactory.query(UserQuery.class).ids(dmpUsers.stream().map(x -> x.getUserId()).collect(Collectors.toList())).isActive(IsActive.Active).collect();
for (UserEntity user: users) {
if (!user.getId().equals(this.userScope.getUserIdSafe()) && !this.conventionService.isListNullOrEmpty(dmpUsers.stream().filter(x -> x.getUserId().equals(user.getId())).collect(Collectors.toList()))){
this.createDmpDepositNotificationEvent(dmpEntity, user);
}
}
}
private void createDmpDepositNotificationEvent(DmpEntity dmp, UserEntity user) throws InvalidApplicationException {
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(user.getId());
UserContactInfoQuery query = this.queryFactory.query(UserContactInfoQuery.class).userIds(user.getId());
query.setOrder(new Ordering().addAscending(UserContactInfo._ordinal));
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, query.first().getValue()));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setNotificationType(notificationProperties.getDmpDepositType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();
fieldInfoList.add(new FieldInfo("{recipient}", DataType.String, user.getName()));
fieldInfoList.add(new FieldInfo("{reasonName}", DataType.String, this.queryFactory.query(UserQuery.class).ids(this.userScope.getUserId()).first().getName()));
fieldInfoList.add(new FieldInfo("{name}", DataType.String, dmp.getLabel()));
fieldInfoList.add(new FieldInfo("{id}", DataType.String, dmp.getId().toString()));
data.setFields(fieldInfoList);
event.setData(jsonHandlingService.toJsonSafe(data));
eventHandler.handle(event);
}
private String addFileToSharedStorage(eu.eudat.model.file.FileEnvelope file) throws IOException {
StorageFilePersist storageFilePersist = new StorageFilePersist();
storageFilePersist.setName(FilenameUtils.removeExtension(file.getFilename()));

View File

@ -302,8 +302,7 @@ public class DescriptionServiceImpl implements DescriptionService {
if (!dmpUser.getUserId().equals(this.userScope.getUserIdSafe())){
UserEntity user = this.queryFactory.query(UserQuery.class).ids(dmpUser.getUserId()).first();
if (user != null){
this.createDescriptionNotificationEvent(description, user, NotificationContactType.EMAIL);
this.createDescriptionNotificationEvent(description, user, NotificationContactType.IN_APP);
this.createDescriptionNotificationEvent(description, user);
}
}
}
@ -353,9 +352,9 @@ public class DescriptionServiceImpl implements DescriptionService {
return cleanData;
}
private void createDescriptionNotificationEvent(DescriptionEntity description, UserEntity user, NotificationContactType type) throws InvalidApplicationException {
private void createDescriptionNotificationEvent(DescriptionEntity description, UserEntity user) throws InvalidApplicationException {
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(this.userScope.getUserId());
event.setUserId(user.getId());
UserContactInfoQuery query = this.queryFactory.query(UserContactInfoQuery.class).userIds(user.getId());
query.setOrder(new Ordering().addAscending(UserContactInfo._ordinal));
@ -364,7 +363,6 @@ public class DescriptionServiceImpl implements DescriptionService {
contactPairs.add(new ContactPair(ContactInfoType.Email, query.first().getValue()));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(type);
event = this.applyNotificationType(description.getStatus(), event);
NotificationFieldData data = new NotificationFieldData();
@ -421,6 +419,7 @@ public class DescriptionServiceImpl implements DescriptionService {
this.eventBroker.emit(new DescriptionTouchedEvent(data.getId()));
this.annotationEntityTouchedIntegrationEventHandler.handleDescription(data.getId());
if (data.getStatus().equals(DescriptionStatus.Finalized)) this.sendNotification(data);
}
return this.builderFactory.builder(DescriptionBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Description._id), data);
}

View File

@ -245,8 +245,7 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
data.setRole(user.getRole());
this.entityManager.persist(data);
if (!this.userScope.getUserId().equals(user.getUserId())) {
this.sendDescriptionTemplateInvitationEvent(data, NotificationContactType.EMAIL);
this.sendDescriptionTemplateInvitationEvent(data, NotificationContactType.IN_APP);
this.sendDescriptionTemplateInvitationEvent(data);
}
}
updatedCreatedIds.add(data.getUserId());
@ -256,13 +255,18 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
this.deleterFactory.deleter(UserDescriptionTemplateDeleter.class).delete(toDelete);
}
private void sendDescriptionTemplateInvitationEvent(UserDescriptionTemplateEntity userDescriptionTemplate, NotificationContactType type) throws InvalidApplicationException {
private void sendDescriptionTemplateInvitationEvent(UserDescriptionTemplateEntity userDescriptionTemplate) throws InvalidApplicationException {
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(userScope.getUserIdSafe());
UserEntity user = this.entityManager.find(UserEntity.class, userDescriptionTemplate.getUserId());
if (user == null){
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userDescriptionTemplate.getUserId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale()));
}
DescriptionTemplateEntity descriptionTemplate = this.queryFactory.query(DescriptionTemplateQuery.class).isActive(IsActive.Active).ids(userDescriptionTemplate.getDescriptionTemplateId()).first();
if (descriptionTemplate == null){
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userDescriptionTemplate.getDescriptionTemplateId(), DescriptionTemplate.class.getSimpleName()}, LocaleContextHolder.getLocale()));
}
event.setUserId(user.getId());
UserContactInfoQuery query = this.queryFactory.query(UserContactInfoQuery.class).userIds(user.getId());
query.setOrder(new Ordering().addAscending(UserContactInfo._ordinal));
@ -270,7 +274,6 @@ public class DescriptionTemplateServiceImpl implements DescriptionTemplateServic
contactPairs.add(new ContactPair(ContactInfoType.Email, query.first().getValue()));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(type);
event.setNotificationType(notificationProperties.getDescriptionTemplateInvitationType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();

View File

@ -258,16 +258,15 @@ public class DmpServiceImpl implements DmpService {
if (!dmpUser.getUserId().equals(this.userScope.getUserIdSafe())){
UserEntity user = this.queryFactory.query(UserQuery.class).ids(dmpUser.getUserId()).first();
if (user != null){
this.createDmpNotificationEvent(dmp, user, NotificationContactType.EMAIL);
this.createDmpNotificationEvent(dmp, user, NotificationContactType.IN_APP);
this.createDmpNotificationEvent(dmp, user);
}
}
}
}
private void createDmpNotificationEvent(DmpEntity dmp, UserEntity user, NotificationContactType type) throws InvalidApplicationException {
private void createDmpNotificationEvent(DmpEntity dmp, UserEntity user) throws InvalidApplicationException {
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(this.userScope.getUserId());
event.setUserId(user.getId());
UserContactInfoQuery query = this.queryFactory.query(UserContactInfoQuery.class).userIds(user.getId());
query.setOrder(new Ordering().addAscending(UserContactInfo._ordinal));
@ -275,7 +274,6 @@ public class DmpServiceImpl implements DmpService {
contactPairs.add(new ContactPair(ContactInfoType.Email, query.first().getValue()));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(type);
this.applyNotificationType(dmp.getStatus(), event);
NotificationFieldData data = new NotificationFieldData();
@ -1186,6 +1184,15 @@ public class DmpServiceImpl implements DmpService {
throw new InvalidApplicationException("Dmp does not exist!");
}
List<DmpUserEntity> existingUsers = this.queryFactory.query(DmpUserQuery.class)
.dmpIds(dmp.getId())
.isActives(IsActive.Active)
.collect();
if (this.conventionService.isListNullOrEmpty(existingUsers)){
throw new InvalidApplicationException("Dmp does not have users!");
}
List<DmpUserPersist> usersToAssign = new ArrayList<>();
for (DmpUserPersist user :users) {
UUID userId = null;
@ -1200,7 +1207,7 @@ public class DmpServiceImpl implements DmpService {
if (userId != null){
user.setUser(userId);
usersToAssign.add(user);
if (this.userScope.getUserId() != userId){
if (this.userScope.getUserId() != userId && !existingUsers.stream().map(x -> x.getUserId()).collect(Collectors.toList()).contains(userId)){
this.sendDmpInvitationExistingUser(user.getUser(), dmp, user.getRole());
}
}else if (user.getEmail() != null) {
@ -1215,20 +1222,18 @@ public class DmpServiceImpl implements DmpService {
private void sendDmpInvitationExistingUser(UUID userId, DmpEntity dmp, DmpUserRole role) throws InvalidApplicationException {
UserEntity recipient = this.queryFactory.query(UserQuery.class).ids(userId).isActive(IsActive.Active).first();
String email = this.queryFactory.query(UserContactInfoQuery.class).userIds(recipient.getId()).first().getValue();
this.createDmpInvitationExistingUserEvent(recipient, dmp, role, email, NotificationContactType.EMAIL);
this.createDmpInvitationExistingUserEvent(recipient, dmp, role, email, NotificationContactType.IN_APP);
this.createDmpInvitationExistingUserEvent(recipient, dmp, role, email);
}
private void createDmpInvitationExistingUserEvent(UserEntity recipient, DmpEntity dmp, DmpUserRole role, String email, NotificationContactType type) throws InvalidApplicationException {
private void createDmpInvitationExistingUserEvent(UserEntity recipient, DmpEntity dmp, DmpUserRole role, String email) throws InvalidApplicationException {
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(this.userScope.getUserIdSafe());
event.setUserId(recipient.getId());
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(type);
event.setNotificationType(notificationProperties.getDmpInvitationExistingUserType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();
@ -1246,7 +1251,6 @@ public class DmpServiceImpl implements DmpService {
String token = this.createActionConfirmation(email, dmp, role);
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
event.setUserId(this.userScope.getUserIdSafe());
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));

View File

@ -117,11 +117,17 @@ public class ExternalFetcherServiceImpl implements ExternalFetcherService {
Map<String, Object> rawData = new HashMap<>();
for (Object object: item.getOptions()) {
StaticOptionEntity staticOption = (StaticOptionEntity) object;
if (!this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike()) && !externalReferenceCriteria.getLike().toUpperCase().contains(staticOption.getValue())) continue;
if (this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike())){
rawData.put(staticOption.getCode(), staticOption.getValue());
result.put(staticOption.getCode(), staticOption.getValue());
result.put(ReferenceEntity.KnownFields.SourceLabel, staticSource.getLabel());
result.put(ReferenceEntity.KnownFields.Key, staticSource.getKey());
}else if (!this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike()) && externalReferenceCriteria.getLike().toUpperCase().contains(staticOption.getValue().toUpperCase())){
rawData.put(staticOption.getCode(), staticOption.getValue());
result.put(staticOption.getCode(), staticOption.getValue());
result.put(ReferenceEntity.KnownFields.SourceLabel, staticSource.getLabel());
result.put(ReferenceEntity.KnownFields.Key, staticSource.getKey());
}
}
if (!rawData.isEmpty()) externalDataResult.getRawData().add(rawData);

View File

@ -66,15 +66,15 @@ public class MetricsServiceImpl implements MetricsService {
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculatePublishedDmps(true), MetricLabels.PUBLISHED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculateDoiedDmps(true), MetricLabels.DOIED);
this.setGaugeValue(gauges, MetricNames.DMP, calculateDraftDmpsWithGrant(false), MetricLabels.DRAFT);
this.setGaugeValue(gauges, MetricNames.DMP, calculateFinalizedDmpsWithGrant(false), MetricLabels.FINALIZED);
this.setGaugeValue(gauges, MetricNames.DMP, calculatePublishedDmpsWithGrant(false), MetricLabels.PUBLISHED);
this.setGaugeValue(gauges, MetricNames.DMP, calculateDoiedDmpsWithGrant(false), MetricLabels.DOIED);
this.setGaugeValue(gauges, MetricNames.DMP_WITH_GRANT, calculateDraftDmpsWithGrant(false), MetricLabels.DRAFT);
this.setGaugeValue(gauges, MetricNames.DMP_WITH_GRANT, calculateFinalizedDmpsWithGrant(false), MetricLabels.FINALIZED);
this.setGaugeValue(gauges, MetricNames.DMP_WITH_GRANT, calculatePublishedDmpsWithGrant(false), MetricLabels.PUBLISHED);
this.setGaugeValue(gauges, MetricNames.DMP_WITH_GRANT, calculateDoiedDmpsWithGrant(false), MetricLabels.DOIED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculateDraftDmpsWithGrant(true), MetricLabels.DRAFT);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculateFinalizedDmpsWithGrant(true), MetricLabels.FINALIZED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculatePublishedDmpsWithGrant(true), MetricLabels.PUBLISHED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP, calculateDoiedDmpsWithGrant(true), MetricLabels.DOIED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP_WITH_GRANT, calculateDraftDmpsWithGrant(true), MetricLabels.DRAFT);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP_WITH_GRANT, calculateFinalizedDmpsWithGrant(true), MetricLabels.FINALIZED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP_WITH_GRANT, calculatePublishedDmpsWithGrant(true), MetricLabels.PUBLISHED);
this.setGaugeValue(gauges, MetricNames.NEXUS_PREFIX + MetricNames.DMP_WITH_GRANT, calculateDoiedDmpsWithGrant(true), MetricLabels.DOIED);
this.setGaugeValue(gauges, MetricNames.FUNDERS, calculateAllFunders(false), null);
this.setGaugeValue(gauges, MetricNames.GRANTS, calculateAllGrants(false), null);
@ -181,6 +181,7 @@ public class MetricsServiceImpl implements MetricsService {
dmpQuery.after(_config.getNexusDate());
EntityDoiQuery entityDoiQuery = this.queryFactory.query(EntityDoiQuery.class).types(EntityType.DMP).isActive(IsActive.Active);
dmpQuery.entityDoiSubQuery(entityDoiQuery);
dmpQuery.setDistinct(true);
return dmpQuery.count();
}

View File

@ -423,7 +423,6 @@ public class UserServiceImpl implements UserService {
String token = this.createMergeAccountConfirmation(model.getEmail());
createMergeNotificationEvent(token, user, model.getEmail(), NotificationContactType.EMAIL);
createMergeNotificationEvent(token, user, model.getEmail(), NotificationContactType.IN_APP);
}
private void createMergeNotificationEvent(String token, UserEntity user, String email, NotificationContactType type) throws InvalidApplicationException {
@ -453,7 +452,6 @@ public class UserServiceImpl implements UserService {
String token = this.createRemoveConfirmation(data.getId());
this.createRemoveCredentialNotificationEvent(token, data.getUserId(), NotificationContactType.EMAIL);
this.createRemoveCredentialNotificationEvent(token, data.getUserId(), NotificationContactType.IN_APP);
}
private void createRemoveCredentialNotificationEvent(String token, UUID userId, NotificationContactType type) throws InvalidApplicationException {

View File

@ -1,5 +1,9 @@
metrics:
task:
enable: true
intervalSeconds: 600
nexusDate: "2021-01-01T00:00:00.00Z"
usersLoginClient: ${IDP_APIKEY_CLIENT_ID_UUID:}
referenceTypes:
funderIds: ["538928bb-c7c6-452e-b66d-08e539f5f082"]
grantIds: ["5b9c284f-f041-4995-96cc-fad7ad13289c"]

View File

@ -1,9 +1,9 @@
metrics:
task:
enable: true
intervalSeconds: 600
nexusDate: "2021-01-01T00:00:00.00Z"
usersLoginClient: ${IDP_APIKEY_CLIENT_ID_UUID:}
enable: false
intervalSeconds: null
nexusDate: null
usersLoginClient: null
referenceTypes:
funderIds: []
grantIds: []

View File

@ -4,6 +4,5 @@ export enum AppRole {
TenantAdmin = "TenantAdmin",
TenantUser = "TenantUser",
TenantManager = "TenantManager",
TenantDescriptionTemplateEditor = "TenantDescriptionTemplateEditor",
TenantDescriptionTemplateEditor = "TenantDescriptionTemplateEditor"
}

View File

@ -107,7 +107,7 @@ ngx-guided-tour {
&.tour-top-right {
.tour-arrow::before {
transform: translateX(-100%);
left: calc(100% - 5px);
left: calc(100% - 15px);
}
}

View File

@ -7,32 +7,47 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div>
<mat-label>{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.STATUS' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.STATUS' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.statuses">
<mat-option *ngFor="let descriptionTemplateStatus of descriptionTemplateStatusEnumValues" [value]="descriptionTemplateStatus">{{enumUtils.toDescriptionTemplateTypeStatusString(descriptionTemplateStatus)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'DESCRIPTION-TEMPLATE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -7,32 +7,47 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div>
<mat-label>{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.STATUS' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.STATUS' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.statuses">
<mat-option *ngFor="let descriptionTemplateTypeStatus of descriptionTemplateTypeStatusEnumValues" [value]="descriptionTemplateTypeStatus">{{enumUtils.toDescriptionTemplateTypeStatusString(descriptionTemplateTypeStatus)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'DESCRIPTION-TEMPLATE-TYPE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -39,7 +39,7 @@
</div>
</div>
<div class="row justify-content-end align-items-center mt-4 gap-1-rem">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'DMP-BLUEPRINT-LISTING.FILTER.CANCEL' | translate}}

View File

@ -7,39 +7,47 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'LOCK-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="mt-4">
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'LOCK-LISTING.FILTER.USERS' | translate}}</mat-label>
<app-multiple-auto-complete [(ngModel)]="internalFilters.userIds" [hidePlaceholder]="true" [separatorKeysCodes]="separatorKeysCodes" [configuration]="userService.multipleAutocompleteConfiguration">
</app-multiple-auto-complete>
</mat-form-field>
</div>
<div>
<mat-form-field class="col-12">
<mat-label>{{'LOCK-LISTING.FILTER.TARGET-TYPE' | translate}}
</div>
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'LOCK-LISTING.FILTER.TARGET-TYPE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.targetTypes">
<mat-option *ngFor="let targetType of lockTargetTypeEnumValues" [value]="targetType">{{enumUtils.toLockTargetTypeString(targetType)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-between mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-between mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'LOCK-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'LOCK-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -30,7 +30,7 @@
</mat-card-header>
<mat-card-content>
<div class="row mt-3">
<div class="col-4" *ngIf="isNew">
<div class="col-6" *ngIf="isNew">
<mat-form-field class="w-100">
<mat-label>{{'LANGUAGE-EDITOR.FIELDS.CODE' | translate}}</mat-label>
<input matInput type="text" name="code" [formControl]="formGroup.get('code')" required>
@ -38,7 +38,7 @@
<mat-error *ngIf="formGroup.get('code').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-4" *ngIf="!isNew">
<div class="col-6" *ngIf="!isNew">
<mat-form-field class="w-100">
<mat-label>{{'LANGUAGE-EDITOR.FIELDS.CODE' | translate}}</mat-label>
<mat-select (selectionChange)="selectedCodeChanged($event.value)" name= "code" [formControl]="formGroup.get('code')">

View File

@ -7,24 +7,36 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'LANGUAGE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'LANGUAGE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'LANGUAGE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'LANGUAGE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -18,4 +18,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -25,7 +25,7 @@
</div>
<form *ngIf="formGroup" (ngSubmit)="formSubmit()">
<mat-card appearance="outlined">
<mat-card appearance="outlined" class="mb-1">
<mat-card-header>
<mat-card-title *ngIf="isNew">{{'PREFILLING-SOURCE-EDITOR.NEW' | translate}}</mat-card-title>
</mat-card-header>
@ -42,7 +42,7 @@
</div>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card appearance="outlined" class="mb-1">
<mat-card-header>
<mat-card-title>{{'PREFILLING-SOURCE-EDITOR.FIELDS.FIXED-VALUE-FIELDS' | translate}}</mat-card-title>
</mat-card-header>
@ -82,7 +82,7 @@
</div>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card appearance="outlined" class="mb-1">
<mat-card-header>
<mat-card-title>{{'PREFILLING-SOURCE-EDITOR.FIELDS.FIELDS' | translate}}</mat-card-title>
</mat-card-header>
@ -122,7 +122,7 @@
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card appearance="outlined" class="mb-1">
<mat-card-header>
<mat-card-title>{{'PREFILLING-SOURCE-EDITOR.FIELDS.SOURCE-CONFIGURATION' | translate}}</mat-card-title>
</mat-card-header>
@ -138,7 +138,7 @@
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" *ngIf="formGroup.get('definition').get('getEnabled').value == true">
<mat-card appearance="outlined" *ngIf="formGroup.get('definition').get('getEnabled').value == true" class="mb-1">
<mat-card-header>
<mat-card-title>{{'PREFILLING-SOURCE-EDITOR.FIELDS.GET-SOURCE-CONFIGURATION' | translate}}</mat-card-title>
</mat-card-header>

View File

@ -7,24 +7,36 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'PREFILLING-SOURCE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'PREFILLING-SOURCE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'PREFILLING-SOURCE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'PREFILLING-SOURCE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -2,7 +2,7 @@
<div class="row reference-type-editor">
<div class="col-md-10 offset-md-1 colums-gapped">
<div class="row justify-content-between align-items-center">
<div class="col">
<div class="col-md col-12">
<h3 *ngIf="isNew">{{'REFERENCE-TYPE-EDITOR.NEW' | translate}}</h3>
<app-navigation-breadcrumb />
</div>

View File

@ -7,24 +7,36 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'REFERENCE-TYPE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'REFERENCE-TYPE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'REFERENCE-TYPE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'REFERENCE-TYPE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -2,7 +2,7 @@
<div class="row reference-editor">
<div class="col-md-10 offset-md-1 colums-gapped">
<div class="row align-items-center mt-4 mb-4" *ngIf="formGroup">
<div class="col-auto">
<div class="col-md col-12">
<h3 *ngIf="isNew && !isClone">{{'REFERENCE-EDITOR.NEW' | translate}}</h3>
<app-navigation-breadcrumb />
</div>

View File

@ -7,39 +7,58 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'REFERENCE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'REFERENCE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
<div>
<mat-label>{{'REFERENCE-LISTING.FILTER.TYPE' | translate}}
<app-single-auto-complete [(ngModel)]="internalFilters.typeIds" placeholder="{{'REFERENCE-LISTING.FILTER.TYPE' | translate}}" [configuration]="referenceTypeService.singleAutocompleteConfiguration">
</app-single-auto-complete>
</mat-label>
</section>
</div>
</div>
<div>
<mat-label>{{'REFERENCE-LISTING.FILTER.SOURCE-TYPE' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'REFERENCE-LISTING.FILTER.TYPE' | translate}}</mat-label>
<app-single-auto-complete [(ngModel)]="internalFilters.typeIds" placeholder="{{'REFERENCE-LISTING.FILTER.TYPE' | translate}}" [configuration]="referenceTypeService.singleAutocompleteConfiguration">
</app-single-auto-complete>
</mat-form-field>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'REFERENCE-LISTING.FILTER.SOURCE-TYPE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.sourceTypes">
<mat-option *ngFor="let referenceSourceType of referenceSourceTypeEnumValues" [value]="referenceSourceType">{{enumUtils.toReferenceSourceTypeString(referenceSourceType)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'REFERENCE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'REFERENCE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -18,4 +18,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -2,7 +2,7 @@
<div class="row">
<div class="col-md-10 offset-md-1 tenant-editor">
<div class="row align-items-center mb-4 mt-4" *ngIf="formGroup">
<div class="col-auto">
<div class="col-md col-12">
<h3 *ngIf="isNew && !isClone">{{'TENANT-EDITOR.NEW' | translate}}</h3>
<app-navigation-breadcrumb />
</div>

View File

@ -7,24 +7,36 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'TENANT-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'TENANT-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'TENANT-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'TENANT-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -7,32 +7,47 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'USER-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'USER-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div>
<mat-label>{{'USER-LISTING.FILTER.ROLES' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'USER-LISTING.FILTER.ROLES' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.userRoleSubQuery.roles">
<mat-option *ngFor="let appRole of appRoleEnumValues" [value]="appRole">{{enumUtils.toAppRoleString(appRole)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'USER-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'USER-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -22,4 +22,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -1,17 +1,18 @@
<form class="row user-role-editor" *ngIf="formGroup" [formGroup]="formGroup" (ngSubmit)="formSubmit()">
<div class="container-fluid">
<div class="row align-items-center">
<div *ngIf="!this.nowEditing"class="roles col">
<ng-container *ngFor="let role of this.formGroup.get('roles').value">
<div>
<span class="user-role" [ngClass]="{'user': role == appRole.User, 'tenant-manager': role == appRole.TenantManager, 'admin': role == appRole.Admin, 'tenant-admin': role == appRole.TenantAdmin, 'tenant-user': role == appRole.TenantUser,'tenant-description-template-editor': role == appRole.TenantDescriptionTemplateEditor}">
<div *ngIf="!this.nowEditing"class="roles col-8">
<div *ngFor="let role of this.formGroup.get('roles').value" class="row">
<div class="col-auto p-0">
<span class="user-role" [ngClass]="{'user': role == appRole.User, 'tenant-manager': role == appRole.TenantManager, 'admin': role == appRole.Admin, 'tenant-description-template-editor': role == appRole.TenantDescriptionTemplateEditor, 'tenant-admin': role == appRole.TenantAdmin, 'tenant-user': role == appRole.TenantUser}">
{{enumUtils.toAppRoleString(role)}}
</span>
</div>
</ng-container>
</div>
<div class="col-8">
<mat-form-field *ngIf="this.nowEditing" class="w-100">
<mat-select formControlName="roles" multiple required>
</div>
<div *ngIf="this.nowEditing" class="pl-0 col-8">
<mat-form-field class="w-100">
<mat-select formControlName="roles" [panelWidth]="auto" multiple required>
<ng-container *ngFor="let role of appRoleEnumValues">
<mat-option [value]="role">{{enumUtils.toAppRoleString(role)}}</mat-option>
</ng-container>
@ -20,13 +21,19 @@
</mat-form-field>
</div>
<button *ngIf="!this.nowEditing" class="col" mat-icon-button color="primary" type="button" (click)="editItem()">
<div *ngIf="!this.nowEditing" class="col-2 p-0">
<button mat-icon-button color="primary" type="button" (click)="editItem()">
<mat-icon class="mat-24" matTooltip="{{'USER-LISTING.ACTIONS.EDIT' | translate}}">edit</mat-icon>
<span class="row-action"></span>
</button>
<button *ngIf="this.nowEditing" class="col-auto save-button" mat-icon-button color="primary" type="submit">
</div>
<div *ngIf="this.nowEditing" class="col-2 p-0">
<button class="save-button" mat-icon-button color="primary" type="submit">
<mat-icon class="mat-24" matTooltip="{{'USER-LISTING.ACTIONS.SAVE' | translate}}">save</mat-icon>
</button>
</div>
</div>
</div>
</form>

View File

@ -85,6 +85,24 @@
padding-right: 10px;
}
.tenant-admin {
// display: flex;
// justify-content: center;
// align-items: center;
min-width: 77px;
min-height: 28px;
color: #ffa631;
background: #ffe6c6 0% 0% no-repeat padding-box;
border-radius: 44px;
letter-spacing: 0.11px;
font-weight: 400;
opacity: 1;
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 10px;
padding-right: 10px;
}
.tenant-description-template-editor {
// display: flex;
// justify-content: center;

View File

@ -1,9 +1,8 @@
<div class="main-content dashboard-main-container h-100" [class.non-auth-main-container]="!this.isAuthenticated()">
<div class="main-content dashboard-main-container h-100">
<div *ngIf="this.isAuthenticated()" class="container-fluid">
<div *ngIf="this.dashboardStatistics" class="main-content">
<div class="container-fluid">
<div class="row flex-column-reverse flex-xl-row">
<div class="col-12 col-xl-10">
<div class="row">
<div *ngIf="newReleaseNotificationVisible" class="new-releases-card col-auto mt-0 mr-4">
@ -79,7 +78,6 @@
</mat-tab-group>
</div>
</div>
<!-- Right Sidebar -->
<div *ngIf="!this.hasDmps()" class="col-12 col-xl-2 mb-4 stats">
<div class="row">
@ -108,7 +106,6 @@
</div>
</div>
</div>
<div *ngIf="this.hasDmps()" class="col-12 col-xl-2 mb-4 stats">
<div class="row">
<div class="col-12">
@ -147,13 +144,13 @@
</div>
<!-- Home screen on log out -->
<div class="col p-0" *ngIf="!this.isAuthenticated()">
<div class="col-auto">
<div class="container-fluid" *ngIf="!this.isAuthenticated()">
<div class="main-content">
<div class="col">
<div class="container-fluid">
<div class="row flex-column-reverse flex-xl-row">
<div class="col-12 col-xl">
<div class="row">
<div class="col d-flex flex-column">
<div class="card non-auth-card" [style.display]="isIntroCardVisible ? 'block' : 'none'">
<div class="col-auto card" [style.display]="isIntroCardVisible ? 'block' : 'none'">
<a class="col-auto d-flex" (click)="dismissIntroCard()"><span class="ml-auto mt-3 material-icons clear-icon">clear</span></a>
<div class="d-flex flex-column align-items-center non-auth-title-container">
<h4 class="pt-4">{{'DASHBOARD.TITLE' | translate}}</h4>
@ -163,6 +160,7 @@
<img class="col-auto ml-auto laptop-img" src="../../../assets/images/dashboard-popup.png">
</div>
</div>
</div>
<!-- <div *ngIf="hasDmps()" class="col activity">
<div class="latest-activity-title">{{'DASHBOARD.LATEST-ACTIVITY' | translate}}</div>
<mat-tab-group mat-align-tabs="start" class="remove-border-bottom" ( [selectedIndex]="indexFromCurrentType" selectedTabChange)="currentType = $event.tab.ariaLabel">
@ -181,31 +179,50 @@
</mat-tab-group>
</div> -->
</div>
<div class="col-12 col-xl-auto mb-4 stats">
<div *ngIf="!hasDmps()" class="row flex-xl-column">
<div class="col-12">
<div class="personal-usage"><span>{{'DASHBOARD.PUBLIC-USAGE' | translate}}</span></div>
</div>
<div class="col-auto">
<div *ngIf="!hasDmps()" class="ml-auto pl-4 personal-usage-block">
<div class="personal-usage">{{'DASHBOARD.PUBLIC-USAGE' | translate}}</div>
<div class="counter-zero">0</div>
<a>{{'DASHBOARD.PUBLIC-DMPS' | translate}}</a>
<div class="counter-zero">0</div>
<a>{{'DASHBOARD.PUBLIC-DATASETS' | translate}}</a>
<div class="counter-zero">0</div>
<div class="counter-zero"><span>0</span></div>
<a class="link">{{'DASHBOARD.PUBLIC-DMPS' | translate}}</a>
</div>
<div class="col-auto">
<div class="counter-zero"><span>0</span></div>
<a class="link">{{'DASHBOARD.PUBLIC-DESCRIPTIONS' | translate}}</a>
</div>
<div class="col-auto">
<div class="counter-zero"><span>0</span></div>
<a class="link-disabled">{{'DASHBOARD.GRANTS' | translate}}</a>
<div class="counter-zero">0</div>
</div>
<div class="col-auto">
<div class="counter-zero"><span>0</span></div>
<a class="link-disabled">{{'DASHBOARD.RELATED-ORGANISATIONS' | translate}}</a>
</div>
<div *ngIf="hasDmps()" class="ml-auto stats">
<div class="personal-usage">{{'DASHBOARD.PUBLIC-USAGE' | translate}}</div>
<div [ngClass]="{'counter': dashboardStatistics?.dmpCount != 0, 'counter-zero': dashboardStatistics?.dmpCount == 0}">
{{dashboardStatistics?.dmpCount}}</div>
</div>
<div *ngIf="hasDmps()" class="row flex-xl-column">
<div class="col-12">
<div class="personal-usage" ><span>{{'DASHBOARD.PUBLIC-USAGE' | translate}}</span></div>
</div>
<div class="col-auto">
<div [ngClass]="{'counter': dashboardStatistics?.dmpCount != 0, 'counter-zero': dashboardStatistics?.dmpCount == 0}" ><span>{{dashboardStatistics?.dmpCount}}</span></div>
<a [routerLink]="['/explore-plans']" class="link">{{'DASHBOARD.PUBLIC-DMPS' | translate}}</a>
<div [ngClass]="{'counter': dashboardStatistics?.descriptionCount != 0, 'counter-zero': dashboardStatistics?.descriptionCount == 0}">
{{dashboardStatistics?.descriptionCount}}</div>
</div>
<div class="col-auto">
<div [ngClass]="{'counter': dashboardStatistics?.descriptionCount != 0, 'counter-zero': dashboardStatistics?.descriptionCount == 0}"><span>{{dashboardStatistics?.descriptionCount}}</span></div>
<a [routerLink]="['/explore-descriptions']" class="link">{{'DASHBOARD.PUBLIC-DESCRIPTIONS' | translate}}</a>
<div [ngClass]="{'counter': grantCount != 0, 'counter-zero': grantCount == 0}">
{{grantCount}}</div>
</div>
<div class="col-auto">
<div [ngClass]="{'counter': grantCount != 0, 'counter-zero': grantCount == 0}" ><span>{{grantCount}}</span></div>
<a href="#" class="link-disabled">{{'DASHBOARD.GRANTS' | translate}}</a>
<div [ngClass]="{'counter': organizationCount != 0, 'counter-zero': organizationCount == 0}">
{{organizationCount}}</div>
</div>
<div class="col-auto">
<div [ngClass]="{'counter': organizationCount != 0, 'counter-zero': organizationCount == 0}"><span>{{organizationCount}}</span></div>
<a href="#" class="link-disabled">{{'DASHBOARD.RELATED-ORGANISATIONS' | translate}}</a>
</div>
</div>

View File

@ -374,14 +374,6 @@ input[type="text"] {
padding: 0rem 7em 0rem 3rem;
}
.non-auth-main-container {
padding-left: 0;
}
.non-auth-card {
margin-left: 3rem;
}
.app-info {
font-size: 1rem;
padding: 1rem 2rem;

View File

@ -1,19 +1,16 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { DescriptionStatus } from '@app/core/common/enum/description-status';
import { DescriptionTemplateVersionStatus } from '@app/core/common/enum/description-template-version-status';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { DescriptionTemplate } from '@app/core/model/description-template/description-template';
import { Description } from '@app/core/model/description/description';
import { DmpBlueprintDefinitionSection } from '@app/core/model/dmp-blueprint/dmp-blueprint';
import { Dmp, DmpDescriptionTemplate } from '@app/core/model/dmp/dmp';
import { DescriptionTemplateService } from '@app/core/services/description-template/description-template.service';
import { DmpDescriptionTemplate } from '@app/core/model/dmp/dmp';
import { DescriptionService } from '@app/core/services/description/description.service';
import { DmpBlueprintService } from '@app/core/services/dmp/dmp-blueprint.service';
import { BaseComponent } from '@common/base/base.component';
import { takeUntil } from 'rxjs/operators';
import { DeprecatedDescriptionTemplateDialog } from './dialog-description-template/deprecated-description-template-dialog.component';
import { DescriptionTemplateVersionStatus } from '@app/core/common/enum/description-template-version-status';
import { DescriptionStatus } from '@app/core/common/enum/description-status';
@Component({
selector: 'app-description-base-fields-editor-component',
@ -47,6 +44,7 @@ export class DescriptionBaseFieldsEditorComponent extends BaseComponent {
const currentVersionsOfDescriptionTemplates = dmpDescriptionTemplates.map(x => x.currentDescriptionTemplate);
this.availableDescriptionTemplates.push(...currentVersionsOfDescriptionTemplates);
if (this.description?.descriptionTemplate != null) {
const isPreviousVersion: boolean = this.description.descriptionTemplate.versionStatus === DescriptionTemplateVersionStatus.Previous;
if (isPreviousVersion === true) {
if (this.description.status === DescriptionStatus.Draft) {
@ -58,6 +56,7 @@ export class DescriptionBaseFieldsEditorComponent extends BaseComponent {
this.availableDescriptionTemplates.push(this.description.descriptionTemplate);
}
}
}
private openDeprecatedDescriptionTemplateDialog(): void {
const dialogRef = this.dialog.open(DeprecatedDescriptionTemplateDialog, {
@ -67,7 +66,7 @@ export class DescriptionBaseFieldsEditorComponent extends BaseComponent {
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(
result => {
if(result) {
if (result) {
this.descriptionService.updateDescriptionTemplate({
id: this.description.id,
hash: this.description.hash

View File

@ -111,13 +111,13 @@
<span class="material-icons">chevron_left</span>
<div>{{'DMP-EDITOR.ACTIONS.PREVIOUS-STEP' | translate}}</div>
</div>
<div *ngIf="this.step < this.maxStep" mat-raised-button type="button" class="col-auto stepper-btn ml-auto" [ngClass]="{ 'next-disabled': this.step === this.maxStep, 'next': this.step < selectedBlueprint?.definition?.sections?.length, 'description-next': this.step >= selectedBlueprint?.definition?.sections?.length }" (click)="nextStep()">
<div *ngIf="this.step < this.maxSteps" mat-raised-button type="button" class="col-auto stepper-btn ml-auto" [ngClass]="{ 'next-disabled': this.step === this.maxSteps, 'next': this.step < selectedBlueprint?.definition?.sections?.length, 'description-next': this.step >= selectedBlueprint?.definition?.sections?.length }" (click)="nextStep()">
<div>{{'DMP-EDITOR.ACTIONS.NEXT-STEP' | translate}}</div>
<span class="material-icons">chevron_right</span>
</div>
</div>
<div class="col-auto pr-0" *ngIf="this.step !== 0">
<!-- <app-form-progress-indication class="col-12" *ngIf="formGroup && !formGroup.disabled && !lockStatus" [formGroup]="formGroup" [isDmpEditor]="true"></app-form-progress-indication> -->
<app-dmp-form-progress-indication class="col-12" *ngIf="formGroup && !formGroup.disabled && !lockStatus" [formGroup]="formGroup"></app-dmp-form-progress-indication>
</div>
</div>

View File

@ -88,7 +88,7 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
filterFn: (searchQuery: string, data?: any) => this.dmpBlueprintService.query(this.dmpBlueprintService.buildAutocompleteLookup(searchQuery, null, null, [DmpBlueprintStatus.Finalized])).pipe(map(x => x.items)),
getSelectedItem: (selectedItem: any) => this.dmpBlueprintService.query(this.dmpBlueprintService.buildAutocompleteLookup(null, null, [selectedItem])).pipe(map(x => x.items[0])),
displayFn: (item: DmpBlueprint) => item.label,
subtitleFn: (item: DmpBlueprint) => this.language.instant('DMP-EDITOR.FIELDS.DMP-BLUEPRINT-VERSION') + ' '+ item.version,
subtitleFn: (item: DmpBlueprint) => this.language.instant('DMP-EDITOR.FIELDS.DMP-BLUEPRINT-VERSION') + ' ' + item.version,
titleFn: (item: DmpBlueprint) => item.label,
valueAssign: (item: DmpBlueprint) => item.id,
};
@ -174,14 +174,14 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
try {
this.editorModel = data ? new DmpEditorModel().fromModel(data) : new DmpEditorModel();
if (data) {
if(data.descriptions){
if (data.descriptions) {
if (data.status == DmpStatus.Finalized) {
data.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status === DescriptionStatus.Finalized);
} else {
data.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status !== DescriptionStatus.Canceled);
}
}
if(data.dmpDescriptionTemplates){
if (data.dmpDescriptionTemplates) {
data.dmpDescriptionTemplates = data.dmpDescriptionTemplates.filter(x => x.isActive === IsActive.Active);
}
@ -304,6 +304,11 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
// Steps
//
//
get maxSteps(): number {
return this.item?.blueprint?.definition?.sections?.length ?? 0;
}
changeStep(index: number) {
this.step = index;
this.resetScroll();
@ -389,7 +394,8 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
DmpEditorModel.reApplyPropertiesValidators(
{
formGroup: this.formGroup,
validationErrorModel: this.editorModel.validationErrorModel
validationErrorModel: this.editorModel.validationErrorModel,
blueprint: this.item.blueprint
}
);
this.formGroup.get('properties').get('contacts').markAsDirty();
@ -404,7 +410,8 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
DmpEditorModel.reApplyPropertiesValidators(
{
formGroup: this.formGroup,
validationErrorModel: this.editorModel.validationErrorModel
validationErrorModel: this.editorModel.validationErrorModel,
blueprint: this.item.blueprint
}
);
this.formGroup.get('properties').get('contacts').markAsDirty();
@ -455,32 +462,32 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
});
}
canAddDescription(section: DmpBlueprintDefinitionSection ): boolean{
if(section.hasTemplates){
if (section.descriptionTemplates?.length > 0){
canAddDescription(section: DmpBlueprintDefinitionSection): boolean {
if (section.hasTemplates) {
if (section.descriptionTemplates?.length > 0) {
const descriptions = this.descriptionsInSection(section.id)
if (this.item.dmpDescriptionTemplates.filter(x => x.sectionId == section.id).length > descriptions.map(x => x.dmpDescriptionTemplate).length){
if (this.item.dmpDescriptionTemplates.filter(x => x.sectionId == section.id).length > descriptions.map(x => x.dmpDescriptionTemplate).length) {
return true;
}
let multiplicityValidResults :boolean[] = [];
let multiplicityValidResults: boolean[] = [];
section.descriptionTemplates.forEach(sectionDescriptionTemplate => {
if (sectionDescriptionTemplate.maxMultiplicity != null){
if (sectionDescriptionTemplate.maxMultiplicity != null) {
const count = descriptions.filter(x => x.dmpDescriptionTemplate.descriptionTemplateGroupId == sectionDescriptionTemplate.descriptionTemplateGroupId).length || 0;
if (count >= sectionDescriptionTemplate.maxMultiplicity) multiplicityValidResults.push(false);
else multiplicityValidResults.push(true);
}else{
} else {
multiplicityValidResults.push(true);
}
})
if(multiplicityValidResults.includes(true)) return true
if (multiplicityValidResults.includes(true)) return true
else return false;
}else{
} else {
return true;
}
}else{
} else {
return false;
}
}
@ -504,10 +511,10 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(groupId => {
if (groupId) {
let data = this.formGroup.get('descriptionTemplates').get(sectionId.toString()).value as Guid[];
if (data){
if (data) {
data.push(groupId);
this.formGroup.get('descriptionTemplates').get(sectionId.toString()).patchValue(data);
} else{
} else {
this.formGroup.get('descriptionTemplates').get(sectionId.toString()).patchValue([groupId]);
}
}

View File

@ -1,11 +1,12 @@
import { FormArray, FormControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { FormArray, FormControl, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { DmpAccessType } from "@app/core/common/enum/dmp-access-type";
import { DmpBlueprintFieldCategory } from "@app/core/common/enum/dmp-blueprint-field-category";
import { DmpContactType } from "@app/core/common/enum/dmp-contact-type";
import { DmpStatus } from "@app/core/common/enum/dmp-status";
import { DmpUserRole } from "@app/core/common/enum/dmp-user-role";
import { DmpUserType } from "@app/core/common/enum/dmp-user-type";
import { IsActive } from "@app/core/common/enum/is-active.enum";
import { DmpBlueprint } from "@app/core/model/dmp-blueprint/dmp-blueprint";
import { DmpBlueprint, FieldInSection } from "@app/core/model/dmp-blueprint/dmp-blueprint";
import { Dmp, DmpBlueprintValue, DmpBlueprintValuePersist, DmpContact, DmpContactPersist, DmpDescriptionTemplate, DmpDescriptionTemplatePersist, DmpPersist, DmpProperties, DmpPropertiesPersist, DmpReferenceDataPersist, DmpReferencePersist, DmpUser, DmpUserPersist } from "@app/core/model/dmp/dmp";
import { DmpReference } from "@app/core/model/dmp/dmp-reference";
import { ReferencePersist } from "@app/core/model/reference/reference";
@ -120,13 +121,16 @@ export class DmpEditorModel extends BaseEditorModel implements DmpPersist {
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'label', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'label')] });
baseValidationArray.push({ key: 'status', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'status')] });
baseValidationArray.push({ key: 'properties', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'properties')] });
baseValidationArray.push({ key: 'status', validators: [BackendErrorValidator(this.validationErrorModel, 'status')] });
baseValidationArray.push({ key: 'properties', validators: [BackendErrorValidator(this.validationErrorModel, 'properties')] });
baseValidationArray.push({ key: 'description', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'description')] });
baseValidationArray.push({ key: 'language', validators: [BackendErrorValidator(this.validationErrorModel, 'language')] });
baseValidationArray.push({ key: 'blueprint', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'blueprint')] });
baseValidationArray.push({ key: 'accessType', validators: [BackendErrorValidator(this.validationErrorModel, 'accessType')] });
baseValidationArray.push({ key: 'language', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'language')] });
baseValidationArray.push({ key: 'blueprint', validators: [BackendErrorValidator(this.validationErrorModel, 'blueprint')] });
baseValidationArray.push({ key: 'accessType', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'accessType')] });
baseValidationArray.push({ key: 'descriptionTemplates', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'descriptionTemplates')] });
baseValidationArray.push({ key: 'dmpDescriptionValidator', validators: [] });
baseValidationArray.push({ key: 'users', validators: [BackendErrorValidator(this.validationErrorModel, `users`)] });
baseValidationArray.push({ key: 'hash', validators: [] });
@ -143,6 +147,7 @@ export class DmpEditorModel extends BaseEditorModel implements DmpPersist {
static reApplyPropertiesValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
blueprint: DmpBlueprint
}): void {
const { formGroup, validationErrorModel } = params;
@ -150,20 +155,36 @@ export class DmpEditorModel extends BaseEditorModel implements DmpPersist {
DmpPropertiesEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `properties.`,
validationErrorModel: validationErrorModel
validationErrorModel: validationErrorModel,
blueprint: params.blueprint
});
}
static reApplyDescriptionTemplateValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
}): void {
const { formGroup, validationErrorModel } = params;
const descriptionTemplates = formGroup?.get('descriptionTemplates') as UntypedFormGroup;
const keys = Object.keys(descriptionTemplates.value as Object);
keys.forEach((key) => {
const control = descriptionTemplates?.get(key);
DmpBlueprintValueEditorModel.reapplyValidators({
DmpDescriptionTemplateEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `descriptionTemplates[${key}].`,
validationErrorModel: validationErrorModel
})
});
}
static reApplyUsersValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
}): void {
const { formGroup, validationErrorModel } = params;
(formGroup.get('users') as FormArray).controls?.forEach(
(control, index) => DmpUserEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
@ -189,11 +210,13 @@ export class DmpPropertiesEditorModel implements DmpPropertiesPersist {
dmpBlueprint.definition.sections.forEach(section => {
section.fields?.forEach(field => {
if (field.category !== DmpBlueprintFieldCategory.System) {
this.dmpBlueprintValues.set(field.id, new DmpBlueprintValueEditorModel(this.validationErrorModel).fromModel(
{
fieldId: field.id,
fieldValue: item?.dmpBlueprintValues?.find(x => x.fieldId == field.id)?.fieldValue,
}, dmpReferences));
}, dmpReferences, field));
}
});
});
if (item?.contacts) { item.contacts.map(x => this.contacts.push(new DmpContactEditorModel(this.validationErrorModel).fromModel(x))); }
@ -254,7 +277,8 @@ export class DmpPropertiesEditorModel implements DmpPropertiesPersist {
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
rootPath: string,
blueprint: DmpBlueprint
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
@ -266,7 +290,8 @@ export class DmpPropertiesEditorModel implements DmpPropertiesPersist {
DmpBlueprintValueEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `${rootPath}dmpBlueprintValues[${key}].`,
validationErrorModel: validationErrorModel
validationErrorModel: validationErrorModel,
isRequired: params.blueprint.definition.sections.flatMap(x => x.fields).find(x => x.id.toString() == key).required
})
});
@ -284,6 +309,8 @@ export class DmpBlueprintValueEditorModel implements DmpBlueprintValuePersist {
fieldId: Guid;
fieldValue: string;
references: DmpReferencePersist[] = [];
isRequired: boolean = false;
category: DmpBlueprintFieldCategory;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
@ -291,7 +318,7 @@ export class DmpBlueprintValueEditorModel implements DmpBlueprintValuePersist {
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
fromModel(item: DmpBlueprintValue, dmpReferences: DmpReference[]): DmpBlueprintValueEditorModel {
fromModel(item: DmpBlueprintValue, dmpReferences: DmpReference[], field: FieldInSection): DmpBlueprintValueEditorModel {
this.fieldId = item.fieldId;
this.fieldValue = item.fieldValue;
this.references = dmpReferences?.filter(x => x.data.blueprintFieldId == this.fieldId && x.isActive == IsActive.Active).map(x => {
@ -311,6 +338,10 @@ export class DmpBlueprintValueEditorModel implements DmpBlueprintValuePersist {
}
});
this.isRequired = field.required;
if (this.isRequired) console.log(field);
this.category = field.category;
return this;
}
@ -323,28 +354,39 @@ export class DmpBlueprintValueEditorModel implements DmpBlueprintValuePersist {
if (context == null) {
context = DmpBlueprintValueEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
rootPath,
isRequired: this.isRequired
});
}
return this.formBuilder.group({
const formGroup = this.formBuilder.group({
fieldId: [{ value: this.fieldId, disabled: disabled }, context.getValidation('fieldId').validators],
fieldValue: [{ value: this.fieldValue, disabled: disabled }, context.getValidation('fieldValue').validators],
references: [{ value: this.references?.map(x => x.reference), disabled: disabled }, context.getValidation('references').validators],
});
switch (this.category) {
case DmpBlueprintFieldCategory.ReferenceType:
formGroup.addControl('references', new FormControl({ value: this.references?.map(x => x.reference), disabled: disabled }, context.getValidation('references').validators));
break;
case DmpBlueprintFieldCategory.System:
case DmpBlueprintFieldCategory.Extra:
formGroup.addControl('fieldValue', new FormControl({ value: this.fieldValue, disabled: disabled }, context.getValidation('fieldValue').validators));
break;
}
return formGroup;
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
validationErrorModel: ValidationErrorModel,
isRequired: boolean,
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'fieldId', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}fieldId`)] });
baseValidationArray.push({ key: 'fieldValue', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}fieldValue`)] });
baseValidationArray.push({ key: 'references', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}references`)] });
baseValidationArray.push({ key: 'fieldValue', validators: params.isRequired ? [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}fieldValue`)] : [BackendErrorValidator(validationErrorModel, `${rootPath}fieldValue`)] });
baseValidationArray.push({ key: 'references', validators: params.isRequired ? [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}references`)] : [BackendErrorValidator(validationErrorModel, `${rootPath}references`)] });
baseContext.validation = baseValidationArray;
return baseContext;
@ -353,13 +395,15 @@ export class DmpBlueprintValueEditorModel implements DmpBlueprintValuePersist {
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
rootPath: string,
isRequired: boolean
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const context = DmpBlueprintValueEditorModel.createValidationContext({
rootPath,
validationErrorModel
validationErrorModel,
isRequired: params.isRequired
});
['fieldId', 'fieldValue', 'references'].forEach(keyField => {
@ -375,7 +419,7 @@ export class DmpContactEditorModel implements DmpContactPersist {
firstName: string;
lastName: string;
email: string;
contactType: DmpContactType= DmpContactType.Internal;
contactType: DmpContactType = DmpContactType.Internal;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
@ -384,7 +428,7 @@ export class DmpContactEditorModel implements DmpContactPersist {
) { }
fromModel(item: DmpContact): DmpContactEditorModel {
if(item?.user?.id) this.userId = item.user.id;
if (item?.user?.id) this.userId = item.user.id;
this.firstName = item.firstName;
this.lastName = item.lastName;
this.email = item.email;
@ -457,7 +501,7 @@ export class DmpUserEditorModel implements DmpUserPersist {
user: Guid;
role: DmpUserRole;
email: string;
userType: DmpUserType= DmpUserType.Internal;
userType: DmpUserType = DmpUserType.Internal;
sectionId: Guid;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
@ -467,7 +511,7 @@ export class DmpUserEditorModel implements DmpUserPersist {
) { }
fromModel(item: DmpUser): DmpUserEditorModel {
if(item?.user?.id) this.user = item.user.id;
if (item?.user?.id) this.user = item.user.id;
this.role = item.role;
// TODO this.email = item.email;
this.userType = (item == null || this.user != null) ? DmpUserType.Internal : DmpUserType.External;
@ -623,7 +667,7 @@ export class DmpDescriptionTemplateEditorModel implements DmpDescriptionTemplate
if (context == null) {
context = DmpDescriptionTemplateEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
rootPath: rootPath,
});
}
@ -635,7 +679,7 @@ export class DmpDescriptionTemplateEditorModel implements DmpDescriptionTemplate
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
validationErrorModel: ValidationErrorModel,
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;

View File

@ -1,15 +1,16 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from '@angular/core';
import { FormattingModule } from '@app/core/formatting.module';
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module';
import { ReferenceFieldModule } from '@app/ui/reference/reference-field/reference-field.module';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { DmpUserFieldModule } from '../dmp-user-field/dmp-user-field.module';
import { DmpEditorComponent } from './dmp-editor.component';
import { DmpEditorRoutingModule } from './dmp-editor.routing';
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
import { ReferenceFieldModule } from '@app/ui/reference/reference-field/reference-field.module';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { DmpUserFieldModule } from '../dmp-user-field/dmp-user-field.module';
import { DmpFormProgressIndicationModule } from './form-progress-indication/dmp-form-progress-indication.module';
@NgModule({
imports: [
@ -22,7 +23,8 @@ import { DmpUserFieldModule } from '../dmp-user-field/dmp-user-field.module';
AutoCompleteModule,
ReferenceFieldModule,
DragDropModule,
DmpUserFieldModule
DmpUserFieldModule,
DmpFormProgressIndicationModule
],
declarations: [
DmpEditorComponent,

View File

@ -0,0 +1,5 @@
<div class="demo-progress-bar-container">
<div class="percentage d-flex justify-content-center">{{progressSoFar}} {{'GENERAL.PREPOSITIONS.OF' | translate}} {{total}}</div>
<mat-progress-bar class="form-progress-bar" [ngClass]="{'progress-bar': true}" mode="determinate" [value]="value"></mat-progress-bar>
<div class="percentage" [ngStyle]="{'padding-left': value ? value - 10 + '%' : 0 + '%' }">{{value}}%</div>
</div>

View File

@ -0,0 +1,16 @@
.percentage {
color: #212121;
opacity: 0.7;
font-weight: 400;
font-size: 0.875rem;
}
.progress-bar {
border-radius: 20px;
height: 11px;
}
::ng-deep .mat-progress-bar .mat-progress-bar-fill::after {
border-radius: 20px !important;
}

View File

@ -0,0 +1,104 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service';
import { BaseComponent } from '@common/base/base.component';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-dmp-form-progress-indication',
templateUrl: './dmp-form-progress-indication.component.html',
styleUrls: ['./dmp-form-progress-indication.component.scss']
})
export class DmpFormProgressIndicationComponent extends BaseComponent implements OnInit, OnChanges {
@Input() formGroup: UntypedFormGroup;
@Input() public progressValueAccuracy = 2;
progressSoFar: number;
total: number;
percent: number;
constructor(private visibilityRulesService: VisibilityRulesService) { super(); }
public value = 0;
ngOnInit() {
this.init();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.formGroup) {
this.init();
}
}
init() {
setTimeout(() => { this.calculateValueForProgressbar(); });
this.formGroup
.valueChanges
.pipe(takeUntil(this._destroyed))
.subscribe(control => {
setTimeout(() => { this.calculateValueForProgressbar(); });
});
}
calculateValueForProgressbar() {
this.progressSoFar = this.countFormControlsValidForProgress(this.formGroup);
this.total = this.countFormControlsRequiredFieldsForTotal(this.formGroup);
this.percent = (this.progressSoFar / this.total) * 100;
this.value = Number.parseFloat(this.percent.toPrecision(this.progressValueAccuracy));
}
countFormControlsValidForProgress(formControl: AbstractControl): number {
let valueCurrent = 0;
if (formControl instanceof UntypedFormControl) {
if (this.controlRequired(formControl) && this.controlEnabled(formControl) && formControl.valid) {
valueCurrent++;
}
} else if (formControl instanceof UntypedFormGroup) {
Object.keys(formControl.controls).forEach(item => {
const control = formControl.get(item);
valueCurrent = valueCurrent + this.countFormControlsValidForProgress(control);
});
} else if (formControl instanceof UntypedFormArray) {
formControl.controls.forEach(item => {
valueCurrent = valueCurrent + this.countFormControlsValidForProgress(item);
});
}
return valueCurrent;
}
countFormControlsRequiredFieldsForTotal(formControl: AbstractControl, checkVisibility = false): number {
let valueCurrent = 0;
if (formControl instanceof UntypedFormControl) {
if (this.controlRequired(formControl) && this.controlEnabled(formControl)) {
valueCurrent++;
}
} else if (formControl instanceof UntypedFormGroup) {
if (!checkVisibility || (!formControl.get('id')?.value || (this.visibilityRulesService.isVisibleMap[formControl.get('id').value] ?? true))) {
Object.keys(formControl.controls).forEach(item => {
const control = formControl.get(item);
valueCurrent = valueCurrent + this.countFormControlsRequiredFieldsForTotal(control, checkVisibility);
});
}
} else if (formControl instanceof UntypedFormArray) {
formControl.controls.forEach(item => {
valueCurrent = valueCurrent + this.countFormControlsRequiredFieldsForTotal(item, checkVisibility);
});
}
return valueCurrent;
}
controlRequired(formControl: AbstractControl) {
if (formControl.validator) {
const validator = formControl.validator({} as AbstractControl);
if (validator && validator.required) {
return true;
}
} else { return false }
}
controlEnabled(formControl: AbstractControl) {
if (formControl.enabled) {
return true;
} else { return false }
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { DmpFormProgressIndicationComponent } from './dmp-form-progress-indication.component';
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule
],
declarations: [
DmpFormProgressIndicationComponent
],
exports: [
DmpFormProgressIndicationComponent
]
})
export class DmpFormProgressIndicationModule { }

View File

@ -43,7 +43,7 @@
</div>
<div class="col-12 col-xl mt-3" *ngIf="user.get('userType').value == dmpUserTypeEnum.Internal">
<mat-form-field class="w-100">
<mat-label>{{'DMP-EDITOR.FIELDS.USER' | translate}}*</mat-label>
<mat-label>{{'DMP-EDITOR.FIELDS.USER' | translate}}</mat-label>
<app-single-auto-complete [formControl]="user.get('user')" [hidePlaceholder]="true" [configuration]="userService.singleAutoCompleteDmpAssociatedUserConfiguration"></app-single-auto-complete>
<mat-error *ngIf="user.get('user').hasError('backendError')">{{user.get('user').getError('backendError').message}}</mat-error>
<mat-error *ngIf="user.get('user').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
@ -51,7 +51,7 @@
</div>
<div class="col-12 col-xl mt-3" *ngIf="user.get('userType').value == dmpUserTypeEnum.External">
<mat-form-field class="w-100">
<mat-label>{{'DMP-EDITOR.FIELDS.EMAIL' | translate}}*</mat-label>
<mat-label>{{'DMP-EDITOR.FIELDS.EMAIL' | translate}}</mat-label>
<input matInput type="text" name="email" [formControl]="user.get('email')">
<mat-error *ngIf="user.get('email').hasError('backendError')">{{user.get('email').getError('backendError').message}}</mat-error>
<mat-error *ngIf="user.get('email').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>

View File

@ -57,10 +57,10 @@ export class DmpUserFieldComponent extends BaseComponent implements OnInit {
removeUser(userIndex: number): void {
(this.form.get('users') as FormArray).removeAt(userIndex);
DmpEditorModel.reApplyPropertiesValidators(
DmpEditorModel.reApplyUsersValidators(
{
formGroup: this.form,
validationErrorModel: this.validationErrorModel
validationErrorModel: this.validationErrorModel,
}
);
this.form.get('users').markAsDirty();
@ -78,7 +78,7 @@ export class DmpUserFieldComponent extends BaseComponent implements OnInit {
moveItemInArray(usersFormArray.controls, event.previousIndex, event.currentIndex);
usersFormArray.updateValueAndValidity();
DmpEditorModel.reApplyPropertiesValidators(
DmpEditorModel.reApplyUsersValidators(
{
formGroup: this.form,
validationErrorModel: this.validationErrorModel

View File

@ -131,7 +131,8 @@ export class DmpListingComponent extends BaseComponent implements OnInit { //IBr
selector: '.dataset-tour',
content: 'Step 2',
orientation: Orientation.Right,
isStepUnique: false
isStepUnique: false,
useHighlightPadding: true
}
]
};

View File

@ -195,8 +195,8 @@
<p class="mb-0 mr-0 pl-2 frame-txt" [matMenuTriggerFor]="exportMenu">{{ 'DMP-OVERVIEW.ACTIONS.EXPORT' | translate }}</p>
</div>
</ng-container>
<ng-container *ngIf="canCreateNewVersion()" (click)="newVersionClicked()">
<div class="col-12 d-flex align-items-center">
<ng-container *ngIf="canCreateNewVersion()">
<div class="col-12 d-flex align-items-center" (click)="newVersionClicked()">
<button mat-mini-fab class="frame-btn">
<mat-icon class="mat-mini-fab-icon">add_to_photos</mat-icon>
</button>

View File

@ -33,6 +33,7 @@
.nav-row {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 10px 20px;
text-align: left;
@ -43,6 +44,19 @@
font-family: 'Roboto',sans-serif;
}
.nav-subrow {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
text-align: left;
letter-spacing: 0px;
color: #000000;
opacity: 1;
font-size: 0.93rem;
font-family: 'Roboto',sans-serif;
}
.nav-row:hover {
background-color: #ececec;
border-radius: 6px;

View File

@ -9,11 +9,13 @@
<i *ngIf="groupMenuRoute.path == '/plans'" class="material-symbols-outlined icon-mask">person</i>
<span [ngClass]="{'pl-0': groupMenuRoute.path == '/plans'}">{{groupMenuRoute.title | translate}}</span>
</a>
<a class="nav-link nav-row dataset-tour" *ngIf="groupMenuRoute.path === '/descriptions'" [routerLink]="[groupMenuRoute.path]">
<span class="inner-line"></span>
<a class="nav-link nav-row" *ngIf="groupMenuRoute.path === '/descriptions'" [routerLink]="[groupMenuRoute.path]">
<span class="mb-2 inner-line"></span>
<div class="pl-0 pt-1 pb-1 container-fluid nav-subrow dataset-tour">
<i class="material-symbols-outlined icon">{{ groupMenuRoute.icon }}</i>
<i class="material-symbols-outlined icon-mask">person</i>
<span class="pl-0">{{groupMenuRoute.title | translate}}</span>
</div>
</a>
<a class="nav-link nav-row" *ngIf="groupMenuRoute.path === '/co-branding'" href="/splash/resources/co-branding.html">
<i class="material-symbols-outlined icon">{{ groupMenuRoute.icon }}</i>

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { UntypedFormArray } from '@angular/forms';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { NotificationServiceEnumUtils } from '@notification-service/core/formatting/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { NotificationFieldInfoEditorModel, NotificationFieldOptionsEditorModel } from '../notification-template-editor.model';
@ -25,7 +25,7 @@ export class NotificationTemplateFieldOptionsComponent extends BaseComponent imp
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(
public enumUtils: EnumUtils,
public enumUtils: NotificationServiceEnumUtils,
) { super(); }
ngOnInit() {

View File

@ -2,7 +2,7 @@
<div class="row notification-template-editor">
<div class="col-md-10 offset-md-1 colums-gapped">
<div class="row align-items-center mt-4 mb-4" *ngIf="formGroup">
<div class="col-auto">
<div class="col-md col-12">
<h3 *ngIf="isNew && !isClone">{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-EDITOR.NEW' | translate}}</h3>
<app-navigation-breadcrumb />
</div>

View File

@ -7,48 +7,69 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div>
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.NOTIFICATION-TYPE' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.NOTIFICATION-TYPE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.notificationTypes">
<mat-option *ngFor="let type of notificationTypeEnumValues" [value]="type">{{enumUtils.toNotificationTypeString(type)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div>
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.KIND' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.KIND' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.kinds">
<mat-option *ngFor="let kind of notificationTemplateKindEnumValues" [value]="kind">{{enumUtils.toNotificationTemplateKindString(kind)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div>
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.CHANNEL' | translate}}
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.CHANNEL' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.channels">
<mat-option *ngFor="let channel of notificationTemplateChannelEnumValues" [value]="channel">{{enumUtils.toNotificationTemplateChannelString(channel)}}</mat-option>
</mat-select>
</mat-label>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'NOTIFICATION-SERVICE.NOTIFICATION-TEMPLATE-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -7,66 +7,85 @@
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<div class="container-fluid" (click)="$event?.stopPropagation?.()">
<div class="row justify-content-between">
<div class="col-auto mt-2">
<h4>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.TITLE' | translate}}</h4>
</div>
<div class="col-auto">
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<section class="w-100">
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
</section>
</div>
</div>
<div class="mt-4">
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.USERS' | translate}}</mat-label>
<app-multiple-auto-complete [(ngModel)]="internalFilters.userIds" [hidePlaceholder]="true" [separatorKeysCodes]="separatorKeysCodes" [configuration]="userAutoCompleteConfiguration">
</app-multiple-auto-complete>
</mat-form-field>
</div>
</div>
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.NOTIFICATION-TYPE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.type">
<mat-option *ngFor="let type of notificationTypeEnumValues" [value]="type">{{enumUtils.toNotificationTypeString(type)}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.CONTACT-TYPE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.contactType">
<mat-option *ngFor="let contactType of notificationContactTypeEnumValues" [value]="contactType">{{enumUtils.toNotificationContactTypeString(contactType)}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.NOTIFY-STATE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.notifyState">
<mat-option *ngFor="let notifyState of notificationNotifyStateEnumValues" [value]="notifyState">{{enumUtils.toNotificationNotifyStateString(notifyState)}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.TRACKING-STATE' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.trackingState">
<mat-option *ngFor="let trackingState of notificationTrackingStateEnumValues" [value]="trackingState">{{enumUtils.toNotificationTrackingStateString(trackingState)}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div>
<mat-form-field class="col-12">
<div class="row mt-3">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.TRACKING-PROCESS' | translate}}</mat-label>
<mat-select multiple [(ngModel)]="internalFilters.trackingProcess">
<mat-option *ngFor="let trackingProcess of notificationTrackingProcessEnumValues" [value]="trackingProcess">{{enumUtils.toNotificationTrackingProcessString(trackingProcess)}}</mat-option>
@ -75,11 +94,14 @@
</div>
</div>
<div class="d-flex justify-content-between mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
</div>
<div class="col-auto">
<button class="normal-btn-sm" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>

View File

@ -1,11 +1,11 @@
// ::ng-deep.mat-mdc-menu-panel {
// max-width: 100% !important;
// height: 100% !important;
// }
::ng-deep.mat-mdc-menu-panel {
max-width: 100% !important;
height: 100% !important;
}
// :host::ng-deep.mat-mdc-menu-content:not(:empty) {
// padding-top: 0 !important;
// }
:host::ng-deep.mat-mdc-menu-content:not(:empty) {
padding-top: 0 !important;
}
.filter-button{
@ -18,4 +18,8 @@
// }
}
::ng-deep .mdc-form-field {
label {
margin: 0;
}
}

View File

@ -2,7 +2,7 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { NotificationFilter } from '@notification-service/core/query/notification.lookup';
import { UserService } from '@app/core/services/user/user.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { NotificationServiceEnumUtils } from '@notification-service/core/formatting/enum-utils.service';
import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
import { BaseComponent } from '@common/base/base.component';
import { Guid } from '@common/types/guid';
@ -37,7 +37,7 @@ export class NotificationListingFiltersComponent extends BaseComponent implement
protected appliedFilterCount: number = 0;
constructor(
public enumUtils: EnumUtils,
public enumUtils: NotificationServiceEnumUtils,
private userService: UserService,
) { super(); }

View File

@ -99,9 +99,9 @@ export class InAppNotificationEditorComponent extends BaseComponent implements O
maxWidth: '300px',
restoreFocus: false,
data: {
message: this.language.instant('COMMONS.CONFIRMATION-DIALOG.DELETE-ITEM'),
confirmButton: this.language.instant('COMMONS.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('COMMONS.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'),
confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {

View File

@ -15,14 +15,12 @@
<mat-divider inset *ngIf="!last"></mat-divider>
</div>
</a>
<mat-list-item *ngIf="authService.hasPermission(authService.permissionEnum.ViewMineInAppNotificationPage)">
<a (click)="goToNotifications()">{{'NAV-BAR.INAPP-NOTIFICATIONS'
| translate}}</a>
</mat-list-item>
<mat-list-item *ngIf="authService.hasPermission(authService.permissionEnum.ViewMineInAppNotificationPage)">
<a (click)="readAllNotifications()">{{'NAV-BAR.READ-ALL-INAPP-NOTIFICATIONS'
| translate}}</a>
</mat-list-item>
<a *ngIf="authService.hasPermission(authService.permissionEnum.ViewMineInAppNotificationPage)" (click)="goToNotifications()">
<mat-list-item>{{'NAV-BAR.INAPP-NOTIFICATIONS' | translate}}</mat-list-item>
</a>
<a *ngIf="authService.hasPermission(authService.permissionEnum.ViewMineInAppNotificationPage)" (click)="readAllNotifications()">
<mat-list-item>{{'NAV-BAR.READ-ALL-INAPP-NOTIFICATIONS' | translate}}</mat-list-item>
</a>
</mat-nav-list>
</div>
</div>

View File

@ -40,7 +40,7 @@
</div>
</div>
<div class="row justify-content-end align-items-center mt-4 gap-1-rem">
<div class="row justify-content-end align-items-center mt-4 mb-1 gap-1-rem">
<div class="col-auto">
<button class="normal-btn-light-sm" (click)="filterMenuTrigger?.closeMenu()">
{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.CANCEL' | translate}}

View File

@ -16,7 +16,7 @@
<div class="row">
<div class="col"></div>
<div class="col-auto">
<button mat-raised-button color="primary" class="rounded-button" (click)="formSubmit()">
<button class="normal-btn-sm" (click)="formSubmit()">
{{'NOTIFICATION-SERVICE.USER-PROFILE.NOTIFIER-LIST-EDITOR.ACTIONS.SAVE' | translate}}
</button>
</div>

View File

@ -2,6 +2,7 @@ package gr.cite.notification.web.controllers;
import gr.cite.notification.audit.AuditableAction;
import gr.cite.notification.authorization.AuthorizationFlags;
import gr.cite.notification.common.enums.IsActive;
import gr.cite.notification.common.enums.TenantConfigurationType;
import gr.cite.notification.common.types.tenantconfiguration.NotifierListConfigurationDataContainer;
import gr.cite.notification.data.UserNotificationPreferenceEntity;
@ -18,6 +19,7 @@ import gr.cite.notification.web.model.QueryResult;
import gr.cite.tools.auditing.AuditService;
import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.censor.CensorFactory;
import gr.cite.tools.data.query.Ordering;
import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.exception.MyForbiddenException;
@ -80,15 +82,16 @@ public class UserNotificationPreferenceController {
@GetMapping("user/{userId}/current")
@Transactional
public UserNotificationPreference current(@PathVariable UUID userId, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
public List<UserNotificationPreference> current(@PathVariable UUID userId, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("retrieving" + UserNotificationPreference.class.getSimpleName()).And("userId", userId).And("fields", fieldSet));
this.censorFactory.censor(UserNotificationPreferenceCensor.class).censor(fieldSet, userId);
UserNotificationPreferenceQuery query = this.queryFactory.query(UserNotificationPreferenceQuery.class).userId(userId);
UserNotificationPreference model = this.builderFactory.builder(UserNotificationPreferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.firstAs(fieldSet));
if (model == null)
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userId, TenantConfiguration.class.getSimpleName()}, LocaleContextHolder.getLocale()));
Ordering ordering = new Ordering();
ordering.addAscending(UserNotificationPreference._ordinal);
UserNotificationPreferenceQuery query = this.queryFactory.query(UserNotificationPreferenceQuery.class).userId(userId).isActives(IsActive.Active);
query.setOrder(ordering);
List<UserNotificationPreference> model = this.builderFactory.builder(UserNotificationPreferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.collectAs(fieldSet));
this.auditService.track(AuditableAction.User_Notification_Preference_Lookup, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("userId", userId),

View File

@ -3,7 +3,7 @@ notification:
fields:
- key: "{installation-url}"
type: "String"
value: "http://localhost:42000"
value: "http://localhost:4200"
resolver:
global-policies:
- #dmpInvitationExternalUser
@ -14,16 +14,16 @@ notification:
contacts: [ inapp, email ]
- #dpmModified
type: 4542262A-22F8-4BAA-9DB6-1C8E70AC1DBB
contacts: [ email ]
contacts: [ inapp, email ]
- #dmpFinalised
type: 90DB0B46-42DE-BD89-AEBF-6F27EFEB256E
contacts: [ email ]
contacts: [ inapp, email ]
- #descriptionModified
type: 4FDBFA80-7A71-4A69-B854-67CBB70648F1
contacts: [ email ]
contacts: [ inapp, email ]
- #descriptionFinalised
type: 33790bad-94d4-488a-8ee2-7f6295ca18ea
contacts: [ email ]
contacts: [ inapp, email ]
- #mergeAcountConfirmation
type: BFE68845-CB05-4C5A-A03D-29161A7C9660
contacts: [ email ]
@ -32,10 +32,10 @@ notification:
contacts: [ email ]
- #dmpDeposit
type: 55736F7A-83AB-4190-AF43-9D031A6F9612
contacts: [ email ]
contacts: [ inapp, email ]
- #descriptionTemplateInvitation
type: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B
contacts: [ email ]
contacts: [ inapp, email ]
- #contactSupportType
type: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD
contacts: [ email ]

View File

@ -2,7 +2,7 @@ notification:
task:
processor:
enable: true
interval-seconds: 3
interval-seconds: 5
options:
retry-threshold: 300
max-retry-delay-seconds: 10800

View File

@ -27,7 +27,7 @@ queue:
exchange: null
rabbitmq:
enable: false
interval-seconds: 30
interval-seconds: 5
options:
retry-threashold: 100
retry-delay-step-seconds: 300
@ -45,7 +45,7 @@ queue:
user-touched-topic: user.touch
rabbitmq:
enable: false
interval-seconds: 30
interval-seconds: 5
options:
retry-threashold: 100
retry-delay-step-seconds: 300

View File

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body class="">
<p>Dear {recipient},</p>
<p>{reasonName} just finalised the Description {name}.</p>
<a href="{installation-url}/descriptions/edit/{id}" target="_blank">Click here to view it.</a>
</body>
</html>

View File

@ -3,302 +3,10 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just made changes to the Description {name}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/descriptions/edit/{id}" target="_blank">Click here to view it.</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
<a href="{installation-url}/descriptions/edit/{id}" target="_blank">Click here to view it.</a>
</body>
</html>

View File

@ -3,303 +3,11 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>You have been invited to co-develop the Template {templateName}.</p>
<p>Click the button to redirect to {templateName}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/description-templates/{templateID}" target="_blank">{templateName}</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
<a href="{installation-url}/description-templates/{templateID}" target="_blank">{templateName}</a>
</body>
</html>

View File

@ -1,304 +0,0 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just finalised the Description {name}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/descriptions/edit/{id}" target="_blank">Click here to view it.</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -3,302 +3,10 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just publish the {name}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/{path}/{id}" target="_blank">Click here to view it.</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
<a href="{installation-url}/{path}/{id}" target="_blank">Click here to view it.</a>
</body>
</html>

View File

@ -3,302 +3,10 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just finalised the Dmp {name}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/plans/edit/{id}" target="_blank">Click here to view it.</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
<a href="{installation-url}/plans/edit/{id}" target="_blank">Click here to view it.</a>
</body>
</html>

View File

@ -4,301 +4,11 @@
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just add you to collaborate to Data Management plan {dmpname} with role {dmprole}.</p>
<p>Click the button to redirect to {dmpname}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/plans/edit/{id}" target="_blank">Join</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
<a href="{installation-url}/plans/edit/{id}" target="_blank">Join</a>
</body>
</html>

View File

@ -3,302 +3,11 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just made changes to the Dmp {name}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/plans/edit/{id}" target="_blank">Click here to view it.</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
<a href="{installation-url}/plans/edit/{id}" target="_blank">Click here to view it.</a>
</table>
</body>
</html>

View File

@ -8,6 +8,7 @@ import gr.cite.tools.data.query.FieldResolver;
import gr.cite.tools.data.query.QueryBase;
import gr.cite.tools.data.query.QueryContext;
import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
@ -89,15 +90,20 @@ public class UserNotificationPreferenceQuery extends QueryBase<UserNotificationP
}
if (this.isActives != null) {
predicates.add(queryContext.Root.get(UserNotificationPreferenceEntity._isActive).in(isActives));
}
CriteriaBuilder.In<IsActive> inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UserNotificationPreferenceEntity._isActive));
for (IsActive item : this.isActives)
inClause.value(item);
predicates.add(inClause); }
if (this.type != null) {
predicates.add(queryContext.Root.get(UserNotificationPreferenceEntity._type).in(this.type));
}
if (this.channel != null) {
predicates.add(queryContext.Root.get(UserNotificationPreferenceEntity._channel).in(this.channel));
CriteriaBuilder.In<NotificationContactType> inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UserNotificationPreferenceEntity._channel));
for (NotificationContactType item : this.channel)
inClause.value(item);
predicates.add(inClause);
}
if (!predicates.isEmpty()) {

View File

@ -1,6 +1,7 @@
package gr.cite.notification.service.inappnotification;
import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.notification.authorization.OwnedResource;
import gr.cite.notification.authorization.Permission;
import gr.cite.notification.common.enums.NotificationInAppTracking;
import gr.cite.notification.common.scope.user.UserScope;
@ -92,7 +93,7 @@ public class InAppNotificationServiceImpl implements InAppNotificationService {
public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException {
logger.debug("deleting in-app notification: {}", id);
this.authService.authorizeForce(Permission.DeleteInAppNotification);
this.authService.authorizeAtLeastOneForce(this.userScope.getUserId() != null ? List.of(new OwnedResource(this.userScope.getUserId())) : null, Permission.DeleteInAppNotification);
this.deleterFactory.deleter(InAppNotificationDeleter.class).deleteAndSaveByIds(List.of(id));
}
}

View File

@ -70,7 +70,7 @@ public class InAppMessageBuilder extends MessageBuilderBase implements MessageBu
}
NotificationProperties.Flow options = this.flowMap.get("in-app").getOrDefault(notification.getType(), null);
NotificationTemplate template = notificationTemplateService.lookupOverriddenTemplates(notification.getType(), NotificationTemplateChannel.Email, messageInfo.getLanguage());
NotificationTemplate template = notificationTemplateService.lookupOverriddenTemplates(notification.getType(), NotificationTemplateChannel.InApp, messageInfo.getLanguage());
if (options == null && template == null) {
logger.error("Could not retrieve flow options for notification " + notification.getId() + " of type " + notification.getType());

View File

@ -165,7 +165,6 @@ public abstract class MessageBuilderBase {
return formatting;
}
//TODO: Here check with a language accepted list and fallback to default
protected String lookupOrReadLocalizedFile(NotificationProperties.Template.TemplateCache templateCache, String path, String language) {
String filename = path.replace("{language}", language);
File file = null;

View File

@ -10,13 +10,16 @@ import gr.cite.notification.convention.ConventionService;
import gr.cite.notification.data.TenantEntity;
import gr.cite.notification.data.TenantEntityManager;
import gr.cite.notification.data.NotificationEntity;
import gr.cite.notification.data.UserNotificationPreferenceEntity;
import gr.cite.notification.errorcode.ErrorThesaurusProperties;
import gr.cite.notification.model.SendNotificationResult;
import gr.cite.notification.model.UserNotificationPreference;
import gr.cite.notification.model.builder.NotificationBuilder;
import gr.cite.notification.model.Notification;
import gr.cite.notification.model.deleter.NotificationDeleter;
import gr.cite.notification.model.persist.NotificationPersist;
import gr.cite.notification.query.NotificationQuery;
import gr.cite.notification.query.UserNotificationPreferenceQuery;
import gr.cite.notification.service.channelResolution.ChannelResolutionService;
import gr.cite.notification.service.contact.extractor.ContactExtractorFactory;
import gr.cite.notification.service.contact.model.Contact;
@ -25,6 +28,7 @@ import gr.cite.notification.service.message.model.Message;
import gr.cite.notification.service.notify.NotifierFactory;
import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.deleter.DeleterFactory;
import gr.cite.tools.data.query.Ordering;
import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.exception.MyForbiddenException;
@ -49,6 +53,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequestScope
@ -141,7 +146,9 @@ public class NotificationServiceImpl implements NotificationService {
}
public SendNotificationResult doNotify(NotificationEntity notification) {
List<NotificationContactType> contactTypes = this.orderContactTypes(notification);
List<NotificationContactType> contactTypes = this.orderContactTypesFromPreferences(notification);
if (this.conventionService.isListNullOrEmpty(contactTypes)) contactTypes = this.orderContactTypes(notification);
for (NotificationContactType contactType: contactTypes) {
SendNotificationResult result = this.sendNotification(notification, contactType);
if (result.getSuccess()) return result;
@ -149,6 +156,17 @@ public class NotificationServiceImpl implements NotificationService {
return null;
}
private List<NotificationContactType> orderContactTypesFromPreferences(NotificationEntity notification) {
Ordering ordering = new Ordering();
ordering.addAscending(UserNotificationPreference._ordinal);
UserNotificationPreferenceQuery query = this.queryFactory.query(UserNotificationPreferenceQuery.class).userId(notification.getUserId()).type(notification.getType()).isActives(IsActive.Active);
query.setOrder(ordering);
List<UserNotificationPreferenceEntity> preferences = query.collectAs(new BaseFieldSet().ensure(UserNotificationPreference._channel));
if (!this.conventionService.isListNullOrEmpty(preferences)) return preferences.stream().map(x -> x.getChannel()).collect(Collectors.toList());
return null;
}
private List<NotificationContactType> orderContactTypes(NotificationEntity notification) {
List<NotificationContactType> contactTypes = this.channelResolutionService.resolve(notification.getType(), notification.getUserId());
if (notification.getContactTypeHint() == null) return contactTypes;

View File

@ -141,8 +141,8 @@ public class NotificationSchedulingServiceImpl implements NotificationScheduling
} else {
tenantScope.setTempTenant(entityManager, null, tenantScope.getDefaultTenantCode());
}
notification = entityManager.merge(notification);
entityManager.persist(notification);
// notification = entityManager.merge(notification);
entityManager.merge(notification);
entityManager.flush();
} finally {
tenantScope.removeTempTenant(entityManager);

View File

@ -70,6 +70,7 @@ public class InAppNotifier implements Notify{
inApp.setTenantId(tenantScope.getTenant());
entityManager.persist(inApp);
entityManager.flush();
InAppTrackingData trackingData = new InAppTrackingData(inApp.getId());
data = this.jsonHandlingService.toJsonSafe(trackingData);

View File

@ -1,7 +1,9 @@
package gr.cite.notification.service.userNotificationPreference;
import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.notification.authorization.OwnedResource;
import gr.cite.notification.authorization.Permission;
import gr.cite.notification.common.enums.IsActive;
import gr.cite.notification.common.enums.NotificationContactType;
import gr.cite.notification.common.types.tenantconfiguration.NotifierListConfigurationDataContainer;
import gr.cite.notification.config.notification.NotificationConfig;
@ -66,7 +68,7 @@ public class UserNotificationPreferenceServiceImpl implements UserNotificationPr
public List<UserNotificationPreference> persist(UserNotificationPreferencePersist model, FieldSet fieldSet) {
logger.debug(new MapLogEntry("persisting").And("model", model).And("fields", fieldSet));
this.authService.authorizeForce(Permission.EditUserNotificationPreference);
this.authService.authorizeAtLeastOneForce(model.getUserId() != null ? List.of(new OwnedResource(model.getUserId())) : null, Permission.EditUserNotificationPreference);
Map<UUID, List<NotificationContactType>> currentNotificationListPolicies;
NotifierListConfigurationDataContainer tenantNotifierListPolicies = this.tenantConfigurationService.collectTenantNotifierList();
@ -169,6 +171,7 @@ public class UserNotificationPreferenceServiceImpl implements UserNotificationPr
preferences = this.queryFactory
.query(UserNotificationPreferenceQuery.class)
.type(type)
.isActives(IsActive.Active)
.userId(userId).collect();
int ordinal = 0;
@ -176,6 +179,7 @@ public class UserNotificationPreferenceServiceImpl implements UserNotificationPr
for (NotificationContactType contactType : contactTypes) {
UserNotificationPreferenceEntity preference = preferences.stream().filter(x -> x.getChannel() == contactType).findFirst().orElse(null);
boolean isUpdate = preference != null;
if (preference != null) {
preference.setOrdinal(ordinal);
@ -186,9 +190,12 @@ public class UserNotificationPreferenceServiceImpl implements UserNotificationPr
preference.setOrdinal(ordinal);
preference.setChannel(contactType);
preference.setCreatedAt(Instant.now());
preference.setIsActive(IsActive.Active);
}
this.entityManager.merge(preference);
this.entityManager.persist(preference);
preference.setUpdatedAt(Instant.now());
if(isUpdate) this.entityManager.merge(preference);
else this.entityManager.persist(preference);
updatedPreferences.add(preference);
ordinal++;
}