<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import Link from 'react-router/lib/Link';
import { FormattedMessage, FormattedNumber, injectIntl, FormattedDate } from 'react-intl';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import withAsyncFetch from '../../common/hocs/AsyncContainer';
import { fetchCollectionSourceList, scrapeSourceFeeds } from '../../../actions/sourceActions';
import AppButton from '../../common/AppButton';
import messages from '../../../resources/messages';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_MEDIA_EDIT } from '../../../lib/auth';
import { updateFeedback } from '../../../actions/appActions';
import { SOURCE_SCRAPE_STATE_QUEUED, SOURCE_SCRAPE_STATE_RUNNING, SOURCE_SCRAPE_STATE_COMPLETED, SOURCE_SCRAPE_STATE_ERROR } from '../../../reducers/sources/sources/selected/sourceDetails';
import FilledStarIcon from '../../common/icons/FilledStarIcon';
import { googleFavIconUrl } from '../../../lib/urlUtil';
import { parseSolrShortDate, jobStatusDateToMoment } from '../../../lib/dateUtil';

const localMessages = {
  title: { id: 'collection.manageSources.title', defaultMessage: 'Review Sources' },
  scrapeAll: { id: 'collection.manageSources.scrapeAll', defaultMessage: 'Scrape all For New Feeds' },
  inLast90Days: { id: 'collection.manageSources.column.last90', defaultMessage: '90 Day Story Count' },
  startedScrapingAll: { id: 'collection.manageSources.startedScrapingAll', defaultMessage: 'Started scraping all sources for RSS feeds' },
  lastScrapeQueuedSince: { id: 'source.basicInfo.feed.lastScrapeQueuedSince', defaultMessage: 'Scrape queued since {date}' },
  lastScrapeRunningSince: { id: 'source.basicInfo.feed.lastScrapeRunningSince', defaultMessage: 'Scrape running since {date}' },
  lastScrapeWorkedOn: { id: 'source.basicInfo.feed.lastScrapeWorkedOn', defaultMessage: 'Last scrape worked on {date}' },
  lastScrapeFailedOn: { id: 'source.basicInfo.feed.lastScrapeFailedOn', defaultMessage: 'Last scrape failed on {date}) ' },
  activeFeedCount: { id: 'collection.manageSources.column.activeFeedCount', defaultMessage: 'Active Feeds' },
};

class ManageSourcesContainer extends React.Component {

  state = {
    scrapedAll: false,
  }

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

  onScrapeAll = () => {
    const { scrapeAllFeeds, sources } = this.props;
    scrapeAllFeeds(sources.map(s => s.media_id));
    this.setState({ scrapedAll: true });
  }

