rewrite workflow and handle finalize differently

This commit is contained in:
Julien Fastré 2022-01-28 23:42:21 +01:00
parent fdafe7c82b
commit 86e7b0f007
11 changed files with 180 additions and 60 deletions

View File

@ -25,6 +25,7 @@ use Iterator;
use RuntimeException;
use Symfony\Component\Serializer\Annotation as Serializer;
use function count;
use function is_array;
/**
* @ORM\Entity
@ -72,6 +73,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
*/
private Collection $steps;
/**
* @var null|array|EntityWorkflowStep[]
*/
private ?array $stepsChainedCache = null;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_final")
@ -129,10 +135,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
if (!$this->steps->contains($step)) {
$this->steps[] = $step;
$step->setEntityWorkflow($this);
if ($this->isFinalize()) {
$step->setFinalizeAfter(true);
}
}
return $this;
@ -253,27 +255,33 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function getStepsChained(): array
{
if (is_array($this->stepsChainedCache)) {
return $this->stepsChainedCache;
}
$iterator = $this->steps->getIterator();
$previous = $next = $current = null;
$current = null;
$steps = [];
$iterator->rewind();
while ($iterator->valid()) {
do {
$previous = $current;
$steps[] = $current = $iterator->current();
$current = $iterator->current();
$steps[] = $current;
$current->setPrevious($previous);
$iterator->next();
if ($iterator->valid()) {
$next = $iterator->current();
$current->setNext($iterator->current());
} else {
$next = null;
$current->setNext(null);
}
} while ($iterator->valid());
$current->setNext($next);
}
$this->stepsChainedCache = $steps;
return $steps;
}
@ -308,7 +316,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->workflowName;
}
public function isFinalize(): bool
public function isFinal(): bool
{
$steps = $this->getStepsChained();
@ -320,7 +328,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
/** @var EntityWorkflowStep $last */
$last = end($steps);
return $last->getPrevious()->isFinalizeAfter();
return $last->isFinal();
}
public function isFreeze(): bool

View File

