Skip to content

Commit 7ec85f3

Browse files
committed
Fix typing generation bugs #3017 #3266
1 parent 1653281 commit 7ec85f3

File tree

9 files changed

+133
-38
lines changed

9 files changed

+133
-38
lines changed

@plotly/dash-generator-test-component-typescript/generator.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,40 @@ describe('Test Typescript component metadata generation', () => {
289289
expect(propType.value[1].value).toBe('small');
290290
}
291291
)
292+
293+
test(
294+
'union of boolean and literal values', () => {
295+
const propType = R.path(
296+
propPath('TypeScriptComponent', 'boolean_enum').concat(
297+
'type'
298+
),
299+
metadata
300+
);
301+
expect(propType.name).toBe('union');
302+
expect(propType.value.length).toBe(3);
303+
expect(propType.value[0].name).toBe('bool');
304+
expect(propType.value[1].name).toBe('literal');
305+
expect(propType.value[2].name).toBe('literal');
306+
expect(propType.value[0].value).toBe(undefined);
307+
expect(propType.value[1].value).toBe('small');
308+
expect(propType.value[2].value).toBe('large');
309+
}
310+
)
311+
312+
test(
313+
'union of duplicated types', () => {
314+
const propType = R.path(
315+
propPath('TypeScriptComponent', 'duplicated_enum').concat(
316+
'type'
317+
),
318+
metadata
319+
);
320+
expect(propType.name).toBe('union');
321+
expect(propType.value.length).toBe(2);
322+
expect(propType.value[0].name).toBe('number');
323+
expect(propType.value[1].name).toBe('bool');
324+
}
325+
)
292326
});
293327

294328
describe('Test component comments', () => {

@plotly/dash-generator-test-component-typescript/src/props.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export type TypescriptComponentProps = {
4848
object_of_string?: {[k: string]: string};
4949
object_of_components?: {[k: string]: JSX.Element};
5050
ignored_prop?: {ignore: {me: string}};
51-
union_enum?: number | 'small' | 'large'
51+
union_enum?: number | 'small' | 'large';
52+
boolean_enum?: boolean | 'small' | 'large';
53+
duplicated_enum?: boolean | number | number;
5254
};
5355

5456
export type WrappedHTMLProps = {

components/dash-core-components/dash_core_components_base/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
"namespace": "dash",
123123
"dynamic": True,
124124
},
125+
{
126+
"dev_package_path": "dcc/proptypes.js",
127+
"dev_only": True,
128+
"namespace": "dash",
129+
}
125130
]
126131
)
127132

components/dash-core-components/src/components/Input.tsx

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ const convert = (val: unknown) => (isNumeric(val) ? +val : NaN);
1818
const isEquivalent = (v1: number, v2: number) =>
1919
v1 === v2 || (isNaN(v1) && isNaN(v2));
2020

21+
enum PersistenceTypes {
22+
'local' = 'local',
23+
'session' = 'session',
24+
'memory' = 'memory',
25+
}
26+
27+
enum PersistedProps {
28+
'value' = 'value',
29+
}
30+
31+
enum HTMLInputTypes {
32+
// Only allowing the input types with wide browser compatibility
33+
'text' = 'text',
34+
'number' = 'number',
35+
'password' = 'password',
36+
'email' = 'email',
37+
'range' = 'range',
38+
'search' = 'search',
39+
'tel' = 'tel',
40+
'url' = 'url',
41+
'hidden' = 'hidden',
42+
}
43+
2144
type InputProps = {
2245
/**
2346
* The value of the input
@@ -222,28 +245,19 @@ type InputProps = {
222245
* component or the page. Since only `value` is allowed this prop can
223246
* normally be ignored.
224247
*/
225-
persisted_props?: ['value'];
248+
persisted_props?: PersistedProps[];
226249
/**
227250
* Where persisted user changes will be stored:
228251
* memory: only kept in memory, reset on page refresh.
229252
* local: window.localStorage, data is kept after the browser quit.
230253
* session: window.sessionStorage, data is cleared once the browser quit.
231254
*/
232-
persistence_type?: 'local' | 'session' | 'memory';
255+
persistence_type?: PersistenceTypes;
233256

234257
/**
235258
* The type of control to render.
236259
*/
237-
type?: // Only allowing the input types with wide browser compatibility
238-
| 'text'
239-
| 'number'
240-
| 'password'
241-
| 'email'
242-
| 'range'
243-
| 'search'
244-
| 'tel'
245-
| 'url'
246-
| 'hidden';
260+
type?: HTMLInputTypes;
247261
};
248262