  render() {
    const { scrapeFeeds, sources } = this.props;
    const { formatMessage, formatDate } = this.props.intl;
    return (
      <Grid>
        <Row>
          <Col lg={8}>

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

          </Col>
          <Col lg={4}>
            <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>

                <AppButton
                  color="primary"
                  className="source-scrape-feeds-button"
                  label={formatMessage(localMessages.scrapeAll)}
                  onClick={this.onScrapeAll}
                  disabled={this.state.scrapedAll}
                />

            </Permissioned>
          </Col>
        </Row>
        <Row>
          <Col lg={12}>

                    <FormattedMessage {...messages.sourceName} />
                    <FormattedMessage {...messages.sourceUrlProp} />
                    <FormattedMessage {...messages.storiesPerDay} />
                    <FormattedMessage {...messages.sourceStartDate} />
                    <FormattedMessage {...localMessages.inLast90Days} />
                    <FormattedMessage {...localMessages.activeFeedCount} />
                    <FormattedMessage {...messages.sourceScrapeStatus} />

                  {sources.map((source, idx) => {
                    const scrapeButton = (
                      <AppButton
                        color="secondary"
                        variant="outlined"
                        className="source-scrape-feeds-button"
                        label={formatMessage(messages.scrapeForFeeds)}
                        onClick={() => scrapeFeeds(source.media_id)}
                      />
                    );
                    let scrapeContent;
                    const lastScrapeUpdatedDate = source.latest_scrape_job ? formatDate(jobStatusDateToMoment(source.latest_scrape_job.last_updated)) : 'n/a';
                    if (source && source.latest_scrape_job && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_QUEUED) {
                      scrapeContent = (

                          <FormattedMessage {...localMessages.lastScrapeQueuedSince} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_RUNNING) {
                      scrapeContent = (

                          <FormattedMessage {...localMessages.lastScrapeRunningSince} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_COMPLETED) {
                      scrapeContent = (

                          {scrapeButton}

                          <FormattedMessage {...localMessages.lastScrapeWorkedOn} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_ERROR) {
                      scrapeContent = (

                          {scrapeButton}

                          <FormattedMessage {...localMessages.lastScrapeFailedOn} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    }
                    return (

                          <Link to={`/sources/${source.id ? source.id : source.media_id}`}>{source.name}</Link>
                          { source.isFavorite ? <FilledStarIcon /> : '' }

                        {source.url}
                        <FormattedNumber value={Math.round(source.num_stories_90)} />
                        <FormattedDate value={parseSolrShortDate(source.start_date)} />
                        <td className={`numeric ${Math.round(source.num_stories_90 * 90) === 0 ? 'error' : ''}`}>
                          <FormattedNumber value={Math.round(source.num_stories_90 * 90)} />

                        <td className={`numeric ${source.active_feed_count === 0 ? 'error' : ''}`}>
                          <FormattedNumber value={source.active_feed_count} />

                          <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>
                            {scrapeContent}
                          </Permissioned>

                    );
                  })}

          </Col>
        </Row>
      </Grid>
    );
  }

}

ManageSourcesContainer.propTypes = {
  intl: PropTypes.object.isRequired,
  // from store
  collectionId: PropTypes.number.isRequired,
  collection: PropTypes.object.isRequired,
  sources: PropTypes.array.isRequired,
  // from dispatch
  fetchData: PropTypes.func.isRequired,
  scrapeFeeds: PropTypes.func.isRequired,
  scrapeAllFeeds: PropTypes.func.isRequired,
};

const mapStateToProps = state => ({
  collectionId: state.sources.collections.selected.id,
  collection: state.sources.collections.selected.collectionDetails.object,
  fetchStatus: state.sources.collections.selected.collectionSourceList.fetchStatus,
  sources: state.sources.collections.selected.collectionSourceList.sources,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (collectionId) => {
    dispatch(fetchCollectionSourceList(collectionId, { details: true }));
  },
  scrapeFeeds: (sourceId) => {
    dispatch(scrapeSourceFeeds(sourceId))
      .then((results) => {
        if ((results.job_state.state === SOURCE_SCRAPE_STATE_QUEUED) ||
          (results.job_state.state === SOURCE_SCRAPE_STATE_RUNNING)) {
          dispatch(updateFeedback({ classes: 'info-notice', open: true, message: ownProps.intl.formatMessage(messages.sourceScraping) }));
          // update the page so the user sees the new scrape status
          window.location.reload();
        } else {
          dispatch(updateFeedback({ classes: 'error-notice', open: true, message: ownProps.intl.formatMessage(messages.sourceScrapeFailed) }));
        }
      });
  },
  scrapeAllFeeds: (mediaIdList) => {
    mediaIdList.forEach(mediaId => dispatch(scrapeSourceFeeds(mediaId)));
    dispatch(updateFeedback({ classes: 'info-notice', open: true, message: ownProps.intl.formatMessage(localMessages.startedScrapingAll) }));
  },
});

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

export default
  injectIntl(
    connect(mapStateToProps, mapDispatchToProps, mergeProps)(
      withAsyncFetch(
        ManageSourcesContainer
      )
    )
  );
<commit_msg>Fit text on button<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import Link from 'react-router/lib/Link';
import { FormattedMessage, FormattedNumber, injectIntl, FormattedDate } from 'react-intl';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import withAsyncFetch from '../../common/hocs/AsyncContainer';
import { fetchCollectionSourceList, scrapeSourceFeeds } from '../../../actions/sourceActions';
import AppButton from '../../common/AppButton';
import messages from '../../../resources/messages';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_MEDIA_EDIT } from '../../../lib/auth';
import { updateFeedback } from '../../../actions/appActions';
import { SOURCE_SCRAPE_STATE_QUEUED, SOURCE_SCRAPE_STATE_RUNNING, SOURCE_SCRAPE_STATE_COMPLETED, SOURCE_SCRAPE_STATE_ERROR } from '../../../reducers/sources/sources/selected/sourceDetails';
import FilledStarIcon from '../../common/icons/FilledStarIcon';
import { googleFavIconUrl } from '../../../lib/urlUtil';
import { parseSolrShortDate, jobStatusDateToMoment } from '../../../lib/dateUtil';

const localMessages = {
  title: { id: 'collection.manageSources.title', defaultMessage: 'Review Sources' },
  scrape: { id: 'collection.manageSources.scrape', defaultMessage: 'Scrape' }, // using this so we have a smaller button
  scrapeAll: { id: 'collection.manageSources.scrapeAll', defaultMessage: 'Scrape all For New Feeds' },
  inLast90Days: { id: 'collection.manageSources.column.last90', defaultMessage: '90 Day Story Count' },
  startedScrapingAll: { id: 'collection.manageSources.startedScrapingAll', defaultMessage: 'Started scraping all sources for RSS feeds' },
  lastScrapeQueuedSince: { id: 'source.basicInfo.feed.lastScrapeQueuedSince', defaultMessage: 'Scrape queued since {date}' },
  lastScrapeRunningSince: { id: 'source.basicInfo.feed.lastScrapeRunningSince', defaultMessage: 'Scrape running since {date}' },
  lastScrapeWorkedOn: { id: 'source.basicInfo.feed.lastScrapeWorkedOn', defaultMessage: 'Last scrape worked on {date}' },
  lastScrapeFailedOn: { id: 'source.basicInfo.feed.lastScrapeFailedOn', defaultMessage: 'Last scrape failed on {date}) ' },
  activeFeedCount: { id: 'collection.manageSources.column.activeFeedCount', defaultMessage: 'Active Feeds' },
};

class ManageSourcesContainer extends React.Component {

  state = {
    scrapedAll: false,
  }

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

  onScrapeAll = () => {
    const { scrapeAllFeeds, sources } = this.props;
    scrapeAllFeeds(sources.map(s => s.media_id));
    this.setState({ scrapedAll: true });
  }

  render() {
    const { scrapeFeeds, sources } = this.props;
    const { formatMessage, formatDate } = this.props.intl;
    return (
      <Grid>
        <Row>
          <Col lg={8}>

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

          </Col>
          <Col lg={4}>
            <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>

                <AppButton
                  color="primary"
                  className="source-scrape-feeds-button"
                  label={formatMessage(localMessages.scrapeAll)}
                  onClick={this.onScrapeAll}
                  disabled={this.state.scrapedAll}
                />

            </Permissioned>
          </Col>
        </Row>
        <Row>
          <Col lg={12}>

                    <FormattedMessage {...messages.sourceName} />
                    <FormattedMessage {...messages.sourceUrlProp} />
                    <FormattedMessage {...messages.storiesPerDay} />
                    <FormattedMessage {...messages.sourceStartDate} />
                    <FormattedMessage {...localMessages.inLast90Days} />
                    <FormattedMessage {...localMessages.activeFeedCount} />
                    <FormattedMessage {...messages.sourceScrapeStatus} />

                  {sources.map((source, idx) => {
                    const scrapeButton = (
                      <AppButton
                        color="secondary"
                        variant="outlined"
                        className="source-scrape-feeds-button"
                        label={formatMessage(localMessages.scrape)}
                        onClick={() => scrapeFeeds(source.media_id)}
                      />
                    );
                    let scrapeContent;
                    const lastScrapeUpdatedDate = source.latest_scrape_job ? formatDate(jobStatusDateToMoment(source.latest_scrape_job.last_updated)) : 'n/a';
                    if (source && source.latest_scrape_job && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_QUEUED) {
                      scrapeContent = (

                          <FormattedMessage {...localMessages.lastScrapeQueuedSince} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_RUNNING) {
                      scrapeContent = (

                          <FormattedMessage {...localMessages.lastScrapeRunningSince} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_COMPLETED) {
                      scrapeContent = (

                          {scrapeButton}

                          <FormattedMessage {...localMessages.lastScrapeWorkedOn} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    } else if (source && source.latest_scrape_job.state === SOURCE_SCRAPE_STATE_ERROR) {
                      scrapeContent = (

                          {scrapeButton}

                          <FormattedMessage {...localMessages.lastScrapeFailedOn} values={{ date: lastScrapeUpdatedDate }} />

                      );
                    }
                    return (

                          <Link to={`/sources/${source.id ? source.id : source.media_id}`}>{source.name}</Link>
                          { source.isFavorite ? <FilledStarIcon /> : '' }

                        {source.url}
                        <FormattedNumber value={Math.round(source.num_stories_90)} />
                        <FormattedDate value={parseSolrShortDate(source.start_date)} />
                        <td className={`numeric ${Math.round(source.num_stories_90 * 90) === 0 ? 'error' : ''}`}>
                          <FormattedNumber value={Math.round(source.num_stories_90 * 90)} />

                        <td className={`numeric ${source.active_feed_count === 0 ? 'error' : ''}`}>
                          <FormattedNumber value={source.active_feed_count} />

                          <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>
                            {scrapeContent}
                          </Permissioned>

                    );
                  })}

          </Col>
        </Row>
      </Grid>
    );
  }

}

ManageSourcesContainer.propTypes = {
  intl: PropTypes.object.isRequired,
  // from store
  collectionId: PropTypes.number.isRequired,
  collection: PropTypes.object.isRequired,
  sources: PropTypes.array.isRequired,
  // from dispatch
  fetchData: PropTypes.func.isRequired,
  scrapeFeeds: PropTypes.func.isRequired,
  scrapeAllFeeds: PropTypes.func.isRequired,
};

const mapStateToProps = state => ({
  collectionId: state.sources.collections.selected.id,
  collection: state.sources.collections.selected.collectionDetails.object,
  fetchStatus: state.sources.collections.selected.collectionSourceList.fetchStatus,
  sources: state.sources.collections.selected.collectionSourceList.sources,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (collectionId) => {
    dispatch(fetchCollectionSourceList(collectionId, { details: true }));
  },
  scrapeFeeds: (sourceId) => {
    dispatch(scrapeSourceFeeds(sourceId))
      .then((results) => {
        if ((results.job_state.state === SOURCE_SCRAPE_STATE_QUEUED) ||
          (results.job_state.state === SOURCE_SCRAPE_STATE_RUNNING)) {
          dispatch(updateFeedback({ classes: 'info-notice', open: true, message: ownProps.intl.formatMessage(messages.sourceScraping) }));
          // update the page so the user sees the new scrape status
          window.location.reload();
        } else {
          dispatch(updateFeedback({ classes: 'error-notice', open: true, message: ownProps.intl.formatMessage(messages.sourceScrapeFailed) }));
        }
      });
  },
  scrapeAllFeeds: (mediaIdList) => {
    mediaIdList.forEach(mediaId => dispatch(scrapeSourceFeeds(mediaId)));
    dispatch(updateFeedback({ classes: 'info-notice', open: true, message: ownProps.intl.formatMessage(localMessages.startedScrapingAll) }));
  },
});

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

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


<commit_before>import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { Button, Modal } from 'react-bootstrap';
import DocumentTitle from 'react-document-title';
import styled from 'styled-components';
import FontAwesome from 'react-fontawesome';
import classnames from 'classnames';
import { EntityDataModelApi } from 'loom-data';
import IntegrationDetailsModal from './IntegrationDetailsModal';

import * as actionFactories from './EntitySetDetailActionFactories';
import * as edmActionFactories from '../edm/EdmActionFactories';
import * as PermissionsActionFactory from '../permissions/PermissionsActionFactory';
import * as psActionFactories from '../permissionssummary/PermissionsSummaryActionFactory';
import EntitySetPermissionsRequestList from '../permissions/components/EntitySetPermissionsRequestList';
import { PermissionsPropType, getPermissions, DEFAULT_PERMISSIONS } from '../permissions/PermissionsStorage';
import { getEdmObject } from '../edm/EdmStorage';
import PropertyTypeList from '../edm/components/PropertyTypeList';
import PermissionsPanel from '../../views/Main/Schemas/Components/PermissionsPanel';
import AddDataForm from '../entitysetforms/AddDataForm';
import ActionDropdown from '../edm/components/ActionDropdown';
import AsyncContent, { AsyncStatePropType } from '../../components/asynccontent/AsyncContent';
import { EntitySetPropType } from '../edm/EdmModel';
import Page from '../../components/page/Page';
import PageConsts from '../../utils/Consts/PageConsts';
import styles from './entitysetdetail.module.css';

import InlineEditableControl from '../../components/controls/InlineEditableControl';
import StyledFlexContainer from '../../components/flex/StyledFlexContainer';
import StyledFlexContainerStacked from '../../components/flex/StyledFlexContainerStacked';

const TitleControlsContainer = styled(StyledFlexContainer)`
  justify-content: space-between;
`;

const ControlsContainer = styled(StyledFlexContainerStacked)`
  align-items: flex-end;
  flex: 1 0 auto;
`;

const permissionOptions = {
  Discover: 'Discover',
  Link: 'Link',
  Read: 'Read',
  Write: 'Write',
  Owner: 'Owner'
};

class EntitySetDetailComponent extends React.Component {
  static propTypes = {
    asyncState: AsyncStatePropType.isRequired,
    router: PropTypes.object.isRequired,

    // Async content
    entitySet: EntitySetPropType,
    entitySetPermissions: PermissionsPropType.isRequired,

    // Loading
    loadEntitySet: PropTypes.func.isRequired,
    updateMetadata: PropTypes.func.isRequired
  };

  constructor(props) {
    super(props);

    this.state = {
      editingPermissions: false,
      confirmingDelete: false,
      addingData: false,
      deleteError: false,
      isIntegrationDetailsOpen: false
    };
  }

  componentDidMount() {
    this.props.resetPermissions();
    this.props.loadEntitySet();
  }

  setEditingPermissions = () => {
    this.setState({ editingPermissions: true });
  };

  updateEntitySetTitle = (title) => {
    if (title && title.length) {
      this.props.updateMetadata(this.props.entitySet.id, { title });
    }
  }

  updateEntitySetDescription = (description) => {
    if (description) this.props.updateMetadata(this.props.entitySet.id, { description })
  }

  updateEntitySetContacts = (contacts) => {
    if (contacts) this.props.updateMetadata(this.props.entitySet.id, { contacts: [contacts] })
  }

  renderTitle = (title, isOwner) => {
    return (<InlineEditableControl
        type="text"
        size="xlarge"
        placeholder="Entity set title..."
        value={title}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetTitle} />);
  }

  renderDescription = (description, isOwner) => {
    return (<InlineEditableControl
        type="textarea"
        size="small"
        placeholder="Entity set description..."
        value={description}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetDescription} />);
  }

  renderEntitySetContacts = (contacts, isOwner) => {
    return (<InlineEditableControl
        type="text"
        size="small"
        placeholder="Entity set owner contacts..."
        value={contacts}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetContacts} />);
  }

  renderHeaderContent = () => {
    const { entitySet, entitySetPermissions } = this.props;

    const contactValue = (entitySet.contacts.length > 0) ? entitySet.contacts.join(', ') : 'none';

    return (

    );
  };

  renderPermissionsPanel = () => {
    if (!this.props.entitySet) return null;
    return (
      <Modal
          show={this.state.editingPermissions}
          onHide={this.closePermissionsPanel}>
        <Modal.Header closeButton>
          <Modal.Title>Manage permissions for entity set: {this.props.entitySet.title}</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <PermissionsPanel entitySetId={this.props.entitySet.id} />
        </Modal.Body>
      </Modal>
    );
  };

  renderSearchEntitySet = () => {
    if (!this.props.entitySet) return null;
    return (

        <Link className={styles.buttonLink} to={`/search/${this.props.entitySet.id}`}>
          <Button bsStyle="primary" className={styles.center}>
            <FontAwesome name="search" />
            Search this entity set
          </Button>
        </Link>

    );
  }

  renderAddDataForm = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.WRITE) return null;
    return (
      <Modal
          show={this.state.addingData}
          onHide={this.closeAddDataModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add a new row of data to this entity set</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <AddDataForm
              entitySetId={this.props.entitySet.id}
              primaryKey={this.props.entitySet.entityType.key}
              propertyTypes={this.props.entitySet.entityType.properties} />
        </Modal.Body>
      </Modal>
    );
  }

  deleteEntitySet = () => {
    if (!this.props.entitySet) return;
    EntityDataModelApi.deleteEntitySet(this.props.entitySet.id)
    .then(() => {
      this.props.router.push(`/${PageConsts.HOME}`);
    }).catch(() => {
      this.setState({
        confirmingDelete: false,
        deleteError: true
      });
    });
  }

  setConfirmingDelete = () => {
    this.setState({ confirmingDelete: true });
  }

  cancelDelete = () => {
    this.setState({ confirmingDelete: false });
  }

  openAddDataModal = () => {
    this.setState({ addingData: true });
  }

  closeAddDataModal = () => {
    this.setState({ addingData: false });
  }

  openIntegrationDetailsModal = () => {
    this.setState({ isIntegrationDetailsOpen: true });
  }

  closeIntegrationDetailsModal = () => {
    this.setState({ isIntegrationDetailsOpen: false });
  }

  renderAddDataButton = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.WRITE) return null;
    return (

        <Button
            bsStyle="success"
            className={styles.center}
            onClick={this.openAddDataModal}>
          <FontAwesome name="plus" size="lg" />
          Add Data
        </Button>

    );
  }

