From 8431f52d971fd50b9ee0f95d7e2095ddf73fbe87 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 18 Aug 2025 13:36:23 -0400 Subject: [PATCH] added url comrpession --- package-lock.json | 11 ++- package.json | 11 ++- src/App.jsx | 19 +++- src/components/ShareModal.jsx | 28 +++++- src/utils/urlSharing.js | 180 +++++++++++++++++++++++----------- 5 files changed, 180 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f18873..7e42936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@uiw/codemirror-theme-vscode": "^4.24.2", "@uiw/react-codemirror": "^4.24.2", "@xyflow/react": "^12.8.1", + "lz-string": "^1.5.0", "plotly.js": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -4149,6 +4150,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/map-limit": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", @@ -5773,4 +5782,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index f13d141..33acbed 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,15 @@ "start:both": "concurrently \"npm run dev\" \"npm run start:backend\"" }, "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@uiw/codemirror-theme-vscode": "^4.24.2", + "@uiw/react-codemirror": "^4.24.2", "@xyflow/react": "^12.8.1", + "lz-string": "^1.5.0", "plotly.js": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-plotly.js": "^2.6.0", - "@uiw/react-codemirror": "^4.24.2", - "@uiw/codemirror-theme-vscode": "^4.24.2", - "@codemirror/lang-python": "^6.2.1" + "react-plotly.js": "^2.6.0" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -33,4 +34,4 @@ "globals": "^16.0.0", "vite": "^6.3.5" } -} \ No newline at end of file +} diff --git a/src/App.jsx b/src/App.jsx index a286e9b..af31d6a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -110,6 +110,7 @@ const DnDFlow = () => { const [shareUrlFeedback, setShareUrlFeedback] = useState(''); const [showShareModal, setShowShareModal] = useState(false); const [shareableURL, setShareableURL] = useState(''); + const [urlMetadata, setUrlMetadata] = useState(null); // Load graph data from URL on component mount useEffect(() => { @@ -581,12 +582,19 @@ const DnDFlow = () => { }; try { - const url = generateShareableURL(graphData); - if (url) { - setShareableURL(url); + const urlResult = generateShareableURL(graphData); + if (urlResult) { + setShareableURL(urlResult.url); + setUrlMetadata({ + length: urlResult.length, + isSafe: urlResult.isSafe, + maxLength: urlResult.maxLength + }); setShowShareModal(true); - // Update browser URL as well - updateURLWithGraphData(graphData, true); + // Only update browser URL if it's safe length + if (urlResult.isSafe) { + updateURLWithGraphData(graphData, true); + } } else { setShareUrlFeedback('Error generating share URL'); setTimeout(() => setShareUrlFeedback(''), 3000); @@ -1219,6 +1227,7 @@ const DnDFlow = () => { isOpen={showShareModal} onClose={() => setShowShareModal(false)} shareableURL={shareableURL} + urlMetadata={urlMetadata} /> diff --git a/src/components/ShareModal.jsx b/src/components/ShareModal.jsx index 1009649..f1f3422 100644 --- a/src/components/ShareModal.jsx +++ b/src/components/ShareModal.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -const ShareModal = ({ isOpen, onClose, shareableURL }) => { +const ShareModal = ({ isOpen, onClose, shareableURL, urlMetadata }) => { const [copyFeedback, setCopyFeedback] = useState(''); const handleCopy = async () => { @@ -35,6 +35,8 @@ const ShareModal = ({ isOpen, onClose, shareableURL }) => { if (!isOpen) return null; + const isLongURL = urlMetadata && !urlMetadata.isSafe; + return (
{ {/* Header */}
- + 🔗 +

+ Share Your Patterns +

Copy this URL to share your workflow with others.

+ + {/* URL Length Warning */} + {isLongURL && ( +
+
+ ⚠️ Large URL Warning +
+
+ This URL is {urlMetadata?.length || 0} characters long. Some servers may reject URLs longer than {urlMetadata?.maxLength || 4000} characters. Consider using the "Save File" option for complex graphs. +
+
+ )}
{/* URL input and copy button */} diff --git a/src/utils/urlSharing.js b/src/utils/urlSharing.js index 2195d09..9fc4c4b 100644 --- a/src/utils/urlSharing.js +++ b/src/utils/urlSharing.js @@ -2,40 +2,52 @@ * URL sharing utilities for PathView * Handles encoding and decoding graph data in URLs */ +import { compressToBase64, decompressFromBase64 } from 'lz-string'; + +// Maximum safe URL length for most servers (conservative estimate) +const MAX_SAFE_URL_LENGTH = 4000; /** - * Encode graph data to a base64 URL parameter + * Encode graph data to a compressed base64 URL parameter * @param {Object} graphData - The complete graph data object - * @returns {string} - Base64 encoded string + * @returns {string} - Compressed base64 encoded string */ export function encodeGraphData(graphData) { - try { - const jsonString = JSON.stringify(graphData); - // Use btoa for base64 encoding, but handle Unicode strings properly - const utf8Bytes = new TextEncoder().encode(jsonString); - const binaryString = Array.from(utf8Bytes, byte => String.fromCharCode(byte)).join(''); - return btoa(binaryString); - } catch (error) { - console.error('Error encoding graph data:', error); - return null; - } -} - -/** - * Decode graph data from a base64 URL parameter - * @param {string} encodedData - Base64 encoded graph data + try { + const jsonString = JSON.stringify(graphData); + // Use lz-string for much better compression than manual whitespace removal + return compressToBase64(jsonString); + } catch (error) { + console.error('Error encoding graph data:', error); + return null; + } +}/** + * Decode graph data from a compressed base64 URL parameter + * @param {string} encodedData - Compressed base64 encoded graph data * @returns {Object|null} - Decoded graph data object or null if error */ export function decodeGraphData(encodedData) { try { - // Decode base64 and handle Unicode properly - const binaryString = atob(encodedData); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); + // First try lz-string decompression (new format) + const jsonString = decompressFromBase64(encodedData); + if (jsonString) { + return JSON.parse(jsonString); + } + + // Fallback for old format (manual base64 encoding) + try { + const binaryString = atob(encodedData); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const oldJsonString = new TextDecoder().decode(bytes); + return JSON.parse(oldJsonString); + } catch (oldFormatError) { + console.warn('Could not decode with old format either:', oldFormatError); } - const jsonString = new TextDecoder().decode(bytes); - return JSON.parse(jsonString); + + return null; } catch (error) { console.error('Error decoding graph data:', error); return null; @@ -45,27 +57,32 @@ export function decodeGraphData(encodedData) { /** * Generate a shareable URL with the current graph data * @param {Object} graphData - The complete graph data object - * @returns {string} - Complete shareable URL + * @returns {Object} - Object with url and metadata about the URL */ export function generateShareableURL(graphData) { - try { - const encodedData = encodeGraphData(graphData); - if (!encodedData) { - throw new Error('Failed to encode graph data'); - } - - const baseURL = window.location.origin + window.location.pathname; - const url = new URL(baseURL); - url.searchParams.set('graph', encodedData); - - return url.toString(); - } catch (error) { - console.error('Error generating shareable URL:', error); - return null; + try { + const encodedData = encodeGraphData(graphData); + if (!encodedData) { + throw new Error('Failed to encode graph data'); } -} - -/** + + const baseURL = window.location.origin + window.location.pathname; + const url = new URL(baseURL); + url.searchParams.set('graph', encodedData); + + const finalURL = url.toString(); + + return { + url: finalURL, + length: finalURL.length, + isSafe: finalURL.length <= MAX_SAFE_URL_LENGTH, + maxLength: MAX_SAFE_URL_LENGTH + }; + } catch (error) { + console.error('Error generating shareable URL:', error); + return null; + } +}/** * Extract graph data from current URL parameters * @returns {Object|null} - Graph data object or null if not found/error */ @@ -91,21 +108,21 @@ export function getGraphDataFromURL() { * @param {boolean} replaceState - Whether to replace current history state (default: false) */ export function updateURLWithGraphData(graphData, replaceState = false) { - try { - const shareableURL = generateShareableURL(graphData); - if (shareableURL) { - if (replaceState) { - window.history.replaceState({}, '', shareableURL); - } else { - window.history.pushState({}, '', shareableURL); - } - } - } catch (error) { - console.error('Error updating URL with graph data:', error); + try { + const urlResult = generateShareableURL(graphData); + if (urlResult && urlResult.isSafe) { + if (replaceState) { + window.history.replaceState({}, '', urlResult.url); + } else { + window.history.pushState({}, '', urlResult.url); + } + } else if (urlResult) { + console.warn(`URL too long (${urlResult.length} chars), not updating browser URL`); } -} - -/** + } catch (error) { + console.error('Error updating URL with graph data:', error); + } +}/** * Clear graph data from URL without page reload */ export function clearGraphDataFromURL() { @@ -116,3 +133,54 @@ export function clearGraphDataFromURL() { console.error('Error clearing graph data from URL:', error); } } + +/** + * Copy shareable URL to clipboard + * @param {Object} graphData - The complete graph data object + * @returns {Promise} - Result object with success status and metadata + */ +export async function copyShareableURLToClipboard(graphData) { + try { + const urlResult = generateShareableURL(graphData); + if (!urlResult) { + throw new Error('Failed to generate shareable URL'); + } + + await navigator.clipboard.writeText(urlResult.url); + return { + success: true, + isSafe: urlResult.isSafe, + length: urlResult.length, + maxLength: urlResult.maxLength, + url: urlResult.url + }; + } catch (error) { + console.error('Error copying to clipboard:', error); + // Fallback for older browsers + try { + const urlResult = generateShareableURL(graphData); + const textArea = document.createElement('textarea'); + textArea.value = urlResult.url; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + return { + success: true, + isSafe: urlResult.isSafe, + length: urlResult.length, + maxLength: urlResult.maxLength, + url: urlResult.url + }; + } catch (fallbackError) { + console.error('Clipboard fallback failed:', fallbackError); + return { + success: false, + isSafe: false, + length: 0, + maxLength: MAX_SAFE_URL_LENGTH, + url: null + }; + } + } +}