<commit_before>/*
Copyright 2016 First People's Cultural Council

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { Component, PropTypes } from "react"
// import ReactDOM from 'react-dom'
import provide from "react-redux-provide"
import selectn from "selectn"

// import classNames from 'classnames'

import ProviderHelpers from "common/ProviderHelpers"
// import StringHelpers from 'common/StringHelpers'
import UIHelpers from "common/UIHelpers"

// import AuthorizationFilter from 'views/components/Document/AuthorizationFilter'

import AppFrontController from "./AppFrontController"

import Shepherd from "tether-shepherd"

// import FontIcon from 'material-ui/lib/font-icon'
// import Paper from 'material-ui/lib/paper'
// import FlatButton from 'material-ui/lib/flat-button'

import IntlService from "views/services/intl"

// const getPosition = function getPosition() {
//   const doc = document
//   const w = window
//   let x
//   let y
//   let docEl

//   if (typeof w.pageYOffset === 'number') {
//     x = w.pageXOffset
//     y = w.pageYOffset
//   } else {
//     docEl = doc.compatMode && doc.compatMode === 'CSS1Compat' ? doc.documentElement : doc.body
//     x = docEl.scrollLeft
//     y = docEl.scrollTop
//   }
//   return { x: x, y: y }
// }

/**
 * Finds the xPath for a component, leading up to the 'app-wrapper'.
 * Used as a utility for creating Shepherd tours
 */
function findComponentParents(_el) {
  let parents = []
  let el = _el
  const originalEl = el

  if (!el.getAttribute("data-component-id")) {
    return null
  }

  while (el.parentNode) {
    el = el.parentNode

    if (el.getAttribute("id") === "app-wrapper") {
      break
    }

    if (el.tagName) {
      const appendClass = el.className ? "." + el.className.replace(/\s/g, ".").replace(/\:/g, "\\:") : "" // eslint-disable-line
      const appendData = el.getAttribute("data-component-id")
        ? "[data-component-id='" + el.getAttribute("data-component-id") + "']"
        : ""

      parents.push(el.tagName.toLowerCase() + appendClass + appendData)
    }
  }

  parents = parents.reverse()
  parents.push(
    originalEl.tagName.toLowerCase() + "[data-component-id='" + originalEl.getAttribute("data-component-id") + "']"
  )

  return parents.join(" ")
}

const getPreferences = function getPreferences(login, dialect) {
  const preferenceString = selectn("response.properties.preferences", login)
  const parsedPreferences = preferenceString ? JSON.parse(preferenceString) : {}
  const flattenedPreferences = {}

  for (const preferenceCat in parsedPreferences) {
    for (const preference in parsedPreferences[preferenceCat]) {
      flattenedPreferences[preference] = parsedPreferences[preferenceCat][preference]
    }
  }

  // Dialect assignment
  flattenedPreferences.primary_dialect_path = selectn("path", dialect)

  return flattenedPreferences
}

@provide
class AppWrapper extends Component {
  intl = IntlService.instance
  intlBaseKey = "views"

  static propTypes = {
    connect: PropTypes.func.isRequired,
    getCurrentUser: PropTypes.func.isRequired,
    fetchDialect2: PropTypes.func.isRequired,
    computeDialect2: PropTypes.object.isRequired,
    computeLogin: PropTypes.object.isRequired,
    windowPath: PropTypes.string.isRequired,
    splitWindowPath: PropTypes.array.isRequired,
    changeTheme: PropTypes.func.isRequired,
    properties: PropTypes.object.isRequired,
  }

  static childContextTypes = {
    muiTheme: React.PropTypes.object,
  }

  // react-redux-provide will pass context such as providers (Note: this is only needed for debugging the store atm)
  static contextTypes = {
    providers: PropTypes.object,
  }

  /**
   * Pass essential context to all children
   */
  getChildContext() {
    const newContext = {
      muiTheme: this.props.properties.theme.palette,
    }

    return newContext
  }

  constructor(props, context) {
    super(props, context)

    // Connect to Nuxeo
    this.props.connect()
    this.props.getCurrentUser()

    this.state = {
      adminGuideStarted: false,
      dialect: null,
    }

    // Bind methods to 'this'
    ;["_startAdminGuideAssist"].forEach((method) => (this[method] = this[method].bind(this)))
  }

  // Force update of theme if out of sync
  // This is a fix that may be unecessary in future versions of Material-UI, React, Reat-redux-provide
  componentWillReceiveProps(nextProps) {
    if (nextProps.properties.theme.id != this.props.properties.theme.id) {
      nextProps.changeTheme(nextProps.properties.theme.id)
    }
  }

  // Changing a theme manually...
  /*_changeTheme(event) {
      let index = event.nativeEvent.target.selectedIndex;
      this.props.changeTheme(event.target[index].value);
    }*/

  _startAdminGuideAssist() {
    const doms = document.querySelectorAll("[data-component-id]")

    const tour = new Shepherd.Tour({
      defaults: {
        classes: "shepherd-theme-arrows",
      },
    })

    document.onkeydown = function documentOnkeydown(_e) {
      const e = _e || window.event
      switch (e.which || e.keyCode) {
        case 37:
          tour.back()
          break

        case 39:
          tour.next()
          break
        default: // Note: do nothing
      }
    }

    doms.forEach(function domsForEach(dom, i) {
      const xpath = findComponentParents(dom)

      //if (!document.querySelector(xpath)) {
      //  console.log(xpath);
      //}

      if (xpath != null) {
        tour.addStep("step" + i, {
          title: "Element xPath",
          text: xpath + "<textarea>" + dom.textContent + "</textarea>",
          classes: "shepherd-theme-arrows admin-guide-step",
          attachTo: xpath + " bottom",
          showCancelLink: true,
          scrollTo: true,
          when: {
            show: () => {
              dom.style.border = "2px blue dashed"
            },
            hide: () => {
              dom.style.border = "initial"
            },
          },
        })
      }
    })

    this.setState({
      adminGuideStarted: true,
    })

    tour.start()
  }

  render() {
    const computeDialect2 = ProviderHelpers.getEntry(this.props.computeDialect2, this.state.dialect)

    const warnings = {}

    const preferences = getPreferences(this.props.computeLogin, selectn("response", computeDialect2))

    return (

        <AppFrontController preferences={preferences} warnings={warnings} />

        {/*<AuthorizationFilter filter={{
                role: ['Everything'],
                entity: selectn('response.entries[0]', dialects),
                login: this.props.computeLogin
            }}>

                    {this.intl.translate({
                        key: 'super_admin_tools',
                        default: 'Super Admin Tools',
                        case: 'words'
                    })}: <FlatButton onTouchTap={this._startAdminGuideAssist.bind(this.props.windowPath)}
                                     disabled={this.state.adminGuideStarted} label={this.intl.translate({
                    key: 'admin_guide_assist',
                    default: 'Admin Guide Assist', case: 'words'
                })}/>
                    {(this.state.adminGuideStarted) ? this.intl.translate({
                        key: 'only_one_tour_per_page',
                        default: 'You can only run one tour per page. Navigate to another page and remember to hit \'Refresh\'',
                        case: 'first'
                    }) : ''}

            </AuthorizationFilter>*/}

    )
  }
}

export default AppWrapper
<commit_msg>AppWrapper: lint, prettier<commit_after>/*
Copyright 2016 First People's Cultural Council

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { Component, PropTypes } from 'react'
// import ReactDOM from 'react-dom'
import provide from 'react-redux-provide'
import selectn from 'selectn'

// import classNames from 'classnames'

import ProviderHelpers from 'common/ProviderHelpers'
// import StringHelpers from 'common/StringHelpers'
import UIHelpers from 'common/UIHelpers'

// import AuthorizationFilter from 'views/components/Document/AuthorizationFilter'

import AppFrontController from './AppFrontController'

import Shepherd from 'tether-shepherd'

// import FontIcon from 'material-ui/lib/font-icon'
// import Paper from 'material-ui/lib/paper'
// import FlatButton from 'material-ui/lib/flat-button'

import IntlService from 'views/services/intl'

// const getPosition = function getPosition() {
//   const doc = document
//   const w = window
//   let x
//   let y
//   let docEl

//   if (typeof w.pageYOffset === 'number') {
//     x = w.pageXOffset
//     y = w.pageYOffset
//   } else {
//     docEl = doc.compatMode && doc.compatMode === 'CSS1Compat' ? doc.documentElement : doc.body
//     x = docEl.scrollLeft
//     y = docEl.scrollTop
//   }
//   return { x: x, y: y }
// }

/**
 * Finds the xPath for a component, leading up to the 'app-wrapper'.
 * Used as a utility for creating Shepherd tours
 */
function findComponentParents(_el) {
  let parents = []
  let el = _el
  const originalEl = el

  if (!el.getAttribute('data-component-id')) {
    return null
  }

  while (el.parentNode) {
    el = el.parentNode

    if (el.getAttribute('id') === 'app-wrapper') {
      break
    }

    if (el.tagName) {
      const appendClass = el.className ? '.' + el.className.replace(/\s/g, '.').replace(/\:/g, '\\:') : '' // eslint-disable-line
      const appendData = el.getAttribute('data-component-id')
        ? "[data-component-id='" + el.getAttribute('data-component-id') + "']"
        : ''

      parents.push(el.tagName.toLowerCase() + appendClass + appendData)
    }
  }

  parents = parents.reverse()
  parents.push(
    originalEl.tagName.toLowerCase() + "[data-component-id='" + originalEl.getAttribute('data-component-id') + "']"
  )

  return parents.join(' ')
}

const getPreferences = function getPreferences(login, dialect) {
  const preferenceString = selectn('response.properties.preferences', login)
  const parsedPreferences = preferenceString ? JSON.parse(preferenceString) : {}
  const flattenedPreferences = {}

  for (const preferenceCat in parsedPreferences) {
    for (const preference in parsedPreferences[preferenceCat]) {
      flattenedPreferences[preference] = parsedPreferences[preferenceCat][preference]
    }
  }

  // Dialect assignment
  flattenedPreferences.primary_dialect_path = selectn('path', dialect)

  return flattenedPreferences
}

@provide
class AppWrapper extends Component {
  intl = IntlService.instance
  intlBaseKey = 'views'

  static propTypes = {
    connect: PropTypes.func.isRequired,
    getCurrentUser: PropTypes.func.isRequired,
    fetchDialect2: PropTypes.func.isRequired,
    computeDialect2: PropTypes.object.isRequired,
    computeLogin: PropTypes.object.isRequired,
    windowPath: PropTypes.string.isRequired,
    splitWindowPath: PropTypes.array.isRequired,
    changeTheme: PropTypes.func.isRequired,
    properties: PropTypes.object.isRequired,
  }

