<commit_before>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import { isEqual } from 'lodash'
import React, { Component } from 'react'
import { Button, ButtonGroup, FormControl, FormGroup, InputGroup } from 'react-bootstrap'

import type {
  Substitution
} from '../../../types'

type Props = {
  activeEditingIndex: number,
  index: number,
  onBeginEdit?: number => void,
  onChange: (Substitution, number) => void,
  onEndEdit: number => void,
  onRemove?: number => void,
  substitution: Substitution
}

// (Encoded) test string to be included to URL to regex test tool (regexr).
const STRING_FOR_REGEXR_TEST = encodeURIComponent('Route 10 to Bay 1 @ Train Station (10th+West)')

// This style is needed otherwise the button group renders not as one row.
const buttonGroupStyle = { width: '60px' }

/**
 * Extract just the subset of fields to build the state variable.
 */
function extractStateFields (props: Props): Substitution {
  const { description, normalizeSpace, pattern, replacement } = props.substitution
  return {
    description: description || '',
    normalizeSpace,
    pattern,
    replacement
  }
}

/**
 * @returns true if the given substitution pattern is null or empty, false otherwise.
 */
export function isSubstitutionInvalid (substitution: Substitution) {
  return !substitution.pattern || substitution.pattern === ''
}

/**
 * Renders and lets user edit a substitution.
 */
export default class SubstitutionRow extends Component<Props, Substitution> {
  constructor (props: Props) {
    super(props)
    this.state = extractStateFields(props)
  }

  componentDidUpdate (prevProps: Props) {
    const { index, substitution } = this.props

    // If a new substition is associated with this component, reset its state.
    if (prevProps.index !== index || !isEqual(prevProps.substitution, substitution)) {
      this.setState(extractStateFields(this.props))
    }
  }

  _isEditing = (): boolean => {
    const { activeEditingIndex, index } = this.props
    return index === activeEditingIndex
  }

  _onChangePattern = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ pattern: evt.target.value })
  }

  _onChangeReplacement = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ replacement: evt.target.value })
  }

  _onEditNormalizeSpace = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    if (this._isEditing()) {
      this.setState({ normalizeSpace: evt.target.checked })
    }
  }

  _onChangeDescription = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ description: evt.target.value })
  }

  _onClickEdit = () => {
    // Don't start editing on field click if another row is being edited.
    const { activeEditingIndex, index, onBeginEdit } = this.props
    if (activeEditingIndex === -1 && onBeginEdit) {
      onBeginEdit(index)
    }
  }

  _stopEditing = (index: number) => {
    this.props.onEndEdit(this.props.index)
  }

  _onClickDiscard = () => {
    this.setState(extractStateFields(this.props))
    this._stopEditing(this.props.index)
  }

  _onClickSave = () => {
    // Trigger the onChange event if the substitution fields were changed.
    const { index, onChange, substitution } = this.props
    if (!isEqual(this.state, substitution)) {
      onChange(this.state, index)
    }
    this._stopEditing(index)
  }

  _onKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
    // Handle Enter and Esc keys on the text controls.
    switch (e.keyCode) {
      case 13: // [Enter]
        e.preventDefault()
        if (this._isEditing()) {
          this._onClickSave()
        }
        break
      case 27: // [Esc]
        e.preventDefault()
        return this._onClickDiscard()
      default:
        break
    }
  }

  _onRemove = () => {
    const { index, onRemove } = this.props
    if (onRemove) {
      onRemove(index)
    }
  }

  render () {
    const { activeEditingIndex } = this.props
    const { description, normalizeSpace, pattern, replacement } = this.state
    const regexTestLink = `https://regexr.com/?expression=${encodeURIComponent(pattern)}&text=${STRING_FOR_REGEXR_TEST}`
    const isEditing = this._isEditing()
    const allowEdit = activeEditingIndex === -1
    const isEditingInvalid = isSubstitutionInvalid(this.state)

    // Construct CSS class for row
    const editingClassName = isEditing ? '' : 'inactive'
    const allowEditClassName = allowEdit ? 'allow-edit' : ''
    const rowClassName = `substitution-row ${editingClassName} ${allowEditClassName}`

    return (

          <FormGroup style={{ margin: 0 }} validationState={isEditingInvalid ? 'error' : null}>
            <InputGroup>
              <FormControl
                // eslint-disable-next-line jsx-a11y/no-autofocus
                autoFocus
                onChange={this._onChangePattern}
                onClick={this._onClickEdit}
                onKeyDown={this._onKeyDown}
                placeholder='Text (regex) to find'
                readOnly={!isEditing}
                title={isEditingInvalid ? 'Substitution search pattern cannot be empty' : null}
                value={pattern}
              />
              <InputGroup.Addon>

                  <Icon type='flask' />

              </InputGroup.Addon>
            </InputGroup>
          </FormGroup>

          <InputGroup>
            <FormControl
              onChange={this._onChangeReplacement}
              onClick={this._onClickEdit}
              onKeyDown={this._onKeyDown}
              placeholder='<blank>'
              readOnly={!isEditing}
              value={replacement}
            />
            <InputGroup.Addon style={{ padding: '0 6px' }}>
              <input
                aria-label='Normalize space'
                checked={normalizeSpace}
                onChange={this._onEditNormalizeSpace}
                onClick={this._onClickEdit}
                style={{ padding: '0 6px' }}
                type='checkbox'
              />
            </InputGroup.Addon>
          </InputGroup>

          <FormControl
            onChange={this._onChangeDescription}
            onClick={this._onClickEdit}
            onKeyDown={this._onKeyDown}
            placeholder='Optional description'
            readOnly={!isEditing}
            value={description}
          />

          {isEditing
            ? (
              // Show save/discard changes buttons if this row is being edited.
              <ButtonGroup style={buttonGroupStyle}>
                <Button
                  bsSize='xsmall'
                  disabled={isEditingInvalid}
                  onClick={this._onClickSave}
                  title={isEditingInvalid ? 'Unable to save, there are errors in this row.' : 'Save changes in this row'}
                >
                  <Icon type='check' />
                </Button>
                <Button
                  bsSize='xsmall'
                  onClick={this._onClickDiscard}
                  title='Discard changes in this row'
                >
                  <Icon type='remove' />
                </Button>
              </ButtonGroup>
            )
            : allowEdit && (
              // Show edit/delete if no other row is being edited.
              <ButtonGroup style={buttonGroupStyle}>
                <Button
                  bsSize='xsmall'
                  onClick={this._onClickEdit}
                  title='Edit this substitution'
                >
                  <Icon type='pencil' />
                </Button>
                <Button
                  bsSize='xsmall'
                  onClick={this._onRemove}
                  title='Remove this substitution'
                >
                  <Icon type='trash' />
                </Button>
              </ButtonGroup>
            )
          }

    )
  }
}
<commit_msg>refactor(SubstitutionRow): Add comment<commit_after>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import { isEqual } from 'lodash'
import React, { Component } from 'react'
import { Button, ButtonGroup, FormControl, FormGroup, InputGroup } from 'react-bootstrap'

import type {
  Substitution
} from '../../../types'

type Props = {
  activeEditingIndex: number,
  index: number,
  onBeginEdit?: number => void,
  onChange: (Substitution, number) => void,
  onEndEdit: number => void,
  onRemove?: number => void,
  substitution: Substitution
}

// (Encoded) test string to be included to URL to regex test tool (regexr).
const STRING_FOR_REGEXR_TEST = encodeURIComponent('Route 10 to Bay 1 @ Train Station (10th+West)')

// This style is needed otherwise the button group renders not as one row.
const buttonGroupStyle = { width: '60px' }

/**
 * Extract just the subset of fields to build the state variable.
 */
function extractStateFields (props: Props): Substitution {
  const { description, normalizeSpace, pattern, replacement } = props.substitution
  return {
    description: description || '',
    normalizeSpace,
    pattern,
    replacement
  }
}

/**
 * @returns true if the given substitution pattern is null or empty, false otherwise.
 */
export function isSubstitutionInvalid (substitution: Substitution) {
  return !substitution.pattern || substitution.pattern === ''
}

/**
 * Renders and lets user edit a substitution.
 */
export default class SubstitutionRow extends Component<Props, Substitution> {
  constructor (props: Props) {
    super(props)
    this.state = extractStateFields(props)
  }

  componentDidUpdate (prevProps: Props) {
    const { index, substitution } = this.props

    // If a new substition is associated with this component, reset its state.
    if (prevProps.index !== index || !isEqual(prevProps.substitution, substitution)) {
      this.setState(extractStateFields(this.props))
    }
  }

  _isEditing = (): boolean => {
    const { activeEditingIndex, index } = this.props
    return index === activeEditingIndex
  }

