Skip to content

Commit

Permalink
Support BigInt for controls
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Oct 24, 2023
1 parent 4ee42ac commit 08337a1
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 10 deletions.
1 change: 1 addition & 0 deletions code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
"process": "^0.11.10",
"raf": "^3.4.1",
"react": "^16.14.0",
"safe-stable-stringify": "^2.4.3",
"semver": "^7.3.7",
"serve-static": "^1.14.1",
"trash": "^7.0.0",
Expand Down
3 changes: 2 additions & 1 deletion code/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Symbols } from '@storybook/components';
import type { PreviewWeb } from '@storybook/preview-api';
import type { ReactRenderer } from '@storybook/react';
import type { Channel } from '@storybook/channels';
import { stringify } from 'safe-stable-stringify';

import { DocsContext } from '@storybook/blocks';

Expand Down Expand Up @@ -262,7 +263,7 @@ export const decorators = [
/>
<div style={{ marginTop: '1rem' }}>
Current <code>{parameters.withRawArg}</code>:{' '}
<pre>{JSON.stringify(args[parameters.withRawArg], null, 2) || 'undefined'}</pre>
<pre>{stringify(args[parameters.withRawArg], null, 2) || 'undefined'}</pre>
</div>
</>
);
Expand Down
1 change: 1 addition & 0 deletions code/ui/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"react-colorful": "^5.1.2",
"safe-stable-stringify": "^2.4.3",
"telejson": "^7.2.0",
"tocbot": "^4.20.1",
"ts-dedent": "^2.0.0",
Expand Down
41 changes: 41 additions & 0 deletions code/ui/blocks/src/controls/BigInt.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';
import { NumberControl } from './Number';

export default {
component: NumberControl,
tags: ['autodocs'],
parameters: { withRawArg: 'value', controls: { include: ['value', 'min', 'max', 'step'] } },
args: { name: 'number' },
} as Meta<typeof NumberControl>;

export const Undefined: StoryObj<typeof NumberControl> = {
args: { value: undefined },
};
// for security reasons a file input field cannot have an initial value, so it doesn't make sense to have stories for it

export const Ten: StoryObj<typeof NumberControl> = {
args: { value: 10 },
};
export const Zero: StoryObj<typeof NumberControl> = {
args: { value: 0 },
};

export const WithMin: StoryObj<typeof NumberControl> = {
args: { min: 1, value: 3 },
};
export const WithMax: StoryObj<typeof NumberControl> = {
args: { max: 7, value: 3 },
};
export const WithMinAndMax: StoryObj<typeof NumberControl> = {
args: { min: -2, max: 5, value: 3 },
};
export const LessThanMin: StoryObj<typeof NumberControl> = {
args: { min: 3, value: 1 },
};
export const MoreThanMax: StoryObj<typeof NumberControl> = {
args: { max: 3, value: 6 },
};

export const WithStep: StoryObj<typeof NumberControl> = {
args: { step: 5, value: 3 },
};
93 changes: 93 additions & 0 deletions code/ui/blocks/src/controls/BigInt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { FC, ChangeEvent } from 'react';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
import { getControlId, getControlSetterButtonId } from './helpers';

import type { ControlProps, NumberValue, NumberConfig } from './types';

const Wrapper = styled.label({
display: 'flex',
});

type NumberProps = ControlProps<NumberValue | null> & NumberConfig;

export const parse = (value: string) => {
const result = parseFloat(value);
return Number.isNaN(result) ? undefined : result;
};

export const format = (value: NumberValue) => (value != null ? String(value) : '');

export const NumberControl: FC<NumberProps> = ({
name,
value,
onChange,
min,
max,
step,
onBlur,
onFocus,
}) => {
const [inputValue, setInputValue] = useState(typeof value === 'number' ? value : '');
const [forceVisible, setForceVisible] = useState(false);
const [parseError, setParseError] = useState<Error>(null);

const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);

