Add Process Journey Timeline Component

- Introduced a new component, ProcessJourneyTimeline.vue, to visualize the journey of processes with interactive nodes and metrics.
- Implemented props for journey data, display options for metrics, issues, and branches, and support for interactive node selection.
- Enhanced user experience with dynamic styling based on node types and dropoff rates, providing visual feedback on process performance.
- Added helper functions for formatting durations, calculating completion rates, and managing node interactions, ensuring a comprehensive overview of process journeys.
- Updated styles for improved layout and responsiveness, aligning with existing design principles in the application.
This commit is contained in:
Md Afiq Iskandar 2025-07-11 15:15:06 +08:00
parent 35a0bd412e
commit ba08d2e466
2 changed files with 117 additions and 25 deletions

View File

@ -1224,7 +1224,6 @@ function fromObject(flowObject) {
/* Node styles from ProcessFlowNodes.js are imported globally in a plugin */
.process-flow-container {
width: 100%;
height: calc(100vh - 190px); /* Adjust based on new header/footer height */
min-height: 500px;
border: 1px solid #e2e8f0;
border-radius: 0;

View File

@ -121,11 +121,8 @@ const checkScreenSize = () => {
showRightPanel.value = true;
}
} else {
// Desktop: show both panels by default
if (!showLeftPanel.value && !showRightPanel.value) {
showLeftPanel.value = true;
showRightPanel.value = true;
}
// Desktop: show both panels by default only on first load
// Don't override user preferences on screen size changes
}
};
@ -148,6 +145,13 @@ const toggleRightPanel = () => {
}
};
// Toggle both panels at once
const toggleBothPanels = () => {
const bothVisible = showLeftPanel.value && showRightPanel.value;
showLeftPanel.value = !bothVisible;
showRightPanel.value = !bothVisible;
};
// Close panels when clicking on canvas (mobile only)
const onPaneClickMobile = () => {
selectedNode.value = null;
@ -162,6 +166,32 @@ const onPaneClickMobile = () => {
}
};
// Keyboard shortcuts for panel toggles
const handleKeyboardShortcuts = (event) => {
// Only handle shortcuts when no input is focused
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return;
}
// Ctrl/Cmd + 1: Toggle left panel
if ((event.ctrlKey || event.metaKey) && event.key === '1') {
event.preventDefault();
toggleLeftPanel();
}
// Ctrl/Cmd + 2: Toggle right panel
if ((event.ctrlKey || event.metaKey) && event.key === '2') {
event.preventDefault();
toggleRightPanel();
}
// Ctrl/Cmd + 3: Toggle both panels
if ((event.ctrlKey || event.metaKey) && event.key === '3') {
event.preventDefault();
toggleBothPanels();
}
};
// Handle node highlighting from variable usage
const handleNodeHighlight = (event) => {
@ -1111,6 +1141,9 @@ onMounted(() => {
// Add node highlight listener for variable navigation
window.addEventListener('highlightNode', handleNodeHighlight);
// Add keyboard shortcuts listener
window.addEventListener('keydown', handleKeyboardShortcuts);
// Initial screen size check
checkScreenSize();
});
@ -1128,6 +1161,7 @@ onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
window.removeEventListener('resize', checkScreenSize);
window.removeEventListener('highlightNode', handleNodeHighlight);
window.removeEventListener('keydown', handleKeyboardShortcuts);
// Clear highlight timeout if it exists
if (highlightTimeout.value) {
@ -2107,31 +2141,57 @@ const sendToBack = () => {
<p class="text-sm text-gray-500">Create business processes with drag and drop</p>
</div>
</div>
<!-- Mobile panel toggles -->
<div v-if="hasCurrentProcess && (isMobile || isTablet)" class="flex items-center gap-1 ml-2">
</div>
<!-- Middle section - Process name and Panel Controls -->
<div class="flex-1 flex justify-center items-center mx-2 md:mx-4 gap-4">
<!-- Panel toggle controls - Always visible when process is loaded -->
<div v-if="hasCurrentProcess" class="flex items-center gap-1">
<RsButton
@click="toggleLeftPanel"
variant="tertiary"
size="sm"
:class="{ 'bg-gray-100': showLeftPanel }"
class="p-1"
:class="{
'bg-blue-100 text-blue-600 border-blue-200': showLeftPanel,
'bg-gray-100 text-gray-600 border-gray-200': !showLeftPanel
}"
class="p-2 border transition-all duration-200 hover:scale-105 relative"
title="Toggle Components Panel (Ctrl+1)"
>
<Icon name="material-symbols:widgets" class="w-4 h-4" />
<span v-if="!showLeftPanel" class="absolute -top-1 -right-1 w-2 h-2 bg-orange-400 rounded-full"></span>
</RsButton>
<RsButton
@click="toggleRightPanel"
variant="tertiary"
size="sm"
:class="{ 'bg-gray-100': showRightPanel }"
class="p-1"
:class="{
'bg-blue-100 text-blue-600 border-blue-200': showRightPanel,
'bg-gray-100 text-gray-600 border-gray-200': !showRightPanel
}"
class="p-2 border transition-all duration-200 hover:scale-105 relative"
title="Toggle Properties Panel (Ctrl+2)"
>
<Icon name="material-symbols:tune" class="w-4 h-4" />
<span v-if="!showRightPanel" class="absolute -top-1 -right-1 w-2 h-2 bg-orange-400 rounded-full"></span>
</RsButton>
<RsButton
@click="toggleBothPanels"
variant="tertiary"
size="sm"
:class="{
'bg-blue-100 text-blue-600 border-blue-200': showLeftPanel && showRightPanel,
'bg-gray-100 text-gray-600 border-gray-200': !(showLeftPanel && showRightPanel)
}"
class="p-2 hidden md:block border transition-all duration-200 hover:scale-105 relative"
title="Toggle Both Panels (Ctrl+3)"
>
<Icon name="material-symbols:view-sidebar" class="w-4 h-4" />
<span v-if="!showLeftPanel || !showRightPanel" class="absolute -top-1 -right-1 w-2 h-2 bg-orange-400 rounded-full"></span>
</RsButton>
</div>
</div>
<!-- Middle section - Process name -->
<div class="flex-1 flex justify-center items-center mx-2 md:mx-4">
<!-- Process name input -->
<FormKit
v-if="hasCurrentProcess"
v-model="processStore.currentProcess.name"
@ -2209,14 +2269,14 @@ const sendToBack = () => {
</header>
<!-- Main Content Area -->
<div class="flex-1 flex overflow-hidden relative" v-if="hasCurrentProcess">
<div class="flex-1 flex overflow-hidden relative h-full" v-if="hasCurrentProcess">
<!-- Left Panel - Components -->
<div
v-show="showLeftPanel"
:class="{
'absolute inset-y-0 left-0 z-20 bg-white shadow-lg': isMobile,
'absolute inset-y-0 left-0 z-10 bg-white shadow-md': isTablet,
'relative w-64': !isMobile && !isTablet,
'relative w-60': !isMobile && !isTablet,
'w-72': isMobile,
'w-80': isTablet
}"
@ -2224,11 +2284,11 @@ const sendToBack = () => {
>
<div class="bg-gray-100 p-3 flex items-center justify-between border-b border-gray-200">
<h2 class="text-sm font-medium text-gray-700">Process Components</h2>
<!-- Close button for mobile/tablet -->
<!-- Close button for all devices -->
<button
v-if="isMobile || isTablet"
@click="showLeftPanel = false"
class="p-1 hover:bg-gray-200 rounded"
class="p-1 hover:bg-gray-200 rounded transition-colors"
title="Close Panel (Ctrl+1)"
>
<Icon name="material-symbols:close" class="w-4 h-4 text-gray-500" />
</button>
@ -2239,7 +2299,16 @@ const sendToBack = () => {
</div>
<!-- Center Panel - Process Canvas -->
<div class="flex-1 relative">
<div class="flex-1 relative h-full" :class="{ 'bg-gray-50': !showLeftPanel && !showRightPanel }">
<!-- Canvas State Indicator -->
<div v-if="!showLeftPanel && !showRightPanel" class="absolute top-4 left-1/2 transform -translate-x-1/2 z-10 bg-white px-4 py-2 rounded-lg shadow-md border border-gray-200">
<div class="flex items-center gap-2 text-sm text-gray-600">
<Icon name="material-symbols:fullscreen" class="w-4 h-4 text-green-600" />
<span>Full Canvas Mode</span>
<span class="text-xs text-gray-400">Press Ctrl+3 to restore panels</span>
</div>
</div>
<ProcessFlowCanvas
ref="processFlowCanvas"
:initial-nodes="canvasNodes"
@ -2250,6 +2319,7 @@ const sendToBack = () => {
@pane-click="onPaneClick"
@nodes-change="onNodesChange"
@edges-change="onEdgesChange"
class="w-full h-full"
/>
<!-- Mobile floating action buttons -->
@ -2279,7 +2349,7 @@ const sendToBack = () => {
:class="{
'absolute inset-y-0 right-0 z-20 bg-white shadow-lg': isMobile,
'absolute inset-y-0 right-0 z-10 bg-white shadow-md': isTablet,
'relative w-72': !isMobile && !isTablet,
'relative w-64': !isMobile && !isTablet,
'w-72': isMobile,
'w-80': isTablet
}"
@ -2287,11 +2357,11 @@ const sendToBack = () => {
>
<div class="bg-gray-100 p-3 flex items-center justify-between border-b border-gray-200">
<h2 class="text-sm font-medium text-gray-700">Properties</h2>
<!-- Close button for mobile/tablet -->
<!-- Close button for all devices -->
<button
v-if="isMobile || isTablet"
@click="showRightPanel = false"
class="p-1 hover:bg-gray-200 rounded"
class="p-1 hover:bg-gray-200 rounded transition-colors"
title="Close Panel (Ctrl+2)"
>
<Icon name="material-symbols:close" class="w-4 h-4 text-gray-500" />
</button>
@ -3825,4 +3895,27 @@ const sendToBack = () => {
.modern-icon-btn[variant~='danger-text']:hover {
background: #fee2e2;
}
/* Ensure full canvas coverage */
.process-builder {
height: 100vh;
overflow: hidden;
}
/* Override Vue Flow container to fill full height */
:deep(.vue-flow) {
width: 100% !important;
height: 100% !important;
min-height: 100% !important;
}
:deep(.vue-flow__pane) {
width: 100% !important;
height: 100% !important;
}
:deep(.vue-flow__renderer) {
width: 100% !important;
height: 100% !important;
}
</style>