<commit_before>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Field } from 'react-final-form';
import setFieldData from 'final-form-set-field-data';

import stripesFinalForm from '@folio/stripes/final-form';
import SafeHTMLMessage from '@folio/react-intl-safe-html';
import {
  Row,
  Col,
  Button,
  TextArea,
  Modal,
  NoValue,
  TextField,
  Checkbox,
  Select,
} from '@folio/stripes/components';

import { calculateSelectedAmount } from '../accountFunctions';

import css from './PayWaive.css';

class ActionModal extends React.Component {
  static propTypes = {
    form: PropTypes.object.isRequired,
    onClose: PropTypes.func,
    handleSubmit: PropTypes.func,
    open: PropTypes.bool,
    accounts: PropTypes.arrayOf(PropTypes.object),
    feeFineActions: PropTypes.arrayOf(PropTypes.object),
    data: PropTypes.arrayOf(PropTypes.object),
    balance: PropTypes.string,
    totalPaidAmount: PropTypes.string,
    owedAmount: PropTypes.string,
    submitting: PropTypes.bool,
    pristine: PropTypes.bool,
    reset: PropTypes.func,
    commentRequired: PropTypes.bool,
    owners: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    action: PropTypes.string,
    intl: PropTypes.object.isRequired,
    checkAmount: PropTypes.string,
    okapi: PropTypes.object,
    initialValues: PropTypes.object,
  };

  static defaultProps = {
    totalPaidAmount: '',
    owedAmount: '',
    feeFineActions: [],
  }

  constructor(props) {
    super(props);

    this.state = {
      actionAllowed: false,
      accountRemainingAmount: '0.00',
      prevValidatedAmount: null,
      prevValidationError: '',
    };

    this._isMounted = false;
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  onClose = () => {
    const { onClose, form: { reset } } = this.props;
    this._isMounted = false;

    onClose();
    reset();
  }

  renderModalLabel() {
    const {
      accounts = [],
      feeFineActions = [],
      action,
      form: { getState },
      intl: { formatMessage },
    } = this.props;

    const { values: { amount } } = getState();
    const selected = calculateSelectedAmount(accounts, this.isRefundAction(action), feeFineActions);
    const type = parseFloat(amount) < parseFloat(selected)
      ? formatMessage({ id: `ui-users.accounts.${action}.summary.partially` })
      : formatMessage({ id: `ui-users.accounts.${action}.summary.fully` });

    return (
      <SafeHTMLMessage
        id="ui-users.accounts.summary"
        values={{
          count: accounts.length,
          amount,
          type,
        }}
      />
    );
  }

  renderTransactionInfo = () => {
    const { accounts } = this.props;
    return (
      <Col xs={4}>
        {accounts.length > 1 ?
 : ''}
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.field.transactionInfo" />
          </Col>
        </Row>
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.transaction.placeholder">
              {placeholder => (
                <Field
                  id="transaction"
                  name="transaction"
                  component={TextField}
                  placeholder={placeholder}
                />
              )}
            </FormattedMessage>
          </Col>
        </Row>
      </Col>
    );
  }

  renderMethod = (options) => {
    const {
      action,
      intl: { formatMessage }
    } = this.props;
    return (
      <Col xs={this.isPaymentAction(action) ? 3 : 7}>
        <Row>
          <Col xs>
            <FormattedMessage id={`ui-users.accounts.${action}.method`} />
            *
          </Col>
        </Row>
        <Row>
          <Col xs id="action-selection">
            <Field
              name="method"
              component={Select}
              dataOptions={options}
              placeholder={formatMessage({ id: `ui-users.accounts.${action}.method.placeholder` })}
              validate={this.validateMethod}
            />
          </Col>
        </Row>
      </Col>
    );
  }

  isPaymentAction = (action) => {
    return action === 'payment';
  }

  isRefundAction = (action) => {
    return action === 'refund';
  }

  onChangeOwner = ({ target: { value } }) => {
    const { change } = this.props.form;

    change('ownerId', value);
    change('method', null);
  }

  singleItemCheck = (amount, accountId) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    return fetch(`${okapi.url}/accounts/${accountId}/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ amount })
      });
  };

  multipleItemsCheck = (amount, accounts) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    const accountIds = accounts.reduce((ids, account) => {
      ids.push(account.id);
      return ids;
    }, []);

    return fetch(`${okapi.url}/accounts-bulk/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ accountIds, amount })
      });
  };

  triggerCheckEndpoint = (amount, accounts) => {
    if (accounts.length === 1) {
      const { id } = _.head(accounts) || {};
      return this.singleItemCheck(amount, id);
    }

    return this.multipleItemsCheck(amount, accounts);
  };

  validateAmount = async (value) => {
    let error;

    const { accounts } = this.props;

    const {
      actionAllowed,
      accountRemainingAmount,
      prevValidatedAmount,
      prevValidationError,
    } = this.state;

    if (_.isEmpty(value)) {
      error = <FormattedMessage id="ui-users.accounts.error.field" />;
    } else if (value !== prevValidatedAmount && this._isMounted) {
      const response = await this.triggerCheckEndpoint(value, accounts);
      const {
        allowed,
        errorMessage,
        remainingAmount,
      } = await response.json();

      this.setState({ prevValidatedAmount: value });

      if (!_.isUndefined(errorMessage)) {
        this.setState({ prevValidationError: errorMessage });
        error = errorMessage;
      }

      if (actionAllowed !== allowed || accountRemainingAmount !== remainingAmount) {
        this.setState({
          actionAllowed: allowed,
          accountRemainingAmount: remainingAmount,
          prevValidationError: errorMessage,
        });
      }
    } else {
      error = prevValidationError;
    }

    return error;
  }

  validateMethod = (value) => {
    let error;
    const { action } = this.props;

    if (!value) {
      error = <FormattedMessage id={`ui-users.accounts.${action}.error.select`} />;
    }

    return error;
  }

  validateComment = (value) => {
    let error;
    const { commentRequired } = this.props;

    if (commentRequired && !value) {
      error = <FormattedMessage id="ui-users.accounts.error.comment" />;
    }

    return error;
  }

  render() {
    const {
      accounts,
      feeFineActions,
      action,
      balance,
      initialValues,
      totalPaidAmount,
      owedAmount,
      commentRequired,
      form: { getState },
      intl: { formatMessage },
      data,
      handleSubmit,
      label,
      open,
      owners,
      pristine,
      submitting,
    } = this.props;
    const { accountRemainingAmount } = this.state;

    const {
      valid,
      values: {
        notify,
        ownerId,
      }
    } = getState();

    const selected = calculateSelectedAmount(accounts, this.isRefundAction(action), feeFineActions);
    const ownerOptions = owners.filter(o => o.owner !== 'Shared').map(o => ({ value: o.id, label: o.owner }));

    let options = (this.isPaymentAction(action)) ? data.filter(d => (d.ownerId === (accounts.length > 1 ? ownerId : (accounts[0] || {}).ownerId))) : data;
    options = _.uniqBy(options.map(o => ({ id: o.id, label: o[label] })), 'label');

    const showNotify = initialValues.notify;

    return (
      <Modal
        data-test-fee-fine-action-modal
        id={`${action}-modal`}
        open={open}
        label={<FormattedMessage id={`ui-users.accounts.${action}.modalLabel`} />}
        onClose={this.onClose}
        size="medium"
        dismissible
      >
        <form onSubmit={handleSubmit}>
          <Row>
            <Col xs>{this.renderModalLabel()}</Col>
          </Row>

          <Row>
            <Col xs={5}>
              { this.isRefundAction(action) ? (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.totalPaid" />
                    :
                  </Col>
                  <Col xs={4}>
                    {totalPaidAmount}
                  </Col>
                </Row>
              ) : (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.totalOwed" />
                    :
                  </Col>
                  <Col xs={4}>
                    {balance}
                  </Col>
                </Row>
              ) }
              <Row end="xs">
                <Col xs={7}>
                  <FormattedMessage id="ui-users.accounts.selectedAmount" />
                  :
                </Col>
                <Col xs={4}>
                  {selected}
                </Col>
              </Row>
              <Row end="xs">
                <Col xs={7}>

                    <FormattedMessage id={`ui-users.accounts.${action}.amount`} />
                    *:

                </Col>
                <Col xs={4} className={css.customCol}>

                    <Field
                      id="amount"
                      name="amount"
                      component={TextField}
                      hasClearIcon={false}
                      fullWidth
                      marginBottom0
                      autoFocus
                      required
                      validate={this.validateAmount}
                    />

                </Col>
              </Row>
              <Row end="xs">
                <Col xs={7}>
                  <FormattedMessage id="ui-users.accounts.remainingAmount" />
                  :
                </Col>
                <Col xs={4}>
                  { accountRemainingAmount || <NoValue /> }
                </Col>
              </Row>
              { this.isRefundAction(action) && (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.otherOwed" />
                    :
                  </Col>
                  <Col xs={4}>
                    {owedAmount}
                  </Col>
                </Row>
              )}
            </Col>
            {(this.isPaymentAction(action) && accounts.length > 1) &&
              <Col xs={4}>
                <Row>
                  <Col xs>
                    <FormattedMessage id="ui-users.accounts.payment.field.ownerDesk" />
                    *
                  </Col>
                </Row>
                <Row>
                  <Col
                    xs
                    data-test-payment-owner
                  >
                    <Field
                      id="ownerId"
                      name="ownerId"
                      component={Select}
                      dataOptions={ownerOptions}
                      placeholder={formatMessage({ id: 'ui-users.accounts.payment.owner.placeholder' })}
                      onChange={this.onChangeOwner}
                      value={ownerId}
                    />
                  </Col>
                </Row>
              </Col>
            }
            {this.renderMethod(options)}
            {this.isPaymentAction(action) && this.renderTransactionInfo()}
          </Row>

          {(action === 'paymany' && accounts.length > 1) &&
            <Row>
              {this.renderTransactionInfo()}
            </Row>
          }
          <Row>
            <Col xs>
              <FormattedMessage id="ui-users.accounts.commentStaff" />
              {(commentRequired) ? '*' : ''}
            </Col>
          </Row>

          <Row>
            <Col xs>
              <Field
                id="comments"
                name="comment"
                component={TextArea}
                validate={this.validateComment}
              />
            </Col>
          </Row>
          {showNotify &&

              <Row>
                <Col xs>
                  <Field
                    id="notify"
                    name="notify"
                    component={Checkbox}
                    type="checkbox"
                    inline
                    label={<FormattedMessage id="ui-users.accounts.notifyPatron" />}
                  />
                </Col>
              </Row>

          }

          {notify && showNotify &&

              <Row>
                <Col xs>
                  <FormattedMessage id="ui-users.accounts.infoPatron" />
                </Col>
              </Row>

              <Row>
                <Col xs>
                  <Field
                    id="patronInfo"
                    name="patronInfo"
                    component={TextArea}
                  />
                </Col>
              </Row>

          }
          <Row end="xs">
            <Col xs>
              <Button
                id="cancel-button"
                onClick={this.onClose}
              >
                <FormattedMessage id="ui-users.cancel" />
              </Button>
              <Button
                id="submit-button"
                buttonStyle="primary"
                type="submit"
                disabled={pristine || submitting || !valid || !this.state.actionAllowed}
              >
                <FormattedMessage id={`ui-users.accounts.${action}`} />
              </Button>
            </Col>
          </Row>
        </form>
      </Modal>
    );
  }
}

