diff --git a/build/plotcss.js b/build/plotcss.js index 75d2ff3967d..0f9c9abcfec 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -55,11 +55,19 @@ var rules = { "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-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;", - "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel": "background-color:#f3f3f3;margin-left:auto;color:#777;", - "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel:hover": "background-color:#e0e2e5;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--cancel,X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel": "background-color:#f3f3f3;margin-left:auto;color:#777;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--cancel:hover,X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel:hover": "background-color:#e0e2e5;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--confirm": "background-color:#447adb;color:#fff;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--confirm:hover": "background-color:#1d3b84;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--custom": "background-color:#fff;color:#777;font-size:11px;padding-left:0;margin-left:0;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn.plotly-cloud-dialog-btn--custom:hover": "text-decoration:underline;", "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--confirm": "background-color:#447adb;color:#fff;", "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--confirm:hover": "background-color:#1d3b84;", Y: "font-family:\"Open Sans\",verdana,arial,sans-serif;position:fixed;top:50px;right:20px;z-index:10000;font-size:10pt;max-width:180px;", 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/cloud_confirm.js b/src/components/modebar/cloud_confirm.js index d1c715acca3..628685e40e2 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,7 +36,7 @@ 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')); var serverUrlText = new URL(serverUrl).hostname; @@ -40,6 +46,29 @@ module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { description.append('span').text(serverUrlText).classed('plotly-cloud-dialog-message--hostname', true); description.append('span').text('. '); + // 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, 'Chart Server 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:///'); + + 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); @@ -58,6 +87,52 @@ 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(); + }); + + var customBtn = buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--custom', true) + .attr('type', 'button') + .text(_(gd, 'Share to Dash Enterprise')); + + customBtn.on('click', function() { + customBtn.style('display', 'none'); + urlField.style('display', ''); + description.text(_(gd, 'Enter the url of your Dash Enterprise chart server below or contact Plotly support to get set up. This chart and its data will be sent to the URL you provide.')); + input.node().focus(); + }); + buttons.append('button') .classed('plotly-cloud-dialog-btn', true) .classed('plotly-cloud-dialog-btn--cancel', true) @@ -68,8 +143,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 fdf66fcee0f..e357990c51a 100644 --- a/src/css/_cloud_dialog.scss +++ b/src/css/_cloud_dialog.scss @@ -47,6 +47,37 @@ } } + .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; @@ -68,6 +99,7 @@ outline-offset: 1px; } + &.plotly-cloud-dialog-btn--cancel, &--cancel { background-color: vars.$color-bg-base; margin-left: auto; @@ -78,6 +110,27 @@ } } + &.plotly-cloud-dialog-btn--confirm { + background-color: vars.$color-brand-primary; + color: vars.$color-bg-light; + + &:hover { + background-color: vars.$color-brand-accent; + } + } + + &.plotly-cloud-dialog-btn--custom { + background-color: vars.$color-bg-light; + color: vars.$color-muted-text; + font-size: 11px; + padding-left: 0; + margin-left: 0; + + &:hover { + text-decoration: underline; + } + } + &--confirm { background-color: vars.$color-brand-primary; color: vars.$color-bg-light;