From 3f6226396d94a83b2724cc09597eea4eba4072ac Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:23:40 -0400 Subject: [PATCH 1/8] Update wording in 'Share Chart' dialog. Provide different wording for default (Plotly Cloud) URL vs. ccustom URL provided in config --- build/plotcss.js | 3 +- src/components/modebar/buttons.js | 16 ++- src/components/modebar/cloud_confirm.js | 152 +++++++++++++++++------- src/css/_cloud_dialog.scss | 9 ++ 4 files changed, 133 insertions(+), 47 deletions(-) diff --git a/build/plotcss.js b/build/plotcss.js index 75d2ff3967d..438288c24a3 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -54,7 +54,8 @@ var rules = { "X .plotly-cloud-dialog .plotly-cloud-dialog-box": "box-sizing:border-box;min-width:300px;max-width:420px;padding:20px 24px;background-color:#fff;border:1px solid #e0e2e5;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,.25);font-size:13px;color:#2a3f5f;", "X .plotly-cloud-dialog .plotly-cloud-dialog-title": "font-size:16px;font-weight:bold;margin-bottom:12px;", "X .plotly-cloud-dialog .plotly-cloud-dialog-message": "line-height:1.5;overflow-wrap:break-word;word-wrap:break-word;", - "X .plotly-cloud-dialog .plotly-cloud-dialog-message--hostname": "font-weight:bold;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-message--hostname": "font-weight:bold;text-decoration:underline;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-message--account": "margin-top:16px;padding:8px;border-radius:3px;font-size:.9em;background-color:#edf1f8;", "X .plotly-cloud-dialog .plotly-cloud-dialog-buttons": "display:flex;justify-content:flex-end;margin-top:20px;", "X .plotly-cloud-dialog .plotly-cloud-dialog-btn": "font-family:inherit;font-size:13px;padding:7px 16px;margin-left:8px;border-radius:3px;border:1px solid rgba(0,0,0,0);cursor:pointer;", "X .plotly-cloud-dialog .plotly-cloud-dialog-btn:focus-visible": "outline:2px solid #447adb;outline-offset:1px;", diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 7cd3a41e66c..d58a31cf691 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -72,25 +72,29 @@ modeBarButtons.toImage = { modeBarButtons.sendChartToCloud = { name: 'sendChartToCloud', title: function (gd) { - return _(gd, 'Share Chart'); + return _(gd, 'Share Chart...'); }, icon: Icons.cloudupload, click: function (gd) { var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; if (!baseUrl) { - console.error('No destination URL provided (plotlyServerURL is not set)'); + console.error('No destination URL provided (plotlyServerURL is empty)'); return; } - // 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. + // Validate that the provided plotlyServerURL is a valid URL + // with an http or https protocol + var baseUrlObj; try { - new URL(baseUrl); + baseUrlObj = new URL(baseUrl); } catch (e) { console.error('Invalid plotlyServerURL: ' + baseUrl); return; } + if (baseUrlObj.protocol !== 'https:' && baseUrlObj.protocol !== 'http:') { + console.error('Invalid protocol for plotlyServerURL: ' + baseUrl); + return; + } confirmCloudDialog(gd, baseUrl, function () { Plots.sendDataToCloud(gd, baseUrl); diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index d1c715acca3..c2af9fb6cac 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/cloud_confirm.js @@ -1,8 +1,99 @@ 'use strict'; -var d3 = require('@plotly/d3'); +const d3 = require('@plotly/d3'); -var _ = require('../../lib')._; +const _ = require('../../lib')._; +const dfltConfig = require('../../plot_api/plot_config').dfltConfig; + +const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) => { + // Wording for dialog box. Must be defined inside this function rather than + // at the top of the file because localization requires a reference to the + // graph div (gd) + const DIALOG_TITLE = _(gd, 'Share Chart'); + + // Messages to be shown when serverUrl matches the default (Plotly Cloud) URL + const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you choose to share.'); + const DIALOG_MESSAGE_CLOUD_ACCOUNT = _(gd, "If you don't have a Plotly Cloud account yet, you'll have a chance to create one."); + + // Message to be shown when serverUrl is not the default URL + const DIALOG_MESSAGE_OTHER = _(gd, 'This chart will be sent to {serverUrl}.'); + + // Labels for buttons + const DIALOG_CANCEL = _(gd, 'Cancel'); + const DIALOG_CONFIRM = _(gd, 'Share'); + + const dialog = overlay.append('div') + .classed('plotly-cloud-dialog-box', true); + + dialog.append('div') + .classed('plotly-cloud-dialog-title', true) + .text(DIALOG_TITLE); + + if (serverUrl === dfltConfig.plotlyServerURL) { + // If serverUrl matches the default Plotly Cloud URL, + // show a custom message designed for Plotly Cloud + const description = dialog.append('div') + .classed('plotly-cloud-dialog-message', true); + + // Link to the base domain only, leaving the endpoint path + const serverUrlHref = new URL(serverUrl).origin; + + // Split description into three parts: Before {, between, and after } + const descriptionParts = DIALOG_MESSAGE_CLOUD.split(/(\{|\})/); + const beforePart = descriptionParts[0]; + const betweenPart = descriptionParts[2]; + const afterPart = descriptionParts[4]; + + // Append the parts to the description div + description.append('span').text(beforePart); + description.append('a') + .classed('plotly-cloud-dialog-message--hostname', true) + .attr('href', serverUrlHref) + .attr('target', '_blank') + .text(betweenPart); + description.append('span').text(afterPart); + + description.append('div') + .classed('plotly-cloud-dialog-message--account', true) + .text(DIALOG_MESSAGE_CLOUD_ACCOUNT); + } else { + // Otherwise, show a generic message with the server URL + // We can trust that serverUrl is a valid URL because it was validated in buttons.js + const serverUrlObj = new URL(serverUrl); + const serverUrlHostname = serverUrlObj.hostname; + // Link to the base domain only, leaving off any endpoint path + const serverUrlHref = serverUrlObj.origin; + const descriptionParts = DIALOG_MESSAGE_OTHER.split(/(\{|\})/); + const beforePart = descriptionParts[0]; + const afterPart = descriptionParts[4]; + + const description = dialog.append('div') + .classed('plotly-cloud-dialog-message', true); + + description.append('span').text(beforePart); + description.append('a') + .classed('plotly-cloud-dialog-message--hostname', true) + .attr('href', serverUrlHref) + .attr('target', '_blank') + .text(serverUrlHostname); + description.append('span').text(afterPart); + } + + const buttons = dialog.append('div') + .classed('plotly-cloud-dialog-buttons', true); + + buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--cancel', true) + .text(DIALOG_CANCEL) + .on('click', onClickCancel); + + buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--confirm', true) + .text(DIALOG_CONFIRM) + .on('click', onClickConfirm); +}; /** * Show a styled confirmation dialog before sharing a chart with Plotly Cloud. @@ -15,61 +106,42 @@ var _ = require('../../lib')._; * @param {string} serverUrl - destination shown in the dialog message * @param {function} onConfirm - called when the user confirms the upload */ -module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { - var container = d3.select(gd._fullLayout._paperdiv.node()); +const confirmCloudDialog = (gd, serverUrl, onConfirm) => { + const container = d3.select(gd._fullLayout._paperdiv.node()); // Never stack dialogs - drop any that is already open. container.selectAll('.plotly-cloud-dialog').remove(); - var overlay = container + const overlay = container .append('div') .classed('plotly-cloud-dialog', true); - var dialog = overlay.append('div') - .classed('plotly-cloud-dialog-box', true); - - dialog.append('div') - .classed('plotly-cloud-dialog-title', true) - .text(_(gd, 'Share with Plotly Cloud')); - - var serverUrlText = new URL(serverUrl).hostname; - - var description = dialog.append('div'); - description.classed('plotly-cloud-dialog-message', true); - description.append('span').text(_(gd, 'This chart and its data will be sent to ')); - description.append('span').text(serverUrlText).classed('plotly-cloud-dialog-message--hostname', true); - description.append('span').text('. '); - - var buttons = dialog.append('div') - .classed('plotly-cloud-dialog-buttons', true); - - function close() { + const close = () => { overlay.remove(); document.removeEventListener('keydown', onKeydown); - } + }; - function onKeydown(e) { + const onKeydown = (e) => { if(e.key === 'Escape' || e.keyCode === 27) close(); - } + }; document.addEventListener('keydown', onKeydown); // Clicking the backdrop (but not the dialog box) cancels. - overlay.on('click', function() { + overlay.on('click', () => { if(d3.event.target === overlay.node()) close(); }); - buttons.append('button') - .classed('plotly-cloud-dialog-btn', true) - .classed('plotly-cloud-dialog-btn--cancel', true) - .text(_(gd, 'Cancel')) - .on('click', close); - - buttons.append('button') - .classed('plotly-cloud-dialog-btn', true) - .classed('plotly-cloud-dialog-btn--confirm', true) - .text(_(gd, 'Share')) - .on('click', function() { + // Build the dialog box and append it to the overlay + buildDialogBox( + gd, + overlay, + serverUrl, + () => { close(); onConfirm(); - }); + }, + close + ); }; + +module.exports = confirmCloudDialog; diff --git a/src/css/_cloud_dialog.scss b/src/css/_cloud_dialog.scss index fdf66fcee0f..37b8a36a947 100644 --- a/src/css/_cloud_dialog.scss +++ b/src/css/_cloud_dialog.scss @@ -44,6 +44,15 @@ &--hostname { font-weight: bold; + text-decoration: underline; + } + + &--account { + margin-top: 16px; + padding: 8px; + border-radius: 3px; + font-size: 0.9em; + background-color: vars.$color-bg-hint; } } From 431d559c3dbd9e31af9fed1295c16244bc7eff55 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:53:41 -0400 Subject: [PATCH 2/8] update jasmine tests for share chart dialog --- test/jasmine/tests/config_test.js | 87 ++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 2a04690027a..f46b041122b 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -2,6 +2,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 dfltConfig = require('../../../src/plot_api/plot_config').dfltConfig; var d3Select = require('../../strict-d3').select; var createGraphDiv = require('../assets/create_graph_div'); @@ -509,12 +510,43 @@ describe('config argument', function() { 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('example.plotly.com'); + expect(msg.textContent).toBe('This chart will be sent to example.plotly.com.'); + + // The host name is shown as a link to the server's origin, + // leaving off the endpoint path + var link = msg.querySelector('.plotly-cloud-dialog-message--hostname'); + expect(link).not.toBe(null, 'host name should be shown as a link'); + expect(link.textContent).toBe('example.plotly.com'); + expect(link.getAttribute('href')).toBe('https://example.plotly.com'); }) .then(done, done.fail); }); - it('should NOT open confirmation dialog when set to an invalid URL', function(done) { + it('should show Plotly Cloud wording when left at the default URL', function(done) { + Plotly.newPlot(gd, [], {}, {}) + .then(function() { + expect(gd._context.plotlyServerURL).toBe(dfltConfig.plotlyServerURL); + 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('This chart will be uploaded to Plotly Cloud to create a sharing link.'); + + var link = msg.querySelector('.plotly-cloud-dialog-message--hostname'); + expect(link).not.toBe(null, 'Plotly Cloud should be shown as a link'); + expect(link.textContent).toBe('Plotly Cloud'); + expect(link.getAttribute('href')).toBe(new URL(dfltConfig.plotlyServerURL).origin); + + var account = msg.querySelector('.plotly-cloud-dialog-message--account'); + expect(account).not.toBe(null, 'account note should be shown'); + expect(account.textContent).toContain('Plotly Cloud account'); + }) + .then(done, done.fail); + }); + + it('should NOT open confirmation dialog when set to an unparseable URL', function(done) { + var errorSpy = spyOn(console, 'error'); + Plotly.newPlot(gd, [], {}, { plotlyServerURL: 'dummy' }) @@ -523,6 +555,21 @@ describe('config argument', function() { modeBarButtons.sendChartToCloud.click(gd); var msg = document.querySelector('.plotly-cloud-dialog-message'); expect(msg).toBe(null, 'confirmation dialog should not be shown'); + expect(errorSpy).toHaveBeenCalledWith('Invalid plotlyServerURL: dummy'); + }) + .then(done, done.fail); + }); + + it('should NOT open confirmation dialog when set to a non-http(s) URL', function(done) { + var errorSpy = spyOn(console, 'error'); + + Plotly.newPlot(gd, [], {}, { + plotlyServerURL: 'ftp://example.plotly.com' + }) + .then(function() { + modeBarButtons.sendChartToCloud.click(gd); + expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'confirmation dialog should not be shown'); + expect(errorSpy).toHaveBeenCalledWith('Invalid protocol for plotlyServerURL: ftp://example.plotly.com'); }) .then(done, done.fail); }); @@ -543,10 +590,46 @@ describe('config argument', function() { // Should open the provided URL's origin in a new tab, // adding the current page's origin as a query parameter expect(openSpy).toHaveBeenCalledWith('https://example.plotly.com/endpoint?origin=http%3A%2F%2Flocalhost%3A9876', '_blank'); + + // Confirming should also dismiss the dialog + expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'dialog should be closed'); }) .then(done, done.fail); }); + [{ + name: 'clicking cancel button', + dismiss: function() { + mouseEvent('click', 0, 0, {element: document.querySelector('.plotly-cloud-dialog-btn--cancel')}); + } + }, { + name: 'clicking the backdrop', + dismiss: function() { + mouseEvent('click', 0, 0, {element: document.querySelector('.plotly-cloud-dialog')}); + } + }, { + name: 'pressing Escape', + dismiss: function() { + document.dispatchEvent(new window.KeyboardEvent('keydown', {key: 'Escape'})); + } + }].forEach(function(spec) { + it('should close dialog without uploading when ' + spec.name, function(done) { + Plotly.newPlot(gd, [], {}, { + plotlyServerURL: 'https://example.plotly.com/endpoint' + }) + .then(function() { + modeBarButtons.sendChartToCloud.click(gd); + expect(document.querySelector('.plotly-cloud-dialog')).not.toBe(null, 'dialog should be shown'); + + spec.dismiss(); + + expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'dialog should be closed'); + expect(openSpy).not.toHaveBeenCalled(); + }) + .then(done, done.fail); + }); + }); + it('has lesser priority than window env', function(done) { window.PLOTLYENV = {BASE_URL: 'https://yo.plotly.com/endpoint'}; From 072fe8454ae7bb1c48a175926b7ff70ef0fdc100 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:29:44 -0400 Subject: [PATCH 3/8] add draftlog --- draftlogs/7928_change.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7928_change.md diff --git a/draftlogs/7928_change.md b/draftlogs/7928_change.md new file mode 100644 index 00000000000..c06e42fe350 --- /dev/null +++ b/draftlogs/7928_change.md @@ -0,0 +1 @@ +- Update "Share chart" dialog with more informative wording [[#7928](https://github.com/plotly/plotly.js/pull/7928)] \ No newline at end of file From 3a9781d9df35599b24706d952f9493a2391eb6bb Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:29:41 -0400 Subject: [PATCH 4/8] tweak wording --- src/components/modebar/cloud_confirm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index c2af9fb6cac..b059e247c54 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/cloud_confirm.js @@ -12,7 +12,7 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = const DIALOG_TITLE = _(gd, 'Share Chart'); // Messages to be shown when serverUrl matches the default (Plotly Cloud) URL - const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you choose to share.'); + const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you change its visibility.'); const DIALOG_MESSAGE_CLOUD_ACCOUNT = _(gd, "If you don't have a Plotly Cloud account yet, you'll have a chance to create one."); // Message to be shown when serverUrl is not the default URL From 4897de4922f28f0cad980a3871c80822b56b5d75 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:38:30 -0400 Subject: [PATCH 5/8] lowercase the word 'chart' --- src/components/modebar/buttons.js | 2 +- src/components/modebar/cloud_confirm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index d58a31cf691..fa69a854d6c 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -72,7 +72,7 @@ modeBarButtons.toImage = { modeBarButtons.sendChartToCloud = { name: 'sendChartToCloud', title: function (gd) { - return _(gd, 'Share Chart...'); + return _(gd, 'Share chart...'); }, icon: Icons.cloudupload, click: function (gd) { diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index b059e247c54..30deec7bfb1 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/cloud_confirm.js @@ -9,7 +9,7 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = // Wording for dialog box. Must be defined inside this function rather than // at the top of the file because localization requires a reference to the // graph div (gd) - const DIALOG_TITLE = _(gd, 'Share Chart'); + const DIALOG_TITLE = _(gd, 'Share chart'); // Messages to be shown when serverUrl matches the default (Plotly Cloud) URL const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you change its visibility.'); From a569bdd9cfc653ae73aff88320c1bdf29defb256 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:29:25 -0400 Subject: [PATCH 6/8] Update src/components/modebar/cloud_confirm.js Co-authored-by: Cameron DeCoster --- src/components/modebar/cloud_confirm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index 30deec7bfb1..6d494b4fbfb 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/cloud_confirm.js @@ -3,7 +3,7 @@ const d3 = require('@plotly/d3'); const _ = require('../../lib')._; -const dfltConfig = require('../../plot_api/plot_config').dfltConfig; +const { dfltConfig } = require('../../plot_api/plot_config'); const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) => { // Wording for dialog box. Must be defined inside this function rather than From 573747b744a905ea45cff3a6cd9ebb29b8cc0a98 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:04:24 -0400 Subject: [PATCH 7/8] small refactor; create strings file --- src/components/modebar/buttons.js | 2 +- .../dialog.js} | 45 ++++++++++--------- src/components/modebar/share_chart/strings.js | 32 +++++++++++++ 3 files changed, 56 insertions(+), 23 deletions(-) rename src/components/modebar/{cloud_confirm.js => share_chart/dialog.js} (77%) create mode 100644 src/components/modebar/share_chart/strings.js diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index fa69a854d6c..9cba6724c27 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -5,7 +5,7 @@ var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); var eraseActiveShape = require('../shapes/draw').eraseActiveShape; -var confirmCloudDialog = require('./cloud_confirm'); +var confirmCloudDialog = require('./share_chart/dialog'); var Lib = require('../../lib'); var _ = Lib._; diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/share_chart/dialog.js similarity index 77% rename from src/components/modebar/cloud_confirm.js rename to src/components/modebar/share_chart/dialog.js index 6d494b4fbfb..1792687bedb 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/share_chart/dialog.js @@ -2,32 +2,33 @@ const d3 = require('@plotly/d3'); -const _ = require('../../lib')._; -const { dfltConfig } = require('../../plot_api/plot_config'); +const { dfltConfig } = require('../../../plot_api/plot_config'); +const getDialogStrings = require('./strings'); +/** + * Build the chart-sharing confirmation dialog box and add it to DOM + * inside the overlay element. + * + * The message wording depends on the destination: when serverUrl is the + * default Plotly Cloud URL we show wording specific to Plotly Cloud, + * otherwise we show a generic message naming the server's hostname. + * + * @param {DOM node} gd - the graph div (used for localizing strings) + * @param {d3 selection} overlay - the dialog backdrop element to append the box to + * @param {string} serverUrl - destination URL (must be a valid URL) + * @param {function} onClickConfirm - called when the confirm button is clicked + * @param {function} onClickCancel - called when the cancel button is clicked + */ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) => { - // Wording for dialog box. Must be defined inside this function rather than - // at the top of the file because localization requires a reference to the - // graph div (gd) - const DIALOG_TITLE = _(gd, 'Share chart'); - - // Messages to be shown when serverUrl matches the default (Plotly Cloud) URL - const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you change its visibility.'); - const DIALOG_MESSAGE_CLOUD_ACCOUNT = _(gd, "If you don't have a Plotly Cloud account yet, you'll have a chance to create one."); - - // Message to be shown when serverUrl is not the default URL - const DIALOG_MESSAGE_OTHER = _(gd, 'This chart will be sent to {serverUrl}.'); - // Labels for buttons - const DIALOG_CANCEL = _(gd, 'Cancel'); - const DIALOG_CONFIRM = _(gd, 'Share'); + const strings = getDialogStrings(gd); const dialog = overlay.append('div') .classed('plotly-cloud-dialog-box', true); dialog.append('div') .classed('plotly-cloud-dialog-title', true) - .text(DIALOG_TITLE); + .text(strings.DIALOG_TITLE); if (serverUrl === dfltConfig.plotlyServerURL) { // If serverUrl matches the default Plotly Cloud URL, @@ -39,7 +40,7 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = const serverUrlHref = new URL(serverUrl).origin; // Split description into three parts: Before {, between, and after } - const descriptionParts = DIALOG_MESSAGE_CLOUD.split(/(\{|\})/); + const descriptionParts = strings.DIALOG_MESSAGE_CLOUD.split(/(\{|\})/); const beforePart = descriptionParts[0]; const betweenPart = descriptionParts[2]; const afterPart = descriptionParts[4]; @@ -55,7 +56,7 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = description.append('div') .classed('plotly-cloud-dialog-message--account', true) - .text(DIALOG_MESSAGE_CLOUD_ACCOUNT); + .text(strings.DIALOG_MESSAGE_CLOUD_ACCOUNT); } else { // Otherwise, show a generic message with the server URL // We can trust that serverUrl is a valid URL because it was validated in buttons.js @@ -63,7 +64,7 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = const serverUrlHostname = serverUrlObj.hostname; // Link to the base domain only, leaving off any endpoint path const serverUrlHref = serverUrlObj.origin; - const descriptionParts = DIALOG_MESSAGE_OTHER.split(/(\{|\})/); + const descriptionParts = strings.DIALOG_MESSAGE_OTHER.split(/(\{|\})/); const beforePart = descriptionParts[0]; const afterPart = descriptionParts[4]; @@ -85,13 +86,13 @@ const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) = buttons.append('button') .classed('plotly-cloud-dialog-btn', true) .classed('plotly-cloud-dialog-btn--cancel', true) - .text(DIALOG_CANCEL) + .text(strings.DIALOG_CANCEL) .on('click', onClickCancel); buttons.append('button') .classed('plotly-cloud-dialog-btn', true) .classed('plotly-cloud-dialog-btn--confirm', true) - .text(DIALOG_CONFIRM) + .text(strings.DIALOG_CONFIRM) .on('click', onClickConfirm); }; diff --git a/src/components/modebar/share_chart/strings.js b/src/components/modebar/share_chart/strings.js new file mode 100644 index 00000000000..6eb0084969c --- /dev/null +++ b/src/components/modebar/share_chart/strings.js @@ -0,0 +1,32 @@ +const _ = require('../../../lib')._; + +/** + * Get the localized wording for the share chart dialog box. + * + * The strings must be built inside this function rather than defined + * as constants because localization requires a reference to the graph div. + * + * Braces in the message strings mark the span of text that dialog.js turns + * into a link to the destination server, so translations must keep them. + * + * @param {DOM node} gd - the graph div, used for localization + * @returns {object} object containing localized strings + */ +const getDialogStrings = function(gd) { + return { + DIALOG_TITLE: _(gd, 'Share chart'), + + // Messages to be shown when serverUrl matches the default (Plotly Cloud) URL + DIALOG_MESSAGE_CLOUD: _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you change its visibility.'), + DIALOG_MESSAGE_CLOUD_ACCOUNT: _(gd, "If you don't have a Plotly Cloud account yet, you'll have a chance to create one."), + + // Message to be shown when serverUrl is not the default URL + DIALOG_MESSAGE_OTHER: _(gd, 'This chart will be sent to {serverUrl}.'), + + // Labels for buttons + DIALOG_CANCEL: _(gd, 'Cancel'), + DIALOG_CONFIRM: _(gd, 'Share'), + } +} + +module.exports = getDialogStrings; \ No newline at end of file From f3b78fb4c60c86e31eca9f905bb2d55bb7eba5a9 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:14:58 -0400 Subject: [PATCH 8/8] update invalid protocal error message --- src/components/modebar/buttons.js | 5 +++-- test/jasmine/tests/config_test.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 9cba6724c27..358ea14ee03 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -91,8 +91,9 @@ modeBarButtons.sendChartToCloud = { console.error('Invalid plotlyServerURL: ' + baseUrl); return; } - if (baseUrlObj.protocol !== 'https:' && baseUrlObj.protocol !== 'http:') { - console.error('Invalid protocol for plotlyServerURL: ' + baseUrl); + const supportedProtocols = ['http:', 'https:']; + if (!supportedProtocols.includes(baseUrlObj.protocol)) { + console.error(`Invalid protocol '${baseUrlObj.protocol}' in plotlyServerURL '${baseUrl}'. Must be one of: ${supportedProtocols.join(', ')}`); return; } diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index f46b041122b..6055da55216 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -569,7 +569,7 @@ describe('config argument', function() { .then(function() { modeBarButtons.sendChartToCloud.click(gd); expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'confirmation dialog should not be shown'); - expect(errorSpy).toHaveBeenCalledWith('Invalid protocol for plotlyServerURL: ftp://example.plotly.com'); + expect(errorSpy).toHaveBeenCalledWith("Invalid protocol 'ftp:' in plotlyServerURL 'ftp://example.plotly.com'. Must be one of: http:, https:"); }) .then(done, done.fail); });