diff --git a/build/plotcss.js b/build/plotcss.js index b1e2798597f..503adc9afe1 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -54,6 +54,10 @@ 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-label": "display:block;margin-top:16px;margin-bottom:6px;font-weight:bold;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-input": "box-sizing:border-box;width:100%;padding:7px 10px;font-family:inherit;font-size:13px;color:#2a3f5f;background-color:#fff;border:1px solid #e0e2e5;border-radius:3px;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-input:focus": "outline:none;border-color:#447adb;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-error": "margin-top:6px;color:#db44a5;", "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/draftlogs/7896_add.md b/draftlogs/7896_add.md new file mode 100644 index 00000000000..ee80904db81 --- /dev/null +++ b/draftlogs/7896_add.md @@ -0,0 +1 @@ +- Add ability to use custom URL for chart upload from the dialog UI [[#7896](https://github.com/plotly/plotly.js/pull/7896)] diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 267d109d5c9..aa02c4db45a 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -89,8 +89,10 @@ modeBarButtons.sendChartToCloud = { return; } - confirmCloudDialog(gd, baseUrl, function() { - Plots.sendDataToCloud(gd, baseUrl); + // The dialog pre-fills baseUrl but lets the user override it with a + // custom server URL, which is passed back here as `chosenUrl`. + confirmCloudDialog(gd, baseUrl, function(chosenUrl) { + Plots.sendDataToCloud(gd, chosenUrl); }); } }; diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index 4f4ed1b6bf6..33d39a54044 100644 --- a/src/components/modebar/cloud_confirm.js +++ b/src/components/modebar/cloud_confirm.js @@ -11,9 +11,15 @@ var _ = require('../../lib')._; * so it is centered over the plot rather than the whole viewport. It can be * dismissed by clicking Cancel, clicking the backdrop, or pressing Escape. * + * By default the chart is uploaded to the configured plotlyServerURL. A + * "Use a custom server URL" link reveals an editable "Server URL" field + * pre-filled with that default; whatever the user leaves in the field when they + * confirm becomes the destination, so a custom URL entered here overrides the + * plotlyServerURL config for that upload. + * * @param {DOM node} gd - the graph div, used to scope the dialog to the plot - * @param {string} serverUrl - destination shown in the dialog message - * @param {function} onConfirm - called when the user confirms the upload + * @param {string} serverUrl - default destination + * @param {function} onConfirm - called with the (possibly edited) server URL when confirmed */ module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { var container = d3.select(gd._fullLayout._paperdiv.node()); @@ -30,11 +36,34 @@ module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { dialog.append('div') .classed('plotly-cloud-dialog-title', true) - .text(_(gd, 'Share with Plotly Cloud')); + .text(_(gd, 'Share Chart')); - dialog.append('div') + var description = dialog.append('div') .classed('plotly-cloud-dialog-message', true) - .text(_(gd, 'This chart and its data will be sent to') + ' ' + serverUrl + '.'); + .text(_(gd, 'This chart and its data will be sent to') + ' ' + serverUrl + '. '); + + // The custom-URL field stays hidden until the user opts in. By default the + // chart is shared to the configured serverUrl; a button in the button row + // reveals this input for anyone who wants to override that destination. + var urlField = dialog.append('div') + .classed('plotly-cloud-dialog-url-field', true) + .style('display', 'none'); + + urlField.append('label') + .classed('plotly-cloud-dialog-label', true) + .attr('for', 'plotly-cloud-dialog-url') + .text(_(gd, 'Dash Enterprise URL')); + + var input = urlField.append('input') + .classed('plotly-cloud-dialog-input', true) + .attr('id', 'plotly-cloud-dialog-url') + .attr('type', 'text') + .attr('spellcheck', false) + .attr('placeholder', 'https:///newchart'); + + var error = dialog.append('div') + .classed('plotly-cloud-dialog-error', true) + .style('display', 'none'); var buttons = dialog.append('div') .classed('plotly-cloud-dialog-buttons', true); @@ -54,6 +83,54 @@ module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { if(d3.event.target === overlay.node()) close(); }); + function confirm() { + var url = (input.property('value') || serverUrl).trim(); + + if(!url) { + error.text(_(gd, 'Please enter a server URL.')).style('display', ''); + input.node().focus(); + return; + } + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + try { + new URL(url); + } catch(e) { + error.text(_(gd, 'Please enter a valid server URL.')).style('display', ''); + input.node().focus(); + return; + } + + close(); + onConfirm(url); + } + + // Hide the error and allow Enter to confirm from the input. + input.on('input', function() { + error.style('display', 'none'); + }); + input.on('keydown', function() { + if(d3.event.key === 'Enter' || d3.event.keyCode === 13) confirm(); + }); + + // Sits at the left of the button row, in line with Cancel/Share. Reveals + // the Server URL field and hides itself once a custom URL is requested. + var customBtn = buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--custom', true) + .attr('type', 'button') + .text(_(gd, 'Share with Dash Enterprise')); + + customBtn.on('click', function() { + customBtn.style('display', 'none'); + urlField.style('display', ''); + description.text(_(gd, 'This chart and its data will be sent to the server URL you provide.')); + input.node().focus(); + }); + buttons.append('button') .classed('plotly-cloud-dialog-btn', true) .classed('plotly-cloud-dialog-btn--cancel', true) @@ -64,8 +141,5 @@ module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { .classed('plotly-cloud-dialog-btn', true) .classed('plotly-cloud-dialog-btn--confirm', true) .text(_(gd, 'Share')) - .on('click', function() { - close(); - onConfirm(); - }); + .on('click', confirm); }; diff --git a/src/css/_cloud_dialog.scss b/src/css/_cloud_dialog.scss index aca353f1978..717aebabcef 100644 --- a/src/css/_cloud_dialog.scss +++ b/src/css/_cloud_dialog.scss @@ -43,6 +43,37 @@ word-wrap: break-word; } + .plotly-cloud-dialog-label { + display: block; + margin-top: 16px; + margin-bottom: 6px; + font-weight: bold; + } + + .plotly-cloud-dialog-input { + box-sizing: border-box; + width: 100%; + padding: 7px 10px; + + font-family: inherit; + font-size: 13px; + color: #2a3f5f; + + background-color: vars.$color-bg-light; + border: 1px solid vars.$color-bg-darker; + border-radius: 3px; + + &:focus { + outline: none; + border-color: vars.$color-brand-primary; + } + } + + .plotly-cloud-dialog-error { + margin-top: 6px; + color: vars.$color-inline-code; + } + .plotly-cloud-dialog-buttons { display: flex; justify-content: flex-end;