diff --git a/.changes/unreleased/DX-20251003-114344.yaml b/.changes/unreleased/DX-20251003-114344.yaml new file mode 100644 index 000000000..c7b36e93a --- /dev/null +++ b/.changes/unreleased/DX-20251003-114344.yaml @@ -0,0 +1,6 @@ +kind: DX +body: Introduce a WaitingScreen component to display a waiting screen +time: 2025-10-03T11:43:44.688572711+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20250724-161556.yaml b/.changes/unreleased/Feature-20250724-161556.yaml new file mode 100644 index 000000000..e661f72e1 --- /dev/null +++ b/.changes/unreleased/Feature-20250724-161556.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Only allow delete of attachment on workflows that are not final +time: 2025-07-24T16:15:56.042884578+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20250724-161628.yaml b/.changes/unreleased/Feature-20250724-161628.yaml new file mode 100644 index 000000000..db2013b66 --- /dev/null +++ b/.changes/unreleased/Feature-20250724-161628.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Move up signature buttons on index workflow page for easier access +time: 2025-07-24T16:16:28.609598883+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20250724-172013.yaml b/.changes/unreleased/Feature-20250724-172013.yaml new file mode 100644 index 000000000..ab12bd88b --- /dev/null +++ b/.changes/unreleased/Feature-20250724-172013.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Filter out document from attachment list if it is the same as the workflow document +time: 2025-07-24T17:20:13.118537573+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20251003-113838.yaml b/.changes/unreleased/Feature-20251003-113838.yaml new file mode 100644 index 000000000..22257a5e1 --- /dev/null +++ b/.changes/unreleased/Feature-20251003-113838.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Block edition on attached document on workflow, if the workflow is finalized or sent external +time: 2025-10-03T11:38:38.879030263+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20251003-113937.yaml b/.changes/unreleased/Feature-20251003-113937.yaml new file mode 100644 index 000000000..85f4118bb --- /dev/null +++ b/.changes/unreleased/Feature-20251003-113937.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Convert workflow's attached document to pdf while sending them external +time: 2025-10-03T11:39:37.382834874+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20251003-114233.yaml b/.changes/unreleased/Feature-20251003-114233.yaml new file mode 100644 index 000000000..42296ca94 --- /dev/null +++ b/.changes/unreleased/Feature-20251003-114233.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition +time: 2025-10-03T11:42:33.859561072+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20251003-114144.yaml b/.changes/unreleased/Fixed-20251003-114144.yaml new file mode 100644 index 000000000..92be727be --- /dev/null +++ b/.changes/unreleased/Fixed-20251003-114144.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Fix permissions on storedObject which are subject by a workflow +time: 2025-10-03T11:41:44.17608896+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts index facc55e58..497cfee5a 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts @@ -25,7 +25,7 @@ export interface GenericDoc { type: "doc_store_generic_doc"; uniqueKey: string; key: string; - identifiers: object; + identifiers: { id: number }; context: "person" | "accompanying-period"; doc_date: DateTime; metadata: GenericDocMetadata; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 161360c52..15e8d7aac 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -46,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { + // we first try to get the permission from the workflow, as attachement (this is the less intensive query) + $workflowPermissionAsAttachment = match ($attribute) { + StoredObjectRoleEnum::SEE => $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/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index fc6099557..d1a96a5bb 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/ChillMainBundle/Controller/WorkflowApiController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php index 28dca8016..eee731d98 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\Component\Security\Core\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 24de6f1a0..aacee5c13 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowSignatureStateChangeController.php @@ -44,7 +44,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', ); } @@ -56,11 +56,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, @@ -79,12 +86,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 6b33b998d..70f0df071 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/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 @@