<commit_before>// @flow
import * as I from 'immutable'
import * as Types from '../../constants/types/chat2'
import * as Constants from '../../constants/chat2'
import * as React from 'react'
import * as Styles from '../../styles'
import AutoSizer from 'react-virtualized-auto-sizer'
import {VariableSizeList} from 'react-window'
import {ErrorBoundary} from '../../common-adapters'
import {makeRow} from './row'
import BuildTeam from './row/build-team/container'
import ChatInboxHeader from './row/chat-inbox-header/container'
import BigTeamsDivider from './row/big-teams-divider/container'
import TeamsDivider from './row/teams-divider/container'
import {debounce} from 'lodash-es'
import UnreadShortcut from './unread-shortcut'
import {Owl} from './owl'
import NewConversation from './new-conversation/container'
import type {Props, RowItem, RowItemSmall, RowItemBig, RouteState} from './index.types'
import {virtualListMarks} from '../../local-debug'
import {inboxWidth, getRowHeight} from './row/sizes'

type State = {
  showFloating: boolean,
  showUnread: boolean,
}

class Inbox extends React.PureComponent<Props, State> {
  state = {
    showFloating: false,
    showUnread: false,
  }

  _mounted: boolean = false
  _list: ?VariableSizeList<any>
  _clearedFilterCount: number = 0
  _selectedVisible: boolean = false

  // stuff for UnreadShortcut
  _firstOffscreenIdx: number = -1
  _lastVisibleIdx: number = -1
  _scrollDiv = React.createRef()

  componentDidUpdate(prevProps: Props) {
    let listRowsResized = false
    if (prevProps.smallTeamsExpanded !== this.props.smallTeamsExpanded) {
      listRowsResized = true
    }

    // filter / not filter
    if (!!prevProps.filter !== !!this.props.filter) {
      listRowsResized = true
    }
    if (prevProps.filter !== this.props.filter && this._list) {
      this._list.scrollTo(0)
    }

    // list changed
    if (this.props.rows.length !== prevProps.rows.length) {
      this._calculateShowFloating()
      listRowsResized = true
    }

    if (listRowsResized) {
      this._list && this._list.resetAfterIndex(0)
    }

    if (!I.is(this.props.unreadIndices, prevProps.unreadIndices)) {
      this._calculateShowUnreadShortcut()
    }

    if (this.props.filter && this.props.selectedConversationIDKey !== prevProps.selectedConversationIDKey) {
      const selectedIndex = this.props.rows.findIndex(
        // $ForceType
        r => r.conversationIDKey === this.props.selectedConversationIDKey
      )
      selectedIndex >= 0 && this._list && this._list.scrollToItem(selectedIndex)
    }
  }

  componentDidMount() {
    this._mounted = true
  }

  componentWillUnmount() {
    this._mounted = false
  }

  _itemSizeGetter = index => {
    if (this.props.filter.length) {
      return 56
    }
    const row = this.props.rows[index]
    if (!row) {
      return 0
    }

    return getRowHeight(row.type, !!this.props.filter.length, row.type === 'divider' && row.showButton)
  }

  _itemRenderer = (index, style) => {
    const row = this.props.rows[index]
    const divStyle = Styles.collapseStyles([style, virtualListMarks && styles.divider])
    if (row.type === 'divider') {
      return (

          <TeamsDivider
            key="divider"
            toggle={this.props.toggleSmallTeamsExpanded}
            showButton={row.showButton}
            rows={this.props.rows}
          />

      )
    }

    const conversationIDKey: Types.ConversationIDKey = row.conversationIDKey || Constants.noConversationIDKey
    const teamname = row.teamname || ''
    const isHighlighted = index === 0 && !!this.props.filter && !this._selectedVisible

    // pointer events on so you can click even right after a scroll
    return (

        {makeRow({
          channelname: (row.type === 'big' && row.channelname) || '',
          conversationIDKey,
          filtered: !!this.props.filter,
          teamname,
          type: row.type,
        })}

    )
  }

  _calculateShowUnreadShortcut = () => {
    if (!this.props.unreadIndices.size || this._lastVisibleIdx < 0) {
      this.setState(s => (s.showUnread ? {showUnread: false} : null))
      return
    }

    const firstOffscreenIdx = this.props.unreadIndices.find(idx => idx > this._lastVisibleIdx)
    if (firstOffscreenIdx) {
      this.setState(s => (s.showUnread ? null : {showUnread: true}))
      this._firstOffscreenIdx = firstOffscreenIdx
    } else {
      this.setState(s => (s.showUnread ? {showUnread: false} : null))
      this._firstOffscreenIdx = -1
    }
  }

  _calculateShowFloating = () => {
    if (this._lastVisibleIdx < 0) {
      return
    }
    let showFloating = true
    const row = this.props.rows[this._lastVisibleIdx]
    if (!row || row.type !== 'small') {
      showFloating = false
    }

    this.setState(old => (old.showFloating !== showFloating ? {showFloating} : null))
  }

  _onItemsRendered = debounce(({visibleStartIndex, visibleStopIndex}) => {
    this._lastVisibleIdx = visibleStopIndex
    if (this.props.filter.length) {
      return
    }
    if (this.props.clearedFilterCount > this._clearedFilterCount) {
      // just cleared out filter
      // re-rendering normal inbox for the first time
      // no new / potentially out of date rows here
      this._clearedFilterCount = this.props.clearedFilterCount
      return
    }
    const toUnbox = this.props.rows.slice(visibleStartIndex, visibleStopIndex + 1).reduce((arr, r) => {
      if (r.type === 'small' && r.conversationIDKey) {
        arr.push(r.conversationIDKey)
      }
      return arr
    }, [])

    this._calculateShowFloating()
    this._calculateShowUnreadShortcut()

    this.props.onUntrustedInboxVisible(toUnbox)
  }, 200)

  _scrollToUnread = () => {
    if (this._firstOffscreenIdx <= 0 || !this._scrollDiv.current) {
      return
    }
    let top = 100 // give it some space below
    for (let i = this._lastVisibleIdx; i <= this._firstOffscreenIdx; i++) {
      top += this._itemSizeGetter(i)
    }
    this._scrollDiv.current.scrollBy({behavior: 'smooth', top})
  }

  _setRef = (list: ?VariableSizeList<any>) => {
    this._list = list
  }

  _prepareNewChat = () => {
    this._list && this._list.scrollTo(0)
    this.props.onNewChat()
  }

  _onEnsureSelection = () => this.props.onEnsureSelection()
  _onSelectUp = () => this.props.onSelectUp()
  _onSelectDown = () => this.props.onSelectDown()

  render() {
    this._selectedVisible =
      !!this.props.filter &&
      !!this.props.rows.find(
        r => r.conversationIDKey && r.conversationIDKey === this.props.selectedConversationIDKey
      )
    const owl = !this.props.rows.length && !!this.props.filter && <Owl />
    const floatingDivider = this.state.showFloating && this.props.allowShowFloatingButton && (
      <BigTeamsDivider toggle={this.props.toggleSmallTeamsExpanded} />
    )
    return (
      <ErrorBoundary>

          <ChatInboxHeader
            filterFocusCount={this.props.filterFocusCount}
            focusFilter={this.props.focusFilter}
            onNewChat={this._prepareNewChat}
            onEnsureSelection={this._onEnsureSelection}
            onSelectUp={this._onSelectUp}
            onSelectDown={this._onSelectDown}
          />
          <NewConversation />

            <AutoSizer>
              {({height, width}) => (
                <VariableSizeList
                  height={height}
                  width={width}
                  ref={this._setRef}
                  outerRef={this._scrollDiv}
                  onItemsRendered={this._onItemsRendered}
                  itemCount={this.props.rows.length}
                  itemSize={this._itemSizeGetter}
                  estimatedItemSize={56}
                >
                  {({index, style}) => this._itemRenderer(index, style)}
                </VariableSizeList>
              )}
            </AutoSizer>

          {owl}
          {floatingDivider || <BuildTeam />}
          {this.state.showUnread && !this.state.showFloating && (
            <UnreadShortcut onClick={this._scrollToUnread} />
          )}

      </ErrorBoundary>
    )
  }
}

const styles = Styles.styleSheetCreate({
  container: Styles.platformStyles({
    isElectron: {
      ...Styles.globalStyles.flexBoxColumn,
      backgroundColor: Styles.globalColors.blueGrey,
      contain: 'strict',
      height: '100%',
      maxWidth: inboxWidth,
      minWidth: inboxWidth,
      position: 'relative',
    },
  }),
  divider: {
    backgroundColor: 'purple',
    overflow: 'hidden',
  },
  hover: {
    backgroundColor: Styles.globalColors.blueGrey2,
  },
  list: {flex: 1},
})

