diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php index b451e9209..f0e728252 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php @@ -93,6 +93,45 @@ class WorkflowApiController ); } + /** + * Return a list of workflow which are waiting an action for the user. + * + * @Route("/api/1.0/main/workflow/my-cc", methods={"GET"}) + */ + public function myWorkflowCc(Request $request): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) { + throw new AccessDeniedException(); + } + + $total = $this->entityWorkflowRepository->countByCc($this->security->getUser()); + + if ($request->query->getBoolean('countOnly', false)) { + return new JsonResponse( + $this->serializer->serialize(new Counter($total), 'json'), + JsonResponse::HTTP_OK, + [], + true + ); + } + + $paginator = $this->paginatorFactory->create($total); + + $workflows = $this->entityWorkflowRepository->findByCc( + $this->security->getUser(), + ['id' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + return new JsonResponse( + $this->serializer->serialize(new Collection($workflows, $paginator), 'json', ['groups' => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } + /** * @Route("/api/1.0/main/workflow/{id}/subscribe", methods={"POST"}) */ diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index 0f00a177f..ee09cc1a6 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -228,6 +228,33 @@ class WorkflowController extends AbstractController ); } + /** + * @Route("/{_locale}/main/workflow/list/cc", name="chill_main_workflow_list_cc") + */ + public function myWorkflowsCc(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $total = $this->entityWorkflowRepository->countByDest($this->getUser()); + $paginator = $this->paginatorFactory->create($total); + + $workflows = $this->entityWorkflowRepository->findByCc( + $this->getUser(), + ['createdAt' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + return $this->render( + '@ChillMain/Workflow/list.html.twig', + [ + 'workflows' => $this->buildHandler($workflows), + 'paginator' => $paginator, + 'step' => 'cc', + ] + ); + } + /** * @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest") */ @@ -306,7 +333,11 @@ class WorkflowController extends AbstractController $transitionForm = $this->createForm( WorkflowStepType::class, $entityWorkflow->getCurrentStep(), - ['transition' => true, 'entity_workflow' => $entityWorkflow, 'suggested_users' => $usersInvolved] + [ + 'transition' => true, + 'entity_workflow' => $entityWorkflow, + 'suggested_users' => $usersInvolved + ] ); $transitionForm->handleRequest($request); @@ -330,6 +361,7 @@ class WorkflowController extends AbstractController } // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context) + $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData(); $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData(); $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData(); diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index e4683f24b..d0f558cd5 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -16,6 +16,7 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Validator\Constraints\Entity\WorkflowStepUsersOnTransition; use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation; use DateTimeInterface; use Doctrine\Common\Collections\ArrayCollection; @@ -24,6 +25,7 @@ use Doctrine\ORM\Mapping as ORM; use Iterator; use RuntimeException; use Symfony\Component\Serializer\Annotation as Serializer; +use Symfony\Component\Validator\Constraints as Assert; use function count; use function is_array; @@ -41,6 +43,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface use TrackUpdateTrait; + /** + * a list of future cc users for the next steps. + * + * @var array|User[] + */ + public array $futureCcUsers = []; + /** * a list of future dest emails for the next steps. * @@ -90,7 +99,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\OneToMany(targetEntity=EntityWorkflowStep::class, mappedBy="entityWorkflow", orphanRemoval=true, cascade={"persist"}) * @ORM\OrderBy({"transitionAt": "ASC", "id": "ASC"}) - * + * @Assert\Valid(traverse=true) * @var Collection|EntityWorkflowStep[] */ private Collection $steps; diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index 4bff7a3f5..e43d524a4 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -32,6 +32,12 @@ class EntityWorkflowStep */ private string $accessKey; + /** + * @ORM\ManyToMany(targetEntity=User::class) + * @ORM\JoinTable(name="chill_main_workflow_entity_step_cc_user") + */ + private Collection $ccUser; + /** * @ORM\Column(type="text", options={"default": ""}) */ @@ -114,11 +120,21 @@ class EntityWorkflowStep public function __construct() { + $this->ccUser = new ArrayCollection(); $this->destUser = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection(); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); } + public function addCcUser(User $user): self + { + if (!$this->ccUser->contains($user)) { + $this->ccUser[] = $user; + } + + return $this; + } + public function addDestEmail(string $email): self { if (!in_array($email, $this->destEmail, true)) { @@ -167,6 +183,11 @@ class EntityWorkflowStep ); } + public function getCcUser(): Collection + { + return $this->ccUser; + } + public function getComment(): string { return $this->comment; @@ -261,6 +282,13 @@ class EntityWorkflowStep return true; } + public function removeCcUser(User $user): self + { + $this->ccUser->removeElement($user); + + return $this; + } + public function removeDestEmail(string $email): self { $this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) { diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index 1b18b3ef8..16dc0a4a5 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -156,6 +156,13 @@ class WorkflowStepType extends AbstractType 'mapped' => false, 'suggested' => $options['suggested_users'], ]) + ->add('future_cc_users', PickUserDynamicType::class, [ + 'label' => 'workflow.cc for next steps', + 'multiple' => true, + 'mapped' => false, + 'required' => false, + 'suggested' => $options['suggested_users'], + ]) ->add('future_dest_emails', ChillCollectionType::class, [ 'label' => 'workflow.dest by email', 'help' => 'workflow.dest by email help', @@ -236,6 +243,20 @@ class WorkflowStepType extends AbstractType } } ), + new Callback( + function ($step, ExecutionContextInterface $context, $payload) { + $form = $context->getObject(); + + foreach($form->get('future_dest_users')->getData() as $u) { + if (in_array($u, $form->get('future_cc_users')->getData(), true)) { + $context + ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') + ->atPath('ccUsers') + ->addViolation(); + } + } + } + ) ]); } } diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php index 8ccd420c5..0720c6da6 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php @@ -74,7 +74,8 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface } return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [ - 'notifications' => $notifications, 'appendCommentForms' => $appendCommentForms, + 'notifications' => $notifications, + 'appendCommentForms' => $appendCommentForms, ]); } } diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 1fc309d6e..a304ff6d7 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -27,6 +27,13 @@ class EntityWorkflowRepository implements ObjectRepository $this->repository = $entityManager->getRepository(EntityWorkflow::class); } + public function countByCc(User $user): int + { + $qb = $this->buildQueryByCc($user)->select('count(ew)'); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + public function countByDest(User $user): int { $qb = $this->buildQueryByDest($user)->select('count(ew)'); @@ -103,6 +110,19 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + public function findByCc(User $user, ?array $orderBy = null, $limit = null, $offset = null): array + { + $qb = $this->buildQueryByCc($user)->select('ew'); + + foreach ($orderBy as $key => $sort) { + $qb->addOrderBy('ew.' . $key, $sort); + } + + $qb->setMaxResults($limit)->setFirstResult($offset); + + return $qb->getQuery()->getResult(); + } + public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array { $qb = $this->buildQueryByDest($user)->select('ew'); @@ -165,6 +185,25 @@ class EntityWorkflowRepository implements ObjectRepository return EntityWorkflow::class; } + private function buildQueryByCc(User $user): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('ew'); + + $qb->join('ew.steps', 'step'); + + $qb->where( + $qb->expr()->andX( + $qb->expr()->isMemberOf(':user', 'step.ccUser'), + $qb->expr()->isNull('step.transitionAfter'), + $qb->expr()->eq('step.isFinal', "'FALSE'") + ) + ); + + $qb->setParameter('user', $user); + + return $qb; + } + private function buildQueryByDest(User $user): QueryBuilder { $qb = $this->repository->createQueryBuilder('ew'); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue index fbf453b47..315fd863f 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue @@ -46,8 +46,7 @@ :class="{'active': activeTab === 'MyTasks'}" @click="selectTab('MyTasks')"> {{ $t('my_tasks.tab') }} - - +
  • - + {% if c.notification_cc is defined %} + {% if c.notification_cc %} + + + {{ 'notification.cc'|trans }} : + + + {% else %} + + + {{ 'notification.to'|trans }} : + + + {% endif %} + {% else %} + {{ 'notification.to'|trans }} : + {% endif %} {% for a in c.notification.addressees %} {{ a|chill_entity_render_string }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig index 1f1cbe673..6dff67299 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig @@ -50,7 +50,8 @@ {% for data in datas %} {% set notification = data.notification %} {% include 'ChillMainBundle:Notification:_list_item.html.twig' with { - 'fold_item': true + 'fold_item': true, + 'notification_cc': data.template_data.notificationCc is defined ? data.template_data.notificationCc : false } %} {% endfor %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig index cce7ebd64..2f9d5d63f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig @@ -27,7 +27,8 @@ }, 'action_button': false, 'full_content': true, - 'fold_item': false + 'fold_item': false, + 'notification_cc': handler.getTemplateData(notification).notificationCc is defined ? handler.getTemplateData(notification).notificationCc : false } %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 0444abc69..2aec17cf9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -65,6 +65,8 @@
    {{ form_row(transition_form.future_dest_users) }} + {{ form_row(transition_form.future_cc_users) }} + {{ form_row(transition_form.future_dest_emails) }} {{ form_errors(transition_form.future_dest_users) }}
    diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index 72aab397c..acbf97cf7 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -81,6 +81,15 @@ {% endif %} + {% if step.ccUser|length > 0 %} +

    {{ 'workflow.Users put in Cc'|trans }} :

    +
      + {% for u in step.ccUser %} +
    • {{ u|chill_entity_render_box }}
    • + {% endfor %} +
    + {% endif %} + {% if entity_workflow.currentStep.destEmail|length > 0 %}

    {{ 'workflow.An access key was also sent to those addresses'|trans }} :