diff --git a/.changes/unreleased/Fixed-20250909-173639.yaml b/.changes/unreleased/Fixed-20250909-173639.yaml deleted file mode 100644 index 57dc42e20..000000000 --- a/.changes/unreleased/Fixed-20250909-173639.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Fix display of 'duplicate' and 'merge' buttons in CRUD templates -time: 2025-09-09T17:36:39.960419639+02:00 -custom: - Issue: "" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20251003-224044.yaml b/.changes/unreleased/Fixed-20251003-224044.yaml new file mode 100644 index 000000000..c5b3e7304 --- /dev/null +++ b/.changes/unreleased/Fixed-20251003-224044.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted +time: 2025-10-03T22:40:44.685474863+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/v4.4.0.md b/.changes/v4.4.0.md new file mode 100644 index 000000000..bc8d2e98f --- /dev/null +++ b/.changes/v4.4.0.md @@ -0,0 +1,8 @@ +## v4.4.0 - 2025-09-11 +### Feature +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works +* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works +### Fixed +* Fix display of 'duplicate' and 'merge' buttons in CRUD templates +* Fix saving notification preferences in user's profile diff --git a/.changes/v4.4.1.md b/.changes/v4.4.1.md new file mode 100644 index 000000000..dd4c339ab --- /dev/null +++ b/.changes/v4.4.1.md @@ -0,0 +1,3 @@ +## v4.4.1 - 2025-09-11 +### Fixed +* fix translations in duplicate evaluation document modal and realign close modal button diff --git a/.changes/v4.4.2.md b/.changes/v4.4.2.md new file mode 100644 index 000000000..c04db028f --- /dev/null +++ b/.changes/v4.4.2.md @@ -0,0 +1,3 @@ +## v4.4.2 - 2025-09-12 +### Fixed +* Fix document generation and workflow generation do not work on accompanying period work documents diff --git a/.changes/v4.5.0.md b/.changes/v4.5.0.md new file mode 100644 index 000000000..1744eca0c --- /dev/null +++ b/.changes/v4.5.0.md @@ -0,0 +1,13 @@ +## v4.5.0 - 2025-10-03 +### Feature +* Only allow delete of attachment on workflows that are not final +* Move up signature buttons on index workflow page for easier access +* Filter out document from attachment list if it is the same as the workflow document +* Block edition on attached document on workflow, if the workflow is finalized or sent external +* Convert workflow's attached document to pdf while sending them external +* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition +### Fixed +* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance +* Fix permissions on storedObject which are subject by a workflow +### DX +* Introduce a WaitingScreen component to display a waiting screen diff --git a/.changes/v4.5.1.md b/.changes/v4.5.1.md new file mode 100644 index 000000000..d2bca7be9 --- /dev/null +++ b/.changes/v4.5.1.md @@ -0,0 +1,4 @@ +## v4.5.1 - 2025-10-03 +### Fixed +* Add missing javascript dependency +* Add exception handling for conversion of attachment on sending external, when documens are already in pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6e978ac..359b777c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,42 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.5.1 - 2025-10-03 +### Fixed +* Add missing javascript dependency +* Add exception handling for conversion of attachment on sending external, when documens are already in pdf + +## v4.5.0 - 2025-10-03 +### Feature +* Only allow delete of attachment on workflows that are not final +* Move up signature buttons on index workflow page for easier access +* Filter out document from attachment list if it is the same as the workflow document +* Block edition on attached document on workflow, if the workflow is finalized or sent external +* Convert workflow's attached document to pdf while sending them external +* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition +### Fixed +* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance +* Fix permissions on storedObject which are subject by a workflow +### DX +* Introduce a WaitingScreen component to display a waiting screen + +## v4.4.2 - 2025-09-12 +### Fixed +* Fix document generation and workflow generation do not work on accompanying period work documents + +## v4.4.1 - 2025-09-11 +### Fixed +* fix translations in duplicate evaluation document modal and realign close modal button + +## v4.4.0 - 2025-09-11 +### Feature +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works +* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works +### Fixed +* Fix display of 'duplicate' and 'merge' buttons in CRUD templates +* Fix saving notification preferences in user's profile + ## v4.3.0 - 2025-09-08 ### Feature * ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges diff --git a/package.json b/package.json index 73fa14d86..1b4c7df13 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@tsconfig/node20": "^20.1.4", "@types/dompurify": "^3.0.5", "@types/leaflet": "^1.9.3", + "@vueuse/core": "^13.9.0", "bootstrap-icons": "^1.11.3", "dropzone": "^5.7.6", "es6-promise": "^4.2.8", diff --git a/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectVersionApiController.php b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectVersionApiController.php index 1e24f8adf..40d5aec21 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectVersionApiController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectVersionApiController.php @@ -58,7 +58,7 @@ final readonly class StoredObjectVersionApiController return new JsonResponse( $this->serializer->serialize( - new Collection($items, $paginator), + new Collection(array_values($items->toArray()), $paginator), 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]] ), diff --git a/src/Bundle/ChillDocStoreBundle/Exception/ConversionWithSameMimeTypeException.php b/src/Bundle/ChillDocStoreBundle/Exception/ConversionWithSameMimeTypeException.php new file mode 100644 index 000000000..06fb4a9ed --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Exception/ConversionWithSameMimeTypeException.php @@ -0,0 +1,20 @@ + - Ajouter un document - - + $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject), + StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject), + }; + + if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) { + return false; + } + // Retrieve the related entity $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); @@ -66,7 +76,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface return match ($workflowPermission) { WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission, + WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission, }; } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 97d45eec3..11dea0652 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -14,6 +14,12 @@ namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +/** + * Interface for voting on stored object permissions. + * + * Each time a stored object is attached to a document, the voter is responsible for determining + * whether the user has the necessary permissions to access or modify the stored object. + */ interface StoredObjectVoterInterface { public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectToPdfConverter.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectToPdfConverter.php index ac570c513..8a5ea6e8c 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectToPdfConverter.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectToPdfConverter.php @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; use Chill\DocStoreBundle\Entity\StoredObjectVersion; +use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException; use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\WopiBundle\Service\WopiConverter; use Symfony\Component\Mime\MimeTypesInterface; @@ -41,9 +42,10 @@ class StoredObjectToPdfConverter * * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true * - * @throws \UnexpectedValueException if the preferred mime type for the conversion is not found - * @throws \RuntimeException if the conversion or storage of the new version fails + * @throws \UnexpectedValueException if the preferred mime type for the conversion is not found + * @throws \RuntimeException if the conversion or storage of the new version fails * @throws StoredObjectManagerException + * @throws ConversionWithSameMimeTypeException if the document has already the same mime type79* */ public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array { @@ -56,7 +58,7 @@ class StoredObjectToPdfConverter $currentVersion = $storedObject->getCurrentVersion(); if ($currentVersion->getType() === $newMimeType) { - throw new \UnexpectedValueException('Already at the same mime type'); + throw new ConversionWithSameMimeTypeException($newMimeType); } $content = $this->storedObjectManager->read($currentVersion); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/StoredObjectVersionApiControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/StoredObjectVersionApiControllerTest.php index f25f6f145..2dc997bc5 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/StoredObjectVersionApiControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/StoredObjectVersionApiControllerTest.php @@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase $storedObject->registerVersion(); } + // remove one version in the history + $v5 = $storedObject->getVersions()->get(5); + $storedObject->removeVersion($v5); + $security = $this->prophesize(Security::class); $security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject) ->willReturn(true) @@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase self::assertEquals($response->getStatusCode(), 200); self::assertIsArray($body); self::assertArrayHasKey('results', $body); + self::assertIsList($body['results']); self::assertCount(10, $body['results']); } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index 4831317e9..f33b99fea 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -86,9 +86,165 @@ class AbstractStoredObjectVoterTest extends TestCase } /** - * @dataProvider dataProviderVoteOnAttribute + * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission */ - public function testVoteOnAttribute( + public function testVoteOnAttributeWithStoredObjectPermission( + StoredObjectRoleEnum $attribute, + bool $expected, + bool $isGrantedRegularPermission, + string $isGrantedWorkflowPermission, + string $isGrantedStoredObjectAttachment, + ): void { + $storedObject = new StoredObject(); + $repository = new DummyRepository($related = new \stdClass()); + $token = new UsernamePasswordToken(new User(), 'dummy'); + + $security = $this->prophesize(Security::class); + $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); + + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); + + $security = $this->prophesize(Security::class); + $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); + + if (StoredObjectRoleEnum::SEE === $attribute) { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject) + ->shouldBeCalled() + ->willReturn($isGrantedStoredObjectAttachment); + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) + ->willReturn($isGrantedWorkflowPermission); + } elseif (StoredObjectRoleEnum::EDIT === $attribute) { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject) + ->shouldBeCalled() + ->willReturn($isGrantedStoredObjectAttachment); + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) + ->willReturn($isGrantedWorkflowPermission); + } else { + throw new \LogicException('Invalid attribute for StoredObjectVoter'); + } + + $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter { + public function __construct(private $repository, $helper, $security) + { + parent::__construct($security, $helper); + } + + protected function getRepository(): AssociatedEntityToStoredObjectInterface + { + return $this->repository; + } + + protected function getClass(): string + { + return \stdClass::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + return 'SOME_ROLE'; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return true; + } + }; + + $actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token); + + self::assertEquals($expected, $actual); + } + + public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable + { + foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) { + yield 'Not related to any workflow nor attachment ('.$action.')' => [ + $attribute, + true, + true, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + ]; + + yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [ + $attribute, + false, + false, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + ]; + + yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [ + $attribute, + false, + true, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + ]; + + yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [ + $attribute, + false, + true, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + ]; + + yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [ + $attribute, + false, + true, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + ]; + + yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [ + $attribute, + false, + true, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + ]; + + yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [ + $attribute, + false, + false, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + ]; + + yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [ + $attribute, + false, + false, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + ]; + + yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [ + $attribute, + true, + false, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + ]; + + yield 'Force grant inverse the regular permission (so) ('.$action.')' => [ + $attribute, + true, + false, + WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + ]; + + } + } + + /** + * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission + */ + public function testVoteOnAttributeWithoutStoredObjectPermission( StoredObjectRoleEnum $attribute, bool $expected, bool $canBeAssociatedWithWorkflow, @@ -105,6 +261,10 @@ class AbstractStoredObjectVoterTest extends TestCase $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); + + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); + if (null !== $isGrantedWorkflowPermissionRead) { $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled(); @@ -123,7 +283,7 @@ class AbstractStoredObjectVoterTest extends TestCase self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); } - public static function dataProviderVoteOnAttribute(): iterable + public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable { // not associated on a workflow yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper']; diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 1aea94892..94db395df 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -23,6 +23,8 @@ See the document: Voir le document document: Any title: Aucun titre + replace: Remplacer + Add: Ajouter un document generic_doc: filter: diff --git a/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommand.php b/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommand.php new file mode 100644 index 000000000..f0751cb58 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommand.php @@ -0,0 +1,64 @@ +getPhonenumber()); + + foreach ($flagManager->getAllNotificationFlagProviders() as $provider) { + $updateProfileCommand->setNotificationFlag( + $provider->getFlag(), + User::NOTIF_FLAG_IMMEDIATE_EMAIL, + $user->isNotificationSendImmediately($provider->getFlag()) + ); + $updateProfileCommand->setNotificationFlag( + $provider->getFlag(), + User::NOTIF_FLAG_DAILY_DIGEST, + $user->isNotificationDailyDigest($provider->getFlag()) + ); + } + + return $updateProfileCommand; + } + + /** + * @param User::NOTIF_FLAG_IMMEDIATE_EMAIL|User::NOTIF_FLAG_DAILY_DIGEST $kind + */ + private function setNotificationFlag(string $type, string $kind, bool $value): void + { + if (!array_key_exists($type, $this->notificationFlags)) { + $this->notificationFlags[$type] = ['immediate_email' => true, 'daily_digest' => false]; + } + + $k = match ($kind) { + User::NOTIF_FLAG_IMMEDIATE_EMAIL => 'immediate_email', + User::NOTIF_FLAG_DAILY_DIGEST => 'daily_digest', + }; + + $this->notificationFlags[$type][$k] = $value; + } +} diff --git a/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommandHandler.php b/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommandHandler.php new file mode 100644 index 000000000..4c46e686e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Action/User/UpdateProfile/UpdateProfileCommandHandler.php @@ -0,0 +1,27 @@ +setPhonenumber($command->phonenumber); + + foreach ($command->notificationFlags as $flag => $values) { + $user->setNotificationImmediately($flag, $values['immediate_email']); + $user->setNotificationDailyDigest($flag, $values['daily_digest']); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/UserUpdateProfileController.php b/src/Bundle/ChillMainBundle/Controller/UserUpdateProfileController.php new file mode 100644 index 000000000..b23c201f0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/UserUpdateProfileController.php @@ -0,0 +1,75 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + $user = $this->security->getUser(); + + $command = UpdateProfileCommand::create($user, $this->notificationFlagManager); + $editForm = $this->formFactory->create(UpdateProfileType::class, $command); + + $editForm->handleRequest($request); + + if ($editForm->isSubmitted() && $editForm->isValid()) { + $this->updateProfileCommandHandler->updateProfile($user, $command); + $this->entityManager->flush(); + $session->getFlashBag()->add('success', $this->translator->trans('user.profile.Profile successfully updated!')); + + return new RedirectResponse($this->urlGenerator->generate('chill_main_user_profile')); + } + + return new Response($this->twig->render('@ChillMain/User/profile.html.twig', [ + 'user' => $user, + 'form' => $editForm->createView(), + ])); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php index 8f960b412..e49b62290 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; +use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Pagination\PaginatorFactory; @@ -27,7 +28,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Serializer\SerializerInterface; -class WorkflowApiController +class WorkflowApiController extends ApiController { public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {} diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php index b342612ec..d83257682 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php @@ -43,7 +43,7 @@ final readonly class WorkflowSignatureStateChangeController $signature, $request, EntityWorkflowStepSignatureVoter::CANCEL, - function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); }, + fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature), '@ChillMain/WorkflowSignature/cancel.html.twig', ); } @@ -55,11 +55,18 @@ final readonly class WorkflowSignatureStateChangeController $signature, $request, EntityWorkflowStepSignatureVoter::REJECT, - function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); }, + fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature), '@ChillMain/WorkflowSignature/reject.html.twig', ); } + /** + * @param callable(EntityWorkflowStepSignature): string $markSignature + * + * @throws \Twig\Error\LoaderError + * @throws \Twig\Error\RuntimeError + * @throws \Twig\Error\SyntaxError + */ private function markSignatureAction( EntityWorkflowStepSignature $signature, Request $request, @@ -78,12 +85,13 @@ final readonly class WorkflowSignatureStateChangeController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) { - $markSignature($signature); - }); + $expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature)); return new RedirectResponse( - $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]) + $this->chillUrlGenerator->forwardReturnPath( + 'chill_main_workflow_wait', + ['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep] + ) ); } diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowWaitStepChangeController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowWaitStepChangeController.php new file mode 100644 index 000000000..53e966495 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowWaitStepChangeController.php @@ -0,0 +1,41 @@ +getStep() === $expectedStep) { + return new RedirectResponse( + $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]) + ); + } + + return new Response( + $this->twig->render('@ChillMain/Workflow/waiting.html.twig', ['workflow' => $entityWorkflow, 'expectedStep' => $expectedStep]) + ); + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 21b450540..7d961dcf6 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -30,6 +30,7 @@ use Chill\MainBundle\Controller\UserGroupAdminController; use Chill\MainBundle\Controller\UserGroupApiController; use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobController; +use Chill\MainBundle\Controller\WorkflowApiController; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Chill\MainBundle\Doctrine\DQL\Age; use Chill\MainBundle\Doctrine\DQL\Extract; @@ -66,6 +67,7 @@ use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Form\CenterType; use Chill\MainBundle\Form\CivilityType; use Chill\MainBundle\Form\CountryType; @@ -79,6 +81,7 @@ use Chill\MainBundle\Form\UserGroupType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; use Chill\MainBundle\Security\Authorization\ChillExportVoter; +use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType; use Ramsey\Uuid\Doctrine\UuidType; use Symfony\Component\Config\FileLocator; @@ -940,6 +943,21 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => EntityWorkflow::class, + 'name' => 'workflow', + 'base_path' => '/api/1.0/main/workflow', + 'base_role' => EntityWorkflowVoter::SEE, + 'controller' => WorkflowApiController::class, + 'actions' => [ + '_entity' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + ], + ], ], ]); } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 024d27988..fdcd857d8 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -635,42 +635,66 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter return true; } - public function getNotificationFlags(): array + private function getNotificationFlagData(string $flag): array { - return $this->notificationFlags; - } - - public function setNotificationFlags(array $notificationFlags): void - { - $this->notificationFlags = $notificationFlags; - } - - public function getNotificationFlagData(string $flag): array - { - return $this->notificationFlags[$flag] ?? []; - } - - public function setNotificationFlagData(string $flag, array $data): void - { - $this->notificationFlags[$flag] = $data; + return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL]; } public function isNotificationSendImmediately(string $type): bool { - if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) { - return true; + return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL); + } + + public function setNotificationImmediately(string $type, bool $active): void + { + $this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_IMMEDIATE_EMAIL); + } + + public function setNotificationDailyDigest(string $type, bool $active): void + { + $this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_DAILY_DIGEST); + } + + /** + * @param self::NOTIF_FLAG_IMMEDIATE_EMAIL|self::NOTIF_FLAG_DAILY_DIGEST $kind + */ + private function setNotificationFlagElement(string $type, bool $active, string $kind): void + { + $notificationFlags = [...$this->notificationFlags]; + $changed = false; + + if (!isset($notificationFlags[$type])) { + $notificationFlags[$type] = [self::NOTIF_FLAG_IMMEDIATE_EMAIL]; + $changed = true; } - return false; + if ($active) { + if (!in_array($kind, $notificationFlags[$type], true)) { + $notificationFlags[$type] = [...$notificationFlags[$type], $kind]; + $changed = true; + } + } else { + if (in_array($kind, $notificationFlags[$type], true)) { + $notificationFlags[$type] = array_values( + array_filter($notificationFlags[$type], static fn ($k) => $k !== $kind) + ); + $changed = true; + } + } + + if ($changed) { + $this->notificationFlags = [...$notificationFlags]; + } + } + + private function isNotificationForElement(string $type, string $kind): bool + { + return in_array($kind, $this->getNotificationFlagData($type), true); } public function isNotificationDailyDigest(string $type): bool { - if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) { - return true; - } - - return false; + return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST); } public function getLocale(): string diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php deleted file mode 100644 index d904ed5b5..000000000 --- a/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php +++ /dev/null @@ -1,75 +0,0 @@ -notificationFlagProviders as $flagProvider) { - $flag = $flagProvider->getFlag(); - - if (isset($formsArray[$flag])) { - $flagForm = $formsArray[$flag]; - - $immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true) - || !array_key_exists($flag, $viewData); - $dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true); - - if ($flagForm->has('immediate_email')) { - $flagForm->get('immediate_email')->setData($immediateEmailChecked); - } - if ($flagForm->has('daily_email')) { - $flagForm->get('daily_email')->setData($dailyEmailChecked); - } - } - } - } - - public function mapFormsToData($forms, &$viewData): void - { - $formsArray = iterator_to_array($forms); - $viewData = []; - - foreach ($this->notificationFlagProviders as $flagProvider) { - $flag = $flagProvider->getFlag(); - - if (isset($formsArray[$flag])) { - $flagForm = $formsArray[$flag]; - $viewData[$flag] = []; - - if (true === $flagForm['immediate_email']->getData()) { - $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL; - } - - if (true === $flagForm['daily_email']->getData()) { - $viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST; - } - - if ([] === $viewData[$flag]) { - $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL; - } - } - } - } -} diff --git a/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php b/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php index 4535a4815..b9e15f5d9 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php @@ -11,11 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type; -use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper; use Chill\MainBundle\Notification\NotificationFlagManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -30,27 +28,24 @@ class NotificationFlagsType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders)); - foreach ($this->notificationFlagProviders as $flagProvider) { $flag = $flagProvider->getFlag(); - $builder->add($flag, FormType::class, [ + $flagBuilder = $builder->create($flag, options: [ 'label' => $flagProvider->getLabel(), - 'required' => false, + 'compound' => true, ]); - $builder->get($flag) + $flagBuilder ->add('immediate_email', CheckboxType::class, [ 'label' => false, 'required' => false, - 'mapped' => false, ]) - ->add('daily_email', CheckboxType::class, [ + ->add('daily_digest', CheckboxType::class, [ 'label' => false, 'required' => false, - 'mapped' => false, ]) ; + $builder->add($flagBuilder); } } @@ -58,6 +53,7 @@ class NotificationFlagsType extends AbstractType { $resolver->setDefaults([ 'data_class' => null, + 'compound' => true, ]); } } diff --git a/src/Bundle/ChillMainBundle/Form/UserProfileType.php b/src/Bundle/ChillMainBundle/Form/UpdateProfileType.php similarity index 82% rename from src/Bundle/ChillMainBundle/Form/UserProfileType.php rename to src/Bundle/ChillMainBundle/Form/UpdateProfileType.php index 76f005e97..6aa8f0943 100644 --- a/src/Bundle/ChillMainBundle/Form/UserProfileType.php +++ b/src/Bundle/ChillMainBundle/Form/UpdateProfileType.php @@ -11,13 +11,14 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; +use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand; use Chill\MainBundle\Form\Type\ChillPhoneNumberType; use Chill\MainBundle\Form\Type\NotificationFlagsType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class UserProfileType extends AbstractType +class UpdateProfileType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { @@ -25,17 +26,14 @@ class UserProfileType extends AbstractType ->add('phonenumber', ChillPhoneNumberType::class, [ 'required' => false, ]) - ->add('notificationFlags', NotificationFlagsType::class, [ - 'label' => false, - 'mapped' => false, - ]) + ->add('notificationFlags', NotificationFlagsType::class) ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'data_class' => \Chill\MainBundle\Entity\User::class, + 'data_class' => UpdateProfileCommand::class, ]); } } diff --git a/src/Bundle/ChillMainBundle/Form/UserPasswordType.php b/src/Bundle/ChillMainBundle/Form/UserPasswordType.php index 239f36c41..a3fffaee5 100644 --- a/src/Bundle/ChillMainBundle/Form/UserPasswordType.php +++ b/src/Bundle/ChillMainBundle/Form/UserPasswordType.php @@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType 'invalid_message' => 'The password fields must match', 'constraints' => [ new Length([ - 'min' => 9, + 'min' => 14, 'minMessage' => 'The password must be greater than {{ limit }} characters', ]), new NotBlank(), diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/return_path/returnPathHelper.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/return_path/returnPathHelper.ts new file mode 100644 index 000000000..e4316329d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/return_path/returnPathHelper.ts @@ -0,0 +1,13 @@ +/** + * Extracts the "returnPath" parameter from the current URL's query string and returns it. + * If the parameter is not present, returns the provided fallback path. + * + * @param {string} fallbackPath - The fallback path to use if "returnPath" is not found in the query string. + * @return {string} The "returnPath" from the query string, or the fallback path if "returnPath" is not present. + */ +export function returnPathOr(fallbackPath: string): string { + const urlParams = new URLSearchParams(window.location.search); + const returnPath = urlParams.get("returnPath"); + + return returnPath ?? fallbackPath; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/api.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/api.ts new file mode 100644 index 000000000..773abbfaf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/api.ts @@ -0,0 +1,16 @@ +import { EntityWorkflow } from "ChillMainAssets/types"; +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; + +export const fetchWorkflow = async ( + workflowId: number, +): Promise => { + try { + return await makeFetch( + "GET", + `/api/1.0/main/workflow/${workflowId}.json`, + ); + } catch (error) { + console.error(`Failed to fetch workflow ${workflowId}:`, error); + throw error; + } +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 2cd83bc64..b96462ceb 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -1,5 +1,6 @@ import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc"; import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; +import { Person } from "../../../ChillPersonBundle/Resources/public/types"; export interface DateTime { datetime: string; @@ -202,6 +203,58 @@ export interface WorkflowAttachment { genericDoc: null | GenericDoc; } +export interface Workflow { + name: string; + text: string; +} + +export interface EntityWorkflowStep { + type: "entity_workflow_step"; + id: number; + comment: string; + currentStep: StepDefinition; + isFinal: boolean; + isFreezed: boolean; + isFinalized: boolean; + transitionPrevious: Transition | null; + transitionAfter: Transition | null; + previousId: number | null; + nextId: number | null; + transitionPreviousBy: User | null; + transitionPreviousAt: DateTime | null; +} + +export interface Transition { + name: string; + text: string; + isForward: boolean; +} + +export interface StepDefinition { + name: string; + text: string; +} + +export interface EntityWorkflow { + type: "entity_workflow"; + id: number; + relatedEntityClass: string; + relatedEntityId: number; + workflow: Workflow; + currentStep: EntityWorkflowStep; + steps: EntityWorkflowStep[]; + datas: WorkflowData; + title: string; + isOnHoldAtCurrentStep: boolean; + _permissions: { + CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT: boolean; + }; +} + +export interface WorkflowData { + persons: Person[]; +} + export interface ExportGeneration { id: string; type: "export_generation"; @@ -215,3 +268,8 @@ export interface ExportGeneration { export interface PrivateCommentEmbeddable { comments: Record; } + +/** + * Possible states for the WaitingScreen Component. + */ +export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready"; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue index 033eb6cac..b9ffeff34 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue @@ -10,7 +10,8 @@ import { computed, onMounted, ref } from "vue"; import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export"; import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; -import { ExportGeneration } from "ChillMainAssets/types"; +import WaitingScreen from "../_components/WaitingScreen.vue"; +import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types"; interface AppProps { exportGenerationId: string; @@ -34,13 +35,16 @@ const storedObject = computed(() => { }); const isPending = computed(() => status.value === "pending"); -const isFetching = computed( - () => tryiesForReady.value < maxTryiesForReady, -); -const isReady = computed(() => status.value === "ready"); -const isFailure = computed(() => status.value === "failure"); const filename = computed(() => `${props.title}-${props.createdDate}`); +const state = computed((): WaitingScreenState => { + if (status.value === "empty") { + return "pending"; + } + + return status.value; +}); + /** * counter for the number of times that we check for a new status */ @@ -85,57 +89,36 @@ onMounted(() => { - - diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/App.vue new file mode 100644 index 000000000..578cb6558 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/App.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts new file mode 100644 index 000000000..ac52a90cd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts @@ -0,0 +1,51 @@ +import { createApp } from "vue"; +import App from "./App.vue"; + +function mountApp(): void { + const el = document.querySelector(".screen-wait"); + if (!el) { + console.error( + "WaitPostProcessWorkflow: mount element .screen-wait not found", + ); + return; + } + + const workflowIdAttr = el.getAttribute("data-workflow-id"); + const expectedStep = el.getAttribute("data-expected-step") || ""; + + if (!workflowIdAttr) { + console.error( + "WaitPostProcessWorkflow: data-workflow-id attribute missing on mount element", + ); + return; + } + + if (!expectedStep) { + console.error( + "WaitPostProcessWorkflow: data-expected-step attribute missing on mount element", + ); + return; + } + + const workflowId = Number(workflowIdAttr); + if (Number.isNaN(workflowId)) { + console.error( + "WaitPostProcessWorkflow: data-workflow-id is not a valid number:", + workflowIdAttr, + ); + return; + } + + const app = createApp(App, { + workflowId, + expectedStep, + }); + + app.mount(el); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", mountApp); +} else { + mountApp(); +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue index 9164e134b..289b49788 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue @@ -1,10 +1,11 @@