Skip to content

Commit

Permalink
Merge pull request #2739 from plotly/master-2.15.0
Browse files Browse the repository at this point in the history
Master 2.15.0
  • Loading branch information
T4rk1n authored Jan 31, 2024
2 parents 70d9df9 + 9243f93 commit 115aa4e
Show file tree
Hide file tree
Showing 77 changed files with 10,036 additions and 8,079 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [2.15.0] - 2024-01-31

## Added
- [#2695](https://github.com/plotly/dash/pull/2695) Adds `triggered_id` to `dash_clientside.callback_context`. Fixes [#2692](https://github.com/plotly/dash/issues/2692)
- [#2723](https://github.com/plotly/dash/pull/2723) Improve dcc Slider/RangeSlider tooltips. Fixes [#1846](https://github.com/plotly/dash/issues/1846)
- Add `tooltip.template` a string for the format template, {value} will be formatted with the actual value.
- Add `tooltip.style` a style object to give to the div of the tooltip.
- Add `tooltip.transform` a reference to a function in the `window.dccFunctions` namespace.
- [#2732](https://github.com/plotly/dash/pull/2732) Add special key `_dash_error` to `setProps`, allowing component developers to send error without throwing in render. Usage `props.setProps({_dash_error: new Error("custom error")})`

## Fixed

- [#2732](https://github.com/plotly/dash/pull/2732) Sanitize html props that are vulnerable to xss vulnerability if user data is inserted. Fix Validate url to prevent XSS attacks [#2729](https://github.com/plotly/dash/issues/2729)

## Changed
- [#2652](https://github.com/plotly/dash/pull/2652) dcc.Clipboard supports htm_content and triggers a copy to clipboard when n_clicks are changed
- [#2721](https://github.com/plotly/dash/pull/2721) Remove ansi2html, fixes [#2613](https://github.com/plotly/dash/issues/2713)

## [2.14.2] - 2023-11-27

## Fixed
Expand Down
5,101 changes: 2,953 additions & 2,148 deletions components/dash-core-components/package-lock.json

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions components/dash-core-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-core-components",
"version": "2.12.1",
"version": "2.13.0",
"description": "Core component suite for Dash",
"repository": {
"type": "git",
Expand Down Expand Up @@ -35,6 +35,7 @@
"maintainer": "Alex Johnson <alex@plotly.com>",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.0.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
Expand All @@ -49,7 +50,7 @@
"moment": "^2.29.4",
"node-polyfill-webpack-plugin": "^2.0.1",
"prop-types": "^15.8.1",
"ramda": "^0.29.0",
"ramda": "^0.29.1",
"rc-slider": "^9.7.5",
"react-addons-shallow-compare": "^15.6.3",
"react-dates": "^21.8.0",
Expand All @@ -64,11 +65,11 @@
"uniqid": "^5.4.0"
},
"devDependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.0",
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.22.15",
"@babel/preset-env": "^7.23.8",
"@babel/preset-react": "^7.23.3",
"@plotly/dash-component-plugins": "^1.2.3",
"@plotly/webpack-dash-dynamic-import": "^1.3.0",
"babel-loader": "^9.1.3",
Expand All @@ -88,9 +89,10 @@
"react-jsx-parser": "1.21.0",
"style-loader": "^3.3.3",
"styled-jsx": "^3.4.4",
"webpack": "^5.88.2",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
},
"optionalDependencies": { "fsevents": "*" },
"files": [
"/dash_core_components/*{.js,.map}",
"/lib/"
Expand Down
55 changes: 45 additions & 10 deletions components/dash-core-components/src/components/Clipboard.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class Clipboard extends React.Component {
constructor(props) {
super(props);
this.copyToClipboard = this.copyToClipboard.bind(this);
this.onClickHandler = this.onClickHandler.bind(this);
this.copySuccess = this.copySuccess.bind(this);
this.getTargetText = this.getTargetText.bind(this);
this.loading = this.loading.bind(this);
Expand All @@ -26,6 +27,22 @@ export default class Clipboard extends React.Component {
};
}

onClickHandler() {
this.props.setProps({n_clicks: this.props.n_clicks + 1});
}

componentDidUpdate(prevProps) {
// If the clicks has not changed, do nothing
if (
!this.props.n_clicks ||
this.props.n_clicks === prevProps.n_clicks
) {
return;
}
// If the clicks has changed, copy to clipboard
this.copyToClipboard();
}

// stringifies object ids used in pattern matching callbacks
stringifyId(id) {
if (typeof id !== 'object') {
Expand All @@ -38,9 +55,23 @@ export default class Clipboard extends React.Component {
return '{' + parts.join(',') + '}';
}

async copySuccess(content) {
async copySuccess(content, htmlContent) {
const showCopiedIcon = 1000;
await clipboardAPI.writeText(content);
if (htmlContent) {
const blobHtml = new Blob([htmlContent], {type: 'text/html'});
const blobText = new Blob([content ?? htmlContent], {
type: 'text/plain',
});
const data = [
new ClipboardItem({
['text/plain']: blobText,
['text/html']: blobHtml,
}),
];
await navigator.clipboard.write(data);
} else {
await clipboardAPI.writeText(content);
}
this.setState({copied: true});
await wait(showCopiedIcon);
this.setState({copied: false});
Expand Down Expand Up @@ -71,20 +102,18 @@ export default class Clipboard extends React.Component {
}

async copyToClipboard() {
this.props.setProps({
n_clicks: this.props.n_clicks + 1,
});

let content;
let htmlContent;
if (this.props.target_id) {
content = this.getTargetText();
} else {
await wait(100); // gives time for callback to start
await this.loading();
content = this.props.content;
htmlContent = this.props.html_content;
}
if (content) {
this.copySuccess(content);
if (content || htmlContent) {
this.copySuccess(content, htmlContent);
}
}

Expand All @@ -106,7 +135,7 @@ export default class Clipboard extends React.Component {
title={title}
style={style}
className={className}
onClick={this.copyToClipboard}
onClick={this.onClickHandler}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
Expand All @@ -119,6 +148,7 @@ export default class Clipboard extends React.Component {

Clipboard.defaultProps = {
content: null,
html_content: null,
target_id: null,
n_clicks: 0,
};
Expand All @@ -137,7 +167,7 @@ Clipboard.propTypes = {
target_id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),

/**
* The text to be copied to the clipboard if the `target_id` is None.
* The text to be copied to the clipboard if the `target_id` is None.
*/
content: PropTypes.string,

Expand All @@ -146,6 +176,11 @@ Clipboard.propTypes = {
*/
n_clicks: PropTypes.number,

/**
* The clipboard html text be copied to the clipboard if the `target_id` is None.
*/
html_content: PropTypes.string,

/**
* The text shown as a tooltip when hovering over the copy icon.
*/
Expand Down
93 changes: 47 additions & 46 deletions components/dash-core-components/src/components/Link.react.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';

import React, {Component} from 'react';

import React, {useEffect, useMemo} from 'react';
import {sanitizeUrl} from '@braintree/sanitize-url';
import {isNil} from 'ramda';

/*
Expand Down Expand Up @@ -33,15 +33,23 @@ CustomEvent.prototype = window.Event.prototype;
* For links with destinations outside the current app, `html.A` is a better
* component to use.
*/
export default class Link extends Component {
constructor(props) {
super(props);
this.updateLocation = this.updateLocation.bind(this);
}
const Link = props => {
const {
className,
style,
id,
href,
loading_state,
children,
title,
target,
refresh,
setProps,
} = props;
const sanitizedUrl = useMemo(() => sanitizeUrl(href), [href]);

updateLocation(e) {
const updateLocation = e => {
const hasModifiers = e.metaKey || e.shiftKey || e.altKey || e.ctrlKey;
const {href, refresh, target} = this.props;

if (hasModifiers) {
return;
Expand All @@ -52,49 +60,40 @@ export default class Link extends Component {
// prevent anchor from updating location
e.preventDefault();
if (refresh) {
window.location = href;
window.location = sanitizedUrl;
} else {
window.history.pushState({}, '', href);
window.history.pushState({}, '', sanitizedUrl);
window.dispatchEvent(new CustomEvent('_dashprivate_pushstate'));
}
// scroll back to top
window.scrollTo(0, 0);
}
};

render() {
const {
className,
style,
id,
href,
loading_state,
children,
title,
target,
} = this.props;
/*
* ideally, we would use cloneElement however
* that doesn't work with dash's recursive
* renderTree implementation for some reason
*/
return (
<a
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
id={id}
className={className}
style={style}
href={href}
onClick={e => this.updateLocation(e)}
title={title}
target={target}
>
{isNil(children) ? href : children}
</a>
);
}
}
useEffect(() => {
if (sanitizedUrl !== href) {
setProps({
_dash_error: new Error(`Dangerous link detected:: ${href}`),
});
}
}, [href, sanitizedUrl]);

return (
<a
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
id={id}
className={className}
style={style}
href={sanitizedUrl}
onClick={updateLocation}
title={title}
target={target}
>
{isNil(children) ? sanitizedUrl : children}
</a>
);
};

Link.propTypes = {
/**
Expand Down Expand Up @@ -151,8 +150,10 @@ Link.propTypes = {
*/
component_name: PropTypes.string,
}),
setProps: PropTypes.func,
};

Link.defaultProps = {
refresh: false,
};
export default Link;
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,31 @@ RangeSlider.propTypes = {
'bottomLeft',
'bottomRight',
]),
/**
* Template string to display the tooltip in.
* Must contain `{value}`, which will be replaced with either
* the default string representation of the value or the result of the
* transform function if there is one.
*/
template: PropTypes.string,
/**
* Custom style for the tooltip.
*/
style: PropTypes.object,
/**
* Reference to a function in the `window.dccFunctions` namespace.
* This can be added in a script in the asset folder.
*
* For example, in `assets/tooltip.js`:
* ```
* window.dccFunctions = window.dccFunctions || {};
* window.dccFunctions.multByTen = function(value) {
* return value * 10;
* }
* ```
* Then in the component `tooltip={'transform': 'multByTen'}`
*/
transform: PropTypes.string,
}),

/**
Expand Down
Loading

0 comments on commit 115aa4e

Please sign in to comment.