  _onChangePattern = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ pattern: evt.target.value })
  }

  _onChangeReplacement = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ replacement: evt.target.value })
  }

  _onEditNormalizeSpace = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    if (this._isEditing()) {
      this.setState({ normalizeSpace: evt.target.checked })
    }
  }

  _onChangeDescription = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({ description: evt.target.value })
  }

  _onClickEdit = () => {
    // Don't start editing on field click if another row is being edited.
    const { activeEditingIndex, index, onBeginEdit } = this.props
    if (activeEditingIndex === -1 && onBeginEdit) {
      onBeginEdit(index)
    }
  }

  _stopEditing = (index: number) => {
    this.props.onEndEdit(this.props.index)
  }

  _onClickDiscard = () => {
    this.setState(extractStateFields(this.props))
    this._stopEditing(this.props.index)
  }

  _onClickSave = () => {
    // Trigger the onChange event if the substitution fields were changed.
    const { index, onChange, substitution } = this.props
    if (!isEqual(this.state, substitution)) {
      onChange(this.state, index)
    }
    this._stopEditing(index)
  }

  _onKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
    // Handle Enter and Esc keys on the text controls.
    switch (e.keyCode) {
      case 13: // [Enter]
        e.preventDefault()
        if (this._isEditing()) {
          this._onClickSave()
        }
        break
      case 27: // [Esc]
        e.preventDefault()
        return this._onClickDiscard()
      default:
        break
    }
  }

  _onRemove = () => {
    const { index, onRemove } = this.props
    if (onRemove) {
      onRemove(index)
    }
  }

  render () {
    const { activeEditingIndex } = this.props
    const { description, normalizeSpace, pattern, replacement } = this.state
    const regexTestLink = `https://regexr.com/?expression=${encodeURIComponent(pattern)}&text=${STRING_FOR_REGEXR_TEST}`
    const isEditing = this._isEditing()
    const allowEdit = activeEditingIndex === -1
    const isEditingInvalid = isSubstitutionInvalid(this.state)

    // Construct CSS class for row
    const editingClassName = isEditing ? '' : 'inactive'
    const allowEditClassName = allowEdit ? 'allow-edit' : ''
    const rowClassName = `substitution-row ${editingClassName} ${allowEditClassName}`

    return (

          <FormGroup style={{ margin: 0 }} validationState={isEditingInvalid ? 'error' : null}>
            <InputGroup>
              <FormControl
                // Autofocus the pattern field when user begins editing the substitution row.
                // eslint-disable-next-line jsx-a11y/no-autofocus
                autoFocus
                onChange={this._onChangePattern}
                onClick={this._onClickEdit}
                onKeyDown={this._onKeyDown}
                placeholder='Text (regex) to find'
                readOnly={!isEditing}
                title={isEditingInvalid ? 'Substitution search pattern cannot be empty' : null}
                value={pattern}
              />
              <InputGroup.Addon>

                  <Icon type='flask' />

              </InputGroup.Addon>
            </InputGroup>
          </FormGroup>

          <InputGroup>
            <FormControl
              onChange={this._onChangeReplacement}
              onClick={this._onClickEdit}
              onKeyDown={this._onKeyDown}
              placeholder='<blank>'
              readOnly={!isEditing}
              value={replacement}
            />
            <InputGroup.Addon style={{ padding: '0 6px' }}>
              <input
                aria-label='Normalize space'
                checked={normalizeSpace}
                onChange={this._onEditNormalizeSpace}
                onClick={this._onClickEdit}
                style={{ padding: '0 6px' }}
                type='checkbox'
              />
            </InputGroup.Addon>
          </InputGroup>

          <FormControl
            onChange={this._onChangeDescription}
            onClick={this._onClickEdit}
            onKeyDown={this._onKeyDown}
            placeholder='Optional description'
            readOnly={!isEditing}
            value={description}
          />

          {isEditing
            ? (
              // Show save/discard changes buttons if this row is being edited.
              <ButtonGroup style={buttonGroupStyle}>
                <Button
                  bsSize='xsmall'
                  disabled={isEditingInvalid}
                  onClick={this._onClickSave}
                  title={isEditingInvalid ? 'Unable to save, there are errors in this row.' : 'Save changes in this row'}
                >
                  <Icon type='check' />
                </Button>
                <Button
                  bsSize='xsmall'
                  onClick={this._onClickDiscard}
                  title='Discard changes in this row'
                >
                  <Icon type='remove' />
                </Button>
              </ButtonGroup>
            )
            : allowEdit && (
              // Show edit/delete if no other row is being edited.
              <ButtonGroup style={buttonGroupStyle}>
                <Button
                  bsSize='xsmall'
                  onClick={this._onClickEdit}
                  title='Edit this substitution'
                >
                  <Icon type='pencil' />
                </Button>
                <Button
                  bsSize='xsmall'
                  onClick={this._onRemove}
                  title='Remove this substitution'
                >
                  <Icon type='trash' />
                </Button>
              </ButtonGroup>
            )
          }

    )
  }
}
<|endoftext|>


<commit_before>var date = require('./date_helpers.js').today(process.env.NODE_ENV)
var due_date = require('./date_helpers.js').due_dates

const subsQueryTemplate = (columns, news_type) => (
  `select first_name, last_name, title, ${columns},
  datediff(${date}, max(payments.date)) as overdue,
  members.standing_order, members.due_date, members.id,
  sum(case payments.category
    when 'payment' then -payments.amount
    else                 payments.amount
    end) as amount
    from members right outer join payments
    on members.id = payments.member
    where members.primary_email is${news_type === 'online' ? ' not' : ''} null
    and members.membership_type in
    ('annual-single', 'annual-double', 'annual-family')
    and activation_status='activated'
    group by members.id
    having sum(case payments.category
      when 'payment' then -payments.amount
      else                 payments.amount
      end) > 0
      and datediff(${date}, max(payments.date)) > 30;`
)

const newsletterQueryTemplate = (columns, news_type) => (
  `select first_name, last_name, title, ${columns}
  from members
  where news_type = '${news_type}'
  and activation_status='activated';`
)

exports.update_subscription = body =>
  `insert into payments (member, category, amount, date, createdAt)
  select id, 'subscription', amount, ${date}, now() from members, membershiptypes
  where members.membership_type = membershiptypes.value
  and news_type = '${body.news_type}'
  and members.membership_type in
  ('annual-single', 'annual-double', 'annual-family', 'annual-corporate', 'annual-group')
  and ${due_date(body.start)(body.end)('members.due_date')}
  and activation_status='activated';`

exports.subscription_due_template = body =>
  `select first_name, last_name, title, address1, address2, address3, address4,
  county, postcode, id, due_date, membership_type, primary_email, secondary_email, amount
  from members, membershiptypes
  where members.membership_type = membershiptypes.value
  and news_type = '${body.news_type}'
  and standing_order in (null, false)
  and members.membership_type in
  ('annual-single', 'annual-double', 'annual-family', 'annual-corporate', 'annual-group')
  and ${due_date(body.start)(body.end)('members.due_date')}
  and activation_status='activated';`

const post_columns = 'address1, address2, address3, address4, county, postcode'

const online_columns = 'primary_email, secondary_email'

exports.newsletter = newsletterQueryTemplate(online_columns, 'online')

exports.newstype_post = newsletterQueryTemplate(post_columns, 'post')

exports.subscriptions = subsQueryTemplate(online_columns, 'online')

exports.newstype_post_nonzero = subsQueryTemplate(post_columns, 'post')

exports.custom_email =
  `select first_name, last_name, title, primary_email, secondary_email
  from members
  where primary_email is not null
  and activation_status='activated';`

exports.newsletter_labels =
  `select title, first_name, last_name, initials,
  address1, address2, address3, address4,
  postcode, deliverer from members
  where members.news_type = 'post'
  or members.email_bounced = true
  and activation_status='activated';`
<commit_msg>Balance overdue emails to everyone with a primary email, and letters to those who do not<commit_after>var date = require('./date_helpers.js').today(process.env.NODE_ENV)
var due_date = require('./date_helpers.js').due_dates

const subsQueryTemplate = (columns, news_type) => (
  `select first_name, last_name, title, ${post_columns}, ${online_columns},
  datediff(${date}, max(payments.date)) as overdue,
  members.standing_order, members.due_date, members.id,
  sum(case payments.category
    when 'payment' then -payments.amount
    else                 payments.amount
    end) as amount
    from members right outer join payments
    on members.id = payments.member
    where members.primary_email is${news_type === 'online' ? ' not' : ''} null
    and members.membership_type in
    ('annual-single', 'annual-double', 'annual-family')
    and activation_status='activated'
    group by members.id
    having sum(case payments.category
      when 'payment' then -payments.amount
      else                 payments.amount
      end) > 0
      and datediff(${date}, max(payments.date)) > 30;`
)

const newsletterQueryTemplate = (columns, news_type) => (
  `select first_name, last_name, title, ${columns}
  from members
  where news_type = '${news_type}'
  and activation_status='activated';`
)

exports.update_subscription = body =>
  `insert into payments (member, category, amount, date, createdAt)
  select id, 'subscription', amount, ${date}, now() from members, membershiptypes
  where members.membership_type = membershiptypes.value
  and news_type = '${body.news_type}'
  and members.membership_type in
  ('annual-single', 'annual-double', 'annual-family', 'annual-corporate', 'annual-group')
  and ${due_date(body.start)(body.end)('members.due_date')}
  and activation_status='activated';`

exports.subscription_due_template = body =>
  `select first_name, last_name, title, address1, address2, address3, address4,
  county, postcode, id, due_date, membership_type, primary_email, secondary_email, amount
  from members, membershiptypes
  where members.membership_type = membershiptypes.value
  and news_type = '${body.news_type}'
  and standing_order in (null, false)
  and members.membership_type in
  ('annual-single', 'annual-double', 'annual-family', 'annual-corporate', 'annual-group')
  and ${due_date(body.start)(body.end)('members.due_date')}
  and activation_status='activated';`

const post_columns = 'address1, address2, address3, address4, county, postcode'

const online_columns = 'primary_email, secondary_email'

exports.newsletter = newsletterQueryTemplate(online_columns, 'online')

exports.newstype_post = newsletterQueryTemplate(post_columns, 'post')

exports.subscriptions = subsQueryTemplate(online_columns, 'online')

exports.newstype_post_nonzero = subsQueryTemplate(post_columns, 'post')

exports.custom_email =
  `select first_name, last_name, title, primary_email, secondary_email
  from members
  where primary_email is not null
  and activation_status='activated';`

exports.newsletter_labels =
  `select title, first_name, last_name, initials,
  address1, address2, address3, address4,
  postcode, deliverer from members
  where members.news_type = 'post'
  or members.email_bounced = true
  and activation_status='activated';`
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name flowsimUiApp.directive:fgPacketCtx
 * @description
 * # fgPacketCtx
 */
