From 18cbbd8a2284f73115e1ae1320222fd9412721b4 Mon Sep 17 00:00:00 2001 From: Oana-Lavinia Florean Date: Wed, 12 Jun 2024 09:25:29 +0300 Subject: [PATCH 1/2] Saving imported draw.io file fails #267 * WIP: this is a version that works partially, there are still cases that need to be handled * it is not working if you import on a empty diagram multiple pages, the first page is lost --- .../resources/Diagram/DiagramEditSheet.xml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml b/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml index 57c41d3..e158a13 100644 --- a/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml +++ b/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml @@ -218,18 +218,23 @@ define('diagram-store', ['jquery', 'xwiki-meta', 'xwiki-utils', 'diagram-utils', return this.input.val(); }, setData: function(data) { - this.input.val(data); let pages = this.ui.getPagesForXml(data); if (pages.length > 0) { - this.ui.pages = this.ui.getPagesForXml(data); + this.ui.pages = pages; } + this.input.val(data); + }, + isCompressed: function() { + return false; + }, + createData: function() { + var result = this.ui.getFileData(null, null, null, null, + null, null, null, null, this, !this.isCompressed(), true); + return result; }, open: function() { var graphXML = this.getData() || '<mxGraphModel/>'; - var graphNode = mxUtils.parseXml(graphXML).documentElement; - graphNode.fromStorage = true; - this.ui.editor.setGraphXml(graphNode); - this.ui.currentPage = this.ui.pages[0]; + this.ui.setFileData(graphXML); this.changeListener = mxUtils.bind(this, function(sender, eventObject) { this.setModified(true); }); @@ -249,7 +254,7 @@ define('diagram-store', ['jquery', 'xwiki-meta', 'xwiki-utils', 'diagram-utils', forEachOpenedFile(function(file) { // This is a workaround for https://github.com/jgraph/drawio/issues/490 // Stop editing for getting the latest content from diagram - file.ui.editor.graph.stopEditing(false); + file.ui.editor.graph.stopEditing(); file.updateFileData(); }); }; From b58feb84d7d8f5ddad6e12f6e7a4e703891e142e Mon Sep 17 00:00:00 2001 From: Oana-Lavinia Florean Date: Tue, 18 Jun 2024 11:27:14 +0300 Subject: [PATCH 2/2] Saving imported draw.io file fails #267 * fix remained usecase, where the import was failing on empty diagram * copied method from a recent version of drawio to include a fix - in the old version of the method, when you import a diagram file on an empty diagram, they would unload the old XWikiFile and use LocalFile, and this breaks a lot of functionalities * update setData too, since pages are already initialized at that point --- .../resources/Diagram/DiagramEditSheet.xml | 332 +++++++++++++++++- 1 file changed, 321 insertions(+), 11 deletions(-) diff --git a/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml b/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml index e158a13..34b5bd4 100644 --- a/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml +++ b/application-diagram-ui/src/main/resources/Diagram/DiagramEditSheet.xml @@ -218,20 +218,11 @@ define('diagram-store', ['jquery', 'xwiki-meta', 'xwiki-utils', 'diagram-utils', return this.input.val(); }, setData: function(data) { - let pages = this.ui.getPagesForXml(data); - if (pages.length > 0) { - this.ui.pages = pages; - } this.input.val(data); }, isCompressed: function() { return false; }, - createData: function() { - var result = this.ui.getFileData(null, null, null, null, - null, null, null, null, this, !this.isCompressed(), true); - return result; - }, open: function() { var graphXML = this.getData() || '<mxGraphModel/>'; this.ui.setFileData(graphXML); @@ -254,7 +245,7 @@ define('diagram-store', ['jquery', 'xwiki-meta', 'xwiki-utils', 'diagram-utils', forEachOpenedFile(function(file) { // This is a workaround for https://github.com/jgraph/drawio/issues/490 // Stop editing for getting the latest content from diagram - file.ui.editor.graph.stopEditing(); + file.ui.editor.graph.stopEditing(false); file.updateFileData(); }); }; @@ -1157,6 +1148,325 @@ define('diagram-image-editor', ['xwiki-utils', 'diagram-link-handler', 'draw.io' return converter; }; + // Copied from the drawio version 24.5.4 to include a fix for importing a file in an empty diagram. This method should + // be removed once we upgrade to a drawio version >= 24.4.0. Being a big method, the structure was altered just to + // make it shorter. + var importFilesNew = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn, + resizeDialog, maxBytes, resampleThreshold, ignoreEmbeddedXml, evt) { + maxSize = (maxSize != null) ? maxSize : this.maxImageSize; + maxBytes = (maxBytes != null) ? maxBytes : this.maxImageBytes; + var crop = x != null && y != null; + var resizeImages = true; + x = (x != null) ? x : 0; + y = (y != null) ? y : 0; + // Checks if large images are imported + var largeImages = false; + if (!mxClient.IS_CHROMEAPP && files != null) { + var thresh = resampleThreshold || this.resampleThreshold; + for (var i = 0; i < files.length; i++) { + if (files[i].type.substring(0, 9) !== 'image/svg' && + files[i].type.substring(0, 6) === 'image/' && + files[i].size > thresh) { + largeImages = true; + break; + } + } + } + + var doImportFiles = mxUtils.bind(this, function() { + var graph = this.editor.graph; + var gs = graph.gridSize; + + fn = (fn != null) ? fn : mxUtils.bind(this, function(data, mimeType, x, y, w, h, filename, done, file) { + try { + if (data != null && data.substring(0, 10) == '<mxlibrary') { + this.spinner.stop(); + this.loadLibrary(new LocalLibrary(this, data, filename)); + this.showSidebar(); + + return null; + } + else { + // Drop on empty file ignores drop location + if (this.isCompatibleString(data) && files.length == 1 && evt != null && + evt.type == 'drop' && this.isBlankFile() && !this.canUndo()) { + crop = false; + x = 0; + y = 0; + } + return this.importFile(data, mimeType, x, y, w, h, filename, + done, file, crop, ignoreEmbeddedXml, evt); + } + } + catch (e) { + this.handleError(e); + return null; + } + }); + + resultFn = (resultFn != null) ? resultFn : mxUtils.bind(this, function(cells) { + graph.setSelectionCells(cells); + }); + + if (this.spinner.spin(document.body, mxResources.get('loading'))) { + var count = files.length; + var remain = count; + var queue = []; + + // Barrier waits for all files to be loaded asynchronously + var barrier = mxUtils.bind(this, function(index, fnc) { + queue[index] = fnc; + if (--remain == 0) { + this.spinner.stop(); + if (barrierFn != null) { + barrierFn(queue); + } + else { + var cells = []; + graph.getModel().beginUpdate(); + try { + for (var j = 0; j < queue.length; j++) { + var tmp = queue[j](); + if (tmp != null) { + cells = cells.concat(tmp); + } + } + } + finally { + graph.getModel().endUpdate(); + } + } + resultFn(cells); + } + }); + + for (var i = 0; i < count; i++) { + (mxUtils.bind(this, function(index) { + var file = files[index]; + if (file != null) { + var reader = new FileReader(); + reader.onload = mxUtils.bind(this, function(e) { + if (filterFn == null || filterFn(file)) { + try { + if (file.type.substring(0, 6) == 'image/') { + if (file.type.substring(0, 9) == 'image/svg') { + // Checks if SVG contains content attribute + var data = Graph.clipSvgDataUri(e.target.result); + var comma = data.indexOf(','); + var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1)))); + var root = mxUtils.parseXml(svgText); + var svgs = root.getElementsByTagName('svg'); + if (svgs.length > 0) { + var svgRoot = svgs[0]; + var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content'); + if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%') { + cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true)); + } + if (cont != null && cont.charAt(0) == '%') { + cont = decodeURIComponent(cont); + } + if (cont != null && (cont.substring(0, 8) === '<mxfile ' || + cont.substring(0, 14) === '<mxGraphModel ')) { + barrier(index, mxUtils.bind(this, function() { + return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name); + })); + } + else { + // SVG needs special handling to add viewbox if missing and + // find initial size from SVG attributes (only for IE11) + barrier(index, mxUtils.bind(this, function() { + try { + // Parses SVG and find width and height + if (root != null) { + var svgs = root.getElementsByTagName('svg'); + if (svgs.length > 0) { + var svgRoot = svgs[0]; + var w = svgRoot.getAttribute('width'); + var h = svgRoot.getAttribute('height'); + if (w != null && w.charAt(w.length - 1) != '%') { + w = parseFloat(w); + } + else { + w = NaN; + } + if (h != null && h.charAt(h.length - 1) != '%') { + h = parseFloat(h); + } + else { + h = NaN; + } + // Check if viewBox attribute already exists + var vb = svgRoot.getAttribute('viewBox'); + if (vb == null || vb.length == 0) { + svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h); + } + // Uses width and height from viewbox for + // missing width and height attributes + else if (isNaN(w) || isNaN(h)) { + var tokens = vb.split(' '); + if (tokens.length > 3) { + w = parseFloat(tokens[2]); + h = parseFloat(tokens[3]); + } + } + data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); + var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h)); + var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max( + 1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name); + // Hack to fix width and height asynchronously + if (cells != null && (isNaN(w) || isNaN(h))) { + var img = new Image(); + img.onload = mxUtils.bind(this, function() { + w = Math.max(1, img.width); + h = Math.max(1, img.height); + cells[0].geometry.width = w; + cells[0].geometry.height = h; + svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h); + data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); + var semi = data.indexOf(';'); + if (semi > 0) { + data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1)); + } + graph.setCellStyles('image', data, [cells[0]]); + }); + img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); + } + return cells; + } + } + } + catch (e) { + // ignores any SVG parsing errors + } + return null; + })); + } + } + else { + barrier(index, mxUtils.bind(this, function() { + return null; + })); + } + } + else { + // Checks if PNG+XML is available to bypass code below + var containsModel = false; + if (file.type == 'image/png') { + var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result); + if (xml != null && xml.length > 0) { + var img = new Image(); + img.src = e.target.result; + barrier(index, mxUtils.bind(this, function() { + return fn(xml, 'text/xml', x + index * gs, y + index * gs, + img.width, img.height, file.name); + })); + containsModel = true; + } + } + // Additional asynchronous step for finding image size + if (!containsModel) { + // Cannot load local files in Chrome App + if (mxClient.IS_CHROMEAPP) { + this.spinner.stop(); + this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'), + mxResources.get('cancel'), mxUtils.bind(this, function() + { + // Hides the dialog + }), null, mxResources.get('ok'), mxUtils.bind(this, function() + { + // Redirects to import function + this.actions.get('import').funct(); + }) + ); + } + else { + this.loadImage(e.target.result, mxUtils.bind(this, function(img) { + this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2) { + barrier(index, mxUtils.bind(this, function() { + // Refuses to insert images above a certain size as they kill the app + if (data2 != null && data2.length < maxBytes) { + var s = (!resizeImages || !this.isResampleImageSize( + file.size, resampleThreshold)) ? 1 : + Math.min(1, Math.min(maxSize / w2, maxSize / h2)); + return fn(data2, file.type, x + index * gs, y + index * gs, + Math.round(w2 * s), Math.round(h2 * s), file.name); + } + else { + this.handleError({message: mxResources.get('imageTooBig')}); + return null; + } + })); + }), resizeImages, maxSize, resampleThreshold, file.size); + }), mxUtils.bind(this, function() { + this.handleError({message: mxResources.get('invalidOrMissingFile')}); + })); + } + } + } + } + else { + var data = e.target.result; + fn(data, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) { + barrier(index, function() { + return cells; + }); + }, file); + } + } + catch (e) { + // Ignores file parsing error + barrier(index, mxUtils.bind(this, function() { + return null; + })); + if (window.console != null) { + console.error(e, file); + } + } + } + else { + // Ignores file and decrements counter + barrier(index, mxUtils.bind(this, function() + { + return null; + })); + } + }); + // Handles special cases + if (/(\.v(dx|sdx?))($|\?)/i.test(file.name) || /(\.vs(x|sx?))($|\?)/i.test(file.name)) { + fn(null, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) { + barrier(index, function() { + return cells; + }); + }, file); + } + else if (file.type.substring(0, 5) == 'image' || file.type == 'application/pdf') { + reader.readAsDataURL(file); + } + else { + reader.readAsText(file); + } + } + }))(i); + } + } + }); + if (largeImages) { + // Workaround for lost files array in async code + var tmp = []; + for (var i = 0; i < files.length; i++) { + tmp.push(files[i]); + } + files = tmp; + this.confirmImageResize(function(doResize) { + resizeImages = doResize; + doImportFiles(); + }, resizeDialog); + } + else { + doImportFiles(); + } + }; + // Override for uploading the image as attachment instead of encode it to Base64. var originalImportFiles = EditorUi.prototype.importFiles; EditorUi.prototype.importFiles = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn, resizeDialog, @@ -1186,7 +1496,7 @@ define('diagram-image-editor', ['xwiki-utils', 'diagram-link-handler', 'draw.io' } }; } - originalImportFiles.apply(this, importFilesArgs); + importFilesNew.apply(this, importFilesArgs); }; // Add support for inserting images by specifying the XWiki attachment reference.