export default stripesFinalForm({
  initialValuesEqual: (a, b) => _.isEqual(a, b),
  navigationCheck: true,
  subscription: { values: true },
  mutators: { setFieldData },
})(ActionModal);
<commit_msg>UIU-1897: Clean up<commit_after>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Field } from 'react-final-form';
import setFieldData from 'final-form-set-field-data';

import stripesFinalForm from '@folio/stripes/final-form';
import SafeHTMLMessage from '@folio/react-intl-safe-html';
import {
  Row,
  Col,
  Button,
  TextArea,
  Modal,
  NoValue,
  TextField,
  Checkbox,
  Select,
} from '@folio/stripes/components';

import { calculateSelectedAmount } from '../accountFunctions';

import css from './PayWaive.css';

class ActionModal extends React.Component {
  static propTypes = {
    form: PropTypes.object.isRequired,
    onClose: PropTypes.func,
    handleSubmit: PropTypes.func,
    open: PropTypes.bool,
    accounts: PropTypes.arrayOf(PropTypes.object),
    feeFineActions: PropTypes.arrayOf(PropTypes.object),
    data: PropTypes.arrayOf(PropTypes.object),
    balance: PropTypes.string,
    totalPaidAmount: PropTypes.string,
    owedAmount: PropTypes.string,
    submitting: PropTypes.bool,
    pristine: PropTypes.bool,
    reset: PropTypes.func,
    commentRequired: PropTypes.bool,
    owners: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    action: PropTypes.string,
    intl: PropTypes.object.isRequired,
    checkAmount: PropTypes.string,
    okapi: PropTypes.object,
    initialValues: PropTypes.object,
  };

  static defaultProps = {
    totalPaidAmount: '',
    owedAmount: '',
  }

  constructor(props) {
    super(props);

    this.state = {
      actionAllowed: false,
      accountRemainingAmount: '0.00',
      prevValidatedAmount: null,
      prevValidationError: '',
    };

    this._isMounted = false;
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  onClose = () => {
    const { onClose, form: { reset } } = this.props;
    this._isMounted = false;

    onClose();
    reset();
  }

  renderModalLabel() {
    const {
      accounts = [],
      feeFineActions = [],
      action,
      form: { getState },
      intl: { formatMessage },
    } = this.props;

    const { values: { amount } } = getState();
    const selected = calculateSelectedAmount(accounts, this.isRefundAction(action), feeFineActions);
    const type = parseFloat(amount) < parseFloat(selected)
      ? formatMessage({ id: `ui-users.accounts.${action}.summary.partially` })
      : formatMessage({ id: `ui-users.accounts.${action}.summary.fully` });

    return (
      <SafeHTMLMessage
        id="ui-users.accounts.summary"
        values={{
          count: accounts.length,
          amount,
          type,
        }}
      />
    );
  }

  renderTransactionInfo = () => {
    const { accounts } = this.props;
    return (
      <Col xs={4}>
        {accounts.length > 1 ?
 : ''}
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.field.transactionInfo" />
          </Col>
        </Row>
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.transaction.placeholder">
              {placeholder => (
                <Field
                  id="transaction"
                  name="transaction"
                  component={TextField}
                  placeholder={placeholder}
                />
              )}
            </FormattedMessage>
          </Col>
        </Row>
      </Col>
    );
  }

  renderMethod = (options) => {
    const {
      action,
      intl: { formatMessage }
    } = this.props;
    return (
      <Col xs={this.isPaymentAction(action) ? 3 : 7}>
        <Row>
          <Col xs>
            <FormattedMessage id={`ui-users.accounts.${action}.method`} />
            *
          </Col>
        </Row>
        <Row>
          <Col xs id="action-selection">
            <Field
              name="method"
              component={Select}
              dataOptions={options}
              placeholder={formatMessage({ id: `ui-users.accounts.${action}.method.placeholder` })}
              validate={this.validateMethod}
            />
          </Col>
        </Row>
      </Col>
    );
  }

  isPaymentAction = (action) => {
    return action === 'payment';
  }

  isRefundAction = (action) => {
    return action === 'refund';
  }

  onChangeOwner = ({ target: { value } }) => {
    const { change } = this.props.form;

    change('ownerId', value);
    change('method', null);
  }

  singleItemCheck = (amount, accountId) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    return fetch(`${okapi.url}/accounts/${accountId}/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ amount })
      });
  };

  multipleItemsCheck = (amount, accounts) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    const accountIds = accounts.reduce((ids, account) => {
      ids.push(account.id);
      return ids;
    }, []);

    return fetch(`${okapi.url}/accounts-bulk/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ accountIds, amount })
      });
  };

  triggerCheckEndpoint = (amount, accounts) => {
    if (accounts.length === 1) {
      const { id } = _.head(accounts) || {};
      return this.singleItemCheck(amount, id);
    }

    return this.multipleItemsCheck(amount, accounts);
  };

  validateAmount = async (value) => {
    let error;

    const { accounts } = this.props;

    const {
      actionAllowed,
      accountRemainingAmount,
      prevValidatedAmount,
      prevValidationError,
    } = this.state;

    if (_.isEmpty(value)) {
      error = <FormattedMessage id="ui-users.accounts.error.field" />;
    } else if (value !== prevValidatedAmount && this._isMounted) {
      const response = await this.triggerCheckEndpoint(value, accounts);
      const {
        allowed,
        errorMessage,
        remainingAmount,
      } = await response.json();

      this.setState({ prevValidatedAmount: value });

      if (!_.isUndefined(errorMessage)) {
        this.setState({ prevValidationError: errorMessage });
        error = errorMessage;
      }

      if (actionAllowed !== allowed || accountRemainingAmount !== remainingAmount) {
        this.setState({
          actionAllowed: allowed,
          accountRemainingAmount: remainingAmount,
          prevValidationError: errorMessage,
        });
      }
    } else {
      error = prevValidationError;
    }

    return error;
  }

  validateMethod = (value) => {
    let error;
    const { action } = this.props;

    if (!value) {
      error = <FormattedMessage id={`ui-users.accounts.${action}.error.select`} />;
    }

    return error;
  }

  validateComment = (value) => {
    let error;
    const { commentRequired } = this.props;

    if (commentRequired && !value) {
      error = <FormattedMessage id="ui-users.accounts.error.comment" />;
    }

    return error;
  }

  render() {
    const {
      accounts,
      feeFineActions,
      action,
      balance,
      initialValues,
      totalPaidAmount,
      owedAmount,
      commentRequired,
      form: { getState },
      intl: { formatMessage },
      data,
      handleSubmit,
      label,
      open,
      owners,
      pristine,
      submitting,
    } = this.props;
    const { accountRemainingAmount } = this.state;

    const {
      valid,
      values: {
        notify,
        ownerId,
      }
    } = getState();

    const selected = calculateSelectedAmount(accounts, this.isRefundAction(action), feeFineActions);
    const ownerOptions = owners.filter(o => o.owner !== 'Shared').map(o => ({ value: o.id, label: o.owner }));

    let options = (this.isPaymentAction(action)) ? data.filter(d => (d.ownerId === (accounts.length > 1 ? ownerId : (accounts[0] || {}).ownerId))) : data;
    options = _.uniqBy(options.map(o => ({ id: o.id, label: o[label] })), 'label');

    const showNotify = initialValues.notify;

    return (
      <Modal
        data-test-fee-fine-action-modal
        id={`${action}-modal`}
        open={open}
        label={<FormattedMessage id={`ui-users.accounts.${action}.modalLabel`} />}
        onClose={this.onClose}
        size="medium"
        dismissible
      >
        <form onSubmit={handleSubmit}>
          <Row>
            <Col xs>{this.renderModalLabel()}</Col>
          </Row>

          <Row>
            <Col xs={5}>
              { this.isRefundAction(action) ? (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.totalPaid" />
                    :
                  </Col>
                  <Col xs={4}>
                    {totalPaidAmount}
                  </Col>
                </Row>
              ) : (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.totalOwed" />
                    :
                  </Col>
                  <Col xs={4}>
                    {balance}
                  </Col>
                </Row>
              ) }
              <Row end="xs">
                <Col xs={7}>
                  <FormattedMessage id="ui-users.accounts.selectedAmount" />
                  :
                </Col>
                <Col xs={4}>
                  {selected}
                </Col>
              </Row>
              <Row end="xs">
                <Col xs={7}>

                    <FormattedMessage id={`ui-users.accounts.${action}.amount`} />
                    *:

                </Col>
                <Col xs={4} className={css.customCol}>

                    <Field
                      id="amount"
                      name="amount"
                      component={TextField}
                      hasClearIcon={false}
                      fullWidth
                      marginBottom0
                      autoFocus
                      required
                      validate={this.validateAmount}
                    />

                </Col>
              </Row>
              <Row end="xs">
                <Col xs={7}>
                  <FormattedMessage id="ui-users.accounts.remainingAmount" />
                  :
                </Col>
                <Col xs={4}>
                  { accountRemainingAmount || <NoValue /> }
                </Col>
              </Row>
              { this.isRefundAction(action) && (
                <Row end="xs">
                  <Col xs={7}>
                    <FormattedMessage id="ui-users.accounts.otherOwed" />
                    :
                  </Col>
                  <Col xs={4}>
                    {owedAmount}
                  </Col>
                </Row>
              )}
            </Col>
            {(this.isPaymentAction(action) && accounts.length > 1) &&
              <Col xs={4}>
                <Row>
                  <Col xs>
                    <FormattedMessage id="ui-users.accounts.payment.field.ownerDesk" />
                    *
                  </Col>
                </Row>
                <Row>
                  <Col
                    xs
                    data-test-payment-owner
                  >
                    <Field
                      id="ownerId"
                      name="ownerId"
                      component={Select}
                      dataOptions={ownerOptions}
                      placeholder={formatMessage({ id: 'ui-users.accounts.payment.owner.placeholder' })}
                      onChange={this.onChangeOwner}
                      value={ownerId}
                    />
                  </Col>
                </Row>
              </Col>
            }
            {this.renderMethod(options)}
            {this.isPaymentAction(action) && this.renderTransactionInfo()}
          </Row>

          {(action === 'paymany' && accounts.length > 1) &&
            <Row>
              {this.renderTransactionInfo()}
            </Row>
          }
          <Row>
            <Col xs>
              <FormattedMessage id="ui-users.accounts.commentStaff" />
              {(commentRequired) ? '*' : ''}
            </Col>
          </Row>

          <Row>
            <Col xs>
              <Field
                id="comments"
                name="comment"
                component={TextArea}
                validate={this.validateComment}
              />
            </Col>
          </Row>
          {showNotify &&

              <Row>
                <Col xs>
                  <Field
                    id="notify"
                    name="notify"
                    component={Checkbox}
                    type="checkbox"
                    inline
                    label={<FormattedMessage id="ui-users.accounts.notifyPatron" />}
                  />
                </Col>
              </Row>

          }

          {notify && showNotify &&

              <Row>
                <Col xs>
                  <FormattedMessage id="ui-users.accounts.infoPatron" />
                </Col>
              </Row>

              <Row>
                <Col xs>
                  <Field
                    id="patronInfo"
                    name="patronInfo"
                    component={TextArea}
                  />
                </Col>
              </Row>

          }
          <Row end="xs">
            <Col xs>
              <Button
                id="cancel-button"
                onClick={this.onClose}
              >
                <FormattedMessage id="ui-users.cancel" />
              </Button>
              <Button
                id="submit-button"
                buttonStyle="primary"
                type="submit"
                disabled={pristine || submitting || !valid || !this.state.actionAllowed}
              >
                <FormattedMessage id={`ui-users.accounts.${action}`} />
              </Button>
            </Col>
          </Row>
        </form>
      </Modal>
    );
  }
}