angular.module('flowsimUiApp')
    .directive('fgPacketCtx', function () {
        return {
            templateUrl: 'views/simulation/fgpacketctx.html',
            replace: true,
            scope: {
                view: '=',
                packet: '='
            },
            restrict: 'E',
            link: function (scope) {

                scope.$watch('view', function(){
                    if(_.isNull(scope.view)){
                        scope.actionSet = null;
                    }
                }, true);

                scope.$watch('view.actionSet', function () {
                    if(scope.view){
                        if((_.isUndefined(scope.actionSet) || _.isNull(scope.actionSet)) && !(_.isUndefined(scope.view.actionSet))){
                            scope.actionSet = _.clone(scope.view.actionSet);
                        }else {
                            if(scope.view.actionSet > scope.actionSet){
                                _.each(scope.view.actionSet, function(el){
                                    if(!_.some(scope.actionSet, {value1: el.value1, value2: el.value2})){
                                        scope.actionSet.push(el);
                                    }
                                });
                            }else if(scope.view.actionSet < scope.actionSet) {
                                _.each(scope.actionSet, function(el, idx){
                                    if(_.isUndefined(el)){
                                        return;
                                    }
                                    if(!_.some(scope.view.actionSet, {value1: el.value1, value2: el.value2})){
                                        scope.actionSet.splice(idx,1);
                                    }
                                });
                            }
                        }
                    }
                }, true);
            }
        };
    });
<commit_msg>Fix for issue #135<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name flowsimUiApp.directive:fgPacketCtx
 * @description
 * # fgPacketCtx
 */
angular.module('flowsimUiApp')
    .directive('fgPacketCtx', function () {
        return {
            templateUrl: 'views/simulation/fgpacketctx.html',
            replace: true,
            scope: {
                view: '=',
                packet: '='
            },
            restrict: 'E',
            link: function (scope) {

                scope.$watch('view', function () {
                    if (_.isNull(scope.view)) {
                        scope.actionSet = null;
                    }
                }, true);

                scope.$watch('view.actionSet', function () {
                    if (scope.view) {
                        if ((_.isUndefined(scope.actionSet) || _.isNull(scope.actionSet)) && !(_.isUndefined(scope.view.actionSet))) {
                            scope.actionSet = _.clone(scope.view.actionSet);
                        } else {
                            //Update and/or add missing
                            _.each(scope.view.actionSet, function (el) {
                                var predicate = null;
                                if (el.value1 === 'eth') {//eth action unique key is value1 and value2
                                    predicate = {value1: el.value1, value2: el.value2};
                                } else {
                                    predicate = {value1: el.value1};
                                }
                                var action = _.findWhere(scope.actionSet, predicate);
                                if (_.isUndefined(action)) {
                                    scope.actionSet.push(el);
                                } else {
                                    action.value2 = el.value2;
                                    action.tip = el.tip;
                                }
                            });

                            _.each(scope.actionSet, function (el, idx) {
                                if (_.isUndefined(el)) {
                                    return;
                                }
                                if (!_.some(scope.view.actionSet, {value1: el.value1, value2: el.value2})) {
                                    scope.actionSet.splice(idx, 1);
                                }
                            });

                        }
                    }
                }, true);
            }
        };
    });
<|endoftext|>


<commit_before>/* global chrome */
import React from 'react'
import Navigation from './navigation/Navigation'
import { connect } from 'react-redux'
import Popup from 'react-popup'
import Help from './Help'
import Settings from './settings/Settings'
import Logs from './Logs'
import Gallery from './Gallery'
import AccessControl from './AccessControl'
import Editor from './editor/Editor'
import Configuration from '../../models/Configuration'
import DemoMonkeyServer from '../../models/DemoMonkeyServer'
import PropTypes from 'prop-types'
import Repository from '../../models/Repository'
import { Base64 } from 'js-base64'
import ErrorBox from '../shared/ErrorBox'
import WarningBox from '../shared/WarningBox'
import Page from '../shared/Page'
import JSZip from 'jszip'
import { logger } from '../../helpers/logger'

/* The OptionsPageApp will be defined below */
class App extends React.Component {
  static propTypes = {
    actions: PropTypes.objectOf(PropTypes.func).isRequired,
    configurations: PropTypes.arrayOf(PropTypes.object).isRequired,
    initialView: PropTypes.string.isRequired,
    demoMonkeyServer: PropTypes.instanceOf(DemoMonkeyServer).isRequired,
    onCurrentViewChange: PropTypes.func.isRequired,
    settings: PropTypes.object.isRequired,
    log: PropTypes.arrayOf(PropTypes.object).isRequired,
    permissions: PropTypes.object.isRequired
  }

  static getDerivedStateFromError(e) {
    return { withError: e }
  }

  constructor(props) {
    super(props)
    this.state = {
      isDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
      withError: false,
      permissions: this.props.permissions,
      currentView: this.props.initialView
    }
  }

  _getDarkMode() {
    if (this.props.settings.optionalFeatures.syncDarkMode) {
      return this.state.isDarkMode
    }
    return this.props.settings.optionalFeatures.preferDarkMode
  }

  componentDidMount() {
    this.mql = window.matchMedia('(prefers-color-scheme: dark)')
    this.darkModeUpdated = (e) => {
      this.setState({ isDarkMode: e.matches })
    }
    this.mql.addListener(this.darkModeUpdated)

    this.permissionsUpdated = () => {
      chrome.permissions.getAll((permissions) => {
        logger('info', 'Permissions updated:', permissions).write()
        this.setState({ permissions })
      })
    }

    if (chrome.permissions.onAdded) {
      chrome.permissions.onAdded.addListener(this.permissionsUpdated)
      chrome.permissions.onRemoved.addListener(this.permissionsUpdated)
    }
  }

  componentWillUnmount() {
    this.mql.removeListener(this.darkModeUpdated)
    if (chrome.permissions.onAdded) {
      chrome.permissions.onAdded.removeListener(this.permissionsUpdated)
      chrome.permissions.onRemoved.removeListener(this.permissionsUpdated)
    }
    window.removeListener('onpopstate', this.ops)
    delete this.mql
    delete this.permissionsUpdated
  }

  navigateTo(target) {
    this.setState({ currentView: target }, () => {
      this.props.onCurrentViewChange(target)
    })
  }

  downloadAll() {
    event.preventDefault()
    var zip = new JSZip()

    this.props.configurations.forEach((configuration) => {
      zip.file(configuration.name + '.mnky', configuration.content)
    })

    zip.generateAsync({ type: 'base64' })
      .then(function (content) {
        window.chrome.downloads.download({
          url: 'data:application/zip;base64,' + content,
          filename: 'demomonkey-' + (new Date()).toISOString().split('T')[0] + '.zip' // Optional
        })
      })
  }

  saveConfiguration(configuration) {
    if (configuration.id === 'new') {
      this.addConfiguration(configuration)
    } else {
      if (typeof configuration.values !== 'undefined') {
        const variables = (new Configuration(configuration.content, this.getRepository(), false, configuration.values)).getVariables().map(v => v.id)

        Object.keys(configuration.values).forEach(name => {
          if (!variables.includes(name)) {
            delete configuration.values[name]
          }
        })
      }

      this.props.actions.saveConfiguration(configuration.id, configuration)
    }
  }

  uploadConfiguration(upload) {
    if (Array.isArray(upload)) {
      this.props.actions.batchAddConfiguration(upload)
    } else {
      this.addConfiguration(upload)
    }
  }

  addConfiguration(configuration) {
    this.props.actions.addConfiguration(configuration).then(() => {
      const latest = this.props.configurations[this.props.configurations.length - 1]
      console.log(latest)
      this.navigateTo('configuration/' + latest.id)
    })
  }

  shareConfiguration(configuration) {
    // if shared is a string we have an id, so we can check for that.
    configuration.shared = !(typeof configuration.shared === 'string')
    this.saveConfiguration(configuration)
  }

  copyConfiguration(configuration) {
    var path = configuration.name.split('/')
    var name = 'Copy of ' + path.pop()
    if (configuration.connector) {
      delete configuration.connector
      delete configuration.remoteLocation
    }
    this.addConfiguration({
      ...configuration,
      name: path.length > 0 ? (path.join('/') + '/' + name) : name,
      id: 'new',
      enabled: false,
      readOnly: false
    })
  }

  downloadConfiguration(configuration) {
    window.chrome.downloads.download({
      url: 'data:text/octet-stream;base64,' + Base64.encode(configuration.content),
      filename: configuration.name.split('/').pop() + '.mnky'
    })
  }

  deleteConfiguration(configuration) {
    Popup.create({
      title: 'Please confirm',
      content: Do you really want to remove {configuration.name}?,
      buttons: {
        left: [{
          text: 'Cancel',
          action: () => Popup.close()
        }],
        right: [{
          text: 'Delete',
          className: 'danger',
          action: () => {
            Popup.close()
            this.navigateTo('help')
            // Delete all configurations within it if a directory is given
            logger('info', `Deleting ${configuration.name} (${configuration.nodeType})`).write()
            if (configuration.nodeType === 'directory') {
              this.props.actions.deleteConfigurationByPrefix(configuration.id.split('/').reverse().join('/'))
            } else {
              this.props.actions.deleteConfiguration(configuration.id)
            }
          }
        }]
      }
    })
  }

  getRepository() {
    return this._repo
  }

  updateRepository() {
    this._repo = new Repository(this.getConfigurations().reduce(function (repo, rawConfig) {
      repo[rawConfig.name] = new Configuration(rawConfig.content)
      return repo
    }, {}))
  }

  getConfigurations() {
    return this.props.configurations.filter((config) => typeof config.deleted_at === 'undefined' && typeof config._deleted === 'undefined')
  }

  getConfiguration(id) {
    if (id === 'create' || id === 'new') {
      return {
        name: '',
        content: this.props.settings.baseTemplate,
        id: 'new',
        hotkeys: []
      }
    }
    if (id === 'latest') {
      return this.props.configurations[this.props.configurations.length - 1]
    }
    return this.props.configurations.find((item) => item.id === id)
  }

  registerProtocolHandler() {
    const url = chrome.runtime.getURL('/options.html?s=%s')
    const method = this.props.settings.optionalFeatures.registerProtocolHandler ? 'registerProtocolHandler' : 'unregisterProtocolHandler'
    console.log(method)
    window.navigator[method](
      'web+mnky',
      url,
      'Demo Monkey Handler')
  }

