From 3a98a316fec2e770a4792d4ce8f40150a051031c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Feb 2022 10:02:33 +0100 Subject: [PATCH] improve workflow decision form --- .../Entity/ActivityType.php | 2 +- .../Entity/Workflow/EntityWorkflow.php | 14 ++++ .../ChillMainBundle/Form/WorkflowStepType.php | 75 +++++++++++++---- .../public/lib/show_hide/show_hide.js | 65 ++++++++------- .../public/page/workflow-show/index.js | 83 ++++++++++++++----- .../views/Workflow/_decision.html.twig | 55 +++++++++++- .../views/Workflow/_history.html.twig | 12 +++ .../translations/messages.fr.yml | 6 +- .../Entity/AccompanyingPeriod.php | 2 +- .../AccompanyingPeriodRepository.php | 10 +-- 10 files changed, 250 insertions(+), 74 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php index 94f732a89..bdf75ed05 100644 --- a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php @@ -271,7 +271,7 @@ class ActivityType public function checkSocialActionsVisibility(ExecutionContextInterface $context, $payload) { if ($this->socialIssuesVisible !== $this->socialActionsVisible) { - if (!($this->socialIssuesVisible === 2 && $this->socialActionsVisible === 1)) { + if (!(2 === $this->socialIssuesVisible && 1 === $this->socialActionsVisible)) { $context ->buildViolation('The socialActionsVisible value is not compatible with the socialIssuesVisible value') ->atPath('socialActionsVisible') diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index c7a322338..a132706d7 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -174,6 +174,20 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface return null; } + public function getCurrentStepChained(): ?EntityWorkflowStep + { + $steps = $this->getStepsChained(); + $currentStep = $this->getCurrentStep(); + + foreach ($steps as $step) { + if ($step === $currentStep) { + return $step; + } + } + + return null; + } + public function getCurrentStepCreatedAt(): ?DateTimeInterface { if (null !== $previous = $this->getPreviousStepIfAny()) { diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index 18ac10c47..eaca35812 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -48,6 +48,8 @@ class WorkflowStepType extends AbstractType $entityWorkflow = $options['entity_workflow']; $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $place = $workflow->getMarking($entityWorkflow); + $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]); if (true === $options['transition']) { if (null === $options['entity_workflow']) { @@ -68,40 +70,77 @@ class WorkflowStepType extends AbstractType $transitions ); + if (array_key_exists('validationFilterInputLabels', $placeMetadata)) { + $inputLabels = $placeMetadata['validationFilterInputLabels']; + + $builder->add('transitionFilter', ChoiceType::class, [ + 'multiple' => false, + 'label' => 'workflow.My decision', + 'choices' => [ + 'forward' => 'forward', + 'backward' => 'backward', + 'neutral' => 'neutral', + ], + 'choice_label' => function (string $key) use ($inputLabels) { + return $this->translatableStringHelper->localize($inputLabels[$key]); + }, + 'choice_attr' => static function (string $key) { + return [ + $key => $key, + ]; + }, + 'mapped' => false, + 'expanded' => true, + 'data' => 'forward', + ]); + } + $builder ->add('transition', ChoiceType::class, [ - 'label' => 'workflow.Transition to apply', + 'label' => 'workflow.Next step', 'mapped' => false, 'multiple' => false, 'expanded' => true, 'choices' => $choices, 'choice_label' => function (Transition $transition) use ($workflow) { - $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); + $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); - if (array_key_exists('label', $meta)) { - return $this->translatableStringHelper->localize($meta['label']); - } + if (array_key_exists('label', $meta)) { + return $this->translatableStringHelper->localize($meta['label']); + } - return $transition->getName(); - }, + return $transition->getName(); + }, 'choice_attr' => static function (Transition $transition) use ($workflow) { - $toFinal = true; + $toFinal = true; + $isForward = 'neutral'; - foreach ($transition->getTos() as $to) { - $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); + $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); - if ( + if (array_key_exists('isForward', $metadata)) { + if ($metadata['isForward']) { + $isForward = 'forward'; + } else { + $isForward = 'backward'; + } + } + + foreach ($transition->getTos() as $to) { + $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); + + if ( !array_key_exists('isFinal', $meta) || false === $meta['isFinal'] ) { - $toFinal = false; - } + $toFinal = false; } + } - return [ - 'data-is-transition' => 'data-is-transition', - 'data-to-final' => $toFinal ? '1' : '0', - ]; - }, + return [ + 'data-is-transition' => 'data-is-transition', + 'data-to-final' => $toFinal ? '1' : '0', + 'data-is-forward' => $isForward, + ]; + }, ]) ->add('future_dest_users', PickUserDynamicType::class, [ 'label' => 'workflow.dest for next steps', diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js b/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js index f2bcc9b45..25307a120 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js @@ -1,19 +1,19 @@ /** * Create a control to show or hide values - * + * * Possible options are: - * + * * - froms: an Element, an Array of Element, or a NodeList. A * listener will be attached to **all** input of those elements * and will trigger the check on changes - * - test: a function which will test the element and will return true + * - test: a function which will test the element and will return true * if the content must be shown, false if it must be hidden. - * The function will receive the `froms` as first argument, and the + * The function will receive the `froms` as first argument, and the * event as second argument. - * - container: an Element, an Array of Element, or a Node List. The + * - container: an Element, an Array of Element, or a Node List. The * child nodes will be hidden / shown inside this container * - event_name: the name of the event to listen to. `'change'` by default. - * + * * @param object options */ var ShowHide = function(options) { @@ -26,8 +26,10 @@ var ShowHide = function(options) { container_content = [], debug = 'debug' in options ? options.debug : false, load_event = 'load_event' in options ? options.load_event : 'load', - id = 'uid' in options ? options.id : Math.random(); - + id = 'uid' in options ? options.id : Math.random(), + toggle_callback = 'toggle_callback' in options ? options.toggle_callback : null + ; + var bootstrap = function(event) { if (debug) { console.log('debug is activated on this show-hide', this); @@ -39,15 +41,14 @@ var ShowHide = function(options) { contents.push(el); } container_content.push(contents); - // console.log('container content', container_content); } // attach the listener on each input for (let f of froms.values()) { - let - inputs = f.querySelectorAll('input'), + let + inputs = f.querySelectorAll('input'), selects = f.querySelectorAll('select'); - + for (let input of inputs.values()) { if (debug) { console.log('attaching event to input', input); @@ -67,10 +68,10 @@ var ShowHide = function(options) { } // first launch of the show/hide - onChange(event); + onChange(event); }; - + var onChange = function (event) { var result = test(froms, event), me; @@ -89,45 +90,53 @@ var ShowHide = function(options) { } else { throw "the result of test is not a boolean"; } - + }; - + var forceHide = function() { if (debug) { console.log('force hide'); } - for (let contents of container_content.values()) { - for (let el of contents.values()) { - el.remove(); + if (toggle_callback !== null) { + toggle_callback(container, 'hide'); + } else { + for (let contents of container_content.values()) { + for (let el of contents.values()) { + el.remove(); + } } } is_shown = false; }; - + var forceShow = function() { if (debug) { console.log('show'); } - for (let i of container_content.keys()) { - var contents = container_content[i]; - for (let el of contents.values()) { - container[i].appendChild(el); + if (toggle_callback !== null) { + toggle_callback(container, 'show'); + } else { + for (let i of container_content.keys()) { + var contents = container_content[i]; + for (let el of contents.values()) { + container[i].appendChild(el); + } } } is_shown = true; }; - + var forceCompute = function(event) { onChange(event); }; - - + + if (load_event !== null) { window.addEventListener('load', bootstrap); } else { bootstrap(null); } - + return { forceHide: forceHide, forceShow: forceShow, diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js index 2e2d4e89c..f589ee7d5 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js @@ -2,29 +2,74 @@ import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js'; window.addEventListener('DOMContentLoaded', function() { let - finalizeAfterContainer = document.querySelector('#finalizeAfter'), + divTransitions = document.querySelector('#transitions'), futureDestUsersContainer = document.querySelector('#futureDestUsers') ; - if (null === finalizeAfterContainer) { - return; - } - - new ShowHide({ - load_event: null, - froms: [finalizeAfterContainer], - container: [futureDestUsersContainer], - test: function(containers, arg2, arg3) { - for (let container of containers) { - for (let input of container.querySelectorAll('input')) { - if (!input.checked) { - return true; - } else { - return false; + if (null !== divTransitions) { + new ShowHide({ + load_event: null, + froms: [divTransitions], + container: [futureDestUsersContainer], + test: function(divs, arg2, arg3) { + for (let div of divs) { + for (let input of div.querySelectorAll('input')) { + if (input.checked) { + if (input.dataset.toFinal === "1") { + return false; + } else { + return true; + } + } } } - } - }, - }) + return true; + }, + }); + } + + let + transitionFilterContainer = document.querySelector('#transitionFilter'), + transitions = document.querySelector('#transitions') + ; + + if (null !== transitionFilterContainer) { + transitions.querySelectorAll('.form-check').forEach(function(row) { + + const isForward = row.querySelector('input').dataset.isForward; + console.log(row); + console.log(isForward); + + new ShowHide({ + load_event: null, + froms: [transitionFilterContainer], + container: row, + test: function (containers) { + for (let container of containers) { + for (let input of container.querySelectorAll('input')) { + if (input.checked) { + return isForward === input.value; + } + } + } + }, + toggle_callback: function (c, dir) { + for (let div of c) { + let input = div.querySelector('input'); + if ('hide' === dir) { + input.checked = false; + input.disabled = true; + } else { + input.disabled = false; + } + } + }, + }); + }); + } + + + + }); diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 4b14b39bc..e557d9e89 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -3,7 +3,60 @@ {% if transition_form is not null %} {{ form_start(transition_form) }} - {{ form_row(transition_form.transition) }} + {% set step = entity_workflow.currentStepChained %} + {% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %} + {% set label = labels is null ? step.currentStep : labels|localize_translatable_string %} + +
+
+
+
+ {{ 'workflow.Current step'|trans }} : + {{ label }} +
+
+ + {% if step.previous is not null %} + {% if step.previous.comment is not empty %} +
+
+
+ {{ step.previous.comment|chill_markdown_to_html }} +
+
+
+ {% endif %} +
+
+ {{ 'By'|trans }} + {{ step.previous.transitionBy|chill_entity_render_box }}, + {{ step.previous.transitionAt|format_datetime('short', 'short') }} +
+
+ {% else %} +
+
{{ 'workflow.Created by'|trans }}
+
{{ step.entityWorkflow.createdBy|chill_entity_render_box }}
+
+
+
{{ 'Le'|trans }}
+
{{ step.entityWorkflow.createdAt|format_datetime('short', 'short') }}
+
+ {% endif %} +
+
+ + + +
+ {% if transition_form.transitionFilter is defined %} + {{ form_row(transition_form.transitionFilter) }} + {% endif %} +
+ +
+ {{ form_row(transition_form.transition) }} +
{% if transition_form.freezeAfter is defined %} {{ form_row(transition_form.freezeAfter) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index cee9d219c..e40e82dab 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -69,6 +69,18 @@ {% endif %} + {% if loop.last and step.destUser|length > 0 %} +
+
+

{{ 'workflow.Users allowed to apply transition'|trans }} :

+
    + {% for u in step.destUser %} +
  • {{ u|chill_entity_render_box }}
  • + {% endfor %} +
+
+
+ {% endif %} {% endfor %} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 605cafe8e..d3173bd7c 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -370,7 +370,8 @@ Workflow history: Historique de la décision workflow: Created by: Créé par - Transition to apply: Ma décision + My decision: Ma décision + Next step: Prochaine étape dest for next steps: Utilisateurs qui valideront la prochaine étape Freeze: Geler Freezed: Gelé @@ -392,6 +393,9 @@ workflow: dest: Workflows en attente d'action you subscribed to all steps: Vous recevrez une notification à chaque étape you subscribed to final step: Vous recevrez une notification à l'étape finale + Current step: Étape actuelle + Comment on last change: Commentaire à la transition précédente + Users allowed to apply transition: Utilisateurs pouvant valider cette étape Subscribe final: Recevoir une notification à l'étape finale Subscribe all steps: Recevoir une notification à chaque étape diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index d71ac2b9f..07fb859b7 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -213,7 +213,7 @@ class AccompanyingPeriod implements * * @ORM\Column(type="date") * @Groups({"read", "write", "docgen:read"}) - * @Assert\LessThan(value= "today", groups={AccompanyingPeriod::STEP_CONFIRMED}) + * @Assert\LessThan(value="today", groups={AccompanyingPeriod::STEP_CONFIRMED}) * @Assert\LessThan(propertyPath="closingDate", groups={AccompanyingPeriod::STEP_CONFIRMED}) */ private ?DateTime $openingDate = null; diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php index 3f0c30e47..10b424cef 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php @@ -28,6 +28,11 @@ final class AccompanyingPeriodRepository implements ObjectRepository $this->repository = $entityManager->getRepository(AccompanyingPeriod::class); } + public function countBy(array $criteria): int + { + return $this->repository->count($criteria); + } + public function countByRecentUserHistory(User $user, DateTimeImmutable $since): int { $qb = $this->buildQueryByRecentUserHistory($user, $since); @@ -35,11 +40,6 @@ final class AccompanyingPeriodRepository implements ObjectRepository return $qb->select('count(a)')->getQuery()->getSingleScalarResult(); } - public function countBy(array $criteria): int - { - return $this->repository->count($criteria); - } - public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder { return $this->repository->createQueryBuilder($alias, $indexBy);