  static childContextTypes = {
    muiTheme: React.PropTypes.object,
  }

  // react-redux-provide will pass context such as providers (Note: this is only needed for debugging the store atm)
  static contextTypes = {
    providers: PropTypes.object,
  }

  /**
   * Pass essential context to all children
   */
  getChildContext() {
    const newContext = {
      muiTheme: this.props.properties.theme.palette,
    }

    return newContext
  }

  constructor(props, context) {
    super(props, context)

    // Connect to Nuxeo
    this.props.connect()
    this.props.getCurrentUser()

    this.state = {
      adminGuideStarted: false,
      dialect: null,
    }

    // Bind methods to 'this'
    ;['_startAdminGuideAssist'].forEach((method) => (this[method] = this[method].bind(this)))
  }

  // Force update of theme if out of sync
  // This is a fix that may be unecessary in future versions of Material-UI, React, Reat-redux-provide
  componentWillReceiveProps(nextProps) {
    if (nextProps.properties.theme.id != this.props.properties.theme.id) {
      nextProps.changeTheme(nextProps.properties.theme.id)
    }
  }

  // Changing a theme manually...
  /*_changeTheme(event) {
      let index = event.nativeEvent.target.selectedIndex;
      this.props.changeTheme(event.target[index].value);
    }*/

  _startAdminGuideAssist() {
    const doms = document.querySelectorAll('[data-component-id]')

    const tour = new Shepherd.Tour({
      defaults: {
        classes: 'shepherd-theme-arrows',
      },
    })

    document.onkeydown = function documentOnkeydown(_e) {
      const e = _e || window.event
      switch (e.which || e.keyCode) {
        case 37:
          tour.back()
          break

        case 39:
          tour.next()
          break
        default: // Note: do nothing
      }
    }

    doms.forEach(function domsForEach(dom, i) {
      const xpath = findComponentParents(dom)

      //if (!document.querySelector(xpath)) {
      //  console.log(xpath);
      //}

      if (xpath != null) {
        tour.addStep('step' + i, {
          title: 'Element xPath',
          text: xpath + '<textarea>' + dom.textContent + '</textarea>',
          classes: 'shepherd-theme-arrows admin-guide-step',
          attachTo: xpath + ' bottom',
          showCancelLink: true,
          scrollTo: true,
          when: {
            show: () => {
              dom.style.border = '2px blue dashed'
            },
            hide: () => {
              dom.style.border = 'initial'
            },
          },
        })
      }
    })

    this.setState({
      adminGuideStarted: true,
    })

    tour.start()
  }

  render() {
    const computeDialect2 = ProviderHelpers.getEntry(this.props.computeDialect2, this.state.dialect)

    const warnings = {}

    const preferences = getPreferences(this.props.computeLogin, selectn('response', computeDialect2))

    return (

        <AppFrontController preferences={preferences} warnings={warnings} />

        {/*<AuthorizationFilter filter={{
                role: ['Everything'],
                entity: selectn('response.entries[0]', dialects),
                login: this.props.computeLogin
            }}>

                    {this.intl.translate({
                        key: 'super_admin_tools',
                        default: 'Super Admin Tools',
                        case: 'words'
                    })}: <FlatButton onTouchTap={this._startAdminGuideAssist.bind(this.props.windowPath)}
                                     disabled={this.state.adminGuideStarted} label={this.intl.translate({
                    key: 'admin_guide_assist',
                    default: 'Admin Guide Assist', case: 'words'
                })}/>
                    {(this.state.adminGuideStarted) ? this.intl.translate({
                        key: 'only_one_tour_per_page',
                        default: 'You can only run one tour per page. Navigate to another page and remember to hit \'Refresh\'',
                        case: 'first'
                    }) : ''}

            </AuthorizationFilter>*/}

    )
  }
}

export default AppWrapper
<|endoftext|>


<commit_before>function LoginStateViewModel() {
    var self = this;

    self.loggedIn = ko.observable(false);
    self.username = ko.observable(undefined);
    self.isAdmin = ko.observable(false);
    self.isUser = ko.observable(false);

    self.currentUser = ko.observable(undefined);

    self.userMenuText = ko.computed(function() {
        if (self.loggedIn()) {
            return "\"" + self.username() + "\"";
        } else {
            return gettext("Login");
        }
    });

    self.subscribers = [];
    self.subscribe = function(callback) {
        if (callback === undefined) return;
        self.subscribers.push(callback);
    };

    self.requestData = function() {
        $.ajax({
            url: API_BASEURL + "login",
            type: "POST",
            data: {"passive": true},
            success: self.fromResponse
        })
    };

    self.fromResponse = function(response) {
        if (response && response.name) {
            self.loggedIn(true);
            self.username(response.name);
            self.isUser(response.user);
            self.isAdmin(response.admin);

            self.currentUser(response);

            _.each(self.subscribers, function(callback) { callback("login", response); });
        } else {
            self.loggedIn(false);
            self.username(undefined);
            self.isUser(false);
            self.isAdmin(false);

            self.currentUser(undefined);

            _.each(self.subscribers, function(callback) { callback("logout", {}); });
        }
    };

    self.login = function() {
        var username = $("#login_user").val();
        var password = $("#login_pass").val();
        var remember = $("#login_remember").is(":checked");

        $("#login_user").val("");
        $("#login_pass").val("");
        $("#login_remember").prop("checked", false);

        $.ajax({
            url: API_BASEURL + "login",
            type: "POST",
            data: {"user": username, "pass": password, "remember": remember},
            success: function(response) {
                new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"});
                self.fromResponse(response);
            },
            error: function(jqXHR, textStatus, errorThrown) {
                new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"});
            }
        })
    };

    self.logout = function() {
        $.ajax({
            url: API_BASEURL + "logout",
            type: "POST",
            success: function(response) {
                new PNotify({title: gettext("Logout successful"), text: gettext("You are now logged out"), type: "success"});
                self.fromResponse(response);
            }
        })
    };

    self.onDataUpdaterReconnect = function() {
        self.requestData();
    };

    self.onStartup = function() {
        self.requestData();
    };
}
<commit_msg>Don't use scare quotes when displaying the username.<commit_after>function LoginStateViewModel() {
    var self = this;

    self.loggedIn = ko.observable(false);
    self.username = ko.observable(undefined);
    self.isAdmin = ko.observable(false);
    self.isUser = ko.observable(false);

    self.currentUser = ko.observable(undefined);

    self.userMenuText = ko.computed(function() {
        if (self.loggedIn()) {
            return self.username();
        } else {
            return gettext("Login");
        }
    });

    self.subscribers = [];
    self.subscribe = function(callback) {
        if (callback === undefined) return;
        self.subscribers.push(callback);
    };

    self.requestData = function() {
        $.ajax({
            url: API_BASEURL + "login",
            type: "POST",
            data: {"passive": true},
            success: self.fromResponse
        })
    };

    self.fromResponse = function(response) {
        if (response && response.name) {
            self.loggedIn(true);
            self.username(response.name);
            self.isUser(response.user);
            self.isAdmin(response.admin);

            self.currentUser(response);

            _.each(self.subscribers, function(callback) { callback("login", response); });
        } else {
            self.loggedIn(false);
            self.username(undefined);
            self.isUser(false);
            self.isAdmin(false);

            self.currentUser(undefined);

            _.each(self.subscribers, function(callback) { callback("logout", {}); });
        }
    };

    self.login = function() {
        var username = $("#login_user").val();
        var password = $("#login_pass").val();
        var remember = $("#login_remember").is(":checked");

        $("#login_user").val("");
        $("#login_pass").val("");
        $("#login_remember").prop("checked", false);

        $.ajax({
            url: API_BASEURL + "login",
            type: "POST",
            data: {"user": username, "pass": password, "remember": remember},
            success: function(response) {
                new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"});
                self.fromResponse(response);
            },
            error: function(jqXHR, textStatus, errorThrown) {
                new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"});
            }
        })
    };

    self.logout = function() {
        $.ajax({
            url: API_BASEURL + "logout",
            type: "POST",
            success: function(response) {
                new PNotify({title: gettext("Logout successful"), text: gettext("You are now logged out"), type: "success"});
                self.fromResponse(response);
            }
        })
    };

    self.onDataUpdaterReconnect = function() {
        self.requestData();
    };

    self.onStartup = function() {
        self.requestData();
    };
}
<|endoftext|>


<commit_before>import {
  NOTIFICATIONS_MOUNT,
  NOTIFICATIONS_UNMOUNT,
  NOTIFICATIONS_SET_VISIBILITY,
  NOTIFICATIONS_UPDATE,
  NOTIFICATIONS_EXPAND_SUCCESS,
  NOTIFICATIONS_EXPAND_REQUEST,
  NOTIFICATIONS_EXPAND_FAIL,
  NOTIFICATIONS_FILTER_SET,
  NOTIFICATIONS_CLEAR,
  NOTIFICATIONS_SCROLL_TOP,
  NOTIFICATIONS_DELETE_MARKED_REQUEST,
  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
  NOTIFICATION_MARK_FOR_DELETE,
  NOTIFICATIONS_DELETE_MARKED_FAIL,
  NOTIFICATIONS_ENTER_CLEARING_MODE,
  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
} from 'flavours/glitch/actions/notifications';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap({
  items: ImmutableList(),
  hasMore: true,
  top: true,
  mounted: 0,
  unread: 0,
  lastReadId: '0',
  isLoading: false,
  cleaningMode: false,
  isTabVisible: true,
  // notification removal mark of new notifs loaded whilst cleaningMode is true.
  markNewForDelete: false,
});

const notificationToMap = (state, notification) => ImmutableMap({
  id: notification.id,
  type: notification.type,
  account: notification.account.id,
  markedForDelete: state.get('markNewForDelete'),
  status: notification.status ? notification.status.id : null,
});

const normalizeNotification = (state, notification) => {
  const top = !shouldCountUnreadNotifications(state);

  if (top) {
    state = state.set('lastReadId', notification.id);
  } else {
    state = state.update('unread', unread => unread + 1);
  }

  return state.update('items', list => {
    if (top && list.size > 40) {
      list = list.take(20);
    }

    return list.unshift(notificationToMap(state, notification));
  });
};