export default stripesFinalForm({
  initialValuesEqual: (a, b) => _.isEqual(a, b),
  navigationCheck: true,
  subscription: { values: true },
  mutators: { setFieldData },
})(ActionModal);
<|endoftext|>


<commit_before>import _ from 'underscore';
import React from 'react/addons';
import shell from 'shell';
import ContainerUtil from '../utils/ContainerUtil';
import containerActions from '../actions/ContainerActions';
import containerStore from '../stores/ContainerStore';
import metrics from '../utils/MetricsUtil';
import {webPorts} from '../utils/Util';

var ContainerSettingsPorts = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  getInitialState: function () {
    return {
      ports: ContainerUtil.ports(this.props.container)
    };
  },
  handleViewLink: function (url) {
    metrics.track('Opened In Browser', {
      from: 'settings'
    });
    shell.openExternal(url);
  },
  handleChangePort: function(key, e) {
    let ports = this.state.ports;
    let port = e.target.value;

    // save updated port
    ports[key] = _.extend(ports[key], {
      url: 'http://' + ports[key]['ip'] + ':' + port,
      port: port,
      error: null
    });

    // basic validation, if number is integer, if its in range, if there
    // is no collision with ports of other containers and also if there is no
    // collision with ports for current container
    const otherContainers = _.filter(_.values(containerStore.getState().containers), c => c.Name !== this.props.container.Name);
    const otherPorts = _.flatten(otherContainers.map(container => {
      return _.values(container.NetworkSettings.Ports).map(hosts => hosts.map(host => {return {port: host.HostPort, name: container.Name}}));
    })).reduce((prev, pair) => {
      prev[pair.port] = pair.name;
      return prev;
    }, {});

    const duplicates = _.filter(ports, (v, i) => {
      return (i != key && _.isEqual(v.port, port));
    });

    if (!port.match(/^[0-9]+$/g)) {
      ports[key].error = 'Needs to be an integer.';
    } else if (port <= 0 || port > 65535) {
      ports[key].error = 'Needs to be in range <1,65535>.';
    } else if (otherPorts[port]) {
      ports[key].error = 'Collision with port at container "'+ otherPorts[port] +'"';
    } else if (duplicates.length > 0) {
      ports[key].error = 'Collision with another port in this container.';
    } else if (port == 22 || port == 2376) {
      ports[key].error = 'Ports 22 and 2376 are reserved ports for Kitematic/Docker.';
    }
    this.setState({ports: ports});
  },
  handleSave: function() {
    containerActions.update(this.props.container.Name, {
      NetworkSettings: {
        Ports: _.reduce(this.state.ports, function(res, value, key) {
          res[key + '/tcp'] = [{
            HostIp: value.ip,
            HostPort: value.port,
          }];
          return res;
        }, {})
      }
    });
  },
  render: function () {
    if (!this.props.container) {
      return false;
    }
    var isUpdating = (this.props.container.State.Updating);
    var isValid = true;
    var ports = _.map(_.pairs(this.state.ports), pair => {
      var key = pair[0];
      var {ip, port, url, error} = pair[1];
      isValid = (error) ? false : isValid;
      let ipLink = (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) ? ({ip}):({ip});
      return (

          {key}

            {ipLink}:
            <input
              type="text"
              disabled={isUpdating}
              onChange={this.handleChangePort.bind(this, key)}
              defaultValue={port} />

          {error}

      );
    });
    return (

Configure Ports

                DOCKER PORT
                MAC IP:PORT

              {ports}

            Save

    );
  }
});

module.exports = ContainerSettingsPorts;
<commit_msg>Small grammar fix<commit_after>import _ from 'underscore';
import React from 'react/addons';
import shell from 'shell';
import ContainerUtil from '../utils/ContainerUtil';
import containerActions from '../actions/ContainerActions';
import containerStore from '../stores/ContainerStore';
import metrics from '../utils/MetricsUtil';
import {webPorts} from '../utils/Util';

var ContainerSettingsPorts = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  getInitialState: function () {
    return {
      ports: ContainerUtil.ports(this.props.container)
    };
  },
  handleViewLink: function (url) {
    metrics.track('Opened In Browser', {
      from: 'settings'
    });
    shell.openExternal(url);
  },
  handleChangePort: function(key, e) {
    let ports = this.state.ports;
    let port = e.target.value;

    // save updated port
    ports[key] = _.extend(ports[key], {
      url: 'http://' + ports[key]['ip'] + ':' + port,
      port: port,
      error: null
    });

    // basic validation, if number is integer, if its in range, if there
    // is no collision with ports of other containers and also if there is no
    // collision with ports for current container
    const otherContainers = _.filter(_.values(containerStore.getState().containers), c => c.Name !== this.props.container.Name);
    const otherPorts = _.flatten(otherContainers.map(container => {
      return _.values(container.NetworkSettings.Ports).map(hosts => hosts.map(host => {return {port: host.HostPort, name: container.Name}}));
    })).reduce((prev, pair) => {
      prev[pair.port] = pair.name;
      return prev;
    }, {});

    const duplicates = _.filter(ports, (v, i) => {
      return (i != key && _.isEqual(v.port, port));
    });

    if (!port.match(/^[0-9]+$/g)) {
      ports[key].error = 'Needs to be an integer.';
    } else if (port <= 0 || port > 65535) {
      ports[key].error = 'Needs to be in range <1,65535>.';
    } else if (otherPorts[port]) {
      ports[key].error = 'Collision with container "'+ otherPorts[port] +'"';
    } else if (duplicates.length > 0) {
      ports[key].error = 'Collision with another port in this container.';
    } else if (port == 22 || port == 2376) {
      ports[key].error = 'Ports 22 and 2376 are reserved ports for Kitematic/Docker.';
    }
    this.setState({ports: ports});
  },
  handleSave: function() {
    containerActions.update(this.props.container.Name, {
      NetworkSettings: {
        Ports: _.reduce(this.state.ports, function(res, value, key) {
          res[key + '/tcp'] = [{
            HostIp: value.ip,
            HostPort: value.port,
          }];
          return res;
        }, {})
      }
    });
  },
  render: function () {
    if (!this.props.container) {
      return false;
    }
    var isUpdating = (this.props.container.State.Updating);
    var isValid = true;
    var ports = _.map(_.pairs(this.state.ports), pair => {
      var key = pair[0];
      var {ip, port, url, error} = pair[1];
      isValid = (error) ? false : isValid;
      let ipLink = (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) ? ({ip}):({ip});
      return (

          {key}

            {ipLink}:
            <input
              type="text"
              disabled={isUpdating}
              onChange={this.handleChangePort.bind(this, key)}
              defaultValue={port} />

          {error}

      );
    });
    return (

Configure Ports

                DOCKER PORT
                MAC IP:PORT

              {ports}

            Save

    );
  }
});

module.exports = ContainerSettingsPorts;
<|endoftext|>


<commit_before>import * as THREE from 'three';
import threeLine2d from 'three-line-2d';
import threeLine2dShader from 'three-line-2d-shader';

import COLOR from 'configs/colors';
import pileColors from 'components/fragments/pile-colors';

export const ANIMATION = true;

export const ARRANGE_MEASURES = [];

export const CELL_SIZE = 6;

export const CELL_SIZE_HALF = CELL_SIZE / 2;

export const CELL_THRESHOLD = 0.0;  // only cells above are shown

export const CLICK_DELAY_TIME = 300;

export const DBL_CLICK_DELAY_TIME = 250;

/**
 * External config to initialize fragments.
 *
 * @description
 * The config is different from the visual state of the app.
 *
 * @type  {Object}
 */
export const CONFIG = {};

export const DIAGONAL_VALUE = 0.11;

export const DURATION = 250;

export const FONT_URL = 'src/assets/fonts/rubik-regular.json';

export const FPS = 25;

export const HIGHLIGHT_FRAME_LINE_WIDTH = 5;

export const LABEL_DIST = 60;

export const LABEL_WIDTH = 130;

export const LABEL_TEXT_SPEC = {
  size: 6,
  height: 1,
  curveSegments: 3
};

export const LASSO_IS_ROUND = true;

export const LINE = threeLine2d(THREE);

export const LINE_SHADER = threeLine2dShader(THREE);

export const LASSO_LINE_WIDTH = 2;

export const LASSO_MIN_MOVE = 0.033;

export const LASSO_SHADER = threeLine2dShader(THREE)({
  side: THREE.DoubleSide,
  diffuse: COLOR.PRIMARY,
  thickness: LASSO_LINE_WIDTH
});

export const LASSO_MATERIAL = new THREE.ShaderMaterial(LASSO_SHADER);

export const LETTER_SPACE = 6;

export const MARGIN_BOTTOM = 2;

export const MARGIN_LEFT = 2;

export const MARGIN_RIGHT = 2;

export const MARGIN_TOP = 2;

export const MATRIX_FRAME_ENCODING = null;

export const MATRIX_FRAME_THICKNESS = 1;

export const MATRIX_FRAME_THICKNESS_MAX = 10;

