Panel.js 6.1 KB
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import qs from 'qs';
import { document } from 'global';
import styled from '@emotion/styled';
import copy from 'copy-to-clipboard';

import { Placeholder, TabWrapper, TabsState, ActionBar, ActionButton } from '@storybook/components';

import Types from './types';
import PropForm from './PropForm';

const getTimestamp = () => +new Date();

const DEFAULT_GROUP_ID = 'ALL';

const PanelWrapper = styled.div({
  height: '100%',
  overflow: 'auto',
  width: '100%',
});

export default class Panel extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      knobs: {},
    };
    this.options = {};

    this.lastEdit = getTimestamp();
    this.loadedFromUrl = false;
  }

  componentDidMount() {
    const { channel, api } = this.props;
    channel.on('addon:knobs:setKnobs', this.setKnobs);
    channel.on('addon:knobs:setOptions', this.setOptions);

    this.stopListeningOnStory = api.onStory(() => {
      this.setState({ knobs: {} });
      channel.emit('addon:knobs:reset');
    });
  }

  componentWillUnmount() {
    const { channel } = this.props;

    channel.removeListener('addon:knobs:setKnobs', this.setKnobs);
    this.stopListeningOnStory();
  }

  setOptions = (options = { timestamps: false }) => {
    this.options = options;
  };

  setKnobs = ({ knobs, timestamp }) => {
    const queryParams = {};
    const { api, channel } = this.props;

    if (!this.options.timestamps || !timestamp || this.lastEdit <= timestamp) {
      Object.keys(knobs).forEach(name => {
        const knob = knobs[name];
        // For the first time, get values from the URL and set them.
        if (!this.loadedFromUrl) {
          const urlValue = api.getQueryParam(`knob-${name}`);
          if (urlValue !== undefined) {
            // If the knob value present in url
            knob.value = Types[knob.type].deserialize(urlValue);
            channel.emit('addon:knobs:knobChange', knob);
          }
        }

        // set all knobsquery params to be deleted from URL
        queryParams[`knob-${name}`] = null;
      });

      api.setQueryParams(queryParams);
      this.setState({ knobs });

      this.loadedFromUrl = true;
    }
  };

  reset = () => {
    const { channel } = this.props;

    channel.emit('addon:knobs:reset');
  };

  copy = () => {
    const { location } = document;
    const query = qs.parse(location.search.replace('?', ''));
    const { knobs } = this.state;

    Object.entries(knobs).forEach(([name, knob]) => {
      query[`knob-${name}`] = Types[knob.type].serialize(knob.value);
    });

    copy(`${location.origin + location.pathname}?${qs.stringify(query)}`);

    // TODO: show some notification of this
  };

  emitChange = changedKnob => {
    const { channel } = this.props;

    channel.emit('addon:knobs:knobChange', changedKnob);
  };

  handleChange = changedKnob => {
    this.lastEdit = getTimestamp();
    const { knobs } = this.state;
    const { name } = changedKnob;
    const newKnobs = { ...knobs };
    newKnobs[name] = {
      ...newKnobs[name],
      ...changedKnob,
    };

    this.setState({ knobs: newKnobs }, this.emitChange(changedKnob));
  };

  handleClick = knob => {
    const { channel } = this.props;

    channel.emit('addon:knobs:knobClick', knob);
  };

  render() {
    const { knobs } = this.state;
    const { active } = this.props;

    if (!active) {
      return null;
    }

    const groups = {};
    const groupIds = [];

    const knobKeysArray = Object.keys(knobs).filter(key => knobs[key].used);

    knobKeysArray
      .filter(key => knobs[key].groupId)
      .forEach(key => {
        const knobKeyGroupId = knobs[key].groupId;
        groupIds.push(knobKeyGroupId);
        groups[knobKeyGroupId] = {
          render: ({ active: groupActive, selected }) => (
            <TabWrapper active={groupActive || selected === DEFAULT_GROUP_ID}>
              <PropForm
                // false positive
                // eslint-disable-next-line no-use-before-define
                knobs={knobsArray.filter(knob => knob.groupId === knobKeyGroupId)}
                onFieldChange={this.handleChange}
                onFieldClick={this.handleClick}
              />
            </TabWrapper>
          ),
          title: knobKeyGroupId,
        };
      });

    groups[DEFAULT_GROUP_ID] = {
      render: ({ active: groupActive }) => {
        // false positive
        // eslint-disable-next-line no-use-before-define
        const defaultKnobs = knobsArray.filter(
          knob => !knob.groupId || knob.groupId === DEFAULT_GROUP_ID
        );

        if (defaultKnobs.length === 0) {
          return null;
        }
        return (
          <TabWrapper active={groupActive}>
            <PropForm
              knobs={defaultKnobs}
              onFieldChange={this.handleChange}
              onFieldClick={this.handleClick}
            />
          </TabWrapper>
        );
      },
      title: DEFAULT_GROUP_ID,
    };

    const knobsArray = knobKeysArray.map(key => knobs[key]);

    if (knobsArray.length === 0) {
      return <Placeholder>NO KNOBS</Placeholder>;
    }

    return (
      <PanelWrapper>
        {groupIds.length > 0 ? (
          <TabsState>
            {Object.entries(groups).map(([k, v]) => (
              <div id={k} key={k} title={v.title}>
                {v.render}
              </div>
            ))}
          </TabsState>
        ) : (
          <PropForm
            knobs={knobsArray}
            onFieldChange={this.handleChange}
            onFieldClick={this.handleClick}
          />
        )}
        <ActionBar>
          <ActionButton onClick={this.copy}>COPY</ActionButton>
          <ActionButton onClick={this.reset}>RESET</ActionButton>
        </ActionBar>
      </PanelWrapper>
    );
  }
}

Panel.propTypes = {
  active: PropTypes.bool.isRequired,
  onReset: PropTypes.object, // eslint-disable-line
  channel: PropTypes.shape({
    emit: PropTypes.func,
    on: PropTypes.func,
    removeListener: PropTypes.func,
  }).isRequired,
  api: PropTypes.shape({
    onStory: PropTypes.func,
    getQueryParam: PropTypes.func,
    setQueryParams: PropTypes.func,
  }).isRequired,
};