fix lock and add entity locks admin page

This commit is contained in:
amentis 2024-03-21 09:46:18 +02:00
parent 48a310e561
commit ba33b29e41
49 changed files with 1021 additions and 196 deletions

View File

@ -125,6 +125,8 @@ public class AuditableAction {
public static final EventId Lock_Delete = new EventId(17003, "Lock_Delete"); public static final EventId Lock_Delete = new EventId(17003, "Lock_Delete");
public static final EventId Lock_IsLocked = new EventId(17004, "Lock_IsLocked"); public static final EventId Lock_IsLocked = new EventId(17004, "Lock_IsLocked");
public static final EventId Lock_UnLocked = new EventId(17005, "Lock_UnLocked"); public static final EventId Lock_UnLocked = new EventId(17005, "Lock_UnLocked");
public static final EventId Lock_Touched = new EventId(17006, "Lock_Touched");
public static final EventId Lock_Locked = new EventId(17007, "Lock_Locked");
public static final EventId Deposit_GetAvailableRepositories = new EventId(18000, "Deposit_GetAvailableRepositories"); public static final EventId Deposit_GetAvailableRepositories = new EventId(18000, "Deposit_GetAvailableRepositories");
public static final EventId Deposit_GetAccessToken = new EventId(18001, "Deposit_GetAccessToken"); public static final EventId Deposit_GetAccessToken = new EventId(18001, "Deposit_GetAccessToken");

View File

@ -7,7 +7,9 @@ import java.util.Map;
public enum LockTargetType implements DatabaseEnum<Short> { public enum LockTargetType implements DatabaseEnum<Short> {
Dmp((short) 0), Dmp((short) 0),
Decription((short) 1); Description((short) 1),
DmpBlueprint((short) 2),
DescriptionTemplate((short) 3);
private final Short value; private final Short value;
LockTargetType(Short value) { LockTargetType(Short value) {

View File

@ -0,0 +1,20 @@
package eu.eudat.configurations.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(LockProperties.class)
public class LockConfiguration {
private final LockProperties properties;
@Autowired
public LockConfiguration(LockProperties properties) {
this.properties = properties;
}
public LockProperties getProperties() {
return properties;
}
}

View File

@ -0,0 +1,17 @@
package eu.eudat.configurations.lock;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lock")
public class LockProperties {
private Integer lockInterval;
public Integer getLockInterval() {
return lockInterval;
}
public void setLockInterval(Integer lockInterval) {
this.lockInterval = lockInterval;
}
}

View File

@ -0,0 +1,24 @@
package eu.eudat.model;
public class LockStatus {
Boolean status;
Lock lock;
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
public Lock getLock() {
return lock;
}
public void setLock(Lock lock) {
this.lock = lock;
}
}

View File

@ -31,6 +31,14 @@ public class LockPersist {
public static final String _hash = "hash"; public static final String _hash = "hash";
public LockPersist() {
}
public LockPersist(UUID target, LockTargetType targetType) {
this.target = target;
this.targetType = targetType;
}
public UUID getId() { public UUID getId() {
return id; return id;
} }

View File

@ -21,6 +21,7 @@ import java.util.*;
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class LockQuery extends QueryBase<LockEntity> { public class LockQuery extends QueryBase<LockEntity> {
private String like;
private Collection<UUID> ids; private Collection<UUID> ids;
private Collection<UUID> targetIds; private Collection<UUID> targetIds;
@ -30,8 +31,15 @@ public class LockQuery extends QueryBase<LockEntity> {
private Collection<UUID> excludedIds; private Collection<UUID> excludedIds;
private Collection<UUID> userIds;
private EnumSet<AuthorizationFlags> authorize = EnumSet.of(AuthorizationFlags.None); private EnumSet<AuthorizationFlags> authorize = EnumSet.of(AuthorizationFlags.None);
public LockQuery like(String value) {
this.like = value;
return this;
}
public LockQuery ids(UUID value) { public LockQuery ids(UUID value) {
this.ids = List.of(value); this.ids = List.of(value);
return this; return this;
@ -107,6 +115,21 @@ public class LockQuery extends QueryBase<LockEntity> {
return this; return this;
} }
public LockQuery userIds(UUID value) {
this.userIds = List.of(value);
return this;
}
public LockQuery userIds(UUID... value) {
this.userIds = Arrays.asList(value);
return this;
}
public LockQuery userIds(Collection<UUID> values) {
this.userIds = values;
return this;
}
public LockQuery authorize(EnumSet<AuthorizationFlags> values) { public LockQuery authorize(EnumSet<AuthorizationFlags> values) {
this.authorize = values; this.authorize = values;
return this; return this;
@ -128,6 +151,9 @@ public class LockQuery extends QueryBase<LockEntity> {
@Override @Override
protected <X, Y> Predicate applyFilters(QueryContext<X, Y> queryContext) { protected <X, Y> Predicate applyFilters(QueryContext<X, Y> queryContext) {
List<Predicate> predicates = new ArrayList<>(); List<Predicate> predicates = new ArrayList<>();
if (this.like != null && !this.like.isEmpty()) {
predicates.add(queryContext.CriteriaBuilder.like(queryContext.Root.get(LockEntity._id), this.like));
}
if (this.ids != null) { if (this.ids != null) {
CriteriaBuilder.In<UUID> inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._id)); CriteriaBuilder.In<UUID> inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._id));
for (UUID item : this.ids) for (UUID item : this.ids)
@ -158,6 +184,12 @@ public class LockQuery extends QueryBase<LockEntity> {
notInClause.value(item); notInClause.value(item);
predicates.add(notInClause.not()); predicates.add(notInClause.not());
} }
if (this.userIds != null) {
CriteriaBuilder.In<UUID> inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._lockedBy));
for (UUID item : this.userIds)
inClause.value(item);
predicates.add(inClause);
}
if (!predicates.isEmpty()) { if (!predicates.isEmpty()) {
Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); Predicate[] predicatesArray = predicates.toArray(new Predicate[0]);
return queryContext.CriteriaBuilder.and(predicatesArray); return queryContext.CriteriaBuilder.and(predicatesArray);

View File

@ -11,6 +11,8 @@ import java.util.UUID;
public class LockLookup extends Lookup { public class LockLookup extends Lookup {
private String like;
private List<UUID> ids; private List<UUID> ids;
private List<UUID> targetIds; private List<UUID> targetIds;
@ -18,6 +20,16 @@ public class LockLookup extends Lookup {
private List<LockTargetType> targetTypes; private List<LockTargetType> targetTypes;
private List<UUID> excludedIds; private List<UUID> excludedIds;
private List<UUID> userIds;
public String getLike() {
return like;
}
public void setLike(String like) {
this.like = like;
}
public List<UUID> getIds() { public List<UUID> getIds() {
return ids; return ids;
@ -51,12 +63,22 @@ public class LockLookup extends Lookup {
this.targetTypes = targetTypes; this.targetTypes = targetTypes;
} }
public List<UUID> getUserIds() {
return userIds;
}
public void setUserIds(List<UUID> userIds) {
this.userIds = userIds;
}
public LockQuery enrich(QueryFactory queryFactory) { public LockQuery enrich(QueryFactory queryFactory) {
LockQuery query = queryFactory.query(LockQuery.class); LockQuery query = queryFactory.query(LockQuery.class);
if (this.like != null) query.like(this.like);
if (this.ids != null) query.ids(this.ids); if (this.ids != null) query.ids(this.ids);
if (this.targetIds != null) query.targetIds(this.targetIds); if (this.targetIds != null) query.targetIds(this.targetIds);
if (this.targetTypes != null) query.targetTypes(this.targetTypes); if (this.targetTypes != null) query.targetTypes(this.targetTypes);
if (this.excludedIds != null) query.excludedIds(this.excludedIds); if (this.excludedIds != null) query.excludedIds(this.excludedIds);
if (this.userIds != null) query.userIds(this.userIds);
this.enrichCommon(query); this.enrichCommon(query);

View File

@ -1,6 +1,8 @@
package eu.eudat.service.lock; package eu.eudat.service.lock;
import eu.eudat.commons.enums.LockTargetType;
import eu.eudat.model.Lock; import eu.eudat.model.Lock;
import eu.eudat.model.LockStatus;
import eu.eudat.model.persist.LockPersist; import eu.eudat.model.persist.LockPersist;
import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.exception.MyForbiddenException; import gr.cite.tools.exception.MyForbiddenException;
@ -15,7 +17,11 @@ public interface LockService {
Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException;
boolean isLocked(UUID target) throws InvalidApplicationException; LockStatus isLocked(UUID target, FieldSet fields) throws InvalidApplicationException;
void lock(UUID target, LockTargetType targetType) throws InvalidApplicationException;
void touch(UUID target) throws InvalidApplicationException;
void unlock(UUID target) throws InvalidApplicationException; void unlock(UUID target) throws InvalidApplicationException;

View File

@ -4,11 +4,14 @@ import eu.eudat.authorization.AffiliatedResource;
import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.AuthorizationFlags;
import eu.eudat.authorization.Permission; import eu.eudat.authorization.Permission;
import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver; import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver;
import eu.eudat.commons.enums.LockTargetType;
import eu.eudat.commons.scope.user.UserScope; import eu.eudat.commons.scope.user.UserScope;
import eu.eudat.configurations.lock.LockProperties;
import eu.eudat.convention.ConventionService; import eu.eudat.convention.ConventionService;
import eu.eudat.data.LockEntity; import eu.eudat.data.LockEntity;
import eu.eudat.errorcode.ErrorThesaurusProperties; import eu.eudat.errorcode.ErrorThesaurusProperties;
import eu.eudat.model.Lock; import eu.eudat.model.Lock;
import eu.eudat.model.LockStatus;
import eu.eudat.model.builder.LockBuilder; import eu.eudat.model.builder.LockBuilder;
import eu.eudat.model.deleter.LockDeleter; import eu.eudat.model.deleter.LockDeleter;
import eu.eudat.model.persist.LockPersist; import eu.eudat.model.persist.LockPersist;
@ -54,18 +57,19 @@ public class LockServiceImpl implements LockService {
private final MessageSource messageSource; private final MessageSource messageSource;
private final ErrorThesaurusProperties errors; private final ErrorThesaurusProperties errors;
private final AuthorizationContentResolver authorizationContentResolver; private final AuthorizationContentResolver authorizationContentResolver;
private final LockProperties lockProperties;
@Autowired @Autowired
public LockServiceImpl( public LockServiceImpl(
EntityManager entityManager, EntityManager entityManager,
UserScope userScope, UserScope userScope,
AuthorizationService authorizationService, AuthorizationService authorizationService,
DeleterFactory deleterFactory, DeleterFactory deleterFactory,
BuilderFactory builderFactory, BuilderFactory builderFactory,
QueryFactory queryFactory, QueryFactory queryFactory,
ConventionService conventionService, ConventionService conventionService,
MessageSource messageSource, MessageSource messageSource,
ErrorThesaurusProperties errors, AuthorizationContentResolver authorizationContentResolver) { ErrorThesaurusProperties errors, AuthorizationContentResolver authorizationContentResolver, LockProperties lockProperties) {
this.entityManager = entityManager; this.entityManager = entityManager;
this.userScope = userScope; this.userScope = userScope;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
@ -76,6 +80,7 @@ public class LockServiceImpl implements LockService {
this.messageSource = messageSource; this.messageSource = messageSource;
this.errors = errors; this.errors = errors;
this.authorizationContentResolver = authorizationContentResolver; this.authorizationContentResolver = authorizationContentResolver;
this.lockProperties = lockProperties;
} }
public Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { public Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException {
@ -112,71 +117,57 @@ public class LockServiceImpl implements LockService {
return this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Lock._id), data); return this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Lock._id), data);
} }
public boolean isLocked(UUID target) throws InvalidApplicationException { public LockStatus isLocked(UUID target, FieldSet fields) throws InvalidApplicationException {
LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); LockStatus lockStatus = new LockStatus();
if (query.count() == 1) { LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first();
LockEntity lock = query.first();
if (lock.getLockedBy().equals(this.userScope.getUserId())) { if (lock == null) {
lock.setTouchedAt(Instant.now()); lockStatus.setStatus(false);
this.entityManager.merge(lock); return lockStatus;
this.entityManager.flush(); }
return false;
} if (lock.getLockedBy().equals(this.userScope.getUserId())) lockStatus.setStatus(false);
return this.forceUnlock(target) > 0; else {
} else if (query.count() > 1) { if (new Date().getTime() - Date.from(lock.getTouchedAt()).getTime() > lockProperties.getLockInterval()) {
this.forceUnlock(target); lockStatus.setStatus(false);
return this.isLocked(target); this.deleteAndSave(lock.getId());
} else lockStatus.setStatus(true);
}
lockStatus.setLock(this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Lock._id), lock));
return lockStatus;
}
public void lock(UUID target, LockTargetType targetType) throws InvalidApplicationException {
LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first();
if (lock == null) {
this.persist(new LockPersist(target, targetType), null);
}else{
if (!lock.getLockedBy().equals(this.userScope.getUserId())) throw new MyApplicationException("Entity is already locked");
this.touch(target);
} }
return false;
} }
private Long forceUnlock(UUID target) throws InvalidApplicationException { public void touch(UUID target) throws InvalidApplicationException {
LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first();
LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); if (lock == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{target, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale()));
Long availableLocks = query.count(); if (!lock.getLockedBy().equals(this.userScope.getUserId())) throw new MyApplicationException("Only the user who created that lock can touch it");
long deletedLocks = 0L;
if (availableLocks > 0) { lock.setTouchedAt(Instant.now());
List<LockEntity> locks = query.collect(); this.entityManager.merge(lock);
for (LockEntity lock : locks) { this.entityManager.flush();
if (new Date().getTime() - Date.from(lock.getTouchedAt()).getTime() > 120000) {
this.deleteAndSave(lock.getId());
deletedLocks++;
}
}
if (deletedLocks == 0) {
LockEntity recentLock = locks.stream().max(compareByTouchedAt).get();
for (LockEntity lock : locks) {
if (lock != recentLock) {
this.deleteAndSave(lock.getId());
deletedLocks++;
}
}
}
}
return availableLocks - deletedLocks;
} }
public void unlock(UUID target) throws InvalidApplicationException { public void unlock(UUID target) throws InvalidApplicationException {
LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first();
LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); if (lock == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{target, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (query.count() == 1) { if (!lock.getLockedBy().equals(this.userScope.getUserId())) {
LockEntity lock = query.first(); throw new InvalidApplicationException("Only the user who created that lock can delete it");
if (!lock.getLockedBy().equals(this.userScope.getUserId())) {
throw new InvalidApplicationException("Only the user who created that lock can delete it");
}
this.deleteAndSave(lock.getId());
} else if (query.count() > 1) {
List<LockEntity> locks = query.collect();
locks.stream().filter(lock -> lock.getLockedBy().equals(this.userScope.getUserIdSafe())).forEach(lock -> {
try {
this.deleteAndSave(lock.getId());
} catch (InvalidApplicationException e) {
throw new RuntimeException(e);
}
});
} }
this.deleteAndSave(lock.getId());
} }
public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException {

View File

@ -5,6 +5,8 @@ import eu.eudat.authorization.AffiliatedResource;
import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.AuthorizationFlags;
import eu.eudat.authorization.Permission; import eu.eudat.authorization.Permission;
import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver; import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver;
import eu.eudat.commons.enums.LockTargetType;
import eu.eudat.model.LockStatus;
import gr.cite.tools.validation.ValidationFilterAnnotation; import gr.cite.tools.validation.ValidationFilterAnnotation;
import eu.eudat.data.LockEntity; import eu.eudat.data.LockEntity;
import eu.eudat.model.Lock; import eu.eudat.model.Lock;
@ -155,15 +157,45 @@ public class LockController {
@Transactional @Transactional
@GetMapping("target/status/{id}") @GetMapping("target/status/{id}")
public Boolean getLocked(@PathVariable("id") UUID targetId) throws Exception { public LockStatus getLocked(@PathVariable("id") UUID targetId, FieldSet fieldSet) throws Exception {
logger.debug(new MapLogEntry("is locked" + Lock.class.getSimpleName()).And("targetId", targetId)); logger.debug(new MapLogEntry("is locked" + Lock.class.getSimpleName()).And("targetId", targetId).And("fields", fieldSet));
this.authService.authorizeForce(Permission.BrowseLock); this.authService.authorizeForce(Permission.BrowseLock);
Boolean isLocked = this.lockService.isLocked(targetId); LockStatus lockStatus = this.lockService.isLocked(targetId, fieldSet);
this.auditService.track(AuditableAction.Lock_IsLocked, Map.ofEntries( this.auditService.track(AuditableAction.Lock_IsLocked, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("targetId", targetId),
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
));
return lockStatus;
}
@Transactional
@GetMapping("target/lock/{id}/{targetType}")
public boolean lock(@PathVariable("id") UUID targetId, @PathVariable("targetType") int targetType) throws Exception {
AffiliatedResource affiliatedResourceDmp = this.authorizationContentResolver.dmpAffiliation(targetId);
AffiliatedResource affiliatedResourceDescription = this.authorizationContentResolver.descriptionAffiliation(targetId);
this.authService.authorizeAtLeastOneForce(List.of(affiliatedResourceDmp, affiliatedResourceDescription), Permission.EditLock);
this.lockService.lock(targetId, LockTargetType.of((short) targetType));
this.auditService.track(AuditableAction.Lock_Locked, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("targetId", targetId),
new AbstractMap.SimpleEntry<String, Object>("targetType", targetType)
));
return true;
}
@Transactional
@DeleteMapping("target/touch/{id}")
public boolean touch(@PathVariable("id") UUID targetId) throws Exception {
AffiliatedResource affiliatedResourceDmp = this.authorizationContentResolver.dmpAffiliation(targetId);
AffiliatedResource affiliatedResourceDescription = this.authorizationContentResolver.descriptionAffiliation(targetId);
this.authService.authorizeAtLeastOneForce(List.of(affiliatedResourceDmp, affiliatedResourceDescription), Permission.EditLock);
this.lockService.touch(targetId);
this.auditService.track(AuditableAction.Lock_Touched, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("targetId", targetId) new AbstractMap.SimpleEntry<String, Object>("targetId", targetId)
)); ));
return isLocked; return true;
} }
@Transactional @Transactional

View File

@ -30,5 +30,7 @@ spring:
optional:classpath:config/locale.yml[.yml], optional:classpath:config/locale-${spring.profiles.active}.yml[.yml], optional:file:../config/locale-${spring.profiles.active}.yml[.yml], optional:classpath:config/locale.yml[.yml], optional:classpath:config/locale-${spring.profiles.active}.yml[.yml], optional:file:../config/locale-${spring.profiles.active}.yml[.yml],
optional:classpath:config/public-api.yml[.yml], optional:classpath:config/public-api-${spring.profiles.active}.yml[.yml], optional:file:../config/public-api-${spring.profiles.active}.yml[.yml], optional:classpath:config/public-api.yml[.yml], optional:classpath:config/public-api-${spring.profiles.active}.yml[.yml], optional:file:../config/public-api-${spring.profiles.active}.yml[.yml],
optional:classpath:config/dashboard.yml[.yml], optional:classpath:config/dashboard-${spring.profiles.active}.yml[.yml], optional:file:../config/dashboard-${spring.profiles.active}.yml[.yml], optional:classpath:config/dashboard.yml[.yml], optional:classpath:config/dashboard-${spring.profiles.active}.yml[.yml], optional:file:../config/dashboard-${spring.profiles.active}.yml[.yml],
optional:classpath:config/transformer.yml[.yml], optional:classpath:config/transformer-${spring.profiles.active}.yml[.yml], optional:file:../config/transformer-${spring.profiles.active}.yml[.yml] optional:classpath:config/transformer.yml[.yml], optional:classpath:config/transformer-${spring.profiles.active}.yml[.yml], optional:file:../config/transformer-${spring.profiles.active}.yml[.yml],
optional:classpath:config/lock.yml[.yml], optional:classpath:config/lock-${spring.profiles.active}.yml[.yml], optional:file:../config/lock-${spring.profiles.active}.yml[.yml]

View File

@ -0,0 +1,2 @@
lock:
lockInterval: 120000

View File

@ -318,6 +318,18 @@ const appRoutes: Routes = [
}) })
}, },
}, },
{
path: 'entity-locks',
loadChildren: () => import('./ui/admin/entity-locks/lock.module').then(m => m.LockModule),
data: {
authContext: {
permissions: [AppPermission.ViewEntityLockPage]
},
...BreadcrumbService.generateRouteDataConfiguration({
title: 'BREADCRUMBS.ENTITY-LOCKS'
})
},
},
{ {
path: 'index-managment', path: 'index-managment',
loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule), loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule),

View File

@ -1,4 +1,6 @@
export enum LockTargetType { export enum LockTargetType {
Dmp = 0, Dmp = 0,
Description = 1 Description = 1,
DmpBlueprint = 2,
DescriptionTemplate= 3
} }

View File

@ -46,6 +46,7 @@ export enum AppPermission {
ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage", ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage",
ViewNotificationPage = "ViewNotificationPage", ViewNotificationPage = "ViewNotificationPage",
ViewPrefillingSourcePage = "ViewPrefillingSourcePage", ViewPrefillingSourcePage = "ViewPrefillingSourcePage",
ViewEntityLockPage = "ViewEntityLockPage",
//ReferenceType //ReferenceType
BrowseReferenceType = "BrowseReferenceType", BrowseReferenceType = "BrowseReferenceType",

View File

@ -19,3 +19,8 @@ export interface LockPersist extends BaseEntityPersist {
target: Guid; target: Guid;
targetType: LockTargetType; targetType: LockTargetType;
} }
export interface LockStatus {
status: Boolean;
lock: Lock;
}

View File

@ -3,19 +3,23 @@ import { Guid } from "@common/types/guid";
import { LockTargetType } from "../common/enum/lock-target-type"; import { LockTargetType } from "../common/enum/lock-target-type";
export class LockLookup extends Lookup implements LockLookup { export class LockLookup extends Lookup implements LockLookup {
like: string
ids: Guid[]; ids: Guid[];
excludedIds: Guid[]; excludedIds: Guid[];
targetIds: Guid[]; targetIds: Guid[];
targetTypes: LockTargetType[]; targetTypes: LockTargetType[];
userIds: Guid[];
constructor() { constructor() {
super(); super();
} }
} }
export interface LockLookup { export interface LockFilter {
like: string
ids: Guid[]; ids: Guid[];
excludedIds: Guid[]; excludedIds: Guid[];
targetIds: Guid[]; targetIds: Guid[];
targetTypes: LockTargetType[]; targetTypes: LockTargetType[];
userIds: Guid[];
} }

View File

@ -1,6 +1,6 @@
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Lock, LockPersist } from '@app/core/model/lock/lock.model'; import { Lock, LockPersist, LockStatus } from '@app/core/model/lock/lock.model';
import { LockLookup } from '@app/core/query/lock.lookup'; import { LockLookup } from '@app/core/query/lock.lookup';
import { QueryResult } from '@common/model/query-result'; import { QueryResult } from '@common/model/query-result';
import { FilterService } from '@common/modules/text-filter/filter-service'; import { FilterService } from '@common/modules/text-filter/filter-service';
@ -9,6 +9,7 @@ import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { ConfigurationService } from '../configuration/configuration.service'; import { ConfigurationService } from '../configuration/configuration.service';
import { BaseHttpV2Service } from '../http/base-http-v2.service'; import { BaseHttpV2Service } from '../http/base-http-v2.service';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
@Injectable() @Injectable()
export class LockService { export class LockService {
@ -50,13 +51,21 @@ export class LockService {
catchError((error: any) => throwError(error))); catchError((error: any) => throwError(error)));
} }
checkLockStatus(targetId: Guid): Observable<Boolean> { checkLockStatus(targetId: Guid, reqFields: string[] = []): Observable<LockStatus> {
return this.http.get<Boolean>(`${this.apiBase}/target/status/${targetId}`) const url = `${this.apiBase}/target/status/${targetId}`;
const options = { params: { f: reqFields } };
return this.http.get<LockStatus>(url, options)
.pipe(catchError((error: any) => throwError(error)));
}
lock(targetId: Guid, targetType: LockTargetType): Observable<Boolean> {
return this.http.get<Boolean>(`${this.apiBase}/target/lock/${targetId}/${targetType}`)
.pipe(catchError((error: any) => throwError(error))); .pipe(catchError((error: any) => throwError(error)));
} }
touchLock(targetId: Guid): Observable<Boolean> { touchLock(targetId: Guid): Observable<Boolean> {
return this.http.get<Boolean>(`${this.apiBase}/touch/${targetId}`) return this.http.get<Boolean>(`${this.apiBase}/target/touch/${targetId}`)
.pipe(catchError((error: any) => throwError(error))); .pipe(catchError((error: any) => throwError(error)));
} }

View File

@ -40,6 +40,7 @@ import { DmpBlueprintFieldCategory } from '@app/core/common/enum/dmp-blueprint-f
import { DmpUserType } from '@app/core/common/enum/dmp-user-type'; import { DmpUserType } from '@app/core/common/enum/dmp-user-type';
import { PrefillingSourceSystemTargetType } from '@app/core/common/enum/prefilling-source-system-target-type'; import { PrefillingSourceSystemTargetType } from '@app/core/common/enum/prefilling-source-system-target-type';
import { AnnotationProtectionType } from '@app/core/common/enum/annotation-protection-type.enum'; import { AnnotationProtectionType } from '@app/core/common/enum/annotation-protection-type.enum';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
@Injectable() @Injectable()
export class EnumUtils { export class EnumUtils {
@ -416,4 +417,13 @@ export class EnumUtils {
} }
} }
public toLockTargetTypeString(status: LockTargetType): string {
switch (status) {
case LockTargetType.Dmp: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DMP');
case LockTargetType.Description: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DESCRIPTION');
case LockTargetType.DmpBlueprint: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DMP-BLUEPRINT');
case LockTargetType.DescriptionTemplate: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DESCRIPTION-TEMPLATE');
}
}
} }

View File

@ -39,6 +39,9 @@ import { DescriptionTemplateEditorModel, DescriptionTemplateFieldEditorModel, De
import { DescriptionTemplateEditorResolver } from './description-template-editor.resolver'; import { DescriptionTemplateEditorResolver } from './description-template-editor.resolver';
import { DescriptionTemplateEditorService } from './description-template-editor.service'; import { DescriptionTemplateEditorService } from './description-template-editor.service';
import { NewEntryType, ToCEntry, ToCEntryType } from './table-of-contents/description-template-table-of-contents-entry'; import { NewEntryType, ToCEntry, ToCEntryType } from './table-of-contents/description-template-table-of-contents-entry';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
@Component({ @Component({
@ -104,8 +107,10 @@ export class DescriptionTemplateEditorComponent extends BaseEditor<DescriptionTe
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private descriptionTemplateService: DescriptionTemplateService, private descriptionTemplateService: DescriptionTemplateService,
public descriptionTemplateTypeService: DescriptionTemplateTypeService, public descriptionTemplateTypeService: DescriptionTemplateTypeService,
@ -116,7 +121,7 @@ export class DescriptionTemplateEditorComponent extends BaseEditor<DescriptionTe
private languageInfoService: LanguageInfoService, private languageInfoService: LanguageInfoService,
public userService: UserService public userService: UserService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {
@ -158,6 +163,8 @@ export class DescriptionTemplateEditorComponent extends BaseEditor<DescriptionTe
this.isDeleted = data ? data.isActive === IsActive.Inactive : false; this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.buildForm(); this.buildForm();
if (data && data.id) this.checkLock(data.id, LockTargetType.DescriptionTemplate);
setTimeout(() => { setTimeout(() => {
this.steps = this.stepper.steps; this.steps = this.stepper.steps;
}); });

View File

@ -23,6 +23,8 @@ import { map, takeUntil } from 'rxjs/operators';
import { DescriptionTemplateTypeEditorModel } from './description-template-type-editor.model'; import { DescriptionTemplateTypeEditorModel } from './description-template-type-editor.model';
import { DescriptionTemplateTypeEditorResolver } from './description-template-type-editor.resolver'; import { DescriptionTemplateTypeEditorResolver } from './description-template-type-editor.resolver';
import { DescriptionTemplateTypeEditorService } from './description-template-type-editor.service'; import { DescriptionTemplateTypeEditorService } from './description-template-type-editor.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
templateUrl: './description-template-type-editor.component.html', templateUrl: './description-template-type-editor.component.html',
@ -66,14 +68,16 @@ export class DescriptionTemplateTypeEditorComponent extends BaseEditor<Descripti
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private descriptionTemplateTypeService: DescriptionTemplateTypeService, private descriptionTemplateTypeService: DescriptionTemplateTypeService,
private logger: LoggingService, private logger: LoggingService,
private descriptionTemplateTypeEditorService: DescriptionTemplateTypeEditorService private descriptionTemplateTypeEditorService: DescriptionTemplateTypeEditorService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -43,6 +43,9 @@ import { DmpBlueprintEditorService } from './dmp-blueprint-editor.service';
import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service'; import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service';
import { SemanticsService } from '@app/core/services/semantic/semantics.service'; import { SemanticsService } from '@app/core/services/semantic/semantics.service';
import { PrefillingSourceService } from '@app/core/services/prefilling-source/prefilling-source.service'; import { PrefillingSourceService } from '@app/core/services/prefilling-source/prefilling-source.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
@Component({ @Component({
@ -117,8 +120,10 @@ export class DmpBlueprintEditorComponent extends BaseEditor<DmpBlueprintEditorMo
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private dmpBlueprintService: DmpBlueprintService, private dmpBlueprintService: DmpBlueprintService,
private logger: LoggingService, private logger: LoggingService,
@ -130,7 +135,7 @@ export class DmpBlueprintEditorComponent extends BaseEditor<DmpBlueprintEditorMo
public semanticsService: SemanticsService, public semanticsService: SemanticsService,
public prefillingSourceService: PrefillingSourceService public prefillingSourceService: PrefillingSourceService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {
@ -174,6 +179,9 @@ export class DmpBlueprintEditorComponent extends BaseEditor<DmpBlueprintEditorMo
this.editorModel = data ? new DmpBlueprintEditorModel().fromModel(data) : new DmpBlueprintEditorModel(); this.editorModel = data ? new DmpBlueprintEditorModel().fromModel(data) : new DmpBlueprintEditorModel();
this.isDeleted = data ? data.isActive === IsActive.Inactive : false; this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.buildForm(); this.buildForm();
if (data && data.id) this.checkLock(data.id, LockTargetType.DmpBlueprint);
} catch (error) { } catch (error) {
this.logger.error('Could not parse dmpBlueprint item: ' + data + error); this.logger.error('Could not parse dmpBlueprint item: ' + data + error);
this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error);

View File

@ -0,0 +1,51 @@
<div class="d-flex align-items-center gap-1-rem">
<button mat-flat-button [matMenuTriggerFor]="filterMenu" #filterMenuTrigger="matMenuTrigger" (click)="updateFilters()" class="filter-button">
<mat-icon aria-hidden="false" [matBadgeHidden]="!appliedFilterCount" [matBadge]="appliedFilterCount" matBadgeColor="warn" matBadgeSize="small">filter_alt</mat-icon>
{{'COMMONS.LISTING-COMPONENT.SEARCH-FILTER-BTN' | translate}}
</button>
<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">
<h4>{{'LOCK-LISTING.FILTER.TITLE' | translate}}</h4>
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
<div class="mt-4">
<div>
<mat-form-field class="col-12">
<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}}
<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()">
{{'LOCK-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'LOCK-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>
</div>
</div>
</mat-menu>
<app-expandable-search-field [(value)]=internalFilters.like (valueChange)="onSearchTermChange($event)" />
</div>

View File

@ -0,0 +1,21 @@
::ng-deep.mat-mdc-menu-panel {
max-width: 100% !important;
height: 100% !important;
}
:host::ng-deep.mat-mdc-menu-content:not(:empty) {
padding-top: 0 !important;
}
.filter-button{
padding-top: .6rem;
padding-bottom: .6rem;
// .mat-icon{
// font-size: 1.5em;
// width: 1.2em;
// height: 1.2em;
// }
}

View File

@ -0,0 +1,108 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
import { LockFilter } from '@app/core/query/lock.lookup';
import { UserService } from '@app/core/services/user/user.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { Guid } from '@common/types/guid';
import { nameof } from 'ts-simple-nameof';
@Component({
selector: 'app-lock-listing-filters',
templateUrl: './lock-listing-filters.component.html',
styleUrls: ['./lock-listing-filters.component.scss']
})
export class LockListingFiltersComponent extends BaseComponent implements OnInit, OnChanges {
@Input() readonly filter: LockFilter;
@Output() filterChange = new EventEmitter<LockFilter>();
lockTargetTypeEnumValues = this.enumUtils.getEnumValues<LockTargetType>(LockTargetType);
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
// * State
internalFilters: LockListingFilters = this._getEmptyFilters();
protected appliedFilterCount: number = 0;
constructor(
public enumUtils: EnumUtils,
public userService: UserService,
) { super(); }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
const filterChange = changes[nameof<LockListingFiltersComponent>(x => x.filter)]?.currentValue as LockFilter;
if (filterChange) {
this.updateFilters()
}
}
onSearchTermChange(searchTerm: string): void {
this.applyFilters()
}
protected updateFilters(): void {
this.internalFilters = this._parseToInternalFilters(this.filter);
this.appliedFilterCount = this._computeAppliedFilters(this.internalFilters);
}
protected applyFilters(): void {
const { targetTypes, like, userIds } = this.internalFilters ?? {}
this.filterChange.emit({
...this.filter,
targetTypes,
like,
userIds
})
}
private _parseToInternalFilters(inputFilter: LockFilter): LockListingFilters {
if (!inputFilter) {
return this._getEmptyFilters();
}
let { targetTypes, like, userIds } = inputFilter;
return {
targetTypes: targetTypes,
like: like,
userIds: userIds
}
}
private _getEmptyFilters(): LockListingFilters {
return {
targetTypes: null,
like: null,
userIds: null
}
}
private _computeAppliedFilters(filters: LockListingFilters): number {
let count = 0;
// if (filters?.isActive) {
// count++
// }
return count;
}
clearFilters() {
this.internalFilters = this._getEmptyFilters();
}
}
interface LockListingFilters {
targetTypes: LockTargetType[];
like: string;
userIds: Guid[];
}

View File

@ -0,0 +1,88 @@
<div class="row lock-listing">
<div class="col-md-8 offset-md-2">
<div class="row mb-4 mt-3">
<div class="col">
<h4>{{'LOCK-LISTING.TITLE' | translate}}</h4>
<app-navigation-breadcrumb />
</div>
</div>
<app-hybrid-listing [rows]="gridRows" [columns]="gridColumns" [visibleColumns]="visibleColumns"
[count]="totalElements" [offset]="currentPageNumber" [limit]="lookup.page.size"
[defaultSort]="lookup.order?.items" [externalSorting]="true"
(pageLoad)="alterPage($event)" (columnSort)="onColumnSort($event)"
(columnsChanged)="onColumnsChanged($event)" [listItemTemplate]="listItemTemplate">
<app-lock-listing-filters hybrid-listing-filters [(filter)]="lookup"
(filterChange)="filterChanged($event)" />
<app-user-settings-picker [key]="userSettingsKey" [userPreference]="lookup"
(onSettingSelected)="changeSetting($event)" [autoSelectUserSettings]="autoSelectUserSettings"
user-preference-settings />
</app-hybrid-listing>
</div>
</div>
<ng-template #listItemTemplate let-item="item" let-isColumnSelected="isColumnSelected">
<div class="d-flex align-items-center p-3 gap-1-rem">
<div class="row">
<ng-container *ngIf="isColumnSelected('targetType')">
<span class="col-12">
{{'LOCK-LISTING.FIELDS.TARGET-TYPE' | translate}}:
<small>
{{enumUtils.toLockTargetTypeString(item.targetType) | nullifyValue}}
</small>
</span>
<br>
</ng-container>
<ng-container *ngIf="isColumnSelected('lockedAt')">
<span class="col-12">
{{'LOCK-LISTING.FIELDS.LOCKED-AT' | translate}}:
<small>
{{item?.lockedAt | dateTimeFormatter : 'short' | nullifyValue}}
</small>
</span>
<br>
</ng-container>
<ng-container *ngIf="isColumnSelected('touchedAt')">
<span class="col-12">
{{'LOCK-LISTING.FIELDS.TOUCHED-AT' | translate}}:
<small>
{{item?.touchedAt | dateTimeFormatter : 'short' | nullifyValue}}
</small>
</span>
</ng-container>
<!-- <ng-container *ngIf="isColumnSelected('contactTypeHint')">
<span class="col-12">
{{'LOCK-LISTING.FIELDS.CONTACT-TYPE' | translate}}:
<small>
{{enumUtils.toNotificationContactTypeString(item.contactTypeHint) | nullifyValue}}
</small>
</span>
<br>
</ng-container> -->
</div>
</div>
</ng-template>
<ng-template #actions let-row="row" let-item>
<div class="row" (click)="$event.stopPropagation()">
<div class="col-auto">
<button mat-icon-button [matMenuTriggerFor]="actionsMenu">
<mat-icon>more_horiz</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
<button mat-menu-item (click)="deleteType(row.id)">
<mat-icon>delete</mat-icon>
{{'LOCK-LISTING.ACTIONS.DELETE' | translate}}
</button>
</mat-menu>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,36 @@
::ng-deep datatable-header-cell {
width: fit-content !important;
padding: 0.9rem 0.7rem !important;
}
::ng-deep .datatable-header-cell-template-wrap {
width: fit-content !important;
}
.lock-listing {
margin-top: 1.3rem;
margin-left: 1rem;
margin-right: 2rem;
.mat-header-row{
background: #f3f5f8;
}
.mat-card {
margin: 16px 0;
padding: 0px;
}
.mat-row {
cursor: pointer;
min-height: 4.5em;
}
mat-row:hover {
background-color: #eef5f6;
}
.mat-fab-bottom-right {
float: right;
z-index: 5;
}
}

View File

@ -0,0 +1,176 @@
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '@app/core/services/auth/auth.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { QueryParamsService } from '@app/core/services/utilities/query-params.service';
import { BaseListingComponent } from '@common/base/base-listing-component';
import { PipeService } from '@common/formatting/pipe.service';
import { DataTableDateTimeFormatPipe } from '@common/formatting/pipes/date-time-format.pipe';
import { QueryResult } from '@common/model/query-result';
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { ColumnDefinition, ColumnsChangedEvent, HybridListingComponent, PageLoadEvent } from '@common/modules/hybrid-listing/hybrid-listing.component';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { Lock } from '@app/core/model/lock/lock.model';
import { LockLookup } from '@app/core/query/lock.lookup';
import { LockService } from '@app/core/services/lock/lock.service';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { LockTargetTypePipe } from '@common/formatting/pipes/lock-target-type.pipe';
import { User } from '@app/core/model/user/user';
@Component({
templateUrl: './lock-listing.component.html',
styleUrls: ['./lock-listing.component.scss']
})
export class LockListingComponent extends BaseListingComponent<Lock, LockLookup> implements OnInit {
publish = false;
userSettingsKey = { key: 'LockListingUserSettings' };
propertiesAvailableForOrder: ColumnDefinition[];
@ViewChild('actions', { static: true }) actions?: TemplateRef<any>;
@ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent;
private readonly lookupFields: string[] = [
nameof<Lock>(x => x.id),
nameof<Lock>(x => x.target),
nameof<Lock>(x => x.targetType),
nameof<Lock>(x => x.lockedBy),
[nameof<Lock>(x => x.lockedBy), nameof<User>(x => x.name)].join('.'),
nameof<Lock>(x => x.lockedAt),
nameof<Lock>(x => x.touchedAt),
nameof<Lock>(x => x.hash),
];
rowIdentity = x => x.id;
constructor(
protected router: Router,
protected route: ActivatedRoute,
protected uiNotificationService: UiNotificationService,
protected httpErrorHandlingService: HttpErrorHandlingService,
protected queryParamsService: QueryParamsService,
private lockService: LockService,
public authService: AuthService,
private pipeService: PipeService,
public enumUtils: EnumUtils,
private language: TranslateService,
private dialog: MatDialog
) {
super(router, route, uiNotificationService, httpErrorHandlingService, queryParamsService);
// Lookup setup
// Default lookup values are defined in the user settings class.
this.lookup = this.initializeLookup();
}
ngOnInit() {
super.ngOnInit();
}
protected initializeLookup(): LockLookup {
const lookup = new LockLookup();
lookup.metadata = { countAll: true };
lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE };
lookup.order = { items: [this.toDescSortField(nameof<Lock>(x => x.touchedAt))] };
this.updateOrderUiFields(lookup.order);
lookup.project = {
fields: this.lookupFields
};
return lookup;
}
protected setupColumns() {
this.gridColumns.push(...[{
prop: nameof<Lock>(x => x.target),
sortable: true,
languageName: 'LOCK-LISTING.FIELDS.TARGET',
},
{
prop: nameof<Lock>(x => x.targetType),
sortable: true,
languageName: 'LOCK-LISTING.FIELDS.TARGET-TYPE',
pipe: this.pipeService.getPipe<LockTargetTypePipe>(LockTargetTypePipe)
},
{
prop: nameof<Lock>(x => x.lockedBy.name),
sortable: true,
languageName: 'LOCK-LISTING.FIELDS.LOCKED-BY',
},
{
prop: nameof<Lock>(x => x.lockedAt),
sortable: true,
languageName: 'LOCK-LISTING.FIELDS.LOCKED-AT',
pipe: this.pipeService.getPipe<DataTableDateTimeFormatPipe>(DataTableDateTimeFormatPipe).withFormat('short')
},
{
prop: nameof<Lock>(x => x.touchedAt),
sortable: true,
languageName: 'LOCK-LISTING.FIELDS.TOUCHED-AT',
pipe: this.pipeService.getPipe<DataTableDateTimeFormatPipe>(DataTableDateTimeFormatPipe).withFormat('short')
},
{
alwaysShown: true,
cellTemplate: this.actions,
maxWidth: 120
}
]);
this.propertiesAvailableForOrder = this.gridColumns.filter(x => x.sortable);
}
//
// Listing Component functions
//
onColumnsChanged(event: ColumnsChangedEvent) {
super.onColumnsChanged(event);
this.onColumnsChangedInternal(event.properties.map(x => x.toString()));
}
private onColumnsChangedInternal(columns: string[]) {
// Here are defined the projection fields that always requested from the api.
const fields = new Set(this.lookupFields);
this.gridColumns.map(x => x.prop)
.filter(x => !columns?.includes(x as string))
.forEach(item => {
fields.delete(item as string)
});
this.lookup.project = { fields: [...fields] };
this.onPageLoad({ offset: 0 } as PageLoadEvent);
}
protected loadListing(): Observable<QueryResult<Lock>> {
return this.lockService.query(this.lookup);
}
public deleteType(id: Guid) {
if (id) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
isDeleteConfirmation: true,
message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'),
confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.lockService.delete(id).pipe(takeUntil(this._destroyed))
.subscribe(
complete => this.onCallbackSuccess(),
error => this.onCallbackError(error)
);
}
});
}
}
onCallbackSuccess(): void {
this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success);
this.refresh();
}
}

View File

@ -0,0 +1,41 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from "@angular/core";
import { AutoCompleteModule } from "@app/library/auto-complete/auto-complete.module";
import { CommonFormattingModule } from '@common/formatting/common-formatting.module';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module';
import { HybridListingModule } from "@common/modules/hybrid-listing/hybrid-listing.module";
import { TextFilterModule } from "@common/modules/text-filter/text-filter.module";
import { UserSettingsModule } from "@common/modules/user-settings/user-settings.module";
import { CommonUiModule } from '@common/ui/common-ui.module';
import { NgxDropzoneModule } from "ngx-dropzone";
import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module';
import { LockRoutingModule } from './lock.routing';
import { LockListingFiltersComponent } from './filters/lock-listing-filters.component';
import { MatIconModule } from '@angular/material/icon';
import { EditorModule } from '@tinymce/tinymce-angular';
import { LockListingComponent } from './lock-listing.component';
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
ConfirmationDialogModule,
LockRoutingModule,
NgxDropzoneModule,
DragDropModule,
AutoCompleteModule,
HybridListingModule,
TextFilterModule,
UserSettingsModule,
CommonFormattingModule,
RichTextEditorModule,
MatIconModule,
EditorModule
],
declarations: [
LockListingComponent,
LockListingFiltersComponent
]
})
export class LockModule { }

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@app/core/auth-guard.service';
import { LockListingComponent } from './lock-listing.component';
const routes: Routes = [
{
path: '',
component: LockListingComponent,
canActivate: [AuthGuard]
},
{ path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class LockRoutingModule { }

View File

@ -29,6 +29,8 @@ import { LanguageEditorService } from './language-editor.service';
import { LanguageEditorModel } from './language-editor.model'; import { LanguageEditorModel } from './language-editor.model';
import { LanguageHttpService } from '@app/core/services/language/language.http.service'; import { LanguageHttpService } from '@app/core/services/language/language.http.service';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
@ -75,8 +77,10 @@ export class LanguageEditorComponent extends BaseEditor<LanguageEditorModel, Lan
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private languageHttpService: LanguageHttpService, private languageHttpService: LanguageHttpService,
private logger: LoggingService, private logger: LoggingService,
@ -84,7 +88,7 @@ export class LanguageEditorComponent extends BaseEditor<LanguageEditorModel, Lan
private fileUtils: FileUtils, private fileUtils: FileUtils,
private matomoService: MatomoService private matomoService: MatomoService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -34,6 +34,8 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
import { NotificationDataType } from '@app/core/common/enum/notification-data-type'; import { NotificationDataType } from '@app/core/common/enum/notification-data-type';
import { EmailOverrideMode } from '@app/core/common/enum/email-override-mode'; import { EmailOverrideMode } from '@app/core/common/enum/email-override-mode';
import { NotificationType } from '@app/core/common/enum/notification-type'; import { NotificationType } from '@app/core/common/enum/notification-type';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
selector: 'app-notification-template-editor', selector: 'app-notification-template-editor',
@ -87,8 +89,10 @@ export class NotificationTemplateEditorComponent extends BaseEditor<Notification
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private languageHttpService: LanguageHttpService, private languageHttpService: LanguageHttpService,
private notificationTemplateService: NotificationTemplateService, private notificationTemplateService: NotificationTemplateService,
@ -96,7 +100,7 @@ export class NotificationTemplateEditorComponent extends BaseEditor<Notification
private logger: LoggingService, private logger: LoggingService,
private matomoService: MatomoService private matomoService: MatomoService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -28,6 +28,8 @@ import { PrefillingSourceEditorService } from './prefilling-source-editor.servic
import { PrefillingSourceDefinitionEditorModel, PrefillingSourceEditorModel } from './prefilling-source-editor.model'; import { PrefillingSourceDefinitionEditorModel, PrefillingSourceEditorModel } from './prefilling-source-editor.model';
import { ResultFieldsMappingConfigurationEditorModel } from '@app/ui/external-fetcher/external-fetcher-source-editor.model'; import { ResultFieldsMappingConfigurationEditorModel } from '@app/ui/external-fetcher/external-fetcher-source-editor.model';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
selector: 'app-prefilling-source-editor-component', selector: 'app-prefilling-source-editor-component',
@ -66,15 +68,17 @@ export class PrefillingSourceEditorComponent extends BaseEditor<PrefillingSource
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private prefillingSourceService: PrefillingSourceService, private prefillingSourceService: PrefillingSourceService,
private logger: LoggingService, private logger: LoggingService,
private prefillingSourceEditorService: PrefillingSourceEditorService, private prefillingSourceEditorService: PrefillingSourceEditorService,
private matomoService: MatomoService private matomoService: MatomoService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -28,6 +28,8 @@ import { map, takeUntil } from 'rxjs/operators';
import { ReferenceTypeEditorModel} from './reference-type-editor.model'; import { ReferenceTypeEditorModel} from './reference-type-editor.model';
import { ReferenceTypeEditorResolver } from './reference-type-editor.resolver'; import { ReferenceTypeEditorResolver } from './reference-type-editor.resolver';
import { ReferenceTypeEditorService } from './reference-type-editor.service'; import { ReferenceTypeEditorService } from './reference-type-editor.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
@ -79,14 +81,16 @@ export class ReferenceTypeEditorComponent extends BaseEditor<ReferenceTypeEditor
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
public referenceTypeService: ReferenceTypeService, public referenceTypeService: ReferenceTypeService,
private logger: LoggingService, private logger: LoggingService,
private referenceTypeEditorService: ReferenceTypeEditorService private referenceTypeEditorService: ReferenceTypeEditorService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -30,6 +30,8 @@ import { map, takeUntil } from 'rxjs/operators';
import { ReferenceEditorModel } from './reference-editor.model'; import { ReferenceEditorModel } from './reference-editor.model';
import { ReferenceEditorResolver } from './reference-editor.resolver'; import { ReferenceEditorResolver } from './reference-editor.resolver';
import { ReferenceEditorService } from './reference-editor.service'; import { ReferenceEditorService } from './reference-editor.service';
import { LockService } from '@app/core/services/lock/lock.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
@Component({ @Component({
@ -76,8 +78,10 @@ export class ReferenceEditorComponent extends BaseEditor<ReferenceEditorModel, R
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private referenceService: ReferenceService, private referenceService: ReferenceService,
private logger: LoggingService, private logger: LoggingService,
@ -86,7 +90,7 @@ export class ReferenceEditorComponent extends BaseEditor<ReferenceEditorModel, R
private matomoService: MatomoService, private matomoService: MatomoService,
public referenceTypeService: ReferenceTypeService public referenceTypeService: ReferenceTypeService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -28,6 +28,8 @@ import { map, takeUntil } from 'rxjs/operators';
import { TenantEditorResolver } from './tenant-editor.resolver'; import { TenantEditorResolver } from './tenant-editor.resolver';
import { TenantEditorService } from './tenant-editor.service'; import { TenantEditorService } from './tenant-editor.service';
import { TenantEditorModel } from './tenant-editor.model'; import { TenantEditorModel } from './tenant-editor.model';
import { LockService } from '@app/core/services/lock/lock.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
@Component({ @Component({
@ -74,8 +76,10 @@ export class TenantEditorComponent extends BaseEditor<TenantEditorModel, Tenant>
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private tenantService: TenantService, private tenantService: TenantService,
private logger: LoggingService, private logger: LoggingService,
@ -83,7 +87,7 @@ export class TenantEditorComponent extends BaseEditor<TenantEditorModel, Tenant>
private fileUtils: FileUtils, private fileUtils: FileUtils,
private matomoService: MatomoService private matomoService: MatomoService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -40,6 +40,8 @@ import { ToCEntry } from './table-of-contents/models/toc-entry';
import { ToCEntryType } from './table-of-contents/models/toc-entry-type.enum'; import { ToCEntryType } from './table-of-contents/models/toc-entry-type.enum';
import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component'; import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component';
import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component'; import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
@Component({ @Component({
selector: 'app-description-editor-component', selector: 'app-description-editor-component',
@ -90,8 +92,10 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private descriptionService: DescriptionService, private descriptionService: DescriptionService,
private logger: LoggingService, private logger: LoggingService,
@ -100,11 +104,10 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
private fileUtils: FileUtils, private fileUtils: FileUtils,
private matomoService: MatomoService, private matomoService: MatomoService,
private dmpService: DmpService, private dmpService: DmpService,
private lockService: LockService,
public visibilityRulesService: VisibilityRulesService public visibilityRulesService: VisibilityRulesService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {
@ -192,41 +195,8 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
//Regular Editor case //Regular Editor case
if (itemId != null && newDmpId == null) { if (itemId != null && newDmpId == null) {
this.isNew = false; this.checkLock(this.item.id, LockTargetType.Description);
this.lockService.checkLockStatus(itemId).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
if (this.item.status === DescriptionStatus.Finalized || lockStatus) {
this.formGroup.disable();
this.viewOnly = true;
}
if (lockStatus) {
this.dialog.open(PopupNotificationDialogComponent, {
data: {
title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'),
message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE')
}, maxWidth: '30em'
});
}
if (!lockStatus && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) {
//TODO: lock it.
// const lockedBy: UserInfoListingModel = {
// email: this.authService.getUserProfileEmail(),
// id: this.authService.userId()?.toString(),
// name: this.authService.getPrincipalName(),
// role: 0 //TODO
// //role: this.authService.getRoles()?.at(0)
// }
// this.lock = new LockModel(data.id, lockedBy);
// this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
// this.lock.id = Guid.parse(result);
// interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
// });
}
// this.loadDescriptionProfiles();
// this.registerFormListeners();
});
} else if (dmpId != null && dmpSectionId != null) { } else if (dmpId != null && dmpSectionId != null) {
this.isNew = true; this.isNew = true;
const dialogRef = this.dialog.open(PrefillDescriptionDialogComponent, { const dialogRef = this.dialog.open(PrefillDescriptionDialogComponent, {

View File

@ -13,7 +13,7 @@ import { DmpService } from '@app/core/services/dmp/dmp.service';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { BaseEditorResolver } from '@common/base/base-editor.resolver'; import { BaseEditorResolver } from '@common/base/base-editor.resolver';
import { Guid } from '@common/types/guid'; import { Guid } from '@common/types/guid';
import { map, takeUntil, tap } from 'rxjs/operators'; import { concatMap, map, takeUntil, tap } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof'; import { nameof } from 'ts-simple-nameof';
@Injectable() @Injectable()
@ -176,7 +176,7 @@ export class DescriptionEditorResolver extends BaseEditorResolver {
return description; return description;
})); }));
} else if (copyDmpId != null && id != null && dmpSectionId != null) { } else if (copyDmpId != null && id != null && dmpSectionId != null) {
return this.dmpService.getSingle(Guid.parse(copyDmpId), DescriptionEditorResolver.dmpLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), map(dmp => { return this.dmpService.getSingle(Guid.parse(copyDmpId), DescriptionEditorResolver.dmpLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), concatMap(dmp => {
return this.descriptionService.getSingle(Guid.parse(id), DescriptionEditorResolver.cloneLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), map(description => { return this.descriptionService.getSingle(Guid.parse(id), DescriptionEditorResolver.cloneLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), map(description => {
description.dmp = dmp; description.dmp = dmp;

View File

@ -194,7 +194,7 @@ export class DescriptionListingItemComponent extends BaseComponent implements On
deleteClicked(id: Guid) { deleteClicked(id: Guid) {
this.lockService.checkLockStatus(id).pipe(takeUntil(this._destroyed)) this.lockService.checkLockStatus(id).pipe(takeUntil(this._destroyed))
.subscribe(lockStatus => { .subscribe(lockStatus => {
if (!lockStatus) { if (!lockStatus.status) {
this.openDeleteDialog(id); this.openDeleteDialog(id);
} else { } else {
this.openLockedByUserDialog(); this.openLockedByUserDialog();

View File

@ -21,7 +21,6 @@ import { DescriptionSectionPermissionResolver } from '@app/core/model/descriptio
import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint'; import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint';
import { Dmp, DmpPersist } from '@app/core/model/dmp/dmp'; import { Dmp, DmpPersist } from '@app/core/model/dmp/dmp';
import { LanguageInfo } from '@app/core/model/language-info'; import { LanguageInfo } from '@app/core/model/language-info';
import { LockPersist } from '@app/core/model/lock/lock.model';
import { AuthService } from '@app/core/services/auth/auth.service'; import { AuthService } from '@app/core/services/auth/auth.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { LanguageInfoService } from '@app/core/services/culture/language-info-service'; import { LanguageInfoService } from '@app/core/services/culture/language-info-service';
@ -64,11 +63,8 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
isDeleted = false; isDeleted = false;
item: Dmp; item: Dmp;
selectedBlueprint: DmpBlueprint; selectedBlueprint: DmpBlueprint;
lockStatus: Boolean = false;
step: number = 0; step: number = 0;
//Enums //Enums
descriptionStatusEnum = DescriptionStatus; descriptionStatusEnum = DescriptionStatus;
dmpBlueprintSectionFieldCategoryEnum = DmpBlueprintFieldCategory; dmpBlueprintSectionFieldCategoryEnum = DmpBlueprintFieldCategory;
@ -135,14 +131,14 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
private dmpService: DmpService, private dmpService: DmpService,
private logger: LoggingService, private logger: LoggingService,
public dmpBlueprintService: DmpBlueprintService, public dmpBlueprintService: DmpBlueprintService,
private matomoService: MatomoService, private matomoService: MatomoService,
private lockService: LockService,
private configurationService: ConfigurationService,
// public visibilityRulesService: VisibilityRulesService, // public visibilityRulesService: VisibilityRulesService,
private languageInfoService: LanguageInfoService, private languageInfoService: LanguageInfoService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
@ -151,7 +147,7 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
public descriptionService: DescriptionService public descriptionService: DescriptionService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {
@ -202,7 +198,7 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
} }
if (this.item.id != null) { if (this.item.id != null) {
this.checkLock(this.item.id); this.checkLock(this.item.id, LockTargetType.Dmp);
} }
} catch (error) { } catch (error) {
this.logger.error('Could not parse Dmp item: ' + data + error); this.logger.error('Could not parse Dmp item: ' + data + error);
@ -471,45 +467,6 @@ export class DmpEditorComponent extends BaseEditor<DmpEditorModel, Dmp> implemen
// }); // });
} }
//
//
// Lock
//
//
private checkLock(itemId: Guid) {
if (itemId != null) {
this.isNew = false;
// check if locked.
this.lockService.checkLockStatus(itemId).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
if (lockStatus) {
this.formGroup.disable();
this.dialog.open(PopupNotificationDialogComponent, {
data: {
title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'),
message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE')
}, maxWidth: '30em'
});
}
if (!lockStatus && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) {
// lock it.
const lockPersist: LockPersist = {
target: itemId,
targetType: LockTargetType.Dmp,
}
this.lockService.persist(lockPersist).pipe(takeUntil(this._destroyed)).subscribe(async result => {
interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.touchLock(itemId));
});
}
});
}
}
private touchLock(targetId: Guid) {
this.lockService.checkLockStatus(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { });
}
// //
// //
// Misc // Misc

View File

@ -156,7 +156,7 @@ export class DmpListingItemComponent extends BaseComponent implements OnInit {
deleteClicked(id: Guid) { deleteClicked(id: Guid) {
this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed)) this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed))
.subscribe(lockStatus => { .subscribe(lockStatus => {
if (!lockStatus) { if (!lockStatus.status) {
this.openDeleteDialog(id); this.openDeleteDialog(id);
} else { } else {
this.openLockedByUserDialog(); this.openLockedByUserDialog();

View File

@ -709,8 +709,8 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
checkLockStatus(id: Guid) { checkLockStatus(id: Guid) {
this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed)) this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed))
.subscribe(lockStatus => { .subscribe(lockStatus => {
this.lockStatus = lockStatus this.lockStatus = lockStatus.status;
if (lockStatus) { if (this.lockStatus) {
this.dialog.open(PopupNotificationDialogComponent, { this.dialog.open(PopupNotificationDialogComponent, {
data: { data: {
title: this.language.instant('DMP-OVERVIEW.LOCKED-DIALOG.TITLE'), title: this.language.instant('DMP-OVERVIEW.LOCKED-DIALOG.TITLE'),

View File

@ -51,6 +51,7 @@ export const ADMIN_ROUTES: RouteInfo[] = [
{ path: '/dmp-blueprints', title: 'SIDE-BAR.DMP-BLUEPRINTS', icon: 'library_books' }, { path: '/dmp-blueprints', title: 'SIDE-BAR.DMP-BLUEPRINTS', icon: 'library_books' },
{ path: '/description-templates', title: 'SIDE-BAR.DESCRIPTION-TEMPLATES', icon: 'description' }, { path: '/description-templates', title: 'SIDE-BAR.DESCRIPTION-TEMPLATES', icon: 'description' },
{ path: '/description-template-type', title: 'SIDE-BAR.DESCRIPTION-TEMPLATE-TYPES', icon: 'stack' }, { path: '/description-template-type', title: 'SIDE-BAR.DESCRIPTION-TEMPLATE-TYPES', icon: 'stack' },
{ path: '/entity-locks', title: 'SIDE-BAR.ENTITY-LOCKS', icon: 'build'},
{ path: '/references', title: 'SIDE-BAR.REFERENCES', icon: 'dataset_linked' }, { path: '/references', title: 'SIDE-BAR.REFERENCES', icon: 'dataset_linked' },
{ path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'add_link' }, { path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'add_link' },
{ path: '/prefilling-sources', title: 'SIDE-BAR.PREFILLING-SOURCES', icon: 'add_link' }, { path: '/prefilling-sources', title: 'SIDE-BAR.PREFILLING-SOURCES', icon: 'add_link' },

View File

@ -34,6 +34,7 @@ import { LanguageHttpService } from '@app/core/services/language/language.http.s
import { nameof } from 'ts-simple-nameof'; import { nameof } from 'ts-simple-nameof';
import { Language } from '@app/core/model/language/language'; import { Language } from '@app/core/model/language/language';
import { LanguageLookup } from '@app/core/query/language.lookup'; import { LanguageLookup } from '@app/core/query/language.lookup';
import { LockService } from '@app/core/services/lock/lock.service';
@Component({ @Component({
@ -64,8 +65,10 @@ export class SupportiveMaterialEditorComponent extends BaseEditor<SupportiveMate
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService, protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
// Rest dependencies. Inject any other needed deps here: // Rest dependencies. Inject any other needed deps here:
public authService: AuthService,
public enumUtils: EnumUtils, public enumUtils: EnumUtils,
private supportiveMaterialService: SupportiveMaterialService, private supportiveMaterialService: SupportiveMaterialService,
private languageService: LanguageService, private languageService: LanguageService,
@ -75,7 +78,7 @@ export class SupportiveMaterialEditorComponent extends BaseEditor<SupportiveMate
private fileUtils: FileUtils, private fileUtils: FileUtils,
private matomoService: MatomoService private matomoService: MatomoService
) { ) {
super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -272,7 +272,8 @@
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notifications",
"PREFILLING-SOURCES": "Prefilling Sources", "PREFILLING-SOURCES": "Prefilling Sources",
"NEW-PREFILLING-SOURCE": "New", "NEW-PREFILLING-SOURCE": "New",
"EDIT-PREFILLING-SOURCE": "Edit" "EDIT-PREFILLING-SOURCE": "Edit",
"ENTITY-LOCKS": "Entity Locks"
}, },
"COOKIE": { "COOKIE": {
"MESSAGE": "This website uses cookies to enhance the user experience.", "MESSAGE": "This website uses cookies to enhance the user experience.",
@ -376,7 +377,8 @@
"MAINTENANCE": "Maintenance", "MAINTENANCE": "Maintenance",
"NOTIFICATION-TEMPLATES": "Notification Templates", "NOTIFICATION-TEMPLATES": "Notification Templates",
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notifications",
"PREFILLING-SOURCES": "Prefilling Sources" "PREFILLING-SOURCES": "Prefilling Sources",
"ENTITY-LOCKS": "Entity Locks"
}, },
"DESCRIPTION-TEMPLATE-PREVIEW": { "DESCRIPTION-TEMPLATE-PREVIEW": {
"TEMPLATE": "Template" "TEMPLATE": "Template"
@ -1347,6 +1349,34 @@
"SUCCESSFUL-DELETE": "Successful Delete", "SUCCESSFUL-DELETE": "Successful Delete",
"UNSUCCESSFUL-DELETE": "This item could not be deleted." "UNSUCCESSFUL-DELETE": "This item could not be deleted."
}, },
"LOCK-LISTING": {
"TITLE": "Entity Locks",
"FIELDS": {
"TARGET": "Target",
"TARGET-TYPE": "Type",
"LOCKED-BY": "Locked By",
"LOCKED-AT": "Locked At",
"TOUCHED-AT": "Touched At"
},
"FILTER": {
"TITLE": "Filters",
"TARGET-TYPE": "Type",
"USERS": "Users",
"CANCEL": "Cancel",
"APPLY-FILTERS": "Apply filters"
},
"CONFIRM-DELETE-DIALOG": {
"MESSAGE": "Would you like to delete this Enttiy Lock?",
"CONFIRM-BUTTON": "Yes, delete",
"CANCEL-BUTTON": "No"
},
"ACTIONS": {
"DELETE": "Delete",
"EDIT": "Edit"
},
"SUCCESSFUL-DELETE": "Successful Delete",
"UNSUCCESSFUL-DELETE": "This item could not be deleted."
},
"LANGUAGE-LISTING": { "LANGUAGE-LISTING": {
"TITLE": "Languages", "TITLE": "Languages",
"CREATE": "Create Language", "CREATE": "Create Language",
@ -2596,6 +2626,12 @@
"LABEL": "Label", "LABEL": "Label",
"DESCRIPTION": "Description", "DESCRIPTION": "Description",
"TAGS": "Tags" "TAGS": "Tags"
},
"LOCK-TARGET-TYPE": {
"DMP": "DMP",
"DESCRIPTION": "Description",
"DMP-BLUEPRINT": "DMP Blueprint",
"DESCRIPTION-TEMPLATE": "Description Template"
} }
}, },
"ADDRESEARCHERS-EDITOR": { "ADDRESEARCHERS-EDITOR": {

View File

@ -10,13 +10,20 @@ import { FormService } from '@common/forms/form-service';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { FilterService } from '@common/modules/text-filter/filter-service'; import { FilterService } from '@common/modules/text-filter/filter-service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { interval, Observable } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof'; import { nameof } from 'ts-simple-nameof';
import { Guid } from '../types/guid'; import { Guid } from '../types/guid';
import { BaseEditorModel } from './base-form-editor-model'; import { BaseEditorModel } from './base-form-editor-model';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; import { QueryParamsService } from '@app/core/services/utilities/query-params.service';
import { LockService } from '@app/core/services/lock/lock.service';
import { LockPersist } from '@app/core/model/lock/lock.model';
import { LockTargetType } from '@app/core/common/enum/lock-target-type';
import { isNullOrUndefined } from '@swimlane/ngx-datatable';
import { PopupNotificationDialogComponent } from '@app/library/notification/popup/popup-notification.component';
import { AuthService } from '@app/core/services/auth/auth.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
@Component({ @Component({
selector: 'app-base-editor-component', selector: 'app-base-editor-component',
@ -26,6 +33,7 @@ export abstract class BaseEditor<EditorModelType extends BaseEditorModel, Entity
isNew = true; isNew = true;
isDeleted = false; isDeleted = false;
isLocked: Boolean = false;
formGroup: UntypedFormGroup = null; formGroup: UntypedFormGroup = null;
lookupParams: any; lookupParams: any;
@ -55,7 +63,10 @@ export abstract class BaseEditor<EditorModelType extends BaseEditorModel, Entity
protected filterService: FilterService, protected filterService: FilterService,
protected datePipe: DatePipe, protected datePipe: DatePipe,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected queryParamsService: QueryParamsService protected queryParamsService: QueryParamsService,
protected lockService: LockService,
protected authService: AuthService,
protected configurationService: ConfigurationService,
) { super(); } ) { super(); }
public ngOnInit(): void { public ngOnInit(): void {
@ -154,4 +165,49 @@ export abstract class BaseEditor<EditorModelType extends BaseEditorModel, Entity
nameof<BaseEntity>(x => x.hash), nameof<BaseEntity>(x => x.hash),
]; ];
} }
//
//
// Lock
//
//
protected checkLock(itemId: Guid, targetType: LockTargetType) {
if (itemId != null) {
this.isNew = false;
// check if locked.
this.lockService.checkLockStatus(itemId).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.isLocked = lockStatus.status;
if (this.isLocked) {
this.formGroup.disable();
this.dialog.open(PopupNotificationDialogComponent, {
data: {
title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'),
message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE')
}, maxWidth: '30em'
});
}
if (!this.isLocked && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) {
// lock it.
this.lockService.lock(itemId, targetType).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.isLocked = true;
interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.touchLock(itemId));
});
}
});
}
}
private unlockTarget(targetId: Guid) {
this.lockService.unlockTarget(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { });
}
private touchLock(targetId: Guid) {
this.lockService.touchLock(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { });
}
ngOnDestroy(): void {
super.ngOnDestroy();
if(this.isLocked) this.unlockTarget(this.editorModel.id);
}
} }

View File

@ -13,6 +13,7 @@ import { NotificationTrackingProcessPipe } from './pipes/notification-tracking-p
import { NotificationTrackingStatePipe } from './pipes/notification-tracking-state.pipe'; import { NotificationTrackingStatePipe } from './pipes/notification-tracking-state.pipe';
import { NotificationTypePipe } from './pipes/notification-type.pipe'; import { NotificationTypePipe } from './pipes/notification-type.pipe';
import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe';
import { LockTargetTypePipe } from './pipes/lock-target-type.pipe';
// //
// //
@ -37,7 +38,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe';
NotificationContactTypePipe, NotificationContactTypePipe,
NotificationNotifyStatePipe, NotificationNotifyStatePipe,
NotificationTrackingProcessPipe, NotificationTrackingProcessPipe,
NotificationTrackingStatePipe NotificationTrackingStatePipe,
LockTargetTypePipe
], ],
exports: [ exports: [
DateFormatPipe, DateFormatPipe,
@ -56,7 +58,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe';
NotificationContactTypePipe, NotificationContactTypePipe,
NotificationNotifyStatePipe, NotificationNotifyStatePipe,
NotificationTrackingProcessPipe, NotificationTrackingProcessPipe,
NotificationTrackingStatePipe NotificationTrackingStatePipe,
LockTargetTypePipe
], ],
providers: [ providers: [
DateFormatPipe, DateFormatPipe,
@ -75,7 +78,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe';
NotificationContactTypePipe, NotificationContactTypePipe,
NotificationNotifyStatePipe, NotificationNotifyStatePipe,
NotificationTrackingProcessPipe, NotificationTrackingProcessPipe,
NotificationTrackingStatePipe NotificationTrackingStatePipe,
LockTargetTypePipe
] ]
}) })
export class CommonFormattingModule { } export class CommonFormattingModule { }

View File

@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
@Pipe({ name: 'LockTargetTypeFormat' })
export class LockTargetTypePipe implements PipeTransform {
constructor(private enumUtils: EnumUtils) { }
public transform(value): any {
return this.enumUtils.toLockTargetTypeString(value);
}
}