export const MATRIX_GAP_HORIZONTAL = 6;

export const MATRIX_GAP_VERTICAL = 6;

export const MATRIX_ORIENTATION_UNDEF = 0;

export const MATRIX_ORIENTATION_INITIAL = 1;

export const MATRIX_ORIENTATION_5_TO_3 = 2;

export const MATRIX_ORIENTATION_3_TO_5 = 3;

/**
 * Mean
 *
 * @type {Number}
 */
export const MODE_MEAN = 0;

/**
 * Mean average deviation
 *
 * @type {Number}
 */
export const MODE_MAD = 1;

/**
 * Standard deviaton
 *
 * @type {Number}
 */
export const MODE_STD = 2;

export const ORDER_DATA = 0;

export const ORDER_GLOBAL = 1;

export const ORDER_LOCAL = 2;

export const PILE_LABEL_HEIGHT = 10;

export const PILE_MENU_CLOSING_DELAY = 200;

export const PILES = {};

export const PILES_COLORS = {};

export const PILE_COLORS_CATEGORICAL = pileColors.categorical.length;

export const PREVIEW_SIZE = 2;

export const PREVIEW_MAX = 8;

export const SHADER_ATTRIBUTES = {
  customColor: { type: 'c', value: [] }
};

export const SHOW_MATRICES = 1000;

export const SHOW_SPECIAL_CELLS = false;

export const SVG_MENU_HEIGHT = 30;

export const TIMELINE_HEIGHT = 130;

/**
 * Three.js's WebGL config
 *
 * @type  {Object}
 */
export const WEB_GL_CONFIG = {
  alpha: true,
  antialias: true
};

export const Z_BASE = 1;

export const Z_DRAG = 2;

export const Z_HIGHLIGHT = 1.5;

export const Z_LASSO = 2;

export const Z_MENU = 4;

export const Z_STACK_PILE_TARGET = 2;

export const ZOOM_DELAY_TIME = 500;
<commit_msg>Increase thickness of default border<commit_after>import * as THREE from 'three';
import threeLine2d from 'three-line-2d';
import threeLine2dShader from 'three-line-2d-shader';

import COLOR from 'configs/colors';
import pileColors from 'components/fragments/pile-colors';

export const ANIMATION = true;

export const ARRANGE_MEASURES = [];

export const CELL_SIZE = 6;

export const CELL_SIZE_HALF = CELL_SIZE / 2;

export const CELL_THRESHOLD = 0.0;  // only cells above are shown

export const CLICK_DELAY_TIME = 300;

export const DBL_CLICK_DELAY_TIME = 250;

/**
 * External config to initialize fragments.
 *
 * @description
 * The config is different from the visual state of the app.
 *
 * @type  {Object}
 */
export const CONFIG = {};

export const DIAGONAL_VALUE = 0.11;

export const DURATION = 250;

export const FONT_URL = 'src/assets/fonts/rubik-regular.json';

export const FPS = 25;

export const HIGHLIGHT_FRAME_LINE_WIDTH = 5;

export const LABEL_DIST = 60;

export const LABEL_WIDTH = 130;

export const LABEL_TEXT_SPEC = {
  size: 6,
  height: 1,
  curveSegments: 3
};

export const LASSO_IS_ROUND = true;

export const LINE = threeLine2d(THREE);

export const LINE_SHADER = threeLine2dShader(THREE);

export const LASSO_LINE_WIDTH = 2;

export const LASSO_MIN_MOVE = 0.033;

export const LASSO_SHADER = threeLine2dShader(THREE)({
  side: THREE.DoubleSide,
  diffuse: COLOR.PRIMARY,
  thickness: LASSO_LINE_WIDTH
});

export const LASSO_MATERIAL = new THREE.ShaderMaterial(LASSO_SHADER);

export const LETTER_SPACE = 6;

export const MARGIN_BOTTOM = 2;

export const MARGIN_LEFT = 2;

export const MARGIN_RIGHT = 2;

export const MARGIN_TOP = 2;

export const MATRIX_FRAME_ENCODING = null;

export const MATRIX_FRAME_THICKNESS = 2;

export const MATRIX_FRAME_THICKNESS_MAX = 10;

export const MATRIX_GAP_HORIZONTAL = 6;

export const MATRIX_GAP_VERTICAL = 6;

export const MATRIX_ORIENTATION_UNDEF = 0;

export const MATRIX_ORIENTATION_INITIAL = 1;

export const MATRIX_ORIENTATION_5_TO_3 = 2;

export const MATRIX_ORIENTATION_3_TO_5 = 3;

/**
 * Mean
 *
 * @type {Number}
 */
export const MODE_MEAN = 0;

/**
 * Mean average deviation
 *
 * @type {Number}
 */
export const MODE_MAD = 1;

/**
 * Standard deviaton
 *
 * @type {Number}
 */
export const MODE_STD = 2;

export const ORDER_DATA = 0;

export const ORDER_GLOBAL = 1;

export const ORDER_LOCAL = 2;

export const PILE_LABEL_HEIGHT = 10;

export const PILE_MENU_CLOSING_DELAY = 200;

export const PILES = {};

export const PILES_COLORS = {};

export const PILE_COLORS_CATEGORICAL = pileColors.categorical.length;

export const PREVIEW_SIZE = 2;

export const PREVIEW_MAX = 8;

export const SHADER_ATTRIBUTES = {
  customColor: { type: 'c', value: [] }
};

export const SHOW_MATRICES = 1000;

export const SHOW_SPECIAL_CELLS = false;

export const SVG_MENU_HEIGHT = 30;

export const TIMELINE_HEIGHT = 130;

/**
 * Three.js's WebGL config
 *
 * @type  {Object}
 */
export const WEB_GL_CONFIG = {
  alpha: true,
  antialias: true
};

export const Z_BASE = 1;

export const Z_DRAG = 2;

export const Z_HIGHLIGHT = 1.5;

export const Z_LASSO = 2;

export const Z_MENU = 4;

export const Z_STACK_PILE_TARGET = 2;

export const ZOOM_DELAY_TIME = 500;
<|endoftext|>


<commit_before>// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.

var request = require('request');
	options = {
		url: 'https://lightwave.atlassian.net/rest/api/2/search?jql=assignee=test',
		headers: {
    	'Authorization': 'Basic amVubnkuc29uZzAxOlRlc3QxMjM='
  	}
	}
	request(options, function (error, response, body) {
	 if (!error && response.statusCode == 200) {
	   console.log(body) // Show the HTML for the Google homepage.
	 }
})<commit_msg>changed search query<commit_after>// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.

var request = require('request');
	options = {
		url: 'https://lightwave.atlassian.net/rest/api/2/search?jql=project%20%3D%20LIG%20AND%20fixVersion%20%3D%20"Version%202.0"',
		headers: {
    	'Authorization': 'Basic amVubnkuc29uZzAxOlRlc3QxMjM='
  	}
	}
	request(options, function (error, response, body) {
	 if (!error && response.statusCode == 200) {
	   console.log(JSON.parse(body).total) // Show the HTML for the Google homepage.
	 }
})<|endoftext|>


<commit_before>import { Container } from 'aurelia-framework';

import { EventAggregator } from 'aurelia-event-aggregator';

import States from 'services/states';

import { dispersePiles, trashPiles } from 'components/fragments/fragments-actions';

import {
  MODE_MEAN, MODE_TREND, MODE_VARIANCE, MODE_DIFFERENCE
} from 'components/fragments/fragments-defaults';

import COLORS from 'configs/colors';

const store = Container.instance.get(States).store;
const event = Container.instance.get(EventAggregator);

export const INSPECT = {
  name: 'Inspect',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'I',
  trigger (pile) {
    event.publish('decompose.fgm.inspectPile', { pile });
  },
  single: false,
  stackedPileOnly: true
};

export const DISPERSE = {
  name: 'Disperse',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'D',
  trigger (pile) {
    store.dispatch(dispersePiles([pile.id]));
  },
  single: false,
  stackedPileOnly: true
};

export const DIFFERENCE = {
  name: 'Difference',
  color: COLORS.WHITE,
  background: 0x666666,
  row: 0,
  shortCut: 'F',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_DIFFERENCE, pile });
  },
  triggerEvent: 'hover',
  single: false,
  stackedPileOnly: true
};

export const MEAN = {
  name: 'Mean',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'M',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_MEAN, pile });
  },
  triggerEvent: 'hover',
  single: false,
  stackedPileOnly: true
};

export const TRASH = {
  name: 'Trash',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'R',
  trigger (pile) {
    store.dispatch(trashPiles([pile.id]));
  },
  single: false
};

export const TREND = {
  name: 'Trend',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'T',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_TREND, pile });
  },
  triggerEvent: 'hover',
  single: false,
  stackedPileOnly: true
};

export const VARIANCE = {
  name: 'Variance',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'V',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_VARIANCE, pile });
  },
  triggerEvent: 'hover',
  single: false,
  stackedPileOnly: true
};

export default [
  INSPECT,
  DISPERSE,
  TRASH,
  MEAN,
  VARIANCE,
  TREND,
  DIFFERENCE
];
<commit_msg>Add recover to the pile menu<commit_after>import { Container } from 'aurelia-framework';

import { EventAggregator } from 'aurelia-event-aggregator';

import States from 'services/states';

import {
  dispersePiles,
  recoverPiles,
  trashPiles
} from 'components/fragments/fragments-actions';

import {
  MODE_MEAN, MODE_TREND, MODE_VARIANCE, MODE_DIFFERENCE
} from 'components/fragments/fragments-defaults';

import COLORS from 'configs/colors';

const store = Container.instance.get(States).store;
const event = Container.instance.get(EventAggregator);

export const INSPECT = {
  name: 'Inspect',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'I',
  trigger (pile) {
    event.publish('decompose.fgm.inspectPile', { pile });
  },
  stackedPileOnly: true
};

export const DISPERSE = {
  name: 'Disperse',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'D',
  trigger (pile) {
    store.dispatch(dispersePiles([pile.id]));
  },
  stackedPileOnly: true
};

export const DIFFERENCE = {
  name: 'Difference',
  color: COLORS.WHITE,
  background: 0x666666,
  row: 0,
  shortCut: 'F',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_DIFFERENCE, pile });
  },
  triggerEvent: 'hover',
  stackedPileOnly: true
};

export const MEAN = {
  name: 'Mean',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'M',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_MEAN, pile });
  },
  triggerEvent: 'hover',
  stackedPileOnly: true
};

export const RECOVER = {
  name: 'Recover',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'R',
  trigger (pile) {
    store.dispatch(recoverPiles([pile.id]));
  },
  trashedOnly: true
};

