Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FOUR-20337 : [SPRING] Speed up In-Flight Modeler UI Component #7849

Open
wants to merge 17 commits into
base: release-2024-fall
Choose a base branch
from
Open
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
23 changes: 22 additions & 1 deletion ProcessMaker/Http/Controllers/CasesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
namespace ProcessMaker\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use ProcessMaker\Events\ModelerStarting;
use ProcessMaker\Events\ScreenBuilderStarting;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Controllers\Process\ModelerController;
use ProcessMaker\Managers\ModelerManager;
use ProcessMaker\Managers\ScreenBuilderManager;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\PackageComments\PackageServiceProvider;
use ProcessMaker\ProcessTranslations\ScreenTranslation;
use ProcessMaker\Traits\ProcessMapTrait;

class CasesController extends Controller
{
use ProcessMapTrait;
/**
* Get the list of requests.
*
Expand All @@ -38,6 +43,10 @@ public function show($case_number)
// Load event ScreenBuilderStarting
$manager = app(ScreenBuilderManager::class);
event(new ScreenBuilderStarting($manager, 'FORM'));
// Load event ModelerStarting
$managerModeler = app(ModelerManager::class);
event(new ModelerStarting($managerModeler));

// Get all the request related to this case number
$allRequests = ProcessRequest::where('case_number', $case_number)->get();
$parentRequest = null;
Expand Down Expand Up @@ -76,6 +85,14 @@ public function show($case_number)
// Get the summary screen tranlations
$this->summaryScreenTranslation($request);

// Load the process map
$inflightData = $this->loadProcessMap($request);
$bpmn = $inflightData['bpmn'];

// Get all PM-Blocks
$modelerController = new ModelerController();
$pmBlockList = $modelerController->getPmBlockList();

// Return the view
return view('cases.edit', compact(
'request',
Expand All @@ -85,7 +102,11 @@ public function show($case_number)
'canViewComments',
'canPrintScreens',
'isProcessManager',
'manager'
'manager',
'managerModeler',
'bpmn',
'inflightData',
'pmBlockList'
));
}

Expand Down
8 changes: 8 additions & 0 deletions ProcessMaker/Http/Controllers/RequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
use ProcessMaker\ProcessTranslations\ScreenTranslation;
use ProcessMaker\RetryProcessRequest;
use ProcessMaker\Traits\HasControllerAddons;
use ProcessMaker\Traits\ProcessMapTrait;
use ProcessMaker\Traits\SearchAutocompleteTrait;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class RequestController extends Controller
{
use SearchAutocompleteTrait;
use HasControllerAddons;
use ProcessMapTrait;

/**
* Get the list of requests.
Expand Down Expand Up @@ -182,6 +184,10 @@ public function show(ProcessRequest $request, Media $mediaItems)
}
$this->summaryScreenTranslation($request);

//Load the process map
$inflightData = $this->loadProcessMap($request);
$bpmn = $inflightData['bpmn'];

if (isset($_SERVER['HTTP_USER_AGENT']) && MobileHelper::isMobile($_SERVER['HTTP_USER_AGENT'])) {
return view('requests.showMobile', compact(
'request',
Expand Down Expand Up @@ -217,6 +223,8 @@ public function show(ProcessRequest $request, Media $mediaItems)
'eligibleRollbackTask',
'errorTask',
'userConfiguration',
'bpmn',
'inflightData',
));
}

Expand Down
45 changes: 45 additions & 0 deletions ProcessMaker/Traits/ProcessMapTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ProcessMaker\Traits;

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

Expand All @@ -17,6 +18,9 @@ private function loadAndPrepareXML(string $bpmn): SimpleXMLElement
$xml = simplexml_load_string($bpmn);
$namespaces = $xml->getNamespaces(true);

// Register the BPMN namespace explicitly
$xml->registerXPathNamespace('bpmn', 'http://www.omg.org/spec/BPMN/20100524/MODEL');

foreach ($namespaces as $prefix => $ns) {
$xml->registerXPathNamespace($prefix, $ns);
}
Expand Down Expand Up @@ -108,4 +112,45 @@ private function getCountFlag(int $sourceCount, int $targetCount, string $source

return $maxToken->status === 'ACTIVE' && $sourceCount === $targetCount;
}

private function loadProcessMap(ProcessRequest $request): array
{
$processRequest = ProcessRequest::find($request->id);
$bpmn = $request->process->bpmn;
$filteredCompletedNodes = [];
$requestInProgressNodes = [];
$requestIdleNodes = [];

if ($processRequest) {
$requestCompletedNodes = $processRequest->tokens()
->whereIn('status', ['CLOSED', 'COMPLETED', 'TRIGGERED'])
->pluck('element_id');
$requestInProgressNodes = $processRequest->tokens()
->whereIn('status', ['ACTIVE', 'INCOMING'])
->pluck('element_id');

// Remove any node that is 'ACTIVE' from the completed list.
$filteredCompletedNodes = $requestCompletedNodes->diff($requestInProgressNodes)->values();

// Obtain In-Progress nodes that were completed before
$matchingNodes = $requestInProgressNodes->intersect($requestCompletedNodes);

// 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(' '), $matchingNodes->implode(' '));
$filteredCompletedNodes = $filteredCompletedNodes->merge($sequenceFlowNodes);
}

return [
'bpmn' => $bpmn,
'requestCompletedNodes' => $filteredCompletedNodes,
'requestInProgressNodes' => $requestInProgressNodes,
'requestIdleNodes' => $requestIdleNodes,
'requestId' => $request->id,
];
}
}
2 changes: 2 additions & 0 deletions resources/js/requests/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import FilesMobile from "./components/FilesMobile.vue";
import RequestHeaderMobile from "./components/RequestHeaderMobile.vue";
import FilterMobile from "../Mobile/FilterMobile.vue";
import FilterMixin from "../Mobile/FilterMixin";
import NewOverview from "../../jscomposition/cases/casesDetail/components/NewOverview.vue";

Vue.component("DataSummary", DataSummary);
Vue.component("RequestDetail", RequestDetail);
Expand All @@ -31,6 +32,7 @@ Vue.component("SummaryMobile", SummaryMobile);
Vue.component("FilesMobile", FilesMobile);
Vue.component("RequestHeaderMobile", RequestHeaderMobile);
Vue.component("FilterMobile", FilterMobile);
Vue.component("NewOverview", NewOverview);
Vue.mixin(FilterMixin);

Vue.use("vue-form-renderer", VueFormRenderer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import RequestTable from "./RequestTable.vue";
import TabHistory from "./TabHistory.vue";
import CompletedForms from "./CompletedForms.vue";
import TabFiles from "./TabFiles.vue";
import Overview from "./Overview.vue";
import Overview from "./NewOverview.vue";
import TabSummary from "./TabSummary.vue";
import ErrorsTab from "./ErrorsTab.vue";
import { getRequestCount, getRequestStatus, isErrors } from "../variables/index";
Expand Down Expand Up @@ -67,7 +67,7 @@ const tabs = [
name: translate.t("Summary"),
href: "#summary",
current: "summary",
show: getRequestStatus() !== 'ERROR',
show: getRequestStatus() !== "ERROR",
content: TabSummary,
},
{
Expand Down
44 changes: 44 additions & 0 deletions resources/jscomposition/cases/casesDetail/components/MapLegend.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div
id="map-legend"
class="tw-absolute tw-top-12 tw-right-12 tw-rounded-md tw-shadow-md tw-bg-white tw-z-10"
>
<div class="tw-p-4 tw-flex tw-flex-col">
<p
v-for="(status, index) in statusLegend"
:key="index"
class="tw-font-bold tw-mb-2 tw-flex tw-items-center tw-gap-2"
>
<span
:style="{ backgroundColor: status.color }"
class="tw-inline-block tw-w-1 tw-h-8 tw-mr-4 tw-border-r-3 tw-border-r-gray-400 tw-transform tw-rotate-45"
/>
{{ status.name }}
</p>
</div>
</div>
</template>

<script setup>
import { ref } from "vue";

const translate = ProcessMaker.i18n;

const statusLegend = ref(
[
{
name: translate.t("In Progress"),
color: "#3FA6FF",
},
{
name: translate.t("Completed"),
color: "#00BA7C",
},
{
name: translate.t("Pending / Not Executed"),
color: "#CCCCCC",
},
],
);

</script>
145 changes: 145 additions & 0 deletions resources/jscomposition/cases/casesDetail/components/NewOverview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<template>
<div
class="tw-w-full tw-h-full tw-overflow-hidden tw-relative"
data-test="body-container"
>
<h4 class="tw-fixed tw-z-10 tw-p-4">
{{ processTitle }}
</h4>
<MapLegend />
<ProcessMapTooltip
v-show="showTooltip"
ref="tooltipRef"
:enabled="enableTooltip"
:node-id="tooltip.nodeId"
:node-name="tooltip.nodeName"
:request-id="inflightData.requestId"
:style="{
left: `${tooltip.newX}px`,
top: `${tooltip.newY}px`
}"
@is-loading="isTooltipLoading"
/>
<transition
name="fade"
mode="in-out"
>
<Modeler
ref="modelerRef"
:key="keyModeler"
:decorations="decorations"
:request-completed-nodes="inflightData.requestCompletedNodes"
:request-in-progress-nodes="inflightData.requestInProgressNodes"
:request-idle-nodes="inflightData.requestIdleNodes"
:read-only="true"
@set-xml-manager="xmlManager = $event"
@click="handleClick"
/>
</transition>
</div>
</template>

<script setup>
import {
ref, watchEffect, onMounted, computed, nextTick, onBeforeUnmount,
} from "vue";
import { Modeler } from "@processmaker/modeler";
import ProcessMapTooltip from "../../../../js/processes/modeler/components/ProcessMapTooltip.vue";
import MapLegend from "./MapLegend.vue";
import { getInflightData, getProcessName } from "../variables";

const translate = ProcessMaker.i18n;
const processTitle = ref(`${getProcessName()} ${translate.t("In-Flight Map")}`);
const keyModeler = ref(Math.random());
const modelerRef = ref("");
const tooltipRef = ref(null);
const enableTooltip = ref(true);
const decorations = ref({
borderOutline: {},
});
const xmlManager = ref();
const tooltip = ref({
isActive: false,
isLoading: false,
nodeId: null,
nodeName: null,
allowedNodes: [
"bpmn:Task",
"bpmn:ManualTask",
"bpmn:SequenceFlow",
"bpmn:ScriptTask",
"bpmn:CallActivity",
"bpmn:ServiceTask",
],
coordinates: { x: 0, y: 0 },
newX: 0,
newY: 0,
});
const inflightData = ref(getInflightData());

const isMappingActive = computed(() => (window.ProcessMaker.modeler.enableProcessMapping !== undefined
? window.ProcessMaker.modeler.enableProcessMapping
: true));

const showTooltip = computed(() => enableTooltip.value && tooltip.value.isActive);

const calculateTooltipPosition = () => {
const rectTooltip = tooltipRef.value.$el.getBoundingClientRect();
tooltip.value.newY = tooltip.value.coordinates.y - rectTooltip.height - 20;
if (tooltip.value.newY <= 0) {
tooltip.value.newY = 10;
}
tooltip.value.newX = tooltip.value.coordinates.x - (rectTooltip.width / 2);
if (tooltip.value.newX < 0) {
tooltip.value.newX = 0;
} else if (tooltip.value.newX + rectTooltip.width > window.innerWidth) {
tooltip.value.newX = window.innerWidth - rectTooltip.width;
}
};

const setupTooltip = ({ event, node }) => {
const isNodeTooltipAllowed = tooltip.value.allowedNodes.includes(node.$type);
if ((isNodeTooltipAllowed && !tooltip.value.isActive)
|| (isNodeTooltipAllowed && tooltip.value.nodeId !== node.id)) {
tooltip.value.nodeId = node.id;
tooltip.value.nodeName = node.name;
tooltip.value.isActive = true;
nextTick(() => {
tooltip.value.coordinates = { x: event.clientX, y: event.clientY };
calculateTooltipPosition();
});
} else if (tooltip.value.nodeId === node.id && tooltip.value.isActive) {
tooltip.value.isActive = false;
}
};

const isTooltipLoading = (value) => {
tooltip.value.isLoading = value;
};

const handleClick = (payload) => {
if (isMappingActive.value) {
setupTooltip(payload);
}
};

watchEffect(() => {
if (!tooltip.value.isLoading) {
nextTick(() => {
calculateTooltipPosition();
});
}
});

onMounted(() => {
ProcessMaker.$modeler = modelerRef.value;
});

onBeforeUnmount(() => {
ProcessMaker.$modeler = null;
modelerRef.value.reset({ readOnly: true });
modelerRef.value.reset({ panMode: true });
modelerRef.value = null;
tooltipRef.value = null;
});
</script>
4 changes: 4 additions & 0 deletions resources/jscomposition/cases/casesDetail/variables/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ export const getRequestCount = () => requestCount;
export const getErrors = () => errorLogs;

export const isErrors = () => request.status === "ERROR";

export const getInflightData = () => inflightData;

export const getXML = () => inflightData.bpmn;
Loading