From 21fd258181c0df36def2b216b7744314da68ef16 Mon Sep 17 00:00:00 2001 From: "CITE\\amentis" Date: Fri, 20 Sep 2024 11:50:19 +0300 Subject: [PATCH] implement plan status logic (in progress) --- .../org/opencdmp/audit/AuditableAction.java | 1 + .../java/org/opencdmp/model/PublicPlan.java | 7 +- .../org/opencdmp/model/PublicPlanStatus.java | 32 +++++++ .../builder/PublicDescriptionBuilder.java | 2 +- .../model/builder/PublicPlanBuilder.java | 35 ++++++- .../builder/PublicPlanStatusBuilder.java | 52 ++++++++++ .../model/builder/plan/PlanBuilder.java | 2 +- .../opencdmp/model/persist/PlanPersist.java | 2 +- .../java/org/opencdmp/model/plan/Plan.java | 10 ++ .../java/org/opencdmp/query/PlanQuery.java | 3 + .../description/DescriptionServiceImpl.java | 18 ++-- .../opencdmp/service/plan/PlanService.java | 4 +- .../service/plan/PlanServiceImpl.java | 95 ++++++++++++------- .../service/planstatus/PlanStatusService.java | 3 + .../planstatus/PlanStatusServiceImpl.java | 32 ++++++- .../DescriptionStatusController.java | 11 +-- .../opencdmp/controllers/PlanController.java | 38 ++------ .../controllers/PlanStatusController.java | 15 +++ frontend/src/app/core/model/plan/plan.ts | 5 +- .../core/services/plan/plan-status.service.ts | 9 ++ .../app/core/services/plan/plan.service.ts | 14 +-- .../recent-edited-activity.component.ts | 7 +- .../editor/description-editor.component.html | 2 - .../editor/description-editor.component.ts | 2 +- .../description-editor-entity.resolver.ts | 7 +- .../listing/description-listing.component.ts | 5 +- .../description-overview.component.ts | 9 +- .../plan-listing-item.component.html | 11 ++- .../plan-listing-item.component.ts | 12 +-- .../ui/plan/listing/plan-listing.component.ts | 7 +- .../overview/plan-overview.component.html | 38 ++++++-- .../plan/overview/plan-overview.component.ts | 51 ++++++++-- .../plan-editor.component.html | 9 +- .../plan-editor.component.ts | 61 ++++++++---- .../plan-editor.model.ts | 8 +- .../resolvers/plan-editor-enitity.resolver.ts | 7 ++ 36 files changed, 458 insertions(+), 168 deletions(-) create mode 100644 backend/core/src/main/java/org/opencdmp/model/PublicPlanStatus.java create mode 100644 backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanStatusBuilder.java diff --git a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java index 73325a616..c8d18b37d 100644 --- a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java +++ b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java @@ -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"); diff --git a/backend/core/src/main/java/org/opencdmp/model/PublicPlan.java b/backend/core/src/main/java/org/opencdmp/model/PublicPlan.java index b95aded25..1415cc27a 100644 --- a/backend/core/src/main/java/org/opencdmp/model/PublicPlan.java +++ b/backend/core/src/main/java/org/opencdmp/model/PublicPlan.java @@ -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; } diff --git a/backend/core/src/main/java/org/opencdmp/model/PublicPlanStatus.java b/backend/core/src/main/java/org/opencdmp/model/PublicPlanStatus.java new file mode 100644 index 000000000..0962270f4 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/PublicPlanStatus.java @@ -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; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/PublicDescriptionBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/PublicDescriptionBuilder.java index 8d7bc74b0..6b8989995 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/PublicDescriptionBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/PublicDescriptionBuilder.java @@ -186,7 +186,7 @@ public class PublicDescriptionBuilder extends BaseBuilder 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); diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanBuilder.java index 560305ba5..e63666b61 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanBuilder.java @@ -71,6 +71,9 @@ public class PublicPlanBuilder extends BaseBuilder { FieldSet otherPlanVersionsFields = fields.extractPrefixed(this.asPrefix(PublicPlan._otherPlanVersions)); Map> otherPlanVersionsMap = this.collectOtherPlanVersions(otherPlanVersionsFields, data); + FieldSet planStatusFields = fields.extractPrefixed(this.asPrefix(PublicPlan._status)); + Map 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 { 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 { return itemMap; } + private Map collectPlanStatuses(FieldSet fields, List data) throws MyApplicationException { + if (fields.isEmpty() || data.isEmpty()) + return null; + this.logger.debug("checking related - {}", PublicPlanStatus.class.getSimpleName()); + + Map 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; + } + } diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanStatusBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanStatusBuilder.java new file mode 100644 index 000000000..f0cba11c5 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/builder/PublicPlanStatusBuilder.java @@ -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 { + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + @Autowired + public PublicPlanStatusBuilder( + ConventionService conventionService) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(PublicPlanStatusBuilder.class))); + } + + public PublicPlanStatusBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List 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 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; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/plan/PlanBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/plan/PlanBuilder.java index 1f84d649e..1456cb128 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/plan/PlanBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/plan/PlanBuilder.java @@ -87,7 +87,7 @@ public class PlanBuilder extends BaseBuilder { List models = new ArrayList<>(); - FieldSet statusFields = fields.extractPrefixed(this.asPrefix(Description._status)); + FieldSet statusFields = fields.extractPrefixed(this.asPrefix(Plan._status)); Map statusItemsMap = this.collectPlanStatuses(statusFields, data); FieldSet entityDoisFields = fields.extractPrefixed(this.asPrefix(Plan._entityDois)); diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/PlanPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/PlanPersist.java index 1b39c96a4..225bc8b80 100644 --- a/backend/core/src/main/java/org/opencdmp/model/persist/PlanPersist.java +++ b/backend/core/src/main/java/org/opencdmp/model/persist/PlanPersist.java @@ -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)), diff --git a/backend/core/src/main/java/org/opencdmp/model/plan/Plan.java b/backend/core/src/main/java/org/opencdmp/model/plan/Plan.java index 298edbbe5..a52722487 100644 --- a/backend/core/src/main/java/org/opencdmp/model/plan/Plan.java +++ b/backend/core/src/main/java/org/opencdmp/model/plan/Plan.java @@ -97,6 +97,8 @@ public class Plan { private List otherPlanVersions; public static final String _otherPlanVersions = "otherPlanVersions"; + private List 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 otherPlanVersions) { this.otherPlanVersions = otherPlanVersions; } + + public List getAvailableTransitions() { + return availableTransitions; + } + + public void setAvailableTransitions(List availableTransitions) { + this.availableTransitions = availableTransitions; + } } diff --git a/backend/core/src/main/java/org/opencdmp/query/PlanQuery.java b/backend/core/src/main/java/org/opencdmp/query/PlanQuery.java index 6b195c891..19ad1f409 100644 --- a/backend/core/src/main/java/org/opencdmp/query/PlanQuery.java +++ b/backend/core/src/main/java/org/opencdmp/query/PlanQuery.java @@ -425,6 +425,8 @@ public class PlanQuery extends QueryBase { 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 { 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)); diff --git a/backend/core/src/main/java/org/opencdmp/service/description/DescriptionServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/description/DescriptionServiceImpl.java index 4146f9b06..61ecc0ee4 100644 --- a/backend/core/src/main/java/org/opencdmp/service/description/DescriptionServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/description/DescriptionServiceImpl.java @@ -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()); diff --git a/backend/core/src/main/java/org/opencdmp/service/plan/PlanService.java b/backend/core/src/main/java/org/opencdmp/service/plan/PlanService.java index 2970494af..f53f6466b 100644 --- a/backend/core/src/main/java/org/opencdmp/service/plan/PlanService.java +++ b/backend/core/src/main/java/org/opencdmp/service/plan/PlanService.java @@ -34,9 +34,7 @@ public interface PlanService { void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException, IOException; - void finalize(UUID id, List 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 descriptionIds) throws InvalidApplicationException, IOException; PlanValidationResult validate(UUID id) throws InvalidApplicationException; diff --git a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java index 244ca06db..98eefe61f 100644 --- a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java @@ -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 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 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 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 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 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 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); @@ -1636,13 +1666,12 @@ 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); diff --git a/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusService.java b/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusService.java index f98cb87dd..a2df92905 100644 --- a/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusService.java +++ b/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusService.java @@ -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 getAvailableTransitionStatuses(UUID planId) throws InvalidApplicationException; } diff --git a/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusServiceImpl.java index 3ac7b896d..18a66d3d9 100644 --- a/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/planstatus/PlanStatusServiceImpl.java @@ -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 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 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<>(); + } } diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionStatusController.java b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionStatusController.java index ea4a77cd9..98142d043 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionStatusController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionStatusController.java @@ -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 GetAvailableTransitions( @Parameter(name = "descriptionId", description = "The id of description", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID descriptionId diff --git a/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java b/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java index 6e0005f45..309096e3e 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java @@ -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("id", id), + new AbstractMap.SimpleEntry("newStatusId", newStatusId), new AbstractMap.SimpleEntry("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("id", id) - )); - - return true; - } - @GetMapping("validate/{id}") @OperationWithTenantHeader(summary = "Validate if a plan is ready for finalization by id") @Hidden diff --git a/backend/web/src/main/java/org/opencdmp/controllers/PlanStatusController.java b/backend/web/src/main/java/org/opencdmp/controllers/PlanStatusController.java index 65f670596..4fd5abec4 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/PlanStatusController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/PlanStatusController.java @@ -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 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 availableTransitionStatuses = this.planStatusService.getAvailableTransitionStatuses(planId); + + this.auditService.track(AuditableAction.PlanStatus_Delete, "planId", planId); + return availableTransitionStatuses; + } } diff --git a/frontend/src/app/core/model/plan/plan.ts b/frontend/src/app/core/model/plan/plan.ts index 245f3f7c0..f9fe2512f 100644 --- a/frontend/src/app/core/model/plan/plan.ts +++ b/frontend/src/app/core/model/plan/plan.ts @@ -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; diff --git a/frontend/src/app/core/services/plan/plan-status.service.ts b/frontend/src/app/core/services/plan/plan-status.service.ts index 0d72b5644..b472ed58d 100644 --- a/frontend/src/app/core/services/plan/plan-status.service.ts +++ b/frontend/src/app/core/services/plan/plan-status.service.ts @@ -57,6 +57,15 @@ export class PlanStatusService { catchError((error: any) => throwError(() => error))); } + getAvailableTransitions(planId: Guid, reqFields: string[] = []): Observable> { + const url = `${this.apiBase}/available-transitions/${planId}`; + const options = { params: { f: reqFields } }; + + return this.http + .get>(url, options).pipe( + catchError((error: any) => throwError(() => error))); + } + buildLookup(params: { like?: string, excludedIds?: Guid[], diff --git a/frontend/src/app/core/services/plan/plan.service.ts b/frontend/src/app/core/services/plan/plan.service.ts index 78cece372..0758ccb2f 100644 --- a/frontend/src/app/core/services/plan/plan.service.ts +++ b/frontend/src/app/core/services/plan/plan.service.ts @@ -94,24 +94,14 @@ export class PlanService { catchError((error: any) => throwError(error))); } - finalize(id: Guid, descriptionIds: Guid[] = []): Observable { - const url = `${this.apiBase}/finalize/${id}`; + setStatus(id: Guid, newStatusId: Guid, descriptionIds: Guid[] = []): Observable { + const url = `${this.apiBase}/set-status/${id}/${newStatusId}`; return this.http .post(url, {descriptionIds: descriptionIds}).pipe( catchError((error: any) => throwError(error))); } - undoFinalize(id: Guid, reqFields: string[] = []): Observable { - const url = `${this.apiBase}/undo-finalize/${id}`; - - const options = { params: { f: reqFields } }; - - return this.http - .get(url, options).pipe( - catchError((error: any) => throwError(error))); - } - validate(id: Guid): Observable { const url = `${this.apiBase}/validate/${id}`; diff --git a/frontend/src/app/ui/dashboard/recent-edited-activity/recent-edited-activity.component.ts b/frontend/src/app/ui/dashboard/recent-edited-activity/recent-edited-activity.component.ts index 1b46896b5..b19b69bcd 100644 --- a/frontend/src/app/ui/dashboard/recent-edited-activity/recent-edited-activity.component.ts +++ b/frontend/src/app/ui/dashboard/recent-edited-activity/recent-edited-activity.component.ts @@ -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(x => x.plan), nameof(x => x.id)].join('.'), [nameof(x => x.plan), nameof(x => x.label)].join('.'), [nameof(x => x.plan), nameof(x => x.description)].join('.'), - [nameof(x => x.plan), nameof(x => x.status)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), [nameof(x => x.plan), nameof(x => x.accessType)].join('.'), [nameof(x => x.plan), nameof(x => x.version)].join('.'), [nameof(x => x.plan), nameof(x => x.versionStatus)].join('.'), diff --git a/frontend/src/app/ui/description/editor/description-editor.component.html b/frontend/src/app/ui/description/editor/description-editor.component.html index abe0a7f15..d431952d0 100644 --- a/frontend/src/app/ui/description/editor/description-editor.component.html +++ b/frontend/src/app/ui/description/editor/description-editor.component.html @@ -70,9 +70,7 @@ - - diff --git a/frontend/src/app/ui/description/editor/description-editor.component.ts b/frontend/src/app/ui/description/editor/description-editor.component.ts index 6395ba5d8..a0f3f1c7f 100644 --- a/frontend/src/app/ui/description/editor/description-editor.component.ts +++ b/frontend/src/app/ui/description/editor/description-editor.component.ts @@ -572,7 +572,7 @@ export class DescriptionEditorComponent extends BaseEditor(x => x.id)].join('.'), (prefix ? prefix + '.' : '') + [nameof(x => x.label)].join('.'), - (prefix ? prefix + '.' : '') + [nameof(x => x.status)].join('.'), + + (prefix ? prefix + '.' : '') + [nameof(x => x.status), nameof(x => x.id)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.status), nameof(x => x.name)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.isActive)].join('.'), (prefix ? prefix + '.' : '') + [nameof(x => x.authorizationFlags), AppPermission.EditPlan].join('.'), diff --git a/frontend/src/app/ui/description/listing/description-listing.component.ts b/frontend/src/app/ui/description/listing/description-listing.component.ts index befdde935..2b9af85a4 100644 --- a/frontend/src/app/ui/description/listing/description-listing.component.ts +++ b/frontend/src/app/ui/description/listing/description-listing.component.ts @@ -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(x => x.descriptionTemplate), nameof(x => x.groupId)].join('.'), [nameof(x => x.plan), nameof(x => x.id)].join('.'), [nameof(x => x.plan), nameof(x => x.label)].join('.'), - [nameof(x => x.plan), nameof(x => x.status)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), [nameof(x => x.plan), nameof(x => x.accessType)].join('.'), [nameof(x => x.plan), nameof(x => x.finalizedAt)].join('.'), [nameof(x => x.plan), nameof(x => x.blueprint), nameof(x => x.id)].join('.'), diff --git a/frontend/src/app/ui/description/overview/description-overview.component.ts b/frontend/src/app/ui/description/overview/description-overview.component.ts index 4955accbb..9b3998f55 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.ts +++ b/frontend/src/app/ui/description/overview/description-overview.component.ts @@ -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(x => x.plan), nameof(x => x.id)].join('.'), [nameof(x => x.plan), nameof(x => x.label)].join('.'), [nameof(x => x.plan), nameof(x => x.accessType)].join('.'), - [nameof(x => x.plan), nameof(x => x.status)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.plan), nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), [nameof(x => x.plan), nameof(x => x.authorizationFlags), AppPermission.InvitePlanUsers].join('.'), [nameof(x => x.plan), nameof(x => x.blueprint), nameof(x => x.id)].join('.'), [nameof(x => x.plan), nameof(x => x.blueprint), nameof(x => x.label)].join('.'), diff --git a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.html b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.html index 0bc1af233..d5af1acf4 100644 --- a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.html +++ b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.html @@ -17,13 +17,16 @@
{{ enumUtils.toPlanUserRolesString(planService.getCurrentUserRolesInPlan(plan?.planUsers)) }} . - public{{'TYPES.PLAN-VISIBILITY.PUBLIC' | translate}} - done{{ enumUtils.toPlanStatusString(plan.status) }} + public{{'TYPES.PLAN-VISIBILITY.PUBLIC' | translate}} + done{{ plan.status.name }} - create{{ enumUtils.toPlanStatusString(plan.status) }} + create{{ plan.status.name }} - visibility{{ enumUtils.toPlanStatusString(plan.status) }} + visibility{{ plan.status.name }} + + + {{ plan.status.name }} . {{'PLAN-LISTING.VERSION' | translate}} {{plan.version}} diff --git a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts index 355283033..e630abdfa 100644 --- a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts +++ b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts @@ -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 { diff --git a/frontend/src/app/ui/plan/listing/plan-listing.component.ts b/frontend/src/app/ui/plan/listing/plan-listing.component.ts index 4542aeea3..874383797 100644 --- a/frontend/src/app/ui/plan/listing/plan-listing.component.ts +++ b/frontend/src/app/ui/plan/listing/plan-listing.component.ts @@ -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(x => x.id), nameof(x => x.label), nameof(x => x.description), - nameof(x => x.status), + + [nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), + nameof(x => x.accessType), nameof(x => x.version), nameof(x => x.versionStatus), diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.html b/frontend/src/app/ui/plan/overview/plan-overview.component.html index 5b5abc602..25aafe0b9 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.html +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.html @@ -51,7 +51,7 @@ {{plan.updatedAt | dateTimeFormatter: "d MMMM y"}}
+ + - +
@@ -254,7 +276,7 @@
diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.ts b/frontend/src/app/ui/plan/overview/plan-overview.component.ts index d2288a3ab..28ae2b750 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.ts +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.ts @@ -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(x => x.id), nameof(x => x.label), nameof(x => x.description), - nameof(x => x.status), + [nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), nameof(x => x.accessType), nameof(x => x.version), nameof(x => x.versionStatus), diff --git a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.html b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.html index 1cf2b38b0..ff12dd0ac 100644 --- a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.html +++ b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.html @@ -52,12 +52,11 @@
-
- +
+
-
- - +
+
diff --git a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.ts b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.ts index 6fd461922..896386bdb 100644 --- a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.ts +++ b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.component.ts @@ -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 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 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 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 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 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 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 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 { if (this.selectedBlueprint?.definition == null) return; @@ -449,7 +463,23 @@ export class PlanEditorComponent extends BaseEditor 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 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 imple } - reverseFinalization() { + reverseFinalization(newStatusId: Guid) { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { restoreFocus: false, data: { @@ -486,7 +516,7 @@ export class PlanEditorComponent extends BaseEditor 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 imple label: this.formGroup.get('label').value, description: this.formGroup.get('description').value, blueprint: this.selectedBlueprint, - status: PlanStatusEnum.Draft } this.prepareForm(plan); diff --git a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.model.ts b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.model.ts index 5994f394a..5bac9a766 100644 --- a/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.model.ts +++ b/frontend/src/app/ui/plan/plan-editor-blueprint/plan-editor.model.ts @@ -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(); 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')] }); diff --git a/frontend/src/app/ui/plan/plan-editor-blueprint/resolvers/plan-editor-enitity.resolver.ts b/frontend/src/app/ui/plan/plan-editor-blueprint/resolvers/plan-editor-enitity.resolver.ts index ea915d420..d51e88226 100644 --- a/frontend/src/app/ui/plan/plan-editor-blueprint/resolvers/plan-editor-enitity.resolver.ts +++ b/frontend/src/app/ui/plan/plan-editor-blueprint/resolvers/plan-editor-enitity.resolver.ts @@ -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(x => x.id), nameof(x => x.label), + // TODO status remove later nameof(x => x.status), + + [nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.status), nameof(x => x.name)].join('.'), + [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), + nameof(x => x.versionStatus), nameof(x => x.groupId), nameof(x => x.description),