const expandNormalizedNotifications = (state, notifications, next) => {
  const top = !(shouldCountUnreadNotifications(state));
  const lastReadId = state.get('lastReadId');
  let items = ImmutableList();

  notifications.forEach((n, i) => {
    items = items.set(i, notificationToMap(state, n));
  });

  return state.withMutations(mutable => {
    if (!items.isEmpty()) {
      mutable.update('items', list => {
        const lastIndex = 1 + list.findLastIndex(
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
        );

        const firstIndex = 1 + list.take(lastIndex).findLastIndex(
          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
        );

        return list.take(firstIndex).concat(items, list.skip(lastIndex));
      });
    }

    if (top) {
      if (!items.isEmpty()) {
        mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id'));
      }
    } else {
      mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size);
    }

    if (!next) {
      mutable.set('hasMore', false);
    }

    mutable.set('isLoading', false);
  });
};

const filterNotifications = (state, relationship) => {
  return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
};

const clearUnread = (state) => {
  state = state.set('unread', 0);
  const lastNotification = state.get('items').find(item => item !== null);
  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
}

const updateTop = (state, top) => {
  state = state.set('top', top);

  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }

  return state.set('top', top);
};

const deleteByStatus = (state, statusId) => {
  const top = !(shouldCountUnreadNotifications(state));
  if (!top) {
    const lastReadId = state.get('lastReadId');
    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
    state = state.update('unread', unread => unread - deletedUnread.size);
  }
  return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
};

const markForDelete = (state, notificationId, yes) => {
  return state.update('items', list => list.map(item => {
    if(item.get('id') === notificationId) {
      return item.set('markedForDelete', yes);
    } else {
      return item;
    }
  }));
};

const markAllForDelete = (state, yes) => {
  return state.update('items', list => list.map(item => {
    if(yes !== null) {
      return item.set('markedForDelete', yes);
    } else {
      return item.set('markedForDelete', !item.get('markedForDelete'));
    }
  }));
};

const unmarkAllForDelete = (state) => {
  return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
};

const deleteMarkedNotifs = (state) => {
  return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
};

const updateMounted = (state) => {
  state = state.update('mounted', count => count + 1);
  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }
  return state;
};

const updateVisibility = (state, visibility) => {
  state = state.set('isTabVisible', visibility);
  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }
  return state;
};

const shouldCountUnreadNotifications = (state) => {
  return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0);
};

export default function notifications(state = initialState, action) {
  let st;

  switch(action.type) {
  case NOTIFICATIONS_MOUNT:
    return updateMounted(state);
  case NOTIFICATIONS_UNMOUNT:
    return state.update('mounted', count => count - 1);
  case NOTIFICATIONS_SET_VISIBILITY:
    return updateVisibility(state, action.visibility);
  case NOTIFICATIONS_EXPAND_REQUEST:
  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
    return state.set('isLoading', true);
  case NOTIFICATIONS_DELETE_MARKED_FAIL:
  case NOTIFICATIONS_EXPAND_FAIL:
    return state.set('isLoading', false);
  case NOTIFICATIONS_FILTER_SET:
    return state.set('items', ImmutableList()).set('hasMore', true);
  case NOTIFICATIONS_SCROLL_TOP:
    return updateTop(state, action.top);
  case NOTIFICATIONS_UPDATE:
    return normalizeNotification(state, action.notification);
  case NOTIFICATIONS_EXPAND_SUCCESS:
    return expandNormalizedNotifications(state, action.notifications, action.next);
  case ACCOUNT_BLOCK_SUCCESS:
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
  case NOTIFICATIONS_CLEAR:
    return state.set('items', ImmutableList()).set('hasMore', false);
  case TIMELINE_DELETE:
    return deleteByStatus(state, action.id);
  case TIMELINE_DISCONNECT:
    return action.timeline === 'home' ?
      state.update('items', items => items.first() ? items.unshift(null) : items) :
      state;

  case NOTIFICATION_MARK_FOR_DELETE:
    return markForDelete(state, action.id, action.yes);

  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
    return deleteMarkedNotifs(state).set('isLoading', false);

  case NOTIFICATIONS_ENTER_CLEARING_MODE:
    st = state.set('cleaningMode', action.yes);
    if (!action.yes) {
      return unmarkAllForDelete(st).set('markNewForDelete', false);
    } else {
      return st;
    }

  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
    st = state;
    if (action.yes === null) {
      // Toggle - this is a bit confusing, as it toggles the all-none mode
      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
    } else {
      st = st.set('markNewForDelete', action.yes);
    }
    return markAllForDelete(st, action.yes);

  default:
    return state;
  }
};
<commit_msg>Fix web UI not removing notifications after block (#10108)<commit_after>import {
  NOTIFICATIONS_MOUNT,
  NOTIFICATIONS_UNMOUNT,
  NOTIFICATIONS_SET_VISIBILITY,
  NOTIFICATIONS_UPDATE,
  NOTIFICATIONS_EXPAND_SUCCESS,
  NOTIFICATIONS_EXPAND_REQUEST,
  NOTIFICATIONS_EXPAND_FAIL,
  NOTIFICATIONS_FILTER_SET,
  NOTIFICATIONS_CLEAR,
  NOTIFICATIONS_SCROLL_TOP,
  NOTIFICATIONS_DELETE_MARKED_REQUEST,
  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
  NOTIFICATION_MARK_FOR_DELETE,
  NOTIFICATIONS_DELETE_MARKED_FAIL,
  NOTIFICATIONS_ENTER_CLEARING_MODE,
  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
} from 'flavours/glitch/actions/notifications';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap({
  items: ImmutableList(),
  hasMore: true,
  top: true,
  mounted: 0,
  unread: 0,
  lastReadId: '0',
  isLoading: false,
  cleaningMode: false,
  isTabVisible: true,
  // notification removal mark of new notifs loaded whilst cleaningMode is true.
  markNewForDelete: false,
});

const notificationToMap = (state, notification) => ImmutableMap({
  id: notification.id,
  type: notification.type,
  account: notification.account.id,
  markedForDelete: state.get('markNewForDelete'),
  status: notification.status ? notification.status.id : null,
});

const normalizeNotification = (state, notification) => {
  const top = !shouldCountUnreadNotifications(state);

  if (top) {
    state = state.set('lastReadId', notification.id);
  } else {
    state = state.update('unread', unread => unread + 1);
  }

  return state.update('items', list => {
    if (top && list.size > 40) {
      list = list.take(20);
    }

    return list.unshift(notificationToMap(state, notification));
  });
};

const expandNormalizedNotifications = (state, notifications, next) => {
  const top = !(shouldCountUnreadNotifications(state));
  const lastReadId = state.get('lastReadId');
  let items = ImmutableList();

  notifications.forEach((n, i) => {
    items = items.set(i, notificationToMap(state, n));
  });

  return state.withMutations(mutable => {
    if (!items.isEmpty()) {
      mutable.update('items', list => {
        const lastIndex = 1 + list.findLastIndex(
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
        );

        const firstIndex = 1 + list.take(lastIndex).findLastIndex(
          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
        );

        return list.take(firstIndex).concat(items, list.skip(lastIndex));
      });
    }

    if (top) {
      if (!items.isEmpty()) {
        mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id'));
      }
    } else {
      mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size);
    }

    if (!next) {
      mutable.set('hasMore', false);
    }

    mutable.set('isLoading', false);
  });
};

const filterNotifications = (state, relationship) => {
  return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
};

const clearUnread = (state) => {
  state = state.set('unread', 0);
  const lastNotification = state.get('items').find(item => item !== null);
  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
}

const updateTop = (state, top) => {
  state = state.set('top', top);

  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }

  return state.set('top', top);
};

const deleteByStatus = (state, statusId) => {
  const top = !(shouldCountUnreadNotifications(state));
  if (!top) {
    const lastReadId = state.get('lastReadId');
    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
    state = state.update('unread', unread => unread - deletedUnread.size);
  }
  return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
};

const markForDelete = (state, notificationId, yes) => {
  return state.update('items', list => list.map(item => {
    if(item.get('id') === notificationId) {
      return item.set('markedForDelete', yes);
    } else {
      return item;
    }
  }));
};

const markAllForDelete = (state, yes) => {
  return state.update('items', list => list.map(item => {
    if(yes !== null) {
      return item.set('markedForDelete', yes);
    } else {
      return item.set('markedForDelete', !item.get('markedForDelete'));
    }
  }));
};

const unmarkAllForDelete = (state) => {
  return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
};

const deleteMarkedNotifs = (state) => {
  return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
};

const updateMounted = (state) => {
  state = state.update('mounted', count => count + 1);
  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }
  return state;
};

const updateVisibility = (state, visibility) => {
  state = state.set('isTabVisible', visibility);
  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }
  return state;
};

const shouldCountUnreadNotifications = (state) => {
  return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0);
};

export default function notifications(state = initialState, action) {
  let st;

  switch(action.type) {
  case NOTIFICATIONS_MOUNT:
    return updateMounted(state);
  case NOTIFICATIONS_UNMOUNT:
    return state.update('mounted', count => count - 1);
  case NOTIFICATIONS_SET_VISIBILITY:
    return updateVisibility(state, action.visibility);
  case NOTIFICATIONS_EXPAND_REQUEST:
  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
    return state.set('isLoading', true);
  case NOTIFICATIONS_DELETE_MARKED_FAIL:
  case NOTIFICATIONS_EXPAND_FAIL:
    return state.set('isLoading', false);
  case NOTIFICATIONS_FILTER_SET:
    return state.set('items', ImmutableList()).set('hasMore', true);
  case NOTIFICATIONS_SCROLL_TOP:
    return updateTop(state, action.top);
  case NOTIFICATIONS_UPDATE:
    return normalizeNotification(state, action.notification);
  case NOTIFICATIONS_EXPAND_SUCCESS:
    return expandNormalizedNotifications(state, action.notifications, action.next);
  case ACCOUNT_BLOCK_SUCCESS:
    return filterNotifications(state, action.relationship);
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
  case NOTIFICATIONS_CLEAR:
    return state.set('items', ImmutableList()).set('hasMore', false);
  case TIMELINE_DELETE:
    return deleteByStatus(state, action.id);
  case TIMELINE_DISCONNECT:
    return action.timeline === 'home' ?
      state.update('items', items => items.first() ? items.unshift(null) : items) :
      state;

  case NOTIFICATION_MARK_FOR_DELETE:
    return markForDelete(state, action.id, action.yes);

  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
    return deleteMarkedNotifs(state).set('isLoading', false);

  case NOTIFICATIONS_ENTER_CLEARING_MODE:
    st = state.set('cleaningMode', action.yes);
    if (!action.yes) {
      return unmarkAllForDelete(st).set('markNewForDelete', false);
    } else {
      return st;
    }

  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
    st = state;
    if (action.yes === null) {
      // Toggle - this is a bit confusing, as it toggles the all-none mode
      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
    } else {
      st = st.set('markNewForDelete', action.yes);
    }
    return markAllForDelete(st, action.yes);

  default:
    return state;
  }
};
<|endoftext|>