export default Inbox
export type {RowItem, RowItemSmall, RowItemBig, RouteState}
<commit_msg>Don't get stuck showing the unread messages banner if we shouldn't (#16743)<commit_after>// @flow
import * as I from 'immutable'
import * as Types from '../../constants/types/chat2'
import * as Constants from '../../constants/chat2'
import * as React from 'react'
import * as Styles from '../../styles'
import AutoSizer from 'react-virtualized-auto-sizer'
import {VariableSizeList} from 'react-window'
import {ErrorBoundary} from '../../common-adapters'
import {makeRow} from './row'
import BuildTeam from './row/build-team/container'
import ChatInboxHeader from './row/chat-inbox-header/container'
import BigTeamsDivider from './row/big-teams-divider/container'
import TeamsDivider from './row/teams-divider/container'
import {debounce} from 'lodash-es'
import UnreadShortcut from './unread-shortcut'
import {Owl} from './owl'
import NewConversation from './new-conversation/container'
import type {Props, RowItem, RowItemSmall, RowItemBig, RouteState} from './index.types'
import {virtualListMarks} from '../../local-debug'
import {inboxWidth, getRowHeight} from './row/sizes'

type State = {
  showFloating: boolean,
  showUnread: boolean,
}

class Inbox extends React.PureComponent<Props, State> {
  state = {
    showFloating: false,
    showUnread: false,
  }

  _mounted: boolean = false
  _list: ?VariableSizeList<any>
  _clearedFilterCount: number = 0
  _selectedVisible: boolean = false

  // stuff for UnreadShortcut
  _firstOffscreenIdx: number = -1
  _lastVisibleIdx: number = -1
  _scrollDiv = React.createRef()

  componentDidUpdate(prevProps: Props) {
    let listRowsResized = false
    if (prevProps.smallTeamsExpanded !== this.props.smallTeamsExpanded) {
      listRowsResized = true
    }

    // filter / not filter
    if (!!prevProps.filter !== !!this.props.filter) {
      listRowsResized = true
    }
    if (prevProps.filter !== this.props.filter && this._list) {
      this._list.scrollTo(0)
    }

    // list changed
    if (this.props.rows.length !== prevProps.rows.length) {
      this._calculateShowFloating()
      listRowsResized = true
    }

    if (listRowsResized) {
      this._list && this._list.resetAfterIndex(0)
    }

    if (!I.is(this.props.unreadIndices, prevProps.unreadIndices)) {
      this._calculateShowUnreadShortcut()
    }

    if (this.props.filter && this.props.selectedConversationIDKey !== prevProps.selectedConversationIDKey) {
      const selectedIndex = this.props.rows.findIndex(
        // $ForceType
        r => r.conversationIDKey === this.props.selectedConversationIDKey
      )
      selectedIndex >= 0 && this._list && this._list.scrollToItem(selectedIndex)
    }
  }

  componentDidMount() {
    this._mounted = true
  }

  componentWillUnmount() {
    this._mounted = false
  }

  _itemSizeGetter = index => {
    if (this.props.filter.length) {
      return 56
    }
    const row = this.props.rows[index]
    if (!row) {
      return 0
    }

    return getRowHeight(row.type, !!this.props.filter.length, row.type === 'divider' && row.showButton)
  }

  _itemRenderer = (index, style) => {
    const row = this.props.rows[index]
    const divStyle = Styles.collapseStyles([style, virtualListMarks && styles.divider])
    if (row.type === 'divider') {
      return (

          <TeamsDivider
            key="divider"
            toggle={this.props.toggleSmallTeamsExpanded}
            showButton={row.showButton}
            rows={this.props.rows}
          />

      )
    }

    const conversationIDKey: Types.ConversationIDKey = row.conversationIDKey || Constants.noConversationIDKey
    const teamname = row.teamname || ''
    const isHighlighted = index === 0 && !!this.props.filter && !this._selectedVisible

    // pointer events on so you can click even right after a scroll
    return (

        {makeRow({
          channelname: (row.type === 'big' && row.channelname) || '',
          conversationIDKey,
          filtered: !!this.props.filter,
          teamname,
          type: row.type,
        })}

    )
  }

  _calculateShowUnreadShortcut = () => {
    if (!this.props.unreadIndices.size || this._lastVisibleIdx < 0) {
      this.setState(s => (s.showUnread ? {showUnread: false} : null))
      return
    }

    const firstOffscreenIdx = this.props.unreadIndices.find(idx => idx > this._lastVisibleIdx)
    if (firstOffscreenIdx) {
      this.setState(s => (s.showUnread ? null : {showUnread: true}))
      this._firstOffscreenIdx = firstOffscreenIdx
    } else {
      this.setState(s => (s.showUnread ? {showUnread: false} : null))
      this._firstOffscreenIdx = -1
    }
  }

  _calculateShowFloating = () => {
    if (this._lastVisibleIdx < 0) {
      return
    }
    let showFloating = true
    const row = this.props.rows[this._lastVisibleIdx]
    if (!row || row.type !== 'small') {
      showFloating = false
    }

    this.setState(old => (old.showFloating !== showFloating ? {showFloating} : null))
  }

  _onItemsRendered = debounce(({visibleStartIndex, visibleStopIndex}) => {
    this._lastVisibleIdx = visibleStopIndex
    if (this.props.filter.length) {
      return
    }
    this._calculateShowUnreadShortcut()
    if (this.props.clearedFilterCount > this._clearedFilterCount) {
      // just cleared out filter
      // re-rendering normal inbox for the first time
      // no new / potentially out of date rows here
      this._clearedFilterCount = this.props.clearedFilterCount
      return
    }
    const toUnbox = this.props.rows.slice(visibleStartIndex, visibleStopIndex + 1).reduce((arr, r) => {
      if (r.type === 'small' && r.conversationIDKey) {
        arr.push(r.conversationIDKey)
      }
      return arr
    }, [])

    this._calculateShowFloating()

    this.props.onUntrustedInboxVisible(toUnbox)
  }, 200)

  _scrollToUnread = () => {
    if (this._firstOffscreenIdx <= 0 || !this._scrollDiv.current) {
      return
    }
    let top = 100 // give it some space below
    for (let i = this._lastVisibleIdx; i <= this._firstOffscreenIdx; i++) {
      top += this._itemSizeGetter(i)
    }
    this._scrollDiv.current.scrollBy({behavior: 'smooth', top})
  }

  _setRef = (list: ?VariableSizeList<any>) => {
    this._list = list
  }

  _prepareNewChat = () => {
    this._list && this._list.scrollTo(0)
    this.props.onNewChat()
  }

  _onEnsureSelection = () => this.props.onEnsureSelection()
  _onSelectUp = () => this.props.onSelectUp()
  _onSelectDown = () => this.props.onSelectDown()

  render() {
    this._selectedVisible =
      !!this.props.filter &&
      !!this.props.rows.find(
        r => r.conversationIDKey && r.conversationIDKey === this.props.selectedConversationIDKey
      )
    const owl = !this.props.rows.length && !!this.props.filter && <Owl />
    const floatingDivider = this.state.showFloating && this.props.allowShowFloatingButton && (
      <BigTeamsDivider toggle={this.props.toggleSmallTeamsExpanded} />
    )
    return (
      <ErrorBoundary>

          <ChatInboxHeader
            filterFocusCount={this.props.filterFocusCount}
            focusFilter={this.props.focusFilter}
            onNewChat={this._prepareNewChat}
            onEnsureSelection={this._onEnsureSelection}
            onSelectUp={this._onSelectUp}
            onSelectDown={this._onSelectDown}
          />
          <NewConversation />

            <AutoSizer>
              {({height, width}) => (
                <VariableSizeList
                  height={height}
                  width={width}
                  ref={this._setRef}
                  outerRef={this._scrollDiv}
                  onItemsRendered={this._onItemsRendered}
                  itemCount={this.props.rows.length}
                  itemSize={this._itemSizeGetter}
                  estimatedItemSize={56}
                >
                  {({index, style}) => this._itemRenderer(index, style)}
                </VariableSizeList>
              )}
            </AutoSizer>

          {owl}
          {floatingDivider || <BuildTeam />}
          {this.state.showUnread && !this.state.showFloating && (
            <UnreadShortcut onClick={this._scrollToUnread} />
          )}

      </ErrorBoundary>
    )
  }
}

const styles = Styles.styleSheetCreate({
  container: Styles.platformStyles({
    isElectron: {
      ...Styles.globalStyles.flexBoxColumn,
      backgroundColor: Styles.globalColors.blueGrey,
      contain: 'strict',
      height: '100%',
      maxWidth: inboxWidth,
      minWidth: inboxWidth,
      position: 'relative',
    },
  }),
  divider: {
    backgroundColor: 'purple',
    overflow: 'hidden',
  },
  hover: {
    backgroundColor: Styles.globalColors.blueGrey2,
  },
  list: {flex: 1},
})

export default Inbox
export type {RowItem, RowItemSmall, RowItemBig, RouteState}
<|endoftext|>


<commit_before>(function(root, factory){
    if (typeof exports == 'object') {
        // CommonJS
        module.exports = factory(root, require('./plotly'));
    } else {
        // Browser globals
        if (!root.Plotly) { root.Plotly = {}; }
        factory(root, root.Plotly);
    }
}(this, function(exports, Plotly){
    // `exports` is `window`
    // `Plotly` is `window.Plotly`

    'use strict';

    /**
     * UI controller for interactive plots
     * @Class
     * @Param {object} config
     * @Param {object} config.buttons    nested arrays of grouped buttons to initialize
     * @Param {object} config.container  container div to append modebar
     * @Param {object} config.Plotly     main plotly namespace module
     * @Param {object} config.graphInfo  primary plot object containing data and layout
     */
    function ModeBar (config) {

        if (!(this instanceof ModeBar)) return new ModeBar();

        var _this = this;

        this.Plotly = config.Plotly;
        this.graphInfo = config.graphInfo;
        this.element = document.createElement('div');
        this.element.className = 'modebar';
        this.buttons = config.buttons;
        this.buttonElements = [];

        this.buttons.forEach( function (buttonGroup) {
            var group = _this.createGroup();

            buttonGroup.forEach( function (buttonName) {
                var buttonConfig = _this.config()[buttonName];

                if (!buttonConfig) {
                    throw new Error(buttonName + ' not specfied in modebar configuration');
                }

                var button = _this.createButton(buttonConfig);

                _this.buttonElements.push(button);
                group.appendChild(button);
            });

            _this.element.appendChild(group);
        });

        if (this.graphInfo.displaylogo) {
            this.element.appendChild(this.getLogo());
        }

        config.container.appendChild(this.element);

        this.updateActiveButton();

    }

    /**
     * Empty div for containing a group of buttons
     * @Return {HTMLelement}
     */
    ModeBar.prototype.createGroup = function () {
        var group = document.createElement('div');
        group.className = 'modebar-group';

        return group;
    };

    /**
     * Create a new button div and set constant and configurable attributes
     * @Param {object} config
     * @Param {string} config.attr
     * @Param {string} config.val
     * @Param {string} config.title
     * @Param {function} config.click
     * @Return {HTMLelement}
     */
    ModeBar.prototype.createButton = function (config) {
        var button = document.createElement('a');
        var icon = document.createElement('i');
        var _this = this;
        button.setAttribute('rel', 'tooltip');
        button.className = 'modebar-btn ploticon';

        button.setAttribute('data-attr', config.attr);
        button.setAttribute('data-val', config.val);
        button.setAttribute('data-title', config.title);
        button.setAttribute('data-gravity', config.gravity || 'n');
        button.addEventListener('click', function () {
                config.click.apply(_this, arguments);
            });

        icon.className = config.icon;
        button.appendChild(icon);

        return button;
    };

    /**
     * Updates active button with attribute specified in layout
     * @Param {object} graphInfo plot object containing data and layout
     * @Return {HTMLelement}
     */
    ModeBar.prototype.updateActiveButton = function () {
        var graphInfo = this.graphInfo;
        this.buttonElements.forEach( function (button) {
            var thisval = button.getAttribute('data-val') || true,
                dataAttr = button.getAttribute('data-attr'),
                curval = graphInfo._fullLayout[dataAttr];

            button.classList.toggle('active', curval===thisval);
        });
    };

    /**
     * Check if modebar is configured as button configuration argument
     * @Param {object} buttons 2d array of grouped button names
     * @Return {boolean}
     */
    ModeBar.prototype.hasButtons = function (buttons) {
        var currentButtons = this.buttons;

        if (buttons.length !== currentButtons.length) return false;

        for (var i = 0; i < buttons.length; ++i) {
            if (buttons[i].length !== currentButtons[i].length) return false;
            for (var j = 0; j < buttons[i].length; j++) {
                if (buttons[i][j] !== currentButtons[i][j]) return false;
            }
        }

        return true;
    };

    /**
     * @return {HTMLDivElement} The logo image wrapped in a group
     */
    ModeBar.prototype.getLogo = function(){
        var group = this.createGroup(),
            a = document.createElement('a'),
            i = document.createElement('i');

        a.href = 'https://plot.ly/';
        a.target = '_blank';
        a.setAttribute('data-title', 'Produced with Plotly');
        a.className = 'modebar-btn ploticon modebar-btn--logo';

        i.className = 'ploticon-plotlylogo';
        a.appendChild(i);
        group.appendChild(a);
        return group;
    };

    /**
     * Apply D3 cartesian mode attributes to layout to update hover functionality
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleCartesian = function(ev) {
        var button = ev.currentTarget,
            astr = button.getAttribute('data-attr'),
            val = button.getAttribute('data-val') || true,
            _this = this,
            graphInfo = this.graphInfo,
            fullLayout = this.graphInfo._fullLayout,
            Plotly = this.Plotly,
            aobj = {};

        if(astr === 'zoom') {
            var xr = fullLayout.xaxis.range,
                yr = fullLayout.yaxis.range,
                mag = (val==='in') ? 0.5 : 2,
                r0 = (1+mag)/2, r1 = (1-mag)/2;
            aobj = {
                'xaxis.range[0]': r0*xr[0] + r1*xr[1],
                'xaxis.range[1]': r0*xr[1] + r1*xr[0],
                'yaxis.range[0]': r0*yr[0] + r1*yr[1],
                'yaxis.range[1]': r0*yr[1] + r1*yr[0]
            };
        }

        // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
        if (astr==='hovermode' && (val==='x' || val==='y')) {
            val = fullLayout._isHoriz ? 'y' : 'x';
            button.setAttribute('data-val', val);
        }

        aobj[astr] = val;

        Plotly.relayout(graphInfo, aobj).then( function() {
            _this.updateActiveButton();
            if(astr === 'dragmode') {
                Plotly.Fx.setCursor(
                    fullLayout._paper.select('.nsewdrag'),
                    {pan:'move', zoom:'crosshair'}[val]
                );
                Plotly.Fx.supplyDefaults(graphInfo.layout, fullLayout,
                    graphInfo._fullData);
            }
        });
    };

    /**
     * Toggle the data hover mode
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleHover3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            _this = this,
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout),
            layoutUpdate = {};

        // 3D has only 1 hover mode; toggle it
        var val = fullLayout[attr]!=='closest' ? 'closest' : false;
        layoutUpdate[attr] = val;

        // Apply to all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            scene.spikeEnable = !scene.spikeEnable;
            scene.container.focus();
        }

        Plotly.relayout(graphInfo, layoutUpdate).then( function() {
            _this.updateActiveButton();
        });
    };

    /**
     * Reconfigure keyboard bindings for webgl3D camera control on drag
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleDrag3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            val = button.getAttribute('data-val') || true,
            _this = this,
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout),
            layoutUpdate = {};

        // set dragmode to given value
        layoutUpdate[attr] = val;

        // Update the webgl3D key binding of all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            if ('camera' in scene) {
                scene.camera.keyBindingMode = val;
                scene.container.focus();
            }
        }

        Plotly.relayout(graphInfo, layoutUpdate).then( function() {
            _this.updateActiveButton();
        });
    };

    /**
     * Reset the position of the webgl3D camera
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleCamera3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout);

        // Reset camera of all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            if (attr === 'resetDefault') {
                // Reset camera position to default
                scene.setCameraToDefault();
            } else if (attr === 'resetLastSave') {
                // Reset camera back to the position at the last save
                var cameraPositionLastSave = scene.cameraPositionLastSave;
                scene.setCameraPosition(cameraPositionLastSave);
            }
        }

        /* TODO have a sceneLastTouched in _fullLayout to only
         * update the camera of the scene last touched by the user
         */
    };

    ModeBar.prototype.cleanup = function(){
        this.element.innerHTML = '';
        var modebarParent = this.element.parentNode;
        modebarParent.removeChild(this.element);
    };

    /**
     *
     * @Property config specification hash of button parameters
     */
    ModeBar.prototype.config = function config() {
        return {
            zoom2d: {
                title: 'Zoom',
                attr: 'dragmode',
                val: 'zoom',
                icon: 'ploticon-zoombox',
                click: this.handleCartesian
            },
            pan2d: {
                title: 'Pan',
                attr: 'dragmode',
                val: 'pan',
                icon: 'ploticon-pan',
                click: this.handleCartesian
            },
            zoomIn2d: {
                title: 'Zoom in',
                attr: 'zoom',
                val: 'in',
                icon: 'ploticon-zoom_plus',
                click: this.handleCartesian
            },
            zoomOut2d: {
                title: 'Zoom out',
                attr: 'zoom',
                val: 'out',
                icon: 'ploticon-zoom_minus',
                click: this.handleCartesian
            },
            autoScale2d: {
                title: 'Autoscale',
                attr: 'allaxes.autorange',
                val: '',
                icon: 'ploticon-autoscale',
                click: this.handleCartesian
            },
            hoverClosest2d: {
                title: 'Show closest data on hover',
                attr: 'hovermode',
                val: 'closest',
                icon: 'ploticon-tooltip_basic',
                gravity: 'ne',
                click: this.handleCartesian
            },
            hoverCompare2d: {
                title: 'Compare data on hover',
                attr: 'hovermode',
                val: this.graphInfo._fullLayout._isHoriz ? 'y' : 'x',
                icon: 'ploticon-tooltip_compare',
                gravity: 'ne',
                click: this.handleCartesian
            },
            zoom3d: {
                title: 'Zoom',
                attr: 'dragmode',
                val: 'zoom',
                icon: 'ploticon-zoombox',
                click: this.handleDrag3d
            },
            pan3d: {
                title: 'Pan',
                attr: 'dragmode',
                val: 'pan',
                icon: 'ploticon-pan',
                click: this.handleDrag3d
            },
            rotate3d: {
                title: 'Rotate',
                attr: 'dragmode',
                val: 'rotate',
                icon: 'icon-undo',
                click: this.handleDrag3d
            },
            resetCameraDefault3d: {
                title: 'Reset camera to default',
                attr: 'resetDefault',
                val: false,
                icon: 'icon-home',
                click: this.handleCamera3d
            },
            resetCameraLastSave3d: {
                title: 'Reset camera to last save',
                attr: 'resetLastSave',
                val: false,
                icon: 'icon-camera-retro',
                click: this.handleCamera3d
            },
            hoverClosest3d: {
                title: 'Toggle show closest data on hover',
                attr: 'hovermode',
                val: 'closest',
                icon: 'ploticon-tooltip_basic',
                gravity: 'ne',
                click: this.handleHover3d
            }
        };
    };

    return exports.ModeBar = Plotly.ModeBar =ModeBar;

}));
<commit_msg>renaming icons from icon- to ploticon-<commit_after>(function(root, factory){
    if (typeof exports == 'object') {
        // CommonJS
        module.exports = factory(root, require('./plotly'));
    } else {
        // Browser globals
        if (!root.Plotly) { root.Plotly = {}; }
        factory(root, root.Plotly);
    }
}(this, function(exports, Plotly){
    // `exports` is `window`
    // `Plotly` is `window.Plotly`

    'use strict';

    /**
     * UI controller for interactive plots
     * @Class
     * @Param {object} config
     * @Param {object} config.buttons    nested arrays of grouped buttons to initialize
     * @Param {object} config.container  container div to append modebar
     * @Param {object} config.Plotly     main plotly namespace module
     * @Param {object} config.graphInfo  primary plot object containing data and layout
     */
    function ModeBar (config) {

        if (!(this instanceof ModeBar)) return new ModeBar();

        var _this = this;

        this.Plotly = config.Plotly;
        this.graphInfo = config.graphInfo;
        this.element = document.createElement('div');
        this.element.className = 'modebar';
        this.buttons = config.buttons;
        this.buttonElements = [];

        this.buttons.forEach( function (buttonGroup) {
            var group = _this.createGroup();

            buttonGroup.forEach( function (buttonName) {
                var buttonConfig = _this.config()[buttonName];

                if (!buttonConfig) {
                    throw new Error(buttonName + ' not specfied in modebar configuration');
                }

                var button = _this.createButton(buttonConfig);

                _this.buttonElements.push(button);
                group.appendChild(button);
            });

            _this.element.appendChild(group);
        });

        if (this.graphInfo.displaylogo) {
            this.element.appendChild(this.getLogo());
        }

        config.container.appendChild(this.element);

        this.updateActiveButton();

    }

    /**
     * Empty div for containing a group of buttons
     * @Return {HTMLelement}
     */
    ModeBar.prototype.createGroup = function () {
        var group = document.createElement('div');
        group.className = 'modebar-group';

        return group;
    };

    /**
     * Create a new button div and set constant and configurable attributes
     * @Param {object} config
     * @Param {string} config.attr
     * @Param {string} config.val
     * @Param {string} config.title
     * @Param {function} config.click
     * @Return {HTMLelement}
     */
    ModeBar.prototype.createButton = function (config) {
        var button = document.createElement('a');
        var icon = document.createElement('i');
        var _this = this;
        button.setAttribute('rel', 'tooltip');
        button.className = 'modebar-btn ploticon';

        button.setAttribute('data-attr', config.attr);
        button.setAttribute('data-val', config.val);
        button.setAttribute('data-title', config.title);
        button.setAttribute('data-gravity', config.gravity || 'n');
        button.addEventListener('click', function () {
                config.click.apply(_this, arguments);
            });

        icon.className = config.icon;
        button.appendChild(icon);

        return button;
    };

    /**
     * Updates active button with attribute specified in layout
     * @Param {object} graphInfo plot object containing data and layout
     * @Return {HTMLelement}
     */
    ModeBar.prototype.updateActiveButton = function () {
        var graphInfo = this.graphInfo;
        this.buttonElements.forEach( function (button) {
            var thisval = button.getAttribute('data-val') || true,
                dataAttr = button.getAttribute('data-attr'),
                curval = graphInfo._fullLayout[dataAttr];

            button.classList.toggle('active', curval===thisval);
        });
    };

    /**
     * Check if modebar is configured as button configuration argument
     * @Param {object} buttons 2d array of grouped button names
     * @Return {boolean}
     */
    ModeBar.prototype.hasButtons = function (buttons) {
        var currentButtons = this.buttons;

        if (buttons.length !== currentButtons.length) return false;

        for (var i = 0; i < buttons.length; ++i) {
            if (buttons[i].length !== currentButtons[i].length) return false;
            for (var j = 0; j < buttons[i].length; j++) {
                if (buttons[i][j] !== currentButtons[i][j]) return false;
            }
        }

        return true;
    };

    /**
     * @return {HTMLDivElement} The logo image wrapped in a group
     */
    ModeBar.prototype.getLogo = function(){
        var group = this.createGroup(),
            a = document.createElement('a'),
            i = document.createElement('i');

        a.href = 'https://plot.ly/';
        a.target = '_blank';
        a.setAttribute('data-title', 'Produced with Plotly');
        a.className = 'modebar-btn ploticon modebar-btn--logo';

        i.className = 'ploticon-plotlylogo';
        a.appendChild(i);
        group.appendChild(a);
        return group;
    };

    /**
     * Apply D3 cartesian mode attributes to layout to update hover functionality
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleCartesian = function(ev) {
        var button = ev.currentTarget,
            astr = button.getAttribute('data-attr'),
            val = button.getAttribute('data-val') || true,
            _this = this,
            graphInfo = this.graphInfo,
            fullLayout = this.graphInfo._fullLayout,
            Plotly = this.Plotly,
            aobj = {};

        if(astr === 'zoom') {
            var xr = fullLayout.xaxis.range,
                yr = fullLayout.yaxis.range,
                mag = (val==='in') ? 0.5 : 2,
                r0 = (1+mag)/2, r1 = (1-mag)/2;
            aobj = {
                'xaxis.range[0]': r0*xr[0] + r1*xr[1],
                'xaxis.range[1]': r0*xr[1] + r1*xr[0],
                'yaxis.range[0]': r0*yr[0] + r1*yr[1],
                'yaxis.range[1]': r0*yr[1] + r1*yr[0]
            };
        }

        // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
        if (astr==='hovermode' && (val==='x' || val==='y')) {
            val = fullLayout._isHoriz ? 'y' : 'x';
            button.setAttribute('data-val', val);
        }

        aobj[astr] = val;

        Plotly.relayout(graphInfo, aobj).then( function() {
            _this.updateActiveButton();
            if(astr === 'dragmode') {
                Plotly.Fx.setCursor(
                    fullLayout._paper.select('.nsewdrag'),
                    {pan:'move', zoom:'crosshair'}[val]
                );
                Plotly.Fx.supplyDefaults(graphInfo.layout, fullLayout,
                    graphInfo._fullData);
            }
        });
    };

    /**
     * Toggle the data hover mode
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleHover3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            _this = this,
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout),
            layoutUpdate = {};

        // 3D has only 1 hover mode; toggle it
        var val = fullLayout[attr]!=='closest' ? 'closest' : false;
        layoutUpdate[attr] = val;

        // Apply to all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            scene.spikeEnable = !scene.spikeEnable;
            scene.container.focus();
        }

        Plotly.relayout(graphInfo, layoutUpdate).then( function() {
            _this.updateActiveButton();
        });
    };

    /**
     * Reconfigure keyboard bindings for webgl3D camera control on drag
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleDrag3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            val = button.getAttribute('data-val') || true,
            _this = this,
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout),
            layoutUpdate = {};

        // set dragmode to given value
        layoutUpdate[attr] = val;

        // Update the webgl3D key binding of all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            if ('camera' in scene) {
                scene.camera.keyBindingMode = val;
                scene.container.focus();
            }
        }

        Plotly.relayout(graphInfo, layoutUpdate).then( function() {
            _this.updateActiveButton();
        });
    };

    /**
     * Reset the position of the webgl3D camera
     * @Param {object} ev event object
     */
    ModeBar.prototype.handleCamera3d = function(ev) {
        var button = ev.currentTarget,
            attr = button.getAttribute('data-attr'),
            Plotly = this.Plotly,
            graphInfo = this.graphInfo,
            fullLayout = graphInfo._fullLayout,
            sceneLayouts = Plotly.Lib.getSceneLayouts(fullLayout);

        // Reset camera of all scenes
        for (var i = 0;  i < sceneLayouts.length; ++i) {
            var sceneLayout = sceneLayouts[i],
                scene = sceneLayout._scene;

            if (attr === 'resetDefault') {
                // Reset camera position to default
                scene.setCameraToDefault();
            } else if (attr === 'resetLastSave') {
                // Reset camera back to the position at the last save
                var cameraPositionLastSave = scene.cameraPositionLastSave;
                scene.setCameraPosition(cameraPositionLastSave);
            }
        }

        /* TODO have a sceneLastTouched in _fullLayout to only
         * update the camera of the scene last touched by the user
         */
    };

    ModeBar.prototype.cleanup = function(){
        this.element.innerHTML = '';
        var modebarParent = this.element.parentNode;
        modebarParent.removeChild(this.element);
    };

    /**
     *
     * @Property config specification hash of button parameters
     */
    ModeBar.prototype.config = function config() {
        return {
            zoom2d: {
                title: 'Zoom',
                attr: 'dragmode',
                val: 'zoom',
                icon: 'ploticon-zoombox',
                click: this.handleCartesian
            },
            pan2d: {
                title: 'Pan',
                attr: 'dragmode',
                val: 'pan',
                icon: 'ploticon-pan',
                click: this.handleCartesian
            },
            zoomIn2d: {
                title: 'Zoom in',
                attr: 'zoom',
                val: 'in',
                icon: 'ploticon-zoom_plus',
                click: this.handleCartesian
            },
            zoomOut2d: {
                title: 'Zoom out',
                attr: 'zoom',
                val: 'out',
                icon: 'ploticon-zoom_minus',
                click: this.handleCartesian
            },
            autoScale2d: {
                title: 'Autoscale',
                attr: 'allaxes.autorange',
                val: '',
                icon: 'ploticon-autoscale',
                click: this.handleCartesian
            },
            hoverClosest2d: {
                title: 'Show closest data on hover',
                attr: 'hovermode',
                val: 'closest',
                icon: 'ploticon-tooltip_basic',
                gravity: 'ne',
                click: this.handleCartesian
            },
            hoverCompare2d: {
                title: 'Compare data on hover',
                attr: 'hovermode',
                val: this.graphInfo._fullLayout._isHoriz ? 'y' : 'x',
                icon: 'ploticon-tooltip_compare',
                gravity: 'ne',
                click: this.handleCartesian
            },
            zoom3d: {
                title: 'Zoom',
                attr: 'dragmode',
                val: 'zoom',
                icon: 'ploticon-zoombox',
                click: this.handleDrag3d
            },
            pan3d: {
                title: 'Pan',
                attr: 'dragmode',
                val: 'pan',
                icon: 'ploticon-pan',
                click: this.handleDrag3d
            },
            rotate3d: {
                title: 'Rotate',
                attr: 'dragmode',
                val: 'rotate',
                icon: 'ploticon-undo',
                click: this.handleDrag3d
            },
            resetCameraDefault3d: {
                title: 'Reset camera to default',
                attr: 'resetDefault',
                val: false,
                icon: 'ploticon-home',
                click: this.handleCamera3d
            },
            resetCameraLastSave3d: {
                title: 'Reset camera to last save',
                attr: 'resetLastSave',
                val: false,
                icon: 'ploticon-camera-retro',
                click: this.handleCamera3d
            },
            hoverClosest3d: {
                title: 'Toggle show closest data on hover',
                attr: 'hovermode',
                val: 'closest',
                icon: 'ploticon-tooltip_basic',
                gravity: 'ne',
                click: this.handleHover3d
            }
        };
    };

    return exports.ModeBar = Plotly.ModeBar =ModeBar;

}));
<|endoftext|>


