diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php index 92abf0c58c..8e4a863629 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php @@ -8,10 +8,8 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use Notification; @@ -20,23 +18,26 @@ use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Resources\ApiCollection; +use ProcessMaker\Http\Resources\ApiResource; use ProcessMaker\Http\Resources\ProcessRequests as ProcessRequestResource; use ProcessMaker\Jobs\CancelRequest; use ProcessMaker\Jobs\TerminateRequest; use ProcessMaker\Models\Comment; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; -use ProcessMaker\Models\Setting; use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface; use ProcessMaker\Notifications\ProcessCanceledNotification; use ProcessMaker\Query\SyntaxError; use ProcessMaker\RetryProcessRequest; +use ProcessMaker\Traits\ProcessMapTrait; use Symfony\Component\HttpFoundation\IpUtils; use Throwable; class ProcessRequestController extends Controller { + use ProcessMapTrait; + const DOMAIN_CACHE_TIME = 86400; /** @@ -173,7 +174,7 @@ public function index(Request $request, $getTotal = false, User $user = null) } if (isset($response)) { - //Map each item through its resource + // Map each item through its resource $response = $response->map(function ($processRequest) use ($request) { return new ProcessRequestResource($processRequest); }); @@ -608,4 +609,86 @@ private function getTaskName($fields, $request) return $token->element_name; } + + /** + * Get Information of the last token for the element query + * + * @Parameter + * Request $httpRequest + * ProcessRequest $request + * @return + * object data { + * element_id, + * element_name, + * created_at, + * completed_at, + * username, + * sequenceFlow, + * count + * } + */ + public function getRequestToken(Request $httpRequest, ProcessRequest $request) + { + $httpRequest->validate([ + 'element_id' => 'required|string', + ]); + + $elementId = null; + $maxTokenId = $this->getMaxTokenId($request, $httpRequest->element_id); + if ($maxTokenId === null) { + $bpmn = $request->process->versions() + ->where('id', $request->process_version_id) + ->firstOrFail() + ->bpmn; + + // Get the source and target node for the sequence flow. + $xml = $this->loadAndPrepareXML($bpmn); + $targetAndSourceRef = $this->getRefNodes($xml, $httpRequest->element_id); + + if ($targetAndSourceRef->isNotEmpty()) { + $targetRef = $targetAndSourceRef['targetRef']; + $sourceRef = $targetAndSourceRef['sourceRef']; + + // Get the token counts for the target and source nodes. + $targetTokensCount = $this->getTokenCount($request, $targetRef); + $sourceTokensCount = $this->getTokenCount($request, $sourceRef); + + // Get the minimum repeated node ID. + $elementId = ($sourceTokensCount < $targetTokensCount) ? $sourceRef : $targetRef; + } + + // Get the maximum node ID. + $httpRequest->merge(['element_id' => $elementId]); + $maxTokenId = $this->getMaxTokenId($request, $httpRequest->element_id); + } + + $token = $request->tokens() + ->where('id', $maxTokenId) + ->select('user_id', 'element_id', 'element_name', 'created_at', 'completed_at', 'status') + ->with([ + 'user' => fn ($query) => $query->select('id', 'username'), + ]) + ->firstOrFail(); + + // Flags if the object clicked is a Sequence Flow. + $token->is_sequence_flow = $elementId ? true : false; + + $translatedStatus = match ($token->status) { + 'CLOSED' => __('Completed'), + 'ACTIVE' => __('In Progress'), + default => $token->status, + }; + $token->status_translation = $translatedStatus; + $token->completed_by = $token->completed_at ? ($token->user['username'] ?? '-') : '-'; + + // Get the number of times the flow has run. + $tokensCount = $request->tokens() + ->where([ + 'element_id' => $httpRequest->element_id, + 'process_request_id'=> $request->id, + ])->count(); + $token->count = $tokensCount; + + return new ApiResource($token); + } } diff --git a/ProcessMaker/Http/Controllers/Process/ModelerController.php b/ProcessMaker/Http/Controllers/Process/ModelerController.php index 688f02c0de..27162ed2da 100644 --- a/ProcessMaker/Http/Controllers/Process/ModelerController.php +++ b/ProcessMaker/Http/Controllers/Process/ModelerController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Process; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use ProcessMaker\Events\ModelerStarting; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Managers\ModelerManager; @@ -11,10 +12,13 @@ use ProcessMaker\Models\ProcessRequest; use ProcessMaker\PackageHelper; use ProcessMaker\Traits\HasControllerAddons; +use ProcessMaker\Traits\ProcessMapTrait; +use SimpleXMLElement; class ModelerController extends Controller { use HasControllerAddons; + use ProcessMapTrait; /** * Invokes the Process Modeler for rendering. @@ -53,6 +57,7 @@ public function inflight(ModelerManager $manager, Process $process, Request $req $bpmn = $process->bpmn; $requestCompletedNodes = []; $requestInProgressNodes = []; + $requestIdleNodes = []; // Use the process version that was active when the request was started. $processRequest = ProcessRequest::find($request->request_id); @@ -62,18 +67,28 @@ public function inflight(ModelerManager $manager, Process $process, Request $req ->firstOrFail() ->bpmn; - $requestCompletedNodes = $processRequest->tokens()->where('status', 'CLOSED')->pluck('element_id'); + $requestCompletedNodes = $processRequest->tokens()->whereIn('status', ['CLOSED', 'TRIGGERED'])->pluck('element_id'); $requestInProgressNodes = $processRequest->tokens()->where('status', 'ACTIVE')->pluck('element_id'); - // Remove any node that is 'ACTIVE' from the 'CLOSED' list. + // Remove any node that is 'ACTIVE' from the completed list. $filteredCompletedNodes = $requestCompletedNodes->diff($requestInProgressNodes)->values(); + + // Get idle nodes. + $xml = $this->loadAndPrepareXML($bpmn); + $nodeIds = $this->getNodeIds($xml); + $requestIdleNodes = $nodeIds->diff($filteredCompletedNodes)->diff($requestInProgressNodes)->values(); + + // Add completed sequence flow to the list of completed nodes. + $sequenceFlowNodes = $this->getCompletedSequenceFlow($xml, $filteredCompletedNodes->implode(' '), $requestInProgressNodes->implode(' ')); + $filteredCompletedNodes = $filteredCompletedNodes->merge($sequenceFlowNodes); } return view('processes.modeler.inflight', [ 'manager' => $manager, - 'process' => $process, 'bpmn' => $bpmn, 'requestCompletedNodes' => $filteredCompletedNodes, 'requestInProgressNodes' => $requestInProgressNodes, + 'requestIdleNodes' => $requestIdleNodes, + 'requestId' => $request->request_id, ]); } } diff --git a/ProcessMaker/Traits/ProcessMapTrait.php b/ProcessMaker/Traits/ProcessMapTrait.php new file mode 100644 index 0000000000..cf9b5f55b8 --- /dev/null +++ b/ProcessMaker/Traits/ProcessMapTrait.php @@ -0,0 +1,99 @@ +getNamespaces(true); + + foreach ($namespaces as $prefix => $ns) { + $xml->registerXPathNamespace($prefix, $ns); + } + + return $xml; + } + + /** + * Get the maximum token ID for a given element ID. + */ + private function getMaxTokenId(ProcessRequest $request, ?string $elementId): ?int + { + return $request->tokens() + ->where('element_id', $elementId) + ->max('id'); + } + + /** + * Get the token count for a given element ID. + */ + private function getTokenCount(ProcessRequest $request, string $elementId): int + { + return $request->tokens() + ->where([ + 'element_id' => $elementId, + 'process_request_id' => $request->id, + ]) + ->count(); + } + + /** + * Filter the XML using the provided XPath query and return a Collection of string values. + */ + private function filterXML(SimpleXMLElement $xml, string $xpathQuery): Collection + { + $elements = $xml->xpath($xpathQuery); + + return collect(array_map('strval', $elements)); + } + + /** + * Filter the XML to get IDs of all nodes excluding "lanes" and "pools" nodes. + */ + private function getNodeIds(SimpleXMLElement $xml): Collection + { + $query = '//*[name() != "bpmn:lane" and name() != "bpmn:participant"]/@id'; + + return $this->filterXML($xml, $query); + } + + /** + * Performs an XPath query to get sequenceFlow elements + * whose 'sourceRef' attribute is in the string of completed nodes + * and 'targetRef' attribute is in the string of in-progress and completed nodes. + */ + private function getCompletedSequenceFlow(SimpleXMLElement $xml, string $completedNodesStr, string $inProgressNodesStr): Collection + { + $inProgressAndCompletedNodes = $completedNodesStr . ' ' . $inProgressNodesStr; + $query = '//bpmn:sequenceFlow[contains("' . $completedNodesStr . '", @sourceRef) and contains("' . $inProgressAndCompletedNodes . '", @targetRef)]/@id'; + + return $this->filterXML($xml, $query); + } + + /** + * Performs an XPath query to get the targetRef and SourceRef Node Id + */ + private function getRefNodes(SimpleXMLElement $xml, string $sequenceFlowId): Collection + { + $sequenceFlowNode = $xml->xpath("//bpmn:sequenceFlow[@id='{$sequenceFlowId}']"); + + if (empty($sequenceFlowNode)) { + return collect(); + } + + return collect([ + 'targetRef' => (string) $sequenceFlowNode[0]['targetRef'], + 'sourceRef' => (string) $sequenceFlowNode[0]['sourceRef'], + ]); + } +} diff --git a/resources/js/processes/modeler/components/ProcessMap.vue b/resources/js/processes/modeler/components/ProcessMap.vue index 8d8fe2431d..044d715a28 100644 --- a/resources/js/processes/modeler/components/ProcessMap.vue +++ b/resources/js/processes/modeler/components/ProcessMap.vue @@ -9,10 +9,13 @@ v-show="tooltip.isActive" ref="tooltip" :node-id="tooltip.nodeId" + :node-name="tooltip.nodeName" + :request-id="requestId" :style="{ left: `${tooltip.newX}px`, top: `${tooltip.newY}px` }" + @is-loading="getIsLoading" /> @@ -42,14 +46,15 @@ export default { return { self: this, validationBar: [], - process: window.ProcessMaker.modeler.process, xmlManager: null, decorations: { borderOutline: {}, }, tooltip: { isActive: false, + isLoading: false, nodeId: null, + nodeName: null, allowedNodes: [ "bpmn:Task", "bpmn:ManualTask", @@ -63,8 +68,22 @@ export default { }, requestCompletedNodes: window.ProcessMaker.modeler.requestCompletedNodes, requestInProgressNodes: window.ProcessMaker.modeler.requestInProgressNodes, + requestIdleNodes: window.ProcessMaker.modeler.requestIdleNodes, + requestId: window.ProcessMaker.modeler.requestId, }; }, + watch: { + "tooltip.isLoading": { + handler(value) { + if (!value) { + this.$nextTick().then(() => { + this.calculateTooltipPosition(); + }); + } + }, + deep: true, + }, + }, mounted() { ProcessMaker.$modeler = this.$refs.modeler; }, @@ -84,6 +103,7 @@ export default { if ((isNodeTooltipAllowed && this.tooltip.isActive === false) || (isNodeTooltipAllowed && this.tooltip.nodeId !== node.id)) { this.tooltip.nodeId = node.id; + this.tooltip.nodeName = node.name; this.tooltip.isActive = true; this.$nextTick(() => { this.tooltip.coordinates = { x: event.clientX, y: event.clientY }; @@ -106,6 +126,9 @@ export default { this.tooltip.newX = window.innerWidth - this.rectTooltip.width; } }, + getIsLoading(value) { + this.tooltip.isLoading = value; + }, }, }; diff --git a/resources/js/processes/modeler/components/ProcessMapTooltip.vue b/resources/js/processes/modeler/components/ProcessMapTooltip.vue index a9120a8816..70c4705170 100644 --- a/resources/js/processes/modeler/components/ProcessMapTooltip.vue +++ b/resources/js/processes/modeler/components/ProcessMapTooltip.vue @@ -4,28 +4,61 @@ class="card shadow-sm" >
+
+
+
-

- {{ nodeId }} -

-

- Status:Complete -

-

- Completed By:UserName -

-

- Time Started:11/21/23 16:51 -

-

- Time Completed:11/21/23 16:53 -

+
+

+ {{ tokenResult.element_name }} +

+

+ {{ $t('Status') }}: + {{ tokenResult.status_translation }} +

+

+ {{ $t('Completed By') }}: + {{ tokenResult.completed_by }} +

+

+ {{ $t('Time Started') }}: + {{ tokenResult.created_at }} +

+

+ {{ $t('Time Completed') }}: + {{ tokenResult.completed_at }} +

+
+
+

+ + {{ repeatMessage }} + +

+
+
+

+ {{ nodeName }} +

+

+ {{ tokenResult.message }} +

+
diff --git a/resources/views/processes/modeler/inflight.blade.php b/resources/views/processes/modeler/inflight.blade.php index 5e4ba1ad4a..2c6ba08c8f 100644 --- a/resources/views/processes/modeler/inflight.blade.php +++ b/resources/views/processes/modeler/inflight.blade.php @@ -28,12 +28,12 @@