  renderIntegrationDetailsLink = () => {
    return (

        <Link
            className={`${styles.center} ${styles.link}`}
            onClick={this.openIntegrationDetailsModal}>
          View Integration Details
        </Link>

    );
  }

  renderIntegrationDetailsModal = () => {
    return (
      <IntegrationDetailsModal
          isOpen={this.state.isIntegrationDetailsOpen}
          onClose={this.closeIntegrationDetailsModal}
          entitySet={this.props.entitySet} />
    );
  }

  renderPermissionsSummaryButton = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.OWNER) return null;
    return (

        <Link to={`/entitysets/${this.props.params.id}/allpermissions`}>
          <Button className={styles.center}>
            View Permissions Summary
          </Button>
        </Link>

    );
  }

  renderDeleteEntitySet = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.OWNER) return null;
    const error = (this.state.deleteError) ?
Unable to delete entity set
 : null;
    return (

        <Button
            bsStyle="danger"
            className={styles.center}
            onClick={this.setConfirmingDelete}>
          <FontAwesome name="times" size="lg" />
          Delete this entity set
        </Button>
        {error}

    );
  }

  renderConfirmDeleteModal = () => {
    if (!this.props.entitySet) return null;
    return (
      <Modal
          show={this.state.confirmingDelete}
          onHide={this.cancelDelete}>
        <Modal.Header closeButton>
          <Modal.Title>Are you sure you want to delete {this.props.entitySet.title}?</Modal.Title>
        </Modal.Header>
        <Modal.Body>

            <Button onClick={this.deleteEntitySet} bsStyle="danger">Confirm</Button>

        </Modal.Body>
      </Modal>
    );
  };

  closePermissionsPanel = () => {
    this.setState({ editingPermissions: false });
  };

  getDocumentTitle = () => {
    return (this.props.entitySet) ? this.props.entitySet.title : PageConsts.DEFAULT_DOCUMENT_TITLE;
  }

  render() {
    return (
      <DocumentTitle title={this.getDocumentTitle()}>
        <Page className={styles.page}>
          <Page.Header>
            <AsyncContent {...this.props.asyncState} content={this.renderHeaderContent} />
          </Page.Header>
          <Page.Body>
            {this.renderAddDataButton()}

Data in Entity Set

            <AsyncContent
                {...this.props.asyncState}
                content={() => {
                  // TODO: Remove when removing denormalization
                  const propertyTypeIds = this.props.entitySet.entityType.properties.map((property) => {
                    return property.id;
                  });
                  return (
                    <PropertyTypeList
                        entitySetId={this.props.entitySet.id}
                        propertyTypeIds={propertyTypeIds}
                        className="propertyTypeStyleDefault" />
                  );
                }} />
            {this.renderAddDataForm()}
            {this.renderPermissionsPanel()}
            {this.renderSearchEntitySet()}
            {this.renderPermissionsSummaryButton()}
            {this.renderDeleteEntitySet()}
            {this.renderConfirmDeleteModal()}
            {this.renderIntegrationDetailsLink()}
            {this.renderIntegrationDetailsModal()}
          </Page.Body>
        </Page>
      </DocumentTitle>
    );
  }
}

function mapStateToProps(state) {
  const entitySetDetail = state.get('entitySetDetail');
  const normalizedData = state.get('normalizedData');
  const permissions = state.get('permissions');

  let entitySet;
  let entitySetPermissions;
  const reference = entitySetDetail.get('entitySetReference');
  if (reference) {
    entitySet = getEdmObject(normalizedData.toJS(), reference.toJS());
    entitySetPermissions = getPermissions(permissions, [entitySet.id]);
  }
  else {
    entitySetPermissions = DEFAULT_PERMISSIONS;
  }

  return {
    asyncState: entitySetDetail.get('asyncState').toJS(),
    entitySet,
    entitySetPermissions
  };
}

function mapDispatchToProps(dispatch, ownProps) {
  const id = ownProps.params.id;

  return {
    loadEntitySet: () => {
      dispatch(actionFactories.entitySetDetailRequest(id));
      dispatch(PermissionsActionFactory.getEntitySetsAuthorizations([id]));
      // TODO: Move filter creation in helper function in EdmApi
      dispatch(edmActionFactories.filteredEdmRequest(
        [{
          type: 'EntitySet',
          id,
          include: ['EntitySet', 'EntityType', 'PropertyTypeInEntitySet']
        }]
      ));
    },
    resetPermissions: () => {
      dispatch(psActionFactories.resetPermissions());
    }
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(EntitySetDetailComponent);
<commit_msg>this function was deleted<commit_after>import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { Button, Modal } from 'react-bootstrap';
import DocumentTitle from 'react-document-title';
import styled from 'styled-components';
import FontAwesome from 'react-fontawesome';
import classnames from 'classnames';
import { EntityDataModelApi } from 'loom-data';
import IntegrationDetailsModal from './IntegrationDetailsModal';

import * as actionFactories from './EntitySetDetailActionFactories';
import * as edmActionFactories from '../edm/EdmActionFactories';
import * as PermissionsActionFactory from '../permissions/PermissionsActionFactory';
import * as psActionFactories from '../permissionssummary/PermissionsSummaryActionFactory';
import EntitySetPermissionsRequestList from '../permissions/components/EntitySetPermissionsRequestList';
import { PermissionsPropType, getPermissions, DEFAULT_PERMISSIONS } from '../permissions/PermissionsStorage';
import { getEdmObject } from '../edm/EdmStorage';
import PropertyTypeList from '../edm/components/PropertyTypeList';
import PermissionsPanel from '../../views/Main/Schemas/Components/PermissionsPanel';
import AddDataForm from '../entitysetforms/AddDataForm';
import ActionDropdown from '../edm/components/ActionDropdown';
import AsyncContent, { AsyncStatePropType } from '../../components/asynccontent/AsyncContent';
import { EntitySetPropType } from '../edm/EdmModel';
import Page from '../../components/page/Page';
import PageConsts from '../../utils/Consts/PageConsts';
import styles from './entitysetdetail.module.css';

import InlineEditableControl from '../../components/controls/InlineEditableControl';
import StyledFlexContainer from '../../components/flex/StyledFlexContainer';
import StyledFlexContainerStacked from '../../components/flex/StyledFlexContainerStacked';

const TitleControlsContainer = styled(StyledFlexContainer)`
  justify-content: space-between;
`;

const ControlsContainer = styled(StyledFlexContainerStacked)`
  align-items: flex-end;
  flex: 1 0 auto;
`;

const permissionOptions = {
  Discover: 'Discover',
  Link: 'Link',
  Read: 'Read',
  Write: 'Write',
  Owner: 'Owner'
};

class EntitySetDetailComponent extends React.Component {
  static propTypes = {
    asyncState: AsyncStatePropType.isRequired,
    router: PropTypes.object.isRequired,

    // Async content
    entitySet: EntitySetPropType,
    entitySetPermissions: PermissionsPropType.isRequired,

    // Loading
    loadEntitySet: PropTypes.func.isRequired,
    updateMetadata: PropTypes.func.isRequired
  };

  constructor(props) {
    super(props);

    this.state = {
      editingPermissions: false,
      confirmingDelete: false,
      addingData: false,
      deleteError: false,
      isIntegrationDetailsOpen: false
    };
  }

  componentDidMount() {
    this.props.resetPermissions();
    this.props.loadEntitySet();
  }

  setEditingPermissions = () => {
    this.setState({ editingPermissions: true });
  };

  updateEntitySetTitle = (title) => {
    if (title && title.length) {
      this.props.updateMetadata(this.props.entitySet.id, { title });
    }
  }

  updateEntitySetDescription = (description) => {
    if (description) this.props.updateMetadata(this.props.entitySet.id, { description })
  }

  updateEntitySetContacts = (contacts) => {
    if (contacts) this.props.updateMetadata(this.props.entitySet.id, { contacts: [contacts] })
  }

  renderTitle = (title, isOwner) => {
    return (<InlineEditableControl
        type="text"
        size="xlarge"
        placeholder="Entity set title..."
        value={title}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetTitle} />);
  }

  renderDescription = (description, isOwner) => {
    return (<InlineEditableControl
        type="textarea"
        size="small"
        placeholder="Entity set description..."
        value={description}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetDescription} />);
  }

  renderEntitySetContacts = (contacts, isOwner) => {
    return (<InlineEditableControl
        type="text"
        size="small"
        placeholder="Entity set owner contacts..."
        value={contacts}
        viewOnly={!isOwner}
        onChange={this.updateEntitySetContacts} />);
  }

  renderHeaderContent = () => {
    const { entitySet, entitySetPermissions } = this.props;

    const contactValue = (entitySet.contacts.length > 0) ? entitySet.contacts.join(', ') : 'none';

    return (

    );
  };

  renderPermissionsPanel = () => {
    if (!this.props.entitySet) return null;
    return (
      <Modal
          show={this.state.editingPermissions}
          onHide={this.closePermissionsPanel}>
        <Modal.Header closeButton>
          <Modal.Title>Manage permissions for entity set: {this.props.entitySet.title}</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <PermissionsPanel entitySetId={this.props.entitySet.id} />
        </Modal.Body>
      </Modal>
    );
  };

  renderSearchEntitySet = () => {
    if (!this.props.entitySet) return null;
    return (

        <Link className={styles.buttonLink} to={`/search/${this.props.entitySet.id}`}>
          <Button bsStyle="primary" className={styles.center}>
            <FontAwesome name="search" />
            Search this entity set
          </Button>
        </Link>

    );
  }

  renderAddDataForm = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.WRITE) return null;
    return (
      <Modal
          show={this.state.addingData}
          onHide={this.closeAddDataModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add a new row of data to this entity set</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <AddDataForm
              entitySetId={this.props.entitySet.id}
              primaryKey={this.props.entitySet.entityType.key}
              propertyTypes={this.props.entitySet.entityType.properties} />
        </Modal.Body>
      </Modal>
    );
  }

  deleteEntitySet = () => {
    if (!this.props.entitySet) return;
    EntityDataModelApi.deleteEntitySet(this.props.entitySet.id)
    .then(() => {
      this.props.router.push(`/${PageConsts.HOME}`);
    }).catch(() => {
      this.setState({
        confirmingDelete: false,
        deleteError: true
      });
    });
  }

  setConfirmingDelete = () => {
    this.setState({ confirmingDelete: true });
  }

  cancelDelete = () => {
    this.setState({ confirmingDelete: false });
  }

  openAddDataModal = () => {
    this.setState({ addingData: true });
  }

  closeAddDataModal = () => {
    this.setState({ addingData: false });
  }

  openIntegrationDetailsModal = () => {
    this.setState({ isIntegrationDetailsOpen: true });
  }

  closeIntegrationDetailsModal = () => {
    this.setState({ isIntegrationDetailsOpen: false });
  }

  renderAddDataButton = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.WRITE) return null;
    return (

        <Button
            bsStyle="success"
            className={styles.center}
            onClick={this.openAddDataModal}>
          <FontAwesome name="plus" size="lg" />
          Add Data
        </Button>

    );
  }

  renderIntegrationDetailsLink = () => {
    return (

        <Link
            className={`${styles.center} ${styles.link}`}
            onClick={this.openIntegrationDetailsModal}>
          View Integration Details
        </Link>

    );
  }

  renderIntegrationDetailsModal = () => {
    return (
      <IntegrationDetailsModal
          isOpen={this.state.isIntegrationDetailsOpen}
          onClose={this.closeIntegrationDetailsModal}
          entitySet={this.props.entitySet} />
    );
  }

  renderPermissionsSummaryButton = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.OWNER) return null;
    return (

        <Link to={`/entitysets/${this.props.params.id}/allpermissions`}>
          <Button className={styles.center}>
            View Permissions Summary
          </Button>
        </Link>

    );
  }

  renderDeleteEntitySet = () => {
    if (!this.props.entitySet || !this.props.entitySetPermissions.OWNER) return null;
    const error = (this.state.deleteError) ?
Unable to delete entity set
 : null;
    return (

        <Button
            bsStyle="danger"
            className={styles.center}
            onClick={this.setConfirmingDelete}>
          <FontAwesome name="times" size="lg" />
          Delete this entity set
        </Button>
        {error}

    );
  }

  renderConfirmDeleteModal = () => {
    if (!this.props.entitySet) return null;
    return (
      <Modal
          show={this.state.confirmingDelete}
          onHide={this.cancelDelete}>
        <Modal.Header closeButton>
          <Modal.Title>Are you sure you want to delete {this.props.entitySet.title}?</Modal.Title>
        </Modal.Header>
        <Modal.Body>

            <Button onClick={this.deleteEntitySet} bsStyle="danger">Confirm</Button>

        </Modal.Body>
      </Modal>
    );
  };

  closePermissionsPanel = () => {
    this.setState({ editingPermissions: false });
  };

  getDocumentTitle = () => {
    return (this.props.entitySet) ? this.props.entitySet.title : PageConsts.DEFAULT_DOCUMENT_TITLE;
  }

  render() {
    return (
      <DocumentTitle title={this.getDocumentTitle()}>
        <Page className={styles.page}>
          <Page.Header>
            <AsyncContent {...this.props.asyncState} content={this.renderHeaderContent} />
          </Page.Header>
          <Page.Body>
            {this.renderAddDataButton()}

Data in Entity Set

            <AsyncContent
                {...this.props.asyncState}
                content={() => {
                  // TODO: Remove when removing denormalization
                  const propertyTypeIds = this.props.entitySet.entityType.properties.map((property) => {
                    return property.id;
                  });
                  return (
                    <PropertyTypeList
                        entitySetId={this.props.entitySet.id}
                        propertyTypeIds={propertyTypeIds}
                        className="propertyTypeStyleDefault" />
                  );
                }} />
            {this.renderAddDataForm()}
            {this.renderPermissionsPanel()}
            {this.renderSearchEntitySet()}
            {this.renderPermissionsSummaryButton()}
            {this.renderDeleteEntitySet()}
            {this.renderConfirmDeleteModal()}
            {this.renderIntegrationDetailsLink()}
            {this.renderIntegrationDetailsModal()}
          </Page.Body>
        </Page>
      </DocumentTitle>
    );
  }
}