export const TRASH = {
  name: 'Trash',
  color: COLORS.WHITE,
  background: COLORS.BLACK,
  shortCut: 'R',
  trigger (pile) {
    store.dispatch(trashPiles([pile.id]));
  }
};

export const TREND = {
  name: 'Trend',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'T',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_TREND, pile });
  },
  triggerEvent: 'hover',
  stackedPileOnly: true
};

export const VARIANCE = {
  name: 'Variance',
  color: COLORS.WHITE,
  background: 0x666666,
  shortCut: 'V',
  trigger (pile) {
    event.publish('decompose.fgm.coverDispMode', { mode: MODE_VARIANCE, pile });
  },
  triggerEvent: 'hover',
  stackedPileOnly: true
};

export default [
  INSPECT,
  DISPERSE,
  TRASH,
  RECOVER,
  MEAN,
  VARIANCE,
  TREND,
  DIFFERENCE
];
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { get, map, size } from 'lodash/fp';
import styled from 'styled-components';

import {
  baseAppColor, colorDanger, colorSuccess, textColor
} from 'constants/colors';
import * as snippetActions from 'actions/snippets';
import { copyToClipboard, prepareFilesForUpdate } from 'utils/snippets';

import UtilityIcon from 'components/common/UtilityIcon';
import Input from 'components/common/Input';
import Anchor from 'components/common/Anchor';
import { defaultGistURL } from 'constants/config';
import ExternalLink from 'components/common/ExternalLink';

const SnippetHeaderWrapper = styled.div`
  display: flex;
  align-items: center;
  width: 100%;
`;

const Title = styled.div`
    flex: 1;
    text-align: left;
    padding: 1px 20px 1px 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    position: relative;
    &:after {
      content: "";
      width: 100px;
      height: 50px;
      position: absolute;
      top: 0;
      right: 0;
      background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(56%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 1)));
      background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 56%, rgba(255, 255, 255, 1) 100%);
    }
`;

const Description = styled.span`
  color: ${textColor};
`;

const Languages = styled.span`
  color: ${baseAppColor};
  border: 1px solid ${baseAppColor}
  font-size: 11px;
  padding: 1px 3px;
  border-radius: 2px;
  margin: 0 5px 0 0;
  vertical-align: middle;
  cursor: pointer;
`;

export class SnippetHeader extends React.Component {
  state = {
    showToolbox: true
  };

  toggleToolbox = () => {
    this.setState((prevState) => ({
      showToolbox: !prevState.showToolbox
    }));
  };

  toggleStar = (id, starred) => starred ? this.props.unsetStar(id) : this.props.setStar(id);

  deleteSnippet = (id) => this.props.deleteSnippet(id);

  prepareAndUpdateSnippet = () => {
    const snippet = get(this.props.match.params.id, this.props.snippets);

    this.props.updateSnippet(prepareFilesForUpdate(this.props.tempSnippet), snippet.id);
  };

  renderStarControl = () => {
    const snippet = get(this.props.match.params.id, this.props.snippets);
    const starred = get('star', snippet);
    const iconType = starred ? 'star-full' : 'star-empty';

    return (
      <UtilityIcon size={ 22 }
                   color={ baseAppColor }
                   onClick={ () => this.toggleStar(snippet.id, starred) }
                   type={ iconType }/>
    );
  };

  renderEditControls = () => {
    const {
      editSnippet, cancelEditSnippet, edit, match, snippets
    } = this.props;
    const snippet = get(match.params.id, snippets);

    if (edit) {
      return (
        <React.Fragment>
          <UtilityIcon size={ 22 } color={ colorSuccess } onClick={ () => this.prepareAndUpdateSnippet() } type="check"/>
          <UtilityIcon size={ 22 } color={ colorDanger } onClick={ () => cancelEditSnippet() } type="close"/>
        </React.Fragment>
      );
    }

    return (
      <UtilityIcon size={ 22 } color={ baseAppColor } onClick={ () => editSnippet(snippet.id) } type="edit"/>
    );
  };

  renderSnippetDescription = () => {
    const {
      edit, tempSnippet, snippets, match, updateTempSnippet
    } = this.props;
    const snippet = get(match.params.id, snippets);

    if (!edit) {
      return (
        <Description onMouseOver={ () => this.toggleToolbox() }
                     onMouseOut={ () => this.toggleToolbox() }
                     onBlur={ () => this.toggleToolbox() }
                     onFocus={ () => this.toggleToolbox() }
                     title={ get('description', snippet) }>
          {get('description', snippet)}
        </Description>
      );
    }

    return (
      <Input value={ get('description', tempSnippet) }
             onChange={ (event) => updateTempSnippet('description', event.target.value) }/>
    );
  };

  renderTitle = () => {
    const {
      snippets, match, searchByLanguages, searchByTags, edit
    } = this.props;
    const snippet = get(match.params.id, snippets);

    return (
      <Title>
        { !edit && map((language) => (
          <Languages key={ `${language}${snippet.id}` }
                     onClick={ () => searchByLanguages(language) }>
            { language }
          </Languages>
        ), get('languages', snippet)) }

        { this.renderSnippetDescription() }

        { !edit && map((tag) => (
          <Anchor key={ tag }
                  onClick={ () => searchByTags(tag) }>
            { tag }
          </Anchor>
        ), get('tags', snippet)) }
      </Title>
    );
  };

  render() {
    const {
      snippets, match
    } = this.props;
    const snippet = get(match.params.id, snippets);
    const openOnWebUrl = `${defaultGistURL}/${get('username', snippet)}/${get('id', snippet)}`;
    const httpCloneUrl = `git clone ${defaultGistURL}/${get('id', snippet)}.git`;
    const sshCloneUrl = `git clone git@gist.github.com:${get('id', snippet)}.git`;
    const openInGHDesktop = `x-github-client://openRepo/${defaultGistURL}/${get('id', snippet)}`;

    return (
      <SnippetHeaderWrapper>

        { this.renderTitle() }

        { this.state.showToolbox && (

            { this.renderEditControls() }
            <UtilityIcon title={ `${size(get('files', snippet))} File(s)` } size={ 22 } color={ baseAppColor } type="file"/>
            <UtilityIcon size={ 22 } color={ baseAppColor } type={ get('public', snippet) ? 'unlock' : 'lock' }/>
            <UtilityIcon size={ 22 } color={ colorDanger } onClick={ () => this.deleteSnippet(snippet.id) } type="delete"/>
            <UtilityIcon size={ 22 } color={ baseAppColor } type="chat"/>
            { this.renderStarControl(snippet) }
            <UtilityIcon size={ 22 } color={ baseAppColor } type="ellipsis" dropdown>

Edit

<ExternalLink href={ openOnWebUrl }>Open on web</ExternalLink>

Download

                  <Anchor onClick={ (event) => copyToClipboard(event, snippet.id) }>
                  Copy snippet ID to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, openOnWebUrl) }>
                    Copy Snippet URL to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, httpCloneUrl) }>
                    Copy HTTPS clone command to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, sshCloneUrl) }>
                    Copy SSH clone command to clipboard
                  </Anchor>

                  <ExternalLink href={ openInGHDesktop }>
                    Open in GitHub desktop
                  </ExternalLink>

                  <Anchor onClick={ () => this.deleteSnippet(snippet.id) }>
                  Delete
                  </Anchor>

            </UtilityIcon>

        ) }
      </SnippetHeaderWrapper>
    );
  }
}

const mapStateToProps = (state) => ({
  snippets: get(['snippets', 'snippets'], state),
  edit: get(['ui', 'snippets', 'edit'], state),
  tempSnippet: get(['snippets', 'edit'], state)
});

SnippetHeader.propTypes = {
  snippets: PropTypes.object,
  match: PropTypes.object,
  searchByLanguages: PropTypes.func,
  searchByTags: PropTypes.func,
  setStar: PropTypes.func,
  unsetStar: PropTypes.func,
  deleteSnippet: PropTypes.func,
  editSnippet: PropTypes.func,
  cancelEditSnippet: PropTypes.func,
  updateTempSnippet: PropTypes.func,
  updateSnippet: PropTypes.func,
  edit: PropTypes.bool,
  tempSnippet: PropTypes.object
};

export default connect(mapStateToProps, {
  searchByLanguages: snippetActions.filterSnippetsByLanguage,
  searchByTags: snippetActions.filterSnippetsByTags,
  setStar: snippetActions.starSnippet,
  unsetStar: snippetActions.unStarSnippet,
  deleteSnippet: snippetActions.deleteSnippet,
  editSnippet: snippetActions.editSnippet,
  cancelEditSnippet: snippetActions.cancelEditSnippet,
  updateTempSnippet: snippetActions.updateTempSnippet,
  updateSnippet: snippetActions.updateSnippet
})(SnippetHeader);
<commit_msg>FIX (snippet): sub-header styled syntax error [ci skip]<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { get, map, size } from 'lodash/fp';
import styled from 'styled-components';

import {
  baseAppColor, colorDanger, colorSuccess, textColor
} from 'constants/colors';
import * as snippetActions from 'actions/snippets';
import { copyToClipboard, prepareFilesForUpdate } from 'utils/snippets';

import UtilityIcon from 'components/common/UtilityIcon';
import Input from 'components/common/Input';
import Anchor from 'components/common/Anchor';
import { defaultGistURL } from 'constants/config';
import ExternalLink from 'components/common/ExternalLink';

const SnippetHeaderWrapper = styled.div`
  display: flex;
  align-items: center;
  width: 100%;
`;

const Title = styled.div`
    flex: 1;
    text-align: left;
    padding: 1px 20px 1px 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    position: relative;
    &:after {
      content: "";
      width: 100px;
      height: 50px;
      position: absolute;
      top: 0;
      right: 0;
      background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(56%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 1)));
      background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 56%, rgba(255, 255, 255, 1) 100%);
    }
`;

const Description = styled.span`
  color: ${textColor};
`;

const Languages = styled.span`
  color: ${baseAppColor};
  border: 1px solid ${baseAppColor};
  font-size: 11px;
  padding: 1px 3px;
  border-radius: 2px;
  margin: 0 5px 0 0;
  vertical-align: middle;
  cursor: pointer;
`;

export class SnippetHeader extends React.Component {
  state = {
    showToolbox: true
  };

  toggleToolbox = () => {
    this.setState((prevState) => ({
      showToolbox: !prevState.showToolbox
    }));
  };

  toggleStar = (id, starred) => starred ? this.props.unsetStar(id) : this.props.setStar(id);

  deleteSnippet = (id) => this.props.deleteSnippet(id);

  prepareAndUpdateSnippet = () => {
    const snippet = get(this.props.match.params.id, this.props.snippets);

    this.props.updateSnippet(prepareFilesForUpdate(this.props.tempSnippet), snippet.id);
  };