<commit_before>(function () {

/**
 * UI controller for interactive plots
 * @Class
 * @Param {object} config
 * @Param {object} config.buttons    nested arrays of grouped buttons to initialize
 * @Param {object} config.container  container div to append modebar
 * @Param {object} config.Plotly     main plotly namespace module
 * @Param {object} config.graphInfo  primary plot object containing data and layout
 */
function ModeBar (config) {

    if (!(this instanceof ModeBar)) return new ModeBar();

    var _this = this;

    this.Plotly = config.Plotly;
    this.graphInfo = config.graphInfo;
    this.element = document.createElement('div');
    this.element.className = 'modebar';
    this.buttons = config.buttons;
    this.buttonElements = [];

    this.buttons.forEach( function (buttonGroup) {
        var group = _this.createGroup();

        buttonGroup.forEach( function (buttonName) {
            var buttonConfig = _this.config[buttonName];

            if (!buttonConfig) {
                throw new Error(buttonName + ' not specfied in modebar configuration');
            }

            var button = _this.createButton(buttonConfig);

            _this.buttonElements.push(button);
            group.appendChild(button);
        });

        _this.element.appendChild(group);
    });

    config.container.appendChild(this.element);

    this.updateActiveButton();

}

window.ModeBar = ModeBar;

/**
 * Empty div for containing a group of buttons
 * @Return {HTMLelement}
 */
ModeBar.prototype.createGroup = function () {
    var group = document.createElement('div');
    group.className = 'btn-group float--left';

    return group;
};

/**
 * Create a new button div and set constant and configurable attributes
 * @Param {object} config
 * @Param {string} config.attr
 * @Param {string} config.val
 * @Param {string} config.title
 * @Param {function} config.click
 * @Return {HTMLelement}
 */
ModeBar.prototype.createButton = function (config) {
    var button = document.createElement('button');
    var icon = document.createElement('i');

    button.setAttribute('rel', 'tooltip');
    button.className = 'btn btn--icon btn--plot ploticon';

    button.setAttribute('data-attr', config.attr);
    button.setAttribute('data-val', config.val);
    button.setAttribute('title', config.title);
    button.addEventListener('click', config.click.bind(this));

    icon.className = config.icon;
    button.appendChild(icon);

    return button;
};

/**
 * Updates active button with attribute specified in layout
 * @Param {object} graphInfo plot object containing data and layout
 * @Return {HTMLelement}
 */
ModeBar.prototype.updateActiveButton = function () {
    var graphInfo = this.graphInfo;
    this.buttonElements.forEach( function (button) {
        var thisval = button.getAttribute('data-val') || true,
            dataAttr = button.getAttribute('data-attr'),
            curval = graphInfo.layout[dataAttr];

        button.classList.toggle('active', curval===thisval);
    });
};

/**
 * Check if modebar is configured as button configuration argument
 * @Param {object} buttons 2d array of grouped button names
 * @Return {boolean}
 */
ModeBar.prototype.hasButtons = function (buttons) {
    var currentButtons = this.buttons;

    if (buttons.length !== currentButtons.length) return false;

    for (var i = 0; i < buttons.length; ++i) {
        if (buttons[i].length !== currentButtons[i].length) return false;
        for (var j = 0; j < buttons[i].length; j++) {
            if (buttons[i][j] !== currentButtons[i][j]) return false;
        }
    }

    return true;
};

/**
 * Apply D3 cartesian mode attributes to layout to update hover functionality
 * @Param {object} ev event object
 */
function handleCartesian (ev) {
    var button = ev.currentTarget,
        astr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        aobj = {},
        graphInfo = this.graphInfo,
        layout = this.graphInfo.layout,
        Plotly = this.Plotly,
        _this = this;

    aobj[astr] = val;

    if(astr === 'zoom') {
        var xr = layout.xaxis.range,
            yr = layout.yaxis.range,
            mag = (val==='in') ? 0.5 : 2,
            r0 = (1+mag)/2, r1 = (1-mag)/2;
        aobj = {
            'xaxis.range[0]': r0*xr[0] + r1*xr[1],
            'xaxis.range[1]': r0*xr[1] + r1*xr[0],
            'yaxis.range[0]': r0*yr[0] + r1*yr[1],
            'yaxis.range[1]': r0*yr[1] + r1*yr[0]
        };
    }

    Plotly.relayout(graphInfo, aobj).then( function() {
        _this.updateActiveButton();
        if(astr === 'dragmode') {
            Plotly.Fx.setCursor(
                layout._paper.select('.nsewdrag'),
                {pan:'move', zoom:'crosshair'}[val]
            );
        }
    });
}

/**
 * Toggle the data hover mode
 * @Param {object} ev event object
 * @Return {HTMLelement}
 */
function handleHover3d (ev) {
    var _this = this,
        button = ev.currentTarget,
        attr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        layoutUpdate = {},
        graphInfo = this.graphInfo,
        layout = graphInfo.layout,
        scenes = Object.keys(layout).filter(function(k){
            return k.match(/^scene[0-9]*$/);
        }).map( function (sceneKey) {
            return layout[sceneKey];
        });

    layoutUpdate[attr] = val;

    scenes.forEach( function (scene) {
        scene._webgl.spikeEnable = !scene._webgl.spikeEnable;
    });

    this.Plotly.relayout(graphInfo, layoutUpdate).then( function() {
        _this.updateActiveButton();
        scenes[0]._container.focus();
    });
}

/**
 * Reconfigure keyboard bindings for webgl3D camera control
 * @Param {object} ev event object
 * @Return {HTMLelement}
 */
function handle3dCamera (ev) {
    var _this = this,
        button = ev.currentTarget,
        attr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        layoutUpdate = {},
        graphInfo = this.graphInfo,
        layout = graphInfo.layout,
        scenes = Object.keys(layout).filter(function(k){
            return k.match(/^scene[0-9]*$/);
        }).map( function (sceneKey) {
            return layout[sceneKey];
        });

    if (attr === 'reset') {
        // Reset camera position to initial value, go in rotate mode
        val = 'rotate';
        var cameraPositionInitial = layout.scene._cameraPositionInitial;
        layoutUpdate = {
            'dragmode': 'rotate',
            'scene.cameraposition': $.extend(true, [], cameraPositionInitial)
        };
    } else {
        layoutUpdate[attr] = val;
    }

    scenes.forEach( function (scene) {
        if ('_webgl' in scene && 'camera' in scene._webgl) {
            scene._webgl.camera.keyBindingMode = val;
        }
    });

    this.Plotly.relayout(graphInfo, layoutUpdate).then( function() {
        _this.updateActiveButton();
        scenes[0]._container.focus();
    });
}

/**
 *
 * @Property config specification hash of button parameters
 */
ModeBar.prototype.config = {
    zoom2d: {
        title: 'Zoom',
        attr: 'dragmode',
        val: 'zoom',
        icon: 'ploticon-zoombox',
        click: handleCartesian
    },
    pan2d: {
        title: 'Pan',
        attr: 'dragmode',
        val: 'pan',
        icon: 'ploticon-pan',
        click: handleCartesian
    },
    zoomIn2d: {
        title: 'Zoom in',
        attr: 'zoom',
        val: 'in',
        icon: 'ploticon-zoom_plus',
        click: handleCartesian
    },
    zoomOut2d: {
        title: 'Zoom out',
        attr: 'zoom',
        val: 'out',
        icon: 'ploticon-zoom_minus',
        click: handleCartesian
    },
    autoScale2d: {
        title: 'Autoscale',
        attr: 'allaxes.autorange',
        val: '',
        icon: 'ploticon-autoscale',
        click: handleCartesian
    },
    hoverClosest2d: {
        title: 'Show closest data on hover',
        attr: 'hovermode',
        val: 'closest',
        icon: 'ploticon-tooltip_basic',
        click: handleCartesian
    },
    hoverCompare2d: {
        title: 'Compare data on hover',
        attr: 'hovermode',
        val: 'x',
        icon: 'ploticon-tooltip_compare',
        click: handleCartesian
    },
    resetCamera3d: {
        title: 'Reset camera',
        attr: 'reset',
        val: '',
        icon: 'ploticon-autoscale',
        click: handle3dCamera
    },
    zoom3d: {
        title: 'Zoom',
        attr: 'dragmode',
        val: 'zoom',
        icon: 'ploticon-zoombox',
        click: handle3dCamera
    },
    pan3d: {
        title: 'Pan',
        attr: 'dragmode',
        val: 'pan',
        icon: 'ploticon-pan',
        click: handle3dCamera
    },
    rotate3d: {
        title: 'Rotate',
        attr: 'dragmode',
        val: 'rotate',
        icon: 'icon-undo',
        click: handle3dCamera
    },
    closest3d: {
        title: 'Toggle show closest data on hover',
        attr: 'hovermode',
        val: 'closest',
        icon: 'ploticon-tooltip_basic',
        click: handleHover3d
    }

};

})();
<commit_msg>change reset-camera button to 'home'<commit_after>(function () {

/**
 * UI controller for interactive plots
 * @Class
 * @Param {object} config
 * @Param {object} config.buttons    nested arrays of grouped buttons to initialize
 * @Param {object} config.container  container div to append modebar
 * @Param {object} config.Plotly     main plotly namespace module
 * @Param {object} config.graphInfo  primary plot object containing data and layout
 */
function ModeBar (config) {

    if (!(this instanceof ModeBar)) return new ModeBar();

    var _this = this;

    this.Plotly = config.Plotly;
    this.graphInfo = config.graphInfo;
    this.element = document.createElement('div');
    this.element.className = 'modebar';
    this.buttons = config.buttons;
    this.buttonElements = [];

    this.buttons.forEach( function (buttonGroup) {
        var group = _this.createGroup();

        buttonGroup.forEach( function (buttonName) {
            var buttonConfig = _this.config[buttonName];

            if (!buttonConfig) {
                throw new Error(buttonName + ' not specfied in modebar configuration');
            }

            var button = _this.createButton(buttonConfig);

            _this.buttonElements.push(button);
            group.appendChild(button);
        });

        _this.element.appendChild(group);
    });

    config.container.appendChild(this.element);

    this.updateActiveButton();

}

window.ModeBar = ModeBar;

/**
 * Empty div for containing a group of buttons
 * @Return {HTMLelement}
 */
ModeBar.prototype.createGroup = function () {
    var group = document.createElement('div');
    group.className = 'btn-group float--left';

    return group;
};

/**
 * Create a new button div and set constant and configurable attributes
 * @Param {object} config
 * @Param {string} config.attr
 * @Param {string} config.val
 * @Param {string} config.title
 * @Param {function} config.click
 * @Return {HTMLelement}
 */
ModeBar.prototype.createButton = function (config) {
    var button = document.createElement('button');
    var icon = document.createElement('i');

    button.setAttribute('rel', 'tooltip');
    button.className = 'btn btn--icon btn--plot ploticon';

    button.setAttribute('data-attr', config.attr);
    button.setAttribute('data-val', config.val);
    button.setAttribute('title', config.title);
    button.addEventListener('click', config.click.bind(this));

    icon.className = config.icon;
    button.appendChild(icon);

    return button;
};

/**
 * Updates active button with attribute specified in layout
 * @Param {object} graphInfo plot object containing data and layout
 * @Return {HTMLelement}
 */
ModeBar.prototype.updateActiveButton = function () {
    var graphInfo = this.graphInfo;
    this.buttonElements.forEach( function (button) {
        var thisval = button.getAttribute('data-val') || true,
            dataAttr = button.getAttribute('data-attr'),
            curval = graphInfo.layout[dataAttr];

        button.classList.toggle('active', curval===thisval);
    });
};

/**
 * Check if modebar is configured as button configuration argument
 * @Param {object} buttons 2d array of grouped button names
 * @Return {boolean}
 */
ModeBar.prototype.hasButtons = function (buttons) {
    var currentButtons = this.buttons;

    if (buttons.length !== currentButtons.length) return false;

    for (var i = 0; i < buttons.length; ++i) {
        if (buttons[i].length !== currentButtons[i].length) return false;
        for (var j = 0; j < buttons[i].length; j++) {
            if (buttons[i][j] !== currentButtons[i][j]) return false;
        }
    }

    return true;
};

/**
 * Apply D3 cartesian mode attributes to layout to update hover functionality
 * @Param {object} ev event object
 */
function handleCartesian (ev) {
    var button = ev.currentTarget,
        astr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        aobj = {},
        graphInfo = this.graphInfo,
        layout = this.graphInfo.layout,
        Plotly = this.Plotly,
        _this = this;

    aobj[astr] = val;

    if(astr === 'zoom') {
        var xr = layout.xaxis.range,
            yr = layout.yaxis.range,
            mag = (val==='in') ? 0.5 : 2,
            r0 = (1+mag)/2, r1 = (1-mag)/2;
        aobj = {
            'xaxis.range[0]': r0*xr[0] + r1*xr[1],
            'xaxis.range[1]': r0*xr[1] + r1*xr[0],
            'yaxis.range[0]': r0*yr[0] + r1*yr[1],
            'yaxis.range[1]': r0*yr[1] + r1*yr[0]
        };
    }

    Plotly.relayout(graphInfo, aobj).then( function() {
        _this.updateActiveButton();
        if(astr === 'dragmode') {
            Plotly.Fx.setCursor(
                layout._paper.select('.nsewdrag'),
                {pan:'move', zoom:'crosshair'}[val]
            );
        }
    });
}

/**
 * Toggle the data hover mode
 * @Param {object} ev event object
 * @Return {HTMLelement}
 */
function handleHover3d (ev) {
    var _this = this,
        button = ev.currentTarget,
        attr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        layoutUpdate = {},
        graphInfo = this.graphInfo,
        layout = graphInfo.layout,
        scenes = Object.keys(layout).filter(function(k){
            return k.match(/^scene[0-9]*$/);
        }).map( function (sceneKey) {
            return layout[sceneKey];
        });

    layoutUpdate[attr] = val;

    scenes.forEach( function (scene) {
        scene._webgl.spikeEnable = !scene._webgl.spikeEnable;
    });

    this.Plotly.relayout(graphInfo, layoutUpdate).then( function() {
        _this.updateActiveButton();
        scenes[0]._container.focus();
    });
}

/**
 * Reconfigure keyboard bindings for webgl3D camera control
 * @Param {object} ev event object
 * @Return {HTMLelement}
 */
function handle3dCamera (ev) {
    var _this = this,
        button = ev.currentTarget,
        attr = button.getAttribute('data-attr'),
        val = button.getAttribute('data-val') || true,
        layoutUpdate = {},
        graphInfo = this.graphInfo,
        layout = graphInfo.layout,
        scenes = Object.keys(layout).filter(function(k){
            return k.match(/^scene[0-9]*$/);
        }).map( function (sceneKey) {
            return layout[sceneKey];
        });

    if (attr === 'reset') {
        // Reset camera position to initial value, go in rotate mode
        val = 'rotate';
        var cameraPositionInitial = layout.scene._cameraPositionInitial;
        layoutUpdate = {
            'dragmode': 'rotate',
            'scene.cameraposition': $.extend(true, [], cameraPositionInitial)
        };
    } else {
        layoutUpdate[attr] = val;
    }

    scenes.forEach( function (scene) {
        if ('_webgl' in scene && 'camera' in scene._webgl) {
            scene._webgl.camera.keyBindingMode = val;
        }
    });

    this.Plotly.relayout(graphInfo, layoutUpdate).then( function() {
        _this.updateActiveButton();
        scenes[0]._container.focus();
    });
}

/**
 *
 * @Property config specification hash of button parameters
 */
ModeBar.prototype.config = {
    zoom2d: {
        title: 'Zoom',
        attr: 'dragmode',
        val: 'zoom',
        icon: 'ploticon-zoombox',
        click: handleCartesian
    },
    pan2d: {
        title: 'Pan',
        attr: 'dragmode',
        val: 'pan',
        icon: 'ploticon-pan',
        click: handleCartesian
    },
    zoomIn2d: {
        title: 'Zoom in',
        attr: 'zoom',
        val: 'in',
        icon: 'ploticon-zoom_plus',
        click: handleCartesian
    },
    zoomOut2d: {
        title: 'Zoom out',
        attr: 'zoom',
        val: 'out',
        icon: 'ploticon-zoom_minus',
        click: handleCartesian
    },
    autoScale2d: {
        title: 'Autoscale',
        attr: 'allaxes.autorange',
        val: '',
        icon: 'ploticon-autoscale',
        click: handleCartesian
    },
    hoverClosest2d: {
        title: 'Show closest data on hover',
        attr: 'hovermode',
        val: 'closest',
        icon: 'ploticon-tooltip_basic',
        click: handleCartesian
    },
    hoverCompare2d: {
        title: 'Compare data on hover',
        attr: 'hovermode',
        val: 'x',
        icon: 'ploticon-tooltip_compare',
        click: handleCartesian
    },
    resetCamera3d: {
        title: 'Reset camera',
        attr: 'reset',
        val: '',
        icon: 'icon-home',
        click: handle3dCamera
    },
    zoom3d: {
        title: 'Zoom',
        attr: 'dragmode',
        val: 'zoom',
        icon: 'ploticon-zoombox',
        click: handle3dCamera
    },
    pan3d: {
        title: 'Pan',
        attr: 'dragmode',
        val: 'pan',
        icon: 'ploticon-pan',
        click: handle3dCamera
    },
    rotate3d: {
        title: 'Rotate',
        attr: 'dragmode',
        val: 'rotate',
        icon: 'icon-undo',
        click: handle3dCamera
    },
    closest3d: {
        title: 'Toggle show closest data on hover',
        attr: 'hovermode',
        val: 'closest',
        icon: 'ploticon-tooltip_basic',
        click: handleHover3d
    }

};

})();
<|endoftext|>


