mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-13 13:54:23 +00:00
Merge branch '297-workflow-en-attente' into 'signature-app-master'
Allow user to put workflow on hold See merge request Chill-Projet/chill-bundles!718
This commit is contained in:
commit
dd159f4379
@ -24,6 +24,7 @@ use Chill\MainBundle\Security\ChillSecurity;
|
|||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
@ -283,6 +284,9 @@ class WorkflowController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
*/
|
||||||
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
|
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
|
||||||
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
|
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
class WorkflowOnHoldController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly Security $security,
|
||||||
|
private readonly Registry $registry,
|
||||||
|
private readonly UrlGeneratorInterface $urlGenerator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route(path: '/{_locale}/main/workflow/{id}/hold', name: 'chill_main_workflow_on_hold')]
|
||||||
|
public function putOnHold(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||||
|
{
|
||||||
|
$currentStep = $entityWorkflow->getCurrentStep();
|
||||||
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$currentUser instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('only user can put a workflow on hold');
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$enabledTransitions = $workflow->getEnabledTransitions($entityWorkflow);
|
||||||
|
|
||||||
|
if (0 === count($enabledTransitions)) {
|
||||||
|
throw new AccessDeniedHttpException('You are not allowed to apply any transitions to this workflow, therefore you cannot toggle the hold status.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stepHold = new EntityWorkflowStepHold($currentStep, $currentUser);
|
||||||
|
|
||||||
|
$this->entityManager->persist($stepHold);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate(
|
||||||
|
'chill_main_workflow_show',
|
||||||
|
['id' => $entityWorkflow->getId()]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{_locale}/main/workflow/{id}/remove_hold', name: 'chill_main_workflow_remove_hold')]
|
||||||
|
public function removeOnHold(EntityWorkflowStep $entityWorkflowStep): Response
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('only user can remove workflow on hold');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$entityWorkflowStep->isOnHoldByUser($user)) {
|
||||||
|
throw new AccessDeniedHttpException('You are not allowed to remove workflow on hold');
|
||||||
|
}
|
||||||
|
|
||||||
|
$hold = $entityWorkflowStep->getHoldsOnStep()->findFirst(fn(int $index, EntityWorkflowStepHold $entityWorkflowStepHold) => $user === $entityWorkflowStepHold->getByUser());
|
||||||
|
|
||||||
|
if (null === $hold) {
|
||||||
|
// this should not happens...
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($hold);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate(
|
||||||
|
'chill_main_workflow_show',
|
||||||
|
['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -339,8 +339,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
public function isFreeze(): bool
|
public function isFreeze(): bool
|
||||||
{
|
{
|
||||||
$steps = $this->getStepsChained();
|
|
||||||
|
|
||||||
foreach ($this->getStepsChained() as $step) {
|
foreach ($this->getStepsChained() as $step) {
|
||||||
if ($step->isFreezeAfter()) {
|
if ($step->isFreezeAfter()) {
|
||||||
return true;
|
return true;
|
||||||
@ -350,6 +348,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isOnHoldByUser(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->getCurrentStep()->isOnHoldByUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
public function isUserSubscribedToFinal(User $user): bool
|
public function isUserSubscribedToFinal(User $user): bool
|
||||||
{
|
{
|
||||||
return $this->subscriberToFinal->contains($user);
|
return $this->subscriberToFinal->contains($user);
|
||||||
@ -480,4 +483,9 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
return $this->steps->get($this->steps->count() - 2);
|
return $this->steps->get($this->steps->count() - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isOnHoldAtCurrentStep(): bool
|
||||||
|
{
|
||||||
|
return $this->getCurrentStep()->getHoldsOnStep()->count() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,12 +98,19 @@ class EntityWorkflowStep
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||||
private ?string $transitionByEmail = null;
|
private ?string $transitionByEmail = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
||||||
|
private Collection $holdsOnStep;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->ccUser = new ArrayCollection();
|
$this->ccUser = new ArrayCollection();
|
||||||
$this->destUser = new ArrayCollection();
|
$this->destUser = new ArrayCollection();
|
||||||
$this->destUserByAccessKey = new ArrayCollection();
|
$this->destUserByAccessKey = new ArrayCollection();
|
||||||
$this->signatures = new ArrayCollection();
|
$this->signatures = new ArrayCollection();
|
||||||
|
$this->holdsOnStep = new ArrayCollection();
|
||||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +286,17 @@ class EntityWorkflowStep
|
|||||||
return $this->freezeAfter;
|
return $this->freezeAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isOnHoldByUser(User $user): bool
|
||||||
|
{
|
||||||
|
foreach ($this->getHoldsOnStep() as $onHold) {
|
||||||
|
if ($onHold->getByUser() === $user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function isWaitingForTransition(): bool
|
public function isWaitingForTransition(): bool
|
||||||
{
|
{
|
||||||
if (null !== $this->transitionAfter) {
|
if (null !== $this->transitionAfter) {
|
||||||
@ -413,6 +431,11 @@ class EntityWorkflowStep
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHoldsOnStep(): Collection
|
||||||
|
{
|
||||||
|
return $this->holdsOnStep;
|
||||||
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
|
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
|
||||||
{
|
{
|
||||||
@ -432,4 +455,13 @@ class EntityWorkflowStep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addOnHold(EntityWorkflowStepHold $onHold): self
|
||||||
|
{
|
||||||
|
if (!$this->holdsOnStep->contains($onHold)) {
|
||||||
|
$this->holdsOnStep->add($onHold);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Entity\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table('chill_main_workflow_entity_step_hold')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'chill_main_workflow_hold_unique_idx', columns: ['step_id', 'byUser_id'])]
|
||||||
|
class EntityWorkflowStepHold implements TrackCreationInterface
|
||||||
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
public function __construct(#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private EntityWorkflowStep $step, #[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private User $byUser)
|
||||||
|
{
|
||||||
|
$step->addOnHold($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStep(): EntityWorkflowStep
|
||||||
|
{
|
||||||
|
return $this->step;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getByUser(): User
|
||||||
|
{
|
||||||
|
return $this->byUser;
|
||||||
|
}
|
||||||
|
}
|
@ -230,7 +230,10 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
|
|
||||||
$qb->where(
|
$qb->where(
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
||||||
|
$qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'),
|
||||||
|
),
|
||||||
$qb->expr()->isNull('step.transitionAfter'),
|
$qb->expr()->isNull('step.transitionAfter'),
|
||||||
$qb->expr()->eq('step.isFinal', "'FALSE'")
|
$qb->expr()->eq('step.isFinal', "'FALSE'")
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Repository\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-extends ServiceEntityRepository<EntityWorkflowStepHold>
|
||||||
|
*/
|
||||||
|
class EntityWorkflowStepHoldRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, EntityWorkflowStepHold::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an EntityWorkflowStepHold by its ID.
|
||||||
|
*/
|
||||||
|
public function findById(int $id): ?EntityWorkflowStepHold
|
||||||
|
{
|
||||||
|
return $this->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all EntityWorkflowStepHold records.
|
||||||
|
*
|
||||||
|
* @return EntityWorkflowStepHold[]
|
||||||
|
*/
|
||||||
|
public function findAllHolds(): array
|
||||||
|
{
|
||||||
|
return $this->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find EntityWorkflowStepHold by a specific step.
|
||||||
|
*
|
||||||
|
* @return EntityWorkflowStepHold[]
|
||||||
|
*/
|
||||||
|
public function findByStep(EntityWorkflowStep $step): array
|
||||||
|
{
|
||||||
|
return $this->findBy(['step' => $step]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a single EntityWorkflowStepHold by step and user.
|
||||||
|
*
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
*/
|
||||||
|
public function findOneByStepAndUser(EntityWorkflowStep $step, User $user): ?EntityWorkflowStepHold
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->createQueryBuilder('e')
|
||||||
|
->andWhere('e.step = :step')
|
||||||
|
->andWhere('e.byUser = :user')
|
||||||
|
->setParameter('step', $step)
|
||||||
|
->setParameter('user', $user)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
} catch (NoResultException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -498,6 +498,7 @@ div.workflow {
|
|||||||
div.breadcrumb {
|
div.breadcrumb {
|
||||||
display: initial;
|
display: initial;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
margin-right: .5rem;
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
background-color: tint-color($chill-yellow, 90%);
|
background-color: tint-color($chill-yellow, 90%);
|
||||||
border: 1px solid $chill-yellow;
|
border: 1px solid $chill-yellow;
|
||||||
|
@ -9,13 +9,16 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:tbody>
|
<template v-slot:tbody>
|
||||||
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
|
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
|
||||||
<td>{{ w.title }}</td>
|
<td>
|
||||||
|
{{ w.title }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="workflow">
|
<div class="workflow">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
|
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
|
||||||
<span class="mx-2">{{ getStep(w) }}</span>
|
<span class="mx-2">{{ getStep(w) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="w.isOnHoldAtCurrentStep" class="badge bg-success rounded-pill">{{ $t('on_hold') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="w.datas.persons !== null">
|
<td v-if="w.datas.persons !== null">
|
||||||
|
@ -41,6 +41,7 @@ const appMessages = {
|
|||||||
Step: "Étape",
|
Step: "Étape",
|
||||||
concerned_users: "Usagers concernés",
|
concerned_users: "Usagers concernés",
|
||||||
Object_workflow: "Objet du workflow",
|
Object_workflow: "Objet du workflow",
|
||||||
|
on_hold: "En attente",
|
||||||
show_entity: "Voir {entity}",
|
show_entity: "Voir {entity}",
|
||||||
the_activity: "l'échange",
|
the_activity: "l'échange",
|
||||||
the_course: "le parcours",
|
the_course: "le parcours",
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="item-row col">
|
<div class="item-row col">
|
||||||
<h2>{{ w.title }}</h2>
|
<h2>{{ w.title }}</h2>
|
||||||
<div class="flex-grow-1 ms-3 h3">
|
<div class="flex-grow-1 ms-3 h3">
|
||||||
<div class="visually-hidden">
|
<div class="visually-hidden">
|
||||||
{{ w.relatedEntityClass }}
|
{{ w.relatedEntityClass }}
|
||||||
{{ w.relatedEntityId }}
|
{{ w.relatedEntityId }}
|
||||||
@ -38,6 +38,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="w.isOnHoldAtCurrentStep" class="badge bg-success rounded-pill">{{ $t('on_hold') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-row">
|
<div class="item-row">
|
||||||
@ -73,7 +74,8 @@ const i18n = {
|
|||||||
you_subscribed_to_all_steps: "Vous recevrez une notification à chaque étape",
|
you_subscribed_to_all_steps: "Vous recevrez une notification à chaque étape",
|
||||||
you_subscribed_to_final_step: "Vous recevrez une notification à l'étape finale",
|
you_subscribed_to_final_step: "Vous recevrez une notification à l'étape finale",
|
||||||
by: "Par",
|
by: "Par",
|
||||||
at: "Le"
|
at: "Le",
|
||||||
|
on_hold: "En attente"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,11 @@
|
|||||||
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in step.destUser %}
|
{% for u in step.destUser %}
|
||||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
|
||||||
|
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||||
|
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -39,6 +39,9 @@
|
|||||||
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
|
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
|
||||||
|
|
||||||
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
|
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
|
||||||
|
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||||
|
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
|
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
|
||||||
@ -64,14 +67,21 @@
|
|||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
|
||||||
|
|
||||||
{# useful ?
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li class="cancel">
|
{% if entity_workflow.isOnHoldByUser(app.user) %}
|
||||||
<a class="btn btn-cancel" href="{{ path('chill_main_workflow_list_dest') }}">
|
<li>
|
||||||
{{ 'Back to the list'|trans }}
|
<a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i>
|
||||||
</a>
|
{{ 'workflow.Remove hold'|trans }}
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i>
|
||||||
|
{{ 'workflow.Put on hold'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
#}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -69,6 +69,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
{{ macro.breadcrumb(l) }}
|
{{ macro.breadcrumb(l) }}
|
||||||
|
{% if l.entity_workflow.isOnHoldAtCurrentStep %}
|
||||||
|
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +45,7 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
|
|||||||
'steps' => $this->normalizer->normalize($object->getStepsChained(), $format, $context),
|
'steps' => $this->normalizer->normalize($object->getStepsChained(), $format, $context),
|
||||||
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
||||||
'title' => $handler->getEntityTitle($object),
|
'title' => $handler->getEntityTitle($object),
|
||||||
|
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Controller\WorkflowOnHoldController;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class WorkflowOnHoldControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$definitionBuilder = new DefinitionBuilder();
|
||||||
|
$definition = $definitionBuilder
|
||||||
|
->addPlaces(['initial', 'layout', 'sign'])
|
||||||
|
->addTransition(new Transition('to_layout', 'initial', 'layout'))
|
||||||
|
->addTransition(new Transition('to_sign', 'initial', 'sign'))
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$workflow = new Workflow($definition, new EntityWorkflowMarkingStore(), name: 'dummy_workflow');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPutOnHoldPersistence(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy_workflow');
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
$security->method('getUser')->willReturn($user = new User());
|
||||||
|
|
||||||
|
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$entityManager->expects($this->once())
|
||||||
|
->method('persist')
|
||||||
|
->with($this->isInstanceOf(EntityWorkflowStepHold::class));
|
||||||
|
|
||||||
|
$entityManager->expects($this->once())
|
||||||
|
->method('flush');
|
||||||
|
|
||||||
|
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||||
|
$urlGenerator->method('generate')
|
||||||
|
->with('chill_main_workflow_show', ['id' => null])
|
||||||
|
->willReturn('/some/url');
|
||||||
|
|
||||||
|
$controller = new WorkflowOnHoldController($entityManager, $security, $this->buildRegistry(), $urlGenerator);
|
||||||
|
|
||||||
|
$request = new Request();
|
||||||
|
$response = $controller->putOnHold($entityWorkflow, $request);
|
||||||
|
|
||||||
|
self::assertEquals(302, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRemoveOnHold(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy_workflow');
|
||||||
|
$onHold = new EntityWorkflowStepHold($step = $entityWorkflow->getCurrentStep(), $user);
|
||||||
|
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
$security->method('getUser')->willReturn($user);
|
||||||
|
|
||||||
|
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$entityManager->expects($this->once())
|
||||||
|
->method('remove')
|
||||||
|
->with($onHold);
|
||||||
|
|
||||||
|
$entityManager->expects($this->once())
|
||||||
|
->method('flush');
|
||||||
|
|
||||||
|
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||||
|
$urlGenerator->method('generate')
|
||||||
|
->with('chill_main_workflow_show', ['id' => null])
|
||||||
|
->willReturn('/some/url');
|
||||||
|
|
||||||
|
$controller = new WorkflowOnHoldController($entityManager, $security, $this->buildRegistry(), $urlGenerator);
|
||||||
|
|
||||||
|
$response = $controller->removeOnHold($step);
|
||||||
|
|
||||||
|
self::assertEquals(302, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20240807123801 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create workflow step waiting entity';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_hold_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE chill_main_workflow_entity_step_hold (id INT NOT NULL, step_id INT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, byUser_id INT NOT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1BE2E7C73B21E9C ON chill_main_workflow_entity_step_hold (step_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1BE2E7CD23C0240 ON chill_main_workflow_entity_step_hold (byUser_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1BE2E7C3174800F ON chill_main_workflow_entity_step_hold (createdBy_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX chill_main_workflow_hold_unique_idx ON chill_main_workflow_entity_step_hold (step_id, byUser_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_hold.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold ADD CONSTRAINT FK_1BE2E7C73B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold ADD CONSTRAINT FK_1BE2E7CD23C0240 FOREIGN KEY (byUser_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold ADD CONSTRAINT FK_1BE2E7C3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_hold_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold DROP CONSTRAINT FK_1BE2E7C73B21E9C');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold DROP CONSTRAINT FK_1BE2E7CD23C0240');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_hold DROP CONSTRAINT FK_1BE2E7C3174800F');
|
||||||
|
$this->addSql('DROP TABLE chill_main_workflow_entity_step_hold');
|
||||||
|
}
|
||||||
|
}
|
@ -527,6 +527,9 @@ workflow:
|
|||||||
Access link copied: Lien d'accès copié
|
Access link copied: Lien d'accès copié
|
||||||
This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition
|
This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition
|
||||||
The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant
|
The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant
|
||||||
|
Put on hold: Mettre en attente
|
||||||
|
Remove hold: Enlever la mise en attente
|
||||||
|
On hold: En attente
|
||||||
|
|
||||||
signature_zone:
|
signature_zone:
|
||||||
title: Appliquer les signatures électroniques
|
title: Appliquer les signatures électroniques
|
||||||
|
Loading…
x
Reference in New Issue
Block a user