<commit_before>// grunt-merge-data
// Copyright (c) 2013 Shinnosuke Watanabe
// Licensed under the MIT license.

module.exports = function (grunt) {
  'use strict';

  var path = require('path');

  function mergeObjects (destObj, srcObj) {
    for (var attrname in srcObj) {
      if (srcObj.hasOwnProperty(attrname)) {
        destObj[attrname] = srcObj[attrname];
      }
    }
  }

  function mergeFileData (sources) {
    var data = {};
    sources.filter(function (filePath) {
      // Warn on and remove invalid source files (if nonull was set)
      if (!grunt.file.exists(filePath)) {
        grunt.log.warn('Source file "' + filePath + '" not found.');
        return false;
      } else {
        return true;
      }

    }).forEach(function (filePath) {
      var basename = path.basename(filePath, path.extname(filePath));
      var ext = path.extname(filePath).toLowerCase();

      if(ext === '.json') {
        data[basename] = grunt.file.readJSON(filePath);
      } else if (ext === '.yml' || ext === '.yaml') {
        data[basename] = grunt.file.readYAML(filePath);
      }
    });

    return data;
  }

  var mergeDataTask = function () {
    // Merge task-specific and/or target-specific options with these defaults
    var options = this.options({
      data: null,
      space: null,
      asConfig: false
    });

    // Iterate over all specified src/dest file groups
    this.files.forEach(function (file) {

      var data = mergeFileData(file.src);

      if (options.data) {
        if (typeof options.data === 'function') {
          mergeObjects(data, options.data(data));
        } else {
          mergeObjects(data, options.data);
        }
      }

      if (typeof options.asConfig === 'string') {
        grunt.config(options.asConfig, data);

      } else if (options.asConfig === true) {
        var nameArgs = grunt.task.current.nameArgs;

        grunt.config(
          nameArgs.replace(':', '.') + '.context',
          data
        );
      }

      // Write the destination file if 'dest' is specified
      if (file.dest) {
        grunt.file.write(
          file.dest,
          JSON.stringify(data, null, options.space)
        );
        // Print a success message
        grunt.log.writeln('File "' + file.dest + '" created.');

      } else if (!options.asConfig) {
        //grunt.log.warn("Both destination and "' + filePath + '" not found.');
      }
    });
  };

  grunt.registerMultiTask(
    'merge_data',
    'Merge multiple data into a file or Grint config.',
    mergeDataTask
  );
};
<commit_msg>use grunt.util.kindOf for type check<commit_after>// grunt-merge-data
// Copyright (c) 2013 Shinnosuke Watanabe
// Licensed under the MIT license.

module.exports = function (grunt) {
  'use strict';

  var path = require('path');

  function mergeObjects (destObj, srcObj) {
    for (var attrname in srcObj) {
      if (srcObj.hasOwnProperty(attrname)) {
        destObj[attrname] = srcObj[attrname];
      }
    }
  }

  function mergeFileData (sources) {
    var data = {};
    sources.filter(function (filePath) {
      // Warn on and remove invalid source files (if nonull was set)
      if (!grunt.file.exists(filePath)) {
        grunt.log.warn('Source file "' + filePath + '" not found.');
        return false;
      } else {
        return true;
      }

    }).forEach(function (filePath) {
      var basename = path.basename(filePath, path.extname(filePath));
      var ext = path.extname(filePath).toLowerCase();

      if(ext === '.json') {
        data[basename] = grunt.file.readJSON(filePath);
      } else if (ext === '.yml' || ext === '.yaml') {
        data[basename] = grunt.file.readYAML(filePath);
      }
    });

    return data;
  }

  var mergeDataTask = function () {
    // Merge task-specific and/or target-specific options with these defaults
    var options = this.options({
      data: null,
      space: null,
      asConfig: false
    });

    // Iterate over all specified src/dest file groups
    this.files.forEach(function (file) {

      var data = mergeFileData(file.src);

      if (options.data) {
        if (grunt.util.kindOf(options.data) === 'function') {
          mergeObjects(data, options.data(data));
        } else {
          mergeObjects(data, options.data);
        }
      }

      if (grunt.util.kindOf(options.asConfig) === 'string') {
        grunt.config(options.asConfig, data);

      } else if (options.asConfig === true) {
        var nameArgs = grunt.task.current.nameArgs;

        grunt.config(
          nameArgs.replace(':', '.') + '.context',
          data
        );
      }

      // Write the destination file if 'dest' is specified
      if (file.dest) {
        grunt.file.write(
          file.dest,
          JSON.stringify(data, null, options.space)
        );
        // Print a success message
        grunt.log.writeln('File "' + file.dest + '" created.');

      } else if (!options.asConfig) {
        //grunt.log.warn("Both destination and "' + filePath + '" not found.');
      }
    });
  };

  grunt.registerMultiTask(
    'merge_data',
    'Merge multiple data into a file or Grint config.',
    mergeDataTask
  );
};
<|endoftext|>


<commit_before>import { PLAN_SERVICES, PLAN_TYPES, ADDON_NAMES } from 'proton-shared/lib/constants';
import { hasBit } from 'proton-shared/lib/helpers/bitset';
import { c, msgid } from 'ttag';
import { isEquivalent, pick } from 'proton-shared/lib/helpers/object';

const { PLAN, ADDON } = PLAN_TYPES;
const { MAIL, VPN } = PLAN_SERVICES;

const I18N = {
    included: c('Option').t`included`,
    address(value) {
        return c('Option').ngettext(msgid`1 address`, `${value} addresses`, value);
    },
    space(value) {
        return c('Option').ngettext(msgid`1 GB storage`, `${value} GB storage`, value);
    },
    domain(value) {
        return c('Option').ngettext(msgid`1 custom domain`, `${value} custom domains`, value);
    },
    member(value) {
        return c('Option').ngettext(msgid`1 user`, `${value} users`, value);
    },
    vpn(value) {
        return c('Option').ngettext(msgid`1 VPN connection`, `${value} VPN connections`, value);
    }
};

/**
 * Check if a plans map contains at least b plans map
 * @param {Object} a plans map
 * @param {Object} b plans map
 * @returns {Boolean}
 */
export const containsSamePlans = (a, b) => isEquivalent(pick(a, Object.keys(b)), b);

/**
 * Build plansMap from current subscription and user demand
 * @param {Object} plansMap user demand
 * Possible entries for plansMap
 * {}
 * { vpnplus: 1 }
 * { vpnbasic: 1 }
 * { plus: 1 }, { vpnplus: 1, plus: 1 }
 * { professional: 1 }, { vpnplus: 1, professional: 1 }
 * { visionary: 1 }
 * @param {Array} subscription.Plans
 * @returns {Object} plansMap
 */
export const mergePlansMap = (plansMap = {}, { Plans = [] }) => {
    // Free user subscribing
    if (!Plans.length) {
        return plansMap;
    }

    // Has an existing subscription

    // Visionary includes mail and vpn service with no addons
    if (plansMap.visionary) {
        return plansMap;
    }

    const currentPlansMap = toPlanNames(Plans);

    if (containsSamePlans(plansMap, { vpnplus: 1 }) || containsSamePlans(plansMap, { vpnbasic: 1 })) {
        return {
            ...plansMap,
            plus: currentPlansMap.plus,
            [ADDON_NAMES.DOMAIN]: currentPlansMap[ADDON_NAMES.DOMAIN],
            [ADDON_NAMES.ADDRESS]: currentPlansMap[ADDON_NAMES.ADDRESS],
            [ADDON_NAMES.SPACE]: currentPlansMap[ADDON_NAMES.SPACE]
        };
    }

    if (containsSamePlans(plansMap, { vpnplus: 1, plus: 1 })) {
        return {
            ...plansMap,
            [ADDON_NAMES.DOMAIN]: currentPlansMap[ADDON_NAMES.DOMAIN],
            [ADDON_NAMES.ADDRESS]: currentPlansMap[ADDON_NAMES.ADDRESS],
            [ADDON_NAMES.SPACE]: currentPlansMap[ADDON_NAMES.SPACE]
        };
    }

    if (containsSamePlans(plansMap, { vpnplus: 1, professional: 1 })) {
        return {
            ...plansMap,
            [ADDON_NAMES.DOMAIN]:
                currentPlansMap[ADDON_NAMES.DOMAIN] > 1 ? currentPlansMap[ADDON_NAMES.DOMAIN] : undefined, // pro starts with 2 custom domain
            [ADDON_NAMES.MEMBER]: currentPlansMap[ADDON_NAMES.MEMBER],
            [ADDON_NAMES.VPN]: currentPlansMap[ADDON_NAMES.VPN] // Only possible with vpnplus and professional
        };
    }

    return plansMap;
};

export const getTextOption = (type, value, index) => {
    return `${I18N[type](value)} ${index ? '' : `(${I18N.included})`}`.trim();
};

/**
 * Calculate total for a specific subscription configuration
 * @param {Object} param.plansMap
 * @param {Number} param.cycle
 * @param {Array} param.plans
 * @param {Number} param.services optional
 * @returns {Number} amount
 */
export const getSubTotal = ({ plansMap, cycle, plans, services }) => {
    return Object.entries(plansMap).reduce((acc, [planName, quantity]) => {
        if (quantity) {
            const { Pricing = {} } =
                plans.find(({ Name, Services }) => {
                    if (services) {
                        return Name === planName && hasBit(Services, services);
                    }
                    return Name === planName;
                }) || {};
            const amount = Pricing[cycle] || 0;
            return acc + quantity * amount;
        }
        return acc;
    }, 0);
};

/**
 * Convert subscription plans to PlanIDs format required by API requests
 * @param {Array} plans coming from Subscription API
 * @returns {Object}
 */
export const toPlanMap = (plans = [], key = 'ID') => {
    return plans.reduce((acc, plan) => {
        acc[plan[key]] = acc[plan[key]] || 0;
        acc[plan[key]] += 1;
        return acc;
    }, Object.create(null));
};

export const toPlanIDs = toPlanMap;
export const toPlanNames = (plans = []) => toPlanMap(plans, 'Name');

/**
 * Merge addon to addition parameters
 * @param {Number} oldAddon.Quantity
 * @param {Number} oldAddon.Amount
 * @param {Number} oldAddon.MaxDomains
 * @param {Number} oldAddon.MaxAddresses
 * @param {Number} oldAddon.MaxSpace
 * @param {Number} oldAddon.MaxMembers
 * @param {Number} oldAddon.MaxVPN
 * @param {Object} addon to merge with
 * @returns {Object} new addon
 */