function mapStateToProps(state) {
  const entitySetDetail = state.get('entitySetDetail');
  const normalizedData = state.get('normalizedData');
  const permissions = state.get('permissions');

  let entitySet;
  let entitySetPermissions;
  const reference = entitySetDetail.get('entitySetReference');
  if (reference) {
    entitySet = getEdmObject(normalizedData.toJS(), reference.toJS());
    entitySetPermissions = getPermissions(permissions, [entitySet.id]);
  }
  else {
    entitySetPermissions = DEFAULT_PERMISSIONS;
  }

  return {
    asyncState: entitySetDetail.get('asyncState').toJS(),
    entitySet,
    entitySetPermissions
  };
}

function mapDispatchToProps(dispatch, ownProps) {
  const id = ownProps.params.id;

  return {
    loadEntitySet: () => {
      dispatch(actionFactories.entitySetDetailRequest(id));
      dispatch(PermissionsActionFactory.getEntitySetsAuthorizations([id]));
      // TODO: Move filter creation in helper function in EdmApi
      dispatch(edmActionFactories.filteredEdmRequest(
        [{
          type: 'EntitySet',
          id,
          include: ['EntitySet', 'EntityType', 'PropertyTypeInEntitySet']
        }]
      ));
    },
    resetPermissions: () => {
      dispatch(psActionFactories.resetPermissions());
    },
    updateMetadata: (entitySetId, metadataUpdate) => {
      dispatch(edmActionFactories.updateEntitySetMetadataRequest(entitySetId, metadataUpdate));
    }
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(EntitySetDetailComponent);
<|endoftext|>


<commit_before>const corePath = require('path')
const flatten = require('flatten')
const fs = require('fs')
const junk = require('junk')
const once = require('once')
const parallel = require('run-parallel')

function notHidden (file) {
  return file[0] !== '.'
}

function traversePath (path, fn, cb) {
  fs.stat(path, (err, stats) => {
    if (err) return cb(err)
    if (stats.isDirectory()) {
      fs.readdir(path, (err, entries) => {
        if (err) return cb(err)
        parallel(entries.filter(notHidden).filter(junk.not).map(entry => cb => {
          traversePath(corePath.join(path, entry), fn, cb)
        }), cb)
      })
    } else if (stats.isFile()) {
      fn(path, cb)
    }
    // Ignore other types (not a file or directory)
  })
}

/**
 * Convert a file path to a lazy readable stream.
 * @param  {string} path
 * @return {function}
 */
function getFilePathStream (path) {
  return () => fs.createReadStream(path)
}

function getFiles (path, keepRoot, cb) {
  traversePath(path, getFileInfo, (err, files) => {
    if (err) return cb(err)

    if (Array.isArray(files)) files = flatten(files)
    else files = [files]

    path = corePath.normalize(path)
    if (keepRoot) {
      path = path.slice(0, path.lastIndexOf(corePath.sep) + 1)
    }
    if (path[path.length - 1] !== corePath.sep) path += corePath.sep

    files.forEach(file => {
      file.getStream = getFilePathStream(file.path)
      file.path = file.path.replace(path, '').split(corePath.sep)
    })

    cb(null, files)
  })
}

function getFileInfo (path, cb) {
  cb = once(cb)
  fs.stat(path, (err, stat) => {
    if (err) return cb(err)
    const info = {
      length: stat.size,
      path
    }
    cb(null, info)
  })
}

module.exports = getFiles
<commit_msg>use array.flat ponyfill in get-files<commit_after>const corePath = require('path')
const fs = require('fs')
const junk = require('junk')
const once = require('once')
const parallel = require('run-parallel')

// TODO: When Node 10 support is dropped, replace this with Array.prototype.flat
function flat (arr1) {
  return arr1.reduce(
    (acc, val) => Array.isArray(val)
      ? acc.concat(flat(val))
      : acc.concat(val),
    []
  )
}

function notHidden (file) {
  return file[0] !== '.'
}

function traversePath (path, fn, cb) {
  fs.stat(path, (err, stats) => {
    if (err) return cb(err)
    if (stats.isDirectory()) {
      fs.readdir(path, (err, entries) => {
        if (err) return cb(err)
        parallel(entries.filter(notHidden).filter(junk.not).map(entry => cb => {
          traversePath(corePath.join(path, entry), fn, cb)
        }), cb)
      })
    } else if (stats.isFile()) {
      fn(path, cb)
    }
    // Ignore other types (not a file or directory)
  })
}

/**
 * Convert a file path to a lazy readable stream.
 * @param  {string} path
 * @return {function}
 */
function getFilePathStream (path) {
  return () => fs.createReadStream(path)
}

function getFiles (path, keepRoot, cb) {
  traversePath(path, getFileInfo, (err, files) => {
    if (err) return cb(err)

    if (Array.isArray(files)) files = flat(files)
    else files = [files]

    path = corePath.normalize(path)
    if (keepRoot) {
      path = path.slice(0, path.lastIndexOf(corePath.sep) + 1)
    }
    if (path[path.length - 1] !== corePath.sep) path += corePath.sep

    files.forEach(file => {
      file.getStream = getFilePathStream(file.path)
      file.path = file.path.replace(path, '').split(corePath.sep)
    })

    cb(null, files)
  })
}

function getFileInfo (path, cb) {
  cb = once(cb)
  fs.stat(path, (err, stat) => {
    if (err) return cb(err)
    const info = {
      length: stat.size,
      path
    }
    cb(null, info)
  })
}

module.exports = getFiles
<|endoftext|>


<commit_before>const yahooFinance = require('yahoo-finance');

const CACHE_TIME = 30 * 60 * 1000;

const groups = {
  tech: {
    heading: 'NZ Tech',
    currency: 'NZD',
    securities: {
      'ERD.NZ': '<:eroad:349675137995374592>',
      'OHE.NZ': '<:orion:330132803243212801>',
      'VGL.NZ': '<:vista:349674926736801805>',
    },
  },
  crypto: {
    heading: 'Trendy Crypto',
    currency: 'USD',
    securities: {
      'BTCUSD=X': '<:BTC:350872732747038721>',
      'ETHUSD=X': '<:ETH:350872707258122241>',
    },
  },
  index: {
    heading: 'Actually Smart Investments',
    currency: 'NZD',
    securities: {
      '^NZ50': ':flag_nz:',
    },
  },
  currency: {
    heading: 'Currencies',
    currency: 'NZD',
    securities: {
      'AUDNZD=X': ':flag_au:',
      'NZD=X': ':flag_us:', // USD
      'GBPNZD=X': ':flag_gb:',
    },
  },
};

const cache = {};

const getFromCache = (symbol) => {
  if (cache[symbol] && cache[symbol].expiry > Date.now()) {
    return Promise.resolve(cache[symbol].value);
  }
  return yahooFinance.quote({ symbol, modules: ['price', 'summaryDetail'] }).then((result) => {
    cache[symbol] = {
      value: result,
      expiry: Date.now() + CACHE_TIME,
    };

    return result;
  });
};

const getTrend = (quote) => {
  if (quote.price - quote.close > 0) {
    return ':chart_with_upwards_trend:';
  } else if (quote.price - quote.close < 0) {
    return ':chart_with_downwards_trend:';
  }
  return ':expressionless:';
};

const getYahooPricesForGroup = securities => new Promise((resolve, reject) => {
  const getLogo = symbol => securities.securities[symbol] || null;
  Promise.all(Object.keys(securities.securities).map(symbol => getFromCache(symbol)))
  .then((quotes) => {
    resolve(quotes.map(q => ({
      symbol: q.price.symbol,
      price: q.price.regularMarketPrice,
      close: q.price.regularMarketPreviousClose,
      cap: q.summaryDetail.marketCap,
    }))
    .sort((a, b) => b.cap - a.cap)
    .map(quote =>
        `${getLogo(quote.symbol) || quote.symbol}: \`$${quote.price.toFixed(2)} ${securities.currency}\` ${getTrend(quote)} \`${Math.abs(quote.price - quote.close).toFixed(2)}\``)
    .reduce((a, b) => `${a}\n\t${b}`, `**${securities.heading}**`));
  }).catch(err => reject(err));
});

const getYahooPrices = securities => Promise.all(securities.map(group => getYahooPricesForGroup(group)));

const getPrices = securities => getYahooPrices(securities);

exports.all = () => getPrices(Object.values(groups));
exports.single = name =>
    (groups[name] ? getPrices([groups[name]]) : Promise.reject(`Do not have a group called ${name}. try ${Object.keys(groups).reduce((a, b) => `${a}, ${b}`)}`));
<commit_msg>Fix style<commit_after>const yahooFinance = require('yahoo-finance');

const CACHE_TIME = 30 * 60 * 1000;

const groups = {
  tech: {
    heading: 'NZ Tech',
    currency: 'NZD',
    securities: {
      'ERD.NZ': '<:eroad:349675137995374592>',
      'OHE.NZ': '<:orion:330132803243212801>',
      'VGL.NZ': '<:vista:349674926736801805>',
    },
  },
  crypto: {
    heading: 'Trendy Crypto',
    currency: 'USD',
    securities: {
      'BTCUSD=X': '<:BTC:350872732747038721>',
      'ETHUSD=X': '<:ETH:350872707258122241>',
    },
  },
  index: {
    heading: 'Actually Smart Investments',
    currency: 'NZD',
    securities: {
      '^NZ50': ':flag_nz:',
    },
  },
  currency: {
    heading: 'Currencies',
    currency: 'NZD',
    securities: {
      'AUDNZD=X': ':flag_au:',
      'NZD=X': ':flag_us:', // USD
      'GBPNZD=X': ':flag_gb:',
    },
  },
};

const cache = {};

const getFromCache = (symbol) => {
  if (cache[symbol] && cache[symbol].expiry > Date.now()) {
    return Promise.resolve(cache[symbol].value);
  }
  return yahooFinance.quote({ symbol, modules: ['price', 'summaryDetail'] }).then((result) => {
    cache[symbol] = {
      value: result,
      expiry: Date.now() + CACHE_TIME,
    };

    return result;
  });
};

const getTrend = (quote) => {
  if (quote.price - quote.close > 0) {
    return ':chart_with_upwards_trend:';
  } else if (quote.price - quote.close < 0) {
    return ':chart_with_downwards_trend:';
  }
  return ':expressionless:';
};

const getYahooPricesForGroup = securities => new Promise((resolve, reject) => {
  const getLogo = symbol => securities.securities[symbol] || null;
  Promise.all(Object.keys(securities.securities).map(symbol => getFromCache(symbol)))
  .then((quotes) => {
    resolve(quotes.map(q => ({
      symbol: q.price.symbol,
      price: q.price.regularMarketPrice,
      close: q.price.regularMarketPreviousClose,
      cap: q.summaryDetail.marketCap,
    }))
    .sort((a, b) => b.cap - a.cap)
    .map(q =>
        `${getLogo(q.symbol) || q.symbol}: \`$${q.price.toFixed(2)} ${securities.currency}\` ${getTrend(q)} \`${Math.abs(q.price - q.close).toFixed(2)}\``)
    .reduce((a, b) => `${a}\n\t${b}`, `**${securities.heading}**`));
  }).catch(err => reject(err));
});

const getYahooPrices = securities => Promise.all(securities.map(group => getYahooPricesForGroup(group)));

const getPrices = securities => getYahooPrices(securities);

exports.all = () => getPrices(Object.values(groups));
exports.single = name =>
    (groups[name] ? getPrices([groups[name]]) :
    Promise.reject(`Do not have a group called ${name}. try ${Object.keys(groups).reduce((a, b) => `${a}, ${b}`)}`));
<|endoftext|>


<commit_before>
import createPrivate from "../../../createPrivate"
import EventBus from "../../../EventBus"
import Account from "../../../model/Account"
import ClientFactory from "../../youtube-api/ClientFactory"
import Database from "../Database"
import SyncStorage from "../SyncStorage"

const PRIVATE = createPrivate()

/**
 * Utility for synchronizing the user's Google accounts in the main data
 * storage (Indexed DB) against the modifications reported by the Chrome's
 * (cross-device) synchronized storage.
 */
export default class AccountsSynchronizer {
  /**
   * Initializes the accounts storage synchronizer.
   *
   * @param eventBus The event bus that will deliver event about account
   *        modifications. The accounts synchronizer will use it to notify the
   *        application when an account has been successfully added.
   * @param clientFactory Factory for YouTube API clients.
   * @param database The Indexed DB database connection.
   */
  constructor(eventBus: EventBus, clientFactory: ClientFactory,
      database: Database) {
    /**
     * The event bus that will deliver event about account modifications. The
     * accounts synchronizer will use it to notify the application when an
     * account has been successfully added.
     */
    PRIVATE(this).eventBus = eventBus

    /**
     * Factory for YouTube API clients.
     */
    PRIVATE(this).clientFactory = clientFactory

    /**
     * The Indexed DB database connection.
     */
    PRIVATE(this).database = database

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_ADDED,
      this[PRIVATE.onAccountAdded],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_ENABLED,
      this[PRIVATE.onAccountEnabled],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_DISABLED,
      this[PRIVATE.onAccountDisabled],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_REMOVED,
      this[PRIVATE.onAccountRemoved],
      this
    )
  }

  /**
   * Returns the constants containing the names of the event bus events fired
   * by this accounts synchronizer.
   *
   * @return The constants containing the names of the event bus events fired
   *         by this accounts synchronizer.
   */
  static get EVENTS(): {
    ACCOUNT_ADDED: string
  } {
    const eventPrefix = "background.storage.synchronization.EVENTS."
    return Object.freeze({
      ACCOUNT_ADDED: `${eventPrefix}ACCOUNT_ADDED`
    })
  }

  async [PRIVATE.onAccountAdded](accountInfo: {id: string}) {
    let accountId = accountInfo.id
    let apiClient = PRIVATE(this).clientFactory.getClientForUser(accountId)
    let entityManager = PRIVATE(this).database.createEntityManager()

    // TODO: check if the account is enabled in the sync storage

    let currentAccountId = await this[PRIVATE.getCurrentAccountId]()
    if (accountId !== currentAccountId) {
      // Chrome did not allow access to multiple Google accounts from a single
      // browser instance at the time of implementation.
      await entityManager.persist(new Account({
        id: accountId,
        channelId: null,
        title: null,
        state: null, // TODO
        lastError: null,
        watchHistoryPlaylistId: null,
        watchLaterPlaylistId: null
      }))
      return
    }

    /*
     * TODO: what if the account has been added through synchronization and not
     * interactively? check (or request) authorization?
     */
    let account = await apiClient.getAccountInfo(accountId)

    await entityManager.persist(account)

    PRIVATE(this).eventBus.fire(
      AccountsSynchronizer.EVENTS.ACCOUNT_ADDED, account
    )
  }

  [PRIVATE.onAccountEnabled](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  [PRIVATE.onAccountDisabled](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  [PRIVATE.onAccountRemoved](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  [PRIVATE.getCurrentAccountId](): Promise<string> {
    return new Promise((resolve, reject) => {
      chrome.identity.getProfileUserInfo((userInfo) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message))
        }

        resolve(userInfo.id)
      })
    })
  }
}
<commit_msg>finished handling of added accounts in the accounts synchronizer<commit_after>
import createPrivate from "../../../createPrivate"
import EventBus from "../../../EventBus"
import Lock from "../../../Lock"
import Account from "../../../model/Account"
import AccountState from "../../../model/AccountState"
import ClientFactory from "../../youtube-api/ClientFactory"
import Database from "../Database"
import SyncStorage from "../SyncStorage"

