Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
75a9c65
skip serializing (Synthetic)Events
rhalff Jan 3, 2018
f2f8c49
Merge branch 'master' into rhalff/addon-actions
rhalff Jan 5, 2018
aef9908
add depth configurator
rhalff Jan 8, 2018
41eae7f
remove early bailout for events
rhalff Jan 8, 2018
9091063
stop traversing properties instead of returning string when max depth…
rhalff Jan 8, 2018
cd12993
configure max depth for objects
rhalff Jan 8, 2018
69eef79
Merge branch 'master' into addon-actions
rhalff Jan 8, 2018
e26f3b8
omit some private properties by default
rhalff Mar 2, 2018
a0d5fad
Merge remote-tracking branch 'storybooks' into addon-actions
rhalff Mar 2, 2018
26b9b5d
force maxDepth for non plain objects
rhalff Mar 2, 2018
82ed846
deeper example
rhalff Mar 2, 2018
b07d63f
move omitProperty to util
rhalff Mar 2, 2018
401134d
Merge branch 'master' into addon-actions
ndelangen Mar 9, 2018
ace8cd8
Merge branch 'master' of github.com:storybooks/storybook into addon-a…
rhalff Apr 14, 2018
8df90ef
Merge branch 'addon-actions' of github.com:rhalff/storybook into addo…
rhalff Apr 14, 2018
bdd58bc
move preview to own folder
rhalff Apr 15, 2018
d26ea97
move preview test file
rhalff Apr 15, 2018
dd6e12e
make depth configurable
rhalff Apr 15, 2018
d3b5234
update readme
rhalff Apr 15, 2018
8c7ed2d
Merge branch 'master' of github.com:storybooks/storybook into addon-a…
rhalff Apr 15, 2018
bfa5898
fix export
rhalff Apr 15, 2018
b8aee78
disable no-control-regex again
rhalff Apr 15, 2018
50d81a1
add configuration examples
rhalff Apr 15, 2018
bc883bc
update snapshot
rhalff Apr 16, 2018
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
35 changes: 34 additions & 1 deletion addons/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ Import the `action` function and use it to create actions handlers. When creatin

```js
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { action, configureActions } from '@storybook/addon-actions';

import Button from './button';

action('button-click')

storiesOf('Button', module)
.add('default view', () => (
<Button onClick={ action('button-click') }>
Expand Down Expand Up @@ -69,3 +71,34 @@ storiesOf('Button', module)
</Button>
))
```

## Configuration

Arguments which are passed to the action call will have to be serialized while be "transfered"
over the channel.

This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible
to configure a maximum depth.

To apply the configuration globally use the `configureActions` function in your `config.js` file.

```js
import { configureActions } from '@storybook/addon-actions';

configureActions({
depth: 100
})
```

To apply the configuration per action use:
```js
action('my-action', {
depth: 5
})
```

### Available Options

|Name|Type|Description|Default|
|---|---|---|---|
|`depth`|Number|Configures the transfered depth of any logged objects.|`10`|
4 changes: 3 additions & 1 deletion addons/actions/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { action, configureActions, decorateAction } from './preview';

// addons, panels and events get unique names using a prefix
export const ADDON_ID = 'storybook/actions';
export const PANEL_ID = `${ADDON_ID}/actions-panel`;
export const EVENT_ID = `${ADDON_ID}/action-event`;

export { action, decorateAction } from './preview';
export { action, configureActions, decorateAction };
53 changes: 37 additions & 16 deletions addons/actions/src/lib/decycle.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { DecycleError } from './errors';

import { getPropertiesList, typeReplacer } from './util';
import { getPropertiesList, typeReplacer, omitProperty } from './util';

import { CYCLIC_KEY } from './';

import { objectType } from './types';

import { DEPTH_KEY } from './types/object/configureDepth';

const { hasOwnProperty } = Object.prototype;

