Merge pull request #6594 from ForbesLindesay/ondevice-actions

Ondevice actions
This commit is contained in:
Benoit Dion 2019-05-02 02:23:08 +00:00 committed by GitHub
commit c7266322f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 382 additions and 3 deletions

View File

@ -3,7 +3,7 @@
| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)| [Preact](app/preact)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+|+|
|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+|+|
|[actions](addons/actions) |+|+*|+|+|+|+|+|+|+|+|+|+|
|[backgrounds](addons/backgrounds) |+|*|+|+|+|+|+|+|+|+|+|+|
|[centered](addons/centered) |+| |+|+| |+|+| |+| |+|+|
|[contexts](addons/contexts) |+| |+| | | | | | | | |+|

View File

@ -1,4 +1,5 @@
export * from './constants';
export * from './models';
export * from './preview';
if (module && module.hot && module.hot.decline) {

View File

@ -0,0 +1,32 @@
# Storybook Actions Addon for react-native
Storybook Actions Addon allows you to log events/actions inside stories in [Storybook](https://storybook.js.org).
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
**This addon is a wrapper for addon [@storybook/addon-actions](https://github.com/storybooks/storybook/blob/master/addons/actions).
Refer to its documentation to understand how to use actions**
## Installation
```sh
yarn add -D @storybook/addon-ondevice-actions @storybook/addon-actions
```
## Configuration
Create a file called `rn-addons.js` in your storybook config.
Add following content to it:
```js
import '@storybook/addon-ondevice-actions/register';
```
Then import `rn-addons.js` next to your `getStorybookUI` call.
```js
import './rn-addons';
```
See [@storybook/addon-actions](https://github.com/storybooks/storybook/blob/master/addons/actions) to learn how to write stories with actions and the [crna-kitchen-sink app](../../examples-native/crna-kitchen-sink) for more examples.

View File

@ -0,0 +1,38 @@
{
"name": "@storybook/addon-ondevice-actions",
"version": "5.1.0-alpha.37",
"description": "Action Logger addon for react-native storybook",
"keywords": [
"storybook"
],
"homepage": "https://github.com/storybooks/storybook/tree/master/addons/actions",
"bugs": {
"url": "https://github.com/storybooks/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybooks/storybook.git"
},
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.1.0-alpha.37",
"@storybook/core-events": "5.1.0-alpha.37",
"core-js": "^2.5.7",
"fast-deep-equal": "^2.0.1"
},
"devDependencies": {
"@storybook/addon-actions": "5.1.0-alpha.37"
},
"peerDependencies": {
"@storybook/addon-actions": "*",
"react": "*",
"react-native": "*"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1 @@
require('./dist').register();

View File

@ -0,0 +1,173 @@
import React from 'react';
import { Button, View, Text } from 'react-native';
const theme = {
OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES: 10,
OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES: 5,
OBJECT_NAME_COLOR: 'rgb(136, 19, 145)',
OBJECT_VALUE_NULL_COLOR: 'rgb(128, 128, 128)',
OBJECT_VALUE_UNDEFINED_COLOR: 'rgb(128, 128, 128)',
OBJECT_VALUE_REGEXP_COLOR: 'rgb(196, 26, 22)',
OBJECT_VALUE_STRING_COLOR: 'rgb(196, 26, 22)',
OBJECT_VALUE_SYMBOL_COLOR: 'rgb(196, 26, 22)',
OBJECT_VALUE_NUMBER_COLOR: 'rgb(28, 0, 207)',
OBJECT_VALUE_BOOLEAN_COLOR: 'rgb(28, 0, 207)',
OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'rgb(13, 34, 170)',
ARROW_COLOR: '#6e6e6e',
ARROW_MARGIN_RIGHT: 3,
ARROW_FONT_SIZE: 12,
ARROW_ANIMATION_DURATION: '0',
};
class Inspect extends React.Component<{ name?: string; value: any }, { expanded: boolean }> {
state = { expanded: false };
render() {
const { name, value } = this.props;
const { expanded } = this.state;
const toggle = (
<View style={{ width: 40, height: 40 }}>
{name &&
((Array.isArray(value) && value.length) ||
(value &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).length)) ? (
<Button
onPress={() => this.setState(s => ({ expanded: !s.expanded }))}
title={!expanded ? '▶' : '▼'}
/>
) : null}
</View>
);
const nameComponent = name ? (
<Text style={{ color: theme.OBJECT_NAME_COLOR }}>{name}</Text>
) : null;
if (Array.isArray(value)) {
if (name) {
return (
<>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{toggle}
{nameComponent}
<Text>{': ' + (value.length === 0 ? '[]' : expanded ? '[' : '[...]')}</Text>
</View>
{expanded ? (
<View style={{ marginLeft: 40 }}>
{value.map((v, i) => (
<View key={i} style={{ marginLeft: 40 }}>
<Inspect value={v} />
</View>
))}
<View style={{ marginLeft: 20 }}>
<Text>{']'}</Text>
</View>
</View>
) : null}
</>
);
}
return (
<View>
<Text>{'['}</Text>
{value.map((v, i) => (
<View key={i} style={{ marginLeft: 20 }}>
<Inspect value={v} />
</View>
))}
<Text>{']'}</Text>
</View>
);
}
if (value && typeof value === 'object' && !(value instanceof RegExp)) {
if (name) {
return (
<>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{toggle}
{nameComponent}
<Text>
{': ' + (Object.keys(value).length === 0 ? '{}' : expanded ? '{' : '{...}')}
</Text>
</View>
{expanded ? (
<View style={{ marginLeft: 40 }}>
{Object.entries(value).map(([key, v]) => (
<View key={key}>
<Inspect name={key} value={v} />
</View>
))}
<View style={{ marginLeft: 20 }}>
<Text>{'}'}</Text>
</View>
</View>
) : null}
</>
);
}
return (
<View>
<Text>{'{'}</Text>
{Object.entries(value).map(([key, v]) => (
<View key={key}>
<Inspect name={key} value={v} />
</View>
))}
<Text>{'}'}</Text>
</View>
);
}
if (name) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{toggle}
{nameComponent}
<Text>{': '}</Text>
<Value value={value} />
</View>
);
}
return (
<View>
<Value value={value} />
</View>
);
}
}
function Value({ value }: { value: any }) {
if (value === null) {
return <Text style={{ color: theme.OBJECT_VALUE_NULL_COLOR }}>null</Text>;
}
if (value === undefined) {
return <Text style={{ color: theme.OBJECT_VALUE_UNDEFINED_COLOR }}>undefined</Text>;
}
if (value instanceof RegExp) {
return (
<Text style={{ color: theme.OBJECT_VALUE_REGEXP_COLOR }}>
{'/' + value.source + '/' + value.flags}
</Text>
);
}
switch (typeof value) {
case 'string':
return (
<Text style={{ color: theme.OBJECT_VALUE_STRING_COLOR }}>{JSON.stringify(value)}</Text>
);
case 'number':
return (
<Text style={{ color: theme.OBJECT_VALUE_NUMBER_COLOR }}>{JSON.stringify(value)}</Text>
);
case 'boolean':
return (
<Text style={{ color: theme.OBJECT_VALUE_BOOLEAN_COLOR }}>{JSON.stringify(value)}</Text>
);
case 'function':
return <Text style={{ color: theme.OBJECT_VALUE_FUNCTION_PREFIX_COLOR }}>[Function]</Text>;
}
return <Text>{JSON.stringify(value)}</Text>;
}
export default Inspect;

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Button, View, Text, ScrollView } from 'react-native';
import { ActionDisplay } from '@storybook/addon-actions';
import Inspect from './Inspect';
interface ActionLoggerProps {
actions: ActionDisplay[];
onClear: () => void;
}
export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => (
<ScrollView>
<ScrollView horizontal>
<View>
{actions.map((action: ActionDisplay) => (
<View key={action.id} style={{ flexDirection: 'row' }}>
<View>{action.count > 1 ? <Text>{action.count}</Text> : null}</View>
<View style={{ flexGrow: 1 }}>
<Inspect name={action.data.name} value={action.data.args || action.data} />
</View>
</View>
))}
</View>
</ScrollView>
<View>
<Button onPress={onClear} title="CLEAR" />
</View>
</ScrollView>
);
export default ActionLogger;

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react';
import deepEqual from 'fast-deep-equal';
import { addons } from '@storybook/addons';
import { STORY_RENDERED } from '@storybook/core-events';
import { ActionDisplay, EVENT_ID } from '@storybook/addon-actions';
import { ActionLogger as ActionLoggerComponent } from '../../components/ActionLogger';
interface ActionLoggerProps {
active: boolean;
}
interface ActionLoggerState {
actions: ActionDisplay[];
}
const safeDeepEqual = (a: any, b: any): boolean => {
try {
return deepEqual(a, b);
} catch (e) {
return false;
}
};
export default class ActionLogger extends Component<ActionLoggerProps, ActionLoggerState> {
private channel = addons.getChannel();
constructor(props: ActionLoggerProps) {
super(props);
this.state = { actions: [] };
}
componentDidMount() {
this.channel.addListener(EVENT_ID, this.addAction);
this.channel.addListener(STORY_RENDERED, this.handleStoryChange);
}
componentWillUnmount() {
this.channel.removeListener(STORY_RENDERED, this.handleStoryChange);
this.channel.removeListener(EVENT_ID, this.addAction);
}
handleStoryChange = () => {
const { actions } = this.state;
if (actions.length > 0 && actions[0].options.clearOnStoryChange) {
this.clearActions();
}
};
addAction = (action: ActionDisplay) => {
this.setState((prevState: ActionLoggerState) => {
const actions = [...prevState.actions];
const previous = actions.length && actions[0];
if (previous && safeDeepEqual(previous.data, action.data)) {
previous.count++; // eslint-disable-line
} else {
action.count = 1; // eslint-disable-line
actions.unshift(action);
}
return { actions: actions.slice(0, action.options.limit) };
});
};
clearActions = () => {
this.setState({ actions: [] });
};
render() {
const { actions = [] } = this.state;
const { active } = this.props;
const props = {
actions,
onClear: this.clearActions,
};
return active ? <ActionLoggerComponent {...props} /> : null;
}
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import addons from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from '@storybook/addon-actions';
import ActionLogger from './containers/ActionLogger';
export function register() {
addons.register(ADDON_ID, () => {
addons.addPanel(PANEL_ID, {
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} active={active} />,
});
});
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["src/__tests__/**/*"]
}

View File

@ -24,7 +24,9 @@
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-transform-react-jsx-source": "^7.2.0",
"@storybook/addon-actions": "5.1.0-alpha.37",
"@storybook/addon-knobs": "5.1.0-alpha.37",
"@storybook/addon-ondevice-actions": "5.1.0-alpha.37",
"@storybook/addon-ondevice-backgrounds": "5.1.0-alpha.37",
"@storybook/addon-ondevice-knobs": "5.1.0-alpha.37",
"@storybook/addon-ondevice-notes": "5.1.0-alpha.37",

View File

@ -1,3 +1,4 @@
require('@storybook/addon-ondevice-actions/register');
require('@storybook/addon-ondevice-knobs/register');
require('@storybook/addon-ondevice-notes/register');
require('@storybook/addon-ondevice-backgrounds/register');

View File

@ -241,14 +241,14 @@ class ManagerConsumer extends Component<ConsumerProps<SubState, Combo>> {
prevData?: SubState;
render() {
const { children } = this.props;
const { children, pure } = this.props;
return (
<ManagerContext.Consumer>
{d => {
const data = this.dataMemory ? this.dataMemory(d) : d;
if (
this.props.pure &&
pure &&
this.prevChildren &&
this.prevData &&
shallowEqualObjects(data, this.prevData)