const mergeAddons = (
    { Quantity = 0, Amount = 0, MaxDomains = 0, MaxAddresses = 0, MaxSpace = 0, MaxMembers = 0, MaxVPN = 0 } = {},
    addon
) => ({
    ...addon,
    MaxAddresses: MaxAddresses + addon.MaxAddresses,
    MaxSpace: MaxSpace + addon.MaxSpace,
    MaxMembers: MaxMembers + addon.MaxMembers,
    MaxVPN: MaxVPN + addon.MaxVPN,
    MaxDomains: MaxDomains + addon.MaxDomains,
    Quantity: Quantity + addon.Quantity,
    Amount: Amount + addon.Amount
});

/**
 * Format plans to returns essential structure
 * @param {Array} plans coming from Subscription API
 * @returns {Object} { mailPlan, vpnPlan, addressAddon, vpnAddon, domainAddon, memberAddon, spaceAddon }
 */
export const formatPlans = (plans = []) => {
    return plans.reduce((acc, plan) => {
        if (plan.Type === PLAN) {
            // visionary is a special case because it contains mail and vpn services
            // we consider it as a mail plan
            if (plan.Name === 'visionary') {
                acc.mailPlan = plan;
                return acc;
            }
            if (hasBit(plan.Services, MAIL)) {
                acc.mailPlan = plan;
            }
            if (hasBit(plan.Services, VPN)) {
                acc.vpnPlan = plan;
            }
            return acc;
        }

        if (plan.Type === ADDON) {
            if (plan.Name === ADDON_NAMES.DOMAIN) {
                acc.domainAddon = mergeAddons(acc.domainAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.MEMBER) {
                acc.memberAddon = mergeAddons(acc.memberAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.VPN) {
                acc.vpnAddon = mergeAddons(acc.vpnAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.ADDRESS) {
                acc.addressAddon = mergeAddons(acc.addressAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.SPACE) {
                acc.spaceAddon = mergeAddons(acc.spaceAddon, plan);
            }
            return acc;
        }

        return acc;
    }, {});
};

/**
 * Helper to find plans from Subscription.Plans or Plans
 * @param {Array} plans from Subscription.Plans or Plans
 * @param {String} params.planName examples: 'plus', 'vpnplus', 'visionary', ADDON_NAMES.ADDRESS, ADDON_NAMES.MEMBER
 * @param {String} params.id plan ID
 * @param {Number} params.type default: plan
 * @returns {Object} plan Object
 */
export const getPlan = (plans = [], { type = PLAN, name = '', id = '' }) => {
    if (!plans.length) {
        throw new Error('plans not defined');
    }

    if (!name && !id) {
        throw new Error('name or id not defined to get plan');
    }

    if (name) {
        return plans.find(({ Type, Name }) => Type === type && Name === name);
    }

    return plans.find(({ Type, ID }) => Type === type && ID === id);
};

/**
 * Alias to getPlan with Type = ADDON
 * @param {Array} plans
 * @param {Object} params
 */
export const getAddon = (plans, params) => getPlan(plans, { ...params, type: ADDON });

/**
 * Prepare params for /api/subscription/check request
 * @param {Array} param.plans
 * @param {Object} param.plansMap
 * @param {String} param.coupon
 * @param {String} param.currency
 * @param {Number} param.cycle
 * @returns {Object} parameters
 */
export const getCheckParams = ({
    plans = [],
    plansMap = {},
    coupon: CouponCode,
    currency: Currency,
    cycle: Cycle,
    ...rest
}) => {
    if (!plans.length) {
        throw new Error('plans not defined');
    }

    const PlanIDs = Object.entries(plansMap).reduce((acc, [planName, quantity]) => {
        if (quantity) {
            const { ID } = plans.find((plan) => plan.Name === planName);
            acc[ID] = quantity;
        }
        return acc;
    }, Object.create(null));

    return {
        PlanIDs,
        CouponCode,
        Currency,
        Cycle,
        ...rest
    };
};
<commit_msg>Review mergePlansMap<commit_after>import { PLAN_SERVICES, PLAN_TYPES, ADDON_NAMES } from 'proton-shared/lib/constants';
import { hasBit } from 'proton-shared/lib/helpers/bitset';
import { c, msgid } from 'ttag';
import { isEquivalent, pick } from 'proton-shared/lib/helpers/object';

const { PLAN, ADDON } = PLAN_TYPES;
const { MAIL, VPN } = PLAN_SERVICES;

const I18N = {
    included: c('Option').t`included`,
    address(value) {
        return c('Option').ngettext(msgid`1 address`, `${value} addresses`, value);
    },
    space(value) {
        return c('Option').ngettext(msgid`1 GB storage`, `${value} GB storage`, value);
    },
    domain(value) {
        return c('Option').ngettext(msgid`1 custom domain`, `${value} custom domains`, value);
    },
    member(value) {
        return c('Option').ngettext(msgid`1 user`, `${value} users`, value);
    },
    vpn(value) {
        return c('Option').ngettext(msgid`1 VPN connection`, `${value} VPN connections`, value);
    }
};

/**
 * Check if a plans map contains at least b plans map
 * @param {Object} a plans map
 * @param {Object} b plans map
 * @returns {Boolean}
 */
export const containsSamePlans = (a, b) => isEquivalent(pick(a, Object.keys(b)), b);

/**
 * Build plansMap from current subscription and user demand
 * @param {Object} plansMap user demand
 * Possible entries for plansMap
 * {}
 * { vpnplus: 1 }
 * { vpnbasic: 1 }
 * { plus: 1 }
 * { vpnplus: 1, plus: 1 }
 * { professional: 1 }
 * { vpnplus: 1, professional: 1 }
 * { visionary: 1 }
 * @param {Array} subscription.Plans
 * @returns {Object} plansMap
 */
export const mergePlansMap = (plansMap = {}, { Plans = [] }) => {
    // Free user subscribing
    if (!Plans.length) {
        return plansMap;
    }

    // Has an existing subscription

    // Visionary includes mail and vpn service with no addons
    if (plansMap.visionary) {
        return plansMap;
    }

    const currentPlansMap = toPlanNames(Plans);

    if (containsSamePlans(plansMap, { vpnplus: 1, plus: 1 })) {
        return {
            ...plansMap,
            [ADDON_NAMES.DOMAIN]: currentPlansMap[ADDON_NAMES.DOMAIN],
            [ADDON_NAMES.ADDRESS]: currentPlansMap[ADDON_NAMES.ADDRESS],
            [ADDON_NAMES.SPACE]: currentPlansMap[ADDON_NAMES.SPACE]
        };
    }

    if (containsSamePlans(plansMap, { vpnplus: 1, professional: 1 })) {
        return {
            ...plansMap,
            [ADDON_NAMES.DOMAIN]:
                currentPlansMap[ADDON_NAMES.DOMAIN] > 1 ? currentPlansMap[ADDON_NAMES.DOMAIN] : undefined, // pro starts with 2 custom domain
            [ADDON_NAMES.MEMBER]: currentPlansMap[ADDON_NAMES.MEMBER],
            [ADDON_NAMES.VPN]: currentPlansMap[ADDON_NAMES.VPN] // Only possible with vpnplus and professional
        };
    }

    // Only concern ProtonVPN dashboard
    if (containsSamePlans(plansMap, { vpnplus: 1 }) || containsSamePlans(plansMap, { vpnbasic: 1 })) {
        return {
            ...plansMap,
            plus: currentPlansMap.plus,
            professional: currentPlansMap.professional,
            [ADDON_NAMES.DOMAIN]: currentPlansMap[ADDON_NAMES.DOMAIN],
            [ADDON_NAMES.ADDRESS]: currentPlansMap[ADDON_NAMES.ADDRESS],
            [ADDON_NAMES.SPACE]: currentPlansMap[ADDON_NAMES.SPACE],
            [ADDON_NAMES.MEMBER]: currentPlansMap[ADDON_NAMES.MEMBER],
            [ADDON_NAMES.VPN]: currentPlansMap[ADDON_NAMES.VPN]
        };
    }

    return plansMap;
};

export const getTextOption = (type, value, index) => {
    return `${I18N[type](value)} ${index ? '' : `(${I18N.included})`}`.trim();
};

/**
 * Calculate total for a specific subscription configuration
 * @param {Object} param.plansMap
 * @param {Number} param.cycle
 * @param {Array} param.plans
 * @param {Number} param.services optional
 * @returns {Number} amount
 */
export const getSubTotal = ({ plansMap, cycle, plans, services }) => {
    return Object.entries(plansMap).reduce((acc, [planName, quantity]) => {
        if (quantity) {
            const { Pricing = {} } =
                plans.find(({ Name, Services }) => {
                    if (services) {
                        return Name === planName && hasBit(Services, services);
                    }
                    return Name === planName;
                }) || {};
            const amount = Pricing[cycle] || 0;
            return acc + quantity * amount;
        }
        return acc;
    }, 0);
};

/**
 * Convert subscription plans to PlanIDs format required by API requests
 * @param {Array} plans coming from Subscription API
 * @returns {Object}
 */
export const toPlanMap = (plans = [], key = 'ID') => {
    return plans.reduce((acc, plan) => {
        acc[plan[key]] = acc[plan[key]] || 0;
        acc[plan[key]] += 1;
        return acc;
    }, Object.create(null));
};

export const toPlanIDs = toPlanMap;
export const toPlanNames = (plans = []) => toPlanMap(plans, 'Name');

/**
 * Merge addon to addition parameters
 * @param {Number} oldAddon.Quantity
 * @param {Number} oldAddon.Amount
 * @param {Number} oldAddon.MaxDomains
 * @param {Number} oldAddon.MaxAddresses
 * @param {Number} oldAddon.MaxSpace
 * @param {Number} oldAddon.MaxMembers
 * @param {Number} oldAddon.MaxVPN
 * @param {Object} addon to merge with
 * @returns {Object} new addon
 */
const mergeAddons = (
    { Quantity = 0, Amount = 0, MaxDomains = 0, MaxAddresses = 0, MaxSpace = 0, MaxMembers = 0, MaxVPN = 0 } = {},
    addon
) => ({
    ...addon,
    MaxAddresses: MaxAddresses + addon.MaxAddresses,
    MaxSpace: MaxSpace + addon.MaxSpace,
    MaxMembers: MaxMembers + addon.MaxMembers,
    MaxVPN: MaxVPN + addon.MaxVPN,
    MaxDomains: MaxDomains + addon.MaxDomains,
    Quantity: Quantity + addon.Quantity,
    Amount: Amount + addon.Amount
});

/**
 * Format plans to returns essential structure
 * @param {Array} plans coming from Subscription API
 * @returns {Object} { mailPlan, vpnPlan, addressAddon, vpnAddon, domainAddon, memberAddon, spaceAddon }
 */
export const formatPlans = (plans = []) => {
    return plans.reduce((acc, plan) => {
        if (plan.Type === PLAN) {
            // visionary is a special case because it contains mail and vpn services
            // we consider it as a mail plan
            if (plan.Name === 'visionary') {
                acc.mailPlan = plan;
                return acc;
            }
            if (hasBit(plan.Services, MAIL)) {
                acc.mailPlan = plan;
            }
            if (hasBit(plan.Services, VPN)) {
                acc.vpnPlan = plan;
            }
            return acc;
        }

        if (plan.Type === ADDON) {
            if (plan.Name === ADDON_NAMES.DOMAIN) {
                acc.domainAddon = mergeAddons(acc.domainAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.MEMBER) {
                acc.memberAddon = mergeAddons(acc.memberAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.VPN) {
                acc.vpnAddon = mergeAddons(acc.vpnAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.ADDRESS) {
                acc.addressAddon = mergeAddons(acc.addressAddon, plan);
            }
            if (plan.Name === ADDON_NAMES.SPACE) {
                acc.spaceAddon = mergeAddons(acc.spaceAddon, plan);
            }
            return acc;
        }

        return acc;
    }, {});
};

/**
 * Helper to find plans from Subscription.Plans or Plans
 * @param {Array} plans from Subscription.Plans or Plans
 * @param {String} params.planName examples: 'plus', 'vpnplus', 'visionary', ADDON_NAMES.ADDRESS, ADDON_NAMES.MEMBER
 * @param {String} params.id plan ID
 * @param {Number} params.type default: plan
 * @returns {Object} plan Object
 */
export const getPlan = (plans = [], { type = PLAN, name = '', id = '' }) => {
    if (!plans.length) {
        throw new Error('plans not defined');
    }

    if (!name && !id) {
        throw new Error('name or id not defined to get plan');
    }

    if (name) {
        return plans.find(({ Type, Name }) => Type === type && Name === name);
    }

    return plans.find(({ Type, ID }) => Type === type && ID === id);
};

/**
 * Alias to getPlan with Type = ADDON
 * @param {Array} plans
 * @param {Object} params
 */
export const getAddon = (plans, params) => getPlan(plans, { ...params, type: ADDON });

/**
 * Prepare params for /api/subscription/check request
 * @param {Array} param.plans
 * @param {Object} param.plansMap
 * @param {String} param.coupon
 * @param {String} param.currency
 * @param {Number} param.cycle
 * @returns {Object} parameters
 */
export const getCheckParams = ({
    plans = [],
    plansMap = {},
    coupon: CouponCode,
    currency: Currency,
    cycle: Cycle,
    ...rest
}) => {
    if (!plans.length) {
        throw new Error('plans not defined');
    }

    const PlanIDs = Object.entries(plansMap).reduce((acc, [planName, quantity]) => {
        if (quantity) {
            const { ID } = plans.find((plan) => plan.Name === planName);
            acc[ID] = quantity;
        }
        return acc;
    }, Object.create(null));

    return {
        PlanIDs,
        CouponCode,
        Currency,
        Cycle,
        ...rest
    };
};
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2016 Singular Studios (a.k.a Atom Tecnologia) - www.opensingular.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

(function (label, htmlContainer, hiddenInput, html) {

    var newWindow;

    window['openNewTabWithCKEditor${hash}'] = function () {
        if (typeof newWindow != "undefined") {
            newWindow.close();
        }
        newWindow = window.open("", label);
        appendFunctions(newWindow);
        newWindow.document.open();
        newWindow.document.write(html);
        newWindow.document.close();
        newWindow.document.title = label;
    };

    function appendFunctions(nw) {
        nw.createCKEditor = function () {
            nw.document.getElementById('ck-text-area').value = $('#' + htmlContainer).html();
            nw.CKEDITOR.replace("ck-text-area", {
                extraPlugins: 'finishAndClose,cancel',
                allowedContent: true,
                skin: 'office2013',
                language: 'pt-br',
                width: '215mm',
                savePlugin: {
                    onSave: function (data) {
                        $('#' + htmlContainer).html(data);
                        $('#' + hiddenInput).val(data);
                    }
                },
                toolbar: [
                    {name: 'document', items: ['FinishAndClose', 'Cancel', 'Preview', 'Print']},
                    {
                        name: 'clipboard',
                        items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
                    },
                    {name: 'editing', items: ['Find', 'Replace', '-', 'Scayt']},
                    {
                        name: 'basicstyles',
                        items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']
                    },
                    {
                        name: 'paragraph',
                        items: ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock']
                    },
                    {name: 'links', items: ['Link', 'Unlink']},
                    {name: 'insert', items: ['Table', 'HorizontalRule', 'SpecialChar', 'PageBreak']},
                    '/',
                    {name: 'styles', items: ['Styles', 'Format', 'FontSize']},
                    {name: 'colors', items: ['TextColor', 'BGColor']},
                    {name: 'tools', items: ['ShowBlocks']}
                ],
                on: {
                    'instanceReady': function (evt) {
                        nw.$('.cke_contents').height(nw.$('html').height() - nw.$('.cke_contents').offset().top - nw.$('.cke_bottom').height() - 20);
                    }
                }
            });
            nw.CKEDITOR.config.disableNativeSpellChecker = false;
        };
    }
})('${label}', '${htmlContainer}', '${hiddenInput}', '${html}');<commit_msg>[FORM-WICKET] - Correção do rich text em nova aba para o IE.<commit_after>/*
 * Copyright (C) 2016 Singular Studios (a.k.a Atom Tecnologia) - www.opensingular.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

(function (label, htmlContainer, hiddenInput, html) {

    var newWindow;

    window['openNewTabWithCKEditor${hash}'] = function () {
        if (typeof newWindow != "undefined") {
            newWindow.close();
        }
        newWindow = window.open("", label);
        newWindow.document.open();
        appendFunctions(newWindow);
        newWindow.document.write(html);
        newWindow.document.close();
        newWindow.document.title = label;
    };

    function appendFunctions(nw) {
        nw.createCKEditor = function () {
            nw.document.getElementById('ck-text-area').value = $('#' + htmlContainer).html();
            nw.CKEDITOR.replace("ck-text-area", {
                extraPlugins: 'finishAndClose,cancel',
                allowedContent: true,
                skin: 'office2013',
                language: 'pt-br',
                width: '215mm',
                savePlugin: {
                    onSave: function (data) {
                        $('#' + htmlContainer).html(data);
                        $('#' + hiddenInput).val(data);
                    }
                },
                toolbar: [
                    {name: 'document', items: ['FinishAndClose', 'Cancel', 'Preview', 'Print']},
                    {
                        name: 'clipboard',
                        items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
                    },
                    {name: 'editing', items: ['Find', 'Replace', '-', 'Scayt']},
                    {
                        name: 'basicstyles',
                        items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']
                    },
                    {
                        name: 'paragraph',
                        items: ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock']
                    },
                    {name: 'links', items: ['Link', 'Unlink']},
                    {name: 'insert', items: ['Table', 'HorizontalRule', 'SpecialChar', 'PageBreak']},
                    '/',
                    {name: 'styles', items: ['Styles', 'Format', 'FontSize']},
                    {name: 'colors', items: ['TextColor', 'BGColor']},
                    {name: 'tools', items: ['ShowBlocks']}
                ],
                on: {
                    'instanceReady': function (evt) {
                        nw.$('.cke_contents').height(nw.$('html').height() - nw.$('.cke_contents').offset().top - nw.$('.cke_bottom').height() - 20);
                    }
                }
            });
            nw.CKEDITOR.config.disableNativeSpellChecker = false;
        };
    }
})('${label}', '${htmlContainer}', '${hiddenInput}', '${html}');<|endoftext|>


<commit_before>
// 'parse' accepts a string that is expected to be the stdout stream of an
// osascript invocation. It reads the fist char of the string to determine
// the data-type of the result, and creates the appropriate type parser.
exports.parse = function(str) {
  //console.error(str);
  //var start = new Date;
  var rtn = parseFromFirstRemaining.call({
    value: str,
    index: 0
  });
  //console.error("Parse Time: " + (new Date - start) + "ms");
  return rtn;
}

var WHITESPACE = /\s+/;

function parseFromFirstRemaining() {
  var cur = this.value[this.index];
  switch(cur) {
    case '{':
      return ArrayParser.call(this);
      break;
    case '"':
      return StringParser.call(this);
      break;
    case 'a':
      if (this.value.substring(this.index, this.index+5) == 'alias') {
        return AliasParser.call(this);
      }
      break;
  }
  if (!isNaN(cur)) {
    return NumberParser.call(this);
  }
  return UndefinedParser.call(this);
}

// Parses an AppleScript "alias", which is really just a reference to a
// location on the filesystem, but formatted kinda weirdly.
function AliasParser() {
  this.index += 6;
  return "/Volumes/" + StringParser.call(this).replace(/:/g, "/");
}

// Parses an AppleScript Array. Which looks like {}, instead of JavaScript's [].
function ArrayParser() {
  var rtn = [],
    cur = this.value[++this.index];
  while (cur != '}') {
    rtn.push(parseFromFirstRemaining.call(this));
    if (this.value[this.index] == ',') this.index += 2;
    cur = this.value[this.index];
  }
  this.index++;
  return rtn;
}

// Parses an AppleScript Number into a native JavaScript Number instance.
function NumberParser() {
  return Number(UndefinedParser.call(this));
}

// Parses a standard AppleScript String. Which starts and ends with "" chars.
// The \ char is the escape character, so anything after that is a valid part
// of the resulting String.
function StringParser(str) {
  var end = this.index+1, cur = this.value[end++];
  // 'first' should be a " char for a string.
  while(cur != '"') {
    if (cur == '\\') end++;
    cur = this.value[end++];
  }
  var rtn = this.value.substring(this.index+1, end-1);
  this.index = end;
  return rtn;
}

// When the "parseFromFirstRemaining" function can't figure out the data type
// of "str", then the UndefinedParser is used. It crams everything it sees
// into a String, until it finds a ',' or a '}' or it reaches the end of data.
var END_OF_TOKEN = /}|,|\n/;
function UndefinedParser() {
  var end = this.index, cur = this.value[end++];
  while (!END_OF_TOKEN.test(cur)) {
    cur = this.value[end++];
  }
  var rtn = this.value.substring(this.index, end-1);
  this.index = end-1;
  return rtn;
}
<commit_msg>Fixed StringParser with escaping control characters.<commit_after>
// 'parse' accepts a string that is expected to be the stdout stream of an
// osascript invocation. It reads the fist char of the string to determine
// the data-type of the result, and creates the appropriate type parser.
exports.parse = function(str) {
  //console.error(str);
  //var start = new Date;
  var rtn = parseFromFirstRemaining.call({
    value: str,
    index: 0
  });
  //console.error("Parse Time: " + (new Date - start) + "ms");
  return rtn;
}

var WHITESPACE = /\s+/;

function parseFromFirstRemaining() {
  var cur = this.value[this.index];
  switch(cur) {
    case '{':
      return ArrayParser.call(this);
      break;
    case '"':
      return StringParser.call(this);
      break;
    case 'a':
      if (this.value.substring(this.index, this.index+5) == 'alias') {
        return AliasParser.call(this);
      }
      break;
  }
  if (!isNaN(cur)) {
    return NumberParser.call(this);
  }
  return UndefinedParser.call(this);
}

// Parses an AppleScript "alias", which is really just a reference to a
// location on the filesystem, but formatted kinda weirdly.
function AliasParser() {
  this.index += 6;
  return "/Volumes/" + StringParser.call(this).replace(/:/g, "/");
}

// Parses an AppleScript Array. Which looks like {}, instead of JavaScript's [].
function ArrayParser() {
  var rtn = [],
    cur = this.value[++this.index];
  while (cur != '}') {
    rtn.push(parseFromFirstRemaining.call(this));
    if (this.value[this.index] == ',') this.index += 2;
    cur = this.value[this.index];
  }
  this.index++;
  return rtn;
}

// Parses an AppleScript Number into a native JavaScript Number instance.
function NumberParser() {
  return Number(UndefinedParser.call(this));
}

// Parses a standard AppleScript String. Which starts and ends with "" chars.
// The \ char is the escape character, so anything after that is a valid part
// of the resulting String.
function StringParser(str) {
  var rtn = "",
    end = ++this.index,
    cur = this.value[end++];
  while(cur != '"') {
    if (cur == '\\') {
      rtn += this.value.substring(this.index, end-1);
      this.index = end++;
    }
    cur = this.value[end++];
  }
  rtn += this.value.substring(this.index, end-1);
  this.index = end;
  return rtn;
}

// When the "parseFromFirstRemaining" function can't figure out the data type
// of "str", then the UndefinedParser is used. It crams everything it sees
// into a String, until it finds a ',' or a '}' or it reaches the end of data.
var END_OF_TOKEN = /}|,|\n/;
function UndefinedParser() {
  var end = this.index, cur = this.value[end++];
  while (!END_OF_TOKEN.test(cur)) {
    cur = this.value[end++];
  }
  var rtn = this.value.substring(this.index, end-1);
  this.index = end-1;
  return rtn;
}
<|endoftext|>


<commit_before>/**
* @license lazyload v2.0.4 | github.com/vvo/lazyload#license
*/

(function(window, document){

  var lazyAttrs = ['data-src'];

  window['lazyload'] = lazyload;
  window['lzld'] = lzld();

  // Provide libs using getAttribute early to get the good src
  // and not the fake data-src
  replaceGetAttribute('Image');
  replaceGetAttribute('IFrame');

  function registerLazyAttr(attr) {
    if (indexOf.call(lazyAttrs, attr) === -1) {
      lazyAttrs.push(attr);
    }
  }

  function lzld() {
    var instance;

    return function(element) {
      if (instance === undefined) {
        instance = lazyload();
      }

      instance(element);
    }
  }

  function lazyload(opts) {

    if (arguments.length > 1) {
      return inViewport.apply(undefined, arguments);
    }

    opts = merge({
      'offset': 200,
      'src': 'data-src',
      'container': false
    }, opts || {});

    if (typeof opts['src'] === 'string') {
      registerLazyAttr(opts['src']);
    }

    var elts = [];

    function show(elt) {
      var src = findRealSrc(elt);

      if (src) {
        elt.src = src;
      }

      elt['data-lzled'] = true;
      elts[indexOf.call(elts, elt)] = null;
    }

    function findRealSrc(elt) {
      if (typeof opts['src'] === 'function') {
        return opts['src'](elt);
      } else {
        return elt.getAttribute(opts['src']);
      }
    }

    function register(elt) {
      // unsubscribe onload
      // needed by IE < 9, otherwise we get another onload when changing the src
      elt.onload = null;
      elt.removeAttribute('onload');

      if (indexOf.call(elts, elt) === -1) {
        inViewport(elt, opts, show);
      }
    }

    return register;
  }

  function replaceGetAttribute(elementName) {
    var fullname = 'HTML' + elementName + 'Element';
    if (fullname in window === false) {
      return;
    }

    var original = window[fullname].prototype.getAttribute;
    window[fullname].prototype.getAttribute = function(name) {
      if(name === 'src') {
        var realSrc;
        for (var i = 0, max = lazyAttrs.length; i < max; i++) {
          if (realSrc = original.call(this, lazyAttrs[i])) {
            break;
          }
        }

        return realSrc || original.call(this, name);
      } else {
        // our own lazyloader will go through theses lines
        // because we use getAttribute(opts.src)
        return original.call(this, name);
      }
    }
  }

  function merge(defaults, opts) {
    for (var name in defaults) {
      if (opts[name] === undefined) {
        opts[name] = defaults[name];
      }
    }

    return opts;
  }

  // http://webreflection.blogspot.fr/2011/06/partial-polyfills.html
  function indexOf(value) {
      for (var i = this.length; i-- && this[i] !== value;);
      return i;
  }

}(window, document))
<commit_msg>always create a lazyload instance<commit_after>/**
* @license lazyload v2.0.4 | github.com/vvo/lazyload#license
*/

(function(window, document){

  var lazyAttrs = ['data-src'];

  window['lazyload'] = lazyload;
  window['lzld'] = lazyload();

  // Provide libs using getAttribute early to get the good src
  // and not the fake data-src
  replaceGetAttribute('Image');
  replaceGetAttribute('IFrame');

  function registerLazyAttr(attr) {
    if (indexOf.call(lazyAttrs, attr) === -1) {
      lazyAttrs.push(attr);
    }
  }

  function lazyload(opts) {

    if (arguments.length > 1) {
      return inViewport.apply(undefined, arguments);
    }

    opts = merge({
      'offset': 200,
      'src': 'data-src',
      'container': false
    }, opts || {});

    if (typeof opts['src'] === 'string') {
      registerLazyAttr(opts['src']);
    }

    var elts = [];

    function show(elt) {
      var src = findRealSrc(elt);

      if (src) {
        elt.src = src;
      }

      elt['data-lzled'] = true;
      elts[indexOf.call(elts, elt)] = null;
    }

    function findRealSrc(elt) {
      if (typeof opts['src'] === 'function') {
        return opts['src'](elt);
      } else {
        return elt.getAttribute(opts['src']);
      }
    }

    function register(elt) {
      // unsubscribe onload
      // needed by IE < 9, otherwise we get another onload when changing the src
      elt.onload = null;
      elt.removeAttribute('onload');

      if (indexOf.call(elts, elt) === -1) {
        inViewport(elt, opts, show);
      }
    }

    return register;
  }

  function replaceGetAttribute(elementName) {
    var fullname = 'HTML' + elementName + 'Element';
    if (fullname in window === false) {
      return;
    }

    var original = window[fullname].prototype.getAttribute;
    window[fullname].prototype.getAttribute = function(name) {
      if(name === 'src') {
        var realSrc;
        for (var i = 0, max = lazyAttrs.length; i < max; i++) {
          if (realSrc = original.call(this, lazyAttrs[i])) {
            break;
          }
        }

        return realSrc || original.call(this, name);
      } else {
        // our own lazyloader will go through theses lines
        // because we use getAttribute(opts.src)
        return original.call(this, name);
      }
    }
  }

  function merge(defaults, opts) {
    for (var name in defaults) {
      if (opts[name] === undefined) {
        opts[name] = defaults[name];
      }
    }

    return opts;
  }

  // http://webreflection.blogspot.fr/2011/06/partial-polyfills.html
  function indexOf(value) {
      for (var i = this.length; i-- && this[i] !== value;);
      return i;
  }

}(window, document))
<|endoftext|>


<commit_before>import Linter from 'linter';
import JSONParser from 'parsers/json';

import * as messages from 'messages';
import { singleLineString } from 'utils';
import { validManifestJSON } from '../helpers';

describe('JSONParser', function() {

  it('should show a message if bad JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var jsonParser = new JSONParser('blah', addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors.length, 1);
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
    assert.include(
      errors[0].description,
      'SyntaxError: Unexpected token b in JSON at position 0');
  });

});

describe('JSONParser duplicate keys', function() {

  it('should error if duplicate keys are found in a JSON file', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = ['{',
      '"description": "Very good music.",',
      '"manifest_version": 2,',
      '"name": "Prince",',
      '"version": "0.0.1",',
      '"name": "The Artist Formerly Known As Prince",',
      '"applications": {',
          '"gecko": {',
              '"id": "@webextension-guid"',
          '}',
      '}',
    '}'].join('\n');

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.equal(errors[0].line, 6);
    assert.include(errors[0].description, 'Duplicate key: name found');
  });

  it('should report all dupes if multiple duplicate keys are found', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = singleLineString`{
      "description": "Very good music.",
      "manifest_version": 2,
      "name": "Prince",
      "name": "Male Symbol",
      "version": "0.0.1",
      "version": "0.0.2",
      "name": "The Artist Formerly Known As Prince",
      "applications": {
          "gecko": {
              "id": "@webextension-guid"
          }
      }
    }`;

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 3);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    // We expect the duplicate error messages to be in the order of the
    // dupliate keys in the manifest.
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.include(errors[0].description, 'Duplicate key: name found');
    assert.include(errors[1].description, 'Duplicate key: version found');
    assert.include(errors[2].description, 'Duplicate key: name found');
  });

  it('should not expose other RJSON errors', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = ['{',
      '"description": "Very good music.",',
      '"manifest_version": 2,',
      '"name": "Prince",',
      '"version": "0.0.1",',
      '"applications": {',
          '"gecko": {',
              '"id": "@webextension-guid"',
          '}',
      '}',
    '}'].join('\n');

    var fakeRJSON = { parse: () => {} };
    var parseStub = sinon.stub(fakeRJSON, 'parse', () => {
      throw {
        warnings: [
          {
            line: 1,
            message: 'Duplicate key: not actually found but this is a test',
          },
          {
            line: 1,
            message: 'DifferentError: Who cares',
          },
        ],
      };
    });

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse(fakeRJSON);

    assert.equal(jsonParser.isValid, false);
    assert.ok(parseStub.called);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.equal(errors[0].line, 1);
    assert.include(errors[0].description,
                   'Duplicate key: not actually found but this is a test');
  });

  it('should not be invalid if unknown RJSON errors', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = '{}';

    var fakeRJSON = { parse: () => {} };
    var parseStub = sinon.stub(fakeRJSON, 'parse', () => {
      throw {
        warnings: [
          {
            line: 1,
            message: 'DifferentError: Who cares',
          },
        ],
      };
    });

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse(fakeRJSON);

    // If RJSON throws an error we don't recognise, we should ignore it and
    // continue on like the JSON is valid. Regular parsing errors will be
    // picked up earlier in the process.
    assert.equal(jsonParser.isValid, true);
    assert.ok(parseStub.called);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 0);
  });
});