<commit_before>(function(angular, undefined) {
'use strict';

var Module = angular.module('django.shop.filter', []);

// Directive <form shop-product-filter="attribute"> to be used to communicate selected
// attributes used to narrow down the list of products.
Module.directive('shopProductFilter', ['$location', function($location) {
	return {
		require: 'form',
		restrict: 'AC',
		link: function(scope, element, attrs) {
			var params = $location.search(), filter_attrs;
			if (angular.isUndefined(attrs['shopProductFilter']))
				throw new Error("Directive shop-attribute-filter requires an attribute.");
			filter_attrs = scope.$eval(attrs['shopProductFilter']) || [attrs['shopProductFilter']];

			scope.filters = scope.filters || {};
			angular.forEach(filter_attrs, function(attr) {
				if (params[attr]) {
					scope.filters[attr] = params[attr];
				}
			});

			scope.filterChanged = function() {
				var params = {};
				angular.forEach(filter_attrs, function(attr) {
					if (scope.filters[attr]) {
						params[attr] = scope.filters[attr];
					}
				});
				$location.search(params);
				scope.searchQuery = '';  // remove content in search field
				scope.$emit('shopCatalogFilter', params);
			};
		}
	};
}]);

})(window.angular);
<commit_msg>Revert "attempt to fix #494 filters with more than one field"<commit_after>(function(angular, undefined) {
'use strict';

var Module = angular.module('django.shop.filter', []);

// Directive <form shop-product-filter="attribute"> to be used to communicate selected
// attributes used to narrow down the list of products.
Module.directive('shopProductFilter', ['$location', function($location) {
	return {
		require: 'form',
		restrict: 'AC',
		link: function(scope, element, attrs) {
			var params = $location.search(), attr = attrs['shopProductFilter'];
			if (!attr)
				throw new Error("Directive shop-attribute-filter requires an attribute.");

			scope.filters = scope.filters || {};
			if (params[attr]) {
				scope.filters[attr] = params[attr];
			}

			scope.filterChanged = function() {
				var params = {};
				if (scope.filters[attr]) {
					params[attr] = scope.filters[attr];
				}
				$location.search(params);
				scope.searchQuery = '';  // remove content in search field
				scope.$emit('shopCatalogFilter', params);
			};
		}
	};
}]);

})(window.angular);
<|endoftext|>


