Merge branch 'signature-app-master' into 'master'

Signature app master

Closes #307

See merge request Chill-Projet/chill-bundles!743
This commit is contained in:
2024-11-12 20:30:00 +00:00
426 changed files with 22236 additions and 2253 deletions

View File

@@ -36,6 +36,7 @@ class ChillCollectionType extends AbstractType
$view->vars['identifier'] = $options['identifier'];
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
$view->vars['js_caller'] = $options['js_caller'];
$view->vars['uniqid'] = uniqid();
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Form\DataTransformerInterface;
@@ -26,10 +28,14 @@ class EntityToJsonTransformer implements DataTransformerInterface
public function reverseTransform($value)
{
if ('' === $value) {
if (false === $this->multiple && '' === $value) {
return null;
}
if ($this->multiple && [] === $value) {
return [];
}
$denormalized = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
if ($this->multiple) {
@@ -74,15 +80,23 @@ class EntityToJsonTransformer implements DataTransformerInterface
'user' => User::class,
'person' => Person::class,
'thirdparty' => ThirdParty::class,
'user_group' => UserGroup::class,
'user_group_or_user' => DiscriminatedObjectDenormalizer::TYPE,
default => throw new \UnexpectedValueException('This type is not supported'),
};
$context = [AbstractNormalizer::GROUPS => ['read']];
if ('user_group_or_user' === $this->type) {
$context[DiscriminatedObjectDenormalizer::ALLOWED_TYPES] = [UserGroup::class, User::class];
}
return
$this->denormalizer->denormalize(
['type' => $item['type'], 'id' => $item['id']],
$class,
'json',
[AbstractNormalizer::GROUPS => ['read']],
$context,
);
}
}

View File

@@ -18,16 +18,30 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Pick user dymically, using vuejs module "AddPerson".
*
* Possible options:
*
* - `multiple`: pick one or more users
* - `suggested`: a list of suggested users
* - `suggest_myself`: append the current user to the list of suggested
* - `as_id`: only the id will be set in the returned data
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
*/
class PickUserDynamicType extends AbstractType
{
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {}
public function __construct(
private readonly DenormalizerInterface $denormalizer,
private readonly SerializerInterface $serializer,
private readonly NormalizerInterface $normalizer,
private readonly Security $security,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -46,6 +60,12 @@ class PickUserDynamicType extends AbstractType
foreach ($options['suggested'] as $user) {
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
}
$user = $this->security->getUser();
if ($user instanceof User) {
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
}
}
}
public function configureOptions(OptionsResolver $resolver)
@@ -54,6 +74,8 @@ class PickUserDynamicType extends AbstractType
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false)
->setDefault('suggest_myself', false)
->setAllowedTypes('suggest_myself', ['bool'])
->setDefault('suggested', [])
// if set to true, only the id will be set inside the content. The denormalization will not work.
->setDefault('as_id', false)

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Entity which picks a user **or** a user group.
*/
final class PickUserGroupOrUserDynamicType extends AbstractType
{
public function __construct(
private readonly DenormalizerInterface $denormalizer,
private readonly SerializerInterface $serializer,
private readonly NormalizerInterface $normalizer,
private readonly Security $security,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group_or_user'));
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['user-group', 'user'];
$view->vars['uniqid'] = uniqid('pick_usergroup_dyn');
$view->vars['suggested'] = [];
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
foreach ($options['suggested'] as $userGroup) {
$view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']);
}
$user = $this->security->getUser();
if ($user instanceof User) {
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
}
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false)
->setDefault('suggested', [])
->setDefault('suggest_myself', false)
->setAllowedTypes('suggest_myself', ['bool'])
// if set to true, only the id will be set inside the content. The denormalization will not work.
->setDefault('as_id', false)
->setAllowedTypes('as_id', ['bool'])
->setDefault('submit_on_adding_new_entity', false)
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
}
public function getBlockPrefix()
{
return 'pick_entity_dynamic';
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class UserGroupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TranslatableStringFormType::class, [
'label' => 'user_group.Label',
'required' => true,
])
->add('active')
->add('backgroundColor', ColorType::class, [
'label' => 'user_group.BackgroundColor',
])
->add('foregroundColor', ColorType::class, [
'label' => 'user_group.ForegroundColor',
])
->add('email', EmailType::class, [
'label' => 'user_group.Email',
'help' => 'user_group.EmailHelp',
'empty_data' => '',
'required' => false,
])
->add('excludeKey', TextType::class, [
'label' => 'user_group.ExcludeKey',
'help' => 'user_group.ExcludeKeyHelp',
'required' => false,
'empty_data' => '',
])
->add('users', PickUserDynamicType::class, [
'label' => 'user_group.Users',
'multiple' => true,
'required' => false,
'empty_data' => [],
])
->add('adminUsers', PickUserDynamicType::class, [
'label' => 'user_group.adminUsers',
'multiple' => true,
'required' => false,
'empty_data' => [],
'help' => 'user_group.adminUsersHelp',
])
;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class WorkflowSignatureMetadataType extends AbstractType
{
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$documentTypeChoices = $this->parameterBag->get('chill_main')['workflow_signature']['base_signer']['document_kinds'];
$choices = [];
foreach ($documentTypeChoices as $documentType) {
$labels = [];
foreach ($documentType['labels'] as $label) {
$labels[$label['lang']] = $label['label'];
}
$localizedLabel = $this->translatableStringHelper->localize($labels);
if (null !== $localizedLabel) {
$choices[$localizedLabel] = $documentType['key'];
}
}
$builder
->add('documentType', ChoiceType::class, [
'label' => 'workflow.signature_zone.metadata.docType',
'expanded' => false,
'required' => true,
'choices' => $choices,
])
->add('documentNumber', TextType::class, [
'required' => true,
'label' => 'workflow.signature_zone.metadata.docNumber',
])
->add('expirationDate', ChillDateType::class, [
'required' => true,
'input' => 'datetime_immutable',
'label' => 'workflow.signature_zone.metadata.docExpiration',
]);
}
}