  toggleOptionalFeature(feature) {
    this.props.actions.toggleOptionalFeature(feature).then(() => {
      if (feature === 'registerProtocolHandler') {
        this.registerProtocolHandler()
      }
    })
  }

  setBaseTemplate(baseTemplate) {
    this.props.actions.setBaseTemplate(baseTemplate)
  }

  setMonkeyInterval(interval) {
    this.props.actions.setMonkeyInterval(interval)
  }

  setDemoMonkeyServer(value) {
    this.props.actions.setDemoMonkeyServer(value)
  }

  getCurrentView() {
    if (this.state.withError) {
      return <ErrorBox error={this.state.withError} />
    }

    try {
      var segments = this.state.currentView.split('/')

      this.updateRepository()

      switch (segments[0]) {
        case 'settings':
          return <Settings settings={this.props.settings}
            configurations={this.props.configurations}
            demoMonkeyServer={this.props.demoMonkeyServer}
            onToggleOptionalFeature={(feature) => this.toggleOptionalFeature(feature)}
            onSetBaseTemplate={(baseTemplate) => this.setBaseTemplate(baseTemplate)}
            onSetMonkeyInterval={(value) => this.setMonkeyInterval(value)}
            onSetDemoMonkeyServer={(value) => this.setDemoMonkeyServer(value)}
            onDownloadAll={(event) => this.downloadAll(event)}
            onRequestExtendedPermissions={(revoke) => this.requestExtendedPermissions(revoke)}
            hasExtendedPermissions={this.hasExtendedPermissions()}
            isDarkMode={this._getDarkMode()}
            activeTab={segments[1]}
            onNavigate={(target) => this.navigateTo('settings/' + target)}
          />
        case 'configuration':
          var configuration = this.getConfiguration(segments[1])
          // If an unknown ID is selected, we throw an error.
          if (typeof configuration === 'undefined') {
            return <ErrorBox error={{ message: `Unknown Configuration ${segments[1]}` }} />
          }
          return <Editor getRepository={() => this.getRepository()} currentConfiguration={configuration}
            autoSave={this.props.settings.optionalFeatures.autoSave}
            saveOnClose={this.props.settings.optionalFeatures.saveOnClose}
            editorAutocomplete={this.props.settings.optionalFeatures.editorAutocomplete}
            keyboardHandler={this.props.settings.optionalFeatures.keyboardHandlerVim ? 'vim' : null}
            onDownload={(configuration, _) => this.downloadConfiguration(configuration)}
            onSave={(_, configuration) => this.saveConfiguration(configuration)}
            onShare={(_, configuration) => this.shareConfiguration(configuration)}
            onCopy={(configuration, _) => this.copyConfiguration(configuration)}
            onDelete={(configuration, _) => this.deleteConfiguration(configuration)}
            toggleConfiguration={() => this.props.actions.toggleConfiguration(configuration.id)}
            featureFlags={{
              withEvalCommand: this.props.settings.optionalFeatures.withEvalCommand,
              hookIntoAjax: this.props.settings.optionalFeatures.hookIntoAjax,
              webRequestHook: this.props.settings.optionalFeatures.webRequestHook
            }}
            isDarkMode={this._getDarkMode()}
            activeTab={segments[2]}
            onNavigate={(target) => this.navigateTo('configuration/' + configuration.id + '/' + target)}
          />
        case 'accessControl':
          return <AccessControl for={segments[1]} />
        case 'logs':
          return <Logs entries={this.props.log} />
        case 'gallery':
          return <Gallery />
        default:
          return <Help />
      }
    } catch (e) {
      return <ErrorBox error={e} />
    }
  }

  hasExtendedPermissions() {
    const permissions = this.state.permissions
    if (Array.isArray(permissions.origins) && permissions.origins.length > 0) {
      return permissions.origins.includes('http://*/*') && permissions.origins.includes('https://*/*')
    }
    return false
  }

  requestExtendedPermissions(revoke = false) {
    console.log(revoke)
    if (revoke) {
      chrome.permissions.remove({
        origins: ['http://*/*', 'https://*/*']
      }, function (removed) {
        if (removed) {
          logger('info', 'Additional permissions removed')
        } else {
          logger('warn', 'Additional permissions not removed')
        }
      })
    } else {
      chrome.permissions.request({
        origins: ['http://*/*', 'https://*/*']
      }, function (granted) {
        if (granted) {
          logger('info', 'Additional permissions granted')
        } else {
          logger('warn', 'Additional permissions not granted')
        }
      })
    }
  }

  render() {
    var activeItem = this.state.currentView.indexOf('configuration/') === -1 ? false : this.state.currentView.split('/').pop()

    var configurations = this.getConfigurations()

    var withWarning = (!this.hasExtendedPermissions() && !this.props.settings.optionalFeatures.noWarningForMissingPermissions) ? ' with-warning' : ''

    return <Page className={`main-grid${withWarning}`} preferDarkMode={this.props.settings.optionalFeatures.preferDarkMode} syncDarkMode={this.props.settings.optionalFeatures.syncDarkMode}>
      <Popup className="popup" btnClass="popup__btn" />
      { withWarning !== ''
        ? <WarningBox onDismiss={() => this.toggleOptionalFeature('noWarningForMissingPermissions')}
          onRequestExtendedPermissions={() => this.requestExtendedPermissions()}
        />
        : '' }

        <Navigation onNavigate={(target) => this.navigateTo(target)}
          onUpload={(upload) => this.uploadConfiguration(upload)}
          onDelete={(configuration) => this.deleteConfiguration(configuration)}
          items={configurations}
          onDownloadAll={(event) => this.downloadAll(event)}
          demoMonkeyServer={this.props.demoMonkeyServer}
          remoteLocation={this.props.settings.demoMonkeyServer}
          active={activeItem}
          showLogs={this.props.settings.optionalFeatures.writeLogs === true}
        />

        {this.getCurrentView()}

    </Page>
  }
}

const OptionsPageApp = connect(
  // map state to props
  state => {
    return {
      configurations: state.configurations,
      // currentView: state.currentView,
      demoMonkeyServer: new DemoMonkeyServer(state.settings.demoMonkeyServer, state.connectionState),
      settings: state.settings,
      log: state.log
    }
  },
  // map dispatch to props
  dispatch => ({
    actions: {
      setMonkeyInterval: (monkeyInterval) => {
        dispatch({ type: 'SET_MONKEY_INTERVAL', monkeyInterval })
      },
      setDemoMonkeyServer: (demoMonkeyServer) => {
        dispatch({ type: 'SET_DEMO_MONKEY_SERVER', demoMonkeyServer })
      },
      toggleConfiguration: (id) => {
        dispatch({ type: 'TOGGLE_CONFIGURATION', id: id })
      },
      saveConfiguration: (id, configuration) => {
        dispatch({ type: 'SAVE_CONFIGURATION', id, configuration })
      },
      deleteConfiguration: (id) => {
        dispatch({ type: 'DELETE_CONFIGURATION', id })
      },
      deleteConfigurationByPrefix: (prefix) => {
        dispatch({ type: 'DELETE_CONFIGURATION_BY_PREFIX', prefix })
      },
      batchAddConfiguration: (configurations) => {
        return dispatch({ type: 'BATCH_ADD_CONFIGURATION', configurations })
      },
      addConfiguration: (configuration) => {
        return dispatch({ type: 'ADD_CONFIGURATION', configuration })
      },
      setBaseTemplate: (baseTemplate) => {
        dispatch({ type: 'SET_BASE_TEMPLATE', baseTemplate })
      },
      toggleOptionalFeature: (optionalFeature) => {
        return dispatch({ type: 'TOGGLE_OPTIONAL_FEATURE', optionalFeature })
      }
    }
  }))(App)

export default OptionsPageApp
<commit_msg>Fix #73<commit_after>/* global chrome */
import React from 'react'
import Navigation from './navigation/Navigation'
import { connect } from 'react-redux'
import Popup from 'react-popup'
import Help from './Help'
import Settings from './settings/Settings'
import Logs from './Logs'
import Gallery from './Gallery'
import AccessControl from './AccessControl'
import Editor from './editor/Editor'
import Configuration from '../../models/Configuration'
import DemoMonkeyServer from '../../models/DemoMonkeyServer'
import PropTypes from 'prop-types'
import Repository from '../../models/Repository'
import { Base64 } from 'js-base64'
import ErrorBox from '../shared/ErrorBox'
import WarningBox from '../shared/WarningBox'
import Page from '../shared/Page'
import JSZip from 'jszip'
import { logger } from '../../helpers/logger'

/* The OptionsPageApp will be defined below */
class App extends React.Component {
  static propTypes = {
    actions: PropTypes.objectOf(PropTypes.func).isRequired,
    configurations: PropTypes.arrayOf(PropTypes.object).isRequired,
    initialView: PropTypes.string.isRequired,
    demoMonkeyServer: PropTypes.instanceOf(DemoMonkeyServer).isRequired,
    onCurrentViewChange: PropTypes.func.isRequired,
    settings: PropTypes.object.isRequired,
    log: PropTypes.arrayOf(PropTypes.object).isRequired,
    permissions: PropTypes.object.isRequired
  }

  static getDerivedStateFromError(e) {
    return { withError: e }
  }

  constructor(props) {
    super(props)
    this.state = {
      isDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
      withError: false,
      permissions: this.props.permissions,
      currentView: this.props.initialView
    }
  }

  _getDarkMode() {
    if (this.props.settings.optionalFeatures.syncDarkMode) {
      return this.state.isDarkMode
    }
    return this.props.settings.optionalFeatures.preferDarkMode
  }

  componentDidMount() {
    this.mql = window.matchMedia('(prefers-color-scheme: dark)')
    this.darkModeUpdated = (e) => {
      this.setState({ isDarkMode: e.matches })
    }
    this.mql.addListener(this.darkModeUpdated)

    this.permissionsUpdated = () => {
      chrome.permissions.getAll((permissions) => {
        logger('info', 'Permissions updated:', permissions).write()
        this.setState({ permissions })
      })
    }

    if (chrome.permissions.onAdded) {
      chrome.permissions.onAdded.addListener(this.permissionsUpdated)
      chrome.permissions.onRemoved.addListener(this.permissionsUpdated)
    }
  }