<commit_before>var fs        = require('fs-extra');
var user      = require('../lib/user');
var cliUtil   = require('../lib/cli-util');
var sfClient  = require('../lib/sf-client');
var index     = require('../lib/index');
var config    = require('../lib/config');
var metaMap   = require('../lib/metadata-map');
var getFiles  = require('../lib/get-files');
var Promise   = require('bluebird');
var path      = require('path');
var async     = require('async');
var _         = require('lodash');
var archiver  = require('archiver');
var logger    = require('../lib/logger');
var dmcignore = require('../lib/dmcignore');
var resolve   = require('../lib/resolve');
var hl        = logger.highlight;

function createStubFiles(map, client) {
  var keys = map.index.getMemberTypeNames();

  function iterator(obj, cb) {
    logger.create(obj.type + '::' + hl(obj.object.name));
    client.tooling.insert(obj, function(err, res) {
      if(err) return cb(err);
      map.setMetaId(obj.type, obj.object.name, res.id);
      cb(null, res.id);
    });
  }

  return new Promise(function(resolve, reject) {

    var stubs = [];

    _.each(keys, function(k) {
      _(map.meta[k])
        .filter(function(o) {
          return (!o.id || o.id === null || o.id === '');
        })
        .each(function(o) {
          stubs.push(metaMap.getStub(k, o.name, o.object));
        })
        .value();
    });

    if(!stubs || !stubs.length) return resolve([]);

    async.mapLimit(stubs, 5, iterator, function(err, results) {
      if(err) {
        logger.error(err.message);
        return reject(new Error('unable to create stub files'));
      } else {
        return resolve(results);
      }
    });

  });
}

