diff --git a/addons/actions/README.md b/addons/actions/README.md
index 4d6d3ea15834..dfaf82011937 100644
--- a/addons/actions/README.md
+++ b/addons/actions/README.md
@@ -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', () => (
))
```
+
+## 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`|
diff --git a/addons/actions/src/index.js b/addons/actions/src/index.js
index 2634da6e8a3b..e6823820166a 100644
--- a/addons/actions/src/index.js
+++ b/addons/actions/src/index.js
@@ -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 };
diff --git a/addons/actions/src/lib/decycle.js b/addons/actions/src/lib/decycle.js
index c6a3e9ec44f8..c233e463be4c 100644
--- a/addons/actions/src/lib/decycle.js
+++ b/addons/actions/src/lib/decycle.js
@@ -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);
@@ -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) {
@@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) {
}
return value;
- })(object, '$', 0);
+ })(object, '$', 0, depth);
return res;
}
diff --git a/addons/actions/src/lib/types/object/__tests__/index.js b/addons/actions/src/lib/types/object/__tests__/index.js
index 56341c9d11b2..04caf09e5d04 100644
--- a/addons/actions/src/lib/types/object/__tests__/index.js
+++ b/addons/actions/src/lib/types/object/__tests__/index.js
@@ -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', () => {
diff --git a/addons/actions/src/lib/types/object/configureDepth.js b/addons/actions/src/lib/types/object/configureDepth.js
new file mode 100644
index 000000000000..8481e84b3364
--- /dev/null
+++ b/addons/actions/src/lib/types/object/configureDepth.js
@@ -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;
+}
diff --git a/addons/actions/src/lib/types/object/index.js b/addons/actions/src/lib/types/object/index.js
index fdaee125c7b7..42b0871955f7 100644
--- a/addons/actions/src/lib/types/object/index.js
+++ b/addons/actions/src/lib/types/object/index.js
@@ -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),
};
diff --git a/addons/actions/src/lib/util/index.js b/addons/actions/src/lib/util/index.js
index f10fd4416c63..af41b84ccca5 100644
--- a/addons/actions/src/lib/util/index.js
+++ b/addons/actions/src/lib/util/index.js
@@ -5,3 +5,4 @@ export muteProperty from './muteProperty';
export prepareArguments from './prepareArguments';
export typeReviver from './typeReviver';
export typeReplacer from './typeReplacer';
+export omitProperty from './omitProperty';
diff --git a/addons/actions/src/lib/util/omitProperty.js b/addons/actions/src/lib/util/omitProperty.js
new file mode 100644
index 000000000000..41fa1b2c10a6
--- /dev/null
+++ b/addons/actions/src/lib/util/omitProperty.js
@@ -0,0 +1,3 @@
+export default function omitProperty(name) {
+ return name.startsWith('__') || name.startsWith('STORYBOOK_');
+}
diff --git a/addons/actions/src/lib/util/prepareArguments.js b/addons/actions/src/lib/util/prepareArguments.js
index ed441336a895..8789edce1bc7 100644
--- a/addons/actions/src/lib/util/prepareArguments.js
+++ b/addons/actions/src/lib/util/prepareArguments.js
@@ -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.
}
diff --git a/addons/actions/src/preview/__tests__/action.test.js b/addons/actions/src/preview/__tests__/action.test.js
new file mode 100644
index 000000000000..fd5c301bb59f
--- /dev/null
+++ b/addons/actions/src/preview/__tests__/action.test.js
@@ -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',
+ },
+ },
+ },
+ },
+ })
+ );
+ });
+});
diff --git a/addons/actions/src/preview/__tests__/configureActions.test.js b/addons/actions/src/preview/__tests__/configureActions.test.js
new file mode 100644
index 000000000000..299d8f802c3b
--- /dev/null
+++ b/addons/actions/src/preview/__tests__/configureActions.test.js
@@ -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,
+ });
+ });
+});
diff --git a/addons/actions/src/preview.test.js b/addons/actions/src/preview/__tests__/preview.test.js
similarity index 97%
rename from addons/actions/src/preview.test.js
rename to addons/actions/src/preview/__tests__/preview.test.js
index d96cef9576eb..b6e562d5a58b 100644
--- a/addons/actions/src/preview.test.js
+++ b/addons/actions/src/preview/__tests__/preview.test.js
@@ -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');
diff --git a/addons/actions/src/preview.js b/addons/actions/src/preview/action.js
similarity index 52%
rename from addons/actions/src/preview.js
rename to addons/actions/src/preview/action.js
index 1df9c39c291b..3c036da7d4df 100644
--- a/addons/actions/src/preview.js
+++ b/addons/actions/src/preview/action.js
@@ -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, {
@@ -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);
- };
- };
-}
diff --git a/addons/actions/src/preview/configureActions.js b/addons/actions/src/preview/configureActions.js
new file mode 100644
index 000000000000..4c76554b6a3e
--- /dev/null
+++ b/addons/actions/src/preview/configureActions.js
@@ -0,0 +1,7 @@
+export const config = {
+ depth: 10,
+};
+
+export function configureActions(options = {}) {
+ Object.assign(config, options);
+}
diff --git a/addons/actions/src/preview/decorateAction.js b/addons/actions/src/preview/decorateAction.js
new file mode 100644
index 000000000000..90ed85d870d2
--- /dev/null
+++ b/addons/actions/src/preview/decorateAction.js
@@ -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);
+ };
+ };
+}
diff --git a/addons/actions/src/preview/index.js b/addons/actions/src/preview/index.js
new file mode 100644
index 000000000000..ea207b3fd4d9
--- /dev/null
+++ b/addons/actions/src/preview/index.js
@@ -0,0 +1,3 @@
+export { default as action } from './action';
+export { configureActions } from './configureActions';
+export { default as decorateAction } from './decorateAction';
diff --git a/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot b/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot
index 14d27e909bd3..9abce3a828c7 100644
--- a/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot
+++ b/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot
@@ -67,6 +67,11 @@ exports[`Storyshots Addons|Actions All types 1`] = `
>
Plain Object
+
`;
+
+exports[`Storyshots Addons|Actions configureActions 1`] = `
+
+`;
diff --git a/examples/official-storybook/stories/addon-actions.stories.js b/examples/official-storybook/stories/addon-actions.stories.js
index b353ddaf3014..83fbffff1f1b 100644
--- a/examples/official-storybook/stories/addon-actions.stories.js
+++ b/examples/official-storybook/stories/addon-actions.stories.js
@@ -1,7 +1,7 @@
/* global window */
import React from 'react';
import { storiesOf } from '@storybook/react';
-import { action, decorateAction } from '@storybook/addon-actions';
+import { action, configureActions, decorateAction } from '@storybook/addon-actions';
import { setOptions } from '@storybook/addon-options';
import { Button } from '@storybook/react/demo';
import { File } from 'global';
@@ -66,7 +66,16 @@ storiesOf('Addons|Actions', module)
>
Multiple
-
+
+
@@ -75,4 +84,17 @@ storiesOf('Addons|Actions', module)
);
+ })
+ .add('configureActions', () => {
+ configureActions({
+ depth: 2,
+ });
+
+ return (
+
+ );
});