  componentWillUnmount() {
    this.mql.removeListener(this.darkModeUpdated)
    if (chrome.permissions.onAdded) {
      chrome.permissions.onAdded.removeListener(this.permissionsUpdated)
      chrome.permissions.onRemoved.removeListener(this.permissionsUpdated)
    }
    window.removeListener('onpopstate', this.ops)
    delete this.mql
    delete this.permissionsUpdated
  }

  navigateTo(target) {
    this.setState({ currentView: target }, () => {
      this.props.onCurrentViewChange(target)
    })
  }

  downloadAll() {
    event.preventDefault()
    var zip = new JSZip()

    this.props.configurations.forEach((configuration) => {
      zip.file(configuration.name + '.mnky', configuration.content)
    })

    zip.generateAsync({ type: 'base64' })
      .then(function (content) {
        window.chrome.downloads.download({
          url: 'data:application/zip;base64,' + content,
          filename: 'demomonkey-' + (new Date()).toISOString().split('T')[0] + '.zip' // Optional
        })
      })
  }

  saveConfiguration(configuration) {
    if (configuration.id === 'new') {
      this.addConfiguration(configuration)
    } else {
      if (typeof configuration.values !== 'undefined') {
        const variables = (new Configuration(configuration.content, this.getRepository(), false, configuration.values)).getVariables().map(v => v.id)

        Object.keys(configuration.values).forEach(name => {
          if (!variables.includes(name)) {
            delete configuration.values[name]
          }
        })
      }

      this.props.actions.saveConfiguration(configuration.id, configuration)
    }
  }

  uploadConfiguration(upload) {
    if (Array.isArray(upload)) {
      this.props.actions.batchAddConfiguration(upload)
    } else {
      this.addConfiguration(upload)
    }
  }

  addConfiguration(configuration) {
    this.props.actions.addConfiguration(configuration).then(() => {
      const latest = this.props.configurations[this.props.configurations.length - 1]
      console.log(latest)
      this.navigateTo('configuration/' + latest.id)
    })
  }

  shareConfiguration(configuration) {
    // if shared is a string we have an id, so we can check for that.
    configuration.shared = !(typeof configuration.shared === 'string')
    this.saveConfiguration(configuration)
  }

  copyConfiguration(configuration) {
    var path = configuration.name.split('/')
    var name = 'Copy of ' + path.pop()
    if (configuration.connector) {
      delete configuration.connector
      delete configuration.remoteLocation
    }
    this.addConfiguration({
      ...configuration,
      name: path.length > 0 ? (path.join('/') + '/' + name) : name,
      id: 'new',
      enabled: false,
      readOnly: false
    })
  }

  downloadConfiguration(configuration) {
    window.chrome.downloads.download({
      url: 'data:text/octet-stream;base64,' + Base64.encode(configuration.content),
      filename: configuration.name.split('/').pop() + '.mnky'
    }, () => {
      if (chrome.runtime.lastError && chrome.runtime.lastError.message === 'Invalid filename') {
        window.chrome.downloads.download({
          url: 'data:text/octet-stream;base64,' + Base64.encode(configuration.content),
          saveAs: true
        })
      }
    })
  }

  deleteConfiguration(configuration) {
    Popup.create({
      title: 'Please confirm',
      content: Do you really want to remove {configuration.name}?,
      buttons: {
        left: [{
          text: 'Cancel',
          action: () => Popup.close()
        }],
        right: [{
          text: 'Delete',
          className: 'danger',
          action: () => {
            Popup.close()
            this.navigateTo('help')
            // Delete all configurations within it if a directory is given
            logger('info', `Deleting ${configuration.name} (${configuration.nodeType})`).write()
            if (configuration.nodeType === 'directory') {
              this.props.actions.deleteConfigurationByPrefix(configuration.id.split('/').reverse().join('/'))
            } else {
              this.props.actions.deleteConfiguration(configuration.id)
            }
          }
        }]
      }
    })
  }

  getRepository() {
    return this._repo
  }

  updateRepository() {
    this._repo = new Repository(this.getConfigurations().reduce(function (repo, rawConfig) {
      repo[rawConfig.name] = new Configuration(rawConfig.content)
      return repo
    }, {}))
  }

  getConfigurations() {
    return this.props.configurations.filter((config) => typeof config.deleted_at === 'undefined' && typeof config._deleted === 'undefined')
  }

  getConfiguration(id) {
    if (id === 'create' || id === 'new') {
      return {
        name: '',
        content: this.props.settings.baseTemplate,
        id: 'new',
        hotkeys: []
      }
    }
    if (id === 'latest') {
      return this.props.configurations[this.props.configurations.length - 1]
    }
    return this.props.configurations.find((item) => item.id === id)
  }

  registerProtocolHandler() {
    const url = chrome.runtime.getURL('/options.html?s=%s')
    const method = this.props.settings.optionalFeatures.registerProtocolHandler ? 'registerProtocolHandler' : 'unregisterProtocolHandler'
    console.log(method)
    window.navigator[method](
      'web+mnky',
      url,
      'Demo Monkey Handler')
  }

  toggleOptionalFeature(feature) {
    this.props.actions.toggleOptionalFeature(feature).then(() => {
      if (feature === 'registerProtocolHandler') {
        this.registerProtocolHandler()
      }
    })
  }

  setBaseTemplate(baseTemplate) {
    this.props.actions.setBaseTemplate(baseTemplate)
  }

  setMonkeyInterval(interval) {
    this.props.actions.setMonkeyInterval(interval)
  }

  setDemoMonkeyServer(value) {
    this.props.actions.setDemoMonkeyServer(value)
  }

  getCurrentView() {
    if (this.state.withError) {
      return <ErrorBox error={this.state.withError} />
    }

    try {
      var segments = this.state.currentView.split('/')

      this.updateRepository()

      switch (segments[0]) {
        case 'settings':
          return <Settings settings={this.props.settings}
            configurations={this.props.configurations}
            demoMonkeyServer={this.props.demoMonkeyServer}
            onToggleOptionalFeature={(feature) => this.toggleOptionalFeature(feature)}
            onSetBaseTemplate={(baseTemplate) => this.setBaseTemplate(baseTemplate)}
            onSetMonkeyInterval={(value) => this.setMonkeyInterval(value)}
            onSetDemoMonkeyServer={(value) => this.setDemoMonkeyServer(value)}
            onDownloadAll={(event) => this.downloadAll(event)}
            onRequestExtendedPermissions={(revoke) => this.requestExtendedPermissions(revoke)}
            hasExtendedPermissions={this.hasExtendedPermissions()}
            isDarkMode={this._getDarkMode()}
            activeTab={segments[1]}
            onNavigate={(target) => this.navigateTo('settings/' + target)}
          />
        case 'configuration':
          var configuration = this.getConfiguration(segments[1])
          // If an unknown ID is selected, we throw an error.
          if (typeof configuration === 'undefined') {
            return <ErrorBox error={{ message: `Unknown Configuration ${segments[1]}` }} />
          }
          return <Editor getRepository={() => this.getRepository()} currentConfiguration={configuration}
            autoSave={this.props.settings.optionalFeatures.autoSave}
            saveOnClose={this.props.settings.optionalFeatures.saveOnClose}
            editorAutocomplete={this.props.settings.optionalFeatures.editorAutocomplete}
            keyboardHandler={this.props.settings.optionalFeatures.keyboardHandlerVim ? 'vim' : null}
            onDownload={(configuration, _) => this.downloadConfiguration(configuration)}
            onSave={(_, configuration) => this.saveConfiguration(configuration)}
            onShare={(_, configuration) => this.shareConfiguration(configuration)}
            onCopy={(configuration, _) => this.copyConfiguration(configuration)}
            onDelete={(configuration, _) => this.deleteConfiguration(configuration)}
            toggleConfiguration={() => this.props.actions.toggleConfiguration(configuration.id)}
            featureFlags={{
              withEvalCommand: this.props.settings.optionalFeatures.withEvalCommand,
              hookIntoAjax: this.props.settings.optionalFeatures.hookIntoAjax,
              webRequestHook: this.props.settings.optionalFeatures.webRequestHook
            }}
            isDarkMode={this._getDarkMode()}
            activeTab={segments[2]}
            onNavigate={(target) => this.navigateTo('configuration/' + configuration.id + '/' + target)}
          />
        case 'accessControl':
          return <AccessControl for={segments[1]} />
        case 'logs':
          return <Logs entries={this.props.log} />
        case 'gallery':
          return <Gallery />
        default:
          return <Help />
      }
    } catch (e) {
      return <ErrorBox error={e} />
    }
  }

  hasExtendedPermissions() {
    const permissions = this.state.permissions
    if (Array.isArray(permissions.origins) && permissions.origins.length > 0) {
      return permissions.origins.includes('http://*/*') && permissions.origins.includes('https://*/*')
    }
    return false
  }

  requestExtendedPermissions(revoke = false) {
    console.log(revoke)
    if (revoke) {
      chrome.permissions.remove({
        origins: ['http://*/*', 'https://*/*']
      }, function (removed) {
        if (removed) {
          logger('info', 'Additional permissions removed')
        } else {
          logger('warn', 'Additional permissions not removed')
        }
      })
    } else {
      chrome.permissions.request({
        origins: ['http://*/*', 'https://*/*']
      }, function (granted) {
        if (granted) {
          logger('info', 'Additional permissions granted')
        } else {
          logger('warn', 'Additional permissions not granted')
        }
      })
    }
  }

