Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c9f8a8f
[dependencies][lib/components] Add react-editable-json-tree
hydrosquall Oct 20, 2020
718299b
(feat:controls) Add React-editable-tree for Object type
hydrosquall Oct 20, 2020
87149e8
(feat:controls) Update documentation examples for object controls to …
hydrosquall Oct 20, 2020
c93b353
Merge branch 'next' into pr/12824
shilman Dec 2, 2020
d591865
Merge branch 'next' into pr/12824
shilman Dec 2, 2020
866f4b2
Merge branch 'next' into cameron.yick/feature/ui-editable-json-tree-knob
ghengeveld Feb 18, 2021
f30528e
Merge remote-tracking branch 'origin/next' into cameron.yick/feature/…
ghengeveld Feb 18, 2021
e3e2415
Fix lockfile
ghengeveld Feb 18, 2021
cba8343
Many styling tweaks
ghengeveld Feb 19, 2021
91d51d3
More tweaks
ghengeveld Feb 19, 2021
d9c58c2
Design tweaks
ghengeveld Feb 22, 2021
51e6b81
Copy react-editable-json-tree into the project
ghengeveld Feb 22, 2021
adf06ce
Replace react-hotkeys with own key listeners.
ghengeveld Feb 22, 2021
ed2eced
Fix first-child warning
ghengeveld Feb 22, 2021
c8967b5
Replace componentWillReceiveProps with getDerivedStateFromProps
ghengeveld Feb 22, 2021
063af6a
Many tweaks and fixes
ghengeveld Feb 22, 2021
af7cb07
Delete objects and arrays rather than nullifying them
ghengeveld Feb 22, 2021
55d3df4
Dark theme tweaks
ghengeveld Feb 22, 2021
609b901
Tweaks
ghengeveld Feb 22, 2021
5c03dbb
Cleanup
ghengeveld Feb 22, 2021
2f2896a
Drop prop-types and comment blocks
ghengeveld Feb 22, 2021
fa10ff5
Improve addon-controls story
ghengeveld Feb 22, 2021
ca34d99
Use text control for unknown arg types
ghengeveld Feb 22, 2021
9168b45
Add support for entering raw JSON
ghengeveld Feb 22, 2021
5bec7e0
Use JSON editor for arrays
ghengeveld Feb 22, 2021
0472b6c
Fix standalone style
ghengeveld Feb 22, 2021
3a72d71
Remove unused import
ghengeveld Feb 22, 2021
ca4ccb9
Fix data sync
ghengeveld Feb 22, 2021
26ec6b2
Restore propTypes and fix string value editing
ghengeveld Feb 22, 2021
63d3284
Merge branch 'next' into cameron.yick/feature/ui-editable-json-tree-knob
ghengeveld Feb 22, 2021
10dc235
Support removing the root node
ghengeveld Feb 23, 2021
3d7f7a7
Add RAW toggle and fix menu icon hover style
ghengeveld Feb 23, 2021
c3e79d4
Style tweaks
ghengeveld Feb 23, 2021
e13c905
Fix null handling
ghengeveld Feb 23, 2021
6a400ff
Don't show RAW toggle when there's no data
ghengeveld Feb 23, 2021
9ef5b3a
Fix dark style
ghengeveld Feb 23, 2021
5c20661
Merge remote-tracking branch 'origin/next' into cameron.yick/feature/…
ghengeveld Feb 23, 2021
7a1601a
Fix textarea height
ghengeveld Feb 23, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ built-storybooks
lib/cli/test
lib/core-server/prebuilt
lib/codemod/src/transforms/__testfixtures__
lib/components/src/controls/react-editable-json-tree
scripts/storage
*.bundle.js
*.js.map
Expand Down
21 changes: 17 additions & 4 deletions examples/official-storybook/stories/addon-controls.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,33 @@ export default {
argTypes: {
children: { control: 'text', name: 'Children' },
type: { control: 'text', name: 'Type' },
somethingElse: { control: 'object', name: 'Something Else' },
json: { control: 'object', name: 'JSON' },
imageUrls: { control: { type: 'file', accept: '.png' }, name: 'Image Urls' },
},
parameters: { chromatic: { disable: true } },
};