describe('JSONParser with comments', function() {

  it('parses JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `// I am a JSON comment, sigh\n${validManifestJSON()}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, true);
  });

  // Chrome will accept multiline /* */ comments, but Firefox will not and
  // the Web Extension spec does not allow them. So we will error on them.
  it('does not parse JSON with a multiline comment', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `/* I am a JSON comment, sigh*/\n${validManifestJSON()}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    // There should not be another error; a file with block-level comments
    // will throw that specific error and not a parse error.
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_BLOCK_COMMENTS.code);
    assert.equal(errors[0].message, messages.JSON_BLOCK_COMMENTS.message);
  });

  it('parses the example from Chrome developer docs', () => {
    var addonLinter = new Linter({_: ['bar']});
    // Example from https://developer.chrome.com/extensions/manifest
    var json = [
      '{',
      '// Required',
      '"manifest_version": 2,',
      '"name": "My Extension",',
      '// Make the hell sure to use semvar.org if increasing this',
      '"version": "0.0.1"',
      '}',
    ].join('\n');
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, true);
    assert.notInclude(jsonParser._jsonString, 'semvar.org');
  });

  it('returns the correct error for malformed JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `{"something": true,\n// I am a JSON comment, sigh\nblah}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
  });

  it("doesn't evaluate JS code in comments", () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = '// eval("");\n{"something": true}\nvar bla = "foo";';
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
    assert.notInclude(jsonParser._jsonString, 'var bla');
    assert.notInclude(jsonParser._jsonString, 'eval');
  });

  it("doesn't evaluate JS code even though esprima is used", () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = [
      '{',
      '// Required',
      '"manifest_version": 2,',
      '"name": "My Extension",',
      '// Make the hell sure to use semvar.org if increasing this',
      '"version": eval("alert(\'uh-oh\')")',
      '}',
    ].join('\n');
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
  });
});
<commit_msg>More test fixes<commit_after>import Linter from 'linter';
import JSONParser from 'parsers/json';

