implement plan status logic (in progress)

This commit is contained in:
CITE\amentis 2024-09-20 11:50:19 +03:00
parent a7a223d2c6
commit 21fd258181
36 changed files with 458 additions and 168 deletions

View File

@ -50,6 +50,7 @@ public class AuditableAction {
public static final EventId Plan_GetPublicXml = new EventId(5017, "Plan_GetPublicXml");
public static final EventId Plan_ExportPublic = new EventId(5018, "Plan_ExportPublic");
public static final EventId Plan_PublicClone = new EventId(5019, "Plan_PublicClone");
public static final EventId Plan_SetStatus = new EventId(5020, "Plan_SetStatus");
public static final EventId Description_Query = new EventId(6000, "Description_Query");

View File

@ -1,7 +1,6 @@
package org.opencdmp.model;
import org.opencdmp.commons.enums.PlanAccessType;
import org.opencdmp.commons.enums.PlanStatus;
import java.time.Instant;
import java.util.List;
@ -36,7 +35,7 @@ public class PublicPlan {
public static final String _publishedAt = "publishedAt";
private PlanStatus status;
private PublicPlanStatus status;
public static final String _status = "status";
private UUID groupId;
@ -118,11 +117,11 @@ public class PublicPlan {
this.publishedAt = publishedAt;
}
public PlanStatus getStatus() {
public PublicPlanStatus getStatus() {
return status;
}
public void setStatus(PlanStatus status) {
public void setStatus(PublicPlanStatus status) {
this.status = status;
}

View File

@ -0,0 +1,32 @@
package org.opencdmp.model;
import org.opencdmp.commons.enums.PlanStatus;
import java.util.UUID;
public class PublicPlanStatus {
public final static String _id = "id";
private UUID id;
public final static String _name = "name";
private String name;
public final static String _internalStatus = "internalStatus";
private PlanStatus internalStatus;
public UUID getId() { return this.id; }
public void setId(UUID id) { this.id = id; }
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public PlanStatus getInternalStatus() {
return internalStatus;
}
public void setInternalStatus(PlanStatus internalStatus) {
this.internalStatus = internalStatus;
}
}

View File

@ -186,7 +186,7 @@ public class PublicDescriptionBuilder extends BaseBuilder<PublicDescription, Des
Map<UUID, PublicDescriptionStatus> itemMap;
if (!fields.hasOtherField(this.asIndexer(PublicDescriptionStatus._id))) {
itemMap = this.asEmpty(
data.stream().map(DescriptionEntity::getDescriptionTemplateId).distinct().collect(Collectors.toList()),
data.stream().map(DescriptionEntity::getStatusId).distinct().collect(Collectors.toList()),
x -> {
PublicDescriptionStatus item = new PublicDescriptionStatus();
item.setId(x);

View File

@ -71,6 +71,9 @@ public class PublicPlanBuilder extends BaseBuilder<PublicPlan, PlanEntity> {
FieldSet otherPlanVersionsFields = fields.extractPrefixed(this.asPrefix(PublicPlan._otherPlanVersions));
Map<UUID, List<PublicPlan>> otherPlanVersionsMap = this.collectOtherPlanVersions(otherPlanVersionsFields, data);
FieldSet planStatusFields = fields.extractPrefixed(this.asPrefix(PublicPlan._status));
Map<UUID, PublicPlanStatus> planStatusItemsMap = this.collectPlanStatuses(planStatusFields, data);
for (PlanEntity d : data) {
PublicPlan m = new PublicPlan();
if (fields.hasField(this.asIndexer(PublicPlan._id))) m.setId(d.getId());
@ -80,7 +83,7 @@ public class PublicPlanBuilder extends BaseBuilder<PublicPlan, PlanEntity> {
if (fields.hasField(this.asIndexer(PublicPlan._finalizedAt))) m.setFinalizedAt(d.getFinalizedAt());
if (fields.hasField(this.asIndexer(PublicPlan._updatedAt))) m.setUpdatedAt(d.getUpdatedAt());
if (fields.hasField(this.asIndexer(PublicPlan._accessType))) m.setAccessType(d.getAccessType());
if (fields.hasField(this.asIndexer(PublicPlan._status))) m.setStatus(d.getStatus());
if (!planStatusFields.isEmpty() && planStatusItemsMap != null && planStatusItemsMap.containsKey(d.getStatusId())) m.setStatus(planStatusItemsMap.get(d.getStatusId()));
if (fields.hasField(this.asIndexer(PublicPlan._groupId))) m.setGroupId(d.getGroupId());
if (fields.hasField(this.asIndexer(PublicPlan._accessType))) m.setAccessType(d.getAccessType());
@ -190,4 +193,34 @@ public class PublicPlanBuilder extends BaseBuilder<PublicPlan, PlanEntity> {
return itemMap;
}
private Map<UUID, PublicPlanStatus> collectPlanStatuses(FieldSet fields, List<PlanEntity> data) throws MyApplicationException {
if (fields.isEmpty() || data.isEmpty())
return null;
this.logger.debug("checking related - {}", PublicPlanStatus.class.getSimpleName());
Map<UUID, PublicPlanStatus> itemMap;
if (!fields.hasOtherField(this.asIndexer(PublicPlanStatus._id))) {
itemMap = this.asEmpty(
data.stream().map(PlanEntity::getStatusId).distinct().collect(Collectors.toList()),
x -> {
PublicPlanStatus item = new PublicPlanStatus();
item.setId(x);
return item;
},
PublicPlanStatus::getId);
} else {
FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(PublicPlanStatus._id);
PlanStatusQuery q = this.queryFactory.query(PlanStatusQuery.class).disableTracking().authorize(this.authorize).ids(data.stream().map(PlanEntity::getStatusId).distinct().collect(Collectors.toList()));
itemMap = this.builderFactory.builder(PublicPlanStatusBuilder.class).authorize(this.authorize).asForeignKey(q, clone, PublicPlanStatus::getId);
}
if (!fields.hasField(PublicPlanStatus._id)) {
itemMap.forEach((id, item) -> {
if (item != null)
item.setId(null);
});
}
return itemMap;
}
}

View File

@ -0,0 +1,52 @@
package org.opencdmp.model.builder;
import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.fieldset.FieldSet;
import gr.cite.tools.logging.DataLogEntry;
import gr.cite.tools.logging.LoggerService;
import org.opencdmp.authorization.AuthorizationFlags;
import org.opencdmp.convention.ConventionService;
import org.opencdmp.data.PlanStatusEntity;
import org.opencdmp.model.PublicPlanStatus;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PublicPlanStatusBuilder extends BaseBuilder<PublicPlanStatus, PlanStatusEntity> {
private EnumSet<AuthorizationFlags> authorize = EnumSet.of(AuthorizationFlags.None);
@Autowired
public PublicPlanStatusBuilder(
ConventionService conventionService) {
super(conventionService, new LoggerService(LoggerFactory.getLogger(PublicPlanStatusBuilder.class)));
}
public PublicPlanStatusBuilder authorize(EnumSet<AuthorizationFlags> values) {
this.authorize = values;
return this;
}
@Override
public List<PublicPlanStatus> build(FieldSet fields, List<PlanStatusEntity> data) throws MyApplicationException {
this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0), Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size).orElse(0));
this.logger.trace(new DataLogEntry("requested fields", fields));
if (fields == null || data == null || fields.isEmpty())
return new ArrayList<>();
List<PublicPlanStatus> models = new ArrayList<>();
for (PlanStatusEntity d : data) {
PublicPlanStatus m = new PublicPlanStatus();
if (fields.hasField(this.asIndexer(PublicPlanStatus._id))) m.setId(d.getId());
if (fields.hasField(this.asIndexer(PublicPlanStatus._name))) m.setName(d.getName());
if (fields.hasField(this.asIndexer(PublicPlanStatus._internalStatus))) m.setInternalStatus(d.getInternalStatus());
models.add(m);
}
this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0));
return models;
}
}

View File

@ -87,7 +87,7 @@ public class PlanBuilder extends BaseBuilder<Plan, PlanEntity> {
List<Plan> models = new ArrayList<>();
FieldSet statusFields = fields.extractPrefixed(this.asPrefix(Description._status));
FieldSet statusFields = fields.extractPrefixed(this.asPrefix(Plan._status));
Map<UUID, PlanStatus> statusItemsMap = this.collectPlanStatuses(statusFields, data);
FieldSet entityDoisFields = fields.extractPrefixed(this.asPrefix(Plan._entityDois));

View File

@ -243,7 +243,7 @@ public class PlanPersist {
.must(() -> this.isDescriptionTemplateMultiplicityValid(finalPlanBlueprintEntity, item.getId()))
.failOn(PlanPersist._descriptionTemplates).failWith(this.messageSource.getMessage("Validation.InvalidDescriptionTemplateMultiplicityOnPlan", new Object[]{PlanPersist._descriptionTemplates}, LocaleContextHolder.getLocale())),
this.refSpec()
.iff(() -> !this.isNull(item.getProperties()))
.iff(() -> !this.isNull(item.getProperties()) && finalStatusEntity != null)
.on(PlanPersist._properties)
.over(item.getProperties())
.using(() -> this.validatorFactory.validator(PlanPropertiesPersist.PlanPropertiesPersistValidator.class).setStatus(finalStatusEntity.getInternalStatus()).withDefinition(definition)),

View File

@ -97,6 +97,8 @@ public class Plan {
private List<Plan> otherPlanVersions;
public static final String _otherPlanVersions = "otherPlanVersions";
private List<PlanStatus> availableTransitions;
public static final String _availableTransitions = "availableTransitions";
private Boolean belongsToCurrentTenant;
public static final String _belongsToCurrentTenant = "belongsToCurrentTenant";
@ -318,4 +320,12 @@ public class Plan {
public void setOtherPlanVersions(List<Plan> otherPlanVersions) {
this.otherPlanVersions = otherPlanVersions;
}
public List<PlanStatus> getAvailableTransitions() {
return availableTransitions;
}
public void setAvailableTransitions(List<PlanStatus> availableTransitions) {
this.availableTransitions = availableTransitions;
}
}

View File

@ -425,6 +425,8 @@ public class PlanQuery extends QueryBase<PlanEntity> {
return PlanEntity._version;
else if (item.match(Plan._status))
return PlanEntity._status;
else if (item.prefix(Plan._status))
return PlanEntity._statusId;
else if (item.match(Plan._properties))
return PlanEntity._properties;
else if (item.prefix(Plan._properties))
@ -475,6 +477,7 @@ public class PlanQuery extends QueryBase<PlanEntity> {
item.setLabel(QueryBase.convertSafe(tuple, columns, PlanEntity._label, String.class));
item.setVersion(QueryBase.convertSafe(tuple, columns, PlanEntity._version, Short.class));
item.setStatus(QueryBase.convertSafe(tuple, columns, PlanEntity._status, PlanStatus.class));
item.setStatusId(QueryBase.convertSafe(tuple, columns, PlanEntity._statusId, UUID.class));
item.setVersionStatus(QueryBase.convertSafe(tuple, columns, PlanEntity._versionStatus, PlanVersionStatus.class));
item.setProperties(QueryBase.convertSafe(tuple, columns, PlanEntity._properties, String.class));
item.setGroupId(QueryBase.convertSafe(tuple, columns, PlanEntity._groupId, UUID.class));

View File

@ -266,8 +266,10 @@ public class DescriptionServiceImpl implements DescriptionService {
PlanEntity plan = this.entityManager.find(PlanEntity.class, data.getPlanId(), true);
if (plan == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{data.getPlanId(), Plan.class.getSimpleName()}, LocaleContextHolder.getLocale()));
PlanStatusEntity planStatusEntity = this.entityManager.find(PlanStatusEntity.class, plan.getStatusId(), true);
if (planStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{plan.getStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (plan.getStatus().equals(PlanStatus.Finalized) && isUpdate) throw new MyValidationException(this.errors.getPlanIsFinalized().getCode(), this.errors.getPlanIsFinalized().getMessage());
if (planStatusEntity.getInternalStatus() != null && planStatusEntity.getInternalStatus().equals(PlanStatus.Finalized) && isUpdate) throw new MyValidationException(this.errors.getPlanIsFinalized().getCode(), this.errors.getPlanIsFinalized().getMessage());
data.setLabel(model.getLabel());
data.setDescription(model.getDescription());
@ -504,7 +506,9 @@ public class DescriptionServiceImpl implements DescriptionService {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.descriptionAffiliation(model.getId())), Permission.FinalizeDescription);
PlanEntity planEntity = this.entityManager.find(PlanEntity.class, data.getPlanId(), true);
if (planEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{data.getPlanId(), PlanEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if(!planEntity.getStatus().equals(PlanStatus.Draft)) throw new MyValidationException(this.errors.getPlanIsFinalized().getCode(), this.errors.getPlanIsFinalized().getMessage());
PlanStatusEntity planStatusEntity = this.entityManager.find(PlanStatusEntity.class, planEntity.getStatusId(), true);
if (planStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{planEntity.getStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (planStatusEntity.getInternalStatus() != null && planStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) throw new MyValidationException(this.errors.getPlanIsFinalized().getCode(), this.errors.getPlanIsFinalized().getMessage());
}
data.setStatusId(model.getStatusId());
@ -533,11 +537,14 @@ public class DescriptionServiceImpl implements DescriptionService {
return null;
}
DescriptionStatusEntity statusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Finalized).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
if (statusEntity == null) throw new MyApplicationException("finalized status not found");
for (DescriptionEntity description: descriptions) {
DescriptionValidationResult descriptionValidationResult = new DescriptionValidationResult(description.getId(), DescriptionValidationOutput.Invalid);
DescriptionPersist.DescriptionPersistValidator validator = this.validatorFactory.validator(DescriptionPersist.DescriptionPersistValidator.class);
validator.validate(this.buildDescriptionPersist(description));
validator.validate(this.buildDescriptionPersist(description, statusEntity.getId()));
if (validator.result().isValid()) descriptionValidationResult.setResult(DescriptionValidationOutput.Valid);
descriptionValidationResults.add(descriptionValidationResult);
@ -1013,7 +1020,7 @@ public class DescriptionServiceImpl implements DescriptionService {
//region build persist
private @NotNull DescriptionPersist buildDescriptionPersist(DescriptionEntity data) throws InvalidApplicationException {
private @NotNull DescriptionPersist buildDescriptionPersist(DescriptionEntity data, UUID statusId) throws InvalidApplicationException {
DescriptionPersist persist = new DescriptionPersist();
if (data == null) return persist;
@ -1022,8 +1029,7 @@ public class DescriptionServiceImpl implements DescriptionService {
persist.setId(data.getId());
persist.setLabel(data.getLabel());
DescriptionStatusEntity statusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Finalized).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
if (statusEntity != null) persist.setStatusId(statusEntity.getId());
persist.setStatusId(statusId);
persist.setDescription(data.getDescription());
persist.setDescriptionTemplateId(data.getDescriptionTemplateId());
persist.setPlanId(data.getPlanId());

View File

@ -34,9 +34,7 @@ public interface PlanService {
void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException, IOException;
void finalize(UUID id, List<UUID> descriptionIds) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, IOException;
void undoFinalize(UUID id, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, IOException;
void setStatus(UUID id, UUID newStatusId, List<UUID> descriptionIds) throws InvalidApplicationException, IOException;
PlanValidationResult validate(UUID id) throws InvalidApplicationException;

View File

@ -386,7 +386,10 @@ public class PlanServiceImpl implements PlanService {
planQuery.setOrder(new Ordering().addDescending(Plan._version));
previousPlan = planQuery.count() > 0 ? planQuery.collect().getFirst() : null;
if (previousPlan != null){
if (previousPlan.getStatus().equals(PlanStatus.Finalized)) previousPlan.setVersionStatus(PlanVersionStatus.Current);
PlanStatusEntity previousPlanStatusEntity = this.entityManager.find(PlanStatusEntity.class, previousPlan.getStatusId(), true);
if (previousPlanStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{previousPlan.getStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (previousPlanStatusEntity.getInternalStatus() != null && previousPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) previousPlan.setVersionStatus(PlanVersionStatus.Current);
else previousPlan.setVersionStatus(PlanVersionStatus.NotFinalized);
this.entityManager.merge(previousPlan);
}
@ -428,6 +431,9 @@ public class PlanServiceImpl implements PlanService {
.count();
if (notFinalizedCount > 0) throw new MyValidationException(this.errors.getPlanNewVersionAlreadyCreatedDraft().getCode(), this.errors.getPlanNewVersionAlreadyCreatedDraft().getMessage());
PlanStatusEntity startingPlanStatusEntity = this.entityManager.find(PlanStatusEntity.class, this.planWorkflowService.getWorkFlowDefinition().getStartingStatusId(), true);
if (startingPlanStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{this.planWorkflowService.getWorkFlowDefinition().getStartingStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
PlanEntity newPlan = new PlanEntity();
newPlan.setId(UUID.randomUUID());
newPlan.setIsActive(IsActive.Active);
@ -440,6 +446,7 @@ public class PlanServiceImpl implements PlanService {
newPlan.setLabel(model.getLabel());
newPlan.setLanguage(oldPlanEntity.getLanguage());
newPlan.setStatus(PlanStatus.Draft);
newPlan.setStatusId(startingPlanStatusEntity.getId());
newPlan.setProperties(oldPlanEntity.getProperties());
newPlan.setBlueprintId(model.getBlueprintId());
newPlan.setAccessType(oldPlanEntity.getAccessType());
@ -591,7 +598,7 @@ public class PlanServiceImpl implements PlanService {
this.entityManager.flush();
this.updateVersionStatusAndSave(newPlan, PlanStatus.Draft, newPlan.getStatus());
this.updateVersionStatusAndSave(newPlan, PlanStatus.Draft, startingPlanStatusEntity.getInternalStatus());
this.entityManager.flush();
@ -855,9 +862,11 @@ public class PlanServiceImpl implements PlanService {
private void updateVersionStatusAndSave(PlanEntity data, PlanStatus previousStatus, PlanStatus newStatus) throws InvalidApplicationException {
if (previousStatus.equals(newStatus))
if (previousStatus == null && newStatus == null)
return;
if (previousStatus.equals(PlanStatus.Finalized) && newStatus.equals(PlanStatus.Draft)){
if (previousStatus != null && previousStatus.equals(newStatus))
return;
if (previousStatus != null && previousStatus.equals(PlanStatus.Finalized) && (newStatus == null || newStatus.equals(PlanStatus.Draft))){
boolean alreadyCreatedNewVersion = this.queryFactory.query(PlanQuery.class).disableTracking()
.versionStatuses(PlanVersionStatus.NotFinalized, PlanVersionStatus.Current)
.excludedIds(data.getId())
@ -870,7 +879,7 @@ public class PlanServiceImpl implements PlanService {
this.entityManager.merge(data);
}
if (newStatus.equals(PlanStatus.Finalized)) {
if (newStatus != null && newStatus.equals(PlanStatus.Finalized)) {
List<PlanEntity> latestVersionPlans = this.queryFactory.query(PlanQuery.class)
.versionStatuses(PlanVersionStatus.Current).excludedIds(data.getId())
.isActive(IsActive.Active).groupIds(data.getGroupId()).collect();
@ -915,6 +924,7 @@ public class PlanServiceImpl implements PlanService {
newPlan.setLabel(model.getLabel());
newPlan.setLanguage(existingPlanEntity.getLanguage());
newPlan.setStatus(PlanStatus.Draft);
newPlan.setStatusId(this.planWorkflowService.getWorkFlowDefinition().getStartingStatusId());
newPlan.setProperties(existingPlanEntity.getProperties());
newPlan.setBlueprintId(existingPlanEntity.getBlueprintId());
newPlan.setAccessType(existingPlanEntity.getAccessType());
@ -1103,6 +1113,7 @@ public class PlanServiceImpl implements PlanService {
newPlan.setLabel(model.getLabel());
newPlan.setLanguage(existingPlanEntity.getLanguage());
newPlan.setStatus(PlanStatus.Draft);
newPlan.setStatusId(this.planWorkflowService.getWorkFlowDefinition().getStartingStatusId());
newPlan.setProperties(existingPlanEntity.getProperties());
newPlan.setBlueprintId(blueprintEntityByTenant.getId());
newPlan.setAccessType(existingPlanEntity.getAccessType());
@ -1339,7 +1350,7 @@ public class PlanServiceImpl implements PlanService {
data.setIsActive(IsActive.Active);
data.setCreatedAt(Instant.now());
}
PlanStatus previousStatus = data.getStatus();
// PlanStatus previousStatus = data.getStatus();
PlanBlueprintEntity planBlueprintEntity = this.entityManager.find(PlanBlueprintEntity.class, model.getBlueprint(), true);
if (planBlueprintEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getBlueprint(), PlanBlueprint.class.getSimpleName()}, LocaleContextHolder.getLocale()));
@ -1361,7 +1372,7 @@ public class PlanServiceImpl implements PlanService {
this.entityManager.flush();
this.updateVersionStatusAndSave(data, previousStatus, data.getStatus());
// this.updateVersionStatusAndSave(data, previousStatus, data.getStatus());
this.entityManager.flush();
@ -1587,31 +1598,50 @@ public class PlanServiceImpl implements PlanService {
return data;
}
public void finalize(UUID id, List<UUID> descriptionIds) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, IOException {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation(id)), Permission.FinalizePlan);
public void setStatus(UUID id, UUID newStatusId, List<UUID> descriptionIds) throws InvalidApplicationException, IOException {
PlanEntity plan = this.queryFactory.query(PlanQuery.class).authorize(AuthorizationFlags.AllExceptPublic).ids(id).isActive(IsActive.Active).first();
if (plan == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, Plan.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (plan.getStatusId().equals(newStatusId)) throw new MyApplicationException("Old status equals with new");
if (plan == null){
throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, Plan.class.getSimpleName()}, LocaleContextHolder.getLocale()));
PlanStatusEntity oldPlanStatusEntity = this.entityManager.find(PlanStatusEntity.class, plan.getStatusId(), true);
if (oldPlanStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{plan.getStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
PlanStatusEntity newPlanStatusEntity = this.entityManager.find(PlanStatusEntity.class, newStatusId, true);
if (newPlanStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{newStatusId, PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (oldPlanStatusEntity.getInternalStatus() != null && oldPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) {
this.undoFinalize(plan, oldPlanStatusEntity, newPlanStatusEntity);
} else if (newPlanStatusEntity.getInternalStatus() != null && newPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) {
this.finalize(plan, descriptionIds ,oldPlanStatusEntity, newPlanStatusEntity);
} else {
plan.setStatusId(newPlanStatusEntity.getId());
plan.setUpdatedAt(Instant.now());
this.entityManager.merge(plan);
this.entityManager.flush();
}
}
if (plan.getStatus().equals(PlanStatus.Finalized)){
private void finalize(PlanEntity plan, List<UUID> descriptionIds, PlanStatusEntity oldPlanStatusEntity, PlanStatusEntity newPlanStatusEntity) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, IOException {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation(plan.getId())), Permission.FinalizePlan);
if (oldPlanStatusEntity.getInternalStatus() != null && oldPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)){
throw new MyApplicationException("Plan is already finalized");
}
if (this.validate(id).getResult().equals(PlanValidationOutput.Invalid)){
if (this.validate(plan.getId()).getResult().equals(PlanValidationOutput.Invalid)){
throw new MyApplicationException("Plan is invalid");
}
List<DescriptionEntity> descriptions = this.queryFactory.query(DescriptionQuery.class)
.authorize(AuthorizationFlags.AllExceptPublic).planIds(id).isActive(IsActive.Active).collect();
.authorize(AuthorizationFlags.AllExceptPublic).planIds(plan.getId()).isActive(IsActive.Active).collect();
if (!this.conventionService.isListNullOrEmpty(descriptions)) {
List<DescriptionStatusEntity> statusEntities = this.queryFactory.query(DescriptionStatusQuery.class).authorize(AuthorizationFlags.AllExceptPublic).ids(descriptions.stream().map(DescriptionEntity::getStatusId).distinct().toList()).isActive(IsActive.Active).collect();
if (this.conventionService.isListNullOrEmpty(statusEntities)) throw new MyApplicationException("Not found description statuses");
DescriptionStatusEntity finalizedStatusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Finalized).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
if (finalizedStatusEntity == null) throw new MyApplicationException("finalized status not found");
DescriptionStatusEntity descriptionFinalizedStatusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Finalized).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
if (descriptionFinalizedStatusEntity == null) throw new MyApplicationException("finalized status not found");
DescriptionStatusEntity canceledStatusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Canceled).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
if (canceledStatusEntity == null) throw new MyApplicationException("canceled status not found");
@ -1625,7 +1655,7 @@ public class PlanServiceImpl implements PlanService {
if (this.descriptionService.validate(List.of(description.getId())).getFirst().getResult().equals(DescriptionValidationOutput.Invalid)){
throw new MyApplicationException("Description is invalid");
}
if (finalizedStatusEntity != null) description.setStatusId(finalizedStatusEntity.getId());
description.setStatusId(descriptionFinalizedStatusEntity.getId());
description.setUpdatedAt(Instant.now());
description.setFinalizedAt(Instant.now());
this.entityManager.merge(description);
@ -1637,12 +1667,11 @@ public class PlanServiceImpl implements PlanService {
}
}
PlanStatus previousStatus = plan.getStatus();
plan.setStatus(PlanStatus.Finalized);
plan.setStatusId(newPlanStatusEntity.getId());
plan.setUpdatedAt(Instant.now());
plan.setFinalizedAt(Instant.now());
this.updateVersionStatusAndSave(plan, previousStatus, plan.getStatus());
this.updateVersionStatusAndSave(plan, oldPlanStatusEntity.getInternalStatus(), newPlanStatusEntity.getInternalStatus());
plan.setVersionStatus(PlanVersionStatus.Current);
this.entityManager.merge(plan);
@ -1654,24 +1683,21 @@ public class PlanServiceImpl implements PlanService {
this.sendNotification(plan);
}
public void undoFinalize(UUID id, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation(id)), Permission.UndoFinalizePlan);
PlanEntity plan = this.queryFactory.query(PlanQuery.class).authorize(AuthorizationFlags.AllExceptPublic).ids(id).isActive(IsActive.Active).firstAs(fields);
private void undoFinalize(PlanEntity plan, PlanStatusEntity oldPlanStatusEntity, PlanStatusEntity newPlanStatusEntity) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException {
this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation(plan.getId())), Permission.UndoFinalizePlan);
if (plan == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, Plan.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (!plan.getStatus().equals(PlanStatus.Finalized)) throw new MyApplicationException("Plan is already drafted");
if (oldPlanStatusEntity.getInternalStatus() == null && !oldPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) throw new MyApplicationException("Plan is already non finalized");
EntityDoiQuery entityDoiQuery = this.queryFactory.query(EntityDoiQuery.class).types(EntityType.Plan).entityIds(plan.getId()).isActive(IsActive.Active);
if (entityDoiQuery.count() > 0) throw new MyApplicationException("Plan is deposited");
plan.setStatus(PlanStatus.Draft);
plan.setStatusId(newPlanStatusEntity.getId());
plan.setUpdatedAt(Instant.now());
this.entityManager.merge(plan);
this.entityManager.flush();
this.updateVersionStatusAndSave(plan, PlanStatus.Finalized, plan.getStatus());
this.updateVersionStatusAndSave(plan, PlanStatus.Finalized, newPlanStatusEntity.getInternalStatus());
this.entityManager.flush();
PlanQuery planQuery = this.queryFactory.query(PlanQuery.class).disableTracking()
@ -1683,7 +1709,10 @@ public class PlanServiceImpl implements PlanService {
planQuery.setOrder(new Ordering().addDescending(Plan._version));
PlanEntity previousPlan = planQuery.count() > 0 ? planQuery.collect().getFirst() : null;
if (previousPlan != null){
if (previousPlan.getStatus().equals(PlanStatus.Finalized)) previousPlan.setVersionStatus(PlanVersionStatus.Current);
PlanStatusEntity previousPlanStatusEntity = this.entityManager.find(PlanStatusEntity.class, previousPlan.getStatusId(), true);
if (previousPlanStatusEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{previousPlan.getStatusId(), PlanStatusEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (previousPlanStatusEntity.getInternalStatus() != null && previousPlanStatusEntity.getInternalStatus().equals(PlanStatus.Finalized)) previousPlan.setVersionStatus(PlanVersionStatus.Current);
else previousPlan.setVersionStatus(PlanVersionStatus.NotFinalized);
this.entityManager.merge(previousPlan);
}
@ -1721,8 +1750,8 @@ public class PlanServiceImpl implements PlanService {
persist.setId(data.getId());
persist.setHash(data.getId().toString());
persist.setLabel(data.getLabel());
//TODO status PlanStatusEntity statusEntity = this.queryFactory.query(DescriptionStatusQuery.class).disableTracking().internalStatuses(DescriptionStatus.Finalized).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.descriptionstatus.DescriptionStatus._id));
// persist.setStatus(PlanStatus.Finalized);
PlanStatusEntity statusEntity = this.queryFactory.query(PlanStatusQuery.class).disableTracking().internalStatuses(PlanStatus.Finalized).isActives(IsActive.Active).firstAs(new BaseFieldSet().ensure(org.opencdmp.model.planstatus.PlanStatus._id));
if (statusEntity != null) persist.setStatusId(statusEntity.getId());
persist.setDescription(data.getDescription());
persist.setBlueprint(data.getBlueprintId());
persist.setAccessType(data.getAccessType());
@ -2316,7 +2345,6 @@ public class PlanServiceImpl implements PlanService {
PlanPersist persist = new PlanPersist();
persist.setLabel(label);
//TODO status persist.setStatus(PlanStatus.Draft);
persist.setDescription(planXml.getDescription());
persist.setAccessType(planXml.getAccess());
persist.setLanguage(planXml.getLanguage());
@ -2618,7 +2646,6 @@ public class PlanServiceImpl implements PlanService {
PlanPersist persist = new PlanPersist();
persist.setLabel(planCommonModelConfig.getLabel());
// TODO status persist.setStatus(PlanStatus.Draft);
persist.setDescription(model.getDescription());
switch (model.getAccessType()) {
case Public -> persist.setAccessType(PlanAccessType.Public);

View File

@ -10,10 +10,13 @@ import org.opencdmp.model.persist.planstatus.PlanStatusPersist;
import org.opencdmp.model.planstatus.PlanStatus;
import javax.management.InvalidApplicationException;
import java.util.List;
import java.util.UUID;
public interface PlanStatusService {
PlanStatus persist(PlanStatusPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, JAXBException;
void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException;
List<PlanStatus> getAvailableTransitionStatuses(UUID planId) throws InvalidApplicationException;
}

View File

@ -3,6 +3,7 @@ package org.opencdmp.service.planstatus;
import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.deleter.DeleterFactory;
import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.exception.MyForbiddenException;
import gr.cite.tools.exception.MyNotFoundException;
@ -13,24 +14,31 @@ import gr.cite.tools.logging.LoggerService;
import gr.cite.tools.logging.MapLogEntry;
import jakarta.xml.bind.JAXBException;
import org.jetbrains.annotations.NotNull;
import org.opencdmp.authorization.AuthorizationFlags;
import org.opencdmp.authorization.Permission;
import org.opencdmp.commons.XmlHandlingService;
import org.opencdmp.commons.enums.IsActive;
import org.opencdmp.commons.types.planstatus.PlanStatusDefinitionAuthorizationEntity;
import org.opencdmp.commons.types.planstatus.PlanStatusDefinitionAuthorizationItemEntity;
import org.opencdmp.commons.types.planstatus.PlanStatusDefinitionEntity;
import org.opencdmp.commons.types.planworkflow.PlanWorkflowDefinitionEntity;
import org.opencdmp.commons.types.planworkflow.PlanWorkflowDefinitionTransitionEntity;
import org.opencdmp.convention.ConventionService;
import org.opencdmp.data.PlanEntity;
import org.opencdmp.data.PlanStatusEntity;
import org.opencdmp.data.TenantEntityManager;
import org.opencdmp.errorcode.ErrorThesaurusProperties;
import org.opencdmp.event.EventBroker;
import org.opencdmp.model.builder.planstatus.PlanStatusBuilder;
import org.opencdmp.model.deleter.PlanStatusDeleter;
import org.opencdmp.model.descriptionstatus.DescriptionStatus;
import org.opencdmp.model.persist.planstatus.PlanStatusDefinitionAuthorizationItemPersist;
import org.opencdmp.model.persist.planstatus.PlanStatusDefinitionAuthorizationPersist;
import org.opencdmp.model.persist.planstatus.PlanStatusDefinitionPersist;
import org.opencdmp.model.persist.planstatus.PlanStatusPersist;
import org.opencdmp.model.planstatus.PlanStatus;
import org.opencdmp.query.PlanStatusQuery;
import org.opencdmp.service.planworkflow.PlanWorkflowService;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
@ -38,8 +46,10 @@ import org.springframework.stereotype.Service;
import javax.management.InvalidApplicationException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class PlanStatusServiceImpl implements PlanStatusService {
@ -55,8 +65,10 @@ public class PlanStatusServiceImpl implements PlanStatusService {
private final TenantEntityManager entityManager;
private final MessageSource messageSource;
private final ErrorThesaurusProperties errors;
private final QueryFactory queryFactory;
private final PlanWorkflowService planWorkflowService;
public PlanStatusServiceImpl(BuilderFactory builderFactory, DeleterFactory deleterFactory, AuthorizationService authorizationService, ConventionService conventionService, XmlHandlingService xmlHandlingService, TenantEntityManager entityManager, MessageSource messageSource, ErrorThesaurusProperties errors, EventBroker eventBroker) {
public PlanStatusServiceImpl(BuilderFactory builderFactory, DeleterFactory deleterFactory, AuthorizationService authorizationService, ConventionService conventionService, XmlHandlingService xmlHandlingService, TenantEntityManager entityManager, MessageSource messageSource, ErrorThesaurusProperties errors, EventBroker eventBroker, QueryFactory queryFactory, PlanWorkflowService planWorkflowService) {
this.builderFactory = builderFactory;
this.deleterFactory = deleterFactory;
@ -66,6 +78,8 @@ public class PlanStatusServiceImpl implements PlanStatusService {
this.entityManager = entityManager;
this.messageSource = messageSource;
this.errors = errors;
this.queryFactory = queryFactory;
this.planWorkflowService = planWorkflowService;
}
@Override
@ -149,4 +163,20 @@ public class PlanStatusServiceImpl implements PlanStatusService {
return data;
}
public List<PlanStatus> getAvailableTransitionStatuses(UUID planId) throws InvalidApplicationException {
PlanWorkflowDefinitionEntity definition = this.planWorkflowService.getWorkFlowDefinition();
PlanEntity plan = this.entityManager.find(PlanEntity.class, planId);
if (plan == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{planId, PlanEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
List<PlanWorkflowDefinitionTransitionEntity> availableTransitions = definition.getStatusTransitions().stream().filter(x -> x.getFromStatusId().equals(plan.getStatusId())).collect(Collectors.toList());
if (!this.conventionService.isListNullOrEmpty(availableTransitions)){
PlanStatusQuery query = this.queryFactory.query(PlanStatusQuery.class).authorize(AuthorizationFlags.AllExceptPublic).isActives(IsActive.Active).ids(availableTransitions.stream().map(PlanWorkflowDefinitionTransitionEntity::getToStatusId).distinct().toList());
FieldSet fieldSet = new BaseFieldSet().ensure(DescriptionStatus._id).ensure(DescriptionStatus._name).ensure(DescriptionStatus._internalStatus);
return this.builderFactory.builder(PlanStatusBuilder.class).authorize(AuthorizationFlags.AllExceptPublic).build(fieldSet, query.collectAs(fieldSet));
}
return new ArrayList<>();
}
}

View File

@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.xml.bind.JAXBException;
import org.opencdmp.audit.AuditableAction;
import org.opencdmp.authorization.AuthorizationFlags;
import org.opencdmp.commons.enums.IsActive;
import org.opencdmp.controllers.swagger.SwaggerHelpers;
import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader;
import org.opencdmp.controllers.swagger.annotation.Swagger400;
@ -28,15 +27,11 @@ import org.opencdmp.controllers.swagger.annotation.Swagger404;
import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses;
import org.opencdmp.data.DescriptionStatusEntity;
import org.opencdmp.model.builder.descriptionstatus.DescriptionStatusBuilder;
import org.opencdmp.model.builder.descriptionworkflow.DescriptionWorkflowBuilder;
import org.opencdmp.model.censorship.descriptionstatus.DescriptionStatusCensor;
import org.opencdmp.model.censorship.descriptionworkflow.DescriptionWorkflowCensor;
import org.opencdmp.model.descriptionstatus.DescriptionStatus;
import org.opencdmp.model.descriptionworkflow.DescriptionWorkflow;
import org.opencdmp.model.persist.descriptionstatus.DescriptionStatusPersist;
import org.opencdmp.model.result.QueryResult;
import org.opencdmp.query.DescriptionStatusQuery;
import org.opencdmp.query.DescriptionWorkflowQuery;
import org.opencdmp.query.lookup.DescriptionStatusLookup;
import org.opencdmp.service.descriptionstatus.DescriptionStatusService;
import org.slf4j.LoggerFactory;
@ -191,11 +186,7 @@ public class DescriptionStatusController {
@GetMapping("available-transitions/{descriptionId}")
@OperationWithTenantHeader(summary = "Get available status transitions for description", description = "",
responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(
schema = @Schema(
implementation = DescriptionWorkflow.class
))
))
responses = @ApiResponse(description = "OK", responseCode = "200"))
@Swagger404
public List<DescriptionStatus> GetAvailableTransitions(
@Parameter(name = "descriptionId", description = "The id of description", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID descriptionId

View File

@ -245,49 +245,29 @@ public class PlanController {
this.auditService.track(AuditableAction.Plan_Delete, "id", id);
}
@PostMapping("finalize/{id}")
@OperationWithTenantHeader(summary = "Finalize a plan by id", description = "",
@PostMapping("set-status/{id}/{newStatusId}")
@OperationWithTenantHeader(summary = "set status for a plan", description = "",
responses = @ApiResponse(description = "OK", responseCode = "200"))
@Swagger404
@Transactional
public boolean finalize(
@Parameter(name = "id", description = "The id of a plan to finalize", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
public boolean SetStatus(
@Parameter(name = "id", description = "The id of a plan", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@Parameter(name = "newStatusId", description = "The new status of a plan", example = "f1a3da63-0bff-438f-8b46-1a81ca176115", required = true) @PathVariable("newStatusId") UUID newStatusId,
@RequestBody DescriptionsToBeFinalized descriptions
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException {
logger.debug(new MapLogEntry("finalizing" + Plan.class.getSimpleName()).And("id", id).And("descriptionIds", descriptions.getDescriptionIds()));
logger.debug(new MapLogEntry("set status" + Plan.class.getSimpleName()).And("id", id).And("newStatusId", newStatusId).And("descriptionIds", descriptions.getDescriptionIds()));
this.planService.finalize(id, descriptions.getDescriptionIds());
this.planService.setStatus(id, newStatusId, descriptions.getDescriptionIds());
this.auditService.track(AuditableAction.Plan_Finalize, Map.ofEntries(
this.auditService.track(AuditableAction.Plan_SetStatus, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("id", id),
new AbstractMap.SimpleEntry<String, Object>("newStatusId", newStatusId),
new AbstractMap.SimpleEntry<String, Object>("descriptionIds", descriptions.getDescriptionIds())
));
return true;
}
@GetMapping("undo-finalize/{id}")
@OperationWithTenantHeader(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)", description = "",
responses = @ApiResponse(description = "OK", responseCode = "200"))
@Swagger404
@Transactional
public boolean undoFinalize(
@Parameter(name = "id", description = "The id of a plan to revert the finalization", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id,
@Parameter(name = "fieldSet", description = SwaggerHelpers.Commons.fieldset_description, required = true) FieldSet fieldSet
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, IOException, JAXBException {
logger.debug(new MapLogEntry("undo-finalizing" + Plan.class.getSimpleName()).And("id", id));
this.censorFactory.censor(PlanCensor.class).censor(fieldSet, null);
this.planService.undoFinalize(id, fieldSet);
this.auditService.track(AuditableAction.Plan_Undo_Finalize, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("id", id)
));
return true;
}
@GetMapping("validate/{id}")
@OperationWithTenantHeader(summary = "Validate if a plan is ready for finalization by id")
@Hidden

View File

@ -184,4 +184,19 @@ public class PlanStatusController {
this.auditService.track(AuditableAction.PlanStatus_Delete, "id", id);
}
@GetMapping("available-transitions/{planId}")
@OperationWithTenantHeader(summary = "Get available status transitions for plan", description = "",
responses = @ApiResponse(description = "OK", responseCode = "200"))
@Swagger404
public List<PlanStatus> GetAvailableTransitions(
@Parameter(name = "planId", description = "The id of plan", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID planId
) throws InvalidApplicationException {
logger.debug(new MapLogEntry("retrieving available statuses" + PlanStatus.class.getSimpleName()));
List<PlanStatus> availableTransitionStatuses = this.planStatusService.getAvailableTransitionStatuses(planId);
this.auditService.track(AuditableAction.PlanStatus_Delete, "planId", planId);
return availableTransitionStatuses;
}
}

View File

@ -14,6 +14,7 @@ import { PlanReference } from './plan-reference';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { AppPermission } from '@app/core/common/enum/permission.enum';
import { EntityType } from '@app/core/common/enum/entity-type';
import { PlanStatus } from '../plan-status/plan-status';
export interface BasePlan extends BaseEntity {
label?: string;
@ -26,7 +27,7 @@ export interface BasePlan extends BaseEntity {
planReferences?: PlanReference[];
entityDois?: EntityDoi[];
tenantId?: Guid;
status?: PlanStatusEnum;
status?: PlanStatus;
descriptions?: BaseDescription[];
}
export interface Plan extends BasePlan {
@ -96,7 +97,7 @@ export interface PlanDescriptionTemplate extends BaseEntity {
//
export interface PlanPersist extends BaseEntityPersist {
label: string;
status: PlanStatusEnum;
statusId: Guid;
properties: PlanPropertiesPersist;
description: String;
language: String;

View File

@ -57,6 +57,15 @@ export class PlanStatusService {
catchError((error: any) => throwError(() => error)));
}
getAvailableTransitions(planId: Guid, reqFields: string[] = []): Observable<Array<PlanStatus>> {
const url = `${this.apiBase}/available-transitions/${planId}`;
const options = { params: { f: reqFields } };
return this.http
.get<Array<PlanStatus>>(url, options).pipe(
catchError((error: any) => throwError(() => error)));
}
buildLookup(params: {
like?: string,
excludedIds?: Guid[],

View File

@ -94,24 +94,14 @@ export class PlanService {
catchError((error: any) => throwError(error)));
}
finalize(id: Guid, descriptionIds: Guid[] = []): Observable<Boolean> {
const url = `${this.apiBase}/finalize/${id}`;
setStatus(id: Guid, newStatusId: Guid, descriptionIds: Guid[] = []): Observable<Boolean> {
const url = `${this.apiBase}/set-status/${id}/${newStatusId}`;
return this.http
.post<Boolean>(url, {descriptionIds: descriptionIds}).pipe(
catchError((error: any) => throwError(error)));
}
undoFinalize(id: Guid, reqFields: string[] = []): Observable<Boolean> {
const url = `${this.apiBase}/undo-finalize/${id}`;
const options = { params: { f: reqFields } };
return this.http
.get<Boolean>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
validate(id: Guid): Observable<PlanValidationResult> {
const url = `${this.apiBase}/validate/${id}`;

View File

@ -29,6 +29,7 @@ import { debounceTime, map, takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { ActivityListingType } from '../dashboard.component';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Component({
selector: 'app-recent-edited-activity',
@ -232,7 +233,7 @@ export class RecentEditedActivityComponent extends BaseComponent implements OnIn
response.forEach(item => {
if (item.plan){
if (item.plan.descriptions) {
if (item.plan.status == PlanStatusEnum.Finalized) {
if (item.plan.status.internalStatus == PlanStatusEnum.Finalized) {
item.plan.descriptions = item.plan.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus === DescriptionStatusEnum.Finalized);
} else {
item.plan.descriptions = item.plan.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus != DescriptionStatusEnum.Canceled);
@ -271,7 +272,9 @@ export class RecentEditedActivityComponent extends BaseComponent implements OnIn
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.id)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.label)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.description)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.status)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.accessType)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.version)].join('.'),
[nameof<RecentActivityItem>(x => x.plan), nameof<Plan>(x => x.versionStatus)].join('.'),

View File

@ -70,9 +70,7 @@
<ng-container *ngIf="availableStatusesTransitions && availableStatusesTransitions.length > 0 && !isLocked && item.id">
<button *ngFor='let status of availableStatusesTransitions' [disabled]="saving" mat-button class="rounded-btn neutral mr-2" type="button" (click)="persistStatus(status)">{{ status.name }}</button>
</ng-container>
<!-- <button [disabled]="saving" *ngIf="canEdit && !isLocked && !viewOnly && hasReversableStatus() == false && canEdit" mat-button class="rounded-btn neutral mr-2" type="button" (click)="finalize()">{{ 'DESCRIPTION-EDITOR.ACTIONS.FINALIZE' | translate }}</button> -->
<button [disabled]="saving" *ngIf="isLocked" mat-button disabled class="rounded-btn neutral cursor-default" type="button">{{ 'PLAN-OVERVIEW.LOCKED' | translate}}</button>
<!-- <button [disabled]="saving" *ngIf="hasReversableStatus() && !isLocked && canEdit" mat-button class="rounded-btn neutral mr-2" (click)="reverse()" type="button">{{ 'DESCRIPTION-EDITOR.ACTIONS.REVERSE' | translate }}</button> -->
</div>
</div>
</div>

View File

@ -572,7 +572,7 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
hasReversableStatus(): boolean {
if (this.item?.plan) {
return (this.item.plan.status == PlanStatusEnum.Draft && this.isFinalized);
return (this.item.plan.status?.internalStatus == PlanStatusEnum.Draft && this.isFinalized);
} else {
return false;
}

View File

@ -6,6 +6,7 @@ import { DescriptionStatus } from '@app/core/model/description-status/descriptio
import { DescriptionTemplate } from '@app/core/model/description-template/description-template';
import { Description, DescriptionExternalIdentifier, DescriptionField, DescriptionPropertyDefinition, DescriptionPropertyDefinitionFieldSet, DescriptionPropertyDefinitionFieldSetItem, DescriptionReference, DescriptionReferenceData, DescriptionTag } from '@app/core/model/description/description';
import { DescriptionTemplatesInSection, PlanBlueprint, PlanBlueprintDefinition, PlanBlueprintDefinitionSection } from '@app/core/model/plan-blueprint/plan-blueprint';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
import { Plan, PlanDescriptionTemplate, PlanUser } from '@app/core/model/plan/plan';
import { PrefillingSource } from '@app/core/model/prefilling-source/prefilling-source';
import { ReferenceType } from '@app/core/model/reference-type/reference-type';
@ -121,7 +122,11 @@ export class DescriptionEditorEntityResolver extends BaseEditorResolver {
return [
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.id)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.label)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.status)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.isActive)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Plan>(x => x.authorizationFlags), AppPermission.EditPlan].join('.'),

View File

@ -44,6 +44,7 @@ import { PrincipalService } from '@app/core/services/http/principal.service';
import { DescriptionListingFilters } from './filtering/description-filter.component';
import { MatSelectChange } from '@angular/material/select';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Component({
selector: 'app-description-listing-component',
@ -507,7 +508,9 @@ export class DescriptionListingComponent extends BaseListingComponent<BaseDescri
[nameof<Description>(x => x.descriptionTemplate), nameof<DescriptionTemplate>(x => x.groupId)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.id)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.label)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.accessType)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.finalizedAt)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.blueprint), nameof<PlanBlueprint>(x => x.id)].join('.'),

View File

@ -46,6 +46,7 @@ import { DescriptionCopyDialogComponent } from '../description-copy-dialog/descr
import { RouterUtilsService } from '@app/core/services/router/router-utils.service';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { DescriptionStatusService } from '@app/core/services/description-status/description-status.service';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Component({
@ -79,7 +80,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
canAssignPlanUsers(): boolean {
const authorizationFlags = !this.isPublicView ? (this.description?.plan as Plan)?.authorizationFlags : [];
return (authorizationFlags?.some(x => x === AppPermission.AssignPlanUsers) || this.authentication.hasPermission(AppPermission.AssignPlanUsers)) &&
!this.isPublicView && this.description?.belongsToCurrentTenant && this.description?.plan?.status === PlanStatusEnum.Draft;
!this.isPublicView && this.description?.belongsToCurrentTenant && this.description?.plan?.status?.internalStatus != PlanStatusEnum.Finalized;
}
authorFocus: string;
@ -513,7 +514,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
}
hasReversableStatus(description: Description): boolean {
return description.plan.status == PlanStatusEnum.Draft && description?.status?.internalStatus == DescriptionStatusEnum.Finalized && this.canFinalize && this.availableStatusesTransitions?.find(x => x.internalStatus === DescriptionStatusEnum.Draft) != null
return description.plan.status.internalStatus == PlanStatusEnum.Draft && description?.status?.internalStatus == DescriptionStatusEnum.Finalized && this.canFinalize && this.availableStatusesTransitions?.find(x => x.internalStatus === DescriptionStatusEnum.Draft) != null
}
reverseFinalization(description: Description, statusId: Guid) {
@ -575,7 +576,9 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.id)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.label)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.accessType)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.authorizationFlags), AppPermission.InvitePlanUsers].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.blueprint), nameof<PlanBlueprint>(x => x.id)].join('.'),
[nameof<Description>(x => x.plan), nameof<Plan>(x => x.blueprint), nameof<PlanBlueprint>(x => x.label)].join('.'),

View File

@ -17,13 +17,16 @@
<div class="plan-subtitle">
<span *ngIf="isUserPlanRelated()" class="col-auto">{{ enumUtils.toPlanUserRolesString(planService.getCurrentUserRolesInPlan(plan?.planUsers)) }}</span>
<span *ngIf="isUserPlanRelated()">.</span>
<span class="col-auto" *ngIf="plan.status === planStatusEnum.Finalized && isPublic"><span class="material-icons icon-align">public</span>{{'TYPES.PLAN-VISIBILITY.PUBLIC' | translate}}</span>
<span *ngIf="plan.status === planStatusEnum.Finalized && !isPublic; else draft" class="col-auto"><span class="material-icons icon-align">done</span>{{ enumUtils.toPlanStatusString(plan.status) }}</span>
<span class="col-auto" *ngIf="plan.status.internalStatus === planStatusEnum.Finalized && isPublic"><span class="material-icons icon-align">public</span>{{'TYPES.PLAN-VISIBILITY.PUBLIC' | translate}}</span>
<span *ngIf="plan.status.internalStatus === planStatusEnum.Finalized && !isPublic; else draft" class="col-auto"><span class="material-icons icon-align">done</span>{{ plan.status.name }}</span>
<ng-template #draft>
<span *ngIf="plan.status === planStatusEnum.Draft && canEditPlan; else preview" class=" col-auto draft"><span class="material-icons icon-align">create</span>{{ enumUtils.toPlanStatusString(plan.status) }}</span>
<span *ngIf="plan.status.internalStatus === planStatusEnum.Draft && canEditPlan; else preview" class=" col-auto draft"><span class="material-icons icon-align">create</span>{{ plan.status.name }}</span>
</ng-template>
<ng-template #preview>
<span *ngIf="plan.status === planStatusEnum.Draft && !canEditPlan" class=" col-auto draft"><span class="material-icons-outlined mr-1 icon-align">visibility</span>{{ enumUtils.toPlanStatusString(plan.status) }}</span>
<span *ngIf="plan.status.internalStatus === planStatusEnum.Draft && !canEditPlan; else otherStatus" class=" col-auto draft"><span class="material-icons-outlined mr-1 icon-align">visibility</span>{{ plan.status.name }}</span>
</ng-template>
<ng-template #otherStatus>
<span *ngIf="!isPublic" class=" col-auto draft">{{ plan.status.name }}</span>
</ng-template>
<span>.</span>
<span class="col-auto">{{'PLAN-LISTING.VERSION' | translate}} {{plan.version}}</span>

View File

@ -87,7 +87,7 @@ export class PlanListingItemComponent extends BaseComponent implements OnInit {
}
get isDraftPlan(): boolean {
return this.plan.status == PlanStatusEnum.Draft;
return this.plan.status?.internalStatus == PlanStatusEnum.Draft;
}
constructor(
@ -112,16 +112,16 @@ export class PlanListingItemComponent extends BaseComponent implements OnInit {
ngOnInit() {
this.analyticsService.trackPageView(AnalyticsService.PlanListingItem);
if (this.plan.status == PlanStatusEnum.Draft) {
if (this.plan.status?.internalStatus == PlanStatusEnum.Draft) {
this.isDraft = true;
this.isFinalized = false;
this.isPublished = false;
}
else if (this.plan.status == PlanStatusEnum.Finalized) {
else if (this.plan.status?.internalStatus == PlanStatusEnum.Finalized) {
this.isDraft = false;
this.isFinalized = true;
this.isPublished = false;
if (this.plan.status === PlanStatusEnum.Finalized && this.plan.accessType === PlanAccessType.Public) { this.isPublished = true }
if (this.plan.status.internalStatus === PlanStatusEnum.Finalized && this.plan.accessType === PlanAccessType.Public) { this.isPublished = true }
}
}
@ -147,7 +147,7 @@ export class PlanListingItemComponent extends BaseComponent implements OnInit {
}
viewVersions(plan: Plan) {
if (plan.accessType == PlanAccessType.Public && plan.status == PlanStatusEnum.Finalized && !this.plan.authorizationFlags?.some(x => x === AppPermission.EditPlan)) {
if (plan.accessType == PlanAccessType.Public && plan.status?.internalStatus == PlanStatusEnum.Finalized && !this.plan.authorizationFlags?.some(x => x === AppPermission.EditPlan)) {
let url = this.router.createUrlTree(['/explore-plans/versions/', plan.groupId]);
window.open(url.toString(), '_blank');
} else {
@ -157,7 +157,7 @@ export class PlanListingItemComponent extends BaseComponent implements OnInit {
}
viewVersionsUrl(plan: Plan): string {
if (plan.accessType == PlanAccessType.Public && plan.status == PlanStatusEnum.Finalized && !this.plan.authorizationFlags?.some(x => x === AppPermission.EditPlan)) {
if (plan.accessType == PlanAccessType.Public && plan.status?.internalStatus == PlanStatusEnum.Finalized && !this.plan.authorizationFlags?.some(x => x === AppPermission.EditPlan)) {
let url = this.router.createUrlTree(['/explore-plans/versions/', plan.groupId]);
return url.toString();
} else {

View File

@ -45,6 +45,7 @@ import { PlanListingFilters } from './filtering/plan-filter.component';
import { Lookup } from '@common/model/lookup';
import { MatSelectChange } from '@angular/material/select';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Component({
selector: 'app-plan-listing-component',
@ -493,7 +494,11 @@ export class PlanListingComponent extends BaseListingComponent<BasePlan, PlanLoo
nameof<Plan>(x => x.id),
nameof<Plan>(x => x.label),
nameof<Plan>(x => x.description),
nameof<Plan>(x => x.status),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
nameof<Plan>(x => x.accessType),
nameof<Plan>(x => x.version),
nameof<Plan>(x => x.versionStatus),

View File

@ -51,7 +51,7 @@
{{plan.updatedAt | dateTimeFormatter: "d MMMM y"}}
</div>
<div class="col-auto d-flex">
<div *ngIf="plan.status== planStatusEnum.Finalized" class="d-flex flex-row uppercase">
<div *ngIf="plan.status.internalStatus== planStatusEnum.Finalized" class="d-flex flex-row uppercase">
<mat-icon class="status-icon">check</mat-icon>
{{'PLAN-OVERVIEW.FINALISED' | translate}}
</div>
@ -125,7 +125,7 @@
<ng-container *ngFor="let description of plan.descriptions">
<div class="col-12 col-lg-7 mt-1">
<a class="w-100 description" [routerLink]="isPublicView ? this.routerUtils.generateUrl(['/explore-descriptions/overview/public/', description.id]) : this.routerUtils.generateUrl(['/descriptions/overview/' + description.id])" target="_blank">
<button class="w-100" [ngClass]="{'plan-btn': description.status === descriptionStatusEnum.Draft, 'plan-finalized-btn': description.status === descriptionStatusEnum.Finalized}">
<button class="w-100" [ngClass]="{'plan-btn': description.status.internalStatus === descriptionStatusEnum.Draft, 'plan-finalized-btn': description.status.internalStatus === descriptionStatusEnum.Finalized}">
<div matTooltip="{{ description.label }}" class="d-flex align-items-center justify-content-between">
<div class="description-btn-label">{{ description.label }}</div>
<mat-icon>launch</mat-icon>
@ -181,7 +181,29 @@
<div class="row">
<div class="col-12">
<div class="frame mb-3 pt-4 pl-4 pr-5 pb-3">
<ng-container *ngIf="isDraftPlan() && canFinalizePlan() && !isLocked">
<ng-container *ngIf="availableStatusesTransitions && availableStatusesTransitions.length > 0 && !isLocked && hasDoi(plan) && plan.belongsToCurrentTenant != false">
<div *ngFor='let status of availableStatusesTransitions'>
<div class="row align-items-center" (click)="persistStatus(status)">
<div class="col-auto pr-0">
<button *ngIf="status.internalStatus === descriptionStatusEnum.Finalized && plan.status?.internalStatus != planStatusEnum.Finalized" mat-mini-fab class="finalize-btn">
<mat-icon class="mat-mini-fab-icon check-icon">check</mat-icon>
</button>
<button *ngIf="plan.status?.internalStatus === descriptionStatusEnum.Finalized" mat-mini-fab class="frame-btn">
<mat-icon class="mat-mini-fab-icon">unarchive</mat-icon>
</button>
</div>
<div class="col-auto pl-0">
<p class="mb-0 pl-2 frame-txt">{{ status.name }}</p>
</div>
</div>
<div class="row align-items-center">
<div class="col-12">
<hr class="hr-line">
</div>
</div>
</div>
</ng-container>
<!-- <ng-container *ngIf="isDraftPlan() && canFinalizePlan() && !isLocked">
<div class="row align-items-center" (click)="finalize(plan)">
<div class="col-auto pr-0">
<button mat-mini-fab class="finalize-btn">
@ -192,14 +214,14 @@
<p class="mb-0 pl-2 finalize-txt">{{ 'PLAN-OVERVIEW.ACTIONS.FINALIZE' | translate }}</p>
</div>
</div>
<div class="row align-items-center">
<div class="row align-items-center">0
<div class="col-12">
<hr class="hr-line">
</div>
</div>
</ng-container>
</ng-container> -->
<app-plan-deposit-dropdown *ngIf="(hasDoi(plan) || moreDeposit()) && isFinalizedPlan(plan) && !this.isPublicView && canDepositPlan(plan) && inputRepos.length > 0" [inputRepos]="inputRepos" [plan]="plan" (outputReposEmitter)="afterDeposit($event)"></app-plan-deposit-dropdown>
<ng-container *ngIf="isFinalizedPlan(plan) && hasDoi(plan) && !isPublishedPlan(plan) && canFinalizePlan(plan)">
<!-- <ng-container *ngIf="isFinalizedPlan(plan) && hasDoi(plan) && !isPublishedPlan(plan) && canFinalizePlan(plan)">
<div (click)="reverseFinalization()" class="row mb-3 align-items-center">
<div class="col-auto pr-0">
<button mat-mini-fab class="frame-btn">
@ -210,7 +232,7 @@
<p class="mb-0 pl-2 frame-txt">{{ 'PLAN-OVERVIEW.ACTIONS.REVERSE' | translate }}</p>
</div>
</div>
</ng-container>
</ng-container> -->
<ng-container *ngIf="canExportPlan() && fileTransformerService.availableFormatsFor(fileTransformerEntityTypeEnum.Plan).length > 0">
<div class="row mb-3 align-items-center">
<div class="col-auto pr-0">
@ -254,7 +276,7 @@
<app-plan-authors
[planUsers]="plan.planUsers"
[username]="userName"
[removeUser]="canAssignPlanUsers(plan) && plan.status === planStatusEnum.Draft"
[removeUser]="canAssignPlanUsers(plan) && (plan.status.internalStatus == null || plan.status.internalStatus === planStatusEnum.Finalized)"
(deleteAuthor)="removeUserFromPlan($event)"
/>
<div *ngIf="canInvitePlanUsers()" class="col-12 d-flex align-items-center justify-content-center mt-2">

View File

@ -51,6 +51,8 @@ import { PlanInvitationDialogComponent } from '../invitation/dialog/plan-invitat
import { NewVersionPlanDialogComponent } from '../new-version-dialog/plan-new-version-dialog.component';
import { RouterUtilsService } from '@app/core/services/router/router-utils.service';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
import { PlanStatusService } from '@app/core/services/plan/plan-status.service';
@Component({
selector: 'app-plan-overview',
@ -83,6 +85,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
authorFocus: string;
userName: string;
availableStatusesTransitions: PlanStatus[];
constructor(
public routerUtils: RouterUtilsService,
@ -108,6 +111,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
private breadcrumbService: BreadcrumbService,
private httpErrorHandlingService: HttpErrorHandlingService,
private userService: UserService,
private pLanStatusService: PlanStatusService
) {
super();
}
@ -130,10 +134,11 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
this.breadcrumbService.addIdResolvedValue(data.id?.toString(), data.label);
this.plan = data;
this.getAvailableStatuses(this.plan.id);
this.plan.planUsers = this.isActive ? data?.planUsers?.filter((x) => x.isActive === IsActive.Active) : data?.planUsers;
this.plan.otherPlanVersions = data.otherPlanVersions?.filter(x => x.isActive === IsActive.Active) || null;
if (this.plan.descriptions) {
if (this.plan.status == PlanStatusEnum.Finalized) {
if (this.plan.status?.internalStatus == PlanStatusEnum.Finalized) {
this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus === DescriptionStatusEnum.Finalized);
} else {
this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus !== DescriptionStatusEnum.Canceled);
@ -230,6 +235,15 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
return this.language.instant('PLAN-OVERVIEW.INFOS.UNAUTHORIZED-ORCID');
}
getAvailableStatuses(id: Guid){
this.pLanStatusService.getAvailableTransitions(id).pipe(takeUntil(this._destroyed))
.subscribe(
(statuses) => {
this.availableStatusesTransitions = statuses;
},
(error) => this.httpErrorHandlingService.handleBackedRequestError(error)
); }
onFetchingDeletedCallbackError(redirectRoot: string) {
this.router.navigate([this.routerUtils.generateUrl(redirectRoot)]);
}
@ -412,15 +426,15 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
}
isDraftPlan() {
return this.plan.status == PlanStatusEnum.Draft;
return this.plan.status?.internalStatus == PlanStatusEnum.Draft;
}
isFinalizedPlan(plan: Plan) {
return plan.status == PlanStatusEnum.Finalized;
return plan.status?.internalStatus == PlanStatusEnum.Finalized;
}
isPublishedPlan() {
return (this.plan.status == PlanStatusEnum.Finalized && this.plan.accessType === PlanAccessType.Public);
return (this.plan.status?.internalStatus == PlanStatusEnum.Finalized && this.plan.accessType === PlanAccessType.Public);
}
hasDoi() {
@ -442,7 +456,24 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
return (this.plan.entityDois.length < this.depositRepos.length);
}
finalize() {
persistStatus(status: PlanStatus) {
if (status.internalStatus != null && status.internalStatus === PlanStatusEnum.Finalized) {
this.finalize(status.id);
} else if (this.plan.status.internalStatus === PlanStatusEnum.Finalized){
this.reverseFinalization(status.id);
} else {
// other statuses
this.planService.setStatus(this.plan.id, status.id).pipe(takeUntil(this._destroyed))
.subscribe({
complete: () => {this.reloadPage(); this.onUpdateCallbackSuccess()},
error:(error: any) => {
this.onUpdateCallbackError(error)
}
});
}
}
finalize(newStatusId) {
const dialogRef = this.dialog.open(PlanFinalizeDialogComponent, {
maxWidth: '500px',
restoreFocus: false,
@ -454,7 +485,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe((result: PlanFinalizeDialogOutput) => {
if (result && !result.cancelled) {
this.planService.finalize(this.plan.id, result.descriptionsToBeFinalized)
this.planService.setStatus(this.plan.id, newStatusId, result.descriptionsToBeFinalized)
.pipe(takeUntil(this._destroyed))
.subscribe({
complete: () => {
@ -512,7 +543,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
}
}
reverseFinalization() {
reverseFinalization(newStatusId: Guid) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
restoreFocus: false,
data: {
@ -524,7 +555,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.planService.undoFinalize(this.plan.id, PlanEditorEntityResolver.lookupFields()).pipe(takeUntil(this._destroyed))
this.planService.setStatus(this.plan.id, newStatusId).pipe(takeUntil(this._destroyed))
.subscribe({
complete: () => {this.reloadPage(); this.onUpdateCallbackSuccess()},
error:(error: any) => {
@ -637,7 +668,9 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit {
nameof<Plan>(x => x.id),
nameof<Plan>(x => x.label),
nameof<Plan>(x => x.description),
nameof<Plan>(x => x.status),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
nameof<Plan>(x => x.accessType),
nameof<Plan>(x => x.version),
nameof<Plan>(x => x.versionStatus),

View File

@ -52,12 +52,11 @@
<button [disabled]="saving" mat-menu-item (click)="formSubmit()" type="button">{{ 'PLAN-EDITOR.ACTIONS.SAVE-AND-CONTINUE' | translate }}</button>
</mat-menu>
</div>
<div class="col-auto d-flex align-items-center" *ngIf="canFinalize && this.hasNotDoi() && formGroup.enabled" [matTooltipDisabled]="canFinalize && formGroup.pristine" matTooltip="{{'PLAN-EDITOR.ACTIONS.FINALIZE.CAN-NOT-FINALIZE' | translate}}">
<button [disabled]="!formGroup.pristine" mat-button class="rounded-btn primary-inverted mr-2" type="button" (click)="finalize()">{{ 'PLAN-EDITOR.ACTIONS.FINALIZE.FINALIZE' | translate }}</button>
<div *ngIf="availableStatusesTransitions && availableStatusesTransitions.length > 0 && !isLocked && !isNew && hasNotDoi()" class="col-auto d-flex align-items-center" [matTooltipDisabled]="formGroup.pristine" matTooltip="{{'PLAN-EDITOR.ACTIONS.FINALIZE.CAN-NOT-FINALIZE' | translate}}">
<button *ngFor='let status of availableStatusesTransitions' [disabled]="saving || !formGroup.pristine" mat-button class="rounded-btn primary-inverted mr-2" type="button" (click)="persistStatus(status)">{{ status.name}}</button>
</div>
<div *ngIf="formGroup.pristine" class="col-auto d-flex align-items-center">
<button [disabled]="saving" *ngIf="isLocked" mat-button class="rounded-btn primary-inverted mr-2" type="button">{{ 'PLAN-EDITOR.ACTIONS.LOCKED' | translate}}</button>
<button [disabled]="saving" *ngIf="canReverseFinalize && this.hasNotDoi()" mat-button class="rounded-btn primary-inverted mr-2" type="button" (click)="reverseFinalization()">{{ 'PLAN-EDITOR.ACTIONS.REVERSE' | translate }}</button>
<div *ngIf="isLocked" class="col-auto d-flex align-items-center">
<button class="col-auto d-flex align-items-center" [disabled]="saving" mat-button class="rounded-btn primary-inverted mr-2" type="button">{{ 'PLAN-EDITOR.ACTIONS.LOCKED' | translate}}</button>
</div>
</div>
</div>

View File

@ -60,6 +60,8 @@ import { PlanEditorService } from './plan-editor.service';
import { PlanEditorEntityResolver } from './resolvers/plan-editor-enitity.resolver';
import { FormAnnotationService } from '@app/ui/annotations/annotation-dialog-component/form-annotation.service';
import { PlanAssociatedUser } from '@app/core/model/user/user';
import { PlanStatusService } from '@app/core/services/plan/plan-status.service';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Component({
selector: 'app-plan-editor',
@ -97,6 +99,8 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
hoveredContact: number = -1;
availableStatusesTransitions: PlanStatus[];
singleAutocompleteBlueprintConfiguration: SingleAutoCompleteConfiguration = {
initialItems: (data?: any) => this.planBlueprintService.query(this.planBlueprintService.buildAutocompleteLookup(null, null, null, [PlanBlueprintStatus.Finalized])).pipe(map(x => x.items)),
filterFn: (searchQuery: string, data?: any) => this.planBlueprintService.query(this.planBlueprintService.buildAutocompleteLookup(searchQuery, null, null, [PlanBlueprintStatus.Finalized])).pipe(map(x => x.items)),
@ -136,7 +140,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
}
protected get canReverseFinalize(): boolean {
return !this.isDeleted && !this.isNew && this.canEdit && this.isLockedByUser && this.item.status == PlanStatusEnum.Finalized && (this.hasPermission(this.authService.permissionEnum.EditPlan) || this.item?.authorizationFlags?.some(x => x === AppPermission.EditPlan));
return !this.isDeleted && !this.isNew && this.canEdit && this.isLockedByUser && this.item.status?.internalStatus == PlanStatusEnum.Finalized && (this.hasPermission(this.authService.permissionEnum.EditPlan) || this.item?.authorizationFlags?.some(x => x === AppPermission.EditPlan));
}
protected canEditSection(id: Guid): boolean {
@ -193,6 +197,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
private breadcrumbService: BreadcrumbService,
public fileTransformerService: FileTransformerService,
private formAnnotationService: FormAnnotationService,
private pLanStatusService: PlanStatusService
) {
const descriptionLabel: string = route.snapshot.data['entity']?.label;
if (descriptionLabel) {
@ -261,8 +266,9 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
}
this.editorModel = data ? new PlanEditorModel().fromModel(data) : new PlanEditorModel();
if (data) {
if (data.id) this.getAvailableStatuses(data.id);
if (data.descriptions) {
if (data.status == PlanStatusEnum.Finalized) {
if (data.status?.internalStatus == PlanStatusEnum.Finalized) {
data.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status.internalStatus === DescriptionStatusEnum.Finalized);
} else {
data.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status.internalStatus !== DescriptionStatusEnum.Canceled);
@ -278,7 +284,6 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
this.selectedBlueprint = data?.blueprint;
this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.isFinalized = data ? data.status === PlanStatusEnum.Finalized : false;
if (data && data.id) {
const descriptionSectionPermissionResolverModel: DescriptionSectionPermissionResolver = {
@ -291,6 +296,14 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
this.buildForm();
}
if (data && data.status?.internalStatus == PlanStatusEnum.Finalized || this.isDeleted) {
this.viewOnly = true;
this.isFinalized = true;
this.formGroup.disable();
} else {
this.viewOnly = false;
}
if (this.item && this.item.id != null) {
this.checkLock(this.item.id, LockTargetType.Plan, 'PLAN-EDITOR.LOCKED-DIALOG.TITLE', 'PLAN-EDITOR.LOCKED-DIALOG.MESSAGE');
}
@ -304,16 +317,17 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.canEdit);
this.sectionToFieldsMap = this.prepareErrorIndication();
if (this.editorModel.status == PlanStatusEnum.Finalized || this.isDeleted) {
this.viewOnly = true;
this.isFinalized = this.editorModel.status == PlanStatusEnum.Finalized;
this.formGroup.disable();
} else {
this.viewOnly = false;
}
}
getAvailableStatuses(id: Guid){
this.pLanStatusService.getAvailableTransitions(id).pipe(takeUntil(this._destroyed))
.subscribe(
(statuses) => {
this.availableStatusesTransitions = statuses;
},
(error) => this.httpErrorHandlingService.handleBackedRequestError(error)
); }
prepareErrorIndication(): Map<string, PlanFieldIndicator> {
if (this.selectedBlueprint?.definition == null) return;
@ -449,7 +463,23 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
return (this.item.entityDois == null || this.item.entityDois.length == 0);
}
finalize() {
persistStatus(status: PlanStatus) {
if (status.internalStatus != null && status.internalStatus === PlanStatusEnum.Finalized) {
this.finalize(status.id);
} else if (this.item.status.internalStatus === PlanStatusEnum.Finalized){
this.reverseFinalization(status.id);
} else {
// other statuses
this.planService.setStatus(this.item.id, status.id).pipe(takeUntil(this._destroyed))
.subscribe(data => {
this.onCallbackSuccess()
}, (error: any) => {
this.onCallbackError(error)
});
}
}
finalize(newStatusId) {
const dialogRef = this.dialog.open(PlanFinalizeDialogComponent, {
maxWidth: '500px',
restoreFocus: false,
@ -461,7 +491,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe((result: PlanFinalizeDialogOutput) => {
if (result && !result.cancelled) {
this.planService.finalize(this.item.id, result.descriptionsToBeFinalized)
this.planService.setStatus(this.item.id, newStatusId, result.descriptionsToBeFinalized)
.pipe(takeUntil(this._destroyed))
.subscribe(data => {
this.onCallbackSuccess()
@ -474,7 +504,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
}
reverseFinalization() {
reverseFinalization(newStatusId: Guid) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
restoreFocus: false,
data: {
@ -486,7 +516,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.planService.undoFinalize(this.item.id, PlanEditorEntityResolver.lookupFields()).pipe(takeUntil(this._destroyed))
this.planService.setStatus(this.item.id, newStatusId).pipe(takeUntil(this._destroyed))
.subscribe(data => {
this.onCallbackSuccess()
}, (error: any) => {
@ -594,7 +624,6 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
label: this.formGroup.get('label').value,
description: this.formGroup.get('description').value,
blueprint: this.selectedBlueprint,
status: PlanStatusEnum.Draft
}
this.prepareForm(plan);

View File

@ -18,7 +18,7 @@ import { Guid } from "@common/types/guid";
export class PlanEditorModel extends BaseEditorModel implements PlanPersist {
label: string;
status: PlanStatusEnum;
statusId: Guid;
properties: PlanPropertiesEditorModel = new PlanPropertiesEditorModel(this.validationErrorModel);
description: String;
language: String;
@ -37,7 +37,7 @@ export class PlanEditorModel extends BaseEditorModel implements PlanPersist {
if (item) {
super.fromModel(item);
this.label = item.label;
this.status = item.status;
this.statusId = item.status?.id;
this.properties = new PlanPropertiesEditorModel(this.validationErrorModel).fromModel(item.properties, item.planReferences?.filter(x => x.isActive === IsActive.Active), item.blueprint);
this.description = item.description;
this.language = item.language;
@ -85,7 +85,7 @@ export class PlanEditorModel extends BaseEditorModel implements PlanPersist {
const formGroup = this.formBuilder.group({
id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators],
label: [{ value: this.label, disabled: disabled }, context.getValidation('label').validators],
status: [{ value: this.status, disabled: disabled }, context.getValidation('status').validators],
statusId: [{ value: this.statusId, disabled: disabled }, context.getValidation('statusId').validators],
properties: this.properties.buildForm({
rootPath: `properties.`,
disabled: disabled
@ -121,7 +121,7 @@ export class PlanEditorModel extends BaseEditorModel implements PlanPersist {
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'label', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'label')] });
baseValidationArray.push({ key: 'status', validators: [BackendErrorValidator(this.validationErrorModel, 'status')] });
baseValidationArray.push({ key: 'statusId', validators: [BackendErrorValidator(this.validationErrorModel, 'status')] });
baseValidationArray.push({ key: 'properties', validators: [BackendErrorValidator(this.validationErrorModel, 'properties')] });
baseValidationArray.push({ key: 'description', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'description')] });
baseValidationArray.push({ key: 'language', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'language')] });

View File

@ -16,6 +16,7 @@ import { takeUntil, tap } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { EntityDoi } from '@app/core/model/entity-doi/entity-doi';
import { DescriptionStatus } from '@app/core/model/description-status/description-status';
import { PlanStatus } from '@app/core/model/plan-status/plan-status';
@Injectable()
export class PlanEditorEntityResolver extends BaseEditorResolver {
@ -29,7 +30,13 @@ export class PlanEditorEntityResolver extends BaseEditorResolver {
...BaseEditorResolver.lookupFields(),
nameof<Plan>(x => x.id),
nameof<Plan>(x => x.label),
// TODO status remove later
nameof<Plan>(x => x.status),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.id)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.name)].join('.'),
[nameof<Plan>(x => x.status), nameof<PlanStatus>(x => x.internalStatus)].join('.'),
nameof<Plan>(x => x.versionStatus),
nameof<Plan>(x => x.groupId),
nameof<Plan>(x => x.description),