const Template = (args) => <Button {...args} />;
const DEFAULT_NESTED_OBJECT = { a: 4, b: { c: 'hello', d: [1, 2, 3] } };

const Template = (args) => (
<div>
<Button type={args.type}>{args.children}</Button>
{args.json && <pre>{JSON.stringify(args.json, null, 2)}</pre>}
</div>
);

export const Basic = Template.bind({});
Basic.args = {
children: 'basic',
somethingElse: { a: 2 },
json: DEFAULT_NESTED_OBJECT,
};
Basic.parameters = { chromatic: { disable: false } };

export const Action = Template.bind({});
Action.args = {
children: 'hmmm',
type: 'action',
somethingElse: { a: 4 },
json: null,
};

export const ImageFileControl = (args) => <img src={args.imageUrls[0]} alt="Your Example Story" />;
Expand All @@ -35,6 +42,12 @@ ImageFileControl.args = {
};

export const CustomControls = Template.bind({});
CustomControls.args = {
children: 'hmmm',
type: 'action',
json: DEFAULT_NESTED_OBJECT,
};

CustomControls.argTypes = {
children: { table: { disable: true } },
type: { control: { disable: true } },
Expand Down
1 change: 1 addition & 0 deletions lib/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"memoizerific": "^1.11.3",
"overlayscrollbars": "^1.13.1",
"polished": "^4.0.5",
"prop-types": "^15.7.2",
"react-color": "^2.19.3",
"react-popper-tooltip": "^3.1.1",
"react-syntax-highlighter": "^13.5.3",
Expand Down
6 changes: 2 additions & 4 deletions lib/components/src/blocks/ArgsTable/ArgControl.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { FC, useCallback, useState, useEffect } from 'react';
import { Args, ArgType } from './types';
import {
ArrayControl,
BooleanControl,
ColorControl,
DateControl,
Expand Down Expand Up @@ -51,7 +50,8 @@ export const ArgControl: FC<ArgControlProps> = ({ row, arg, updateArgs }) => {
const props = { name: key, argType: row, value: boxedValue.value, onChange, onBlur, onFocus };
switch (control.type) {
case 'array':
return <ArrayControl {...props} {...control} />;
case 'object':
return <ObjectControl {...props} {...control} />;
case 'boolean':
return <BooleanControl {...props} {...control} />;
case 'color':
Expand All @@ -60,8 +60,6 @@ export const ArgControl: FC<ArgControlProps> = ({ row, arg, updateArgs }) => {
return <DateControl {...props} {...control} />;
case 'number':
return <NumberControl {...props} {...control} />;
case 'object':
return <ObjectControl {...props} {...control} />;
case 'check':
case 'inline-check':
case 'radio':
Expand Down
277 changes: 224 additions & 53 deletions lib/components/src/controls/Object.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,243 @@
import React, { FC, ChangeEvent, useState, useCallback, useEffect } from 'react';
import { styled } from '@storybook/theming';
import { window } from 'global';
import cloneDeep from 'lodash/cloneDeep';
import React, { ComponentProps, SyntheticEvent, useCallback, useMemo, useState } from 'react';
import { styled, useTheme, Theme } from '@storybook/theming';

import deepEqual from 'fast-deep-equal';
// @ts-ignore
import { JsonTree } from './react-editable-json-tree';
import type { ControlProps, ObjectValue, ObjectConfig } from './types';
import { Form } from '../form';
import { ControlProps, ObjectValue, ObjectConfig } from './types';
import { ArgType } from '../blocks';
import { Icons, IconsProps } from '../icon/icon';

const format = (value: any) => (value ? JSON.stringify(value) : '');
type JsonTreeProps = ComponentProps<typeof JsonTree>;

const parse = (value: string) => {
const trimmed = value && value.trim();
return trimmed ? JSON.parse(trimmed) : {};
};
const Wrapper = styled.div(({ theme }) => ({
display: 'flex',

const validate = (value: any, argType: ArgType) => {
if (argType && argType.type.name === 'array') {
return Array.isArray(value);
}
return true;
};
'.rejt-tree': {
marginLeft: '1rem',
fontSize: '13px',
},
'.rejt-value-node, .rejt-object-node > .rejt-collapsed, .rejt-array-node > .rejt-collapsed, .rejt-object-node > .rejt-not-collapsed > span, .rejt-array-node > .rejt-not-collapsed > span': {
'& > svg': {
opacity: 0,
transition: 'opacity 0.2s',
},
},
'.rejt-value-node:hover, .rejt-object-node:hover > .rejt-collapsed, .rejt-array-node:hover > .rejt-collapsed, .rejt-object-node:hover > .rejt-not-collapsed > span, .rejt-array-node:hover > .rejt-not-collapsed > span': {
'& > svg': {
opacity: 1,
},
},
'.rejt-edit-form button': {
display: 'none',
},
'.rejt-add-form': {
marginLeft: 10,
},
'.rejt-add-value-node': {
display: 'inline-flex',
alignItems: 'center',
},
'.rejt-name': {
lineHeight: '22px',
},
'.rejt-not-collapsed-delimiter': {
lineHeight: '22px',
},
'.rejt-plus-menu': {
marginLeft: 5,
},
'.rejt-object-node > span > *': {
position: 'relative',
zIndex: 2,
},
'.rejt-object-node, .rejt-array-node': {
position: 'relative',
},
'.rejt-object-node > span:first-of-type::after, .rejt-array-node > span:first-of-type::after, .rejt-collapsed::before, .rejt-not-collapsed::before': {
content: '""',
position: 'absolute',
top: 0,
display: 'block',
width: '100%',
marginLeft: '-1rem',
padding: '0 4px 0 1rem',
height: 22,
},
'.rejt-collapsed::before, .rejt-not-collapsed::before': {
zIndex: 1,
background: 'transparent',
borderRadius: 4,
transition: 'background 0.2s',
pointerEvents: 'none',
opacity: 0.1,
},
'.rejt-object-node:hover, .rejt-array-node:hover': {
'& > .rejt-collapsed::before, & > .rejt-not-collapsed::before': {
background: theme.color.secondary,
},
},
'.rejt-collapsed::after, .rejt-not-collapsed::after': {
content: '""',
position: 'absolute',
display: 'inline-block',
pointerEvents: 'none',
width: 0,
height: 0,
},
'.rejt-collapsed::after': {
left: -8,
top: 8,
borderTop: '3px solid transparent',
borderBottom: '3px solid transparent',
borderLeft: '3px solid rgba(153,153,153,0.6)',
},
'.rejt-not-collapsed::after': {
left: -10,
top: 10,
borderTop: '3px solid rgba(153,153,153,0.6)',
borderLeft: '3px solid transparent',
borderRight: '3px solid transparent',
},
'.rejt-value': {
display: 'inline-block',
border: '1px solid transparent',
borderRadius: 4,
margin: '1px 0',
padding: '0 4px',
cursor: 'text',
color: theme.color.defaultText,
},
'.rejt-value-node:hover > .rejt-value': {
background: theme.background.app,
borderColor: theme.color.border,
},
}));

const Wrapper = styled.label({
display: 'flex',
});
const Button = styled.button<{ primary?: boolean }>(({ theme, primary }) => ({
border: 0,
height: 20,
margin: 1,
borderRadius: 4,
background: primary ? theme.color.secondary : 'transparent',
color: primary ? theme.color.lightest : theme.color.dark,
fontWeight: primary ? 'bold' : 'normal',
cursor: 'pointer',
order: primary ? 'initial' : 9,
}));

export type ObjectProps = ControlProps<ObjectValue> & ObjectConfig;
export const ObjectControl: FC<ObjectProps> = ({
name,
argType,
value,
onChange,
onBlur,
onFocus,
}) => {
const [valid, setValid] = useState(true);
const [text, setText] = useState(format(value));
type ActionIconProps = IconsProps & { disabled?: boolean };

useEffect(() => {
const newText = format(value);
if (text !== newText) setText(newText);
}, [value]);
const ActionIcon = styled(Icons)(({ theme, icon, disabled }: ActionIconProps) => ({
display: 'inline-block',
verticalAlign: 'middle',
width: 15,
height: 15,
padding: 3,
marginLeft: 5,
cursor: disabled ? 'not-allowed' : 'pointer',
color: theme.color.mediumdark,
'&:hover': disabled
? {}
: {
color: icon === 'subtract' ? theme.color.negative : theme.color.ancillary,
},
'svg + &': {
marginLeft: 0,
},
}));

const handleChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
const Input = styled.input(({ theme, placeholder }) => ({
outline: 0,
margin: placeholder ? 1 : '1px 0',
padding: '3px 4px',
color: theme.color.defaultText,
background: theme.background.app,
border: `1px solid ${theme.color.border}`,
borderRadius: 4,
lineHeight: '14px',
width: placeholder === 'Key' ? 80 : 120,
'&:focus': {
border: `1px solid ${theme.color.secondary}`,
},
}));

const ENTER_EVENT = { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13 };
const dispatchEnterKey = (event: SyntheticEvent<HTMLInputElement>) => {
event.currentTarget.dispatchEvent(new window.KeyboardEvent('keydown', ENTER_EVENT));
};
const selectValue = (event: SyntheticEvent<HTMLInputElement>) => {
event.currentTarget.select();
};

export type ObjectProps = ControlProps<ObjectValue> &
ObjectConfig & {
theme: any; // TODO: is there a type for this?
};

const getCustomStyleFunction: (theme: Theme) => JsonTreeProps['getStyle'] = (theme) => () => ({
name: {
color: theme.color.secondary,
},
collapsed: {
color: theme.color.dark,
},
ul: {
listStyle: 'none',
margin: '0 0 0 1rem',
padding: 0,
},
li: {
outline: 0,
},
});

export const ObjectControl: React.FC<ObjectProps> = ({ name, value = {}, onChange }) => {
const data = useMemo(() => cloneDeep(value), [value]);
const [parseError, setParseError] = useState();
const updateRaw = useCallback(
(raw) => {
try {
const newVal = parse(e.target.value);
const newValid = validate(newVal, argType);
if (newValid && !deepEqual(value, newVal)) {
onChange(newVal);
}
setValid(newValid);
} catch (err) {
setValid(false);
if (raw) onChange(JSON.parse(raw));
setParseError(undefined);
} catch (e) {
setParseError(e);
}
setText(e.target.value);
},
[onChange, setValid]
[onChange]
);

return (
<Wrapper>
<Form.Textarea
valid={valid ? undefined : 'error'}
value={text}
onChange={handleChange}
size="flex"
placeholder="Adjust object dynamically"
{...{ name, onBlur, onFocus }}
<JsonTree
data={data}
rootName={name}
onFullyUpdate={onChange}
getStyle={getCustomStyleFunction(useTheme())}
cancelButtonElement={<Button type="button">Cancel</Button>}
editButtonElement={<Button type="submit">Save</Button>}
addButtonElement={
<Button type="submit" primary>
Save
</Button>
}
plusMenuElement={<ActionIcon icon="add" />}
minusMenuElement={<ActionIcon icon="subtract" />}
inputElement={(_: any, __: any, ___: any, key: string) =>
key ? <Input onFocus={selectValue} onBlur={dispatchEnterKey} /> : <Input />
}
fallback={
<Form.Textarea
id={name}
name={name}
defaultValue={value}
onBlur={(event) => updateRaw(event.target.value)}
size="flex"
placeholder="Enter JSON string"
valid={parseError ? 'error' : null}
/>
}
/>
</Wrapper>
);
Expand Down
Loading