249263
const inputProps: (keyof InputProps)[] = [
@@ -268,15 +282,16 @@ const inputProps: (keyof InputProps)[] = [
268282
];
269283

270284
const defaultProps: Partial<InputProps> = {
271-
type: 'text',
285+
type: HTMLInputTypes.text,
286+
inputMode: 'verbatim',
272287
n_blur: 0,
273288
n_blur_timestamp: -1,
274289
n_submit: 0,
275290
n_submit_timestamp: -1,
276291
debounce: false,
277292
step: 'any',
278-
persisted_props: ['value'],
279-
persistence_type: 'local',
293+
persisted_props: [PersistedProps.value],
294+
persistence_type: PersistenceTypes.local,
280295
};
281296

282297
/**
@@ -286,14 +301,39 @@ const defaultProps: Partial<InputProps> = {
286301
* the Checklist and RadioItems component. Dates, times, and file uploads
287302
* are also supported through separate components.
288303
*/
289-
function Input(props: InputProps) {
290-
props = {...defaultProps, ...props};
304+
function Input({
305+
type = defaultProps.type,
306+
inputMode = defaultProps.inputMode,
307+
n_blur = defaultProps.n_blur,
308+
n_blur_timestamp = defaultProps.n_blur_timestamp,
309+
n_submit = defaultProps.n_submit,
310+
n_submit_timestamp = defaultProps.n_submit_timestamp,
311+
debounce = defaultProps.debounce,
312+
step = defaultProps.step,
313+
persisted_props = defaultProps.persisted_props,
314+
persistence_type = defaultProps.persistence_type,
315+
...rest
316+
}: InputProps) {
317+
const props = {
318+
type,
319+
inputMode,
320+
n_blur,
321+
n_blur_timestamp,
322+
n_submit,
323+
n_submit_timestamp,
324+
debounce,
325+
step,
326+
persisted_props,
327+
persistence_type,
328+
...rest,
329+
};
291330
const input = useRef(document.createElement('input'));
292331
const [value, setValue] = useState<InputProps['value']>(props.value);
293332
const [pendingEvent, setPendingEvent] = useState<number>();
294333
const inputId = useId();
295334

296-
const valprops = props.type === 'number' ? {} : {value: value ?? ''};
335+
const valprops =
336+
props.type === HTMLInputTypes.number ? {} : {value: value ?? ''};
297337
let {className} = props;
298338
className = 'dash-input' + (className ? ` ${className}` : '');
299339

@@ -314,7 +354,7 @@ function Input(props: InputProps) {
314354
const {value: inputValue} = input.current;
315355
const {setProps} = props;
316356
const valueAsNumber = convert(inputValue);
317-
if (props.type === 'number') {
357+
if (props.type === HTMLInputTypes.number) {
318358
setPropValue(props.value, valueAsNumber ?? value);
319359
} else {
320360
const propValue =
@@ -420,7 +460,7 @@ function Input(props: InputProps) {
420460
}
421461
const valueAsNumber = convert(value);
422462
setInputValue(valueAsNumber ?? value, props.value);
423-
if (props.type !== 'number') {
463+
if (props.type !== HTMLInputTypes.number) {
424464
setValue(props.value);
425465
}
426466
}, [props.value, props.type, pendingEvent]);
@@ -437,7 +477,7 @@ function Input(props: InputProps) {
437477
if (typeof debounce === 'number' && Number.isFinite(debounce)) {
438478
debounceEvent(debounce);
439479
}
440-
if (type !== 'number') {
480+
if (type !== HTMLInputTypes.number) {
441481
setTimeout(() => {
442482
input.current.setSelectionRange(
443483
cursorPosition,
@@ -452,7 +492,7 @@ function Input(props: InputProps) {
452492

453493
const pickedInputs = pick(inputProps, props);
454494

455-
const isNumberInput = props.type === 'number';
495+
const isNumberInput = props.type === HTMLInputTypes.number;
456496
const currentNumericValue = convert(input.current.value || '0');
457497
const minValue = convert(props.min);
458498
const maxValue = convert(props.max);
@@ -467,7 +507,9 @@ function Input(props: InputProps) {
467507
{loadingProps => (
468508
<div
469509
className={`dash-input-container ${className}${
470-
props.type === 'hidden' ? ' dash-input-hidden' : ''
510+
props.type === HTMLInputTypes.hidden
511+
? ' dash-input-hidden'
512+
: ''
471513
}`.trim()}
472514
style={props.style}
473515
>

components/dash-core-components/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export {
4040
Dropdown,
4141
Geolocation,
4242
Graph,
43-
Input,
4443
Interval,
44+
Input,
4545
Link,
4646
Loading,
4747
Location,

dash/development/_generate_prop_types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ def generate_any(*_):
6868

6969

7070
def generate_enum(prop_info):
71-
values = str([v.get("value") for v in prop_info["value"]])
72-
return f"pt.oneOf({values})"
71+
values = [v["value"] for v in prop_info["value"] if v.get("value") is not None]
72+
csv = ",".join(str(v) for v in values)
73+
return f"pt.oneOf([{csv}])"
7374

7475

7576
def generate_object_of(prop_info):

dash/extract-meta.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const BANNED_TYPES = [
6767
'ChildNode',
6868
'ParentNode',
6969
];
70-
const unionSupport = PRIMITIVES.concat('boolean', 'Element', 'enum');
70+
const unionSupport = PRIMITIVES.concat('true', 'false', 'Element', 'enum');
7171

7272
const reArray = new RegExp(`(${unionSupport.join('|')})\\[\\]`);
7373

@@ -269,8 +269,18 @@ function gatherComponents(sources, components = {}) {
269269
unionSupport.includes(typeName) ||
270270
isArray(checker.typeToString(t))
271271
);
272-
})
273-
.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType));
272+
});
273+
value = value.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType));
274+
275+
// de-dupe any types in this union
276+
value = value.reduce((acc, t) => {
277+
const key = `${t.name}:${t.value}`;
278+
if (!acc.seen.has(key)) {
279+
acc.seen.add(key);
280+
acc.result.push(t);
281+
}
282+
return acc;
283+
}, { seen: new Set(), result: [] }).result;
274284

275285
if (!value.length) {
276286
name = 'any';
@@ -285,7 +295,7 @@ function gatherComponents(sources, components = {}) {
285295
const getPropTypeName = propName => {
286296
if (propName.includes('=>') || propName === 'Function') {
287297
return 'func';
288-
} else if (propName === 'boolean') {
298+
} else if (['boolean', 'false', 'true'].includes(propName)) {
289299
return 'bool';
290300
} else if (propName === '[]') {
291301
return 'array';

tests/integration/callbacks/test_basic_callback.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def update_input(value):
9999
pad_input, pad_div = dash_duo.dash_innerhtml_dom.select_one(
100100
"#output > div"
101101
).contents
102+
pad_input = pad_input.next # get the input element, not the wrapper
102103

103104
assert (
104105
pad_input.attrs["value"] == "sub input initial value"

tests/integration/renderer/test_persistence.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -501,17 +501,17 @@ def set_out(val):
501501

502502
for _ in range(3):
503503
dash_duo.wait_for_text_to_equal(".out", "a")
504-
dash_duo.find_element(".persisted").send_keys("lpaca")
504+
dash_duo.find_element(".persisted input").send_keys("lpaca")
505505
dash_duo.wait_for_text_to_equal(".out", "alpaca")
506506

507-
dash_duo.find_element(".persistence-val").send_keys("s")
507+
dash_duo.find_element(".persistence-val input").send_keys("s")
508508
dash_duo.wait_for_text_to_equal(".out", "a")
509-
dash_duo.find_element(".persisted").send_keys("nchovies")
509+
dash_duo.find_element(".persisted input").send_keys("nchovies")
510510
dash_duo.wait_for_text_to_equal(".out", "anchovies")
511511

512-
dash_duo.find_element(".persistence-val").send_keys("2")
512+
dash_duo.find_element(".persistence-val input").send_keys("2")
513513
dash_duo.wait_for_text_to_equal(".out", "a")
514-
dash_duo.find_element(".persisted").send_keys(
514+
dash_duo.find_element(".persisted input").send_keys(
515515
Keys.BACK_SPACE
516516
) # persist falsy value
517517
dash_duo.wait_for_text_to_equal(".out", "")
@@ -521,9 +521,9 @@ def set_out(val):
521521
dash_duo.wait_for_text_to_equal(".out", "a")
522522

523523
# anchovies and aardvark saved
524-
dash_duo.find_element(".persistence-val").send_keys("s")
524+
dash_duo.find_element(".persistence-val input").send_keys("s")
525525
dash_duo.wait_for_text_to_equal(".out", "anchovies")
526-
dash_duo.find_element(".persistence-val").send_keys("2")
526+
dash_duo.find_element(".persistence-val input").send_keys("2")
527527
dash_duo.wait_for_text_to_equal(".out", "")
528528

529529
dash_duo.find_element("#btn").click()

0 commit comments

Comments
 (0)