function createStaticResources(map, client) {

  function iterator(obj, cb) {
    fs.readFile(obj.path, { encoding: 'base64' }, function(err, body) {
      if(err) return cb(err);

      var opts = {
        id: obj.id,
        type: 'StaticResource',
        object: {
          name: obj.name,
          contenttype: 'application/zip', // hard-coding this for now
          body: body
        }
      };

      var method = (obj.id) ? 'update' : 'insert';

      logger.log('executing StaticResource ' + method);

      client.tooling[method](opts, function(err, res) {
        if(err) return cb(err);
        logger[(method === 'insert') ? 'create' : 'update']('StaticResource::' + obj.name);
        cb(null, res);
      });

    });
  }

  return new Promise(function(resolve, reject) {
    async.mapLimit(map.meta.StaticResource, 5, iterator, function(err, srs) {
      if(err) {
        logger.error('StaticResource deploy error');
        return reject(err);
      }
      resolve(srs);
    });
  });
}

function createContainer(client) {
  logger.log('creating container');
  var name = 'dmc:' + (new Date()).getTime();

  return new Promise(function(resolve, reject) {

    client.tooling.createContainer({ name: name }, function(err, container) {
      if(err) return reject(err);
      logger.create('metadata container: ' + hl(container.id));
      resolve(container.id);
    });

  });
}

function createDeployArtifacts(map, containerId, client) {

  var iterator = function(m, cb2) {
    if(map.index.getMemberTypeNames().indexOf(m.type) === -1) {
      return cb2(null);
    }

    fs.readFile(m.path, { encoding: 'utf8' }, function(err, data) {
      if(err) return cb2(err);

      var artifact = client.tooling.createDeployArtifact(m.type + 'Member', {
        body: data,
        contentEntityId: m.id
      });

      if(!artifact) {
        return cb2(new Error('couldn\'t create artifact: ' + m.name));
      }

      var opts = {
        id: containerId,
        artifact: artifact
      };

      client.tooling.addContainerArtifact(opts, function(err, resp) {
        if(err) {
          logger.error('problem creating container artifact: ' + m.type + '::' + m.name);
          return cb2(err);
        }
        logger.create('container member: ' + m.type + '::' + m.name);
        return cb2(null, resp);
      });

    });
  };

  return new Promise(function(resolve, reject) {

    var files = _(map.meta)
      .values()
      .flatten()
      .remove(function(m) {
        return (m.id && m.id !== '');
      })
      .value();

    async.mapLimit(files, 5, iterator, function(err, res) {
      if(err) reject(err);
      else resolve(res);
    });

  });

}

function deployContainer(containerId, client) {

  return new Promise(function(resolve, reject) {

    var asyncContainerId;

    var opts = {
      id:          containerId,
      isCheckOnly: false
    };

    function logStatus(status) {
      logger.list('deploy status: ' + hl(status));
    }

    function poll() {

      var pollOpts = {
        id: asyncContainerId
      };

      client.tooling.getContainerDeployStatus(pollOpts, function(err, resp) {

        if(err) return reject(err);

        logStatus(resp.State);

        if(resp.State === 'Completed') {
          logger.success('deployment successful');
          return resolve(resp);
        } else if(resp.State === 'Failed') {
          var msg = resp.ErrorMsg || 'unknown error occurred';
          logger.error(msg);
          _.each(resp.CompilerErrors, function(e) {
            logger.error('=> ' + e.extent[0] + ': ' + e.name[0]);
            logger.error('   Line ' + e.line[0] + ' - ' + e.problem[0]);
          });
          return reject(new Error('Compiler Errors'));
        } else if(resp.State === 'Errored') {
          logger.error('Compile error:');
          logger.error(res.ErrorMsg);
          return reject(new Error(res.ErrorMsg));
        } else {
          setTimeout(function() {
            poll();
          }, 1000);
        }
      });
    }

    client.tooling.deployContainer(opts, function(err, asyncContainer) {
      if(err) return cb(err);
      logger.log('Deploying...');
      asyncContainerId = asyncContainer.id;
      poll();
    });

  });

}

function deleteContainer(containerId, client) {
  var opts = {
    type: 'MetadataContainer',
    id: containerId
  };

  return new Promise(function(resolve, reject) {
    client.tooling.delete(opts, function(err, res) {
      if(err) return reject(err);
      logger.destroy('metadata container: ' + hl(containerId));
      resolve(containerId);
    });
  });
}

