Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 87 additions & 4 deletions ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
}
}
21 changes: 18 additions & 3 deletions ProcessMaker/Http/Controllers/Process/ModelerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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,
]);
}
}
99 changes: 99 additions & 0 deletions ProcessMaker/Traits/ProcessMapTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace ProcessMaker\Traits;

use Illuminate\Support\Collection;
use ProcessMaker\Models\ProcessRequest;
use SimpleXMLElement;

trait ProcessMapTrait
{
/**
* Load XML from a string and register its namespaces.
* This function will help to prepare the XML for further processing.
*/
private function loadAndPrepareXML(string $bpmn): SimpleXMLElement
{
$xml = simplexml_load_string($bpmn);
$namespaces = $xml->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'],
]);
}
}
25 changes: 24 additions & 1 deletion resources/js/processes/modeler/components/ProcessMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@
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"
/>
<ModelerReadonly
ref="modeler"
:owner="self"
:decorations="decorations"
:request-completed-nodes="requestCompletedNodes"
:request-in-progress-nodes="requestInProgressNodes"
:request-idle-nodes="requestIdleNodes"
@set-xml-manager="xmlManager = $event"
@click="handleClick"
/>
Expand All @@ -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",
Expand All @@ -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;
},
Expand All @@ -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 };
Expand All @@ -106,6 +126,9 @@ export default {
this.tooltip.newX = window.innerWidth - this.rectTooltip.width;
}
},
getIsLoading(value) {
this.tooltip.isLoading = value;
},
},
};
</script>
Loading