const PRIVATE = createPrivate()

/**
 * Utility for synchronizing the user's Google accounts in the main data
 * storage (Indexed DB) against the modifications reported by the Chrome's
 * (cross-device) synchronized storage.
 */
export default class AccountsSynchronizer {
  /**
   * Initializes the accounts storage synchronizer.
   *
   * @param eventBus The event bus that will deliver event about account
   *        modifications. The accounts synchronizer will use it to notify the
   *        application when an account has been successfully added.
   * @param clientFactory Factory for YouTube API clients.
   * @param syncStorage The synchronized settings storage.
   * @param database The Indexed DB database connection.
   */
  constructor(eventBus: EventBus, clientFactory: ClientFactory,
      syncStorage: SyncStorage, database: Database) {
    /**
     * The event bus that will deliver event about account modifications. The
     * accounts synchronizer will use it to notify the application when an
     * account has been successfully added.
     */
    PRIVATE(this).eventBus = eventBus

    /**
     * Factory for YouTube API clients.
     */
    PRIVATE(this).clientFactory = clientFactory

    /**
     * The synchronized settings storage.
     */
    PRIVATE(this).syncStorage = syncStorage

    /**
     * The Indexed DB database connection.
     */
    PRIVATE(this).database = database

    /**
     * Lock for synchronizing the processing of accounts modifications.
     */
    PRIVATE(this).accountsLock = new Lock()

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_ADDED,
      this[PRIVATE.onAccountAdded],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_ENABLED,
      this[PRIVATE.onAccountEnabled],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_DISABLED,
      this[PRIVATE.onAccountDisabled],
      this
    )

    eventBus.addListener(
      SyncStorage.EVENTS.ACCOUNT_REMOVED,
      this[PRIVATE.onAccountRemoved],
      this
    )