function runToolingDeploy(map, client) {
  var containerId;

  return Promise.resolve()

    .then(function() {
      logger.log('loading related metadata ids');
      return map.fetchIds().then(function(results) {
        logger.log('loaded ' + hl(results.length) + ' ids');
      });
    })

    // create stub files if necessary
    .then(function() {
      logger.log('creating stub files');
      return createStubFiles(map, client).then(function(stubs) {
        logger.log('created ' + hl(stubs.length) + ' stub files');
      });
    })

    // create static resources
    .then(function() {
      logger.log('creating static resources');
      return createStaticResources(map, client).then(function(srs) {
        logger.log('deployed ' + hl(srs.length) + ' static resources');
      });
    })

    .then(function(){
      return createContainer(client);
    })

    .then(function(id) {
      containerId = id;
      return createDeployArtifacts(map, containerId, client);
    })

    .then(function(){
      return deployContainer(containerId, client);
    })

    .finally(function() {
      if(containerId) {
        return deleteContainer(containerId, client);
      }
    });

}

function logDetails(res, opts) {
  if(!res.details) return;

  var cType;
  var method;

  if(res.details.componentSuccesses) {

    logger.success('component successes [' + res.details.componentSuccesses.length + '] ====>');

    _(res.details.componentSuccesses)
      .map(function(e) {
        e.cType = (_.isString(e.componentType) && e.componentType.length) ?
          (e.componentType + ': ') :
          '';

        e.method =
          (e.created) ? 'create'  :
          (e.changed) ? 'update'  :
          (e.deleted) ? 'destroy' :
          'noChange';

        return e;
      })
      .sortBy(function(e) {
        return e.cType + e.fullName;
      })
      .each(function(e) {
        logger[e.method](e.cType + e.fullName);
      })
      .value();
  }

  if(res.details.componentFailures) {
    logger.error('component failures [' + res.details.componentFailures.length + '] ====>');

    _(res.details.componentFailures)
      .map(function(e) {
        e.cType = _.isString(e.componentType) ?
          (e.componentType + ': ') :
          '';

        return e;
      })
      .sortBy(function(e) {
        return e.cType + e.fullName;
      })
      .each(function(e) {
        logger.listError(
          '[' + e.cType + e.fullName + '] ' +
          e.problemType + ' at ' +
          'l:' + (e.lineNumber || '0') + '/' +
          'c:' + (e.columnNumber || '0') + ' '  +
          '=> ' +
          e.problem
        );
      })
      .value();
  }

  if(res.details.runTestResult) {
    if(res.details.runTestResult.numFailures) {
      logger.error('test results ====>');
    } else {
      logger.success('test results ====>');
    }
    // console.error(res.details.runTestResult.codeCoverage);
    // console.error(res.details.runTestResult.codeCoverageWarnings);
    logger.list('tests run: ' + res.details.runTestResult.numTestsRun);
    logger.list('failures: ' + res.details.runTestResult.numFailures);
    logger.list('total time: ' + res.details.runTestResult.totalTime);

    var cc = res.details.runTestResult.codeCoverage;

    if(opts.coverage && cc && cc.length) {
      logger.success('code coverage results ====>');

      _(cc)
        .map(function(c) {
          var locations = c.numLocations;
          var notCovered = c.numLocationsNotCovered;
          var covered = locations - notCovered;
          var coverage = ((covered / locations) * 100);

          if(locations === 0) {
            coverage = 100;
          }

          return {
            type: c.type,
            name: c.name,
            locations: locations,
            notCovered: notCovered,
            covered: covered,
            coverage: coverage
          };
        })
        .sortBy(function(c) {
          return c.coverage * -1;
        })
        .each(function(c) {
          logger.list(c.coverage.toFixed(2) + '% => ' + c.type + ':' + c.name + ' (' + c.covered + '/' + c.locations + ')');
        })
        .value();
    }

    var ccw = res.details.runTestResult.codeCoverageWarnings;

    if(ccw && ccw.length) {
      logger.error('code coverage warnings ====>');
      _.each(ccw, function(w) {
        logger.list(w.message);
      });
    }

    _.each(res.details.runTestResult.failures, function (f) {
      logger.error(f);
    });
  }
}

function runMetadataDeploy(map, client, opts) {

  return new Promise(function(resolve, reject) {
    var archive = archiver('zip');

    var promise = client.meta.deployAndPoll({
      zipFile: archive,
      includeDetails: true,
      deployOptions: {
        rollbackOnError: true
      }
    });

    // write the package.xml to the zip
    archive.append(
      new Buffer(map.createPackageXML(client.apiVersion)
    ), { name: 'src/package.xml' });

    promise.poller.on('poll', function(res) {
      logger.log('deploy status: ' + hl(res.status));
    });

    promise.then(function(results){
      logDetails(results, opts);
      resolve();
    }).catch(function(err) {
      if(err.details) {
        logDetails(err, opts);
      } else {
        logger.error(err.message);
      }
      reject(new Error('metatadata api deployment failed'));
    });

    // iterator for adding files
    // checks for existence and adds
    function iterator(p, cb) {
      var exists;

      fs.existsAsync(p).then(function(e) {
        exists = e;

        if(!exists) {
          throw new Error('missing file: ' + p);
        }

        return fs.lstatAsync(p);
      }).then(function(stat) {
        if(stat.isDirectory()) {
          archive.directory(p);
        } else {
          archive.file(p);
        }
        logger.list(p);

        cb(null, {
          file: p,
          exists: exists
        });
      });

    }

    logger.log('adding metadata');

    // map over files to add and add if they exist
    async.mapLimit(map.getFilePathsForDeploy(), 5, iterator, function(err, res) {
      if(err) {
        reject(err);
      } else {
        var hasErrors = false;

        _.each(res, function(r) {
          if(!r.exists) {
            logger.error('missing file: ' + r.file);
            hasErrors = true;
            return;
          }
        });

        if(hasErrors) {
          return reject(new Error('cannot deploy - missing files'));
        }

        logger.log('starting deploy');
        archive.finalize();
      }
    });

  });
}

var run = module.exports.run = function(opts, cb) {

  return resolve(cb, function(){

    var containerId;
    var client;
    var oauth = opts.oauth;
    var globs = (opts.globs && opts.globs.length > 0) ?
      opts.globs:
      ['src/**/*'];

    var map = metaMap.createMap({
      oauth: opts.oauth,
      org:   opts.org
    });

    var ignores = null;

    return Promise.resolve()

    .then(config.loadAll)

    .then(function(){
      dmcignore.load().then(function(lines) {
        ignores = lines;
      });
    })

    .then(function(){
      return sfClient.getClient(opts.oauth);
    })

    // load the index for the org
    .then(function(sfdcClient) {
      client = sfdcClient;
      return map.autoLoad();
    })

    // search src/ for file matches
    .then(function() {
      logger.log('searching for local metadata');
      return getFiles({ globs: globs, ignores: ignores })
        .then(function(files) {
          if(!files || files.length < 1) {
            throw new Error('no files for deployment found');
          }
          logger.log('deploying ' + hl(files.length) + ' metadata files');
          map.addFiles(files);
        });
    })

    .then(function() {

      var deployMode = config.get('deploy_mode') || 'dynamic';

      logger.log('deploy mode: ' + deployMode);

      if(!map.requiresMetadataDeploy() && !opts.meta && deployMode !== 'metadata') {
        logger.info('deploy api: ' + hl('tooling'));
        return runToolingDeploy(map, client);
      } else {
        logger.info('deploy api: ' + hl('metadata'));
        return runMetadataDeploy(map, client, opts);
      }
    });

  });

};

module.exports.cli = function(program) {
  program.command('deploy [globs...]')
    .description('deploy metadata to target org')
    .option('-o, --org <org>', 'the Salesforce organization to use')
    .option('--coverage', 'show code coverage for tests run')
    .option('--meta', 'force deploy with metadata api')
    .action(function(globs, opts) {
      opts.globs = globs;
      opts._loadOrg = true;
      return cliUtil.executeRun(run)(opts);
    });
};
<commit_msg>better logging for deploy errors<commit_after>var fs        = require('fs-extra');
var user      = require('../lib/user');
var cliUtil   = require('../lib/cli-util');
var sfClient  = require('../lib/sf-client');
var index     = require('../lib/index');
var config    = require('../lib/config');
var metaMap   = require('../lib/metadata-map');
var getFiles  = require('../lib/get-files');
var Promise   = require('bluebird');
var path      = require('path');
var async     = require('async');
var _         = require('lodash');
var archiver  = require('archiver');
var logger    = require('../lib/logger');
var dmcignore = require('../lib/dmcignore');
var resolve   = require('../lib/resolve');
var hl        = logger.highlight;

function createStubFiles(map, client) {
  var keys = map.index.getMemberTypeNames();

  function iterator(obj, cb) {
    logger.create(obj.type + '::' + hl(obj.object.name));
    client.tooling.insert(obj, function(err, res) {
      if(err) return cb(err);
      map.setMetaId(obj.type, obj.object.name, res.id);
      cb(null, res.id);
    });
  }

  return new Promise(function(resolve, reject) {

    var stubs = [];

    _.each(keys, function(k) {
      _(map.meta[k])
        .filter(function(o) {
          return (!o.id || o.id === null || o.id === '');
        })
        .each(function(o) {
          stubs.push(metaMap.getStub(k, o.name, o.object));
        })
        .value();
    });

    if(!stubs || !stubs.length) return resolve([]);

    async.mapLimit(stubs, 5, iterator, function(err, results) {
      if(err) {
        logger.error(err.message);
        return reject(new Error('unable to create stub files'));
      } else {
        return resolve(results);
      }
    });

  });
}

function createStaticResources(map, client) {

  function iterator(obj, cb) {
    fs.readFile(obj.path, { encoding: 'base64' }, function(err, body) {
      if(err) return cb(err);

      var opts = {
        id: obj.id,
        type: 'StaticResource',
        object: {
          name: obj.name,
          contenttype: 'application/zip', // hard-coding this for now
          body: body
        }
      };

      var method = (obj.id) ? 'update' : 'insert';

      logger.log('executing StaticResource ' + method);

      client.tooling[method](opts, function(err, res) {
        if(err) return cb(err);
        logger[(method === 'insert') ? 'create' : 'update']('StaticResource::' + obj.name);
        cb(null, res);
      });

    });
  }

  return new Promise(function(resolve, reject) {
    async.mapLimit(map.meta.StaticResource, 5, iterator, function(err, srs) {
      if(err) {
        logger.error('StaticResource deploy error');
        return reject(err);
      }
      resolve(srs);
    });
  });
}

function createContainer(client) {
  logger.log('creating container');
  var name = 'dmc:' + (new Date()).getTime();

  return new Promise(function(resolve, reject) {

    client.tooling.createContainer({ name: name }, function(err, container) {
      if(err) return reject(err);
      logger.create('metadata container: ' + hl(container.id));
      resolve(container.id);
    });

  });
}