  renderStarControl = () => {
    const snippet = get(this.props.match.params.id, this.props.snippets);
    const starred = get('star', snippet);
    const iconType = starred ? 'star-full' : 'star-empty';

    return (
      <UtilityIcon size={ 22 }
                   color={ baseAppColor }
                   onClick={ () => this.toggleStar(snippet.id, starred) }
                   type={ iconType }/>
    );
  };

  renderEditControls = () => {
    const {
      editSnippet, cancelEditSnippet, edit, match, snippets
    } = this.props;
    const snippet = get(match.params.id, snippets);

    if (edit) {
      return (
        <React.Fragment>
          <UtilityIcon size={ 22 } color={ colorSuccess } onClick={ () => this.prepareAndUpdateSnippet() } type="check"/>
          <UtilityIcon size={ 22 } color={ colorDanger } onClick={ () => cancelEditSnippet() } type="close"/>
        </React.Fragment>
      );
    }

    return (
      <UtilityIcon size={ 22 } color={ baseAppColor } onClick={ () => editSnippet(snippet.id) } type="edit"/>
    );
  };

  renderSnippetDescription = () => {
    const {
      edit, tempSnippet, snippets, match, updateTempSnippet
    } = this.props;
    const snippet = get(match.params.id, snippets);

    if (!edit) {
      return (
        <Description onMouseOver={ () => this.toggleToolbox() }
                     onMouseOut={ () => this.toggleToolbox() }
                     onBlur={ () => this.toggleToolbox() }
                     onFocus={ () => this.toggleToolbox() }
                     title={ get('description', snippet) }>
          {get('description', snippet)}
        </Description>
      );
    }

    return (
      <Input value={ get('description', tempSnippet) }
             onChange={ (event) => updateTempSnippet('description', event.target.value) }/>
    );
  };

  renderTitle = () => {
    const {
      snippets, match, searchByLanguages, searchByTags, edit
    } = this.props;
    const snippet = get(match.params.id, snippets);

    return (
      <Title>
        { !edit && map((language) => (
          <Languages key={ `${language}${snippet.id}` }
                     onClick={ () => searchByLanguages(language) }>
            { language }
          </Languages>
        ), get('languages', snippet)) }

        { this.renderSnippetDescription() }

        { !edit && map((tag) => (
          <Anchor key={ tag }
                  onClick={ () => searchByTags(tag) }>
            { tag }
          </Anchor>
        ), get('tags', snippet)) }
      </Title>
    );
  };

  render() {
    const {
      snippets, match
    } = this.props;
    const snippet = get(match.params.id, snippets);
    const openOnWebUrl = `${defaultGistURL}/${get('username', snippet)}/${get('id', snippet)}`;
    const httpCloneUrl = `git clone ${defaultGistURL}/${get('id', snippet)}.git`;
    const sshCloneUrl = `git clone git@gist.github.com:${get('id', snippet)}.git`;
    const openInGHDesktop = `x-github-client://openRepo/${defaultGistURL}/${get('id', snippet)}`;

    return (
      <SnippetHeaderWrapper>

        { this.renderTitle() }

        { this.state.showToolbox && (

            { this.renderEditControls() }
            <UtilityIcon title={ `${size(get('files', snippet))} File(s)` } size={ 22 } color={ baseAppColor } type="file"/>
            <UtilityIcon size={ 22 } color={ baseAppColor } type={ get('public', snippet) ? 'unlock' : 'lock' }/>
            <UtilityIcon size={ 22 } color={ colorDanger } onClick={ () => this.deleteSnippet(snippet.id) } type="delete"/>
            <UtilityIcon size={ 22 } color={ baseAppColor } type="chat"/>
            { this.renderStarControl(snippet) }
            <UtilityIcon size={ 22 } color={ baseAppColor } type="ellipsis" dropdown>

Edit

<ExternalLink href={ openOnWebUrl }>Open on web</ExternalLink>

Download

                  <Anchor onClick={ (event) => copyToClipboard(event, snippet.id) }>
                  Copy snippet ID to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, openOnWebUrl) }>
                    Copy Snippet URL to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, httpCloneUrl) }>
                    Copy HTTPS clone command to clipboard
                  </Anchor>

                  <Anchor onClick={ (event) => copyToClipboard(event, sshCloneUrl) }>
                    Copy SSH clone command to clipboard
                  </Anchor>

                  <ExternalLink href={ openInGHDesktop }>
                    Open in GitHub desktop
                  </ExternalLink>

                  <Anchor onClick={ () => this.deleteSnippet(snippet.id) }>
                  Delete
                  </Anchor>

            </UtilityIcon>

        ) }
      </SnippetHeaderWrapper>
    );
  }
}

const mapStateToProps = (state) => ({
  snippets: get(['snippets', 'snippets'], state),
  edit: get(['ui', 'snippets', 'edit'], state),
  tempSnippet: get(['snippets', 'edit'], state)
});

SnippetHeader.propTypes = {
  snippets: PropTypes.object,
  match: PropTypes.object,
  searchByLanguages: PropTypes.func,
  searchByTags: PropTypes.func,
  setStar: PropTypes.func,
  unsetStar: PropTypes.func,
  deleteSnippet: PropTypes.func,
  editSnippet: PropTypes.func,
  cancelEditSnippet: PropTypes.func,
  updateTempSnippet: PropTypes.func,
  updateSnippet: PropTypes.func,
  edit: PropTypes.bool,
  tempSnippet: PropTypes.object
};

export default connect(mapStateToProps, {
  searchByLanguages: snippetActions.filterSnippetsByLanguage,
  searchByTags: snippetActions.filterSnippetsByTags,
  setStar: snippetActions.starSnippet,
  unsetStar: snippetActions.unStarSnippet,
  deleteSnippet: snippetActions.deleteSnippet,
  editSnippet: snippetActions.editSnippet,
  cancelEditSnippet: snippetActions.cancelEditSnippet,
  updateTempSnippet: snippetActions.updateTempSnippet,
  updateSnippet: snippetActions.updateSnippet
})(SnippetHeader);
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import { Helmet } from 'react-helmet';
import { FormattedMessage, injectIntl } from 'react-intl';
import { push } from 'react-router-redux';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import MenuItem from '@material-ui/core/MenuItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import { selectStory, fetchStory } from '../../../actions/storyActions';
import { fetchTopicStoryInfo } from '../../../actions/topicActions';
import withAsyncFetch from '../../common/hocs/AsyncContainer';
import StoryWordsContainer from './StoryWordsContainer';
import StoryInlinksContainer from './StoryInlinksContainer';
import StoryOutlinksContainer from './StoryOutlinksContainer';
import ActionMenu from '../../common/ActionMenu';
import StoryEntitiesContainer from '../../common/story/StoryEntitiesContainer';
import StoryNytThemesContainer from '../../common/story/StoryNytThemesContainer';
import { TAG_SET_GEOGRAPHIC_PLACES, TAG_SET_NYT_THEMES } from '../../../lib/tagUtil';
import StoryDetails from '../../common/story/StoryDetails';
import StoryPlaces from './StoryPlaces';
import messages from '../../../resources/messages';
import { EditButton, RemoveButton, ReadItNowButton } from '../../common/IconButton';
import ComingSoon from '../../common/ComingSoon';
import StoryIcon from '../../common/icons/StoryIcon';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_TOPIC_WRITE, PERMISSION_STORY_EDIT, PERMISSION_ADMIN } from '../../../lib/auth';
import StatBar from '../../common/statbar/StatBar';
import AppButton from '../../common/AppButton';
import { urlToTopicMapper } from '../../../lib/urlUtil';
import { filteredLinkTo } from '../../util/location';

const MAX_STORY_TITLE_LENGTH = 70; // story titles longer than this will be trimmed and ellipses added

const localMessages = {
  mainTitle: { id: 'story.details.mainTitle', defaultMessage: 'Story: {title} | {topicName} | Topic Manager | Media Cloud' },
  removeTitle: { id: 'story.details.remove', defaultMessage: 'Remove from Next Snapshot' },
  removeAbout: { id: 'story.details.remove.about', defaultMessage: 'If story is clearly not related to the Topic, or is messing up your analysis, you can remove it from the next Snapshot.  Be careful, because this means it won\'t show up anywhere on the new Snapshot you generate.' },
  unknownLanguage: { id: 'story.details.language.unknown', defaultMessage: 'Unknown' },
  toolStoryManagement: { id: 'story.details.manage', defaultMessage: 'Go To Story Management' },
  editStory: { id: 'story.details.edit', defaultMessage: 'Edit This Story' },
  readStory: { id: 'story.details.read', defaultMessage: 'Read at Original URL' },
  removeStory: { id: 'story.details.remove', defaultMessage: 'Remove From Topic' },
  readCachedCopy: { id: 'story.details.readCached', defaultMessage: 'Read Cached Text (admin only)' },
  viewCachedHtml: { id: 'story.details.viewCachedHtml', defaultMessage: 'View Cached HTML (admin only)' },
  storyOptions: { id: 'story.details.storyOptions', defaultMessage: 'Story Options' },
};

class StoryContainer extends React.Component {
  state = {
    open: false,
  };

  componentWillReceiveProps(nextProps) {
    if (nextProps.storiesId !== this.props.storiesId) {
      const { fetchData } = this.props;
      fetchData(nextProps.storiesId, nextProps.filters);
    }
  }

  handleRemoveClick = () => {
    this.setState({ open: true });
  };

  handleRemoveDialogClose = () => {
    this.setState({ open: false });
  };