export default function decycle(object, depth = 10) {
const objects = new WeakMap();

let isCyclic = false;

const res = (function derez(value, path, _depth) {
const res = (function derez(value, path, _depth, _branchDepthMax) {
let oldPath;
let obj;

if (Object(value) === value && _depth > depth) {
const name = value.constructor ? value.constructor.name : typeof value;

return `[${name}...]`;
}
let maxDepth = _branchDepthMax;

const result = typeReplacer(value);

Expand Down Expand Up @@ -51,19 +51,40 @@ export default function decycle(object, depth = 10) {
if (Array.isArray(value)) {
obj = [];
for (let i = 0; i < value.length; i += 1) {
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1);
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1, maxDepth);
}
} else {
obj = objectType.serialize(value);

getPropertiesList(value).forEach(name => {
try {
obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1);
} catch (error) {
console.error(error); // eslint-disable-line no-console
obj[name] = new DecycleError(error.message);
let newDepth;
if (hasOwnProperty.call(obj, DEPTH_KEY)) {
if (_depth + 1 < maxDepth) {
const depthKey = obj[DEPTH_KEY];

newDepth = depthKey === 0 ? 0 : _depth + depthKey;
maxDepth = newDepth >= depth ? depth : newDepth;
}
});

delete obj[DEPTH_KEY];
}

if (_depth <= maxDepth) {
getPropertiesList(value).forEach(name => {
if (!omitProperty(name)) {
try {
obj[name] = derez(
value[name],
`${path}[${JSON.stringify(name)}]`,
_depth + 1,
maxDepth
);
} catch (error) {
console.error(error); // eslint-disable-line no-console
obj[name] = new DecycleError(error.message);
}
}
});
}
}

if (_depth === 0 && value instanceof Object && isCyclic) {
Expand All @@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) {
}

return value;
})(object, '$', 0);
})(object, '$', 0, depth);

return res;
}
6 changes: 5 additions & 1 deletion addons/actions/src/lib/types/object/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import objectType from '../';
import { DEPTH_KEY } from '../configureDepth';