    Object.freeze(this)
  }

  /**
   * Returns the constants containing the names of the event bus events fired
   * by this accounts synchronizer.
   *
   * @return The constants containing the names of the event bus events fired
   *         by this accounts synchronizer.
   */
  static get EVENTS(): {
    ACCOUNT_ADDED: string
  } {
    const eventPrefix = "background.storage.synchronization.EVENTS."
    return Object.freeze({
      ACCOUNT_ADDED: `${eventPrefix}ACCOUNT_ADDED`
    })
  }

  /**
   * Handles an account being added to the synchronized storage. The method
   * retrieves the related account info (if possible) and creates the account's
   * entity in the database.
   *
   * @param accountInfo Received information about the account.
   */
  async [PRIVATE.onAccountAdded](accountInfo: {id: string}): void {
    let accountId = accountInfo.id
    let apiClient = PRIVATE(this).clientFactory.getClientForUser(accountId)
    let entityManager = PRIVATE(this).database.createEntityManager()

    return await PRIVATE(this).accountsLock.lock(async () => {
      let accountEnabled = await this[PRIVATE.isAccountEnabled](accountId)
      if (accountEnabled === null) {
        console.warn(`The account with ID ${accountId} was added but has ` +
            "already been removed from the synchronized storage")
        return
      }
      let state = accountEnabled ?
          AccountState.UNAUTHORIZED :
          AccountState.DISABLED

      let currentAccountId = await this[PRIVATE.getCurrentAccountId]()
      if (accountId !== currentAccountId) {
        // Chrome did not allow access to multiple Google accounts from a
        // single browser instance at the time of implementation.
        await entityManager.persist(this[PRIVATE.getBlankAccountEntity](
          accountId,
          state
        ))
        return
      }

      let account
      try {
        account = await apiClient.getAccountInfo(accountId)
      } catch (error) {
        if (error.name !== "OAuthError") {
          throw error
        }

        // the account is not authorized, it was most likely added through the
        // settings synchronization
        await entityManager.persist(this[PRIVATE.getBlankAccountEntity](
          accountId,
          state
        ))
        return
      }

      if (!accountEnabled) {
        account.state = AccountState.DISABLED
      }
      await entityManager.persist(account)

      PRIVATE(this).eventBus.fire(
        AccountsSynchronizer.EVENTS.ACCOUNT_ADDED, account
      )
    })
  }

  [PRIVATE.onAccountEnabled](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  [PRIVATE.onAccountDisabled](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  [PRIVATE.onAccountRemoved](accountInfo: {id: string}) {
    throw new Error("Not implemented yet")
  }

  /**
   * Creates a new, blank, Google account entity for an account of the
   * specified ID and state.
   *
   * @param id The account's ID.
   * @param state The account's state.
   * @return The created (mostly) blank account entity.
   */
  [PRIVATE.getBlankAccountEntity](id: string, state: string): Account {
    return new Account({
      id,
      channelId: null,
      title: null,
      state,
      lastError: null,
      watchHistoryPlaylistId: null,
      watchLaterPlaylistId: null
    })
  }

  /**
   * Determines whether the specified account is enabled. Returns {@code null}
   * if the specified account is not among the accounts in the synchronized
   * storage.
   *
   * @param accountId The ID of the Google account.
   * @return {@code true} if the account is enabled, {@code false} if the
   *         account is disabled, or {@code null} if the account is not
   *         currently managed by the extension.
   */
  async [PRIVATE.isAccountEnabled](accountId: string): ?boolean {
    let accounts = await PRIVATE(this).syncStorage.getAccounts()
    let accountDetails = accounts.filter((account) => {
      return account.account === accountId
    })[0]

    return accountDetails && accountDetails.enabled
  }

  /**
   * Returns the ID of the user's current Google account.
   *
   * @return A promise that resolves to the ID of the user's current Google
   *         account.
   */
  [PRIVATE.getCurrentAccountId](): Promise<string> {
    return new Promise((resolve, reject) => {
      chrome.identity.getProfileUserInfo((userInfo) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message))
        }

        resolve(userInfo.id)
      })
    })
  }
}
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { loadSearchResults, loadYelpSearchResults } from '../../actions/search';
import { centerMap } from '../../actions/map';
import {
  List,
  NavItem,
  Pagination,
  ProgressIndicator,
  SearchResult,
  SideNav,
  YelpSearchResult
} from '../../components';

class SearchResultsPanel extends Component {

  constructor(props) {
    super(props);
    this.state = { activeNavItem: 'default' };
    this.handleSelectPrevPage = this.handleSelectPrevPage.bind(this);
    this.handleSelectNextPage = this.handleSelectNextPage.bind(this);
    this.handleSelectNavItem = this.handleSelectNavItem.bind(this);
    this.handleClickYelpSearchResult = this.handleClickYelpSearchResult.bind(this);
  }

  componentDidMount() {
    this.loadSearchResultsForActiveNavItem();
  }

  componentDidUpdate(prevProps) {
    const oldQuery = prevProps.params.query;
    const newQuery = this.props.params.query;

    if (newQuery !== oldQuery) {
      this.loadSearchResultsForActiveNavItem();
    }
  }

  renderContent() {
    const { handleSelectNavItem } = this;
    const { activeNavItem } = this.state;

    let searchResults = activeNavItem === 'yelp' ?
      this.props.yelpSearchResults : this.props.searchResults;

    if (!searchResults) return;

    const { isFetching, selectedPage } = searchResults;
    const resultsForPage = searchResults[selectedPage] || {};
    const { total } = resultsForPage;
    let content;

    if (isFetching) {
      content = (

          <ProgressIndicator />

      );
    } else if (!total) {
      content =
No results
;
    } else {
      let listItems;

      if (activeNavItem === 'yelp') {
        const { businesses } = resultsForPage;

        listItems = businesses.map(this.renderYelpSearchResult.bind(this));
      } else {
        const { results } = searchResults[selectedPage] || {};

        listItems = results.map(this.renderSearchResult.bind(this));
      }

      content = <List items={listItems} key="slug" />;
    }

    return (

        <SideNav>
          <NavItem
            name="default"
            icon="home"
            tooltip="Search"
            active={activeNavItem === 'default'}
            onClick={handleSelectNavItem}
          />
          <NavItem
            name="yelp"
            icon="yelp"
            tooltip="Search Yelp"
            active={activeNavItem === 'yelp'}
            onClick={handleSelectNavItem}
          />
        </SideNav>

          {content}

    );
  }

  renderFooter() {
    const { handleSelectPrevPage, handleSelectNextPage } = this;
    const { searchResults } = this.props;
    const { prevPageUrl, nextPageUrl } = searchResults;
    const hasPrev = !!prevPageUrl;
    const hasNext = !!nextPageUrl;

    return (
      <Pagination
        hasPrev={hasPrev}
        hasNext={hasNext}
        onSelectPrev={handleSelectPrevPage}
        onSelectNext={handleSelectNextPage}
      />
    );
  }

  renderSearchResult(searchResult) {
    return <SearchResult result={searchResult} />;
  }

  renderYelpSearchResult(searchResult) {
    return <YelpSearchResult {...searchResult} onClick={this.handleClickYelpSearchResult} />;
  }

  render() {

    return (

          {this.renderContent()}

          {this.renderFooter()}

    );
  }

  loadSearchResultsForActiveNavItem() {
    const { activeNavItem } = this.state;
    const { query } = this.props.params;

    if (activeNavItem === 'yelp') {
      this.props.loadYelpSearchResults(query);
    } else {
      this.props.loadSearchResults(query);
    }
  }

  handleSelectPrevPage() {
    const { query } = this.props.params;
    const { searchResults, loadSearchResults } = this.props;
    const { prevPageUrl } = searchResults;

    loadSearchResults(query, prevPageUrl);
  }

  handleSelectNextPage() {
    const { query } = this.props.params;
    const { searchResults, loadSearchResults } = this.props;
    const { nextPageUrl } = searchResults;

    loadSearchResults(query, nextPageUrl);
  }

  handleSelectNavItem(name) {
    this.setState({ activeNavItem: name },
      () => this.loadSearchResultsForActiveNavItem());
  }

  handleClickYelpSearchResult(result) {
    const yelpCoordinates = result.location.coordinate;
    const mapCoordinates = [yelpCoordinates.longitude, yelpCoordinates.latitude];

    this.props.centerMap(mapCoordinates);
  }

}

SearchResultsPanel.propTypes = {
  searchResults: PropTypes.object,
  loadSearchResults: PropTypes.func.isRequired,
  loadYelpSearchResults: PropTypes.func.isRequired,
  centerMap: PropTypes.func.isRequired
};

SearchResultsPanel.defaultProps = {
  searchResults: {}
};

function mapStateToProps(state, ownProps) {
  return {
    searchResults: state.pagination.searchResults[ownProps.params.query],
    yelpSearchResults: state.pagination.yelpSearchResults[ownProps.params.query]
  };
}

export default connect(mapStateToProps, {
  loadSearchResults,
  loadYelpSearchResults,
  centerMap
})(SearchResultsPanel);
<commit_msg>style(search results panel): remove tooltip from default nav item<commit_after>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { loadSearchResults, loadYelpSearchResults } from '../../actions/search';
import { centerMap } from '../../actions/map';
import {
  List,
  NavItem,
  Pagination,
  ProgressIndicator,
  SearchResult,
  SideNav,
  YelpSearchResult
} from '../../components';

class SearchResultsPanel extends Component {

  constructor(props) {
    super(props);
    this.state = { activeNavItem: 'default' };
    this.handleSelectPrevPage = this.handleSelectPrevPage.bind(this);
    this.handleSelectNextPage = this.handleSelectNextPage.bind(this);
    this.handleSelectNavItem = this.handleSelectNavItem.bind(this);
    this.handleClickYelpSearchResult = this.handleClickYelpSearchResult.bind(this);
  }

  componentDidMount() {
    this.loadSearchResultsForActiveNavItem();
  }

  componentDidUpdate(prevProps) {
    const oldQuery = prevProps.params.query;
    const newQuery = this.props.params.query;

    if (newQuery !== oldQuery) {
      this.loadSearchResultsForActiveNavItem();
    }
  }

  renderContent() {
    const { handleSelectNavItem } = this;
    const { activeNavItem } = this.state;

    let searchResults = activeNavItem === 'yelp' ?
      this.props.yelpSearchResults : this.props.searchResults;

    if (!searchResults) return;

    const { isFetching, selectedPage } = searchResults;
    const resultsForPage = searchResults[selectedPage] || {};
    const { total } = resultsForPage;
    let content;

    if (isFetching) {
      content = (

          <ProgressIndicator />

      );
    } else if (!total) {
      content =
No results
;
    } else {
      let listItems;

      if (activeNavItem === 'yelp') {
        const { businesses } = resultsForPage;

        listItems = businesses.map(this.renderYelpSearchResult.bind(this));
      } else {
        const { results } = searchResults[selectedPage] || {};

        listItems = results.map(this.renderSearchResult.bind(this));
      }

      content = <List items={listItems} key="slug" />;
    }

    return (

        <SideNav>
          <NavItem
            name="default"
            icon="home"
            active={activeNavItem === 'default'}
            onClick={handleSelectNavItem}
          />
          <NavItem
            name="yelp"
            icon="yelp"
            tooltip="Search Yelp"
            active={activeNavItem === 'yelp'}
            onClick={handleSelectNavItem}
          />
        </SideNav>

          {content}

    );
  }

