Skip to content

Commit 78d07c4

Browse files
committed
Merge branch 'dev' into master-2.15.0
# Conflicts: # dash/_dash_renderer.py
2 parents 70d9df9 + 6a8da52 commit 78d07c4

35 files changed

+9819
-7980
lines changed

CHANGELOG.md

+18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [2.15.0] - 2024-01-31
6+
7+
## Added
8+
- [#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)
9+
- [#2723](https://github.com/plotly/dash/pull/2723) Improve dcc Slider/RangeSlider tooltips. Fixes [#1846](https://github.com/plotly/dash/issues/1846)
10+
- Add `tooltip.template` a string for the format template, {value} will be formatted with the actual value.
11+
- Add `tooltip.style` a style object to give to the div of the tooltip.
12+
- Add `tooltip.transform` a reference to a function in the `window.dccFunctions` namespace.
13+
- [#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")})`
14+
15+
## Fixed
16+
17+
- [#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)
18+
19+
## Changed
20+
- [#2652](https://github.com/plotly/dash/pull/2652) dcc.Clipboard supports htm_content and triggers a copy to clipboard when n_clicks are changed
21+
- [#2721](https://github.com/plotly/dash/pull/2721) Remove ansi2html, fixes [#2613](https://github.com/plotly/dash/issues/2713)
22+
523
## [2.14.2] - 2023-11-27
624

725
## Fixed

components/dash-core-components/package-lock.json

+2,953-2,148
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/dash-core-components/package.json

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-core-components",
3-
"version": "2.12.1",
3+
"version": "2.13.0",
44
"description": "Core component suite for Dash",
55
"repository": {
66
"type": "git",
@@ -35,6 +35,7 @@
3535
"maintainer": "Alex Johnson <alex@plotly.com>",
3636
"license": "MIT",
3737
"dependencies": {
38+
"@braintree/sanitize-url": "^7.0.0",
3839
"@fortawesome/fontawesome-svg-core": "1.2.36",
3940
"@fortawesome/free-regular-svg-icons": "^5.15.4",
4041
"@fortawesome/free-solid-svg-icons": "^5.15.4",
@@ -49,7 +50,7 @@
4950
"moment": "^2.29.4",
5051
"node-polyfill-webpack-plugin": "^2.0.1",
5152
"prop-types": "^15.8.1",
52-
"ramda": "^0.29.0",
53+
"ramda": "^0.29.1",
5354
"rc-slider": "^9.7.5",
5455
"react-addons-shallow-compare": "^15.6.3",
5556
"react-dates": "^21.8.0",
@@ -64,11 +65,11 @@
6465
"uniqid": "^5.4.0"
6566
},
6667
"devDependencies": {
67-
"@babel/cli": "^7.23.0",
68-
"@babel/core": "^7.23.0",
68+
"@babel/cli": "^7.23.4",
69+
"@babel/core": "^7.23.7",
6970
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
70-
"@babel/preset-env": "^7.22.20",
71-
"@babel/preset-react": "^7.22.15",
71+
"@babel/preset-env": "^7.23.8",
72+
"@babel/preset-react": "^7.23.3",
7273
"@plotly/dash-component-plugins": "^1.2.3",
7374
"@plotly/webpack-dash-dynamic-import": "^1.3.0",
7475
"babel-loader": "^9.1.3",
@@ -88,9 +89,10 @@
8889
"react-jsx-parser": "1.21.0",
8990
"style-loader": "^3.3.3",
9091
"styled-jsx": "^3.4.4",
91-
"webpack": "^5.88.2",
92+
"webpack": "^5.90.0",
9293
"webpack-cli": "^5.1.4"
9394
},
95+
"optionalDependencies": { "fsevents": "*" },
9496
"files": [
9597
"/dash_core_components/*{.js,.map}",
9698
"/lib/"

components/dash-core-components/src/components/Clipboard.react.js

+45-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class Clipboard extends React.Component {
1717
constructor(props) {
1818
super(props);
1919
this.copyToClipboard = this.copyToClipboard.bind(this);
20+
this.onClickHandler = this.onClickHandler.bind(this);
2021
this.copySuccess = this.copySuccess.bind(this);
2122
this.getTargetText = this.getTargetText.bind(this);
2223
this.loading = this.loading.bind(this);
@@ -26,6 +27,22 @@ export default class Clipboard extends React.Component {
2627
};
2728
}
2829

30+
onClickHandler() {
31+
this.props.setProps({n_clicks: this.props.n_clicks + 1});
32+
}
33+
34+
componentDidUpdate(prevProps) {
35+
// If the clicks has not changed, do nothing
36+
if (
37+
!this.props.n_clicks ||
38+
this.props.n_clicks === prevProps.n_clicks
39+
) {
40+
return;
41+
}
42+
// If the clicks has changed, copy to clipboard
43+
this.copyToClipboard();
44+
}
45+
2946
// stringifies object ids used in pattern matching callbacks
3047
stringifyId(id) {
3148
if (typeof id !== 'object') {
@@ -38,9 +55,23 @@ export default class Clipboard extends React.Component {
3855
return '{' + parts.join(',') + '}';
3956
}
4057

41-
async copySuccess(content) {
58+
async copySuccess(content, htmlContent) {
4259
const showCopiedIcon = 1000;
43-
await clipboardAPI.writeText(content);
60+
if (htmlContent) {
61+
const blobHtml = new Blob([htmlContent], {type: 'text/html'});
62+
const blobText = new Blob([content ?? htmlContent], {
63+
type: 'text/plain',
64+
});
65+
const data = [
66+
new ClipboardItem({
67+
['text/plain']: blobText,
68+
['text/html']: blobHtml,
69+
}),
70+
];
71+
await navigator.clipboard.write(data);
72+
} else {
73+
await clipboardAPI.writeText(content);
74+
}
4475
this.setState({copied: true});
4576
await wait(showCopiedIcon);
4677
this.setState({copied: false});
@@ -71,20 +102,18 @@ export default class Clipboard extends React.Component {
71102
}
72103

73104
async copyToClipboard() {
74-
this.props.setProps({
75-
n_clicks: this.props.n_clicks + 1,
76-
});
77-
78105
let content;
106+
let htmlContent;
79107
if (this.props.target_id) {
80108
content = this.getTargetText();
81109
} else {
82110
await wait(100); // gives time for callback to start
83111
await this.loading();
84112
content = this.props.content;
113+
htmlContent = this.props.html_content;
85114
}
86-
if (content) {
87-
this.copySuccess(content);
115+
if (content || htmlContent) {
116+
this.copySuccess(content, htmlContent);
88117
}
89118
}
90119

@@ -106,7 +135,7 @@ export default class Clipboard extends React.Component {
106135
title={title}
107136
style={style}
108137
className={className}
109-
onClick={this.copyToClipboard}
138+
onClick={this.onClickHandler}
110139
data-dash-is-loading={
111140
(loading_state && loading_state.is_loading) || undefined
112141
}
@@ -119,6 +148,7 @@ export default class Clipboard extends React.Component {
119148

120149
Clipboard.defaultProps = {
121150
content: null,
151+
html_content: null,
122152
target_id: null,
123153
n_clicks: 0,
124154
};
@@ -137,7 +167,7 @@ Clipboard.propTypes = {
137167
target_id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
138168

139169
/**
140-
* The text to be copied to the clipboard if the `target_id` is None.
170+
* The text to be copied to the clipboard if the `target_id` is None.
141171
*/
142172
content: PropTypes.string,
143173

@@ -146,6 +176,11 @@ Clipboard.propTypes = {
146176
*/
147177
n_clicks: PropTypes.number,
148178

179+
/**
180+
* The clipboard html text be copied to the clipboard if the `target_id` is None.
181+
*/
182+
html_content: PropTypes.string,
183+
149184
/**
150185
* The text shown as a tooltip when hovering over the copy icon.
151186
*/

components/dash-core-components/src/components/Link.react.js

+47-46
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import PropTypes from 'prop-types';
22

3-
import React, {Component} from 'react';
4-
3+
import React, {useEffect, useMemo} from 'react';
4+
import {sanitizeUrl} from '@braintree/sanitize-url';
55
import {isNil} from 'ramda';
66

77
/*
@@ -33,15 +33,23 @@ CustomEvent.prototype = window.Event.prototype;
3333
* For links with destinations outside the current app, `html.A` is a better
3434
* component to use.
3535
*/
36-
export default class Link extends Component {
37-
constructor(props) {
38-
super(props);
39-
this.updateLocation = this.updateLocation.bind(this);
40-
}
36+
const Link = props => {
37+
const {
38+
className,
39+
style,
40+
id,
41+
href,
42+
loading_state,
43+
children,
44+
title,
45+
target,
46+
refresh,
47+
setProps,
48+
} = props;
49+
const sanitizedUrl = useMemo(() => sanitizeUrl(href), [href]);
4150

42-
updateLocation(e) {
51+
const updateLocation = e => {
4352
const hasModifiers = e.metaKey || e.shiftKey || e.altKey || e.ctrlKey;
44-
const {href, refresh, target} = this.props;
4553

4654
if (hasModifiers) {
4755
return;
@@ -52,49 +60,40 @@ export default class Link extends Component {
5260
// prevent anchor from updating location
5361
e.preventDefault();
5462
if (refresh) {
55-
window.location = href;
63+
window.location = sanitizedUrl;
5664
} else {
57-
window.history.pushState({}, '', href);
65+
window.history.pushState({}, '', sanitizedUrl);
5866
window.dispatchEvent(new CustomEvent('_dashprivate_pushstate'));
5967
}
6068
// scroll back to top
6169
window.scrollTo(0, 0);
62-
}
70+
};
6371

64-
render() {
65-
const {
66-
className,
67-
style,
68-
id,
69-
href,
70-
loading_state,
71-
children,
72-
title,
73-
target,
74-
} = this.props;
75-
/*
76-
* ideally, we would use cloneElement however
77-
* that doesn't work with dash's recursive
78-
* renderTree implementation for some reason
79-
*/
80-
return (
81-
<a
82-
data-dash-is-loading={
83-
(loading_state && loading_state.is_loading) || undefined
84-
}
85-
id={id}
86-
className={className}
87-
style={style}
88-
href={href}
89-
onClick={e => this.updateLocation(e)}
90-
title={title}
91-
target={target}
92-
>
93-
{isNil(children) ? href : children}
94-
</a>
95-
);
96-
}
97-
}
72+
useEffect(() => {
73+
if (sanitizedUrl !== href) {
74+
setProps({
75+
_dash_error: new Error(`Dangerous link detected:: ${href}`),
76+
});
77+
}
78+
}, [href, sanitizedUrl]);
79+
80+
return (
81+
<a
82+
data-dash-is-loading={
83+
(loading_state && loading_state.is_loading) || undefined
84+
}
85+
id={id}
86+
className={className}
87+
style={style}
88+
href={sanitizedUrl}
89+
onClick={updateLocation}
90+
title={title}
91+
target={target}
92+
>
93+
{isNil(children) ? sanitizedUrl : children}
94+
</a>
95+
);
96+
};
9897

9998
Link.propTypes = {
10099
/**
@@ -151,8 +150,10 @@ Link.propTypes = {
151150
*/
152151
component_name: PropTypes.string,
153152
}),
153+
setProps: PropTypes.func,
154154
};
155155

156156
Link.defaultProps = {
157157
refresh: false,
158158
};
159+
export default Link;

components/dash-core-components/src/components/RangeSlider.react.js

+25
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@ RangeSlider.propTypes = {
125125
'bottomLeft',
126126
'bottomRight',
127127
]),
128+
/**
129+
* Template string to display the tooltip in.
130+
* Must contain `{value}`, which will be replaced with either
131+
* the default string representation of the value or the result of the
132+
* transform function if there is one.
133+
*/
134+
template: PropTypes.string,
135+
/**
136+
* Custom style for the tooltip.
137+
*/
138+
style: PropTypes.object,
139+
/**
140+
* Reference to a function in the `window.dccFunctions` namespace.
141+
* This can be added in a script in the asset folder.
142+
*
143+
* For example, in `assets/tooltip.js`:
144+
* ```
145+
* window.dccFunctions = window.dccFunctions || {};
146+
* window.dccFunctions.multByTen = function(value) {
147+
* return value * 10;
148+
* }
149+
* ```
150+
* Then in the component `tooltip={'transform': 'multByTen'}`
151+
*/
152+
transform: PropTypes.string,
128153
}),
129154

130155
/**

0 commit comments

Comments
 (0)