mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'origin/master' into issue383_referent_in_acc_course
This commit is contained in:
commit
8e2d3616b5
26
CHANGELOG.md
26
CHANGELOG.md
@ -11,15 +11,33 @@ and this project adheres to
|
||||
## Unreleased
|
||||
|
||||
<!-- write down unreleased development here -->
|
||||
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
|
||||
* [notification: formulaire création] descend la box avec la description dans le bas du formulaire
|
||||
* [notification for activity]: fix link to activity
|
||||
* [notification] add "URGENT" before accompanying course with emergency = true
|
||||
* [notification] add a "read more" button on system notification
|
||||
* [notification] add `[Chill]` in the subject of each notification, automatically
|
||||
* [notification] add a counter for notification in activity list and accompanying period list, and search results
|
||||
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
|
||||
* [workflow] add breadcrumb to show steps
|
||||
* [popover] add popover html popup mechanism (used by workflow breadcrumb)
|
||||
* [templates] improve updatedBy macro in item metadatas
|
||||
* [parcours]: bug fix when comment is pinned all other comments remain in the collection (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/385)
|
||||
* [workflow]
|
||||
* add My workflow section with my opened subscriptions
|
||||
* apply workflow on documents, accompanyingCourseWork and Evaluations
|
||||
* [wopi-link] a new vue component allow to open wopi link in a fullscreen chill-themed modal
|
||||
|
||||
## Test releases
|
||||
|
||||
### test release 2022-01-19
|
||||
* vuejs: add dead information on all on-the-fly person render boxes, in vis graph and other templates (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/271)
|
||||
* [thirdparty] fix bug in 3rd party view: types was replaced by thirdPartyTypes
|
||||
* [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246)
|
||||
* [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176)
|
||||
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
|
||||
|
||||
## Test releases
|
||||
|
||||
### test release 2022-01-10
|
||||
### test release 2022-01-17
|
||||
|
||||
* [main] Add editableByUser field to locationType entity, adapt the admin template and add this condition in the location-type endpoint (see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/297)
|
||||
* [main] Add mainLocation field to User entity and add it in user form type
|
||||
@ -27,6 +45,8 @@ and this project adheres to
|
||||
* [main] Add mainLocation field to User entity and add it in user form type
|
||||
* [course list in person context] show full username/label for ref
|
||||
* [accompanying period work] remove the possibility to generate document from an accompanying period work
|
||||
|
||||
## Test releases
|
||||
* vuejs: add validation on required fields for AddPerson, Address and Location components
|
||||
* vuejs: treat 422 validation errors in locations and AddPerson components
|
||||
|
||||
|
@ -31,6 +31,7 @@ use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@ -38,8 +39,8 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
final class ActivityController extends AbstractController
|
||||
@ -471,20 +472,21 @@ final class ActivityController extends AbstractController
|
||||
|
||||
public function showAction(Request $request, int $id): Response
|
||||
{
|
||||
$view = null;
|
||||
$entity = $this->activityRepository->find($id);
|
||||
|
||||
[$person, $accompanyingPeriod] = $this->getEntity($request);
|
||||
if (null === $entity) {
|
||||
throw $this->createNotFoundException('Unable to find Activity entity.');
|
||||
}
|
||||
|
||||
$accompanyingPeriod = $entity->getAccompanyingPeriod();
|
||||
$person = $entity->getPerson();
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$view = 'ChillActivityBundle:Activity:showAccompanyingCourse.html.twig';
|
||||
} elseif ($person instanceof Person) {
|
||||
$view = 'ChillActivityBundle:Activity:showPerson.html.twig';
|
||||
}
|
||||
|
||||
$entity = $this->activityRepository->find($id);
|
||||
|
||||
if (null === $entity) {
|
||||
throw $this->createNotFoundException('Unable to find Activity entity.');
|
||||
} else {
|
||||
throw new RuntimeException('the activity should be linked with a period or person');
|
||||
}
|
||||
|
||||
if (null !== $accompanyingPeriod) {
|
||||
@ -493,8 +495,7 @@ final class ActivityController extends AbstractController
|
||||
$entity->personsNotAssociated = $entity->getPersonsNotAssociated();
|
||||
}
|
||||
|
||||
// TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période
|
||||
// $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity);
|
||||
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $entity);
|
||||
|
||||
$deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod);
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Entity;
|
||||
|
||||
use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
@ -61,13 +62,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?AccompanyingPeriod $accompanyingPeriod = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityType")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
* @SerializedName("activityType")
|
||||
* @ORM\JoinColumn(name="type_id")
|
||||
*/
|
||||
@ -107,13 +108,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
* @ORM\Id
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location")
|
||||
* @groups({"read"})
|
||||
* @groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Location $location = null;
|
||||
|
||||
@ -124,7 +125,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Collection $persons = null;
|
||||
|
||||
@ -146,20 +147,20 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialAction")
|
||||
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialaction")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private Collection $socialActions;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialIssue")
|
||||
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialissue")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private Collection $socialIssues;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Collection $thirdParties = null;
|
||||
|
||||
@ -191,7 +192,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
$this->socialActions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function addDocument(Document $document): self
|
||||
public function addDocument(StoredObject $document): self
|
||||
{
|
||||
$this->documents[] = $document;
|
||||
|
||||
@ -425,7 +426,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
return $this->getEmergency();
|
||||
}
|
||||
|
||||
public function removeDocument(Document $document): void
|
||||
public function removeDocument(StoredObject $document): void
|
||||
{
|
||||
$this->documents->removeElement($document);
|
||||
}
|
||||
|
@ -143,9 +143,17 @@
|
||||
</div>
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
<div class="item-col item-meta">
|
||||
{% set notif_counter = chill_count_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) %}
|
||||
{% if notif_counter.total > 0 %}
|
||||
{{ chill_counter_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: type, id: entity.id },
|
||||
buttonText: entity|chill_entity_render_string,
|
||||
isDead: entity.deathdate is not null,
|
||||
isDead: entity.deathdate is defined and entity.deathdate is not null,
|
||||
parent: parent
|
||||
} %}
|
||||
{% endmacro %}
|
||||
|
@ -83,15 +83,15 @@
|
||||
{{ form_row(edit_form.comment) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if edit_form.documents is defined -%}
|
||||
{{ form_row(edit_form.documents) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if edit_form.attendee is defined -%}
|
||||
{{ form_row(edit_form.attendee) }}
|
||||
{% endif %}
|
||||
|
||||
{# TODO .. status #}
|
||||
{%- if edit_form.documents is defined -%}
|
||||
{{ form_row(edit_form.documents) }}
|
||||
{% endif %}
|
||||
|
||||
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
|
||||
|
||||
{% set person_id = null %}
|
||||
{% if entity.person %}
|
||||
|
@ -24,10 +24,12 @@
|
||||
window.activity = {{ activity_json|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_activity') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('vue_activity') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
@ -39,9 +39,11 @@
|
||||
window.activity = {{ activity_json|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_activity') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('vue_activity') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
@ -1,58 +1,60 @@
|
||||
{% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %}
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
<li>
|
||||
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||
'entityId': activity.id
|
||||
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||
'entityId': activity.id
|
||||
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
@ -86,7 +86,7 @@
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': context,
|
||||
'render': 'bloc',
|
||||
'badge_person': 'true'
|
||||
'badge_person': true
|
||||
} %}
|
||||
|
||||
<h2 class="chill-blue">{{ 'Activity data'|trans }}</h2>
|
||||
|
@ -0,0 +1,223 @@
|
||||
<?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\ActivityBundle\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
class ActivityContext implements
|
||||
DocGeneratorContextWithAdminFormInterface,
|
||||
DocGeneratorContextWithPublicFormInterface
|
||||
{
|
||||
private BaseContextData $baseContextData;
|
||||
|
||||
private DocumentCategoryRepository $documentCategoryRepository;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
private PersonRender $personRender;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
DocumentCategoryRepository $documentCategoryRepository,
|
||||
NormalizerInterface $normalizer,
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
EntityManagerInterface $em,
|
||||
PersonRender $personRender,
|
||||
TranslatorInterface $translator,
|
||||
BaseContextData $baseContextData
|
||||
) {
|
||||
$this->documentCategoryRepository = $documentCategoryRepository;
|
||||
$this->normalizer = $normalizer;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->em = $em;
|
||||
$this->personRender = $personRender;
|
||||
$this->translator = $translator;
|
||||
$this->baseContextData = $baseContextData;
|
||||
}
|
||||
|
||||
public function adminFormReverseTransform(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function adminFormTransform(array $data): array
|
||||
{
|
||||
return [
|
||||
'mainPerson' => $data['mainPerson'] ?? false,
|
||||
'mainPersonLabel' => $data['mainPersonLabel'] ?? $this->translator->trans('docgen.Main person'),
|
||||
'person1' => $data['person1'] ?? false,
|
||||
'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'),
|
||||
'person2' => $data['person2'] ?? false,
|
||||
'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'),
|
||||
];
|
||||
}
|
||||
|
||||
public function buildAdminForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('mainPerson', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for main person',
|
||||
])
|
||||
->add('mainPersonLabel', TextType::class, [
|
||||
'label' => 'main person label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('person1', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for person 1',
|
||||
])
|
||||
->add('person1Label', TextType::class, [
|
||||
'label' => 'person 1 label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('person2', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for person 2',
|
||||
])
|
||||
->add('person2Label', TextType::class, [
|
||||
'label' => 'person 2 label',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $entity
|
||||
*/
|
||||
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
|
||||
{
|
||||
$options = $template->getOptions();
|
||||
$persons = $entity->getPersons();
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $key) {
|
||||
if ($options[$key] ?? false) {
|
||||
$builder->add($key, EntityType::class, [
|
||||
'class' => Person::class,
|
||||
'choices' => $persons,
|
||||
'choice_label' => function (Person $p) {
|
||||
return $this->personRender->renderString($p, []);
|
||||
},
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'label' => $options[$key . 'Label'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
|
||||
{
|
||||
if (!$entity instanceof Activity) {
|
||||
throw new UnexpectedTypeException($entity, Activity::class);
|
||||
}
|
||||
$options = $template->getOptions();
|
||||
|
||||
$data = [];
|
||||
$data = array_merge($data, $this->baseContextData->getData());
|
||||
$data['activity'] = $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Activity::class, 'groups' => 'docgen:read']);
|
||||
|
||||
$data['course'] = $this->normalizer->normalize($entity->getAccompanyingPeriod(), 'docgen', ['docgen:expects' => AccompanyingPeriod::class, 'groups' => 'docgen:read']);
|
||||
$data['person'] = $this->normalizer->normalize($entity->getPerson(), 'docgen', ['docgen:expects' => Person::class, 'groups' => 'docgen:read']);
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||
if ($options[$k]) {
|
||||
$data[$k] = $this->normalizer->normalize($contextGenerationData[$k], 'docgen', [
|
||||
'docgen:expects' => Person::class,
|
||||
'groups' => 'docgen:read',
|
||||
'docgen:person:with-household' => true,
|
||||
'docgen:person:with-relations' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'docgen.A basic context for activity';
|
||||
}
|
||||
|
||||
public function getEntityClass(): string
|
||||
{
|
||||
return Activity::class;
|
||||
}
|
||||
|
||||
public function getFormData(DocGeneratorTemplate $template, $entity): array
|
||||
{
|
||||
return [
|
||||
'activity' => $entity,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getKey(): string
|
||||
{
|
||||
return self::class;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'docgen.Activity basic';
|
||||
}
|
||||
|
||||
public function hasAdminForm(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
|
||||
{
|
||||
$options = $template->getOptions();
|
||||
|
||||
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $entity
|
||||
*/
|
||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||
{
|
||||
$doc = new StoredObject();
|
||||
// TODO push document to remote
|
||||
|
||||
$this->em->persist($doc);
|
||||
|
||||
$entity->addDocument($doc);
|
||||
}
|
||||
}
|
@ -32,3 +32,8 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Validator/Constraints/'
|
||||
|
||||
Chill\ActivityBundle\Service\DocGenerator\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Service/DocGenerator/'
|
||||
|
@ -228,3 +228,7 @@ See activity in accompanying course context: Voir l'activité dans le contexte d
|
||||
You get notified of an activity which does not exists any more: Cette notification ne correspond pas à une activité valide.
|
||||
you are not allowed to see it details: La notification fait référence à une activité à laquelle vous n'avez pas accès.
|
||||
This is the minimal activity data: Activité n°
|
||||
|
||||
docgen:
|
||||
Activity basic: Echange
|
||||
A basic context for activity: Contexte pour les échanges
|
||||
|
@ -0,0 +1,52 @@
|
||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
|
||||
|
||||
<div class="flex-table accompanying_course_work-list">
|
||||
<div class="item-bloc document-item bg-chill-llight-gray">
|
||||
<div class="row justify-content-center my-4">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-4x fa-file-text-o text-success"></i>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h3>{{ document.title }}</h3>
|
||||
<small>{{ document.object.type }}</small>
|
||||
|
||||
{% if document.description is not empty %}
|
||||
<blockquote class="chill-user-quote mt-2">
|
||||
{{ document.description }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if display_action is defined and display_action == true %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
<li>
|
||||
|
||||
{#
|
||||
data-button is optional !
|
||||
OPTIONS:
|
||||
'changeIcon' string
|
||||
'changeClass' string
|
||||
'noText' boolean
|
||||
|
||||
#}{% set button = {
|
||||
'changeIcon': 'fa-unlock',
|
||||
} %}
|
||||
|
||||
{# vue component #}
|
||||
<span
|
||||
data-module="wopi-link"
|
||||
data-wopi-url="{{ path('chill_wopi_file_edit', {'fileId': document.object.uuid}) }}"
|
||||
data-doc-title="{{ document.title|e('html_attr') }}"
|
||||
data-doc-type="{{ document.object.type|e('html_attr') }}"
|
||||
data-button="{{ button|json_encode }}"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
@ -0,0 +1,19 @@
|
||||
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as m %}
|
||||
|
||||
<div class="flex-grow-1 {% if add_classes is defined %}{{ add_classes }}{% else %}h2{% endif %}">
|
||||
<div>
|
||||
{% if concerne is defined and concerne == true %}
|
||||
<span class="item-key">{{ 'Concerne'|trans }}: </span>
|
||||
{% endif %}
|
||||
|
||||
{{ 'workflow.Document (n°%doc%)'|trans({'%doc%': document.id}) }}
|
||||
|
||||
{% if description is defined and description == true %}
|
||||
{{ ' — ' ~ document.title }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if breadcrumb is defined and breadcrumb == true %}
|
||||
{{ m.breadcrumb(_context) }}
|
||||
{% endif %}
|
||||
</div>
|
@ -6,61 +6,71 @@
|
||||
|
||||
{% block title %}
|
||||
{# {{ 'Detail of document of %name%'|trans({ '%name%': accompanyingCourse|chill_entity_render_string } ) }} #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ 'Document %title%' | trans({ '%title%': document.title }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="document-show">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<h1>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h1>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ document.title }}</dd>
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ document.title }}</dd>
|
||||
|
||||
{% if document.category is not null %}
|
||||
<dt>{{ 'Category'|trans }}</dt>
|
||||
<dd>{{ document.category.name|localize_translatable_string }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{{ 'Description' | trans }}</dt>
|
||||
<dd>
|
||||
{% if document.description is empty %}
|
||||
<span class="chill-no-data-statement">{{ 'Any description'|trans }}</span>
|
||||
{% else %}
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ document.description|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{{ 'Description' | trans }}</dt>
|
||||
<dd>
|
||||
{% if document.description is empty %}
|
||||
<span class="chill-no-data-statement">{{ 'Any description'|trans }}</span>
|
||||
{% else %}
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ document.description|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
</dl>
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'id': document.id, 'course': accompanyingCourse.id}) }}" class="btn btn-edit">
|
||||
{{ 'Edit' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% block block_post_menu %}
|
||||
<div class="post-menu pt-4">
|
||||
{% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %}
|
||||
{% if workflows_frame is not empty %}
|
||||
{{ workflows_frame|raw }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'id': document.id, 'course': accompanyingCourse.id}) }}" class="btn btn-edit">
|
||||
{{ 'Edit' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{% endblock %}
|
@ -0,0 +1,74 @@
|
||||
<?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\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
/**
|
||||
* TODO: injecter le repository directement.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
||||
}
|
||||
|
||||
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingCourseDocument
|
||||
{
|
||||
return $this->repository->find($entityWorkflow->getRelatedEntityId());
|
||||
}
|
||||
|
||||
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||
{
|
||||
return '@ChillDocStore/AccompanyingCourseDocument/_workflow.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'document' => $this->getRelatedEntity($entityWorkflow),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||
{
|
||||
return '@ChillDocStore/AccompanyingCourseDocument/_workflow.title.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||
{
|
||||
return $this->getTemplateData($entityWorkflow, $options);
|
||||
}
|
||||
|
||||
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||
{
|
||||
return $entityWorkflow->getRelatedEntityClass() === AccompanyingCourseDocument::class;
|
||||
}
|
||||
|
||||
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -27,3 +27,8 @@ services:
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\DocStoreBundle\Workflow\:
|
||||
resource: './../Workflow/'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
@ -9,6 +9,7 @@ Create new document: Créer un nouveau document
|
||||
New document for %name%: Nouveau document pour %name%
|
||||
Editing document for %name%: Modification d'un document pour %name%
|
||||
Edit Document: Modification d'un document
|
||||
Update document: Modifier le document
|
||||
Existing document: Document existant
|
||||
No document to download: Aucun document à télécharger
|
||||
'Choose a document category': Choisissez une catégorie de document
|
||||
|
@ -31,6 +31,7 @@ use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
||||
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
|
||||
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
@ -56,6 +57,8 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill_main.notification_handler');
|
||||
$container->registerForAutoconfiguration(NotificationCounterInterface::class)
|
||||
->addTag('chill.count_notification.user');
|
||||
$container->registerForAutoconfiguration(EntityWorkflowHandlerInterface::class)
|
||||
->addTag('chill_main.workflow_handler');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass());
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||
|
118
src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php
Normal file
118
src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class WorkflowApiController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security, EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/main/workflow/{id}/subscribe", methods={"POST"})
|
||||
*/
|
||||
public function subscribe(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||
{
|
||||
return $this->handleSubscription($entityWorkflow, $request, 'subscribe');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/main/workflow/{id}/unsubscribe", methods={"POST"})
|
||||
*/
|
||||
public function unsubscribe(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||
{
|
||||
return $this->handleSubscription($entityWorkflow, $request, 'unsubscribe');
|
||||
}
|
||||
|
||||
private function handleSubscription(EntityWorkflow $entityWorkflow, Request $request, string $action): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
throw new AccessDeniedException();
|
||||
}
|
||||
|
||||
if (!$request->query->has('subscribe')) {
|
||||
throw new BadRequestHttpException('missing subscribe parameter');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
switch ($request->query->get('subscribe')) {
|
||||
case 'final':
|
||||
switch ($action) {
|
||||
case 'subscribe':
|
||||
$entityWorkflow->addSubscriberToFinal($user);
|
||||
|
||||
break;
|
||||
|
||||
case 'unsubscribe':
|
||||
$entityWorkflow->removeSubscriberToFinal($user);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'step':
|
||||
switch ($action) {
|
||||
case 'subscribe':
|
||||
$entityWorkflow->addSubscriberToStep($user);
|
||||
|
||||
break;
|
||||
|
||||
case 'unsubscribe':
|
||||
$entityWorkflow->removeSubscriberToStep($user);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BadRequestHttpException('subscribe parameter must be equal to "step" or "final"');
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'step' => $entityWorkflow->isUserSubscribedToStep($user),
|
||||
'final' => $entityWorkflow->isUserSubscribedToFinal($user),
|
||||
],
|
||||
JsonResponse::HTTP_OK,
|
||||
[],
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
257
src/Bundle/ChillMainBundle/Controller/WorkflowController.php
Normal file
257
src/Bundle/ChillMainBundle/Controller/WorkflowController.php
Normal file
@ -0,0 +1,257 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
|
||||
use Chill\MainBundle\Form\EntityWorkflowCommentType;
|
||||
use Chill\MainBundle\Form\WorkflowStepType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\TransitionBlocker;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function count;
|
||||
|
||||
class WorkflowController extends AbstractController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private EntityWorkflowRepository $entityWorkflowRepository;
|
||||
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
private ValidatorInterface $validator;
|
||||
|
||||
public function __construct(EntityWorkflowManager $entityWorkflowManager, EntityWorkflowRepository $entityWorkflowRepository, ValidatorInterface $validator, PaginatorFactory $paginatorFactory, Registry $registry, EntityManagerInterface $entityManager, TranslatorInterface $translator)
|
||||
{
|
||||
$this->entityWorkflowManager = $entityWorkflowManager;
|
||||
$this->entityWorkflowRepository = $entityWorkflowRepository;
|
||||
$this->validator = $validator;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->registry = $registry;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/main/workflow/create", name="chill_main_workflow_create")
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
if (!$request->query->has('entityClass')) {
|
||||
throw new BadRequestHttpException('Missing entityClass parameter');
|
||||
}
|
||||
|
||||
if (!$request->query->has('entityId')) {
|
||||
throw new BadRequestHttpException('missing entityId parameter');
|
||||
}
|
||||
|
||||
if (!$request->query->has('workflow')) {
|
||||
throw new BadRequestHttpException('missing workflow parameter');
|
||||
}
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow
|
||||
->setRelatedEntityClass($request->query->get('entityClass'))
|
||||
->setRelatedEntityId($request->query->getInt('entityId'))
|
||||
->setWorkflowName($request->query->get('workflow'));
|
||||
|
||||
$errors = $this->validator->validate($entityWorkflow, null, ['creation']);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$msg = [];
|
||||
|
||||
foreach ($errors as $error) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $error */
|
||||
$msg[] = $error->getMessage();
|
||||
}
|
||||
|
||||
return new Response(implode("\n", $msg), Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(EntityWorkflowVoter::CREATE, $entityWorkflow);
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($entityWorkflow);
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest")
|
||||
*/
|
||||
public function myWorkflowsDest(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
|
||||
$total = $this->entityWorkflowRepository->countByDest($this->getUser());
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
$workflows = $this->entityWorkflowRepository->findByDest(
|
||||
$this->getUser(),
|
||||
['createdAt' => 'DESC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/list.html.twig',
|
||||
[
|
||||
'workflows' => $this->buildHandler($workflows),
|
||||
'paginator' => $paginator,
|
||||
'step' => 'dest',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/main/workflow/list/subscribed", name="chill_main_workflow_list_subscribed")
|
||||
*/
|
||||
public function myWorkflowsSubscribed(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
|
||||
$total = $this->entityWorkflowRepository->countBySubscriber($this->getUser());
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
$workflows = $this->entityWorkflowRepository->findBySubscriber(
|
||||
$this->getUser(),
|
||||
['createdAt' => 'DESC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/list.html.twig',
|
||||
[
|
||||
'workflows' => $this->buildHandler($workflows),
|
||||
'paginator' => $paginator,
|
||||
'step' => 'subscribed',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/main/workflow/{id}/show", name="chill_main_workflow_show")
|
||||
*/
|
||||
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(EntityWorkflowVoter::SEE, $entityWorkflow);
|
||||
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
if (count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||
// possible transition
|
||||
$transitionForm = $this->createForm(
|
||||
WorkflowStepType::class,
|
||||
$entityWorkflow->getCurrentStep(),
|
||||
['transition' => true, 'entity_workflow' => $entityWorkflow]
|
||||
);
|
||||
|
||||
$transitionForm->handleRequest($request);
|
||||
|
||||
if ($transitionForm->isSubmitted() && $transitionForm->isValid()) {
|
||||
if (!$workflow->can($entityWorkflow, $transition = $transitionForm['transition']->getData()->getName())) {
|
||||
$blockers = $workflow->buildTransitionBlockerList($entityWorkflow, $transition);
|
||||
$msgs = array_map(function (TransitionBlocker $tb) {
|
||||
return $this->translator->trans(
|
||||
$tb->getMessage(),
|
||||
$tb->getParameters()
|
||||
);
|
||||
}, iterator_to_array($blockers));
|
||||
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf(
|
||||
"not allowed to apply transition {$transition}: %s",
|
||||
implode(', ', $msgs)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$workflow->apply($entityWorkflow, $transition);
|
||||
|
||||
foreach ($transitionForm['future_dest_users']->getData() as $user) {
|
||||
$entityWorkflow->getCurrentStep()->addDestUser($user);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||
}
|
||||
|
||||
if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) {
|
||||
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
|
||||
$commentForm->handleRequest($request);
|
||||
|
||||
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
|
||||
$this->entityManager->persist($newComment);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
|
||||
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||
}
|
||||
*/
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/index.html.twig',
|
||||
[
|
||||
'handler_template' => $handler->getTemplate($entityWorkflow),
|
||||
'handler_template_title' => $handler->getTemplateTitle($entityWorkflow),
|
||||
'handler_template_data' => $handler->getTemplateData($entityWorkflow),
|
||||
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
//'comment_form' => $commentForm->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function buildHandler(array $workflows): array
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach ($workflows as $workflow) {
|
||||
$handler = $this->entityWorkflowManager->getHandler($workflow);
|
||||
$lines[] = [
|
||||
'handler' => $handler,
|
||||
'entity_workflow' => $workflow,
|
||||
];
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
}
|
@ -168,6 +168,7 @@ class ChillMainExtension extends Extension implements
|
||||
$loader->load('services/timeline.yaml');
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
$loader->load('services/mailer.yaml');
|
||||
|
||||
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?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\MainBundle\Doctrine\Model;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait TrackCreationTrait
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
|
||||
*/
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $createdBy = null;
|
||||
|
||||
public function getCreatedAt(): ?DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->createdAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedBy(User $user): self
|
||||
{
|
||||
$this->createdBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?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\MainBundle\Doctrine\Model;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait TrackUpdateTrait
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
|
||||
*/
|
||||
private ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $updatedBy = null;
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getUpdatedBy(): ?User
|
||||
{
|
||||
return $this->updatedBy;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updatedAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(User $user): self
|
||||
{
|
||||
$this->updatedBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -23,6 +23,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(
|
||||
* name="chill_main_notification",
|
||||
* indexes={
|
||||
* @ORM\Index(name="chill_main_notification_related_entity_idx", columns={"relatedentityclass", "relatedentityid"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
|
439
src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
Normal file
439
src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
Normal file
@ -0,0 +1,439 @@
|
||||
<?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\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
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\Workflow\Validator\EntityWorkflowCreation;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Iterator;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity")
|
||||
* @EntityWorkflowCreation(groups={"creation"})
|
||||
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "entity_workflow": EntityWorkflow::class
|
||||
* })
|
||||
*/
|
||||
class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=EntityWorkflowComment::class, mappedBy="entityWorkflow", orphanRemoval=true)
|
||||
*
|
||||
* @var Collection|EntityWorkflowComment[]
|
||||
*/
|
||||
private Collection $comments;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private string $relatedEntityClass = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $relatedEntityId;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=EntityWorkflowStep::class, mappedBy="entityWorkflow", orphanRemoval=true, cascade={"persist"})
|
||||
* @ORM\OrderBy({"transitionAt": "ASC", "id": "ASC"})
|
||||
*
|
||||
* @var Collection|EntityWorkflowStep[]
|
||||
*/
|
||||
private Collection $steps;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_final")
|
||||
*
|
||||
* @var Collection|User[]
|
||||
*/
|
||||
private Collection $subscriberToFinal;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_step")
|
||||
*
|
||||
* @var Collection|User[]
|
||||
*/
|
||||
private Collection $subscriberToStep;
|
||||
|
||||
/**
|
||||
* a step which will store all the transition data.
|
||||
*/
|
||||
private ?EntityWorkflowStep $transitionningStep = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private string $workflowName;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->subscriberToFinal = new ArrayCollection();
|
||||
$this->subscriberToStep = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->steps = new ArrayCollection();
|
||||
|
||||
$initialStep = new EntityWorkflowStep();
|
||||
$initialStep
|
||||
->setCurrentStep('initial');
|
||||
$this->addStep($initialStep);
|
||||
}
|
||||
|
||||
public function addComment(EntityWorkflowComment $comment): self
|
||||
{
|
||||
if (!$this->comments->contains($comment)) {
|
||||
$this->comments[] = $comment;
|
||||
$comment->setEntityWorkflow($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal You should prepare a step and run a workflow transition instead of manually adding a step
|
||||
*/
|
||||
public function addStep(EntityWorkflowStep $step): self
|
||||
{
|
||||
if (!$this->steps->contains($step)) {
|
||||
$this->steps[] = $step;
|
||||
$step->setEntityWorkflow($this);
|
||||
|
||||
if ($this->isFinalize()) {
|
||||
$step->setFinalizeAfter(true);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addSubscriberToFinal(User $user): self
|
||||
{
|
||||
if (!$this->subscriberToFinal->contains($user)) {
|
||||
$this->subscriberToFinal[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addSubscriberToStep(User $user): self
|
||||
{
|
||||
if (!$this->subscriberToStep->contains($user)) {
|
||||
$this->subscriberToStep[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function getCurrentStep(): ?EntityWorkflowStep
|
||||
{
|
||||
$step = $this->steps->last();
|
||||
|
||||
if (false !== $step) {
|
||||
return $step;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getCurrentStepCreatedAt(): ?DateTimeInterface
|
||||
{
|
||||
if (null !== $previous = $this->getPreviousStepIfAny()) {
|
||||
return $previous->getTransitionAt();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getCurrentStepCreatedBy(): ?User
|
||||
{
|
||||
if (null !== $previous = $this->getPreviousStepIfAny()) {
|
||||
return $previous->getTransitionBy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRelatedEntityClass(): string
|
||||
{
|
||||
return $this->relatedEntityClass;
|
||||
}
|
||||
|
||||
public function getRelatedEntityId(): int
|
||||
{
|
||||
return $this->relatedEntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used by MarkingStore.
|
||||
*
|
||||
* get a string representation of the step
|
||||
*/
|
||||
public function getStep(): string
|
||||
{
|
||||
return $this->getCurrentStep()->getCurrentStep();
|
||||
}
|
||||
|
||||
public function getStepAfter(EntityWorkflowStep $step): ?EntityWorkflowStep
|
||||
{
|
||||
$iterator = $this->steps->getIterator();
|
||||
|
||||
if ($iterator instanceof Iterator) {
|
||||
$iterator->rewind();
|
||||
|
||||
while ($iterator->valid()) {
|
||||
$curStep = $iterator->current();
|
||||
|
||||
if ($curStep === $step) {
|
||||
$iterator->next();
|
||||
|
||||
if ($iterator->valid()) {
|
||||
return $iterator->current();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
$iterator->next();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|Collection
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
public function getStepsChained(): array
|
||||
{
|
||||
$iterator = $this->steps->getIterator();
|
||||
$previous = $next = $current = null;
|
||||
$steps = [];
|
||||
|
||||
$iterator->rewind();
|
||||
|
||||
while ($iterator->valid()) {
|
||||
$previous = $current;
|
||||
$steps[] = $current = $iterator->current();
|
||||
$current->setPrevious($previous);
|
||||
|
||||
$iterator->next();
|
||||
|
||||
if ($iterator->valid()) {
|
||||
$next = $iterator->current();
|
||||
} else {
|
||||
$next = null;
|
||||
}
|
||||
|
||||
$current->setNext($next);
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|Collection
|
||||
*/
|
||||
public function getSubscriberToFinal()
|
||||
{
|
||||
return $this->subscriberToFinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|Collection
|
||||
*/
|
||||
public function getSubscriberToStep()
|
||||
{
|
||||
return $this->subscriberToStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the step which is transitionning. Should be called only by event which will
|
||||
* concern the transition.
|
||||
*/
|
||||
public function getTransitionningStep(): ?EntityWorkflowStep
|
||||
{
|
||||
return $this->transitionningStep;
|
||||
}
|
||||
|
||||
public function getWorkflowName(): string
|
||||
{
|
||||
return $this->workflowName;
|
||||
}
|
||||
|
||||
public function isFinalize(): bool
|
||||
{
|
||||
$steps = $this->getStepsChained();
|
||||
|
||||
if (1 === count($steps)) {
|
||||
// the initial step cannot be finalized
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflowStep $last */
|
||||
$last = end($steps);
|
||||
|
||||
return $last->getPrevious()->isFinalizeAfter();
|
||||
}
|
||||
|
||||
public function isFreeze(): bool
|
||||
{
|
||||
$steps = $this->getStepsChained();
|
||||
|
||||
if (1 === count($steps)) {
|
||||
// the initial step cannot be finalized
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflowStep $last */
|
||||
$last = end($steps);
|
||||
|
||||
return $last->getPrevious()->isFreezeAfter();
|
||||
}
|
||||
|
||||
public function isUserSubscribedToFinal(User $user): bool
|
||||
{
|
||||
return $this->subscriberToFinal->contains($user);
|
||||
}
|
||||
|
||||
public function isUserSubscribedToStep(User $user): bool
|
||||
{
|
||||
return $this->subscriberToStep->contains($user);
|
||||
}
|
||||
|
||||
public function prepareStepBeforeTransition(EntityWorkflowStep $step): self
|
||||
{
|
||||
$this->transitionningStep = $step;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeComment(EntityWorkflowComment $comment): self
|
||||
{
|
||||
if ($this->comments->removeElement($comment)) {
|
||||
$comment->setEntityWorkflow(null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeStep(EntityWorkflowStep $step): self
|
||||
{
|
||||
if ($this->steps->removeElement($step)) {
|
||||
$step->setEntityWorkflow(null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSubscriberToFinal(User $user): self
|
||||
{
|
||||
$this->subscriberToFinal->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSubscriberToStep(User $user): self
|
||||
{
|
||||
$this->subscriberToStep->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRelatedEntityClass(string $relatedEntityClass): EntityWorkflow
|
||||
{
|
||||
$this->relatedEntityClass = $relatedEntityClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRelatedEntityId(int $relatedEntityId): EntityWorkflow
|
||||
{
|
||||
$this->relatedEntityId = $relatedEntityId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method use by marking store.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStep(string $step): self
|
||||
{
|
||||
$newStep = new EntityWorkflowStep();
|
||||
$newStep->setCurrentStep($step);
|
||||
|
||||
// copy the freeze
|
||||
if ($this->getCurrentStep()->isFreezeAfter()) {
|
||||
$newStep->setFreezeAfter(true);
|
||||
}
|
||||
|
||||
$this->addStep($newStep);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setWorkflowName(string $workflowName): EntityWorkflow
|
||||
{
|
||||
$this->workflowName = $workflowName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getPreviousStepIfAny(): ?EntityWorkflowStep
|
||||
{
|
||||
if (1 === count($this->steps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->steps->get($this->steps->count() - 2);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?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\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity_comment")
|
||||
*/
|
||||
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""})
|
||||
*/
|
||||
private string $comment = '';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="comments")
|
||||
*/
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function getEntityWorkflow(): ?EntityWorkflow
|
||||
{
|
||||
return $this->entityWorkflow;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{EntityWorkflow::addComment}
|
||||
*/
|
||||
public function setEntityWorkflow(?EntityWorkflow $entityWorkflow): self
|
||||
{
|
||||
$this->entityWorkflow = $entityWorkflow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
<?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\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use function count;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity_step")
|
||||
*/
|
||||
class EntityWorkflowStep
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""})
|
||||
*/
|
||||
private string $comment = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private ?string $currentStep = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private array $destEmail = [];
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_step_user")
|
||||
*/
|
||||
private Collection $destUser;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="steps")
|
||||
*/
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
*/
|
||||
private bool $finalizeAfter = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
*/
|
||||
private bool $freezeAfter = false;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* filled by @see{EntityWorkflow::getStepsChained}.
|
||||
*/
|
||||
private ?EntityWorkflowStep $next = null;
|
||||
|
||||
/**
|
||||
* filled by @see{EntityWorkflow::getStepsChained}.
|
||||
*/
|
||||
private ?EntityWorkflowStep $previous = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?string $transitionAfter = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable")
|
||||
*/
|
||||
private ?DateTimeImmutable $transitionAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $transitionBy = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*/
|
||||
private ?string $transitionByEmail = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->destUser = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function addDestEmail(string $email): self
|
||||
{
|
||||
if (!in_array($email, $this->destEmail, true)) {
|
||||
$this->destEmail[] = $email;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDestUser(User $user): self
|
||||
{
|
||||
if (!$this->destUser->contains($user)) {
|
||||
$this->destUser[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function getCurrentStep(): ?string
|
||||
{
|
||||
return $this->currentStep;
|
||||
}
|
||||
|
||||
public function getDestEmail(): array
|
||||
{
|
||||
return $this->destEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|Collection
|
||||
*/
|
||||
public function getDestUser()
|
||||
{
|
||||
return $this->destUser;
|
||||
}
|
||||
|
||||
public function getEntityWorkflow(): ?EntityWorkflow
|
||||
{
|
||||
return $this->entityWorkflow;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getNext(): ?EntityWorkflowStep
|
||||
{
|
||||
return $this->next;
|
||||
}
|
||||
|
||||
public function getPrevious(): ?EntityWorkflowStep
|
||||
{
|
||||
return $this->previous;
|
||||
}
|
||||
|
||||
public function getTransitionAfter(): ?string
|
||||
{
|
||||
return $this->transitionAfter;
|
||||
}
|
||||
|
||||
public function getTransitionAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->transitionAt;
|
||||
}
|
||||
|
||||
public function getTransitionBy(): ?User
|
||||
{
|
||||
return $this->transitionBy;
|
||||
}
|
||||
|
||||
public function getTransitionByEmail(): ?string
|
||||
{
|
||||
return $this->transitionByEmail;
|
||||
}
|
||||
|
||||
public function isFinalizeAfter(): bool
|
||||
{
|
||||
return $this->finalizeAfter;
|
||||
}
|
||||
|
||||
public function isFreezeAfter(): bool
|
||||
{
|
||||
return $this->freezeAfter;
|
||||
}
|
||||
|
||||
public function removeDestEmail(string $email): self
|
||||
{
|
||||
$this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) {
|
||||
return $email !== $existing;
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDestUser(User $user): self
|
||||
{
|
||||
$this->destUser->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComment(?string $comment): EntityWorkflowStep
|
||||
{
|
||||
$this->comment = (string) $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCurrentStep(?string $currentStep): EntityWorkflowStep
|
||||
{
|
||||
$this->currentStep = $currentStep;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDestEmail(array $destEmail): EntityWorkflowStep
|
||||
{
|
||||
$this->destEmail = $destEmail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see(EntityWorkflow::addStep} instead
|
||||
*/
|
||||
public function setEntityWorkflow(?EntityWorkflow $entityWorkflow): EntityWorkflowStep
|
||||
{
|
||||
$this->entityWorkflow = $entityWorkflow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFinalizeAfter(bool $finalizeAfter): EntityWorkflowStep
|
||||
{
|
||||
$this->finalizeAfter = $finalizeAfter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep
|
||||
{
|
||||
$this->freezeAfter = $freezeAfter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EntityWorkflowStep
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setNext(?EntityWorkflowStep $next): self
|
||||
{
|
||||
$this->next = $next;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EntityWorkflowStep
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setPrevious(?EntityWorkflowStep $previous): self
|
||||
{
|
||||
$this->previous = $previous;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTransitionAfter(?string $transitionAfter): EntityWorkflowStep
|
||||
{
|
||||
$this->transitionAfter = $transitionAfter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTransitionAt(?DateTimeImmutable $transitionAt): EntityWorkflowStep
|
||||
{
|
||||
$this->transitionAt = $transitionAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTransitionBy(?User $transitionBy): EntityWorkflowStep
|
||||
{
|
||||
$this->transitionBy = $transitionBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTransitionByEmail(?string $transitionByEmail): EntityWorkflowStep
|
||||
{
|
||||
$this->transitionByEmail = $transitionByEmail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Assert\Callback
|
||||
*
|
||||
* @param mixed $payload
|
||||
*/
|
||||
public function validateOnCreation(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
return;
|
||||
|
||||
if ($this->isFinalizeAfter()) {
|
||||
if (0 !== count($this->getDestUser())) {
|
||||
$context->buildViolation('workflow.No dest users when the workflow is finalized')
|
||||
->atPath('finalizeAfter')
|
||||
->addViolation();
|
||||
}
|
||||
} else {
|
||||
if (0 === count($this->getDestUser())) {
|
||||
$context->buildViolation('workflow.The next step must count at least one dest')
|
||||
->atPath('finalizeAfter')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?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\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class EntityWorkflowCommentType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('comment', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
@ -36,14 +36,20 @@ class UserToJsonTransformer implements DataTransformerInterface
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
$denormalized = json_decode($value, true);
|
||||
|
||||
if ($this->multiple) {
|
||||
if (null === $denormalized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
function ($item) { return $this->denormalizeOne($item); },
|
||||
json_decode($value, true)
|
||||
$denormalized
|
||||
);
|
||||
}
|
||||
|
||||
return $this->denormalizeOne(json_decode($value, true));
|
||||
return $this->denormalizeOne($denormalized);
|
||||
}
|
||||
|
||||
/**
|
||||
|
111
src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
Normal file
111
src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?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\MainBundle\Form;
|
||||
|
||||
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\Workflow\EntityWorkflowManager;
|
||||
use LogicException;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class WorkflowStepType extends AbstractType
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry)
|
||||
{
|
||||
$this->entityWorkflowManager = $entityWorkflowManager;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var \Chill\MainBundle\Entity\Workflow\EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $options['entity_workflow'];
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
|
||||
if (true === $options['transition']) {
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new LogicException('if transition is true, entity_workflow should be defined');
|
||||
}
|
||||
|
||||
$transitions = $this->registry
|
||||
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
|
||||
->getEnabledTransitions($entityWorkflow);
|
||||
|
||||
$choices = array_combine(
|
||||
array_map(static function (Transition $transition) { return $transition->getName(); }, $transitions),
|
||||
$transitions
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Transition',
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
'choice_label' => static function (Transition $transition) {
|
||||
return implode(', ', $transition->getTos());
|
||||
},
|
||||
])
|
||||
->add('future_dest_users', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'mapped' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
$handler->supportsFreeze($entityWorkflow)
|
||||
&& !$entityWorkflow->isFreeze()
|
||||
) {
|
||||
$builder
|
||||
->add('freezeAfter', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'workflow.Freeze',
|
||||
'help' => 'workflow.The associated element will be freezed',
|
||||
]);
|
||||
}
|
||||
|
||||
$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',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefined('class', EntityWorkflowStep::class)
|
||||
->setRequired('transition')
|
||||
->setAllowedTypes('transition', 'bool')
|
||||
->setRequired('entity_workflow')
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
|
||||
}
|
||||
}
|
36
src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php
Normal file
36
src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?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\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class WorkflowTransitionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('current_step', WorkflowStepType::class, [
|
||||
'transition' => true,
|
||||
'entity_workflow' => $options['entity_workflow'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setRequired('entity_workflow')
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ class NotificationMailer
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->to($dest->getEmail())
|
||||
->subject('Re: [Chill] ' . $comment->getNotification()->getTitle())
|
||||
->subject('Re: ' . $comment->getNotification()->getTitle())
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
|
||||
->context([
|
||||
'comment' => $comment,
|
||||
@ -79,11 +79,13 @@ class NotificationMailer
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = new Email();
|
||||
$email
|
||||
->subject($notification->getTitle());
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email
|
||||
->text($notification->getMessage())
|
||||
->subject('[Chill] ' . $notification->getTitle());
|
||||
->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
|
@ -15,12 +15,15 @@ use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Helps to find if a notification exist for a given entity.
|
||||
*/
|
||||
class NotificationPresence
|
||||
{
|
||||
private array $cache = [];
|
||||
|
||||
private NotificationRepository $notificationRepository;
|
||||
|
||||
private Security $security;
|
||||
@ -31,6 +34,29 @@ class NotificationPresence
|
||||
$this->notificationRepository = $notificationRepository;
|
||||
}
|
||||
|
||||
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array
|
||||
{
|
||||
if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) {
|
||||
return $this->cache[$relatedEntityClass][$relatedEntityId];
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
$counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated(
|
||||
$relatedEntityClass,
|
||||
$relatedEntityId,
|
||||
$user
|
||||
);
|
||||
|
||||
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter;
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
return ['unread' => 0, 'read' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
|
@ -23,6 +23,13 @@ class NotificationTwigExtension extends AbstractExtension
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
new TwigFunction('chill_count_notifications', [NotificationTwigExtensionRuntime::class, 'countNotificationsFor'], [
|
||||
'is_safe' => [],
|
||||
]),
|
||||
new TwigFunction('chill_counter_notifications', [NotificationTwigExtensionRuntime::class, 'counterNotificationFor'], [
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,21 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
$this->notificationPresence = $notificationPresence;
|
||||
}
|
||||
|
||||
public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
return $environment->render(
|
||||
'@ChillMain/Notification/extension_counter_notifications_for.html.twig',
|
||||
[
|
||||
'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array
|
||||
{
|
||||
return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
|
||||
}
|
||||
|
||||
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
$notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\DBAL\Statement;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@ -24,6 +25,8 @@ final class NotificationRepository implements ObjectRepository
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null;
|
||||
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
@ -48,6 +51,30 @@ final class NotificationRepository implements ObjectRepository
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
|
||||
{
|
||||
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
|
||||
$sql =
|
||||
'SELECT
|
||||
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread,
|
||||
SUM((cmn.sender_id = 1812)::int) AS sent,
|
||||
COUNT(cmn.*) AS total
|
||||
FROM chill_main_notification cmn
|
||||
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
|
||||
$this->notificationByRelatedEntityAndUserAssociatedStatement =
|
||||
$this->em->getConnection()->prepare($sql);
|
||||
}
|
||||
|
||||
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
|
||||
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]);
|
||||
|
||||
$result = $results->fetchAssociative();
|
||||
|
||||
$results->free();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function countUnreadByUser(User $user): int
|
||||
{
|
||||
$sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId';
|
||||
@ -153,11 +180,29 @@ final class NotificationRepository implements ObjectRepository
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
|
||||
{
|
||||
return
|
||||
$this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user)
|
||||
->select('n')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Notification::class;
|
||||
}
|
||||
|
||||
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->select('n')
|
||||
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'))
|
||||
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId'))
|
||||
->andWhere($qb->expr()->isNotNull('n.sender'))
|
||||
@ -171,17 +216,7 @@ final class NotificationRepository implements ObjectRepository
|
||||
->setParameter('relatedEntityId', $relatedEntityId)
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Notification::class;
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
|
||||
|
@ -0,0 +1,137 @@
|
||||
<?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\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class EntityWorkflowRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(EntityWorkflow::class);
|
||||
}
|
||||
|
||||
public function countByDest(User $user): int
|
||||
{
|
||||
$qb = $this->buildQueryByDest($user)->select('count(ew)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countBySubscriber(User $user): int
|
||||
{
|
||||
$qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function find($id): ?EntityWorkflow
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|EntityWorkflow[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|mixed $limit
|
||||
* @param null|mixed $offset
|
||||
*
|
||||
* @return array|EntityWorkflow[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByDest($user)->select('ew');
|
||||
|
||||
foreach ($orderBy as $key => $sort) {
|
||||
$qb->addOrderBy('ew.' . $key, $sort);
|
||||
}
|
||||
|
||||
$qb->setMaxResults($limit)->setFirstResult($offset);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryBySubscriber($user)->select('ew');
|
||||
|
||||
foreach ($orderBy as $key => $sort) {
|
||||
$qb->addOrderBy('ew.' . $key, $sort);
|
||||
}
|
||||
|
||||
$qb->setMaxResults($limit)->setFirstResult($offset);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?EntityWorkflow
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return EntityWorkflow::class;
|
||||
}
|
||||
|
||||
private function buildQueryByDest(User $user): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('ew');
|
||||
|
||||
$qb->join('ew.steps', 'step');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
||||
$qb->expr()->isNull('step.transitionAfter')
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameter('user', $user);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function buildQueryBySubscriber(User $user): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('ew');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'ew.subscriberToStep'),
|
||||
$qb->expr()->isMemberOf(':user', 'ew.subscriberToFinal'),
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameter('user', $user);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// Access to Bootstrap variables and mixins
|
||||
@import '~ChillMainAssets/module/bootstrap/shared';
|
||||
@import 'ChillMainAssets/module/bootstrap/shared';
|
||||
|
||||
// Chill variables
|
||||
@import './scss/chill_variables';
|
||||
@ -277,11 +277,17 @@ table.table-bordered {
|
||||
}
|
||||
}
|
||||
|
||||
/// meta-data
|
||||
div.updatedBy,
|
||||
div.metadata {
|
||||
span.user, span.date {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
}
|
||||
div.metadata {
|
||||
font-size: smaller;
|
||||
color: $gray-600;
|
||||
span.user, span.date {
|
||||
text-decoration: underline dotted;
|
||||
&:hover {
|
||||
color: $gray-700;
|
||||
}
|
||||
@ -424,6 +430,62 @@ span.item-key {
|
||||
//text-decoration: dotted underline;
|
||||
}
|
||||
|
||||
/// Workflows
|
||||
div.workflow {
|
||||
section.step {
|
||||
border: 1px solid $chill-l-gray;
|
||||
padding: 1em 2em;
|
||||
div.flex-table {
|
||||
margin: 1.5em -2em;
|
||||
}
|
||||
}
|
||||
div.to-decision,
|
||||
div.decided {
|
||||
font-variant: all-small-caps;
|
||||
margin-left: 1em;
|
||||
}
|
||||
div.to-decision {
|
||||
font-weight: 300;
|
||||
}
|
||||
div.decided {
|
||||
font-weight: 600;
|
||||
}
|
||||
div.breadcrumb {
|
||||
display: initial;
|
||||
margin-bottom: 0;
|
||||
padding-right: 0.5em;
|
||||
background-color: tint-color($chill-yellow, 90%);
|
||||
border: 1px solid $chill-yellow;
|
||||
color: $primary;
|
||||
border-radius: 1.5em;
|
||||
font-size: 12pt;
|
||||
font-weight: 500;
|
||||
font-variant: small-caps;
|
||||
span, a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override bootstrap popover styles
|
||||
div.popover {
|
||||
box-shadow: 0 0 10px -5px $dark;
|
||||
.popover-arrow {}
|
||||
.popover-header {}
|
||||
.popover-body {}
|
||||
|
||||
// Specific worflow breadcrumb popover
|
||||
&.workflow-transition {
|
||||
.popover-header {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// increase toast message z-index (above all modals)
|
||||
div.v-toast {
|
||||
z-index: 10000!important;
|
||||
|
@ -18,6 +18,7 @@ $chill-theme-buttons: (
|
||||
"show": $chill-blue,
|
||||
"view": $chill-blue,
|
||||
"misc": $gray-300,
|
||||
"download": $gray-300,
|
||||
"cancel": $gray-300,
|
||||
"choose": $gray-300,
|
||||
"notify": $gray-300,
|
||||
@ -78,6 +79,7 @@ $chill-theme-buttons: (
|
||||
&.btn-choose::before,
|
||||
&.btn-notify::before,
|
||||
&.btn-tpchild::before,
|
||||
&.btn-download::before,
|
||||
&.btn-cancel::before {
|
||||
font: normal normal normal 14px/1 ForkAwesome;
|
||||
margin-right: 0.5em;
|
||||
@ -105,6 +107,7 @@ $chill-theme-buttons: (
|
||||
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
|
||||
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
|
||||
&.btn-tpchild::before { content: "\f007"; } // fa-user
|
||||
&.btn-download::before { content: "\f019"; } // fa-download
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,6 +41,15 @@ div.flex-table {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
div.item-meta {
|
||||
flex-grow: 1 !important;
|
||||
flex-shrink: 1 !important;
|
||||
width: unset !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -68,6 +68,7 @@ div.notification-show {
|
||||
}
|
||||
|
||||
// Override bootstrap accordion
|
||||
div#workflow-fold,
|
||||
div#notification-fold {
|
||||
.accordion-button {
|
||||
padding: 0;
|
||||
@ -78,3 +79,14 @@ div#notification-fold {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Counter
|
||||
div.notification-counter {
|
||||
span {
|
||||
&:not(:first-child) {
|
||||
&::before {
|
||||
content: '/ ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,8 @@ section.chill-entity {
|
||||
// used for comment-embeddable
|
||||
&.entity-comment-embeddable {
|
||||
width: 100%;
|
||||
|
||||
/* already defined !!
|
||||
div.metadata {
|
||||
font-size: smaller;
|
||||
color: $gray-600;
|
||||
@ -116,5 +118,6 @@ section.chill-entity {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
const buildLinkCreate = function(workflowName, relatedEntityClass, relatedEntityId) {
|
||||
let params = new URLSearchParams();
|
||||
params.set('entityClass', relatedEntityClass);
|
||||
params.set('entityId', relatedEntityId);
|
||||
params.set('workflow', workflowName);
|
||||
|
||||
return `/fr/main/workflow/create?`+params.toString();
|
||||
};
|
||||
|
||||
export {
|
||||
buildLinkCreate,
|
||||
};
|
@ -9,9 +9,10 @@ import Dropdown from 'bootstrap/js/src/dropdown';
|
||||
import Modal from 'bootstrap/js/dist/modal';
|
||||
import Collapse from 'bootstrap/js/src/collapse';
|
||||
import Carousel from 'bootstrap/js/src/carousel';
|
||||
import Popover from 'bootstrap/js/src/popover';
|
||||
|
||||
//
|
||||
// ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
|
||||
// Carousel: ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
|
||||
// Initialize options, and show/hide controls in first/last slides
|
||||
//
|
||||
let ACHeaderSlider = document.querySelector('#ACHeaderSlider');
|
||||
@ -48,3 +49,14 @@ if (ACHeaderSlider) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Popover: used in workflow breadcrumb,
|
||||
// (expected in: contextual help, notification-box, workflow-box )
|
||||
//
|
||||
const triggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
const popoverList = triggerList.map(function (el) {
|
||||
return new Popover(el, {
|
||||
html: true,
|
||||
});
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
import { createApp } from "vue";
|
||||
import PickWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
|
||||
import ListWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflow.vue';
|
||||
|
||||
// pick workflow
|
||||
document.querySelectorAll('[data-pick-workflow]')
|
||||
.forEach(function(el) {
|
||||
const app = {
|
||||
components: {
|
||||
PickWorkflowVue
|
||||
},
|
||||
template:
|
||||
'<pick-workflow-vue ' +
|
||||
':relatedEntityClass="relatedEntityClass" ' +
|
||||
':relatedEntityId="relatedEntityId" ' +
|
||||
':workflowsAvailables="workflowsAvailables" ' +
|
||||
'></pick-workflow-vue>',
|
||||
data() {
|
||||
return {
|
||||
relatedEntityClass: el.dataset.relatedEntityClass,
|
||||
relatedEntityId: Number.parseInt(el.dataset.relatedEntityId),
|
||||
workflowsAvailables: JSON.parse(el.dataset.workflowsAvailables),
|
||||
}
|
||||
}
|
||||
};
|
||||
createApp(app).mount(el);
|
||||
})
|
||||
;
|
||||
|
||||
// list workflow
|
||||
document.querySelectorAll('[data-list-workflows]')
|
||||
.forEach(function (el) {
|
||||
const app = {
|
||||
components: {
|
||||
ListWorkflowVue,
|
||||
},
|
||||
template:
|
||||
'<list-workflow-vue ' +
|
||||
':workflows="workflows" ' +
|
||||
'></list-workflow-vue>',
|
||||
data() {
|
||||
return {
|
||||
workflows: JSON.parse(el.dataset.workflows),
|
||||
}
|
||||
}
|
||||
};
|
||||
createApp(app).mount(el);
|
||||
})
|
||||
;
|
@ -0,0 +1,32 @@
|
||||
import {createApp} from "vue";
|
||||
import EntityWorkflowVueSubscriber from 'ChillMainAssets/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue';
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
||||
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
let containers = document.querySelectorAll('[data-entity-workflow-subscribe]');
|
||||
|
||||
containers.forEach(container => {
|
||||
let app = {
|
||||
components: {
|
||||
EntityWorkflowVueSubscriber,
|
||||
},
|
||||
template: '<entity-workflow-vue-subscriber :entityWorkflowId="this.entityWorkflowId" :subscriberStep="this.subscriberStep" :subscriberFinal="this.subscriberFinal" @subscriptionUpdated="onUpdate"></entity-workflow-vue-subscriber>',
|
||||
data() {
|
||||
return {
|
||||
entityWorkflowId: Number.parseInt(container.dataset.entityWorkflowId),
|
||||
subscriberStep: container.dataset.subscribeStep === "1",
|
||||
subscriberFinal: container.dataset.subscribeFinal === "1",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdate(status) {
|
||||
this.subscriberStep = status.step;
|
||||
this.subscriberFinal = status.final;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createApp(app).use(i18n).mount(container);
|
||||
})
|
@ -31,7 +31,7 @@ window.addEventListener('DOMContentLoaded', function(e) {
|
||||
return {
|
||||
multiple: isMultiple,
|
||||
types: JSON.parse(el.dataset.types),
|
||||
picked,
|
||||
picked: picked === null ? [] : picked,
|
||||
uniqid: el.dataset.uniqid,
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { createApp } from 'vue';
|
||||
import OpenWopiLink from 'ChillMainAssets/vuejs/_components/OpenWopiLink';
|
||||
import {_createI18n} from "ChillMainAssets/vuejs/_js/i18n";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll('span[data-module="wopi-link"]')
|
||||
.forEach(function (el) {
|
||||
createApp({
|
||||
template: '<open-wopi-link :wopiUrl="wopiUrl" :title="title" :type="type" :button="button"></open-wopi-link>',
|
||||
components: {
|
||||
OpenWopiLink
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wopiUrl: el.dataset.wopiUrl,
|
||||
title: el.dataset.docTitle,
|
||||
type: el.dataset.docType,
|
||||
button: el.dataset.button ? JSON.parse(el.dataset.button) : {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el)
|
||||
;
|
||||
})
|
||||
;
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
let
|
||||
finalizeAfterContainer = document.querySelector('#finalizeAfter'),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
})
|
||||
});
|
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="d-grid gap-2 my-3">
|
||||
<button class="btn btn-misc" type="button" v-if="!subscriberFinal" @click="subscribeTo('subscribe', 'final')">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_final') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="subscriberFinal" @click="subscribeTo('unsubscribe', 'final')">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
{{ $t('unsubscribe_final') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="!subscriberStep" @click="subscribeTo('subscribe', 'step')">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="subscriberStep" @click="subscribeTo('unsubscribe', 'step')">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
{{ $t('unsubscribe_all_steps') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||
|
||||
export default {
|
||||
name: "EntityWorkflowVueSubscriber",
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
subscribe_final: "Recevoir une notification à l'étape finale",
|
||||
unsubscribe_final: "Ne plus recevoir de notification à l'étape finale",
|
||||
subscribe_all_steps: "Recevoir une notification à chaque étape du suivi",
|
||||
unsubscribe_all_steps: "Ne plus recevoir de notification à chaque étape du suivi",
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
entityWorkflowId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
subscriberStep: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
subscriberFinal: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['subscriptionUpdated'],
|
||||
methods: {
|
||||
subscribeTo(step, to) {
|
||||
let params = new URLSearchParams();
|
||||
params.set('subscribe', to);
|
||||
|
||||
const url = `/api/1.0/main/workflow/${this.entityWorkflowId}/${step}?` + params.toString();
|
||||
|
||||
makeFetch('POST', url).then(response => {
|
||||
this.$emit('subscriptionUpdated', response);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
/*
|
||||
* ALTERNATIVES
|
||||
*
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="laststep">
|
||||
<label class="form-check-label" for="laststep">{{ $t('subscribe_final') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="allsteps">
|
||||
<label class="form-check-label" for="allsteps">{{ $t('subscribe_all_steps') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="list-group my-3">
|
||||
<label class="list-group-item">
|
||||
<input class="form-check-input me-1" type="checkbox" value="">
|
||||
{{ $t('subscribe_final') }}
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<input class="form-check-input me-1" type="checkbox" value="">
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-vertical my-3" role="group">
|
||||
<button type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_final') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</button>
|
||||
</div>
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="list-group my-2 workflow workflow-box">
|
||||
<div class="list-group-item">
|
||||
<h4>Workflow associés</h4>
|
||||
</div>
|
||||
<div class="list-group-item" v-for="w in workflows">
|
||||
{{ w.id }}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
title="voir"
|
||||
:href="goToUrl(w)">
|
||||
<i class="fa fa-eye fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ListWorkflow",
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToUrl(w) {
|
||||
return `/fr/main/workflow/${w.id}/show`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<template v-if="workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in workflowsAvailables" :key="w.name">
|
||||
<a class="dropdown-item" :href="makeLink(w.name)" @click="goToGenerateWorkflow($event, w.name)">{{ w.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
|
||||
export default {
|
||||
name: "PickWorkflow",
|
||||
props: {
|
||||
relatedEntityClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntityId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
workflowsAvailables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: ['goToGenerateWorkflow'],
|
||||
methods: {
|
||||
makeLink(workflowName) {
|
||||
return buildLinkCreate(workflowName, this.relatedEntityClass, this.relatedEntityId);
|
||||
},
|
||||
goToGenerateWorkflow(event, workflowName) {
|
||||
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName)});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -15,7 +15,7 @@
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer" v-if="!hideFooter">
|
||||
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
@ -38,7 +38,16 @@
|
||||
*/
|
||||
export default {
|
||||
name: 'Modal',
|
||||
props: ['modalDialogClass'],
|
||||
props: {
|
||||
modalDialogClass: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
emits: ['close']
|
||||
}
|
||||
</script>
|
||||
|
@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<a v-if="isOpenDocument"
|
||||
class="btn change-icon" :class="[isChangeClass ? button.changeClass : 'btn-edit']"
|
||||
@click="openModal">
|
||||
|
||||
<i class="fa me-2" :class="[isChangeIcon ? button.changeIcon : 'fa-pencil']"></i>
|
||||
|
||||
<span v-if="!noText">
|
||||
{{ $t('Update_document') }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<teleport to="body">
|
||||
<div class="wopi-frame" v-if="isOpenDocument">
|
||||
<modal v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
:hideFooter=true
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<img class="logo" :src="logo" height="45"/>
|
||||
<span class="ms-auto me-3">
|
||||
{{ this.title }}
|
||||
</span>
|
||||
<a class="btn btn-outline-light">
|
||||
<i class="fa fa-save fa-fw"></i>
|
||||
{{ $t('save_and_quit') }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x" :title="$t('loading')"></i>
|
||||
</div>
|
||||
<iframe
|
||||
:src="this.wopiUrl"
|
||||
@load="loaded"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</div>
|
||||
<div v-else>
|
||||
<modal v-if="modal.showModal"
|
||||
modalDialogClass="modal-sm"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<h3>{{ $t('invalid_title') }}</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div class="alert alert-warning">{{ $t('invalid_message') }}</div>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
|
||||
import logo from 'ChillMainAssets/chill/img/logo-chill-sans-slogan_white.png';
|
||||
|
||||
export default {
|
||||
name: "OpenWopiLink",
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
wopiUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
button: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-fullscreen" //modal-dialog-scrollable
|
||||
},
|
||||
logo: logo,
|
||||
loading: false,
|
||||
mime: [
|
||||
// TODO temporary hardcoded. to be replaced by twig extension or a collabora server query
|
||||
'application/clarisworks',
|
||||
'application/coreldraw',
|
||||
'application/macwriteii',
|
||||
'application/msword',
|
||||
'application/pdf',
|
||||
'application/vnd.lotus-1-2-3',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||
'application/vnd.ms-visio.drawing',
|
||||
'application/vnd.ms-word.document.macroEnabled.12',
|
||||
'application/vnd.ms-word.template.macroEnabled.12',
|
||||
'application/vnd.ms-works',
|
||||
'application/vnd.oasis.opendocument.chart',
|
||||
'application/vnd.oasis.opendocument.formula',
|
||||
'application/vnd.oasis.opendocument.graphics',
|
||||
'application/vnd.oasis.opendocument.graphics-flat-xml',
|
||||
'application/vnd.oasis.opendocument.graphics-template',
|
||||
'application/vnd.oasis.opendocument.presentation',
|
||||
'application/vnd.oasis.opendocument.presentation-flat-xml',
|
||||
'application/vnd.oasis.opendocument.presentation-template',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||
'application/vnd.oasis.opendocument.text',
|
||||
'application/vnd.oasis.opendocument.text-flat-xml',
|
||||
'application/vnd.oasis.opendocument.text-master',
|
||||
'application/vnd.oasis.opendocument.text-master-template',
|
||||
'application/vnd.oasis.opendocument.text-template',
|
||||
'application/vnd.oasis.opendocument.text-web',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'application/vnd.sun.xml.calc',
|
||||
'application/vnd.sun.xml.calc.template',
|
||||
'application/vnd.sun.xml.chart',
|
||||
'application/vnd.sun.xml.draw',
|
||||
'application/vnd.sun.xml.draw.template',
|
||||
'application/vnd.sun.xml.impress',
|
||||
'application/vnd.sun.xml.impress.template',
|
||||
'application/vnd.sun.xml.math',
|
||||
'application/vnd.sun.xml.writer',
|
||||
'application/vnd.sun.xml.writer.global',
|
||||
'application/vnd.sun.xml.writer.template',
|
||||
'application/vnd.visio',
|
||||
'application/vnd.visio2013',
|
||||
'application/vnd.wordperfect',
|
||||
'application/x-abiword',
|
||||
'application/x-aportisdoc',
|
||||
'application/x-dbase',
|
||||
'application/x-dif-document',
|
||||
'application/x-fictionbook+xml',
|
||||
'application/x-gnumeric',
|
||||
'application/x-hwp',
|
||||
'application/x-iwork-keynote-sffkey',
|
||||
'application/x-iwork-numbers-sffnumbers',
|
||||
'application/x-iwork-pages-sffpages',
|
||||
'application/x-mspublisher',
|
||||
'application/x-mswrite',
|
||||
'application/x-pagemaker',
|
||||
'application/x-sony-bbeb',
|
||||
'application/x-t602',
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isOpenDocument() {
|
||||
if (this.mime.indexOf(this.type) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
noText() {
|
||||
if (typeof this.button.noText !== 'undefined') {
|
||||
return this.button.noText === true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isChangeIcon() {
|
||||
if (typeof this.button.changeIcon !== 'undefined') {
|
||||
return (!(this.button.changeIcon === null || this.button.changeIcon === ''))
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isChangeClass() {
|
||||
if (typeof this.button.changeClass !== 'undefined') {
|
||||
return (!(this.button.changeClass === null || this.button.changeClass === ''))
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.loading = true;
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
loaded() {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
Update_document: "Modifier le document",
|
||||
save_and_quit: "Enregistrer et quitter",
|
||||
loading: "Chargement de l'éditeur en ligne",
|
||||
invalid_title: "Format incompatible",
|
||||
invalid_message: "Désolé, ce format de document n'est pas éditable en ligne.",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div.wopi-frame {
|
||||
div.modal-header {
|
||||
border-bottom: 0;
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
}
|
||||
div.modal-body {
|
||||
padding: 0;
|
||||
overflow-y: unset !important;
|
||||
iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
div.loading {
|
||||
position: absolute;
|
||||
color: var(--bs-chill-gray);
|
||||
top: calc(50% - 30px);
|
||||
left: calc(50% - 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block navigation_search_bar %}{% endblock %}
|
||||
{% block navigation_section_menu %}
|
||||
{{ chill_menu('admin_section', {
|
||||
'layout': '@ChillMain/Menu/adminSection.html.twig',
|
||||
'layout': '@ChillMain/Menu/admin.html.twig',
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -23,22 +23,22 @@
|
||||
|
||||
{% if options['metadata'] %}
|
||||
<div class="metadata">
|
||||
{% if user is not empty %}
|
||||
{{ 'Last updated by'| trans }}
|
||||
<span class="user">
|
||||
{{ user|chill_entity_render_box(options['user']) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if comment.date is not empty %}
|
||||
{% if user is empty %}
|
||||
{{ 'Last updated on'|trans ~ ' ' }}
|
||||
{% else %}
|
||||
{{ 'on'|trans ~ ' ' }}
|
||||
{% endif %}
|
||||
{{ 'Last updated on'|trans ~ ' ' }}
|
||||
<span class="date">
|
||||
{{ comment.date|format_datetime("medium", "short") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if user is not empty %}
|
||||
{% if comment.date is empty %}
|
||||
{{ 'Last updated by'| trans }}
|
||||
{% else %}
|
||||
{{ 'by_user'|trans ~ ' ' }}
|
||||
{% endif %}
|
||||
<span class="user">
|
||||
{{ user|chill_entity_render_box(options['user']) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</blockquote>
|
||||
|
@ -21,13 +21,13 @@
|
||||
layout ../layoutWithVerticalMenu.html.twig.
|
||||
#}
|
||||
|
||||
<ul class="tab-nav follow-href-path">
|
||||
<li class="title">
|
||||
<div class="list-group vertical-menu {{ 'menu-' ~ menus.name }}">
|
||||
<a class="list-group-item title">
|
||||
{% block v_menu_title %}<!-- title of the verticalMenu is empty -->{% endblock %}
|
||||
</li>
|
||||
</a>
|
||||
{% for menu in menus %}
|
||||
<li class="{% if menu is knp_menu_current %}current {% endif %}">
|
||||
<a href="{{ menu.uri }}" >{{ menu.label|trans }}</a>
|
||||
</li>
|
||||
<a class="list-group-item list-group-item-action" href="{{ menu.uri }}">
|
||||
{{ menu.label|upper }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="item-row title">
|
||||
<h2 class="notification-title">
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">
|
||||
{{ 'notification.object_prefix'|trans ~ c.notification.title }}
|
||||
{{ c.notification.title }}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
@ -58,14 +58,15 @@
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="notification-content">
|
||||
{% if c.full_content is defined and c.full_content == 'true' %}
|
||||
{% if c.full_content is defined and c.full_content == true %}
|
||||
{{ c.notification.message|chill_markdown_to_html }}
|
||||
{% else %}
|
||||
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
||||
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if c.action_button is not defined or c.action_button != 'false' %}
|
||||
{% if c.action_button is not defined or c.action_button != false %}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
@ -85,7 +86,13 @@
|
||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"
|
||||
class="btn btn-show change-icon" title="{{ 'notification.see_comments_thread'|trans }}"><i class="fa fa-comment"></i></a>
|
||||
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}">
|
||||
{% if not c.notification.isSystem() %}
|
||||
<i class="fa fa-comment"></i>
|
||||
{% else %}
|
||||
{{ 'Read more'|trans }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -95,7 +102,7 @@
|
||||
|
||||
<div class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
||||
|
||||
{% if fold_item is defined and fold_item != 'false' %}
|
||||
{% if fold_item is defined and fold_item != false %}
|
||||
<div class="accordion-header" id="flush-heading-{{ notification.id }}">
|
||||
<button type="button" class="accordion-button collapsed"
|
||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
||||
|
@ -21,8 +21,6 @@
|
||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||
<div class="col-12">
|
||||
@ -30,6 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="notification-counter">
|
||||
{% if counter.total > 0 %}
|
||||
<span>
|
||||
{{ 'notification.counter total notifications'|trans({'total': counter.total }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if counter.unread > 0 %}
|
||||
<span>
|
||||
{{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
@ -18,7 +18,7 @@
|
||||
data-notification-id="{{ notification.id }}"
|
||||
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||
data-container="notification-status"
|
||||
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}, false) }}"
|
||||
data-button-class="btn-outline-primary"
|
||||
data-button-text="false"
|
||||
></span>
|
||||
|
@ -50,7 +50,7 @@
|
||||
{% for data in datas %}
|
||||
{% set notification = data.notification %}
|
||||
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
|
||||
'fold_item': 'true'
|
||||
'fold_item': true
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -40,8 +40,8 @@
|
||||
'template': handler.getTemplate(notification),
|
||||
'template_data': handler.getTemplateData(notification)
|
||||
},
|
||||
'action_button': 'false',
|
||||
'full_content': 'true'
|
||||
'action_button': false,
|
||||
'full_content': true
|
||||
} %}
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,120 @@
|
||||
{# TODO Adapt condition #}
|
||||
{% if random(1) == 0 %}
|
||||
|
||||
{# For a document #}
|
||||
<h2>{{ 'Document'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-4x fa-file-text-o text-success"></i>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h3>Imprimé unique, parcours n°14635</h3>
|
||||
<small>Document PDF (6.2 Mo)</small>
|
||||
<p class="mt-2">
|
||||
Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{# For an action #}
|
||||
<h2>{{ 'Accompanying Course Action'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="flex-table accompanying_course_work-list">
|
||||
{# dynamic insertion
|
||||
::: TODO delete all static insertion, remove condition and pass work object in inclusion
|
||||
#}{% if dynamic is defined %}
|
||||
|
||||
{% set work = '<pass work object here>' %}
|
||||
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{# BEGIN static insertion #}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">Exercer un AEB > Conclure l'AEB
|
||||
<ul class="small_in_title columns mt-1">
|
||||
<li><span class="item-key">Date de début : </span><b>25/11/2021</b></li>
|
||||
<li><span class="item-key">Date de fin : </span><b>10/03/2022</b></li>
|
||||
</ul>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Référent</h3></div>
|
||||
<div class="wl-col list"><p class="wl-item">Fred</p></div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Usagers du parcours</h3></div>
|
||||
<div class="wl-col list"><span class="wl-item">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="1937" data-action="show" data-button-text="Vernon SUBUTEX" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Vernon SUBUTEX</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
<span class="wl-item"><span class="onthefly-container" data-target-name="person" data-target-id="1941" data-action="show" data-button-text="Juan RAMON" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Juan RAMON</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Problématique sociale</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item social-issues">
|
||||
<span class="chill-entity entity-social-issue"><span class="badge bg-chill-l-gray text-dark"><span class="parent-0">AD - PREVENTION, ACCES AUX DROITS, BUDGET ></span><span class="child">SOUTIEN EQUILIBRE BUDGET</span></span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row column">
|
||||
<table class="obj-res-eval my-3">
|
||||
<thead>
|
||||
<tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
|
||||
<th class="res"><h4 class="title_label">Résultats - orientations</h4></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="obj">
|
||||
<p class="chill-no-data-statement">Aucun objectif - motif - dispositif</p>
|
||||
</td>
|
||||
<td class="res">
|
||||
<ul class="result_list">
|
||||
<li>Résultat : Arrêt à l'initiative du ménage pour déménagement</li>
|
||||
<li>Orientation vers une MASP</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="updatedBy">
|
||||
Dernière mise à jour par
|
||||
<b><span class="chill-entity entity-user">Fred<span class="user-job">(Responsable tous les territoires)</span><span class="main-scope">(ASE)</span></span></b>,<br>
|
||||
le 3 décembre 2021 à 15:19
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# END static insertion #}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="button" class="btn btn-misc">
|
||||
<i class="fa fa-download fa-fw"></i>{{ 'Download'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
{% set x = random(1) %}
|
||||
<button class="btn btn-update change-icon {% if x == 1 %}disabled{% endif %}">
|
||||
<i class="fa fa-fw fa-{% if x == 0 %}un{% endif %}lock"></i>
|
||||
{{ 'Edit'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
@ -0,0 +1,13 @@
|
||||
<h2>{{ 'Join a comment'|trans }}</h2>
|
||||
|
||||
{{ form_start(comment_form) }}
|
||||
|
||||
{{ form_widget(comment_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(comment_form) }}
|
@ -0,0 +1,48 @@
|
||||
<h2>{{ 'Decision'|trans }}</h2>
|
||||
|
||||
{% if transition_form is not null %}
|
||||
{{ form_start(transition_form) }}
|
||||
|
||||
{{ 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 %}
|
||||
|
||||
<div id="futureDestUsers">
|
||||
{{ form_row(transition_form.future_dest_users) }}
|
||||
</div>
|
||||
|
||||
<p>{{ form_label(transition_form.comment) }}</p>
|
||||
|
||||
{{ form_widget(transition_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(transition_form) }}
|
||||
{% else %}
|
||||
<div class="alert alert-chill-yellow">
|
||||
|
||||
{% if entity_workflow.currentStep.isFinalizeAfter %}
|
||||
<p>{{ 'workflow.This workflow is finalized'|trans }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>
|
||||
<p>{{ 'workflow.Only those users are allowed'|trans }}:</p>
|
||||
|
||||
<ul>
|
||||
{% for u in entity_workflow.currentStep.destUser -%}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
@ -0,0 +1,13 @@
|
||||
{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %}
|
||||
{# vue component #}
|
||||
<div data-pick-workflow="1"
|
||||
data-related-entity-class="{{ blank_workflow.relatedEntityClass }}"
|
||||
data-related-entity-id="{{ blank_workflow.relatedEntityId }}"
|
||||
data-workflows-availables="{{ workflows_availables|json_encode()|e('html_attr') }}"
|
||||
></div>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflows|length > 0 %}
|
||||
{# vue component #}
|
||||
<div data-list-workflows="1" data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"></div>
|
||||
{% endif %}
|
@ -0,0 +1,8 @@
|
||||
<h2>{{ 'Follow workflow'|trans }}</h2>
|
||||
|
||||
{# vue component #}
|
||||
<div data-entity-workflow-subscribe="1"
|
||||
data-entity-workflow-id="{{ entity_workflow.id }}"
|
||||
data-subscribe-step="{{ entity_workflow.isUserSubscribedToStep(app.user)|e('html_attr') }}"
|
||||
data-subscribe-final="{{ entity_workflow.isUserSubscribedToFinal(app.user)|e('html_attr') }}"
|
||||
></div>
|
@ -0,0 +1,59 @@
|
||||
<h2>{{ 'Workflow history'|trans }}</h2>
|
||||
|
||||
<div class="flex-table">
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
<div class="item-bloc {{ 'bloc' ~ step.id }} {% if loop.first %}initial{% endif %}">
|
||||
<div class="item-row">
|
||||
{% if loop.first and step.next is null %}
|
||||
<div class="item-col">
|
||||
{{ 'workflow.No transitions'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="decided">
|
||||
{% if not loop.first %}
|
||||
<i class="fa fa-check fa-fw text-success"></i>
|
||||
{% endif %}
|
||||
{{ step.currentStep }}
|
||||
</div>
|
||||
{#
|
||||
<div class="decided">
|
||||
<i class="fa fa-times fa-fw text-danger"></i>
|
||||
Refusé
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
{% if step.next is not null %}
|
||||
<div class="item-row separator">
|
||||
<div class="item-col" style="width: inherit;">
|
||||
{% if step.transitionBy is not null %}
|
||||
<div>
|
||||
{{ step.transitionBy|chill_entity_render_box }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<span>{{ step.transitionAt|format_datetime('long', 'medium') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="to-decision">
|
||||
<i class="fa fa-share fa-fw text-secondary" title="transféré"></i>
|
||||
{{ step.next.currentStep }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if step.comment is not empty %}
|
||||
<div class="item-row separator">
|
||||
<blockquote class="chill-user-quote col">
|
||||
{{ step.comment|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
@ -0,0 +1,12 @@
|
||||
<div class="item-row col">
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
{% include handler.templateTitle(l.entity_workflow) with handler.templateTitleData(entity_workflow)|merge({
|
||||
'description': true,
|
||||
'breadcrumb': true,
|
||||
'add_classes': 'ms-3 h3'
|
||||
}) %}
|
||||
|
||||
|
||||
</div>
|
@ -0,0 +1,56 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'Workflow'|trans }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
|
||||
{{ encore_entry_script_tags('page_workflow_show') }}
|
||||
{{ encore_entry_script_tags('mod_wopi_link') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_subscribe') }}
|
||||
{{ encore_entry_link_tags('page_workflow_show') }}
|
||||
{{ encore_entry_link_tags('mod_wopi_link') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10 workflow">
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
{# handler_template:
|
||||
- src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation.html.twig
|
||||
- src/Bundle/ChillPersonBundle/Resources/views/Workflow/_accompanying_period_work.html.twig
|
||||
- src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig
|
||||
#}
|
||||
<section class="step my-4">
|
||||
<div class="mb-5">
|
||||
{% include handler_template_title with handler_template_data|merge({'breadcrumb': true }) %}
|
||||
</div>
|
||||
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
|
||||
</section>
|
||||
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.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>
|
||||
|
||||
{# useful ?
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ path('chill_main_workflow_list_dest') }}">
|
||||
{{ 'Back to the list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
#}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,97 @@
|
||||
{% extends 'ChillMainBundle::layout.html.twig' %}
|
||||
|
||||
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'workflow.My workflows'|trans }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10 workflow">
|
||||
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
<ul class="nav nav-pills justify-content-center">
|
||||
<li class="nav-item">
|
||||
<a href="{{ path('chill_main_workflow_list_subscribed') }}"
|
||||
class="nav-link {% if step == 'subscribed' %}active{% endif %}">
|
||||
{{ 'workflow.subscribed'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ path('chill_main_workflow_list_dest') }}"
|
||||
class="nav-link {% if step == 'dest' %}active{% endif %}">
|
||||
{{ 'workflow.dest'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if workflows|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'workflow.No workflow'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table accordion accordion-flush" id="workflow-fold">
|
||||
{% for l in workflows %}
|
||||
<div class="item-bloc">
|
||||
<div class="accordion-header" id="flush-heading-{{ l.entity_workflow.id }}">
|
||||
<button type="button" class="accordion-button collapsed"
|
||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ l.entity_workflow.id }}"
|
||||
aria-expanded="false" aria-controls="flush-collapse-{{ l.entity_workflow.id }}">
|
||||
|
||||
<div class="item-row col">
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
{% include l.handler.templateTitle(l.entity_workflow) with l.handler.templateTitleData(l.entity_workflow)|merge({
|
||||
'description': true,
|
||||
'add_classes': 'ms-3 h3'
|
||||
}) %}
|
||||
</div>
|
||||
|
||||
</button>
|
||||
{{ macro.breadcrumb(l) }}
|
||||
|
||||
</div>
|
||||
<div id="flush-collapse-{{ l.entity_workflow.id }}"
|
||||
class="accordion-collapse collapse"
|
||||
aria-labelledby="flush-heading-{{ l.entity_workflow.id }}"
|
||||
data-bs-parent="#workflow-fold">
|
||||
|
||||
<div class="item-row flex-column">
|
||||
{% include l.handler.template(l.entity_workflow) with l.handler.templateData(l.entity_workflow)|merge({
|
||||
'display_action': false
|
||||
}) %}
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col flex-grow-1">
|
||||
<p>
|
||||
{% if l.entity_workflow.isUserSubscribedToStep(app.user) %}
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ 'workflow.you subscribed to all steps'|trans }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if l.entity_workflow.isUserSubscribedToFinal(app.user) %}
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ 'workflow.you subscribed to final step'|trans }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ path('chill_main_workflow_show', {'id': l.entity_workflow.id}) }}"
|
||||
class="btn btn-show">
|
||||
{{ 'Show'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,45 @@
|
||||
{% 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>
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro breadcrumb(_ctx) %}
|
||||
<div class="breadcrumb">
|
||||
{% for step in _ctx.entity_workflow.stepsChained %}
|
||||
{% if step.previous is null %}
|
||||
{#
|
||||
{% set popContent = "Point de départ du workflow" %}
|
||||
{{ dump(step) }}
|
||||
#}
|
||||
{% set popContent = _self.popoverContent(step) %}
|
||||
{% else %}
|
||||
{% set popContent = _self.popoverContent(step.previous) %}
|
||||
{% endif %}
|
||||
<span class="mx-2"
|
||||
tabindex="0"
|
||||
data-bs-trigger="focus hover"
|
||||
data-bs-toggle="popover"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-custom-class="workflow-transition"
|
||||
title="{{ step.currentStep }}"
|
||||
data-bs-content="{{ popContent|e('html_attr') }}"
|
||||
>
|
||||
{% if step.currentStep == 'initial' %}
|
||||
<i class="fa fa-circle me-1 text-chill-yellow"></i>
|
||||
{% endif %}
|
||||
{{ step.currentStep }}
|
||||
</span>
|
||||
{% if not loop.last %}
|
||||
→
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
@ -0,0 +1,13 @@
|
||||
{{ dest.label }},
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}
|
||||
{%- if is_dest %}
|
||||
|
||||
Vous êtes invités à valider cette étape au plus tôt.
|
||||
{% endif %}
|
||||
|
||||
Vous pouvez visualiser le workflow sur cette page:
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id})) }}
|
||||
|
||||
Cordialement,
|
@ -0,0 +1,5 @@
|
||||
{%- if is_dest -%}
|
||||
Un suivi {{ workflow.text }} demande votre attention
|
||||
{%- else -%}
|
||||
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}
|
||||
{%- endif -%}
|
@ -69,6 +69,15 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
'counter' => $nbNotifications,
|
||||
]);
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
$this->translator->trans('workflow.My workflows'),
|
||||
['route' => 'chill_main_workflow_list_dest']
|
||||
)
|
||||
->setExtras([
|
||||
'order' => 700,
|
||||
]);
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
'Change password',
|
||||
|
@ -0,0 +1,70 @@
|
||||
<?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\MainBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use UnexpectedValueException;
|
||||
use function in_array;
|
||||
|
||||
class EntityWorkflowVoter extends Voter
|
||||
{
|
||||
public const CREATE = 'CHILL_MAIN_WORKFLOW_CREATE';
|
||||
|
||||
public const SEE = 'CHILL_MAIN_WORKFLOW_SEE';
|
||||
|
||||
private EntityWorkflowManager $manager;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityWorkflowManager $manager, Security $security)
|
||||
{
|
||||
$this->manager = $manager;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
return $subject instanceof EntityWorkflow && in_array($attribute, self::getRoles(), true);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
switch ($attribute) {
|
||||
case self::CREATE:
|
||||
case self::SEE:
|
||||
$handler = $this->manager->getHandler($subject);
|
||||
|
||||
$entityAttribute = $handler->getRoleShow($subject);
|
||||
|
||||
if (null === $entityAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject));
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException("attribute {$attribute} not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static function getRoles(): array
|
||||
{
|
||||
return [
|
||||
self::SEE,
|
||||
self::CREATE,
|
||||
];
|
||||
}
|
||||
}
|
50
src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php
Normal file
50
src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?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\MainBundle\Service\Mailer;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
class ChillMailer implements MailerInterface
|
||||
{
|
||||
private LoggerInterface $chillLogger;
|
||||
|
||||
private MailerInterface $initial;
|
||||
|
||||
private string $prefix = '[Chill] ';
|
||||
|
||||
public function __construct(MailerInterface $initial, LoggerInterface $chillLogger)
|
||||
{
|
||||
$this->initial = $initial;
|
||||
$this->chillLogger = $chillLogger;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
||||
{
|
||||
if ($message instanceof Email) {
|
||||
$message->subject($this->prefix . $message->getSubject());
|
||||
}
|
||||
|
||||
$this->chillLogger->info('chill email sent', [
|
||||
'to' => array_map(static function (Address $address) {
|
||||
return $address->getAddress();
|
||||
}, $message->getTo()),
|
||||
'subject' => $message->getSubject(),
|
||||
]);
|
||||
|
||||
$this->initial->send($message, $envelope);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?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\MainBundle\Tests\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class EntityWorkflowTest extends TestCase
|
||||
{
|
||||
public function testIsFinalizeWith1Steps()
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$entityWorkflow->getCurrentStep()->setFinalizeAfter(true);
|
||||
$entityWorkflow->setStep('final');
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFinalize());
|
||||
}
|
||||
|
||||
public function testIsFinalizeWith4Steps()
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinalize());
|
||||
|
||||
$entityWorkflow->setStep('two');
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinalize());
|
||||
|
||||
$entityWorkflow->setStep('previous_final');
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinalize());
|
||||
|
||||
$entityWorkflow->getCurrentStep()->setFinalizeAfter(true);
|
||||
$entityWorkflow->setStep('final');
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFinalize());
|
||||
}
|
||||
|
||||
public function testIsFreeze()
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('step_one');
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('step_three');
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('freezed');
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('after_freeze');
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFreeze());
|
||||
|
||||
$this->assertTrue($entityWorkflow->getCurrentStep()->isFreezeAfter());
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?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\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
|
||||
interface EntityWorkflowHandlerInterface
|
||||
{
|
||||
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
|
||||
|
||||
/**
|
||||
* Return a string representing the role required for seeing the workflow.
|
||||
*
|
||||
* Return Null if any check is required, or a role name. The voter will check for
|
||||
* authorization with the role as attribute, and the
|
||||
*/
|
||||
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string;
|
||||
|
||||
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string;
|
||||
|
||||
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array;
|
||||
|
||||
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
|
||||
|
||||
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array;
|
||||
|
||||
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||
|
||||
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?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\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class EntityWorkflowManager
|
||||
{
|
||||
/**
|
||||
* @var iterable|EntityWorkflowHandlerInterface[]
|
||||
*/
|
||||
private iterable $handlers;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
public function __construct(iterable $handlers, Registry $registry)
|
||||
{
|
||||
$this->handlers = $handlers;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function getHandler(EntityWorkflow $entityWorkflow, array $options = []): EntityWorkflowHandlerInterface
|
||||
{
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler->supports($entityWorkflow, $options)) {
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
||||
throw new HandlerNotFoundException();
|
||||
}
|
||||
|
||||
public function getSupportedWorkflows(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
return $this->registry->all($entityWorkflow);
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
<?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\MainBundle\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use DateTimeImmutable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Workflow\TransitionBlocker;
|
||||
|
||||
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private LoggerInterface $chillLogger;
|
||||
|
||||
private Security $security;
|
||||
|
||||
private UserRender $userRender;
|
||||
|
||||
public function __construct(
|
||||
LoggerInterface $chillLogger,
|
||||
Security $security,
|
||||
UserRender $userRender
|
||||
) {
|
||||
$this->chillLogger = $chillLogger;
|
||||
$this->security = $security;
|
||||
$this->userRender = $userRender;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'workflow.transition' => 'onTransition',
|
||||
'workflow.guard' => [
|
||||
['guardEntityWorkflow', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function guardEntityWorkflow(GuardEvent $event)
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
|
||||
if ($entityWorkflow->isFinalize()) {
|
||||
$event->addTransitionBlocker(
|
||||
new TransitionBlocker(
|
||||
'workflow.The workflow is finalized',
|
||||
'd6306280-7535-11ec-a40d-1f7bee26e2c0'
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entityWorkflow->getCurrentStep()->getDestUser()->contains($this->security->getUser())) {
|
||||
if (!$event->getMarking()->has('initial')) {
|
||||
$event->addTransitionBlocker(new TransitionBlocker(
|
||||
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
|
||||
'f3eeb57c-7532-11ec-9495-e7942a2ac7bc',
|
||||
[
|
||||
'%users%' => implode(
|
||||
', ',
|
||||
$entityWorkflow->getCurrentStep()->getDestUser()->map(function (User $u) {
|
||||
return $this->userRender->renderString($u, []);
|
||||
})->toArray()
|
||||
),
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onTransition(Event $event)
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
|
||||
$step
|
||||
->setTransitionAfter($event->getTransition()->getName())
|
||||
->setTransitionAt(new DateTimeImmutable('now'))
|
||||
->setTransitionBy($this->security->getUser());
|
||||
|
||||
$this->chillLogger->info('[workflow] apply transition on entityWorkflow', [
|
||||
'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(),
|
||||
'relatedEntityId' => $entityWorkflow->getRelatedEntityId(),
|
||||
'transition' => $event->getTransition()->getName(),
|
||||
'by_user' => $this->security->getUser(),
|
||||
'entityWorkflow' => $entityWorkflow->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?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\MainBundle\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use function in_array;
|
||||
|
||||
class NotificationOnTransition implements EventSubscriberInterface
|
||||
{
|
||||
private EngineInterface $engine;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private MetadataExtractor $metadataExtractor;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, EngineInterface $engine, MetadataExtractor $metadataExtractor, Security $security, Registry $registry)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->engine = $engine;
|
||||
$this->metadataExtractor = $metadataExtractor;
|
||||
$this->registry = $registry;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'workflow.completed' => 'onCompleted',
|
||||
];
|
||||
}
|
||||
|
||||
public function onCompleted(Event $event): void
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
|
||||
$dests = array_merge(
|
||||
$entityWorkflow->getSubscriberToStep()->toArray(),
|
||||
$entityWorkflow->isFinalize() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
|
||||
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
|
||||
);
|
||||
|
||||
$place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow);
|
||||
$workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow(
|
||||
$this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName())
|
||||
);
|
||||
|
||||
$visited = [];
|
||||
|
||||
foreach ($dests as $subscriber) {
|
||||
if (
|
||||
$this->security->getUser() === $subscriber
|
||||
|| in_array($subscriber->getId(), $visited, true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context = [
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'dest' => $subscriber,
|
||||
'place' => $place,
|
||||
'workflow' => $workflow,
|
||||
'is_dest' => $entityWorkflow->getCurrentStep()->getDestUser()->contains($subscriber),
|
||||
];
|
||||
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setRelatedEntityId($entityWorkflow->getId())
|
||||
->setRelatedEntityClass(EntityWorkflow::class)
|
||||
->setTitle($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context))
|
||||
->setMessage($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig', $context))
|
||||
->addAddressee($subscriber);
|
||||
$this->entityManager->persist($notification);
|
||||
|
||||
$visited[] = $subscriber->getId();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?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\MainBundle\Workflow\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class HandlerNotFoundException extends RuntimeException
|
||||
{
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?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\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
class MetadataExtractor
|
||||
{
|
||||
private Registry $registry;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(Registry $registry, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function availableWorkflowFor(string $relatedEntityClass, ?int $relatedEntityId = 0): array
|
||||
{
|
||||
$blankEntityWorkflow = new EntityWorkflow();
|
||||
$blankEntityWorkflow
|
||||
->setRelatedEntityId($relatedEntityId)
|
||||
->setRelatedEntityClass($relatedEntityClass);
|
||||
|
||||
// build the list of available workflows, and extract their names from metadata
|
||||
$workflows = $this->registry->all($blankEntityWorkflow);
|
||||
$workflowsList = [];
|
||||
|
||||
foreach ($workflows as $workflow) {
|
||||
$metadata = $workflow->getMetadataStore()->getWorkflowMetadata();
|
||||
$text = array_key_exists('label', $metadata) ?
|
||||
$this->translatableStringHelper->localize($metadata['label']) : $workflow->getName();
|
||||
|
||||
$workflowsList[] = ['name' => $workflow->getName(), 'text' => $text];
|
||||
}
|
||||
|
||||
return $workflowsList;
|
||||
}
|
||||
|
||||
public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
$markingMetadata = $workflow->getMetadataStore()->getPlaceMetadata($entityWorkflow->getCurrentStep()->getCurrentStep());
|
||||
|
||||
$text = array_key_exists('label', $markingMetadata) ?
|
||||
$this->translatableStringHelper->localize($markingMetadata['label']) : $entityWorkflow->getCurrentStep()->getCurrentStep();
|
||||
|
||||
return ['name' => $entityWorkflow->getCurrentStep()->getCurrentStep(), 'text' => $text];
|
||||
}
|
||||
|
||||
public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array
|
||||
{
|
||||
$metadata = $workflow->getMetadataStore()->getWorkflowMetadata();
|
||||
$text = array_key_exists('label', $metadata) ?
|
||||
$this->translatableStringHelper->localize($metadata['label']) : $workflow->getName();
|
||||
|
||||
return ['name' => $workflow->getName(), 'text' => $text];
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?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\MainBundle\Workflow\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
|
||||
class WorkflowNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private EntityWorkflowRepository $entityWorkflowRepository;
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return '@ChillMain/Workflow/_notification_include.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
$entityWorkflow = $this->entityWorkflowRepository->find($notification->getRelatedEntityId());
|
||||
|
||||
return [
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
|
||||
];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return $notification->getRelatedEntityClass() === EntityWorkflow::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?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\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
class RelatedEntityWorkflowSupportsStrategy implements WorkflowSupportStrategyInterface
|
||||
{
|
||||
public function supports(WorkflowInterface $workflow, $subject): bool
|
||||
{
|
||||
if (!$subject instanceof EntityWorkflow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($workflow->getMetadataStore()->getWorkflowMetadata()['related_entity']
|
||||
as $relatedEntityClass) {
|
||||
if ($subject->getRelatedEntityClass() === $relatedEntityClass) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?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\MainBundle\Workflow\Templating;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class WorkflowTwigExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction(
|
||||
'chill_entity_workflow_list',
|
||||
[WorkflowTwigExtensionRuntime::class, 'listWorkflows'],
|
||||
['needs_environment' => true, 'is_safe' => ['html']]
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?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\MainBundle\Workflow\Templating;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private MetadataExtractor $metadataExtractor;
|
||||
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
private EntityWorkflowRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
EntityWorkflowManager $entityWorkflowManager,
|
||||
Registry $registry,
|
||||
EntityWorkflowRepository $repository,
|
||||
MetadataExtractor $metadataExtractor,
|
||||
NormalizerInterface $normalizer
|
||||
) {
|
||||
$this->entityWorkflowManager = $entityWorkflowManager;
|
||||
$this->registry = $registry;
|
||||
$this->repository = $repository;
|
||||
$this->metadataExtractor = $metadataExtractor;
|
||||
$this->normalizer = $normalizer;
|
||||
}
|
||||
|
||||
public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
$blankEntityWorkflow = new EntityWorkflow();
|
||||
$blankEntityWorkflow
|
||||
->setRelatedEntityId($relatedEntityId)
|
||||
->setRelatedEntityClass($relatedEntityClass);
|
||||
|
||||
$workflowsList = $this->metadataExtractor->availableWorkflowFor($relatedEntityClass, $relatedEntityId);
|
||||
|
||||
// get the related entity already created
|
||||
$entityWorkflows = [];
|
||||
|
||||
foreach ($entityWorkflowsNaked = $this->repository->findBy(
|
||||
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]
|
||||
) as $entityWorkflow) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$entityWorkflows[] = [
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'workflow' => $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow),
|
||||
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
|
||||
];
|
||||
}
|
||||
|
||||
return $environment->render('@ChillMain/Workflow/_extension_list_workflow_for.html.twig', [
|
||||
'entity_workflows_json' => $this->normalizer->normalize($entityWorkflowsNaked, 'json', ['groups' => 'read']),
|
||||
'entity_workflows' => $entityWorkflows,
|
||||
'blank_workflow' => $blankEntityWorkflow,
|
||||
'workflows_availables' => $workflowsList,
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?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\MainBundle\Workflow\Validator;
|
||||
|
||||
/**
|
||||
* Validator which will test that:.
|
||||
*
|
||||
* * a handler exists;
|
||||
* * a related entity does exists;
|
||||
* * a workflow can be associated with this entity.
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class EntityWorkflowCreation extends \Symfony\Component\Validator\Constraint
|
||||
{
|
||||
public string $messageEntityNotFound = 'Related entity is not found';
|
||||
|
||||
public string $messageHandlerNotFound = 'Handler not found for this entity';
|
||||
|
||||
public string $messageWorkflowNotAvailable = 'Workflow is not valid';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [self::CLASS_CONSTRAINT];
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?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\MainBundle\Workflow\Validator;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
use function count;
|
||||
|
||||
class EntityWorkflowCreationValidator extends \Symfony\Component\Validator\ConstraintValidator
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
public function __construct(EntityWorkflowManager $entityWorkflowManager)
|
||||
{
|
||||
$this->entityWorkflowManager = $entityWorkflowManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EntityWorkflow $value
|
||||
* @param Constraint|EntityWorkflowCreation $constraint
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!$value instanceof EntityWorkflow) {
|
||||
throw new UnexpectedValueException($value, EntityWorkflow::class);
|
||||
}
|
||||
|
||||
if (!$constraint instanceof EntityWorkflowCreation) {
|
||||
throw new UnexpectedTypeException($constraint, EntityWorkflowCreation::class);
|
||||
}
|
||||
|
||||
try {
|
||||
$handler = $this->entityWorkflowManager->getHandler($value);
|
||||
} catch (HandlerNotFoundException $e) {
|
||||
$this->context->buildViolation($constraint->messageHandlerNotFound)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $handler->getRelatedEntity($value)) {
|
||||
$this->context->buildViolation($constraint->messageEntityNotFound)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
$workflows = $this->entityWorkflowManager->getSupportedWorkflows($value);
|
||||
|
||||
$matched = array_filter($workflows, static function (WorkflowInterface $workflow) use ($value) {
|
||||
return $workflow->getName() === $value->getWorkflowName();
|
||||
});
|
||||
|
||||
if (0 === count($matched)) {
|
||||
$this->context->buildViolation($constraint->messageWorkflowNotAvailable)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ module.exports = function(encore, entries)
|
||||
// Page entrypoints
|
||||
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
|
||||
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
|
||||
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
|
||||
|
||||
buildCKEditor(encore);
|
||||
|
||||
@ -64,6 +65,9 @@ module.exports = function(encore, entries)
|
||||
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
||||
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
|
||||
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
|
||||
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
|
||||
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
|
||||
encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js');
|
||||
|
||||
// Vue entrypoints
|
||||
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
|
||||
|
@ -26,6 +26,20 @@ services:
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
# workflow related
|
||||
Chill\MainBundle\Workflow\:
|
||||
resource: '../Workflow/'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Workflow\EntityWorkflowManager:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
arguments:
|
||||
$handlers: !tagged_iterator chill_main.workflow_handler
|
||||
|
||||
# other stuffes
|
||||
|
||||
chill.main.helper.translatable_string:
|
||||
class: Chill\MainBundle\Templating\TranslatableStringHelper
|
||||
|
||||
|
@ -141,3 +141,5 @@ services:
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Form\Type\LocationFormType: ~
|
||||
|
||||
Chill\MainBundle\Form\WorkflowStepType: ~
|
||||
|
10
src/Bundle/ChillMainBundle/config/services/mailer.yaml
Normal file
10
src/Bundle/ChillMainBundle/config/services/mailer.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Service\Mailer\:
|
||||
resource: '../../Service/Mailer'
|
||||
|
||||
Chill\MainBundle\Service\Mailer\ChillMailer:
|
||||
decorates: Symfony\Component\Mailer\MailerInterface
|
@ -26,6 +26,8 @@ services:
|
||||
|
||||
Chill\MainBundle\Security\Authorization\NotificationVoter: ~
|
||||
|
||||
Chill\MainBundle\Security\Authorization\EntityWorkflowVoter: ~
|
||||
|
||||
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
|
||||
|
||||
chill.main.security.authorization.helper:
|
||||
|
@ -0,0 +1,72 @@
|
||||
<?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 Version20220112123436 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final DROP CONSTRAINT FK_C2CE504C7D99CE94');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step DROP CONSTRAINT FK_ECB8F5417D99CE94');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP CONSTRAINT FK_440AA6FEFB054143');
|
||||
$this->addSql('ALTER TABLE chill_main_entity_workflow_step_user DROP CONSTRAINT FK_A9F001FA7E6AF9D4');
|
||||
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_id_seq CASCADE');
|
||||
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_subscriber_to_final');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_subscriber_to_step');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_step');
|
||||
$this->addSql('DROP TABLE chill_main_entity_workflow_step_user');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create tables for workflow';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity (id INT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, relatedEntityClass VARCHAR(255) NOT NULL, relatedEntityId INT NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, workflowName TEXT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_5F087D553174800F ON chill_main_workflow_entity (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_5F087D5565FF1AEC ON chill_main_workflow_entity (updatedBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_subscriber_to_final (entityworkflow_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflow_id, user_id))');
|
||||
$this->addSql('CREATE INDEX IDX_C2CE504C7D99CE94 ON chill_main_workflow_entity_subscriber_to_final (entityworkflow_id)');
|
||||
$this->addSql('CREATE INDEX IDX_C2CE504CA76ED395 ON chill_main_workflow_entity_subscriber_to_final (user_id)');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_subscriber_to_step (entityworkflow_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflow_id, user_id))');
|
||||
$this->addSql('CREATE INDEX IDX_ECB8F5417D99CE94 ON chill_main_workflow_entity_subscriber_to_step (entityworkflow_id)');
|
||||
$this->addSql('CREATE INDEX IDX_ECB8F541A76ED395 ON chill_main_workflow_entity_subscriber_to_step (user_id)');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_step (id INT NOT NULL, currentStep TEXT NOT NULL, destEmail JSON NOT NULL, finalizeAfter BOOLEAN DEFAULT \'false\' NOT NULL, freezeAfter BOOLEAN DEFAULT \'false\' NOT NULL, transitionAfter TEXT DEFAULT NULL, transitionByEmail TEXT DEFAULT NULL, transitionAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, entityWorkflow_id INT DEFAULT NULL, transitionBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_440AA6FEFB054143 ON chill_main_workflow_entity_step (entityWorkflow_id)');
|
||||
$this->addSql('CREATE INDEX IDX_440AA6FE8829EF37 ON chill_main_workflow_entity_step (transitionBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step.transitionAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('CREATE TABLE chill_main_entity_workflow_step_user (entityworkflowstep_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, user_id))');
|
||||
$this->addSql('CREATE INDEX IDX_A9F001FA7E6AF9D4 ON chill_main_entity_workflow_step_user (entityworkflowstep_id)');
|
||||
$this->addSql('CREATE INDEX IDX_A9F001FAA76ED395 ON chill_main_entity_workflow_step_user (user_id)');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity ADD CONSTRAINT FK_5F087D553174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity ADD CONSTRAINT FK_5F087D5565FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final ADD CONSTRAINT FK_C2CE504C7D99CE94 FOREIGN KEY (entityworkflow_id) REFERENCES chill_main_workflow_entity (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final ADD CONSTRAINT FK_C2CE504CA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step ADD CONSTRAINT FK_ECB8F5417D99CE94 FOREIGN KEY (entityworkflow_id) REFERENCES chill_main_workflow_entity (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step ADD CONSTRAINT FK_ECB8F541A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD CONSTRAINT FK_440AA6FEFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD CONSTRAINT FK_440AA6FE8829EF37 FOREIGN KEY (transitionBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_entity_workflow_step_user ADD CONSTRAINT FK_A9F001FA7E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_entity_workflow_step_user ADD CONSTRAINT FK_A9F001FAA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?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 Version20220114132105 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_comment_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_comment');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP comment');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_user RENAME TO chill_main_entity_workflow_step_user');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add comment to entity workflow';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_comment (id INT NOT NULL, comment TEXT NOT NULL DEFAULT \'\', createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, entityWorkflow_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_2655252F3174800F ON chill_main_workflow_entity_comment (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_2655252FFB054143 ON chill_main_workflow_entity_comment (entityWorkflow_id)');
|
||||
$this->addSql('CREATE INDEX IDX_2655252F65FF1AEC ON chill_main_workflow_entity_comment (updatedBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_comment.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_comment.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252F3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252FFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252F65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD comment TEXT NOT NULL DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_main_entity_workflow_step_user RENAME TO chill_main_workflow_entity_step_user');
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user