describe('Object', () => {
it('Serializes Object', () => {
function C() {}
const c = new C();

expect(objectType.serialize(c)).toEqual({ [objectType.KEY]: 'C' });
expect(objectType.serialize(c)).toEqual({
[DEPTH_KEY]: 2,
[objectType.KEY]: 'C',
});
});

it('Deserializes Object', () => {
Expand Down
7 changes: 7 additions & 0 deletions addons/actions/src/lib/types/object/configureDepth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const DEPTH_KEY = '$___storybook.depthKey';

export default function configureDepth(obj, depth = 0) {
obj[DEPTH_KEY] = depth; // eslint-disable-line no-param-reassign

return obj;
}
11 changes: 10 additions & 1 deletion addons/actions/src/lib/types/object/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import createNamedObject from './createNamedObject';
import getObjectName from './getObjectName';
import configureDepth from './configureDepth';

const maxDepth = 2;
const KEY = '$___storybook.objectName';

const objectType = {
KEY,
// is: (value) => , // not used
serialize: value => ({ [KEY]: getObjectName(value) }),
serialize: value => {
const objectName = getObjectName(value);
if (objectName === 'Object') {
return { [KEY]: objectName };
}

return configureDepth({ [KEY]: objectName }, maxDepth);
},
deserialize: value => createNamedObject(value, KEY),
};

Expand Down
1 change: 1 addition & 0 deletions addons/actions/src/lib/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export muteProperty from './muteProperty';
export prepareArguments from './prepareArguments';
export typeReviver from './typeReviver';
export typeReplacer from './typeReplacer';
export omitProperty from './omitProperty';
3 changes: 3 additions & 0 deletions addons/actions/src/lib/util/omitProperty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function omitProperty(name) {
return name.startsWith('__') || name.startsWith('STORYBOOK_');
}
8 changes: 2 additions & 6 deletions addons/actions/src/lib/util/prepareArguments.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { decycle } from '../index';

export default function prepareArguments(arg) {
if (arg && typeof arg.preventDefault !== 'undefined') {
return JSON.stringify(`[${arg.constructor.name}]`);
}

export default function prepareArguments(arg, depth) {
try {
return JSON.stringify(decycle(arg));
return JSON.stringify(decycle(arg, depth));
} catch (error) {
return error.toString(); // IE still cyclic.
}
Expand Down
90 changes: 90 additions & 0 deletions addons/actions/src/preview/__tests__/action.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import addons from '@storybook/addons';
import { action, configureActions } from '../../';

jest.mock('@storybook/addons');

const getChannelData = channel =>
channel.emit.mock.calls[channel.emit.mock.calls.length - 1][1].data;

describe('Action', () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);

it('with one argument', () => {
action('test-action')('one');

expect(getChannelData(channel).args[0]).toEqual('"one"');
});

it('with multiple arguments', () => {
action('test-action')('one', 'two', 'three');

expect(getChannelData(channel).args).toEqual(['"one"', '"two"', '"three"']);
});

it('with global depth configuration', () => {
const depth = 1;

configureActions({
depth,
});

action('test-action')({
root: {
one: {
two: 'foo',
},
},
});

expect(getChannelData(channel).args[0]).toEqual(
JSON.stringify({
'$___storybook.objectName': 'Object',
root: {
'$___storybook.objectName': 'Object',
one: {
'$___storybook.objectName': 'Object',
},
},
})
);
});

it('per action depth option overrides global config', () => {
configureActions({
depth: 1,
});

action('test-action', { depth: 3 })({
root: {
one: {
two: {
three: {
four: {
five: 'foo',
},
},
},
},
},
});

expect(getChannelData(channel).args[0]).toEqual(
JSON.stringify({
'$___storybook.objectName': 'Object',
root: {
'$___storybook.objectName': 'Object',
one: {
'$___storybook.objectName': 'Object',
two: {
'$___storybook.objectName': 'Object',
three: {
'$___storybook.objectName': 'Object',
},
},
},
},
})
);
});
});
16 changes: 16 additions & 0 deletions addons/actions/src/preview/__tests__/configureActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { config } from '../configureActions';
import { configureActions } from '../../';

describe('Configure Actions', () => {
it('can configure actions', () => {
const depth = 100;

configureActions({
depth,
});

expect(config).toEqual({
depth,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import addons from '@storybook/addons';
import uuid from 'uuid/v1';
import { action } from './preview';
import { undefinedType, symbolType } from './lib/types';
import { action } from '../';
import { undefinedType, symbolType } from '../../lib/types';

jest.mock('uuid/v1');
jest.mock('@storybook/addons');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import addons from '@storybook/addons';
import uuid from 'uuid/v1';
import { EVENT_ID } from './';
import { canConfigureName, prepareArguments } from './lib/util';
import addons from '@storybook/addons';
import { EVENT_ID } from '../';
import { canConfigureName, prepareArguments } from '../lib/util';
import { config } from './configureActions';

export default function action(name, options = {}) {
const actionOptions = {
...config,
...options,
};

export function action(name) {
// eslint-disable-next-line no-shadow
const handler = function action(..._args) {
const args = _args.map(prepareArguments);
const args = _args.map(arg => prepareArguments(arg, actionOptions.depth));
const channel = addons.getChannel();
const id = uuid();
channel.emit(EVENT_ID, {
Expand All @@ -20,13 +26,3 @@ export function action(name) {
}
return handler;
}

export function decorateAction(decorators) {
return name => {
const callAction = action(name);
return (..._args) => {
const decorated = decorators.reduce((args, fn) => fn(args), _args);
callAction(...decorated);
};
};
}
7 changes: 7 additions & 0 deletions addons/actions/src/preview/configureActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const config = {
depth: 10,
};

export function configureActions(options = {}) {
Object.assign(config, options);
}
11 changes: 11 additions & 0 deletions addons/actions/src/preview/decorateAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { action } from '../preview';

export default function decorateAction(decorators) {
return (name, options) => {
const callAction = action(name, options);
return (..._args) => {
const decorated = decorators.reduce((args, fn) => fn(args), _args);
callAction(...decorated);
};
};
}
Loading