const result = parseFloat(event.target.value);
if (Number.isNaN(result)) {
setParseError(new Error(`'${event.target.value}' is not a number`));
} else {
onChange(result);
setParseError(null);
}
},
[onChange, setParseError]
);

const onForceVisible = useCallback(() => {
setInputValue('0');
onChange(0);
setForceVisible(true);
}, [setForceVisible]);

const htmlElRef = useRef(null);
useEffect(() => {
if (forceVisible && htmlElRef.current) htmlElRef.current.select();
}, [forceVisible]);

useEffect(() => {
const newInputValue = typeof value === 'number' ? value : '';
if (inputValue !== newInputValue) {
setInputValue(value);
}
}, [value]);

if (!forceVisible && value === undefined) {
return (
<Form.Button id={getControlSetterButtonId(name)} onClick={onForceVisible}>
Set number
</Form.Button>
);
}

return (
<Wrapper>
<Form.Input
ref={htmlElRef}
id={getControlId(name)}
type="number"
onChange={handleChange}
size="flex"
placeholder="Edit number..."
value={inputValue}
valid={parseError ? 'error' : null}
autoFocus={forceVisible}
{...{ name, min, max, step, onFocus, onBlur }}
/>
</Wrapper>
);
};
2 changes: 1 addition & 1 deletion code/ui/blocks/src/controls/Object.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const Object: StoryObj<typeof ObjectControl> = {
value: {
name: 'Michael',
someDate: new Date('2022-10-30T12:31:11'),
nested: { someBool: true, someNumber: 22 },
nested: { someBool: true, someNumber: 22, someBigInt: 999n },
},
},
};
Expand Down
19 changes: 17 additions & 2 deletions code/ui/blocks/src/controls/Object.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,16 @@ export const ObjectControl: FC<ObjectProps> = ({ name, value, onChange }) => {
const updateRaw: (raw: string) => void = useCallback(
(raw) => {
try {
if (raw) onChange(JSON.parse(raw));
if (raw) {
const parsed = JSON.parse(raw, (_, val) =>
typeof val === 'string' && val.endsWith('$$bigInt$$')
? BigInt(val.replace('$$bigInt$$', ''))
: val
);

onChange(parsed);
}

setParseError(undefined);
} catch (e) {
setParseError(e);
Expand Down Expand Up @@ -271,12 +280,18 @@ export const ObjectControl: FC<ObjectProps> = ({ name, value, onChange }) => {
);
}

const stringified = JSON.stringify(
value,
(_, val) => (typeof val === 'bigint' ? `${val.toString()}$$bigInt$$` : val),
2
);

const rawJSONForm = (
<RawInput
ref={htmlElRef}
id={getControlId(name)}
name={name}
defaultValue={value === null ? '' : JSON.stringify(value, null, 2)}
defaultValue={value === null ? '' : stringified}
onBlur={(event: FocusEvent<HTMLTextAreaElement>) => updateRaw(event.target.value)}
placeholder="Edit JSON string..."
autoFocus={forceVisible}
Expand Down
29 changes: 26 additions & 3 deletions code/ui/blocks/src/controls/react-editable-json-tree/JsonNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type { ReactElement } from 'react';
import React, { cloneElement, Component } from 'react';
import stringify from 'safe-stable-stringify';
import * as inputUsageTypes from './types/inputUsageTypes';

import * as dataTypes from './types/dataTypes';
Expand Down Expand Up @@ -1005,6 +1006,27 @@ export class JsonNode extends Component<JsonNodeProps, JsonNodeState> {
onSubmitValueParser={onSubmitValueParser}
/>
);
case dataTypes.BIG_INT:
return (
<JsonValue
name={name}
value={`${data.toString()}n`}
originalValue={data}
keyPath={keyPath}
deep={deep}
handleRemove={handleRemove}
handleUpdateValue={handleUpdateValue}
readOnly={readOnly}
dataType={dataType}
getStyle={getStyle}
cancelButtonElement={cancelButtonElement}
editButtonElement={editButtonElement}
inputElementGenerator={inputElementGenerator}
minusMenuElement={minusMenuElement}
logger={logger}
onSubmitValueParser={onSubmitValueParser}
/>
);
default:
return null;
}
Expand Down Expand Up @@ -1453,11 +1475,12 @@ export class JsonValue extends Component<JsonValueProps, JsonValueState> {
}

handleEdit() {
const { handleUpdateValue, originalValue, logger, onSubmitValueParser, keyPath } = this.props;
const { handleUpdateValue, originalValue, logger, onSubmitValueParser, keyPath, dataType } =
this.props;
const { inputRef, name, deep } = this.state;
if (!inputRef) return;

const newValue = onSubmitValueParser(true, keyPath, deep, name, inputRef.value);
const newValue = onSubmitValueParser(true, keyPath, deep, name, inputRef.value, dataType);

const result = {
value: newValue,
Expand Down Expand Up @@ -1527,7 +1550,7 @@ export class JsonValue extends Component<JsonValueProps, JsonValueState> {
});
const inputElementLayout = cloneElement(inputElement, {
ref: this.refInput,
defaultValue: JSON.stringify(originalValue),
defaultValue: stringify(originalValue),
});
const minusMenuLayout = cloneElement(minusMenuElement, {
onClick: handleRemove,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ JsonTree.defaultProps = {
beforeAddAction: () => Promise.resolve(),
beforeUpdateAction: () => Promise.resolve(),
logger: { error: () => {} },
onSubmitValueParser: (isEditMode, keyPath, deep, name, rawValue) => parse(rawValue),
onSubmitValueParser: (isEditMode, keyPath, deep, name, rawValue, dataType) =>
parse(rawValue, dataType),
inputElement: () => <input />,
textareaElement: () => <textarea />,
fallback: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,19 @@ const NULL = 'Null';
const UNDEFINED = 'Undefined';
const FUNCTION = 'Function';
const SYMBOL = 'Symbol';
const BIG_INT = 'BigInt';

export { ERROR, OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, DATE, NULL, UNDEFINED, FUNCTION, SYMBOL };
export {
ERROR,
OBJECT,
ARRAY,
STRING,
NUMBER,
BOOLEAN,
DATE,
NULL,
UNDEFINED,
FUNCTION,
SYMBOL,
BIG_INT,
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import * as DATA_TYPES from '../types/dataTypes';

/**
* Parse.
* @param string {String} string to parse
* @returns {*}
*/
export function parse(string: string) {
export function parse(string: string, dataType: typeof DATA_TYPES[keyof typeof DATA_TYPES]) {
let result = string;

// Check if string contains 'function' and start with it to eval it
if (result.indexOf('function') === 0) {
return (0, eval)(`(${result})`); // eslint-disable-line no-eval
}

if (dataType === DATA_TYPES.BIG_INT) {
return BigInt(string);
}

try {
result = JSON.parse(string);
} catch (e) {
Expand Down
9 changes: 9 additions & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6502,6 +6502,7 @@ __metadata:
memoizerific: ^1.11.3
polished: ^4.2.2
react-colorful: ^5.1.2
safe-stable-stringify: ^2.4.3
telejson: ^7.2.0
tocbot: ^4.20.1
ts-dedent: ^2.0.0
Expand Down Expand Up @@ -7942,6 +7943,7 @@ __metadata:
process: ^0.11.10
raf: ^3.4.1
react: ^16.14.0
safe-stable-stringify: ^2.4.3
semver: ^7.3.7
serve-static: ^1.14.1
trash: ^7.0.0
Expand Down Expand Up @@ -28596,6 +28598,13 @@ __metadata:
languageName: node
linkType: hard

"safe-stable-stringify@npm:^2.4.3":
version: 2.4.3
resolution: "safe-stable-stringify@npm:2.4.3"
checksum: 81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768
languageName: node
linkType: hard

"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
Expand Down

0 comments on commit 08337a1

Please sign in to comment.