  render() {
    const { storyInfo, topicStoryInfo, topicId, storiesId, topicName,
      handleStoryCachedTextClick, handleStoryEditClick, filters } = this.props;
    const { formatMessage, formatNumber } = this.props.intl;
    let displayTitle = storyInfo.title;
    if (storyInfo.title && storyInfo.title.length > MAX_STORY_TITLE_LENGTH) {
      displayTitle = `${storyInfo.title.substr(0, MAX_STORY_TITLE_LENGTH)}...`;
    }
    return (

        <Helmet><title>{formatMessage(localMessages.mainTitle, { title: displayTitle, topicName })}</title></Helmet>
        <Grid>
          <Row>
            <Col lg={12}>

                <ActionMenu actionTextMsg={localMessages.storyOptions}>
                  <MenuItem onClick={() => window.open(storyInfo.url, '_blank')}>
                    <ListItemText><FormattedMessage {...localMessages.readStory} /></ListItemText>
                    <ListItemIcon><ReadItNowButton /></ListItemIcon>
                  </MenuItem>
                  <Permissioned onlyTopic={PERMISSION_ADMIN}>
                    <MenuItem onClick={() => handleStoryCachedTextClick(topicId, storiesId, filters)}>
                      <ListItemText><FormattedMessage {...localMessages.readCachedCopy} /></ListItemText>
                    </MenuItem>
                    <MenuItem onClick={() => window.open(`/api/stories/${storyInfo.stories_id}/raw.html`, '_blank')}>
                      <ListItemText><FormattedMessage {...localMessages.viewCachedHtml} /></ListItemText>
                    </MenuItem>
                  </Permissioned>
                  <Permissioned onlyRole={PERMISSION_STORY_EDIT}>
                    <MenuItem onClick={() => handleStoryEditClick(storiesId)}>
                      <ListItemText><FormattedMessage {...localMessages.editStory} /></ListItemText>
                      <ListItemIcon><EditButton tooltip={formatMessage(localMessages.editStory)} /></ListItemIcon>
                    </MenuItem>
                  </Permissioned>
                  <Permissioned onlyTopic={PERMISSION_TOPIC_WRITE}>
                    <MenuItem onClick={this.handleRemoveClick}>
                      <ListItemText><FormattedMessage {...localMessages.removeStory} /></ListItemText>
                      <ListItemIcon><RemoveButton tooltip={formatMessage(localMessages.removeTitle)} /></ListItemIcon>
                    </MenuItem>
                  </Permissioned>
                </ActionMenu>
                <StoryIcon height={32} />
                {displayTitle}

              <Dialog
                modal={false}
                open={this.state.open}
                onClose={this.handleRemoveDialogClose}
                className="app-dialog"
              >
                <DialogTitle>
                  {formatMessage(localMessages.removeTitle)}
                </DialogTitle>
                <DialogContent>

<FormattedMessage {...localMessages.removeAbout} />

                  <ComingSoon />
                </DialogContent>
                <DialogActions>
                  <AppButton
                    label={formatMessage(messages.ok)}
                    primary
                    onTouchTap={this.handleRemoveDialogClose}
                  />
                </DialogActions>
              </Dialog>
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StatBar
                stats={[
                  { message: messages.mediaInlinks, data: formatNumber(topicStoryInfo.media_inlink_count) },
                  { message: messages.inlinks, data: formatNumber(topicStoryInfo.inlink_count) },
                  { message: messages.outlinks, data: formatNumber(topicStoryInfo.outlink_count) },
                  { message: messages.facebookShares, data: formatNumber(topicStoryInfo.facebook_share_count) },
                  { message: messages.language, data: storyInfo.language || formatMessage(localMessages.unknownLanguage) },
                ]}
                columnWidth={2}
              />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryInlinksContainer topicId={topicId} storiesId={storiesId} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryOutlinksContainer topicId={topicId} storiesId={storiesId} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryWordsContainer topicId={topicId} storiesId={storiesId} topicName={topicName} />
            </Col>
          </Row>
          <Row>
            <Col lg={6}>
              <StoryPlaces
                tags={storyInfo.story_tags ? storyInfo.story_tags.filter(t => t.tag_sets_id === TAG_SET_GEOGRAPHIC_PLACES) : []}
                geocoderVersion={storyInfo.geocoderVersion}
              />
            </Col>
            <Col lg={6}>
              <StoryNytThemesContainer
                storyId={storiesId}
                tags={storyInfo.story_tags ? storyInfo.story_tags.filter(t => t.tag_sets_id === TAG_SET_NYT_THEMES) : []}
              />
            </Col>
          </Row>
          <Row>
            <Col lg={6}>
              <StoryDetails mediaLink={urlToTopicMapper(topicId)} story={storyInfo} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryEntitiesContainer storyId={storiesId} />
            </Col>
          </Row>
        </Grid>

    );
  }
}

StoryContainer.propTypes = {
  // from context
  params: PropTypes.object.isRequired, // params from router
  intl: PropTypes.object.isRequired,
  // from parent
  // from dispatch
  asyncFetch: PropTypes.func.isRequired,
  fetchData: PropTypes.func.isRequired,
  handleStoryCachedTextClick: PropTypes.func.isRequired,
  handleStoryEditClick: PropTypes.func.isRequired,
  // from state
  topicStoryInfo: PropTypes.object.isRequired,
  storyInfo: PropTypes.object.isRequired,
  storiesId: PropTypes.number.isRequired,
  topicName: PropTypes.string.isRequired,
  topicId: PropTypes.number.isRequired,
  fetchStatus: PropTypes.string.isRequired,
  filters: PropTypes.object.isRequired,
};

const mapStateToProps = (state, ownProps) => ({
  fetchStatus: state.story.info.fetchStatus,
  filters: state.topics.selected.filters,
  storiesId: parseInt(ownProps.params.storiesId, 10),
  topicId: state.topics.selected.id,
  topicName: state.topics.selected.info.name,
  topicStoryInfo: state.topics.selected.story.info,
  storyInfo: state.story.info,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (storiesId, filters) => {
    dispatch(selectStory(storiesId));
    const q = {
      ...filters,
      id: ownProps.params.topicId,
    };
    dispatch(fetchStory(storiesId, q));
    dispatch(fetchTopicStoryInfo(ownProps.params.topicId, storiesId, filters));
  },
  handleStoryCachedTextClick: (topicId, storiesId, filters) => {
    dispatch(push(filteredLinkTo(`topics/${topicId}/stories/${storiesId}/cached`, filters)));
  },
  handleStoryEditClick: (storiesId) => {
    dispatch(push(`admin/story/${storiesId}/details`)); // to admin page
  },
});

function mergeProps(stateProps, dispatchProps, ownProps) {
  return Object.assign({}, stateProps, dispatchProps, ownProps, {
    asyncFetch: () => dispatchProps.fetchData(stateProps.storiesId, stateProps.filters),
  });
}

export default
injectIntl(
  connect(mapStateToProps, mapDispatchToProps, mergeProps)(
    withAsyncFetch(
      StoryContainer
    )
  )
);
<commit_msg>#1369 link from topics to admin story should be the edit page<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import { Helmet } from 'react-helmet';
import { FormattedMessage, injectIntl } from 'react-intl';
import { push } from 'react-router-redux';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import MenuItem from '@material-ui/core/MenuItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import { selectStory, fetchStory } from '../../../actions/storyActions';
import { fetchTopicStoryInfo } from '../../../actions/topicActions';
import withAsyncFetch from '../../common/hocs/AsyncContainer';
import StoryWordsContainer from './StoryWordsContainer';
import StoryInlinksContainer from './StoryInlinksContainer';
import StoryOutlinksContainer from './StoryOutlinksContainer';
import ActionMenu from '../../common/ActionMenu';
import StoryEntitiesContainer from '../../common/story/StoryEntitiesContainer';
import StoryNytThemesContainer from '../../common/story/StoryNytThemesContainer';
import { TAG_SET_GEOGRAPHIC_PLACES, TAG_SET_NYT_THEMES } from '../../../lib/tagUtil';
import StoryDetails from '../../common/story/StoryDetails';
import StoryPlaces from './StoryPlaces';
import messages from '../../../resources/messages';
import { EditButton, RemoveButton, ReadItNowButton } from '../../common/IconButton';
import ComingSoon from '../../common/ComingSoon';
import StoryIcon from '../../common/icons/StoryIcon';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_TOPIC_WRITE, PERMISSION_STORY_EDIT, PERMISSION_ADMIN } from '../../../lib/auth';
import StatBar from '../../common/statbar/StatBar';
import AppButton from '../../common/AppButton';
import { urlToTopicMapper } from '../../../lib/urlUtil';
import { filteredLinkTo } from '../../util/location';

const MAX_STORY_TITLE_LENGTH = 70; // story titles longer than this will be trimmed and ellipses added

const localMessages = {
  mainTitle: { id: 'story.details.mainTitle', defaultMessage: 'Story: {title} | {topicName} | Topic Manager | Media Cloud' },
  removeTitle: { id: 'story.details.remove', defaultMessage: 'Remove from Next Snapshot' },
  removeAbout: { id: 'story.details.remove.about', defaultMessage: 'If story is clearly not related to the Topic, or is messing up your analysis, you can remove it from the next Snapshot.  Be careful, because this means it won\'t show up anywhere on the new Snapshot you generate.' },
  unknownLanguage: { id: 'story.details.language.unknown', defaultMessage: 'Unknown' },
  toolStoryManagement: { id: 'story.details.manage', defaultMessage: 'Go To Story Management' },
  editStory: { id: 'story.details.edit', defaultMessage: 'Edit This Story' },
  readStory: { id: 'story.details.read', defaultMessage: 'Read at Original URL' },
  removeStory: { id: 'story.details.remove', defaultMessage: 'Remove From Topic' },
  readCachedCopy: { id: 'story.details.readCached', defaultMessage: 'Read Cached Text (admin only)' },
  viewCachedHtml: { id: 'story.details.viewCachedHtml', defaultMessage: 'View Cached HTML (admin only)' },
  storyOptions: { id: 'story.details.storyOptions', defaultMessage: 'Story Options' },
};

class StoryContainer extends React.Component {
  state = {
    open: false,
  };

  componentWillReceiveProps(nextProps) {
    if (nextProps.storiesId !== this.props.storiesId) {
      const { fetchData } = this.props;
      fetchData(nextProps.storiesId, nextProps.filters);
    }
  }

  handleRemoveClick = () => {
    this.setState({ open: true });
  };

  handleRemoveDialogClose = () => {
    this.setState({ open: false });
  };