  render() {
    var activeItem = this.state.currentView.indexOf('configuration/') === -1 ? false : this.state.currentView.split('/').pop()

    var configurations = this.getConfigurations()

    var withWarning = (!this.hasExtendedPermissions() && !this.props.settings.optionalFeatures.noWarningForMissingPermissions) ? ' with-warning' : ''

    return <Page className={`main-grid${withWarning}`} preferDarkMode={this.props.settings.optionalFeatures.preferDarkMode} syncDarkMode={this.props.settings.optionalFeatures.syncDarkMode}>
      <Popup className="popup" btnClass="popup__btn" />
      { withWarning !== ''
        ? <WarningBox onDismiss={() => this.toggleOptionalFeature('noWarningForMissingPermissions')}
          onRequestExtendedPermissions={() => this.requestExtendedPermissions()}
        />
        : '' }

        <Navigation onNavigate={(target) => this.navigateTo(target)}
          onUpload={(upload) => this.uploadConfiguration(upload)}
          onDelete={(configuration) => this.deleteConfiguration(configuration)}
          items={configurations}
          onDownloadAll={(event) => this.downloadAll(event)}
          demoMonkeyServer={this.props.demoMonkeyServer}
          remoteLocation={this.props.settings.demoMonkeyServer}
          active={activeItem}
          showLogs={this.props.settings.optionalFeatures.writeLogs === true}
        />

        {this.getCurrentView()}

    </Page>
  }
}

const OptionsPageApp = connect(
  // map state to props
  state => {
    return {
      configurations: state.configurations,
      // currentView: state.currentView,
      demoMonkeyServer: new DemoMonkeyServer(state.settings.demoMonkeyServer, state.connectionState),
      settings: state.settings,
      log: state.log
    }
  },
  // map dispatch to props
  dispatch => ({
    actions: {
      setMonkeyInterval: (monkeyInterval) => {
        dispatch({ type: 'SET_MONKEY_INTERVAL', monkeyInterval })
      },
      setDemoMonkeyServer: (demoMonkeyServer) => {
        dispatch({ type: 'SET_DEMO_MONKEY_SERVER', demoMonkeyServer })
      },
      toggleConfiguration: (id) => {
        dispatch({ type: 'TOGGLE_CONFIGURATION', id: id })
      },
      saveConfiguration: (id, configuration) => {
        dispatch({ type: 'SAVE_CONFIGURATION', id, configuration })
      },
      deleteConfiguration: (id) => {
        dispatch({ type: 'DELETE_CONFIGURATION', id })
      },
      deleteConfigurationByPrefix: (prefix) => {
        dispatch({ type: 'DELETE_CONFIGURATION_BY_PREFIX', prefix })
      },
      batchAddConfiguration: (configurations) => {
        return dispatch({ type: 'BATCH_ADD_CONFIGURATION', configurations })
      },
      addConfiguration: (configuration) => {
        return dispatch({ type: 'ADD_CONFIGURATION', configuration })
      },
      setBaseTemplate: (baseTemplate) => {
        dispatch({ type: 'SET_BASE_TEMPLATE', baseTemplate })
      },
      toggleOptionalFeature: (optionalFeature) => {
        return dispatch({ type: 'TOGGLE_OPTIONAL_FEATURE', optionalFeature })
      }
    }
  }))(App)

export default OptionsPageApp
<|endoftext|>


<commit_before>
/*
All required uci packages are stored variable uci.
The GUI code displayes and manipulated this variable.
*/
var uci = {};
var gid = 0;

function init()
{
	send("/cgi-bin/settings", { func : "get_settings" }, function(data) {
		uci = fromUCI(data);
		rebuild_general();
	});
}

function updateFrom(src)
{
	var obj = {};
	collect_inputs(src, obj);
	for(var name in obj)
	{
		var value = obj[name];
		var path = name.split('#');

		var pkg = path[0];
		var sec = path[1];
		var opt = path[2];

		uci[pkg].pchanged = true;
		uci[pkg][sec][opt] = value
	}
}

function getChangeModeAction(ifname)
{
	return function(e) {
		var src = (e.target || e.srcElement);
		var mode = src.value;
		delNetSection(ifname);
		addNetSection(ifname, mode);
	};
}