  renderFooter() {
    const { handleSelectPrevPage, handleSelectNextPage } = this;
    const { searchResults } = this.props;
    const { prevPageUrl, nextPageUrl } = searchResults;
    const hasPrev = !!prevPageUrl;
    const hasNext = !!nextPageUrl;

    return (
      <Pagination
        hasPrev={hasPrev}
        hasNext={hasNext}
        onSelectPrev={handleSelectPrevPage}
        onSelectNext={handleSelectNextPage}
      />
    );
  }

  renderSearchResult(searchResult) {
    return <SearchResult result={searchResult} />;
  }

  renderYelpSearchResult(searchResult) {
    return <YelpSearchResult {...searchResult} onClick={this.handleClickYelpSearchResult} />;
  }

  render() {

    return (

          {this.renderContent()}

          {this.renderFooter()}

    );
  }

  loadSearchResultsForActiveNavItem() {
    const { activeNavItem } = this.state;
    const { query } = this.props.params;

    if (activeNavItem === 'yelp') {
      this.props.loadYelpSearchResults(query);
    } else {
      this.props.loadSearchResults(query);
    }
  }

  handleSelectPrevPage() {
    const { query } = this.props.params;
    const { searchResults, loadSearchResults } = this.props;
    const { prevPageUrl } = searchResults;

    loadSearchResults(query, prevPageUrl);
  }

  handleSelectNextPage() {
    const { query } = this.props.params;
    const { searchResults, loadSearchResults } = this.props;
    const { nextPageUrl } = searchResults;

    loadSearchResults(query, nextPageUrl);
  }

  handleSelectNavItem(name) {
    this.setState({ activeNavItem: name },
      () => this.loadSearchResultsForActiveNavItem());
  }

  handleClickYelpSearchResult(result) {
    const yelpCoordinates = result.location.coordinate;
    const mapCoordinates = [yelpCoordinates.longitude, yelpCoordinates.latitude];

    this.props.centerMap(mapCoordinates);
  }

}

SearchResultsPanel.propTypes = {
  searchResults: PropTypes.object,
  loadSearchResults: PropTypes.func.isRequired,
  loadYelpSearchResults: PropTypes.func.isRequired,
  centerMap: PropTypes.func.isRequired
};

SearchResultsPanel.defaultProps = {
  searchResults: {}
};

function mapStateToProps(state, ownProps) {
  return {
    searchResults: state.pagination.searchResults[ownProps.params.query],
    yelpSearchResults: state.pagination.yelpSearchResults[ownProps.params.query]
  };
}

export default connect(mapStateToProps, {
  loadSearchResults,
  loadYelpSearchResults,
  centerMap
})(SearchResultsPanel);
<|endoftext|>


<commit_before>import React, {
  useState,
  useEffect,
  useRef,
  useCallback
} from 'react';
import PropTypes from 'prop-types';
import propTypes from '@styled-system/prop-types';
import invariant from 'invariant';

import StyledSimpleSelect from './simple-select.style';
import SelectTextbox, { formInputPropTypes } from '../select-textbox/select-textbox.component';
import SelectList from '../select-list/select-list.component';
import guid from '../../../utils/helpers/guid';
import getNextChildByText from '../utils/get-next-child-by-text';
import { getNextChildByKey } from '../utils/get-next-child-by-key';

const SimpleSelect = React.forwardRef(({
  value,
  defaultValue,
  id,
  name,
  disabled,
  readOnly,
  children,
  transparent,
  openOnFocus = false,
  onOpen,
  onChange,
  onClick,
  onFocus,
  onKeyDown,
  ...props
}, inputRef) => {
  const selectListId = useRef(guid());
  const labelId = useRef(guid());
  const containerRef = useRef();
  const listboxRef = useRef();
  const filterTimer = useRef();
  const isMouseDownReported = useRef();
  const isControlled = useRef(value !== undefined);
  const isTimerCounting = useRef();
  const isClickTriggeredBySelect = useRef();
  const filterText = useRef();
  const [textboxRef, setTextboxRef] = useState();
  const [isOpen, setOpenState] = useState(false);
  const [textValue, setTextValue] = useState('');
  const [selectedValue, setSelectedValue] = useState('');

  const createCustomEvent = useCallback((newValue) => {
    const customEvent = {
      target: {
        ...(name && { name }),
        ...(id && { id }),
        value: newValue
      }
    };

    return customEvent;
  }, [name, id]);

  const setMatchingText = useCallback((newValue) => {
    const matchingOption = React.Children.toArray(children).find(option => (option.props.value === newValue));

    if (matchingOption) {
      setTextValue(matchingOption.props.text);
    }
  }, [children]);

  const selectValueStartingWithText = useCallback((newFilterText) => {
    setSelectedValue((previousValue) => {
      const previousIndex = React.Children.toArray(children).findIndex((child) => {
        return child.props.value === previousValue;
      });
      const match = getNextChildByText(newFilterText, children, previousIndex);

      if (!match) {
        return previousValue;
      }

      if (onChange) {
        onChange(createCustomEvent(match.props.value));
      }

      if (isControlled.current) {
        return previousValue;
      }

      setTextValue(match.props.text);

      return match.props.value;
    });
  }, [children, createCustomEvent, onChange]);

  const handleTextboxChange = useCallback((event) => {
    const newValue = event.target.value;
    const newCharacter = newValue.slice(-1);
    const isDeleteEvent = event.nativeEvent.inputType === 'deleteContentBackward'
    || event.nativeEvent.inputType === 'deleteContentForward'
    || event.nativeEvent.inputType === 'delete';

    if (isDeleteEvent) {
      return;
    }

    if (isTimerCounting.current) {
      const newVal = filterText.current + newCharacter;

      filterText.current = newVal;
      selectValueStartingWithText(newVal);
      clearTimeout(filterTimer.current);
    } else {
      filterText.current = newCharacter;
      selectValueStartingWithText(newCharacter);
    }

    isTimerCounting.current = true;
    clearTimeout(filterTimer.current);

    filterTimer.current = setTimeout(() => {
      isTimerCounting.current = false;
      filterText.current = '';
    }, 500);
  }, [selectValueStartingWithText]);

  const handleTextboxKeydown = useCallback(((event) => {
    const { key } = event;

    if (onKeyDown) {
      onKeyDown(event);
    }

    if (!event.defaultPrevented && isNavigationKey(event.key)) {
      setSelectedValue((previousSelectedValue) => {
        const nextElement = getNextChildByKey(key, children, previousSelectedValue);

        setTextValue(nextElement.props.text);

        return nextElement.props.value;
      });
    }

    if (key === 'Enter' || key === ' ' || isNavigationKey(key)) {
      event.preventDefault();

      setOpenState((isAlreadyOpen) => {
        if (!isAlreadyOpen && onOpen) {
          onOpen();
        }

        return true;
      });
    }
  }), [children, onKeyDown, onOpen]);

  const handleGlobalClick = useCallback((event) => {
    const notInContainer = containerRef.current && !containerRef.current.contains(event.target);

    if (notInContainer && !isClickTriggeredBySelect.current) {
      setMatchingText(selectedValue);
      setOpenState(false);
      filterText.current = '';
    }

    isClickTriggeredBySelect.current = false;
  }, [setMatchingText, selectedValue]);

  useEffect(() => {
    const newValue = value || defaultValue;
    const modeSwitchedMessage = 'Input elements should not switch from uncontrolled to controlled (or vice versa). '
    + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component';
    const onChageMissingMessage = 'onChange prop required when using a controlled input element';

    invariant(isControlled.current === (value !== undefined), modeSwitchedMessage);
    invariant(!isControlled.current || (isControlled.current && onChange), onChageMissingMessage);

    setSelectedValue(newValue);
    setMatchingText(newValue);
  }, [value, defaultValue, setMatchingText, onChange]);

  useEffect(() => {
    const clickEvent = 'click';

    document.addEventListener(clickEvent, handleGlobalClick);

    return function cleanup() {
      document.removeEventListener(clickEvent, handleGlobalClick);
    };
  }, [handleGlobalClick]);

  useEffect(() => {
    return function cleanup() {
      clearTimeout(filterTimer.current);
    };
  }, []);

  function handleDropdownIconClick(event) {
    handleTextboxClick(event);
  }

  function handleTextboxClick(event) {
    isMouseDownReported.current = false;

    if (onClick) {
      onClick(event);
    }

    setOpenState((isAlreadyOpen) => {
      if (isAlreadyOpen) {
        return false;
      }

      if (onOpen) {
        onOpen();
      }

      return true;
    });
  }

  function handleMouseDown() {
    isMouseDownReported.current = true;
  }

  function handleTextboxFocus(event) {
    if (isClickTriggeredBySelect.current) {
      return;
    }

    if (onFocus) {
      onFocus(event);
    }

    if (isMouseDownReported.current) {
      isMouseDownReported.current = false;

      return;
    }

    if (openOnFocus) {
      setOpenState((isAlreadyOpen) => {
        if (isAlreadyOpen) {
          return true;
        }

        if (onOpen) {
          onOpen();
        }

        return true;
      });
    }
  }

  function onSelectOption(optionData) {
    const { text, value: newValue, selectionType } = optionData;
    const isClickTriggered = selectionType === 'click';

    updateValue(newValue, text);

    if (selectionType !== 'navigationKey') {
      setOpenState(false);
      filterText.current = text;
    }

    if (isClickTriggered) {
      isClickTriggeredBySelect.current = true;
      textboxRef.focus();
    }
  }

  function updateValue(newValue, text) {
    if (!isControlled.current) {
      setSelectedValue(newValue);
      setTextValue(text);
    }

    if (onChange) {
      onChange(createCustomEvent(newValue));
    }
  }

  function onSelectListClose() {
    setOpenState(false);
    filterText.current = '';
    setMatchingText(selectedValue);
  }

  function isNavigationKey(key) {
    return key === 'ArrowDown' || key === 'ArrowUp'
    || key === 'Home' || key === 'End';
  }

  function assignInput(input) {
    setTextboxRef(input.current);

    if (inputRef) {
      inputRef.current = input.current;
    }
  }

  function getTextboxProps() {
    return {
      id,
      name,
      disabled,
      readOnly,
      inputRef: assignInput,
      selectedValue,
      formattedValue: textValue,
      onClick: handleTextboxClick,
      iconOnClick: handleDropdownIconClick,
      onMouseDown: handleMouseDown,
      onFocus: handleTextboxFocus,
      onKeyDown: handleTextboxKeydown,
      onChange: handleTextboxChange,
      ...props
    };
  }

  return (
    <StyledSimpleSelect
      data-component='simple-select'
      transparent={ transparent }
      disabled={ disabled }
      readOnly={ readOnly }
      aria-expanded={ isOpen }
      aria-haspopup='listbox'
      ref={ containerRef }
      { ...props }
    >
      <SelectTextbox
        aria-controls={ isOpen ? selectListId.current : '' }
        type='select'
        labelId={ labelId.current }
        { ...getTextboxProps() }
      />
      { isOpen && (
        <SelectList
          ref={ listboxRef }
          id={ selectListId.current }
          labelId={ labelId.current }
          anchorElement={ textboxRef.parentElement }
          onSelect={ onSelectOption }
          onSelectListClose={ onSelectListClose }
          highlightedValue={ selectedValue }
        >
          { children }
        </SelectList>
      ) }
    </StyledSimpleSelect>
  );
});