import * as messages from 'messages';
import { singleLineString } from 'utils';
import { validManifestJSON } from '../helpers';

describe('JSONParser', function() {

  it('should show a message if bad JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var jsonParser = new JSONParser('blah', addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors.length, 1);
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
    assert.equal(
      errors[0].description,
      'Unexpected token b in JSON at position 0');
  });

});

describe('JSONParser duplicate keys', function() {

  it('should error if duplicate keys are found in a JSON file', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = ['{',
      '"description": "Very good music.",',
      '"manifest_version": 2,',
      '"name": "Prince",',
      '"version": "0.0.1",',
      '"name": "The Artist Formerly Known As Prince",',
      '"applications": {',
          '"gecko": {',
              '"id": "@webextension-guid"',
          '}',
      '}',
    '}'].join('\n');

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.equal(errors[0].line, 6);
    assert.include(errors[0].description, 'Duplicate key: name found');
  });

  it('should report all dupes if multiple duplicate keys are found', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = singleLineString`{
      "description": "Very good music.",
      "manifest_version": 2,
      "name": "Prince",
      "name": "Male Symbol",
      "version": "0.0.1",
      "version": "0.0.2",
      "name": "The Artist Formerly Known As Prince",
      "applications": {
          "gecko": {
              "id": "@webextension-guid"
          }
      }
    }`;

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 3);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    // We expect the duplicate error messages to be in the order of the
    // dupliate keys in the manifest.
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.include(errors[0].description, 'Duplicate key: name found');
    assert.include(errors[1].description, 'Duplicate key: version found');
    assert.include(errors[2].description, 'Duplicate key: name found');
  });

  it('should not expose other RJSON errors', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = ['{',
      '"description": "Very good music.",',
      '"manifest_version": 2,',
      '"name": "Prince",',
      '"version": "0.0.1",',
      '"applications": {',
          '"gecko": {',
              '"id": "@webextension-guid"',
          '}',
      '}',
    '}'].join('\n');

    var fakeRJSON = { parse: () => {} };
    var parseStub = sinon.stub(fakeRJSON, 'parse', () => {
      throw {
        warnings: [
          {
            line: 1,
            message: 'Duplicate key: not actually found but this is a test',
          },
          {
            line: 1,
            message: 'DifferentError: Who cares',
          },
        ],
      };
    });

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse(fakeRJSON);

    assert.equal(jsonParser.isValid, false);
    assert.ok(parseStub.called);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_DUPLICATE_KEY.code);
    assert.include(errors[0].message, 'Duplicate keys are not allowed');
    assert.equal(errors[0].line, 1);
    assert.include(errors[0].description,
                   'Duplicate key: not actually found but this is a test');
  });

  it('should not be invalid if unknown RJSON errors', () => {
    var addonLinter = new Linter({_: ['bar']});
    // We aren't using singleLineString here so we can test the line number
    // reporting.
    var json = '{}';

    var fakeRJSON = { parse: () => {} };
    var parseStub = sinon.stub(fakeRJSON, 'parse', () => {
      throw {
        warnings: [
          {
            line: 1,
            message: 'DifferentError: Who cares',
          },
        ],
      };
    });

    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse(fakeRJSON);

    // If RJSON throws an error we don't recognise, we should ignore it and
    // continue on like the JSON is valid. Regular parsing errors will be
    // picked up earlier in the process.
    assert.equal(jsonParser.isValid, true);
    assert.ok(parseStub.called);
    var errors = addonLinter.collector.errors;
    assert.lengthOf(errors, 0);
  });
});