@ -53,11 +53,6 @@ class EntityWorkflowStep
*/
private ?EntityWorkflow $entityWorkflow = null;
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
private bool $finalizeAfter = false;
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
@ -70,6 +65,11 @@ class EntityWorkflowStep
*/
private ?int $id = null;
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
private bool $isFinal = false;
/**
* filled by @see{EntityWorkflow::getStepsChained}.
*/
@ -187,9 +187,9 @@ class EntityWorkflowStep
return $this->transitionByEmail;
}
public function isFinalizeAfter(): bool
public function isFinal(): bool
{
return $this->finalizeAfter;
return $this->isFinal;
}
public function isFreezeAfter(): bool
@ -244,16 +244,16 @@ class EntityWorkflowStep
return $this;
}
public function setFinalizeAfter(bool $finalizeAfter): EntityWorkflowStep
public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep
{
$this->finalizeAfter = $finalizeAfter;
$this->freezeAfter = $freezeAfter;
return $this;
}
public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep
public function setIsFinal(bool $isFinal): EntityWorkflowStep
{
$this->freezeAfter = $freezeAfter;
$this->isFinal = $isFinal;
return $this;
}

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use LogicException;
use Symfony\Component\Form\AbstractType;
@ -24,6 +25,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
use function array_key_exists;
class WorkflowStepType extends AbstractType
{
@ -31,10 +33,13 @@ class WorkflowStepType extends AbstractType
private Registry $registry;
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry)
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->entityWorkflowManager = $entityWorkflowManager;
$this->registry = $registry;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
@ -42,6 +47,7 @@ class WorkflowStepType extends AbstractType
/** @var \Chill\MainBundle\Entity\Workflow\EntityWorkflow $entityWorkflow */
$entityWorkflow = $options['entity_workflow'];
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
if (true === $options['transition']) {
if (null === $options['entity_workflow']) {
@ -53,20 +59,49 @@ class WorkflowStepType extends AbstractType
->getEnabledTransitions($entityWorkflow);
$choices = array_combine(
array_map(static function (Transition $transition) { return $transition->getName(); }, $transitions),
array_map(
static function (Transition $transition) {
return $transition->getName();
},
$transitions
),
$transitions
);
$builder
->add('transition', ChoiceType::class, [
'label' => 'workflow.Transition',
'label' => 'workflow.Transition to apply',
'mapped' => false,
'multiple' => false,
'expanded' => true,
'choices' => $choices,
'choice_label' => static function (Transition $transition) {
return implode(', ', $transition->getTos());
},
'choice_label' => function (Transition $transition) use ($workflow) {
$meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
if (array_key_exists('label', $meta)) {
return $this->translatableStringHelper->localize($meta['label']);
}
return $transition->getName();
},
'choice_attr' => static function (Transition $transition) use ($workflow) {
$toFinal = true;
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
return [
'data-is-transition' => 'data-is-transition',
'data-to-final' => $toFinal ? '1' : '0',
];
},
])
->add('future_dest_users', PickUserDynamicType::class, [
'label' => 'workflow.dest for next steps',
@ -88,11 +123,6 @@ class WorkflowStepType extends AbstractType
}
$builder
->add('finalizeAfter', CheckboxType::class, [
'required' => false,
'label' => 'workflow.Finalize',
'help' => 'workflow.The workflow will be finalized',
])
->add('comment', ChillTextareaType::class, [
'required' => false,
'label' => 'Comment',

View File

@ -5,10 +5,6 @@
{{ form_row(transition_form.transition) }}
<div id="finalizeAfter">
{{ form_row(transition_form.finalizeAfter) }}
</div>
{% if transition_form.freezeAfter is defined %}
{{ form_row(transition_form.freezeAfter) }}
{% endif %}
@ -31,7 +27,7 @@
{% else %}
<div class="alert alert-chill-yellow">
{% if entity_workflow.currentStep.isFinalizeAfter %}
{% if entity_workflow.currentStep.isFinal %}
<p>{{ 'workflow.This workflow is finalized'|trans }}</p>
{% else %}
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>

View File

@ -1,13 +1,24 @@
{% macro popoverContent(step) %}
<ul class="small_in_title">
<li>
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
<b>{{ step.transitionBy|chill_entity_render_box }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
<b>{{ step.transitionAt|format_datetime('short', 'short') }}</b>
</li>
{% if step.previous is not null %}
<li>
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'Creation by'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdAt|format_datetime('short', 'short') }}</b>
</li>
{% endif %}
</ul>
{% endmacro %}
@ -15,18 +26,21 @@
{% if step.previous is not null and step.previous.freezeAfter == true %}
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
{% endif %}
{{ step.currentStep }}
{% if step.previous is not null %}
{% set transition = chill_workflow_transition_by_string(step.entityWorkflow, step.previous.transitionAfter) %}
{% set labels = workflow_metadata(step.entityWorkflow, 'label', transition) %}
{% set label = labels is null ? step.previous.transitionAfter : labels|localize_translatable_string %}
{{ label }}
{% endif %}
{% endmacro %}
{% macro breadcrumb(_ctx) %}
<div class="breadcrumb">
{% for step in _ctx.entity_workflow.stepsChained %}
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep) %}
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
{% set popTitle = _self.popoverTitle(step) %}
{% if step.previous is null %}
{% set popContent = _self.popoverContent(step) %}
{% else %}
{% set popContent = _self.popoverContent(step.previous) %}
{% endif %}
{% set popContent = _self.popoverContent(step) %}
<span class="mx-2"
tabindex="0"
data-bs-trigger="focus hover"
@ -42,7 +56,7 @@
{% if step.previous is not null and step.previous.freezeAfter == true %}
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
{% endif %}
{{ step.currentStep }}
{{ label }}
</span>
{% if not loop.last %}

View File

@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\TransitionBlocker;
use function array_key_exists;
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
{
@ -44,6 +45,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
{
return [
'workflow.transition' => 'onTransition',
'workflow.completed' => 'onCompleted',
'workflow.guard' => [
['guardEntityWorkflow', 0],
],
@ -59,7 +61,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
if ($entityWorkflow->isFinalize()) {
if ($entityWorkflow->isFinal()) {
$event->addTransitionBlocker(
new TransitionBlocker(
'workflow.The workflow is finalized',
@ -88,7 +90,25 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
}
}
public function onTransition(Event $event)
public function onCompleted(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep();
$placeMetadata = $event->getWorkflow()->getMetadataStore()
->getPlaceMetadata($step->getCurrentStep());
if (array_key_exists('isFinal', $placeMetadata) && true === $placeMetadata['isFinal']) {
$step->setIsFinal(true);
}
}
public function onTransition(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;

View File

@ -61,7 +61,7 @@ class NotificationOnTransition implements EventSubscriberInterface
$dests = array_merge(
$entityWorkflow->getSubscriberToStep()->toArray(),
$entityWorkflow->isFinalize() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
$entityWorkflow->isFinal() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
);

View File

@ -24,6 +24,10 @@ class WorkflowTwigExtension extends AbstractExtension
[WorkflowTwigExtensionRuntime::class, 'listWorkflows'],
['needs_environment' => true, 'is_safe' => ['html']]
),
new TwigFunction(
'chill_workflow_transition_by_string',
[WorkflowTwigExtensionRuntime::class, 'getTransitionByString']
),
];
}
}

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
@ -46,6 +47,20 @@ class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
$this->normalizer = $normalizer;
}
public function getTransitionByString(EntityWorkflow $entityWorkflow, string $key): ?Transition
{
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$transitions = $workflow->getDefinition()->getTransitions();
foreach ($transitions as $transition) {
if ($transition->getName() === $key) {
return $transition;
}
}
return null;
}
public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
{
$blankEntityWorkflow = new EntityWorkflow();

View File

@ -0,0 +1,33 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220128211748 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step RENAME COLUMN isFinal TO finalizeAfter;');
}
public function getDescription(): string
{
return 'rename workflow entity step from finalizeAfter to isFinal';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step RENAME COLUMN finalizeAfter TO isFinal;');
}
}

View File

@ -368,7 +368,7 @@ Workflow history: Historique de la décision
workflow:
Created by: Créé par
Transition: Prochaine étape
Transition to apply: Ma décision
dest for next steps: Utilisateurs qui valideront la prochaine étape
Freeze: Geler
Freezed: Gelé