SimpleSelect.propTypes = {
  /** Styled system spacing props */
  ...propTypes.space,
  ...formInputPropTypes,
  /** The selected value(s), when the component is operating in controlled mode */
  value: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.object
  ]),
  /** The default selected value(s), when the component is operating in uncontrolled mode */
  defaultValue: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.object
  ]),
  /** Child components (such as Option) for the SelectList */
  children: PropTypes.node.isRequired,
  /** If true the Component opens on focus */
  openOnFocus: PropTypes.bool,
  /** If true the component input has no border and a transparent background */
  transparent: PropTypes.bool,
  /** A custom callback for when the dropdown menu opens */
  onOpen: PropTypes.func
};

export default SimpleSelect;
<commit_msg>fix(select): stop passing onblur to style wrapper<commit_after>import React, {
  useState,
  useEffect,
  useRef,
  useCallback
} from 'react';
import PropTypes from 'prop-types';
import propTypes from '@styled-system/prop-types';
import invariant from 'invariant';

import StyledSimpleSelect from './simple-select.style';
import SelectTextbox, { formInputPropTypes } from '../select-textbox/select-textbox.component';
import SelectList from '../select-list/select-list.component';
import guid from '../../../utils/helpers/guid';
import getNextChildByText from '../utils/get-next-child-by-text';
import { getNextChildByKey } from '../utils/get-next-child-by-key';

const SimpleSelect = React.forwardRef(({
  value,
  defaultValue,
  id,
  name,
  disabled,
  readOnly,
  children,
  transparent,
  openOnFocus = false,
  onOpen,
  onChange,
  onClick,
  onFocus,
  onKeyDown,
  onBlur,
  ...props
}, inputRef) => {
  const selectListId = useRef(guid());
  const labelId = useRef(guid());
  const containerRef = useRef();
  const listboxRef = useRef();
  const filterTimer = useRef();
  const isMouseDownReported = useRef();
  const isControlled = useRef(value !== undefined);
  const isTimerCounting = useRef();
  const isClickTriggeredBySelect = useRef();
  const filterText = useRef();
  const [textboxRef, setTextboxRef] = useState();
  const [isOpen, setOpenState] = useState(false);
  const [textValue, setTextValue] = useState('');
  const [selectedValue, setSelectedValue] = useState('');

  const createCustomEvent = useCallback((newValue) => {
    const customEvent = {
      target: {
        ...(name && { name }),
        ...(id && { id }),
        value: newValue
      }
    };

    return customEvent;
  }, [name, id]);

  const setMatchingText = useCallback((newValue) => {
    const matchingOption = React.Children.toArray(children).find(option => (option.props.value === newValue));

    if (matchingOption) {
      setTextValue(matchingOption.props.text);
    }
  }, [children]);

  const selectValueStartingWithText = useCallback((newFilterText) => {
    setSelectedValue((previousValue) => {
      const previousIndex = React.Children.toArray(children).findIndex((child) => {
        return child.props.value === previousValue;
      });
      const match = getNextChildByText(newFilterText, children, previousIndex);

      if (!match) {
        return previousValue;
      }

      if (onChange) {
        onChange(createCustomEvent(match.props.value));
      }

      if (isControlled.current) {
        return previousValue;
      }

      setTextValue(match.props.text);

      return match.props.value;
    });
  }, [children, createCustomEvent, onChange]);

  const handleTextboxChange = useCallback((event) => {
    const newValue = event.target.value;
    const newCharacter = newValue.slice(-1);
    const isDeleteEvent = event.nativeEvent.inputType === 'deleteContentBackward'
    || event.nativeEvent.inputType === 'deleteContentForward'
    || event.nativeEvent.inputType === 'delete';

    if (isDeleteEvent) {
      return;
    }

    if (isTimerCounting.current) {
      const newVal = filterText.current + newCharacter;

      filterText.current = newVal;
      selectValueStartingWithText(newVal);
      clearTimeout(filterTimer.current);
    } else {
      filterText.current = newCharacter;
      selectValueStartingWithText(newCharacter);
    }

    isTimerCounting.current = true;
    clearTimeout(filterTimer.current);

    filterTimer.current = setTimeout(() => {
      isTimerCounting.current = false;
      filterText.current = '';
    }, 500);
  }, [selectValueStartingWithText]);

  const handleTextboxKeydown = useCallback(((event) => {
    const { key } = event;

    if (onKeyDown) {
      onKeyDown(event);
    }

    if (!event.defaultPrevented && isNavigationKey(event.key)) {
      setSelectedValue((previousSelectedValue) => {
        const nextElement = getNextChildByKey(key, children, previousSelectedValue);

        setTextValue(nextElement.props.text);

        return nextElement.props.value;
      });
    }

    if (key === 'Enter' || key === ' ' || isNavigationKey(key)) {
      event.preventDefault();

      setOpenState((isAlreadyOpen) => {
        if (!isAlreadyOpen && onOpen) {
          onOpen();
        }

        return true;
      });
    }
  }), [children, onKeyDown, onOpen]);

  const handleGlobalClick = useCallback((event) => {
    const notInContainer = containerRef.current && !containerRef.current.contains(event.target);

    if (notInContainer && !isClickTriggeredBySelect.current) {
      setMatchingText(selectedValue);
      setOpenState(false);
      filterText.current = '';
    }

    isClickTriggeredBySelect.current = false;
  }, [setMatchingText, selectedValue]);

  useEffect(() => {
    const newValue = value || defaultValue;
    const modeSwitchedMessage = 'Input elements should not switch from uncontrolled to controlled (or vice versa). '
    + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component';
    const onChageMissingMessage = 'onChange prop required when using a controlled input element';

    invariant(isControlled.current === (value !== undefined), modeSwitchedMessage);
    invariant(!isControlled.current || (isControlled.current && onChange), onChageMissingMessage);

    setSelectedValue(newValue);
    setMatchingText(newValue);
  }, [value, defaultValue, setMatchingText, onChange]);

  useEffect(() => {
    const clickEvent = 'click';

    document.addEventListener(clickEvent, handleGlobalClick);

    return function cleanup() {
      document.removeEventListener(clickEvent, handleGlobalClick);
    };
  }, [handleGlobalClick]);

  useEffect(() => {
    return function cleanup() {
      clearTimeout(filterTimer.current);
    };
  }, []);

  function handleDropdownIconClick(event) {
    handleTextboxClick(event);
  }

  function handleTextboxClick(event) {
    isMouseDownReported.current = false;

    if (onClick) {
      onClick(event);
    }

    setOpenState((isAlreadyOpen) => {
      if (isAlreadyOpen) {
        return false;
      }

      if (onOpen) {
        onOpen();
      }

      return true;
    });
  }

  function handleMouseDown() {
    isMouseDownReported.current = true;
  }

  function handleTextboxFocus(event) {
    if (isClickTriggeredBySelect.current) {
      return;
    }

    if (onFocus) {
      onFocus(event);
    }

    if (isMouseDownReported.current) {
      isMouseDownReported.current = false;

      return;
    }

    if (openOnFocus) {
      setOpenState((isAlreadyOpen) => {
        if (isAlreadyOpen) {
          return true;
        }

        if (onOpen) {
          onOpen();
        }

        return true;
      });
    }
  }

  function onSelectOption(optionData) {
    const { text, value: newValue, selectionType } = optionData;
    const isClickTriggered = selectionType === 'click';

    updateValue(newValue, text);

    if (selectionType !== 'navigationKey') {
      setOpenState(false);
      filterText.current = text;
    }

    if (isClickTriggered) {
      isClickTriggeredBySelect.current = true;
      textboxRef.focus();
    }
  }

  function updateValue(newValue, text) {
    if (!isControlled.current) {
      setSelectedValue(newValue);
      setTextValue(text);
    }

    if (onChange) {
      onChange(createCustomEvent(newValue));
    }
  }

  function onSelectListClose() {
    setOpenState(false);
    filterText.current = '';
    setMatchingText(selectedValue);
  }

  function isNavigationKey(key) {
    return key === 'ArrowDown' || key === 'ArrowUp'
    || key === 'Home' || key === 'End';
  }

  function assignInput(input) {
    setTextboxRef(input.current);

    if (inputRef) {
      inputRef.current = input.current;
    }
  }

  function getTextboxProps() {
    return {
      id,
      name,
      disabled,
      readOnly,
      inputRef: assignInput,
      selectedValue,
      formattedValue: textValue,
      onClick: handleTextboxClick,
      iconOnClick: handleDropdownIconClick,
      onMouseDown: handleMouseDown,
      onFocus: handleTextboxFocus,
      onKeyDown: handleTextboxKeydown,
      onChange: handleTextboxChange,
      onBlur,
      ...props
    };
  }

  return (
    <StyledSimpleSelect
      data-component='simple-select'
      transparent={ transparent }
      disabled={ disabled }
      readOnly={ readOnly }
      aria-expanded={ isOpen }
      aria-haspopup='listbox'
      ref={ containerRef }
      { ...props }
    >
      <SelectTextbox
        aria-controls={ isOpen ? selectListId.current : '' }
        type='select'
        labelId={ labelId.current }
        { ...getTextboxProps() }
      />
      { isOpen && (
        <SelectList
          ref={ listboxRef }
          id={ selectListId.current }
          labelId={ labelId.current }
          anchorElement={ textboxRef.parentElement }
          onSelect={ onSelectOption }
          onSelectListClose={ onSelectListClose }
          highlightedValue={ selectedValue }
        >
          { children }
        </SelectList>
      ) }
    </StyledSimpleSelect>
  );
});

SimpleSelect.propTypes = {
  /** Styled system spacing props */
  ...propTypes.space,
  ...formInputPropTypes,
  /** The selected value(s), when the component is operating in controlled mode */
  value: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.object
  ]),
  /** The default selected value(s), when the component is operating in uncontrolled mode */
  defaultValue: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.object
  ]),
  /** Child components (such as Option) for the SelectList */
  children: PropTypes.node.isRequired,
  /** If true the Component opens on focus */
  openOnFocus: PropTypes.bool,
  /** If true the component input has no border and a transparent background */
  transparent: PropTypes.bool,
  /** A custom callback for when the dropdown menu opens */
  onOpen: PropTypes.func
};

export default SimpleSelect;
<|endoftext|>