describe('JSONParser with comments', function() {

  it('parses JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `// I am a JSON comment, sigh\n${validManifestJSON()}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, true);
  });

  // Chrome will accept multiline /* */ comments, but Firefox will not and
  // the Web Extension spec does not allow them. So we will error on them.
  it('does not parse JSON with a multiline comment', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `/* I am a JSON comment, sigh*/\n${validManifestJSON()}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    // There should not be another error; a file with block-level comments
    // will throw that specific error and not a parse error.
    assert.lengthOf(errors, 1);
    assert.equal(errors[0].code, messages.JSON_BLOCK_COMMENTS.code);
    assert.equal(errors[0].message, messages.JSON_BLOCK_COMMENTS.message);
  });

  it('parses the example from Chrome developer docs', () => {
    var addonLinter = new Linter({_: ['bar']});
    // Example from https://developer.chrome.com/extensions/manifest
    var json = [
      '{',
      '// Required',
      '"manifest_version": 2,',
      '"name": "My Extension",',
      '// Make the hell sure to use semvar.org if increasing this',
      '"version": "0.0.1"',
      '}',
    ].join('\n');
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, true);
    assert.notInclude(jsonParser._jsonString, 'semvar.org');
  });

  it('returns the correct error for malformed JSON', () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = `{"something": true,\n// I am a JSON comment, sigh\nblah}`;
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
  });

  it("doesn't evaluate JS code in comments", () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = '// eval("");\n{"something": true}\nvar bla = "foo";';
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
    assert.notInclude(jsonParser._jsonString, 'var bla');
    assert.notInclude(jsonParser._jsonString, 'eval');
  });

  it("doesn't evaluate JS code even though esprima is used", () => {
    var addonLinter = new Linter({_: ['bar']});
    var json = [
      '{',
      '// Required',
      '"manifest_version": 2,',
      '"name": "My Extension",',
      '// Make the hell sure to use semvar.org if increasing this',
      '"version": eval("alert(\'uh-oh\')")',
      '}',
    ].join('\n');
    var jsonParser = new JSONParser(json, addonLinter.collector);
    jsonParser.parse();

    assert.equal(jsonParser.isValid, false);
    var errors = addonLinter.collector.errors;
    assert.equal(errors[0].code, messages.JSON_INVALID.code);
    assert.include(errors[0].message, 'Your JSON is not valid.');
  });
});
<|endoftext|>


<commit_before>/**
 * Created by jiangyi on 16/7/1.
 */

import Vue from 'vue'
import Accordion from '../../Accordion'

const {Panel} = Accordion

describe('Accordion组件单元测试', () => {
  let vm = new Vue({
    data () {
      return {
        oneAtATime: false
      }
    },
    components: {
      Accordion,
      Panel
    },
    replace: false,
    template: `
      <accordion :one-at-a-time="oneAtATime">
        <panel :is-open="true">

Panel #1

          内容一
        </panel>
        <panel>

Panel #2

          内容二
        </panel>
        <panel>

Panel #3

          内容三
        </panel>
        <panel>

Panel #4

          内容四
        </panel>
      </accordion>
    `
  }).$mount()

  it('基本功能渲染', () => {
    expect(vm.$el.querySelectorAll('.atui-panel-group').length).to.be.equal(1)
    expect(vm.$el.querySelectorAll('.atui-panel').length).to.be.equal(4)
  })
})

<commit_msg>unit test<commit_after>/**
 * Created by jiangyi on 16/7/1.
 */

import Vue from 'vue'
import Accordion from '../../Accordion'

const {Panel} = Accordion

describe('Accordion组件单元测试', () => {
  let vm = new Vue({
    data () {
      return {
        oneAtATime: false
      }
    },
    components: {
      Accordion,
      Panel
    },
    replace: false,
    template: `
      <accordion :one-at-a-time="oneAtATime">
        <panel :is-open="true">

Panel #1

          内容一
        </panel>
        <panel>

Panel #2

          内容二
        </panel>
        <panel>

Panel #3

          内容三
        </panel>
        <panel>

Panel #4

          内容四
        </panel>
      </accordion>
    `
  }).$mount()

  it('基本功能渲染', () => {
    expect(vm.$el.querySelectorAll('.atui-panel-group').length).to.be.equal(1)
    expect(vm.$el.querySelectorAll('.atui-panel').length).to.be.equal(4)
  })

  it('test目录', () => {
    expect(1).to.be.equal(1)
  })
})

<|endoftext|>