View File

@@ -12,232 +12,209 @@ 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\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
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\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
class WorkflowStepType extends AbstractType
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function __construct(
private readonly Registry $registry,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly EntityWorkflowManager $entityWorkflowManager,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $options['entity_workflow'];
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$place = $workflow->getMarking($entityWorkflow);
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
$suggestedUsers = $this->entityWorkflowManager->getSuggestedUsers($entityWorkflow);
$suggestedThirdParties = $this->entityWorkflowManager->getSuggestedThirdParties($entityWorkflow);
$suggestedPersons = $this->entityWorkflowManager->getSuggestedPersons($entityWorkflow);
if (true === $options['transition']) {
if (null === $options['entity_workflow']) {
throw new \LogicException('if transition is true, entity_workflow should be defined');
}
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);
$transitions = $this->registry
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
->getEnabledTransitions($entityWorkflow);
$choices = array_combine(
array_map(
static fn (Transition $transition) => $transition->getName(),
$transitions
),
$choices = array_combine(
array_map(
static fn (Transition $transition) => $transition->getName(),
$transitions
);
),
$transitions
);
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
$inputLabels = $placeMetadata['validationFilterInputLabels'];
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
$inputLabels = $placeMetadata['validationFilterInputLabels'];
$builder->add('transitionFilter', ChoiceType::class, [
'multiple' => false,
'label' => 'workflow.My decision',
'choices' => [
'forward' => 'forward',
'backward' => 'backward',
'neutral' => 'neutral',
],
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
'choice_attr' => static fn (string $key) => [
$key => $key,
],
'mapped' => false,
'expanded' => true,
'data' => 'forward',
]);
}
$builder
->add('transition', ChoiceType::class, [
'label' => 'workflow.Next step',
'mapped' => false,
'multiple' => false,
'expanded' => true,
'choices' => $choices,
'constraints' => [new NotNull()],
'choice_label' => function (Transition $transition) use ($workflow) {
$meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
if (\array_key_exists('label', $meta)) {
return $this->translatableStringHelper->localize($meta['label']);
}
return $transition->getName();
},
'choice_attr' => static function (Transition $transition) use ($workflow) {
$toFinal = true;
$isForward = 'neutral';
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
if (\array_key_exists('isForward', $metadata)) {
if ($metadata['isForward']) {
$isForward = 'forward';
} else {
$isForward = 'backward';
}
}
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
return [
'data-is-transition' => 'data-is-transition',
'data-to-final' => $toFinal ? '1' : '0',
'data-is-forward' => $isForward,
];
},
])
->add('future_dest_users', PickUserDynamicType::class, [
'label' => 'workflow.dest for next steps',
'multiple' => true,
'mapped' => false,
'suggested' => $options['suggested_users'],
])
->add('future_cc_users', PickUserDynamicType::class, [
'label' => 'workflow.cc for next steps',
'multiple' => true,
'mapped' => false,
'required' => false,
'suggested' => $options['suggested_users'],
])
->add('future_dest_emails', ChillCollectionType::class, [
'label' => 'workflow.dest by email',
'help' => 'workflow.dest by email help',
'mapped' => false,
'allow_add' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'workflow.Add an email',
'button_remove_label' => 'workflow.Remove an email',
'empty_collection_explain' => 'workflow.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(),
],
'label' => 'Email',
],
]);
$builder->add('transitionFilter', ChoiceType::class, [
'multiple' => false,
'label' => 'workflow.My decision',
'choices' => [
'forward' => 'forward',
'backward' => 'backward',
'neutral' => 'neutral',
],
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
'choice_attr' => static fn (string $key) => [
$key => $key,
],
'mapped' => false,
'expanded' => true,
'data' => 'forward',
]);
}
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('transition', ChoiceType::class, [
'label' => 'workflow.Next step',
'multiple' => false,
'expanded' => true,
'choices' => $choices,
'constraints' => [new NotNull()],
'choice_label' => function (Transition $transition) use ($workflow) {
$meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
if (\array_key_exists('label', $meta)) {
return $this->translatableStringHelper->localize($meta['label']);
}
return $transition->getName();
},
'choice_attr' => static function (Transition $transition) use ($workflow) {
$toFinal = true;
$isForward = 'neutral';
$isSignature = [];
$isSentExternal = false;
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
if (\array_key_exists('isForward', $metadata)) {
if ($metadata['isForward']) {
$isForward = 'forward';
} else {
$isForward = 'backward';
}
}
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
if (\array_key_exists('isSignature', $meta)) {
$isSignature = $meta['isSignature'];
}
$isSentExternal = $isSentExternal ? true : $meta['isSentExternal'] ?? false;
}
return [
'data-is-transition' => 'data-is-transition',
'data-to-final' => $toFinal ? '1' : '0',
'data-is-forward' => $isForward,
'data-is-signature' => json_encode($isSignature),
'data-is-sent-external' => $isSentExternal ? '1' : '0',
];
},
])
->add('isPersonOrUserSignature', ChoiceType::class, [
'mapped' => false,
'multiple' => false,
'expanded' => true,
'label' => 'workflow.signature_zone.type of signature',
'choices' => [
'workflow.signature_zone.persons' => 'person',
'workflow.signature_zone.user' => 'user',
],
])
->add('futurePersonSignatures', PickPersonDynamicType::class, [
'label' => 'workflow.signature_zone.person signatures',
'multiple' => true,
'empty_data' => '[]',
'suggested' => $suggestedPersons,
])
->add('futureUserSignature', PickUserDynamicType::class, [
'label' => 'workflow.signature_zone.user signature',
'multiple' => false,
'suggest_myself' => true,
'suggested' => $suggestedUsers,
])
->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [
'label' => 'workflow.dest for next steps',
'multiple' => true,
'empty_data' => '[]',
'suggested' => $suggestedUsers,
'suggest_myself' => true,
])
->add('futureCcUsers', PickUserDynamicType::class, [
'label' => 'workflow.cc for next steps',
'multiple' => true,
'required' => false,
'suggested' => $suggestedUsers,
'empty_data' => '[]',
'attr' => ['class' => 'future-cc-users'],
'suggest_myself' => true,
])
->add('futureDestineeEmails', ChillCollectionType::class, [
'entry_type' => EmailType::class,
'entry_options' => [
'empty_data' => '',
],
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => static fn (?string $email) => '' === $email || null === $email,
'button_add_label' => 'workflow.transition_destinee_add_emails',
'button_remove_label' => 'workflow.transition_destinee_remove_emails',
'help' => 'workflow.transition_destinee_emails_help',
'label' => 'workflow.transition_destinee_emails_label',
])
->add('futureDestineeThirdParties', PickThirdpartyDynamicType::class, [
'label' => 'workflow.transition_destinee_third_party',
'help' => 'workflow.transition_destinee_third_party_help',
'multiple' => true,
'empty_data' => [],
'suggested' => $suggestedThirdParties,
]);
$builder
->add('comment', ChillTextareaType::class, [
'required' => false,
'label' => 'Comment',
'empty_data' => '',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefined('class')
->setRequired('transition')
->setAllowedTypes('transition', 'bool')
->setDefault('data_class', WorkflowTransitionContextDTO::class)
->setRequired('entity_workflow')
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
->setDefault('suggested_users', [])
->setDefault('constraints', [
new Callback(
function ($step, ExecutionContextInterface $context, $payload) {
/** @var EntityWorkflowStep $step */
$form = $context->getObject();
$workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
$transition = $form['transition']->getData();
$toFinal = true;
if (null === $transition) {
$context
->buildViolation('workflow.You must select a next step, pick another decision if no next steps are available');
} else {
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
$destUsers = $form['future_dest_users']->getData();
$destEmails = $form['future_dest_emails']->getData();
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
$context
->buildViolation('workflow.You must add at least one dest user or email')
->atPath('future_dest_users')
->addViolation();
}
}
}
),
new Callback(
function ($step, ExecutionContextInterface $context, $payload) {
$form = $context->getObject();
foreach ($form->get('future_dest_users')->getData() as $u) {
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
$context
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
->atPath('ccUsers')
->addViolation();
}
}
}
),
]);
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
}
}