diff --git a/components/process-flow/ExportCanvasModal.vue b/components/process-flow/ExportCanvasModal.vue new file mode 100644 index 0000000..f0cca2f --- /dev/null +++ b/components/process-flow/ExportCanvasModal.vue @@ -0,0 +1,441 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 3aeb1fd..0a89178 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -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 { diff --git a/package.json b/package.json index b5d3914..824cc57 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 8f0a469..976483e 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -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 = () => { + + +
@@ -2343,6 +2376,10 @@ const sendToBack = () => { History +