function createDeployArtifacts(map, containerId, client) {

  var iterator = function(m, cb2) {
    if(map.index.getMemberTypeNames().indexOf(m.type) === -1) {
      return cb2(null);
    }

    fs.readFile(m.path, { encoding: 'utf8' }, function(err, data) {
      if(err) return cb2(err);

      var artifact = client.tooling.createDeployArtifact(m.type + 'Member', {
        body: data,
        contentEntityId: m.id
      });

      if(!artifact) {
        return cb2(new Error('couldn\'t create artifact: ' + m.name));
      }

      var opts = {
        id: containerId,
        artifact: artifact
      };

      client.tooling.addContainerArtifact(opts, function(err, resp) {
        if(err) {
          logger.error('problem creating container artifact: ' + m.type + '::' + m.name);
          return cb2(err);
        }
        logger.create('container member: ' + m.type + '::' + m.name);
        return cb2(null, resp);
      });

    });
  };

  return new Promise(function(resolve, reject) {

    var files = _(map.meta)
      .values()
      .flatten()
      .remove(function(m) {
        return (m.id && m.id !== '');
      })
      .value();

    async.mapLimit(files, 5, iterator, function(err, res) {
      if(err) reject(err);
      else resolve(res);
    });

  });

}

function deployContainer(containerId, client) {

  return new Promise(function(resolve, reject) {

    var asyncContainerId;

    var opts = {
      id:          containerId,
      isCheckOnly: false
    };

    function logStatus(status) {
      logger.list('deploy status: ' + hl(status));
    }

    function poll() {

      var pollOpts = {
        id: asyncContainerId
      };

      client.tooling.getContainerDeployStatus(pollOpts, function(err, resp) {

        if(err) return reject(err);

        logStatus(resp.State);

        if(resp.State === 'Completed') {
          logger.success('deployment successful');
          return resolve(resp);
        } else if(resp.State === 'Failed') {
          var msg = resp.ErrorMsg || 'unknown error occurred';
          logger.error(msg);
          _.each(resp.CompilerErrors, function(e) {
            logger.error('=> ' + e.extent[0] + ': ' + e.name[0]);
            logger.error('   Line ' + e.line[0] + ' - ' + e.problem[0]);
          });
          return reject(new Error('Compiler Errors'));
        } else if(resp.State === 'Errored') {
          logger.error('Compile error:');
          logger.error(res.ErrorMsg);
          return reject(new Error(res.ErrorMsg));
        } else {
          setTimeout(function() {
            poll();
          }, 1000);
        }
      });
    }

    client.tooling.deployContainer(opts, function(err, asyncContainer) {
      if(err) return cb(err);
      logger.log('Deploying...');
      asyncContainerId = asyncContainer.id;
      poll();
    });

  });

}

function deleteContainer(containerId, client) {
  var opts = {
    type: 'MetadataContainer',
    id: containerId
  };

  return new Promise(function(resolve, reject) {
    client.tooling.delete(opts, function(err, res) {
      if(err) return reject(err);
      logger.destroy('metadata container: ' + hl(containerId));
      resolve(containerId);
    });
  });
}

function runToolingDeploy(map, client) {
  var containerId;

  return Promise.resolve()

    .then(function() {
      logger.log('loading related metadata ids');
      return map.fetchIds().then(function(results) {
        logger.log('loaded ' + hl(results.length) + ' ids');
      });
    })

    // create stub files if necessary
    .then(function() {
      logger.log('creating stub files');
      return createStubFiles(map, client).then(function(stubs) {
        logger.log('created ' + hl(stubs.length) + ' stub files');
      });
    })

    // create static resources
    .then(function() {
      logger.log('creating static resources');
      return createStaticResources(map, client).then(function(srs) {
        logger.log('deployed ' + hl(srs.length) + ' static resources');
      });
    })

    .then(function(){
      return createContainer(client);
    })

    .then(function(id) {
      containerId = id;
      return createDeployArtifacts(map, containerId, client);
    })

    .then(function(){
      return deployContainer(containerId, client);
    })

    .finally(function() {
      if(containerId) {
        return deleteContainer(containerId, client);
      }
    });

}

function logDetails(res, opts) {
  if(!res.details) return;

  var cType;
  var method;

  if(res.details.componentSuccesses) {

    logger.success('component successes [' + res.details.componentSuccesses.length + '] ====>');

    _(res.details.componentSuccesses)
      .map(function(e) {
        e.cType = (_.isString(e.componentType) && e.componentType.length) ?
          (e.componentType + ': ') :
          '';

        e.method =
          (e.created) ? 'create'  :
          (e.changed) ? 'update'  :
          (e.deleted) ? 'destroy' :
          'noChange';

        return e;
      })
      .sortBy(function(e) {
        return e.cType + e.fullName;
      })
      .each(function(e) {
        logger[e.method](e.cType + e.fullName);
      })
      .value();
  }

  if(res.details.componentFailures) {
    logger.error('component failures [' + res.details.componentFailures.length + '] ====>');

    _(res.details.componentFailures)
      .map(function(e) {
        e.cType = _.isString(e.componentType) ?
          (e.componentType + ': ') :
          '';

        return e;
      })
      .sortBy(function(e) {
        return e.cType + e.fullName;
      })
      .each(function(e) {
        logger.listError(
          '[' + e.cType + e.fullName + '] ' +
          e.problemType + ' at ' +
          'l:' + (e.lineNumber || '0') + '/' +
          'c:' + (e.columnNumber || '0') + ' '  +
          '=> ' +
          e.problem
        );
      })
      .value();
  }

  if(res.details.runTestResult) {
    if(res.details.runTestResult.numFailures) {
      logger.error('test results ====>');
    } else {
      logger.success('test results ====>');
    }
    // console.error(res.details.runTestResult.codeCoverage);
    // console.error(res.details.runTestResult.codeCoverageWarnings);
    logger.list('tests run: ' + res.details.runTestResult.numTestsRun);
    logger.list('failures: ' + res.details.runTestResult.numFailures);
    logger.list('total time: ' + res.details.runTestResult.totalTime);

    var cc = res.details.runTestResult.codeCoverage;

    if(opts.coverage && cc && cc.length) {
      logger.success('code coverage results ====>');

      _(cc)
        .map(function(c) {
          var locations = c.numLocations;
          var notCovered = c.numLocationsNotCovered;
          var covered = locations - notCovered;
          var coverage = ((covered / locations) * 100);

          if(locations === 0) {
            coverage = 100;
          }

          return {
            type: c.type,
            name: c.name,
            locations: locations,
            notCovered: notCovered,
            covered: covered,
            coverage: coverage
          };
        })
        .sortBy(function(c) {
          return c.coverage * -1;
        })
        .each(function(c) {
          logger.list(c.coverage.toFixed(2) + '% => ' + c.type + ':' + c.name + ' (' + c.covered + '/' + c.locations + ')');
        })
        .value();
    }

    var ccw = res.details.runTestResult.codeCoverageWarnings;

    if(ccw && ccw.length) {
      logger.error('code coverage warnings ====>');
      _.each(ccw, function(w) {
        logger.list(w.message);
      });
    }

    _.each(res.details.runTestResult.failures, function (f) {
      logger.error(f);
    });
  }
}

function runMetadataDeploy(map, client, opts) {

  return new Promise(function(resolve, reject) {
    var archive = archiver('zip');

    var promise = client.meta.deployAndPoll({
      zipFile: archive,
      includeDetails: true,
      deployOptions: {
        rollbackOnError: true
      }
    });

    // write the package.xml to the zip
    archive.append(
      new Buffer(map.createPackageXML(client.apiVersion)
    ), { name: 'src/package.xml' });

    promise.poller.on('poll', function(res) {
      logger.log('deploy status: ' + hl(res.status));
    });

    promise.then(function(results){
      logDetails(results, opts);
      resolve();
    }).catch(function(err) {
      if(err.details) {
        logDetails(err, opts);
      }
      if(err.message) {
        logger.error(err.message);
      }
      if(err.errorMessage) {
        logger.error(err.errorMessage);
      }
      reject(new Error('metatadata api deployment failed'));
    });

    // iterator for adding files
    // checks for existence and adds
    function iterator(p, cb) {
      var exists;

      fs.existsAsync(p).then(function(e) {
        exists = e;

        if(!exists) {
          throw new Error('missing file: ' + p);
        }

        return fs.lstatAsync(p);
      }).then(function(stat) {
        if(stat.isDirectory()) {
          archive.directory(p);
        } else {
          archive.file(p);
        }
        logger.list(p);

        cb(null, {
          file: p,
          exists: exists
        });
      });

    }

    logger.log('adding metadata');

    // map over files to add and add if they exist
    async.mapLimit(map.getFilePathsForDeploy(), 5, iterator, function(err, res) {
      if(err) {
        reject(err);
      } else {
        var hasErrors = false;

        _.each(res, function(r) {
          if(!r.exists) {
            logger.error('missing file: ' + r.file);
            hasErrors = true;
            return;
          }
        });

        if(hasErrors) {
          return reject(new Error('cannot deploy - missing files'));
        }

        logger.log('starting deploy');
        archive.finalize();
      }
    });

  });
}

var run = module.exports.run = function(opts, cb) {

  return resolve(cb, function(){

    var containerId;
    var client;
    var oauth = opts.oauth;
    var globs = (opts.globs && opts.globs.length > 0) ?
      opts.globs:
      ['src/**/*'];

    var map = metaMap.createMap({
      oauth: opts.oauth,
      org:   opts.org
    });

    var ignores = null;

    return Promise.resolve()

    .then(config.loadAll)

    .then(function(){
      dmcignore.load().then(function(lines) {
        ignores = lines;
      });
    })

    .then(function(){
      return sfClient.getClient(opts.oauth);
    })

    // load the index for the org
    .then(function(sfdcClient) {
      client = sfdcClient;
      return map.autoLoad();
    })

    // search src/ for file matches
    .then(function() {
      logger.log('searching for local metadata');
      return getFiles({ globs: globs, ignores: ignores })
        .then(function(files) {
          if(!files || files.length < 1) {
            throw new Error('no files for deployment found');
          }
          logger.log('deploying ' + hl(files.length) + ' metadata files');
          map.addFiles(files);
        });
    })

    .then(function() {

      var deployMode = config.get('deploy_mode') || 'dynamic';

      logger.log('deploy mode: ' + deployMode);

      if(!map.requiresMetadataDeploy() && !opts.meta && deployMode !== 'metadata') {
        logger.info('deploy api: ' + hl('tooling'));
        return runToolingDeploy(map, client);
      } else {
        logger.info('deploy api: ' + hl('metadata'));
        return runMetadataDeploy(map, client, opts);
      }
    });

  });

};

module.exports.cli = function(program) {
  program.command('deploy [globs...]')
    .description('deploy metadata to target org')
    .option('-o, --org <org>', 'the Salesforce organization to use')
    .option('--coverage', 'show code coverage for tests run')
    .option('--meta', 'force deploy with metadata api')
    .action(function(globs, opts) {
      opts.globs = globs;
      opts._loadOrg = true;
      return cliUtil.executeRun(run)(opts);
    });
};
<|endoftext|>