  render() {
    const { storyInfo, topicStoryInfo, topicId, storiesId, topicName,
      handleStoryCachedTextClick, handleStoryEditClick, filters } = this.props;
    const { formatMessage, formatNumber } = this.props.intl;
    let displayTitle = storyInfo.title;
    if (storyInfo.title && storyInfo.title.length > MAX_STORY_TITLE_LENGTH) {
      displayTitle = `${storyInfo.title.substr(0, MAX_STORY_TITLE_LENGTH)}...`;
    }
    return (

        <Helmet><title>{formatMessage(localMessages.mainTitle, { title: displayTitle, topicName })}</title></Helmet>
        <Grid>
          <Row>
            <Col lg={12}>

                <ActionMenu actionTextMsg={localMessages.storyOptions}>
                  <MenuItem onClick={() => window.open(storyInfo.url, '_blank')}>
                    <ListItemText><FormattedMessage {...localMessages.readStory} /></ListItemText>
                    <ListItemIcon><ReadItNowButton /></ListItemIcon>
                  </MenuItem>
                  <Permissioned onlyTopic={PERMISSION_ADMIN}>
                    <MenuItem onClick={() => handleStoryCachedTextClick(topicId, storiesId, filters)}>
                      <ListItemText><FormattedMessage {...localMessages.readCachedCopy} /></ListItemText>
                    </MenuItem>
                    <MenuItem onClick={() => window.open(`/api/stories/${storyInfo.stories_id}/raw.html`, '_blank')}>
                      <ListItemText><FormattedMessage {...localMessages.viewCachedHtml} /></ListItemText>
                    </MenuItem>
                  </Permissioned>
                  <Permissioned onlyRole={PERMISSION_STORY_EDIT}>
                    <MenuItem onClick={() => handleStoryEditClick(storiesId)}>
                      <ListItemText><FormattedMessage {...localMessages.editStory} /></ListItemText>
                      <ListItemIcon><EditButton tooltip={formatMessage(localMessages.editStory)} /></ListItemIcon>
                    </MenuItem>
                  </Permissioned>
                  <Permissioned onlyTopic={PERMISSION_TOPIC_WRITE}>
                    <MenuItem onClick={this.handleRemoveClick}>
                      <ListItemText><FormattedMessage {...localMessages.removeStory} /></ListItemText>
                      <ListItemIcon><RemoveButton tooltip={formatMessage(localMessages.removeTitle)} /></ListItemIcon>
                    </MenuItem>
                  </Permissioned>
                </ActionMenu>
                <StoryIcon height={32} />
                {displayTitle}

              <Dialog
                modal={false}
                open={this.state.open}
                onClose={this.handleRemoveDialogClose}
                className="app-dialog"
              >
                <DialogTitle>
                  {formatMessage(localMessages.removeTitle)}
                </DialogTitle>
                <DialogContent>

<FormattedMessage {...localMessages.removeAbout} />

                  <ComingSoon />
                </DialogContent>
                <DialogActions>
                  <AppButton
                    label={formatMessage(messages.ok)}
                    primary
                    onTouchTap={this.handleRemoveDialogClose}
                  />
                </DialogActions>
              </Dialog>
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StatBar
                stats={[
                  { message: messages.mediaInlinks, data: formatNumber(topicStoryInfo.media_inlink_count) },
                  { message: messages.inlinks, data: formatNumber(topicStoryInfo.inlink_count) },
                  { message: messages.outlinks, data: formatNumber(topicStoryInfo.outlink_count) },
                  { message: messages.facebookShares, data: formatNumber(topicStoryInfo.facebook_share_count) },
                  { message: messages.language, data: storyInfo.language || formatMessage(localMessages.unknownLanguage) },
                ]}
                columnWidth={2}
              />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryInlinksContainer topicId={topicId} storiesId={storiesId} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryOutlinksContainer topicId={topicId} storiesId={storiesId} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryWordsContainer topicId={topicId} storiesId={storiesId} topicName={topicName} />
            </Col>
          </Row>
          <Row>
            <Col lg={6}>
              <StoryPlaces
                tags={storyInfo.story_tags ? storyInfo.story_tags.filter(t => t.tag_sets_id === TAG_SET_GEOGRAPHIC_PLACES) : []}
                geocoderVersion={storyInfo.geocoderVersion}
              />
            </Col>
            <Col lg={6}>
              <StoryNytThemesContainer
                storyId={storiesId}
                tags={storyInfo.story_tags ? storyInfo.story_tags.filter(t => t.tag_sets_id === TAG_SET_NYT_THEMES) : []}
              />
            </Col>
          </Row>
          <Row>
            <Col lg={6}>
              <StoryDetails mediaLink={urlToTopicMapper(topicId)} story={storyInfo} />
            </Col>
          </Row>
          <Row>
            <Col lg={12}>
              <StoryEntitiesContainer storyId={storiesId} />
            </Col>
          </Row>
        </Grid>

    );
  }
}

StoryContainer.propTypes = {
  // from context
  params: PropTypes.object.isRequired, // params from router
  intl: PropTypes.object.isRequired,
  // from parent
  // from dispatch
  asyncFetch: PropTypes.func.isRequired,
  fetchData: PropTypes.func.isRequired,
  handleStoryCachedTextClick: PropTypes.func.isRequired,
  handleStoryEditClick: PropTypes.func.isRequired,
  // from state
  topicStoryInfo: PropTypes.object.isRequired,
  storyInfo: PropTypes.object.isRequired,
  storiesId: PropTypes.number.isRequired,
  topicName: PropTypes.string.isRequired,
  topicId: PropTypes.number.isRequired,
  fetchStatus: PropTypes.string.isRequired,
  filters: PropTypes.object.isRequired,
};

const mapStateToProps = (state, ownProps) => ({
  fetchStatus: state.story.info.fetchStatus,
  filters: state.topics.selected.filters,
  storiesId: parseInt(ownProps.params.storiesId, 10),
  topicId: state.topics.selected.id,
  topicName: state.topics.selected.info.name,
  topicStoryInfo: state.topics.selected.story.info,
  storyInfo: state.story.info,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (storiesId, filters) => {
    dispatch(selectStory(storiesId));
    const q = {
      ...filters,
      id: ownProps.params.topicId,
    };
    dispatch(fetchStory(storiesId, q));
    dispatch(fetchTopicStoryInfo(ownProps.params.topicId, storiesId, filters));
  },
  handleStoryCachedTextClick: (topicId, storiesId, filters) => {
    dispatch(push(filteredLinkTo(`topics/${topicId}/stories/${storiesId}/cached`, filters)));
  },
  handleStoryEditClick: (storiesId) => {
    dispatch(push(`admin/story/${storiesId}/update`)); // to admin page
  },
});

function mergeProps(stateProps, dispatchProps, ownProps) {
  return Object.assign({}, stateProps, dispatchProps, ownProps, {
    asyncFetch: () => dispatchProps.fetchData(stateProps.storiesId, stateProps.filters),
  });
}

export default
injectIntl(
  connect(mapStateToProps, mapDispatchToProps, mergeProps)(
    withAsyncFetch(
      StoryContainer
    )
  )
);
<|endoftext|>


<commit_before>/*
 Copyright 2015, 2016 OpenMarket Ltd

 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.
 */

'use strict';

import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';

export default React.createClass({
    displayName: 'SenderProfile',
    propTypes: {
        mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
        text: PropTypes.string, // Text to show. Defaults to sender name
        onClick: PropTypes.func,
    },

    contextTypes: {
        matrixClient: PropTypes.instanceOf(MatrixClient),
    },

    getInitialState() {
        return {
            userGroups: null,
            relatedGroups: [],
        };
    },

    componentWillMount() {
        this.unmounted = false;
        this._updateRelatedGroups();

        FlairStore.getPublicisedGroupsCached(
            this.context.matrixClient, this.props.mxEvent.getSender(),
        ).then((userGroups) => {
            if (this.unmounted) return;
            this.setState({userGroups});
        });

        this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
    },

    componentWillUnmount() {
        this.unmounted = true;
        this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
    },

    onRoomStateEvents(event) {
        if (event.getType() === 'm.room.related_groups' &&
            event.getRoomId() === this.props.mxEvent.getRoomId()
        ) {
            this._updateRelatedGroups();
        }
    },

    _updateRelatedGroups() {
        if (this.unmounted) return;
        const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
        if (!room) return;

        const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
        this.setState({
            relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
        });
    },

    _getDisplayedGroups(userGroups, relatedGroups) {
        let displayedGroups = userGroups || [];
        if (relatedGroups && relatedGroups.length > 0) {
            displayedGroups = displayedGroups.filter((groupId) => {
                return relatedGroups.includes(groupId);
            });
        } else {
            displayedGroups = [];
        }
        return displayedGroups;
    },

    render() {
        const EmojiText = sdk.getComponent('elements.EmojiText');
        const {mxEvent} = this.props;
        const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
        const {msgtype} = mxEvent.getContent();

        if (msgtype === 'm.emote') {
            return ; // emote message must include the name so don't duplicate it
        }

        let flair =
;
        if (this.props.enableFlair) {
            const displayedGroups = this._getDisplayedGroups(
                this.state.userGroups, this.state.relatedGroups,
            );

            flair = <Flair key='flair'
                userId={mxEvent.getSender()}
                groups={displayedGroups}
            />;
        }

        const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;

        // Name + flair
        const nameFlair =

                { nameElem }

            { flair }
        ;

        const content = this.props.text ?

                { _t(this.props.text, { senderName: () => nameElem }) }
             : nameFlair;

        return (

                { content }

        );
    },
});
<commit_msg>Consistently order flairs based on room configuration.<commit_after>/*
 Copyright 2015, 2016 OpenMarket Ltd

 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.
 */

'use strict';

import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';

export default React.createClass({
    displayName: 'SenderProfile',
    propTypes: {
        mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
        text: PropTypes.string, // Text to show. Defaults to sender name
        onClick: PropTypes.func,
    },

    contextTypes: {
        matrixClient: PropTypes.instanceOf(MatrixClient),
    },

    getInitialState() {
        return {
            userGroups: null,
            relatedGroups: [],
        };
    },

    componentWillMount() {
        this.unmounted = false;
        this._updateRelatedGroups();

        FlairStore.getPublicisedGroupsCached(
            this.context.matrixClient, this.props.mxEvent.getSender(),
        ).then((userGroups) => {
            if (this.unmounted) return;
            this.setState({userGroups});
        });

        this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
    },

    componentWillUnmount() {
        this.unmounted = true;
        this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
    },

    onRoomStateEvents(event) {
        if (event.getType() === 'm.room.related_groups' &&
            event.getRoomId() === this.props.mxEvent.getRoomId()
        ) {
            this._updateRelatedGroups();
        }
    },

    _updateRelatedGroups() {
        if (this.unmounted) return;
        const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
        if (!room) return;

        const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
        this.setState({
            relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
        });
    },

    _getDisplayedGroups(userGroups, relatedGroups) {
        let displayedGroups = userGroups || [];
        if (relatedGroups && relatedGroups.length > 0) {
            displayedGroups = relatedGroups.filter((groupId) => {
                return displayedGroups.includes(groupId);
            });
        } else {
            displayedGroups = [];
        }
        return displayedGroups;
    },

    render() {
        const EmojiText = sdk.getComponent('elements.EmojiText');
        const {mxEvent} = this.props;
        const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
        const {msgtype} = mxEvent.getContent();

        if (msgtype === 'm.emote') {
            return ; // emote message must include the name so don't duplicate it
        }

        let flair =
;
        if (this.props.enableFlair) {
            const displayedGroups = this._getDisplayedGroups(
                this.state.userGroups, this.state.relatedGroups,
            );

            flair = <Flair key='flair'
                userId={mxEvent.getSender()}
                groups={displayedGroups}
            />;
        }

        const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;

        // Name + flair
        const nameFlair =

                { nameElem }

            { flair }
        ;

        const content = this.props.text ?

                { _t(this.props.text, { senderName: () => nameElem }) }
             : nameFlair;

        return (

                { content }

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