From 0a10a2256f666c881b1839aa35b65839c52ad353 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:56:12 -0400 Subject: [PATCH 1/6] fix small bug in dialog code --- src/components/modebar/buttons.js | 7 +++---- src/plots/plots.js | 6 ++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 091c6dfd781..267d109d5c9 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -82,16 +82,15 @@ modeBarButtons.sendChartToCloud = { // Plotly Cloud origin, used to validate incoming messages and to target outgoing ones. // `baseUrl` (plotlyServerURL) is the upload page that handles login and signals // back when authentication succeeds. - var cloudOrigin; try { - cloudOrigin = new URL(baseUrl).origin; + new URL(baseUrl); } catch(e) { console.error('Invalid plotlyServerURL: ' + baseUrl); return; } - confirmCloudDialog(gd, serverUrl, function() { - Plots.sendDataToCloud(gd, cloudOrigin); + confirmCloudDialog(gd, baseUrl, function() { + Plots.sendDataToCloud(gd, baseUrl); }); } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 084e527998a..d86c5c7e06b 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -204,6 +204,8 @@ function positionPlayWithData(gd, container) { plots.sendDataToCloud = function(gd, serverURL) { gd.emit('plotly_beforeexport'); + const serverURLOrigin = new URL(serverURL).origin; + // Build the request body: the chart JSON plus the plotly.js version used to // generate it, so Cloud can host the chart with a compatible plotly.js version. var chart = plots.graphJson(gd, false, 'keepdata', 'object'); @@ -220,13 +222,13 @@ plots.sendDataToCloud = function(gd, serverURL) { var handleMessage = function(event) { // Only trust messages coming from the Cloud origin. - if(event.origin !== serverURL) return; + if(event.origin !== serverURLOrigin) return; if(event.data && event.data.type === 'CHART_AUTH_SUCCESS') { cloudWindow.postMessage({ type: 'chart', chart: chart - }, serverURL); + }, serverURLOrigin); window.removeEventListener('message', handleMessage); gd.emit('plotly_afterexport'); From ccc31dfb015c58b73a4e687305db295f948465ac Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:56:29 -0400 Subject: [PATCH 2/6] update tests for new cloud button --- test/jasmine/tests/config_test.js | 79 ++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 1b642a80d43..41514e68bb1 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -1,6 +1,7 @@ var Plotly = require('../../../lib/index'); var Plots = require('../../../src/plots/plots'); var Lib = require('../../../src/lib'); +var modeBarButtons = require('../../../src/components/modebar/buttons'); var d3Select = require('../../strict-d3').select; var createGraphDiv = require('../assets/create_graph_div'); @@ -522,69 +523,93 @@ describe('config argument', function() { describe('plotlyServerURL:', function() { var gd; var form; + var openSpy; beforeEach(function() { gd = createGraphDiv(); - spyOn(HTMLFormElement.prototype, 'submit').and.callFake(function() { - form = this; - }); + // sendDataToCloud hands off the chart by opening the provided URL in a + // new tab (window.open(url, '_blank')), so spy on window.open. + var cloudWindow = jasmine.createSpyObj('cloudWindow', ['postMessage']); + openSpy = spyOn(window, 'open').and.returnValue(cloudWindow); }); afterEach(destroyGraphDiv); - it('should not default to an external plotly cloud', function(done) { + it('should default to an empty string', function(done) { Plotly.newPlot(gd, [], {}) .then(function() { expect(gd._context.plotlyServerURL).not.toBe('https://plot.ly'); expect(gd._context.plotlyServerURL).not.toBe('https://chart-studio.plotly.com'); expect(gd._context.plotlyServerURL).toBe(''); - - Plotly.Plots.sendDataToCloud(gd); - expect(form).toBe(undefined); }) .then(done, done.fail); }); - it('should be able to connect to Chart Studio Cloud when set to https://chart-studio.plotly.com', function(done) { + it('should open confirmation dialog when set to a correctly-formatted URL', function(done) { Plotly.newPlot(gd, [], {}, { - plotlyServerURL: 'https://chart-studio.plotly.com' + plotlyServerURL: 'https://example.plotly.com/endpoint' }) .then(function() { - expect(gd._context.plotlyServerURL).toBe('https://chart-studio.plotly.com'); - - Plotly.Plots.sendDataToCloud(gd); - expect(form.action).toBe('https://chart-studio.plotly.com/external'); - expect(form.method).toBe('post'); + expect(gd._context.plotlyServerURL).toBe('https://example.plotly.com/endpoint'); + modeBarButtons.sendChartToCloud.click(gd); + var msg = document.querySelector('.plotly-cloud-dialog-message'); + expect(msg).not.toBe(null, 'confirmation dialog should be shown'); + expect(msg.textContent).toContain('https://example.plotly.com/endpoint'); }) .then(done, done.fail); }); - it('can be set to other base urls', function(done) { - Plotly.newPlot(gd, [], {}, {plotlyServerURL: 'dummy'}) + it('should NOT open confirmation dialog when set to an invalid URL', function(done) { + Plotly.newPlot(gd, [], {}, { + plotlyServerURL: 'dummy' + }) .then(function() { expect(gd._context.plotlyServerURL).toBe('dummy'); + modeBarButtons.sendChartToCloud.click(gd); + var msg = document.querySelector('.plotly-cloud-dialog-message'); + expect(msg).toBe(null, 'confirmation dialog should not be shown'); + }) + .then(done, done.fail); + }); - Plotly.Plots.sendDataToCloud(gd); - expect(form.action).toContain('/dummy/external'); - expect(form.method).toBe('post'); + it('should open URL in a new tab after clicking confirm button', function(done) { + Plotly.newPlot(gd, [], {}, { + plotlyServerURL: 'https://example.plotly.com/endpoint' + }) + .then(function() { + expect(gd._context.plotlyServerURL).toBe('https://example.plotly.com/endpoint'); + modeBarButtons.sendChartToCloud.click(gd); + + // Click the confirm button in the dialog + var confirmBtn = document.querySelector('.plotly-cloud-dialog-btn--confirm'); + expect(confirmBtn).not.toBe(null, 'confirm button should be shown'); + mouseEvent('click', 0, 0, {element: confirmBtn}); + + // Should open the provided URL's origin in a new tab + expect(openSpy).toHaveBeenCalledWith('https://example.plotly.com/endpoint', '_blank'); }) .then(done, done.fail); }); - it('has lesser priotiy then window env', function(done) { - window.PLOTLYENV = {BASE_URL: 'yo'}; + it('has lesser priority than window env', function(done) { + window.PLOTLYENV = {BASE_URL: 'https://yo.plotly.com/endpoint'}; - Plotly.newPlot(gd, [], {}, {plotlyServerURL: 'dummy'}) + Plotly.newPlot(gd, [], {}, {plotlyServerURL: 'https://example.plotly.com/endpoint2'}) .then(function() { - expect(gd._context.plotlyServerURL).toBe('dummy'); + expect(gd._context.plotlyServerURL).toBe('https://example.plotly.com/endpoint2'); + + // Confirmation dialog message should contain window.PLOTLYENV.BASE_URL, + // which takes priority over plotlyServerURL + modeBarButtons.sendChartToCloud.click(gd); - Plotly.Plots.sendDataToCloud(gd); - expect(form.action).toContain('/yo/external'); - expect(form.method).toBe('post'); + var msg = document.querySelector('.plotly-cloud-dialog-message'); + expect(msg).not.toBe(null, 'confirmation dialog should be shown'); + expect(msg.textContent).toContain('https://yo.plotly.com/endpoint'); + expect(msg.textContent).not.toContain('https://example.plotly.com/endpoint2'); }) .catch(failTest) .then(function() { - delete window.PLOTLY_ENV; + delete window.PLOTLYENV; done(); }); }); From 6c2e36365066342478e64473fd09d8c5876daae0 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:30:55 -0400 Subject: [PATCH 3/6] re-add showEditInChartStudio config option (but deprecate it) --- src/components/modebar/manage.js | 7 +++++++ src/plot_api/plot_config.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 56713ca22db..f00a046c486 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -146,6 +146,13 @@ function getButtonGroups(gd) { // buttons common to all plot types var commonGroup = ['toImage']; if(context.showSendToCloud) commonGroup.push('sendChartToCloud'); + else if(context.showEditInChartStudio) { + console.warn([ + '*showEditInChartStudio* is deprecated.', + 'Use *showSendToCloud* instead.' + ].join(' ')); + commonGroup.push('sendChartToCloud'); + } addGroup(commonGroup); var zoomGroup = []; diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 6d4d0d0ea2f..55237deb55c 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -279,6 +279,13 @@ var configAttributes = { 'send chart data to an external server.' ].join(' ') }, + showEditInChartStudio: { + valType: 'boolean', + dflt: false, + description: [ + 'Deprecated. Use `showSendToCloud` instead.' + ].join(' ') + }, modeBarButtonsToRemove: { valType: 'any', dflt: [], From e8feabf8f4d3a77fec7fc302711595a7d4c3dcc7 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:31:03 -0400 Subject: [PATCH 4/6] update modebar tests --- test/jasmine/tests/modebar_test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index ede3177f289..e3c3fb919ed 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -823,21 +823,21 @@ describe('ModeBar', function() { gd._context.showEditInChartStudio = false; manageModeBar(gd); checkButtons(gd._fullLayout._modeBar, getButtons([ - ['toImage', 'sendDataToCloud'] + ['toImage', 'sendChartToCloud'] ]), 1); gd._context.showSendToCloud = false; gd._context.showEditInChartStudio = true; manageModeBar(gd); checkButtons(gd._fullLayout._modeBar, getButtons([ - ['toImage', 'editInChartStudio'] + ['toImage', 'sendChartToCloud'] ]), 1); gd._context.showSendToCloud = true; gd._context.showEditInChartStudio = true; manageModeBar(gd); checkButtons(gd._fullLayout._modeBar, getButtons([ - ['toImage', 'editInChartStudio'] + ['toImage', 'sendChartToCloud'] ]), 1); }); From 2eea06065627b783709cd47e39e730dc02d2a973 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:34:16 -0400 Subject: [PATCH 5/6] update plot-schema --- test/plot-schema.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/plot-schema.json b/test/plot-schema.json index 290851beb69..78614b86693 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -330,6 +330,11 @@ "dflt": true, "valType": "boolean" }, + "showEditInChartStudio": { + "description": "Deprecated. Use `showSendToCloud` instead.", + "dflt": false, + "valType": "boolean" + }, "showLink": { "description": "Determines whether a link to Chart Studio Cloud is displayed at the bottom right corner of resulting graphs. Use with `sendData` and `linkText`.", "dflt": false, From 6eb3a927cfdb435fe33815006b8b28f8bf102f51 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:49:56 -0400 Subject: [PATCH 6/6] update draftlog --- draftlogs/7802_change.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/draftlogs/7802_change.md b/draftlogs/7802_change.md index 0cc7afe62cd..ccbd4d58ead 100644 --- a/draftlogs/7802_change.md +++ b/draftlogs/7802_change.md @@ -1,2 +1,2 @@ -- Update `sendDataToCloud` modebar button to upload chart to Plotly Cloud [[#7802](https://github.com/plotly/plotly.js/pull/7802)] +- Update `sendDataToCloud` modebar button to upload chart to Plotly Cloud [[#7802](https://github.com/plotly/plotly.js/pull/7802), [#7852](https://github.com/plotly/plotly.js/pull/7852)] - NOTE: The Plotly Cloud endpoint for receiving charts is not yet functional, so this button won't complete the upload.