Add Export Functionality to Process Flow Canvas

- Introduced a new ExportCanvasModal component for exporting the process flow as PNG or PDF.
- Enhanced ProcessFlowCanvas with exportToPNG and exportToPDF methods, utilizing html2canvas and jsPDF for rendering.
- Updated the process builder page to integrate the export modal, allowing users to initiate exports easily.
- Added UI elements for selecting export format and quality, improving user experience during the export process.
- Implemented error handling and progress indicators for export operations, ensuring better feedback for users.
This commit is contained in:
Md Afiq Iskandar 2025-07-23 08:01:29 +08:00
parent 80038e00a3
commit c4a143bb2d
5 changed files with 926 additions and 0 deletions

View File

@ -0,0 +1,441 @@
<template>
<div v-if="show" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click="closeModal">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4" @click.stop>
<!-- Modal Header -->
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">Export Canvas</h3>
<button
@click="closeModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<span class="material-icons">close</span>
</button>
</div>
</div>
<!-- Modal Body -->
<div class="px-6 py-4 space-y-4">
<!-- Export Format -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Export Format
</label>
<div class="grid grid-cols-2 gap-3">
<button
@click="selectedFormat = 'png'"
:class="[
'flex items-center justify-center px-4 py-3 border rounded-lg transition-all',
selectedFormat === 'png'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 hover:border-gray-400'
]"
>
<span class="material-icons mr-2 text-sm">image</span>
PNG Image
</button>
<button
@click="selectedFormat = 'pdf'"
:class="[
'flex items-center justify-center px-4 py-3 border rounded-lg transition-all',
selectedFormat === 'pdf'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 hover:border-gray-400'
]"
>
<span class="material-icons mr-2 text-sm">picture_as_pdf</span>
PDF Document
</button>
</div>
</div>
<!-- Quality Settings -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Quality
</label>
<select
v-model="selectedQuality"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
>
<option value="1">Low (1x)</option>
<option value="2">Standard (2x)</option>
<option value="3">High (3x)</option>
<option value="4">Ultra (4x)</option>
</select>
</div>
<!-- Include Background -->
<div class="flex items-center">
<input
id="includeBackground"
v-model="includeBackground"
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
>
<label for="includeBackground" class="ml-2 block text-sm text-gray-700">
Include background pattern
</label>
</div>
<!-- File Name -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
File Name
</label>
<input
v-model="fileName"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter file name"
>
</div>
<!-- PDF Options (when PDF is selected) -->
<div v-if="selectedFormat === 'pdf'" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Page Size
</label>
<select
v-model="pdfPageSize"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
>
<option value="a4">A4 (210 × 297 mm)</option>
<option value="a3">A3 (297 × 420 mm)</option>
<option value="letter">Letter (8.5 × 11 in)</option>
<option value="legal">Legal (8.5 × 14 in)</option>
<option value="auto">Auto-fit</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Orientation
</label>
<div class="grid grid-cols-2 gap-3">
<button
@click="pdfOrientation = 'portrait'"
:class="[
'flex items-center justify-center px-3 py-2 border rounded-lg transition-all',
pdfOrientation === 'portrait'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 hover:border-gray-400'
]"
>
<span class="material-icons mr-1 text-sm">stay_current_portrait</span>
Portrait
</button>
<button
@click="pdfOrientation = 'landscape'"
:class="[
'flex items-center justify-center px-3 py-2 border rounded-lg transition-all',
pdfOrientation === 'landscape'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 hover:border-gray-400'
]"
>
<span class="material-icons mr-1 text-sm">stay_current_landscape</span>
Landscape
</button>
</div>
</div>
</div>
<!-- Export Progress -->
<div v-if="isExporting" class="flex flex-col items-center justify-center py-4">
<div class="flex items-center space-x-2 mb-3">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
<span class="text-sm text-gray-600">{{ exportProgress }}</span>
</div>
<button
@click="forceClose"
class="text-xs text-gray-500 hover:text-gray-700 underline"
>
Cancel Export
</button>
</div>
</div>
<!-- Modal Footer -->
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
<button
@click="closeModal"
:disabled="isExporting"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
@click="handleExport"
:disabled="isExporting || !fileName.trim()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
<span class="material-icons mr-1 text-sm">download</span>
Export {{ selectedFormat.toUpperCase() }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// Props
const props = defineProps({
show: {
type: Boolean,
default: false
},
processName: {
type: String,
default: 'process'
}
})
// Emits
const emit = defineEmits(['close', 'export'])
// State
const selectedFormat = ref('png')
const selectedQuality = ref('2')
const includeBackground = ref(true)
const fileName = ref('')
const isExporting = ref(false)
const exportProgress = ref('')
// PDF specific options
const pdfPageSize = ref('a4')
const pdfOrientation = ref('landscape')
// Computed
const exportOptions = computed(() => ({
format: selectedFormat.value,
quality: parseInt(selectedQuality.value),
includeBackground: includeBackground.value,
fileName: fileName.value.trim(),
pdf: {
pageSize: pdfPageSize.value,
orientation: pdfOrientation.value
}
}))
// Watch for prop changes to set default filename
watch(() => props.processName, (newName) => {
if (newName && !fileName.value) {
fileName.value = newName.replace(/[^a-zA-Z0-9]/g, '_')
}
}, { immediate: true })
// Watch format changes to update filename extension
watch(selectedFormat, (newFormat) => {
if (fileName.value) {
const nameWithoutExt = fileName.value.replace(/\.(png|pdf)$/i, '')
fileName.value = `${nameWithoutExt}.${newFormat}`
}
})
// Methods
const closeModal = () => {
if (!isExporting.value) {
resetForm()
emit('close')
}
}
const resetForm = () => {
selectedFormat.value = 'png'
selectedQuality.value = '2'
includeBackground.value = true
fileName.value = ''
isExporting.value = false
exportProgress.value = ''
pdfPageSize.value = 'a4'
pdfOrientation.value = 'landscape'
}
// Force close modal (for error recovery)
const forceClose = () => {
resetForm()
emit('close')
}
const handleExport = async () => {
if (!fileName.value.trim() || isExporting.value) return
try {
isExporting.value = true
exportProgress.value = 'Preparing export...'
// Update progress during export
setTimeout(() => {
if (isExporting.value) {
exportProgress.value = 'Capturing canvas...'
}
}, 500)
setTimeout(() => {
if (isExporting.value) {
exportProgress.value = 'Processing image...'
}
}, 2000)
// Emit export event with options
await emit('export', exportOptions.value)
// Success state - ensure we reset the loading state
exportProgress.value = 'Export completed!'
isExporting.value = false
// Close modal after successful export
setTimeout(() => {
closeModal()
}, 1000)
} catch (error) {
console.error('Export error:', error)
exportProgress.value = 'Export failed - ' + (error.message || 'Unknown error')
isExporting.value = false
setTimeout(() => {
exportProgress.value = ''
}, 3000)
}
}
// Update export progress
const updateProgress = (message) => {
exportProgress.value = message
}
// Expose methods for parent component
defineExpose({
updateProgress,
forceClose,
resetForm
})
</script>
<style scoped>
/* Custom styling for better modal appearance */
.modal-enter-active, .modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from, .modal-leave-to {
opacity: 0;
}
/* Ensure proper z-index for dropdowns */
select {
position: relative;
z-index: 10;
}
/* Enhanced button styles */
button {
transition: all 0.2s ease;
}
button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}
/* Format selection buttons */
.format-button {
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.format-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.format-button:hover::before {
left: 100%;
}
/* Quality select styling */
select:focus {
ring-width: 2px;
ring-color: rgb(59 130 246);
ring-opacity: 0.5;
}
/* Checkbox styling */
input[type="checkbox"]:checked {
background-color: rgb(59 130 246);
border-color: rgb(59 130 246);
}
/* Loading spinner animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Progress indicator styling */
.export-progress {
background: linear-gradient(90deg, #3b82f6, #60a5fa, #3b82f6);
background-size: 200% 100%;
animation: progressShimmer 2s ease-in-out infinite;
}
@keyframes progressShimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Modal backdrop blur effect */
.fixed.inset-0 {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
/* Enhanced modal shadow */
.bg-white.rounded-lg.shadow-xl {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* File input styling */
input[type="text"]:focus {
ring-width: 2px;
ring-color: rgb(59 130 246);
ring-opacity: 0.5;
border-color: rgb(59 130 246);
}
/* Responsive adjustments */
@media (max-width: 640px) {
.bg-white.rounded-lg.shadow-xl {
margin: 1rem;
max-width: calc(100vw - 2rem);
}
.grid.grid-cols-2 {
grid-template-columns: 1fr;
gap: 0.75rem;
}
}
</style>

View File

@ -106,6 +106,7 @@ const {
onEdgesChange,
onConnect,
fitView,
fitBounds,
project,
removeNodes,
removeEdges,
@ -890,6 +891,303 @@ const onDragOver = (event) => {
event.dataTransfer.dropEffect = "copy";
};
// Export functions for canvas export
async function exportToPNG(options = {}) {
try {
const { default: html2canvas } = await import('html2canvas');
// Find Vue Flow elements
const vueFlowContainer = document.querySelector('.vue-flow');
const vueFlowViewport = document.querySelector('.vue-flow__viewport');
if (!vueFlowContainer || !vueFlowViewport) {
throw new Error('Vue Flow elements not found');
}
console.log('Vue Flow elements found:', { container: vueFlowContainer, viewport: vueFlowViewport });
// Wait for any animations or transitions to complete
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current nodes from Vue Flow state
const currentNodes = nodes.value || [];
console.log('Current nodes for export:', currentNodes.length);
if (currentNodes.length === 0) {
throw new Error('No nodes found to export');
}
// Store original viewport
const originalViewport = getViewport();
// Use Vue Flow's fitView to ensure all nodes are visible and centered
fitView({
padding: 0.2,
includeHiddenNodes: false,
minZoom: 0.5,
maxZoom: 2
});
// Wait for the view to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Use html2canvas to capture the entire Vue Flow container
const html2canvasOptions = {
backgroundColor: options.includeBackground ? '#f8fafc' : 'white',
scale: options.quality || 2,
useCORS: true,
allowTaint: false,
foreignObjectRendering: true,
imageTimeout: 20000,
logging: false,
removeContainer: false,
// Capture the entire container instead of just viewport
width: vueFlowContainer.offsetWidth,
height: vueFlowContainer.offsetHeight,
x: 0,
y: 0,
scrollX: 0,
scrollY: 0,
windowWidth: vueFlowContainer.offsetWidth,
windowHeight: vueFlowContainer.offsetHeight,
onclone: function(clonedDoc) {
const clonedContainer = clonedDoc.querySelector('.vue-flow');
const clonedViewport = clonedDoc.querySelector('.vue-flow__viewport');
if (clonedContainer && clonedViewport) {
// Ensure container styles don't interfere with capture
clonedContainer.style.overflow = 'visible';
clonedContainer.style.position = 'relative';
clonedViewport.style.overflow = 'visible';
// Make sure all content is visible in the cloned document
const allElements = clonedContainer.querySelectorAll('*');
allElements.forEach(el => {
// Force visibility of all elements
if (el.style.display === 'none') {
el.style.display = 'block';
}
if (el.style.visibility === 'hidden') {
el.style.visibility = 'visible';
}
if (el.style.opacity === '0' || el.style.opacity === '') {
el.style.opacity = '1';
}
// Remove any transforms that might hide content
if (el.style.transform && el.style.transform.includes('scale(0)')) {
el.style.transform = el.style.transform.replace(/scale\([0-9.]*\)/, 'scale(1)');
}
});
console.log('Cloned document prepared, elements made visible');
}
},
ignoreElements: function(element) {
// Only ignore UI controls, not the actual process content
return element.classList.contains('vue-flow__controls') ||
element.classList.contains('vue-flow__minimap') ||
(element.classList.contains('vue-flow__background') && !options.includeBackground);
}
};
console.log('html2canvas options:', html2canvasOptions);
// Capture the Vue Flow container
const canvas = await html2canvas(vueFlowContainer, html2canvasOptions);
console.log('Canvas captured successfully:', canvas.width, 'x', canvas.height);
// Restore original viewport
setViewport(originalViewport);
// Convert to blob and download
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('Failed to create image blob'));
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = options.fileName || 'process-flow.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
resolve();
}, 'image/png', 0.95);
});
} catch (error) {
console.error('Error exporting to PNG:', error);
throw error;
}
}
async function exportToPDF(options = {}) {
try {
// Use the same improved capture logic as PNG export
const { default: html2canvas } = await import('html2canvas');
const vueFlowContainer = document.querySelector('.vue-flow');
const vueFlowViewport = document.querySelector('.vue-flow__viewport');
if (!vueFlowContainer || !vueFlowViewport) {
throw new Error('Vue Flow elements not found');
}
console.log('Vue Flow elements found for PDF:', { container: vueFlowContainer, viewport: vueFlowViewport });
// Wait for any animations or transitions to complete
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current nodes from Vue Flow state
const currentNodes = nodes.value || [];
console.log('Current nodes for PDF export:', currentNodes.length);
if (currentNodes.length === 0) {
throw new Error('No nodes found to export');
}
// Store original viewport
const originalViewport = getViewport();
// Use Vue Flow's fitView to ensure all nodes are visible and centered
fitView({
padding: 0.2,
includeHiddenNodes: false,
minZoom: 0.5,
maxZoom: 2
});
// Wait for the view to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Use html2canvas to capture the entire viewport
const html2canvasOptions = {
backgroundColor: options.includeBackground ? '#f8fafc' : '#ffffff',
scale: options.quality || 2,
useCORS: true,
allowTaint: false,
foreignObjectRendering: true,
imageTimeout: 20000,
logging: false,
removeContainer: false,
// Capture the entire viewport
width: vueFlowViewport.offsetWidth,
height: vueFlowViewport.offsetHeight,
x: 0,
y: 0,
scrollX: 0,
scrollY: 0,
windowWidth: vueFlowViewport.offsetWidth,
windowHeight: vueFlowViewport.offsetHeight,
onclone: function(clonedDoc) {
const clonedContainer = clonedDoc.querySelector('.vue-flow');
const clonedViewport = clonedDoc.querySelector('.vue-flow__viewport');
if (clonedContainer && clonedViewport) {
// Ensure container styles don't interfere with capture
clonedContainer.style.overflow = 'visible';
clonedContainer.style.position = 'relative';
clonedViewport.style.overflow = 'visible';
// Make sure all content is visible in the cloned document
const allElements = clonedContainer.querySelectorAll('*');
allElements.forEach(el => {
// Force visibility of all elements
if (el.style.display === 'none') {
el.style.display = 'block';
}
if (el.style.visibility === 'hidden') {
el.style.visibility = 'visible';
}
if (el.style.opacity === '0' || el.style.opacity === '') {
el.style.opacity = '1';
}
// Remove any transforms that might hide content
if (el.style.transform && el.style.transform.includes('scale(0)')) {
el.style.transform = el.style.transform.replace(/scale\([0-9.]*\)/, 'scale(1)');
}
});
}
},
ignoreElements: function(element) {
// Only ignore UI controls, not the actual process content
return element.classList.contains('vue-flow__controls') ||
element.classList.contains('vue-flow__minimap') ||
(element.classList.contains('vue-flow__background') && !options.includeBackground);
}
};
// Capture the Vue Flow container
const canvas = await html2canvas(vueFlowContainer, html2canvasOptions);
// Restore original viewport
setViewport(originalViewport);
// Now create PDF
const { jsPDF } = await import('jspdf');
const imgData = canvas.toDataURL('image/png');
console.log('PDF Canvas captured:', canvas.width, 'x', canvas.height);
// Calculate dimensions
const imgWidth = canvas.width;
const imgHeight = canvas.height;
// PDF page configurations
const pageConfigs = {
a4: { width: 210, height: 297 },
a3: { width: 297, height: 420 },
letter: { width: 216, height: 279 },
legal: { width: 216, height: 356 }
};
let pageConfig = pageConfigs[options.pdf?.pageSize] || pageConfigs.a4;
const orientation = options.pdf?.orientation || 'landscape';
if (orientation === 'landscape') {
[pageConfig.width, pageConfig.height] = [pageConfig.height, pageConfig.width];
}
// Create PDF
const pdf = new jsPDF({
orientation: orientation,
unit: 'mm',
format: options.pdf?.pageSize === 'auto' ? [imgWidth * 0.26458, imgHeight * 0.26458] : [pageConfig.width, pageConfig.height]
});
if (options.pdf?.pageSize === 'auto') {
// Auto-fit: scale image to fit exactly
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth * 0.26458, imgHeight * 0.26458);
} else {
// Fixed page size: scale to fit within margins
const margin = 10;
const maxWidth = pageConfig.width - (margin * 2);
const maxHeight = pageConfig.height - (margin * 2);
const scaleFactor = Math.min(maxWidth / (imgWidth * 0.26458), maxHeight / (imgHeight * 0.26458));
const scaledWidth = (imgWidth * 0.26458) * scaleFactor;
const scaledHeight = (imgHeight * 0.26458) * scaleFactor;
const x = (pageConfig.width - scaledWidth) / 2;
const y = (pageConfig.height - scaledHeight) / 2;
pdf.addImage(imgData, 'PNG', x, y, scaledWidth, scaledHeight);
}
// Save the PDF
pdf.save(options.fileName || 'process-flow.pdf');
} catch (error) {
console.error('Error exporting to PDF:', error);
throw error;
}
}
// Define methods to expose to parent components
defineExpose({
updateNode,
@ -901,6 +1199,9 @@ defineExpose({
// Add Vue Flow save/restore methods
toObject,
fromObject,
// Add export methods
exportToPNG,
exportToPDF,
// Add direct access to Vue Flow methods for production fallbacks
setNodes: (newNodes) => {
try {

View File

@ -60,7 +60,9 @@
"cross-env": "^7.0.3",
"crypto-js": "^4.1.1",
"floating-vue": "^2.0.0-beta.24",
"html2canvas": "^1.4.1",
"jsonwebtoken": "^8.5.1",
"jspdf": "^3.0.1",
"luxon": "^3.1.0",
"maska": "^1.5.0",
"pinia": "^2.1.6",

View File

@ -23,6 +23,7 @@ import SubprocessNodeConfigurationModal from '~/components/process-flow/Subproce
import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue';
import ExportCanvasModal from '~/components/process-flow/ExportCanvasModal.vue';
// Define page meta
definePageMeta({
@ -94,6 +95,7 @@ const showTemplatesModal = ref(false);
const showProcessSettings = ref(false);
const showDropdown = ref(false);
const showProcessHistoryModal = ref(false);
const showExportModal = ref(false);
// Add mobile responsive state
const showLeftPanel = ref(true);
@ -1830,6 +1832,29 @@ const handleProcessRestored = (restoredProcess) => {
toast.success('Process has been restored successfully');
};
// Handle canvas export
const handleExportCanvas = async (options) => {
try {
if (!processFlowCanvas.value) {
throw new Error('Canvas not available for export');
}
console.log('Starting export with options:', options);
if (options.format === 'png') {
await processFlowCanvas.value.exportToPNG(options);
toast.success('Canvas exported as PNG successfully');
} else if (options.format === 'pdf') {
await processFlowCanvas.value.exportToPDF(options);
toast.success('Canvas exported as PDF successfully');
}
} catch (error) {
console.error('Export error:', error);
toast.error(`Export failed: ${error.message}`);
throw error;
}
};
// Navigate to variables page
const navigateToVariables = () => {
confirmNavigation('/variables');
@ -2324,6 +2349,14 @@ const sendToBack = () => {
<span class="hidden lg:inline">History</span>
</RsButton>
</div>
<!-- Export button - Hidden on mobile -->
<div class="mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2 hidden md:block">
<RsButton @click="showExportModal = true" variant="secondary" size="sm" :disabled="!hasCurrentProcess">
<Icon name="material-symbols:download" class="mr-0 md:mr-1" />
<span class="hidden lg:inline">Export</span>
</RsButton>
</div>
<!-- Secondary actions -->
<div class="flex items-center">
@ -2343,6 +2376,10 @@ const sendToBack = () => {
<Icon name="material-symbols:history" class="mr-2 w-4 h-4" />
<span>History</span>
</button>
<button @click="showExportModal = true; showDropdown = false" :disabled="!hasCurrentProcess" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center disabled:opacity-50">
<Icon name="material-symbols:download" class="mr-2 w-4 h-4" />
<span>Export</span>
</button>
</div>
<button @click="showProcessSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
@ -3001,6 +3038,14 @@ const sendToBack = () => {
@close="showProcessHistoryModal = false"
@restored="handleProcessRestored"
/>
<!-- Export Canvas Modal -->
<ExportCanvasModal
:show="showExportModal"
:process-name="processStore.currentProcess?.name || 'process'"
@close="showExportModal = false"
@export="handleExportCanvas"
/>
</div>
</client-only>
</div>

137
yarn.lock
View File

@ -969,6 +969,11 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.26.7":
version "7.27.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6"
integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
"@babel/standalone@^7.22.9":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.22.10.tgz#0a39a85488d61d301751cc074ea77c44aacb9d07"
@ -2303,6 +2308,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/raf@^3.4.0":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
"@types/resize-observer-browser@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3"
@ -2330,6 +2340,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
"@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/web-bluetooth@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
@ -3075,6 +3090,11 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
@ -3136,6 +3156,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -3211,6 +3236,11 @@ browserslist@^4.0.0, browserslist@^4.21.4, browserslist@^4.21.5, browserslist@^4
node-releases "^2.0.13"
update-browserslist-db "^1.0.11"
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@ -3314,6 +3344,20 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601"
integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==
canvg@^3.0.11:
version "3.0.11"
resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b"
integrity sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==
dependencies:
"@babel/runtime" "^7.12.5"
"@types/raf" "^3.4.0"
core-js "^3.8.3"
raf "^3.4.1"
regenerator-runtime "^0.13.7"
rgbcolor "^1.0.1"
stackblur-canvas "^2.0.0"
svg-pathdata "^6.0.3"
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -3573,6 +3617,11 @@ core-js-pure@^3.30.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.32.0.tgz#5d79f85da7a4373e9a06494ccbef995a4c639f8b"
integrity sha512-qsev1H+dTNYpDUEURRuOXMvpdtAnNEvQWS/FMJ2Vb5AY8ZP4rAPQldkE27joykZPJTe0+IVgHZYh1P5Xu1/i1g==
core-js@^3.6.0, core-js@^3.8.3:
version "3.44.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.44.0.tgz#db4fd4fa07933c1d6898c8b112a1119a9336e959"
integrity sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@ -3650,6 +3699,13 @@ css-declaration-sorter@^6.3.1:
resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"
integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
css-loader@^5.0.0:
version "5.2.7"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae"
@ -4125,6 +4181,13 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@^3.2.4:
version "3.2.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
@ -4849,6 +4912,11 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
fflate@^0.8.1:
version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -5337,6 +5405,14 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
http-assert@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f"
@ -5884,6 +5960,21 @@ jsonwebtoken@^8.5.1:
ms "^2.1.1"
semver "^5.6.0"
jspdf@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-3.0.1.tgz#d81e1964f354f60412516eb2449ea2cccd4d2a3b"
integrity sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==
dependencies:
"@babel/runtime" "^7.26.7"
atob "^2.1.2"
btoa "^1.2.1"
fflate "^0.8.1"
optionalDependencies:
canvg "^3.0.11"
core-js "^3.6.0"
dompurify "^3.2.4"
html2canvas "^1.0.0-rc.5"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2:
version "3.3.5"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
@ -7138,6 +7229,11 @@ perfect-scrollbar@^1.5.5:
resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz#41a211a2fb52a7191eff301432134ea47052b27f"
integrity sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
@ -7897,6 +7993,13 @@ radix3@^1.0.1:
resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.0.1.tgz#de0ac16234f8a63288645854a54fc26e45a4a8eb"
integrity sha512-y+AcwZ3HcUIGc9zGsNVf5+BY/LxL+z+4h4J3/pp8jxSmy1STaCocPS3qrj4tA5ehUSzqtqK+0Aygvz/r/8vy4g==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -7990,6 +8093,11 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.13.7:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
@ -8105,6 +8213,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rgbcolor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -8393,6 +8506,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
stackblur-canvas@^2.0.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
@ -8593,6 +8711,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svg-pathdata@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
svg-tags@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
@ -8791,6 +8914,13 @@ terser@^5.0.0, terser@^5.17.4:
commander "^2.20.0"
source-map-support "~0.5.20"
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -9159,6 +9289,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"