function appendSetting(p, path, value, mode)
{
	var id = path.join('#');
	var b;
	var cfg = path[0]
	var name = path[path.length-1];
	switch(name)
	{
	case "geo":
		b = append_input(p, "GPS-Koordinaten", id, value);
		b.lastChild.placeholder = "52.02713078 8.52829987";
		addInputCheck(b.lastChild,/^\d{1,3}\.\d{1,8} +\d{1,3}\.\d{1,8}$/, "Koordinaten ist ung\xfcltig.");
		addHelpText(b, "Die Koordinaten dieses Knotens auf der Freifunk-Karte (z.B. \"52.02713078 8.52829987\").");
		break;
	case "name":
		b = append_input(p, "Knotenname", id, value);
		b.lastChild.placeholder = "MeinServer";
		addInputCheck(b.lastChild,/^\w+[\w\-]{0,20}\w+$/, name + " ist ung\xfcltig.");
		addHelpText(b, "Der Name dieses Knotens auf der Freifunk-Karte.");
		break;
	case "enabled":
		if(cfg == "autoupdater") {
			b = append_radio(p, "Autoupdater", id, value, [["An", "1"], ["Aus", "0"]]);
			addHelpText(b, "Der Autoupdater aktualisiert die Firmware automatisch auf die neuste Version. Dabei werden allerdings alle Einstellungen zur\xfcckgesetzt.");
		}
		if(cfg == "simple-tc") {
			b = append_radio(p, "Public Traffic Control", id, value, [["An", "1"], ["Aus", "0"]]);
			addHelpText(b, "Bandweitenkontrolle f\xfcr den Upload-/Download \xfcber das Freifunknetz \xfcber den eigenen Internetanschluss.");
		}
		break;
	case "limit_egress":
		b = append_input(p, "Public Upload", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Upload ist ung\xfcltig.");
		addHelpText(b, "Maximaler Upload in KBit/s f\xfcr die Bandweitenkontrolle.");
		break;
	case "limit_ingress":
		b = append_input(p, "Public Download", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Download ist ung\xfcltig.");
		addHelpText(b, "Maximaler Download in KBit/s f\xfcr die Bandweitenkontrolle.");
		break;
	case "share_internet":
		b = append_radio(p, "Gateway Modus", id, value, [["An", "yes"], ["Aus", "no"]]);
		if(!adv_mode)
			onDesc(b, "INPUT", function(e) { e.disabled = true; });
		addHelpText(b, "An bedeutet das der private Internetanschluss f\xfcr die \xD6ffentlichkeit freigegeben wird.
Die empfohlene Einstellung ist Aus, da es zu rechtlichen Problemen kommen kann.");
		break;
	case "access_from":
		b = append_check(p, "SSH/HTTPS Zugriff", id, split(value), [["WAN","wan"], ["Private","private"], ["Public","public"]]);
		addHelpText(b, "Zugang zur Konfiguration \xfcber verschiedene Anschl\xfcsse/Netzwerke erm\xf6glichen.")
		break;
	case "service_link":
		b = append_input(p, "Service Link", id, value);
		b.lastChild.placeholder = "http://[fdef:17a0::1]/seite.html";
		addInputCheck(b.lastChild,/^[\[\] \w\/.:]{3,60}$/, "Ung\xfcltige Eingabe.");
		addHelpText(b, "Ein Verweis auf eine Netzwerkresource. Z.B. \"http://1.2.3.4\".");
		break;
	case "service_label":
		b = append_input(p, "Service Name", id, value);
		b.lastChild.placeholder = "MeineWebseite";
		addInputCheck(b.lastChild,/^[\[\] \w\/.:]{3,30}$/, "Ung\xfcltige Eingabe.");
		addHelpText(b, "Ein Name der angegebenen Netzwerkresource. Z.B. \"Meine Webseite\".");
		break;
	case "service_display_max":
		b = append_input(p, "Max. Angezeigte-Eintr\xe4ge", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Ung\xfcltige Zahl.");
		addHelpText(b, "Maximale Anzahl der auf der Statusseite angezeigten Eintr\xe4ge.");
		break;
	default:
		return;
	}

	b.id = id; //needed for updateFrom
	b.onchange = function() {
		updateFrom(b);
	};

	return b;
}

function rebuild_general()
{
	var gfs = $("general");
	var rfs = $("resource");
	var tfs = $("traffic");

	removeChilds(gfs);
	removeChilds(rfs);
	removeChilds(tfs);

	if('freifunk' in uci) {
		var f = uci.freifunk;
		var i = firstSectionID(f, "settings");
		appendSetting(gfs, ['freifunk', i, "name"], f[i]["name"]);
		appendSetting(gfs, ['freifunk', i, "geo"], f[i]["geo"]);
		appendSetting(gfs, ['freifunk', i, "access_from"], f[i]["access_from"]);
		appendSetting(gfs, ['freifunk', i, "share_internet"], f[i]["share_internet"]);
		appendSetting(rfs, ['freifunk', i, "service_label"], f[i]["service_label"]);
		appendSetting(rfs, ['freifunk', i, "service_link"], f[i]["service_link"]);
		appendSetting(rfs, ['freifunk', i, "service_display_max"], f[i]["service_display_max"]);
	}

	if('autoupdater' in uci) {
		var a = uci.autoupdater;
		var i = firstSectionID(a, "autoupdater");
		appendSetting(gfs, ['autoupdater', i, "enabled"], a[i]["enabled"]);
	}

	if('simple-tc' in uci) {
		var t = uci['simple-tc'];
		var i = firstSectionID(t, "interface");
		appendSetting(tfs, ['simple-tc', i, "enabled"], t[i]["enabled"]);
		appendSetting(tfs, ['simple-tc', i, "limit_ingress"], t[i]["limit_ingress"]);
		appendSetting(tfs, ['simple-tc', i, "limit_egress"], t[i]["limit_egress"]);
	}
}

function save_data()
{
	for(var name in uci)
	{
		var obj = uci[name];
		if(!obj.pchanged)
			continue;
		var data = toUCI(obj);
		send("/cgi-bin/misc", { func : "set_config_file", name : name, data : data },
			function(data) {
				$('msg').innerHTML = data;
				$('msg').focus();
				init();
			}
		);
	}
}
<commit_msg>change label<commit_after>
/*
All required uci packages are stored variable uci.
The GUI code displayes and manipulated this variable.
*/
var uci = {};
var gid = 0;

function init()
{
	send("/cgi-bin/settings", { func : "get_settings" }, function(data) {
		uci = fromUCI(data);
		rebuild_general();
	});
}

function updateFrom(src)
{
	var obj = {};
	collect_inputs(src, obj);
	for(var name in obj)
	{
		var value = obj[name];
		var path = name.split('#');

		var pkg = path[0];
		var sec = path[1];
		var opt = path[2];

		uci[pkg].pchanged = true;
		uci[pkg][sec][opt] = value
	}
}

function getChangeModeAction(ifname)
{
	return function(e) {
		var src = (e.target || e.srcElement);
		var mode = src.value;
		delNetSection(ifname);
		addNetSection(ifname, mode);
	};
}

function appendSetting(p, path, value, mode)
{
	var id = path.join('#');
	var b;
	var cfg = path[0]
	var name = path[path.length-1];
	switch(name)
	{
	case "geo":
		b = append_input(p, "GPS-Koordinaten", id, value);
		b.lastChild.placeholder = "52.02713078 8.52829987";
		addInputCheck(b.lastChild,/^\d{1,3}\.\d{1,8} +\d{1,3}\.\d{1,8}$/, "Koordinaten ist ung\xfcltig.");
		addHelpText(b, "Die Koordinaten dieses Knotens auf der Freifunk-Karte (z.B. \"52.02713078 8.52829987\").");
		break;
	case "name":
		b = append_input(p, "Knotenname", id, value);
		b.lastChild.placeholder = "MeinRouter";
		addInputCheck(b.lastChild,/^\w+[\w\-]{0,20}\w+$/, name + " ist ung\xfcltig.");
		addHelpText(b, "Der Name dieses Knotens auf der Freifunk-Karte.");
		break;
	case "enabled":
		if(cfg == "autoupdater") {
			b = append_radio(p, "Autoupdater", id, value, [["An", "1"], ["Aus", "0"]]);
			addHelpText(b, "Der Autoupdater aktualisiert die Firmware automatisch auf die neuste Version. Dabei werden allerdings alle Einstellungen zur\xfcckgesetzt.");
		}
		if(cfg == "simple-tc") {
			b = append_radio(p, "Public Traffic Control", id, value, [["An", "1"], ["Aus", "0"]]);
			addHelpText(b, "Bandweitenkontrolle f\xfcr den Upload-/Download \xfcber das Freifunknetz \xfcber den eigenen Internetanschluss.");
		}
		break;
	case "limit_egress":
		b = append_input(p, "Public Upload", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Upload ist ung\xfcltig.");
		addHelpText(b, "Maximaler Upload in KBit/s f\xfcr die Bandweitenkontrolle.");
		break;
	case "limit_ingress":
		b = append_input(p, "Public Download", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Download ist ung\xfcltig.");
		addHelpText(b, "Maximaler Download in KBit/s f\xfcr die Bandweitenkontrolle.");
		break;
	case "share_internet":
		b = append_radio(p, "Gateway Modus", id, value, [["An", "yes"], ["Aus", "no"]]);
		if(!adv_mode)
			onDesc(b, "INPUT", function(e) { e.disabled = true; });
		addHelpText(b, "An bedeutet das der private Internetanschluss f\xfcr die \xD6ffentlichkeit freigegeben wird.
Die empfohlene Einstellung ist Aus, da es zu rechtlichen Problemen kommen kann.");
		break;
	case "access_from":
		b = append_check(p, "SSH/HTTPS Zugriff", id, split(value), [["WAN","wan"], ["Private","private"], ["Public","public"]]);
		addHelpText(b, "Zugang zur Konfiguration \xfcber verschiedene Anschl\xfcsse/Netzwerke erm\xf6glichen.")
		break;
	case "service_link":
		b = append_input(p, "Service Link", id, value);
		b.lastChild.placeholder = "http://[fdef:17a0::1]/seite.html";
		addInputCheck(b.lastChild,/^[\[\] \w\/.:]{3,60}$/, "Ung\xfcltige Eingabe.");
		addHelpText(b, "Ein Verweis auf eine Netzwerkresource. Z.B. \"http://1.2.3.4\".");
		break;
	case "service_label":
		b = append_input(p, "Service Name", id, value);
		b.lastChild.placeholder = "MeineWebseite";
		addInputCheck(b.lastChild,/^[\[\] \w\/.:]{3,30}$/, "Ung\xfcltige Eingabe.");
		addHelpText(b, "Ein Name der angegebenen Netzwerkresource. Z.B. \"Meine Webseite\".");
		break;
	case "service_display_max":
		b = append_input(p, "Max. Angezeigte-Eintr\xe4ge", id, value);
		addInputCheck(b.lastChild,/^\d+$/, "Ung\xfcltige Zahl.");
		addHelpText(b, "Maximale Anzahl der auf der Statusseite angezeigten Eintr\xe4ge.");
		break;
	default:
		return;
	}

	b.id = id; //needed for updateFrom
	b.onchange = function() {
		updateFrom(b);
	};

	return b;
}

function rebuild_general()
{
	var gfs = $("general");
	var rfs = $("resource");
	var tfs = $("traffic");

	removeChilds(gfs);
	removeChilds(rfs);
	removeChilds(tfs);

	if('freifunk' in uci) {
		var f = uci.freifunk;
		var i = firstSectionID(f, "settings");
		appendSetting(gfs, ['freifunk', i, "name"], f[i]["name"]);
		appendSetting(gfs, ['freifunk', i, "geo"], f[i]["geo"]);
		appendSetting(gfs, ['freifunk', i, "access_from"], f[i]["access_from"]);
		appendSetting(gfs, ['freifunk', i, "share_internet"], f[i]["share_internet"]);
		appendSetting(rfs, ['freifunk', i, "service_label"], f[i]["service_label"]);
		appendSetting(rfs, ['freifunk', i, "service_link"], f[i]["service_link"]);
		appendSetting(rfs, ['freifunk', i, "service_display_max"], f[i]["service_display_max"]);
	}

	if('autoupdater' in uci) {
		var a = uci.autoupdater;
		var i = firstSectionID(a, "autoupdater");
		appendSetting(gfs, ['autoupdater', i, "enabled"], a[i]["enabled"]);
	}

	if('simple-tc' in uci) {
		var t = uci['simple-tc'];
		var i = firstSectionID(t, "interface");
		appendSetting(tfs, ['simple-tc', i, "enabled"], t[i]["enabled"]);
		appendSetting(tfs, ['simple-tc', i, "limit_ingress"], t[i]["limit_ingress"]);
		appendSetting(tfs, ['simple-tc', i, "limit_egress"], t[i]["limit_egress"]);
	}
}

function save_data()
{
	for(var name in uci)
	{
		var obj = uci[name];
		if(!obj.pchanged)
			continue;
		var data = toUCI(obj);
		send("/cgi-bin/misc", { func : "set_config_file", name : name, data : data },
			function(data) {
				$('msg').innerHTML = data;
				$('msg').focus();
				init();
			}
		);
	}
}
<|endoftext|>


<commit_before>namespace('App.Bundle.Todo.Controller');

App.Bundle.Todo.Controller.Main = function () {
    Sy.Controller.call(this);
    this.tasks = null;
    this.repo = null;
    this.templating = null;
};
App.Bundle.Todo.Controller.Main.prototype = Object.create(Sy.Controller.prototype, {

    init: {
        value: function () {
            this.tasks = new Sy.Registry();
            this.repo = this.container
                .get('sy::core::storage')
                .getManager()
                .getRepository('Todo::Task');
            this.templating = this.container
                .get('sy::core::view::template::engine');
            this.repo.findBy({
                index: 'uuid',
                value: [''],
                callback: this.loadPreviousTasks.bind(this)
            });
        }
    },

    loadPreviousTasks: {
        value: function (tasks) {

            for (var i = 0, l = tasks.length; i < l; i++) {
                this.tasks.set(
                    tasks[i].get('uuid'),
                    tasks[i]
                );
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .append(tasks[i].get());
            }

        }
    },

    newAction: {
        value: function (event) {

            if (event.keyCode === 13 && event.target.value.trim() !== '') {
                var t = this.new('Todo::Task', {});
                t.set({
                    name: event.target.value,
                    status: t.ACTIVE
                });
                this.repo.persist(t);
                this.repo.flush();
                this.tasks.set(
                    t.get('uuid'),
                    t
                );
                event.target.value = null;
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .append(t.get());
            }

        }
    },

    listChangesAction: {
        value: function (event) {

            var n = event.target;

            event.preventDefault();

            if (event.type === 'change' && n.classList.contains('toggle')) {
                this.toggleStatus(n);
            } else if (event.type === 'click') {
                if (n.classList.contains('destroy')) {
                    this.removeTask(n);
                }
            }

        }
    },

    toggleStatus: {
        value: function (n) {

            var t = this.tasks.get(n.dataset.uuid);

            event.preventDefault();

            if (n.checked) {
                t.set('status', t.COMPLETED);
            } else {
                t.set('status', t.ACTIVE);
            }

            this.templating.render(
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .findOne('li[data-uuid="' + t.get('uuid') + '"]'),
                t.get()
            );

        }
    },

    removeTask: {
        value: function (n) {

            var t = this.tasks.get(n.dataset.uuid),
                list = this.viewscreen.getLayout('main').getList('tasks'),
                node = list.findOne('li[data-uuid="' + t.get('uuid') + '"]');

            this.tasks.remove(t.get('uuid'));
            this.repo.remove(t);
            list.getNode().removeChild(node);

        }
    }

});<commit_msg>flush the repo when a task is removed<commit_after>namespace('App.Bundle.Todo.Controller');

App.Bundle.Todo.Controller.Main = function () {
    Sy.Controller.call(this);
    this.tasks = null;
    this.repo = null;
    this.templating = null;
};
App.Bundle.Todo.Controller.Main.prototype = Object.create(Sy.Controller.prototype, {

    init: {
        value: function () {
            this.tasks = new Sy.Registry();
            this.repo = this.container
                .get('sy::core::storage')
                .getManager()
                .getRepository('Todo::Task');
            this.templating = this.container
                .get('sy::core::view::template::engine');
            this.repo.findBy({
                index: 'uuid',
                value: [''],
                callback: this.loadPreviousTasks.bind(this)
            });
        }
    },

    loadPreviousTasks: {
        value: function (tasks) {

            for (var i = 0, l = tasks.length; i < l; i++) {
                this.tasks.set(
                    tasks[i].get('uuid'),
                    tasks[i]
                );
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .append(tasks[i].get());
            }

        }
    },

    newAction: {
        value: function (event) {

            if (event.keyCode === 13 && event.target.value.trim() !== '') {
                var t = this.new('Todo::Task', {});
                t.set({
                    name: event.target.value,
                    status: t.ACTIVE
                });
                this.repo.persist(t);
                this.repo.flush();
                this.tasks.set(
                    t.get('uuid'),
                    t
                );
                event.target.value = null;
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .append(t.get());
            }

        }
    },

    listChangesAction: {
        value: function (event) {

            var n = event.target;

            event.preventDefault();

            if (event.type === 'change' && n.classList.contains('toggle')) {
                this.toggleStatus(n);
            } else if (event.type === 'click') {
                if (n.classList.contains('destroy')) {
                    this.removeTask(n);
                }
            }

        }
    },

    toggleStatus: {
        value: function (n) {

            var t = this.tasks.get(n.dataset.uuid);

            event.preventDefault();

            if (n.checked) {
                t.set('status', t.COMPLETED);
            } else {
                t.set('status', t.ACTIVE);
            }

            this.templating.render(
                this.viewscreen
                    .getLayout('main')
                    .getList('tasks')
                    .findOne('li[data-uuid="' + t.get('uuid') + '"]'),
                t.get()
            );

        }
    },

    removeTask: {
        value: function (n) {

            var t = this.tasks.get(n.dataset.uuid),
                list = this.viewscreen.getLayout('main').getList('tasks'),
                node = list.findOne('li[data-uuid="' + t.get('uuid') + '"]');

            this.tasks.remove(t.get('uuid'));
            this.repo.remove(t);
            this.repo.flush();
            list.getNode().removeChild(node);

        }
    }

});<|endoftext|>


<commit_before>import Turbolinks from 'turbolinks'
import { AUTOCOMPLETE_DEFAULTS } from 'lib/shared'
import 'devbridge-autocomplete'

$(document).on('turbolinks:load', function() {
  var $input = $('input#gobierto_search:visible');

  function truncateOnWord(str, limit) {
    var trimmable = '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF';
    var reg = new RegExp('(?=[' + trimmable + '])');
    var words = str.split(reg);
    var count = 0;
    return words.filter(function(word) {
      count += word.length;
      return count <= limit;
    }).join('');
  }

  function itemUpdatedAt(d){
    if (d['searchable_updated_at'] == null)
      return;

    return `${I18n.t("layouts.search.updated")}: ${I18n.l("date.formats.short", d['searchable_updated_at'])}`;
  }

  function itemDescription(d){
    var maxLength = 200;
    var description = (d['description'] || '');
    if (description === '' || description.length < maxLength)
      return description;
    return truncateOnWord(description, maxLength) + '...';
  }

  function itemType(d){
    let class_name = d['class_name'] || d['searchable_type']
    if (class_name === undefined)
      return;

    switch (class_name){
      case 'GobiertoPeople::Person':
        return I18n.t("layouts.search.person_item");
      case 'GobiertoPeople::PersonPost':
        return I18n.t("layouts.search.person_post_item");
      case 'GobiertoCalendars::Event':
        return I18n.t("layouts.search.event_item");
      case 'GobiertoParticipation::Contribution':
        return I18n.t("layouts.search.contribution_item");
      case 'GobiertoParticipation::Process':
        return I18n.t("layouts.search.process_item");
      case 'GobiertoPeople::PersonStatement':
        return I18n.t("layouts.search.person_statement_item");
      case 'GobiertoCms::Page':
        return I18n.t("layouts.search.page_item");
      case 'GobiertoBudgets::BudgetLine':
        if (d["meta"] != null && d['meta']['kind'] === 'I') {
          return I18n.t("layouts.search.budget_line_item_income");
        } else {
          return I18n.t("layouts.search.budget_line_item_expense");
        }
      case 'GobiertoCitizensCharters::Charter':
        return I18n.t("layouts.search.charter");
      case 'GobiertoCitizensCharters::Commitment':
        return I18n.t("layouts.search.charter_commitment");
      case 'GobiertoPlans::Node':
        return I18n.t("layouts.search.plan_project_item");
    }
  }

  function formattedResult(d){
    return `<h2>${d['title']}</h2>` +
           '
' +
             `<div>${itemDescription(d)}</div>` +
             `<span class="soft item_type">${itemType(d)}</span>` +
             (itemUpdatedAt(d) ? ` · <span class="soft updated_at">${itemUpdatedAt(d)}</span>` : '') +
           '
'
  }

  var searchOptions = {
    serviceUrl: '/api/v1/search',
    dataType: 'json',
    params: window.searchClient === undefined ?
      {} :
      {
        filters:
          {
            searchable_type: window.searchClient.searchable_types
          }
      },
    appendTo: $input.parent(),
    beforeRender(container) {
      container.css({ width: '100%' })
    },
    onSelect: function(suggestion){
      Turbolinks.visit(suggestion.data.resource_path);
    },
    transformResult: function(response) {
      return {
        suggestions: response.data.map(item => ({
          value: item.attributes.title,
          data: item.attributes
        }))
      }
    },
    formatResult: function(suggestion){
      return formattedResult(suggestion.data)
    },
    noSuggestionNotice: `<div class="result"><p>${ I18n.t("layouts.search.no_results") }</p></div>`
  };

  $input.devbridgeAutocomplete($.extend({}, AUTOCOMPLETE_DEFAULTS, searchOptions));
});
<commit_msg>Disable turbolinks in search box results<commit_after>import Turbolinks from 'turbolinks'
import { AUTOCOMPLETE_DEFAULTS } from 'lib/shared'
import 'devbridge-autocomplete'

$(document).on('turbolinks:load', function() {
  var $input = $('input#gobierto_search:visible');

  function truncateOnWord(str, limit) {
    var trimmable = '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF';
    var reg = new RegExp('(?=[' + trimmable + '])');
    var words = str.split(reg);
    var count = 0;
    return words.filter(function(word) {
      count += word.length;
      return count <= limit;
    }).join('');
  }

  function itemUpdatedAt(d){
    if (d['searchable_updated_at'] == null)
      return;

    return `${I18n.t("layouts.search.updated")}: ${I18n.l("date.formats.short", d['searchable_updated_at'])}`;
  }

  function itemDescription(d){
    var maxLength = 200;
    var description = (d['description'] || '');
    if (description === '' || description.length < maxLength)
      return description;
    return truncateOnWord(description, maxLength) + '...';
  }

  function itemType(d){
    let class_name = d['class_name'] || d['searchable_type']
    if (class_name === undefined)
      return;

    switch (class_name){
      case 'GobiertoPeople::Person':
        return I18n.t("layouts.search.person_item");
      case 'GobiertoPeople::PersonPost':
        return I18n.t("layouts.search.person_post_item");
      case 'GobiertoCalendars::Event':
        return I18n.t("layouts.search.event_item");
      case 'GobiertoParticipation::Contribution':
        return I18n.t("layouts.search.contribution_item");
      case 'GobiertoParticipation::Process':
        return I18n.t("layouts.search.process_item");
      case 'GobiertoPeople::PersonStatement':
        return I18n.t("layouts.search.person_statement_item");
      case 'GobiertoCms::Page':
        return I18n.t("layouts.search.page_item");
      case 'GobiertoBudgets::BudgetLine':
        if (d["meta"] != null && d['meta']['kind'] === 'I') {
          return I18n.t("layouts.search.budget_line_item_income");
        } else {
          return I18n.t("layouts.search.budget_line_item_expense");
        }
      case 'GobiertoCitizensCharters::Charter':
        return I18n.t("layouts.search.charter");
      case 'GobiertoCitizensCharters::Commitment':
        return I18n.t("layouts.search.charter_commitment");
      case 'GobiertoPlans::Node':
        return I18n.t("layouts.search.plan_project_item");
    }
  }

  function formattedResult(d){
    return `<a class="result" <a href="${d['resource_path']}" data-turbolinks="false">` +
             `<h2>${d['title']}</h2>` +
             '
' +
               `<div>${itemDescription(d)}</div>` +
               `<span class="soft item_type">${itemType(d)}</span>` +
               (itemUpdatedAt(d) ? ` · <span class="soft updated_at">${itemUpdatedAt(d)}</span>` : '') +
             '
' +
           ''
  }

  var searchOptions = {
    serviceUrl: '/api/v1/search',
    dataType: 'json',
    params: window.searchClient === undefined ?
      {} :
      {
        filters:
          {
            searchable_type: window.searchClient.searchable_types
          }
      },
    appendTo: $input.parent(),
    beforeRender(container) {
      container.css({ width: '100%' })
    },
    transformResult: function(response) {
      return {
        suggestions: response.data.map(item => ({
          value: item.attributes.title,
          data: item.attributes
        }))
      }
    },
    formatResult: function(suggestion){
      return formattedResult(suggestion.data)
    },
    noSuggestionNotice: `<div class="result"><p>${ I18n.t("layouts.search.no_results") }</p></div>`
  };

  $input.devbridgeAutocomplete($.extend({}, AUTOCOMPLETE_DEFAULTS, searchOptions));
});
<|endoftext|>