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:
parent
80038e00a3
commit
c4a143bb2d
441
components/process-flow/ExportCanvasModal.vue
Normal file
441
components/process-flow/ExportCanvasModal.vue
Normal 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>
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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
137
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user