<commit_before>import React, { Suspense, lazy } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Input from '@material-ui/core/Input'
import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import SearchIcon from '@material-ui/icons/Search'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Paper from '@material-ui/core/Paper'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { isSearchPageEnabled } from 'js/utils/feature-flags'
import {
  adblockerWhitelistingForSearchURL,
  dashboardURL,
  modifyURLParams,
} from 'js/navigation/navigation'
import { externalRedirect } from 'js/navigation/utils'
import LogoWithText from 'js/components/Logo/LogoWithText'
import { parseUrlSearchString } from 'js/utils/utils'
import SearchResults from 'js/components/Search/SearchResults'
import { isReactSnapClient } from 'js/utils/search-utils'
import SearchMenuQuery from 'js/components/Search/SearchMenuQuery'
import detectAdblocker from 'js/utils/detectAdblocker'
import Link from 'js/components/General/Link'

const Footer = lazy(() => import('js/components/General/Footer'))

const searchBoxBorderColor = '#ced4da'
const searchBoxBorderColorFocused = '#bdbdbd'

const styles = theme => ({
  inputRootStyle: {
    padding: 0,
    borderRadius: 28,
    backgroundColor: theme.palette.common.white,
    border: `1px solid ${searchBoxBorderColor}`,
    fontSize: 16,
    boxShadow: '0rem 0rem 0.02rem 0.02rem rgba(0, 0, 0, 0.1)',
    transition: theme.transitions.create(['border-color', 'box-shadow']),
    '&:hover': {
      borderColor: searchBoxBorderColorFocused,
      boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
    },
  },
  inputRootFocused: {
    borderColor: searchBoxBorderColorFocused,
    boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
  },
  inputStyle: {
    padding: '12px 16px',
  },
  tabsContainerRootStyle: {
    minHeight: 0,
  },
  tabRootStyle: {
    color: '#505050', // Same as search result description
    minHeight: 34,
    minWidth: 10,
  },
})

class SearchPage extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isAdBlockerEnabled: false,
      query: '',
      searchFeatureEnabled: isSearchPageEnabled(),
      searchSource: null,
      searchText: '',
      showPlaceholderText: false,
    }
  }

  componentDidMount() {
    if (!this.state.searchFeatureEnabled) {
      // Cannot use pushState now that the apps are separate.
      externalRedirect(dashboardURL)
    }
    const { location } = this.props

    // Wait until after mount to update prerendered state.
    const query = parseUrlSearchString(location.search).q || ''
    this.setState({
      // We always derive the query and page values URL parameter
      // values. We keep tehse in state so that we update the
      // prerendered components after mount, because at prerender
      // time we do not know the query string. We can remove this
      // from state if we switch to server-side rendering.
      query: query,
      page: this.getPageNumberFromSearchString(location.search),
      showPlaceholderText: !isReactSnapClient(),
      searchSource: parseUrlSearchString(location.search).src || null,
      searchText: query,
    })

    // AdBlockerDetection
    detectAdblocker()
      .then(isEnabled => {
        this.setState({
          isAdBlockerEnabled: isEnabled,
        })
      })
      .catch(e => {
        console.error(e)
      })
  }

  componentDidUpdate(prevProps) {
    const { location } = this.props
    const currentQuery = parseUrlSearchString(location.search).q
    const prevQuery = parseUrlSearchString(prevProps.location.search).q
    if (currentQuery !== prevQuery) {
      this.setState({
        query: currentQuery,
        searchText: currentQuery,
      })
    }

    // Check if the page number has changed.
    const currentPage = this.getPageNumberFromSearchString(location.search)
    const prevPage = this.getPageNumberFromSearchString(
      prevProps.location.search
    )
    if (currentPage !== prevPage) {
      this.setState({
        page: currentPage,
      })
    }
  }

  /**
   * Take a search string, such as ?abc=hi&p=12, and return the
   * integer value of the "page" URL parameter. If the parameter is
   * not set or is not an integer, return 1.
   * @param {String} searchStr - The URL parameter string,
   *   such as '?myParam=foo&another=bar'
   * @return {Number} The search results page inded
   */
  getPageNumberFromSearchString(searchStr) {
    return parseInt(parseUrlSearchString(searchStr).page, 10) || 1
  }

  search() {
    const newQuery = this.state.searchText
    if (newQuery) {
      modifyURLParams({
        page: 1,
        q: newQuery,
        src: 'self',
      })
      this.setState({
        searchSource: 'self',
      })
    }
  }

  onSearchTextChange(e) {
    this.setState({
      searchText: e.target.value,
    })
  }

  render() {
    const { classes } = this.props

    // For demonstration only
    let isAdBlockerEnabled = false
    const {
      // isAdBlockerEnabled,
      page,
      query,
      searchSource,
      searchText,
    } = this.state
    const queryEncoded = query ? encodeURI(query) : ''
    const searchResultsPaddingLeft = 170
    if (!this.state.searchFeatureEnabled) {
      return null
    }
    return (
      <div
        data-test-id={'search-page'}
        style={{
          backgroundColor: '#fff',
          // For demonstration only
          minWidth: 1100,
          // minWidth: '100vw',
          minHeight: '100vh',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div
          style={{
            backgroundColor: '#fff',
            borderBottom: '1px solid #e4e4e4',
          }}
        >
          <div
            style={{
              padding: '20px 18px 6px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-start',
            }}
          >
            <LogoWithText
              style={{
                width: 100,
                height: 36,
              }}
            />
            <div
              style={{
                maxWidth: 600,
                marginLeft: 30,
                flex: 1,
              }}
            >
              <Input
                id="search-input"
                type={'text'}
                value={searchText}
                onChange={this.onSearchTextChange.bind(this)}
                onKeyPress={e => {
                  if (e.key === 'Enter') {
                    this.search()
                  }
                }}
                placeholder={
                  // Don't immediately render the placeholder text because
                  // we may rapidly replace it with the query on first render.
                  this.state.showPlaceholderText
                    ? 'Search to raise money for charity...'
                    : ''
                }
                disableUnderline
                fullWidth
                classes={{
                  root: classes.inputRootStyle,
                  input: classes.inputStyle,
                  focused: classes.inputRootFocused,
                }}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="Search button"
                      onClick={this.search.bind(this)}
                    >
                      <SearchIcon
                        style={{ color: searchBoxBorderColorFocused }}
                      />
                    </IconButton>
                  </InputAdornment>
                }
              />
            </div>
            <SearchMenuQuery
              style={{
                marginLeft: 'auto',
              }}
            />
          </div>
          <Tabs
            value={0}
            indicatorColor={'secondary'}
            style={{
              marginTop: 8,
              marginLeft: 149,
            }}
            classes={{
              root: classes.tabsContainerRootStyle,
            }}
          >
            <Tab
              label="Web"
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Images"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=isch`
                  : 'https://images.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="News"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=nws`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Videos"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=vid`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Maps"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/maps/?q=${queryEncoded}`
                  : 'https://www.google.com/maps'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
          </Tabs>
        </div>
        <div
          style={{
            display: 'flex',
          }}
        >
          <div
            style={{
              marginLeft: searchResultsPaddingLeft,
              marginTop: 20,
              width: 600,
            }}
          >
            {isAdBlockerEnabled ? (
              <div
                data-test-id={'search-prevented-warning'}
                style={{
                  marginBottom: 20,
                  width: '100%',
                }}
              >
                <Paper
                  style={{
                    padding: '10px 18px',
                    backgroundColor: 'rgb(242, 222, 222)',
                  }}
                >
                  <span
                    style={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'flex-start',
                    }}
                  >
                    <Typography
                      style={{
                        color: 'rgb(169, 68, 66)',
                        fontWeight: 'bold',
                        marginBottom: 8,
                        marginTop: 8,
                      }}
                      variant={'h6'}
                    >
                      Please disable your ad blocker
                    </Typography>
                    <Typography variant={'body2'}>
                      We use search ads to raise money for charity. You'll
                      likely need to whitelist Search for a Cause for search
                      results to show.
                    </Typography>
                    <div
                      style={{
                        display: 'flex',
                        alignSelf: 'flex-end',
                        marginTop: 10,
                      }}
                    >
                      <Link
                        to={adblockerWhitelistingForSearchURL}
                        target="_blank"
                        rel="noopener noreferrer"
                      >
                        <Button color={'default'}>Show me how</Button>
                      </Link>
                    </div>
                  </span>
                </Paper>
              </div>
            ) : null}
            <SearchResults
              query={query}
              page={page}
              onPageChange={newPageIndex => {
                modifyURLParams({
                  page: newPageIndex,
                })
              }}
              searchSource={searchSource}
              style={{
                maxWidth: 600,
                marginBottom: 40,
                height: 700,
                // For demonstration only
                backgroundColor: 'aqua',
              }}
            />
          </div>
          <div
            data-test-id={'search-sidebar'}
            style={{
              display: 'flex',
              margin: '20px 40px',
              width: '100%',
              maxWidth: 410,
              minWidth: 300,
              // For demonstration only
              backgroundColor: 'green',
            }}
          >
            <div
              style={{
                // For demonstration only
                height: 500,
                width: '100%',
                backgroundColor: 'purple',
              }}
            >
              some stuff here
            </div>
          </div>
        </div>
        <Suspense fallback={null}>
          <Footer
            style={{
              marginTop: 'auto',
            }}
          />
        </Suspense>
      </div>
    )
  }
}

SearchPage.propTypes = {
  classes: PropTypes.object.isRequired,
  location: PropTypes.shape({
    search: PropTypes.string.isRequired,
  }),
  theme: PropTypes.object.isRequired,
}

SearchPage.defaultProps = {}

export default withStyles(styles, { withTheme: true })(SearchPage)
<commit_msg>Add an introduction notification in the sidebar<commit_after>import React, { Suspense, lazy } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Input from '@material-ui/core/Input'
import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import SearchIcon from '@material-ui/icons/Search'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Paper from '@material-ui/core/Paper'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { isSearchPageEnabled } from 'js/utils/feature-flags'
import {
  adblockerWhitelistingForSearchURL,
  dashboardURL,
  modifyURLParams,
} from 'js/navigation/navigation'
import { externalRedirect } from 'js/navigation/utils'
import LogoWithText from 'js/components/Logo/LogoWithText'
import { parseUrlSearchString } from 'js/utils/utils'
import SearchResults from 'js/components/Search/SearchResults'
import { isReactSnapClient } from 'js/utils/search-utils'
import SearchMenuQuery from 'js/components/Search/SearchMenuQuery'
import detectAdblocker from 'js/utils/detectAdblocker'
import Link from 'js/components/General/Link'

const Footer = lazy(() => import('js/components/General/Footer'))

const searchBoxBorderColor = '#ced4da'
const searchBoxBorderColorFocused = '#bdbdbd'

const styles = theme => ({
  inputRootStyle: {
    padding: 0,
    borderRadius: 28,
    backgroundColor: theme.palette.common.white,
    border: `1px solid ${searchBoxBorderColor}`,
    fontSize: 16,
    boxShadow: '0rem 0rem 0.02rem 0.02rem rgba(0, 0, 0, 0.1)',
    transition: theme.transitions.create(['border-color', 'box-shadow']),
    '&:hover': {
      borderColor: searchBoxBorderColorFocused,
      boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
    },
  },
  inputRootFocused: {
    borderColor: searchBoxBorderColorFocused,
    boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
  },
  inputStyle: {
    padding: '12px 16px',
  },
  tabsContainerRootStyle: {
    minHeight: 0,
  },
  tabRootStyle: {
    color: '#505050', // Same as search result description
    minHeight: 34,
    minWidth: 10,
  },
})

class SearchPage extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isAdBlockerEnabled: false,
      query: '',
      searchFeatureEnabled: isSearchPageEnabled(),
      searchSource: null,
      searchText: '',
      showPlaceholderText: false,
    }
  }

  componentDidMount() {
    if (!this.state.searchFeatureEnabled) {
      // Cannot use pushState now that the apps are separate.
      externalRedirect(dashboardURL)
    }
    const { location } = this.props

    // Wait until after mount to update prerendered state.
    const query = parseUrlSearchString(location.search).q || ''
    this.setState({
      // We always derive the query and page values URL parameter
      // values. We keep tehse in state so that we update the
      // prerendered components after mount, because at prerender
      // time we do not know the query string. We can remove this
      // from state if we switch to server-side rendering.
      query: query,
      page: this.getPageNumberFromSearchString(location.search),
      showPlaceholderText: !isReactSnapClient(),
      searchSource: parseUrlSearchString(location.search).src || null,
      searchText: query,
    })

    // AdBlockerDetection
    detectAdblocker()
      .then(isEnabled => {
        this.setState({
          isAdBlockerEnabled: isEnabled,
        })
      })
      .catch(e => {
        console.error(e)
      })
  }

  componentDidUpdate(prevProps) {
    const { location } = this.props
    const currentQuery = parseUrlSearchString(location.search).q
    const prevQuery = parseUrlSearchString(prevProps.location.search).q
    if (currentQuery !== prevQuery) {
      this.setState({
        query: currentQuery,
        searchText: currentQuery,
      })
    }

    // Check if the page number has changed.
    const currentPage = this.getPageNumberFromSearchString(location.search)
    const prevPage = this.getPageNumberFromSearchString(
      prevProps.location.search
    )
    if (currentPage !== prevPage) {
      this.setState({
        page: currentPage,
      })
    }
  }

  /**
   * Take a search string, such as ?abc=hi&p=12, and return the
   * integer value of the "page" URL parameter. If the parameter is
   * not set or is not an integer, return 1.
   * @param {String} searchStr - The URL parameter string,
   *   such as '?myParam=foo&another=bar'
   * @return {Number} The search results page inded
   */
  getPageNumberFromSearchString(searchStr) {
    return parseInt(parseUrlSearchString(searchStr).page, 10) || 1
  }

  search() {
    const newQuery = this.state.searchText
    if (newQuery) {
      modifyURLParams({
        page: 1,
        q: newQuery,
        src: 'self',
      })
      this.setState({
        searchSource: 'self',
      })
    }
  }

  onSearchTextChange(e) {
    this.setState({
      searchText: e.target.value,
    })
  }

  render() {
    const { classes } = this.props

    // For demonstration only
    let isAdBlockerEnabled = true
    const {
      // isAdBlockerEnabled,
      page,
      query,
      searchSource,
      searchText,
    } = this.state
    const queryEncoded = query ? encodeURI(query) : ''
    const searchResultsPaddingLeft = 170
    if (!this.state.searchFeatureEnabled) {
      return null
    }
    return (
      <div
        data-test-id={'search-page'}
        style={{
          backgroundColor: '#fff',
          // For demonstration only
          minWidth: 1100,
          // minWidth: '100vw',
          minHeight: '100vh',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div
          style={{
            backgroundColor: '#fff',
            borderBottom: '1px solid #e4e4e4',
          }}
        >
          <div
            style={{
              padding: '20px 18px 6px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-start',
            }}
          >
            <LogoWithText
              style={{
                width: 100,
                height: 36,
              }}
            />
            <div
              style={{
                maxWidth: 600,
                marginLeft: 30,
                flex: 1,
              }}
            >
              <Input
                id="search-input"
                type={'text'}
                value={searchText}
                onChange={this.onSearchTextChange.bind(this)}
                onKeyPress={e => {
                  if (e.key === 'Enter') {
                    this.search()
                  }
                }}
                placeholder={
                  // Don't immediately render the placeholder text because
                  // we may rapidly replace it with the query on first render.
                  this.state.showPlaceholderText
                    ? 'Search to raise money for charity...'
                    : ''
                }
                disableUnderline
                fullWidth
                classes={{
                  root: classes.inputRootStyle,
                  input: classes.inputStyle,
                  focused: classes.inputRootFocused,
                }}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="Search button"
                      onClick={this.search.bind(this)}
                    >
                      <SearchIcon
                        style={{ color: searchBoxBorderColorFocused }}
                      />
                    </IconButton>
                  </InputAdornment>
                }
              />
            </div>
            <SearchMenuQuery
              style={{
                marginLeft: 'auto',
              }}
            />
          </div>
          <Tabs
            value={0}
            indicatorColor={'secondary'}
            style={{
              marginTop: 8,
              marginLeft: 149,
            }}
            classes={{
              root: classes.tabsContainerRootStyle,
            }}
          >
            <Tab
              label="Web"
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Images"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=isch`
                  : 'https://images.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="News"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=nws`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Videos"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=vid`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Maps"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/maps/?q=${queryEncoded}`
                  : 'https://www.google.com/maps'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
          </Tabs>
        </div>
        <div
          style={{
            display: 'flex',
          }}
        >
          <div
            style={{
              marginLeft: searchResultsPaddingLeft,
              marginTop: 20,
              width: 600,
            }}
          >
            {isAdBlockerEnabled ? (
              <div
                data-test-id={'search-prevented-warning'}
                style={{
                  marginBottom: 20,
                  width: '100%',
                }}
              >
                <Paper
                  style={{
                    padding: '10px 18px',
                    backgroundColor: 'rgb(242, 222, 222)',
                  }}
                >
                  <span
                    style={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'flex-start',
                    }}
                  >
                    <Typography
                      style={{
                        color: 'rgb(169, 68, 66)',
                        fontWeight: 'bold',
                        marginBottom: 8,
                        marginTop: 8,
                      }}
                      variant={'h6'}
                    >
                      Please disable your ad blocker
                    </Typography>
                    <Typography variant={'body2'}>
                      We use search ads to raise money for charity. You'll
                      likely need to whitelist Search for a Cause for search
                      results to show.
                    </Typography>
                    <div
                      style={{
                        display: 'flex',
                        alignSelf: 'flex-end',
                        marginTop: 10,
                      }}
                    >
                      <Link
                        to={adblockerWhitelistingForSearchURL}
                        target="_blank"
                        rel="noopener noreferrer"
                      >
                        <Button color={'default'}>Show me how</Button>
                      </Link>
                    </div>
                  </span>
                </Paper>
              </div>
            ) : null}
            <SearchResults
              query={query}
              page={page}
              onPageChange={newPageIndex => {
                modifyURLParams({
                  page: newPageIndex,
                })
              }}
              searchSource={searchSource}
              style={{
                maxWidth: 600,
                marginBottom: 40,
                height: 700,
                // For demonstration only
                backgroundColor: 'aqua',
              }}
            />
          </div>
          <div
            data-test-id={'search-sidebar'}
            style={{
              display: 'flex',
              flexDirection: 'column',
              margin: '20px 40px',
              width: '100%',
              boxSizing: 'border-box',
              maxWidth: 410,
              minWidth: 300,
              // For demonstration only
              backgroundColor: 'green',
            }}
          >
            <div
              style={{
                // For demonstration only
                height: 500,
                width: '100%',
                backgroundColor: 'purple',
              }}
            >
              some stuff here
            </div>
            <Paper
              style={{
                width: '100%',
                boxSizing: 'border-box',
                padding: '10px 18px',
              }}
            >
              <span
                style={{
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'flex-start',
                }}
              >
                <Typography variant={'h6'}>Your searches do good!</Typography>
                <Typography variant={'body2'}>
                  When you search, you raise money for charity! The money comes
                  from the ads in search results, and you decide where the money
                  goes by donating your hearts to your favorite nonprofit.
                </Typography>
                <div
                  style={{
                    display: 'flex',
                    alignSelf: 'flex-end',
                    marginTop: 10,
                  }}
                >
                  <Button color={'primary'}>Got it!</Button>
                </div>
              </span>
            </Paper>
          </div>
        </div>
        <Suspense fallback={null}>
          <Footer
            style={{
              marginTop: 'auto',
            }}
          />
        </Suspense>
      </div>
    )
  }
}

SearchPage.propTypes = {
  classes: PropTypes.object.isRequired,
  location: PropTypes.shape({
    search: PropTypes.string.isRequired,
  }),
  theme: PropTypes.object.isRequired,
}

SearchPage.defaultProps = {}

export default withStyles(styles, { withTheme: true })(SearchPage)
<|endoftext|>


<commit_before>const fixtures = require('../../fixtures')
const urls = require('../../../../../src/lib/urls')
const selectors = require('../../../../selectors')
const {
  assertLocalHeader,
  assertBreadcrumbs,
} = require('../../support/assertions')

const selectTypeahead = (fieldName, input, spanNumber) =>
  cy.contains(fieldName).within(() => {
    cy.get('div')
      .eq(0)
      .type(input)
      .type('{enter}')
  })

describe('Send a referral form', () => {
  before(() => {
    cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
  })

  describe('All but successful completion', () => {
    beforeEach(() => {
      cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
    })

    context('when viewing the "send referral" form', () => {
      it('should display breadcrumbs', () => {
        assertBreadcrumbs({
          Home: urls.dashboard(),
          Companies: urls.companies.index(),
          'Venus Ltd': urls.companies.detail(fixtures.company.withContacts.id),
          'Send a referral': null,
        })
      })

      it('should display the header', () => {
        assertLocalHeader('Send a referral')
      })

      it('should display the headings and four fields', () => {
        cy.get(selectors.companySendReferral.form)
          .should('contain', 'Adviser')
          .and('contain', 'Subject')
          .and('contain', 'Notes')
          .and('contain', 'Company contact (optional')
        cy.get(selectors.companySendReferral.adviserField).should('be.visible')
        cy.get(selectors.companySendReferral.subjectField).should('be.visible')
        cy.get(selectors.companySendReferral.notesField).should('be.visible')
        cy.get(selectors.companySendReferral.contactField).should('be.visible')
      })

      it('should display "Continue" button', () => {
        cy.get(selectors.companySendReferral.continueButton).should(
          'be.visible'
        )
      })

      it('should display "Cancel" link', () => {
        cy.get(selectors.companySendReferral.cancelLink).should('be.visible')
      })
    })

    context('When the "Cancel" link is clicked', () => {
      it('should return to the company page', () => {
        cy.get(selectors.companySendReferral.cancelLink).click()
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
      })
    })

    context(
      'when "Continue" button is clicked without filling in mandatory fields',
      () => {
        it('should display two error messages', () => {
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form)
            .should('contain', 'Select an adviser for the referral')
            .and('contain', 'Enter a subject for the referral')
        })
      }
    )

    context(
      'when "Continue" button is clicked without chosing an adviser',
      () => {
        it('should display error message', () => {
          cy.get(selectors.companySendReferral.subjectField)
            .click()
            .type('Example subject')
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form).contains(
            'Select an adviser for the referral'
          )
        })
      }
    )

    context(
      'when "Continue" button is clicked without specifying a subject',
      () => {
        it('should display error message', () => {
          selectTypeahead('Adviser', 'Shawn')
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form).contains(
            'Enter a subject for the referral'
          )
        })
      }
    )

    context(
      'when more than 255 characters are entered in the subject field',
      () => {
        it('should display error message', () => {
          cy.get(selectors.companySendReferral.subjectField)
            .click()
            .type(
              'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nec interdum velit. Nulla facilisi. In ultricies sed ex at malesuada. Proin quis faucibus felis, iaculis malesuada sem. Maecenas semper elit magna, tincidunt consectetur risus volutpat at. Sed'
            )
          cy.get(selectors.companySendReferral.form).contains(
            'Subject must be 255 characters or less'
          )
        })
      }
    )

    context('when the text in the subject field is removed', () => {
      it('should display error message', () => {
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('example subject')
        cy.get(selectors.companySendReferral.subjectField).clear()
        cy.get(selectors.companySendReferral.form).contains(
          'Enter a subject for the referral'
        )
      })
    }),
      context(
        'when "Continue" button is clicked with just mandatory fields filled in',
        () => {
          it('should display confirmation component', () => {
            selectTypeahead('Adviser', 'Shawn')
            cy.get(selectors.companySendReferral.subjectField)
              .click()
              .type('Example subject')
              .get(selectors.companySendReferral.continueButton)
              .click()
            cy.contains('Check referral details').should('be.visible')
          })
        }
      )
  })

  context(
    'when "Continue" button is clicked after all fields filled in',
    () => {
      it('should display the confirmation component with the values just input', () => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
        selectTypeahead('Adviser', 'Shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.notesField)
          .click()
          .type('Example notes')
        selectTypeahead('Company contact', 'Johnny')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.get('table')
          .should('contain', 'Shawn Cohen')
          .and('contain', 'Example subject')
          .and('contain', 'Example notes')
          .and('contain', 'Johnny Cakeman')
        cy.contains('Edit referral').should('be.visible')
        cy.get('h4').should('contain', 'What happens next')
        cy.get('p')
          .eq(2)
          .should(
            'have.text',
            'Clicking “Send referral” will show the referral in the activity of Venus Ltd, as well as in the Referrals section on both your Data Hub' +
              ' Homepage and the Homepage of the recipient.' +
              'It might take up to 24 hours for the referral to appear.' +
              'You will not be able to edit the referral after this point.'
          )
        cy.get('button')
          .eq(2)
          .should('be.visible')
        cy.contains('Cancel').should('be.visible')
      })
    }
  )

  context(
    'when "Continue" button is clicked after all fields filled in',
    () => {
      it('the input data should appear in the form when "Edit referral" is clicked', () => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
        selectTypeahead('Adviser', 'Shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.notesField)
          .click()
          .type('Example notes')
        selectTypeahead('Company contact', 'Johnny')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.contains('Edit referral').click()
        cy.get(selectors.companySendReferral.adviserField).should(
          'contain',
          'Shawn Cohen'
        )
        cy.get(selectors.companySendReferral.subjectField).should(
          'have.attr',
          'value',
          'Example subject'
        )
        cy.get(selectors.companySendReferral.notesField).should(
          'have.text',
          'Example notes'
        )
        cy.get(selectors.companySendReferral.contactField).should(
          'contain',
          'Johnny Cakeman'
        )
      })
    }
  )

  context(
    'When the "Cancel" link is clicked from the confirmation component',
    () => {
      it('should return to the company page', () => {
        selectTypeahead('Adviser', 'Shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.contains('Cancel').click()
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
      })
    }
  )

  context(
    'When the "Send referral" button is clicked from the confirmation component',
    () => {
      before(() => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
      })
      it('should take user to the company page, display flash message and link to the homepage', () => {
        selectTypeahead('Adviser', 'Shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.get('button')
          .eq(2)
          .click() //Fix reference to send referral button
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
        cy.get(selectors.localHeader().flash).should('contain', 'Referral sent')
        cy.contains('find your referrals on the Homepage').click()
        cy.url().should('contain', urls.dashboard())
      })
    }
  )
})
<commit_msg>Enhance the way we selet typeahead<commit_after>const fixtures = require('../../fixtures')
const urls = require('../../../../../src/lib/urls')
const selectors = require('../../../../selectors')
const {
  assertLocalHeader,
  assertBreadcrumbs,
} = require('../../support/assertions')

const selectTypeahead = (fieldName, input, spanNumber) =>
  cy.contains(fieldName).within(() => {
    cy.server()
    cy.route('/api-proxy/adviser/?*').as('adviserResults')
    cy.get('div')
      .eq(0)
      .type(input)
    cy.wait('@adviserResults')
    cy.get('[class*="MenuList"] > div > span').click()
  })

describe('Send a referral form', () => {
  before(() => {
    cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
  })

  describe('All but successful completion', () => {
    beforeEach(() => {
      cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
    })

    context('when viewing the "send referral" form', () => {
      it('should display breadcrumbs', () => {
        assertBreadcrumbs({
          Home: urls.dashboard(),
          Companies: urls.companies.index(),
          'Venus Ltd': urls.companies.detail(fixtures.company.withContacts.id),
          'Send a referral': null,
        })
      })

      it('should display the header', () => {
        assertLocalHeader('Send a referral')
      })

      it('should display the headings and four fields', () => {
        cy.get(selectors.companySendReferral.form)
          .should('contain', 'Adviser')
          .and('contain', 'Subject')
          .and('contain', 'Notes')
          .and('contain', 'Company contact (optional')
        cy.get(selectors.companySendReferral.adviserField).should('be.visible')
        cy.get(selectors.companySendReferral.subjectField).should('be.visible')
        cy.get(selectors.companySendReferral.notesField).should('be.visible')
        cy.get(selectors.companySendReferral.contactField).should('be.visible')
      })

      it('should display "Continue" button', () => {
        cy.get(selectors.companySendReferral.continueButton).should(
          'be.visible'
        )
      })

      it('should display "Cancel" link', () => {
        cy.get(selectors.companySendReferral.cancelLink).should('be.visible')
      })
    })

    context('When the "Cancel" link is clicked', () => {
      it('should return to the company page', () => {
        cy.get(selectors.companySendReferral.cancelLink).click()
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
      })
    })

    context(
      'when "Continue" button is clicked without filling in mandatory fields',
      () => {
        it('should display two error messages', () => {
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form)
            .should('contain', 'Select an adviser for the referral')
            .and('contain', 'Enter a subject for the referral')
        })
      }
    )

    context(
      'when "Continue" button is clicked without chosing an adviser',
      () => {
        it('should display error message', () => {
          cy.get(selectors.companySendReferral.subjectField)
            .click()
            .type('Example subject')
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form).contains(
            'Select an adviser for the referral'
          )
        })
      }
    )

    context(
      'when "Continue" button is clicked without specifying a subject',
      () => {
        it('should display error message', () => {
          selectTypeahead('Adviser', 'shawn')
          cy.get(selectors.companySendReferral.continueButton).click()
          cy.get(selectors.companySendReferral.form).should(
            'contain',
            'Enter a subject for the referral'
          )
        })
      }
    )

    context(
      'when more than 255 characters are entered in the subject field',
      () => {
        it('should display error message', () => {
          cy.get(selectors.companySendReferral.subjectField)
            .click()
            .type(
              'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nec interdum velit. Nulla facilisi. In ultricies sed ex at malesuada. Proin quis faucibus felis, iaculis malesuada sem. Maecenas semper elit magna, tincidunt consectetur risus volutpat at. Sed'
            )
          cy.get(selectors.companySendReferral.form).should(
            'contain',
            'Subject must be 255 characters or less'
          )
        })
      }
    )

    context('when the text in the subject field is removed', () => {
      it('should display error message', () => {
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('example subject')
        cy.get(selectors.companySendReferral.subjectField).clear()
        cy.get(selectors.companySendReferral.form).should(
          'contain',
          'Enter a subject for the referral'
        )
      })
    }),
      context(
        'when "Continue" button is clicked with just mandatory fields filled in',
        () => {
          it('should display confirmation component', () => {
            selectTypeahead('Adviser', 'shawn')
            cy.get(selectors.companySendReferral.subjectField)
              .click()
              .type('Example subject')
              .get(selectors.companySendReferral.continueButton)
              .click()
            cy.contains('Check referral details').should('be.visible')
          })
        }
      )
  })

  context(
    'when "Continue" button is clicked after all fields filled in',
    () => {
      it('should display the confirmation component with the values just input', () => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
        selectTypeahead('Adviser', 'shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.notesField)
          .click()
          .type('Example notes')
        selectTypeahead('Company contact', 'johnny')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.get('table')
          .should('contain', 'Shawn Cohen')
          .and('contain', 'Example subject')
          .and('contain', 'Example notes')
          .and('contain', 'Johnny Cakeman')
        cy.contains('Edit referral').should('be.visible')
        cy.get('h4').should('contain', 'What happens next')
        cy.get('p')
          .eq(2)
          .should(
            'have.text',
            'Clicking “Send referral” will show the referral in the activity of Venus Ltd, as well as in the Referrals section on both your Data Hub' +
              ' Homepage and the Homepage of the recipient.' +
              'It might take up to 24 hours for the referral to appear.' +
              'You will not be able to edit the referral after this point.'
          )
        cy.get('button')
          .eq(2)
          .should('be.visible')
        cy.contains('Cancel').should('be.visible')
      })
    }
  )

  context(
    'when "Continue" button is clicked after all fields filled in',
    () => {
      it('the input data should appear in the form when "Edit referral" is clicked', () => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
        selectTypeahead('Adviser', 'shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.notesField)
          .click()
          .type('Example notes')
        selectTypeahead('Company contact', 'johnny')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.contains('Edit referral').click()
        cy.get(selectors.companySendReferral.adviserField).should(
          'contain',
          'Shawn Cohen'
        )
        cy.get(selectors.companySendReferral.subjectField).should(
          'have.attr',
          'value',
          'Example subject'
        )
        cy.get(selectors.companySendReferral.notesField).should(
          'have.text',
          'Example notes'
        )
        cy.get(selectors.companySendReferral.contactField).should(
          'contain',
          'Johnny Cakeman'
        )
      })
    }
  )

  context(
    'When the "Cancel" link is clicked from the confirmation component',
    () => {
      it('should return to the company page', () => {
        selectTypeahead('Adviser', 'shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.contains('Cancel').click()
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
      })
    }
  )

  context(
    'When the "Send referral" button is clicked from the confirmation component',
    () => {
      before(() => {
        cy.visit(urls.companies.sendReferral(fixtures.company.withContacts.id))
      })
      it('should take user to the company page, display flash message and link to the homepage', () => {
        selectTypeahead('Adviser', 'shawn')
        cy.get(selectors.companySendReferral.subjectField)
          .click()
          .type('Example subject')
          .get(selectors.companySendReferral.continueButton)
          .click()
        cy.get('button')
          .eq(2)
          .click() //Fix reference to send referral button
        cy.url().should(
          'contain',
          urls.companies.activity.index(fixtures.company.withContacts.id)
        )
        cy.get(selectors.localHeader().flash).should('contain', 'Referral sent')
        cy.contains('find your referrals on the Homepage').click()
        cy.url().should('contain', urls.dashboard())
      })
    }
  )
})
<|endoftext|>


<commit_before>var _ = require('underscore');
var MBTiles = require('mbtiles');
var iconv = new require('iconv').Iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE');
var crypto = require('crypto');
var intstore = require('./intstore');

module.exports = MBTiles;

MBTiles.prototype._carmen = {
    term: {},
    grid: {}
};

// Implements carmen#search method.
MBTiles.prototype.search = function(query, id, callback) {
    var source = this;
    var shardlevel = 2;
    var terms = intstore.terms(query);
    var freqs = {};

    var getids = function(queue, result, callback) {
        if (!queue.length) return callback(null, intstore.mostfreq(result));

        var term = queue.shift();
        var shard = intstore.shard(shardlevel, term);
        source.getCarmen('term', shard, function(err, data) {
            if (err) return callback(err);
            result = result.concat(data[term]);
            freqs[term] = data[term] ? data[term].length : 0;
            getids(queue, result, callback);
        });
    };

    var getzxy = function(queue, result, callback) {
        if (!queue.length) return callback(null, result);

        var id = queue.shift();
        var shard = intstore.shard(shardlevel, id);
        source.getCarmen('grid', shard, function(err, data) {
            if (err) return callback(err);
            termfreq(_(data[id].text).chain().flatten().uniq().value(), function(err) {
                if (err) return callback(err);

                var score = 0;
                for (var i = 0; i < data[id].text.length; i++) {
                    var total = 0;
                    var local = 0;
                    var text = data[id].text[i];
                    for (var j = 0; j < text.length; j++) {
                        total += freqs[text[j]];
                    }
                    for (var j = 0; j < terms.length; j++) {
                        local += text.indexOf(terms[j]) >= 0 ? freqs[terms[j]]/total : 0;
                    }
                    score = Math.max(score, local);
                }

                // @TODO opportunity to filter out results here based on
                // score + threshold.
                result.push({
                    id: id,
                    score: score,
                    zxy: data[id].zxy
                });
                getzxy(queue, result, callback);
            });
        });
    };

    var termfreq = function(terms, callback) {
        if (!terms.length) return callback();
        var term = terms.shift();

        // Term frequency is already known. Continue.
        if (freqs[term]) return termfreq(terms, callback);

        // Look up term frequency.
        var shard = intstore.shard(shardlevel, term);
        source.getCarmen('term', shard, function(err, data) {
            if (err) return callback(err);
            freqs[term] = data[term].length;
            return termfreq(terms, callback);
        });
    };

    getids([].concat(terms), [], function(err, ids) {
        if (err) return callback(err);
        getzxy(ids, [], callback);
    });
};

// Implements carmen#feature method.
MBTiles.prototype.feature = function(id, callback) {
    this._db.get('SELECT key_name AS id, key_json AS data FROM keymap WHERE key_name = ?', id, function(err, row) {
        if (err) return callback(err);
        try { return callback(null, JSON.parse(row.data)); }
        catch(err) { return callback(err); }
    });
};

MBTiles.prototype.getCarmen = function(type, shard, callback) {
    if (this._carmen[type][shard]) return callback(null, this._carmen[type][shard]);

    return this._db.get('SELECT data FROM carmen_' + type + ' WHERE shard = ?', shard, function(err, row) {
        if (err) return callback(err);
        this._carmen[type][shard] = row ? JSON.parse(row.data) : {};
        callback(null, this._carmen[type][shard]);
    }.bind(this));
};

MBTiles.prototype.putCarmen = function(type, shard, data, callback) {
    return this._db.run('REPLACE INTO carmen_' + type + ' (shard, data) VALUES (?, ?)', shard, JSON.stringify(data), function(err) {
        if (err) return callback(err);
        this._carmen[type][shard] = data;
        callback(null);
    }.bind(this));
};

// Implements carmen#index method.
MBTiles.prototype.index = function(docs, callback) {
    var source = this;
    var shardlevel = 2;
    var remaining = docs.length;
    var patch = { term: {}, grid: {} };
    var done = function(err) {
        if (err) {
            remaining = -1;
            callback(err);
        } else if (!--remaining) {
            callback(null);
        }
    };
    docs.forEach(function(doc) {
        var docid = doc.id|0;
        intstore.terms(doc.text).reduce(function(memo, id) {
            var shard = intstore.shard(shardlevel, id);
            memo[shard] = memo[shard] || {};
            memo[shard][id] = memo[shard][id] || [];
            memo[shard][id].push(docid);
            return memo;
        }, patch.term);
        var shard = intstore.shard(shardlevel, docid);
        patch.grid[shard] = patch.grid[shard] || {};
        patch.grid[shard][docid] = {
            text: doc.text.split(',').map(intstore.terms),
            zxy: doc.zxy.map(intstore.zxy)
        };
    }.bind(this));
    // Number of term shards.
    remaining += Object.keys(patch.term).length;
    // Number of grid shards.
    remaining += Object.keys(patch.grid).length;
    // Add each doc individually to the keymap table.
    // @TODO API method for get/put features.
    docs.forEach(function(doc) {
        source._db.run('REPLACE INTO keymap (key_name, key_json) VALUES (?, ?)', doc.id, JSON.stringify(doc.doc), done);
    });
    _(patch).each(function(shards, type) {
        _(shards).each(function(data, shard) {
            source.getCarmen(type, shard, function(err, current) {
                switch (type) {
                case 'term':
                    // This merges new entries on top of old ones.
                    // @TODO invalidate old entries (?) can this be done incrementally?
                    _(data).each(function(val, key) {
                        current[key] = current[key] || [];
                        current[key] = _(current[key].concat(val)).uniq();
                    });
                    break;
                case 'grid':
                    _(data).each(function(val, key) { current[key] = val });
                    break;
                }
                source.putCarmen(type, shard, current, done);
            });
        });
    });
};

// Implements carmen#indexable method.
MBTiles.prototype.indexable = function(pointer, callback) {
    pointer = pointer || {};
    pointer.limit = pointer.limit || 10000;
    pointer.offset = pointer.offset || 0;

    // If 'carmen' option is passed in initial pointer, retrieve indexables from
    // carmen table. This option can be used to access the previously indexed
    // documents from an MBTiles database without having to know what search
    // field was used in the past (see comment below).
    if (pointer.table === 'carmen') return this._db.all("SELECT c.id, c.text, c.zxy, k.key_json FROM carmen c JOIN keymap k ON c.id = k.key_name LIMIT ? OFFSET ?", pointer.limit, pointer.offset, function(err, rows) {
        if (err) return callback(err);
        var docs = rows.map(function(row) {
            var doc = {};
            doc.id = row.id;
            doc.doc = JSON.parse(row.key_json);
            doc.text = row.text;
            doc.zxy = row.zxy.split(',');
            return doc;
        });
        pointer.offset += pointer.limit;
        return callback(null, docs, pointer);
    }.bind(this));

    // By default the keymap table contains all indexable documents.
    this.getInfo(function(err, info) {
        this._db.all("SELECT k.key_name, k.key_json, GROUP_CONCAT(zoom_level||'/'||tile_column ||'/'||tile_row,',') AS zxy FROM keymap k JOIN grid_key g ON k.key_name = g.key_name JOIN map m ON g.grid_id = m.grid_id WHERE m.zoom_level=? GROUP BY k.key_name LIMIT ? OFFSET ?;", info.maxzoom, pointer.limit, pointer.offset, function(err, rows) {
            if (err) return callback(err);
            var docs = rows.map(function(row) {
                var doc = {};
                doc.id = row.key_name;
                doc.doc = JSON.parse(row.key_json);
                // @TODO the doc field name for searching probably (?) belongs
                // in `metadata` and should be un-hardcoded in the future.
                doc.text = doc.doc.search || '';
                doc.zxy = row.zxy.split(',');
                return doc;
            });
            pointer.offset += pointer.limit;
            return callback(null, docs, pointer);
        }.bind(this));
    }.bind(this));
};

// Adds carmen schema to startWriting.
MBTiles.prototype.startWriting = _(MBTiles.prototype.startWriting).wrap(function(parent, callback) {
    parent.call(this, function(err) {
        if (err) return callback(err);
        var sql = '\
        CREATE INDEX IF NOT EXISTS map_grid_id ON map (grid_id);\
        CREATE TABLE IF NOT EXISTS carmen_term(shard INTEGER PRIMARY KEY, data BLOB);\
        CREATE TABLE IF NOT EXISTS carmen_grid(shard INTEGER PRIMARY KEY, data BLOB);';
        this._db.exec(sql, function(err) {
            if (err) {
                return callback(err);
            } else {
                return callback();
            }
        });
    }.bind(this));
});

<commit_msg>Make sure memory cache is segmented by source.<commit_after>var _ = require('underscore');
var MBTiles = require('mbtiles');
var iconv = new require('iconv').Iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE');
var crypto = require('crypto');
var intstore = require('./intstore');

module.exports = MBTiles;

// Implements carmen#search method.
MBTiles.prototype.search = function(query, id, callback) {
    var source = this;
    var shardlevel = 2;
    var terms = intstore.terms(query);
    var freqs = {};

    var getids = function(queue, result, callback) {
        if (!queue.length) return callback(null, intstore.mostfreq(result));

        var term = queue.shift();
        var shard = intstore.shard(shardlevel, term);
        source.getCarmen('term', shard, function(err, data) {
            if (err) return callback(err);
            if (!data[term]) return getids(queue, result, callback);

            result = result.concat(data[term]);
            freqs[term] = data[term] ? data[term].length : 0;
            getids(queue, result, callback);
        });
    };

    var getzxy = function(queue, result, callback) {
        if (!queue.length) return callback(null, result);

        var id = queue.shift();
        var shard = intstore.shard(shardlevel, id);
        source.getCarmen('grid', shard, function(err, data) {
            if (err) return callback(err);
            if (!data[id]) return getzxy(queue, result, callback);

            termfreq(_(data[id].text).chain().flatten().uniq().value(), function(err) {
                if (err) return callback(err);

                var score = 0;
                for (var i = 0; i < data[id].text.length; i++) {
                    var total = 0;
                    var local = 0;
                    var text = data[id].text[i];
                    for (var j = 0; j < text.length; j++) {
                        total += freqs[text[j]];
                    }
                    for (var j = 0; j < terms.length; j++) {
                        local += text.indexOf(terms[j]) >= 0 ? freqs[terms[j]]/total : 0;
                    }
                    score = Math.max(score, local);
                }

                // @TODO opportunity to filter out results here based on
                // score + threshold.
                result.push({
                    id: id,
                    score: score,
                    zxy: data[id].zxy
                });
                getzxy(queue, result, callback);
            });
        });
    };

    var termfreq = function(terms, callback) {
        if (!terms.length) return callback();
        var term = terms.shift();

        // Term frequency is already known. Continue.
        if (freqs[term]) return termfreq(terms, callback);

        // Look up term frequency.
        var shard = intstore.shard(shardlevel, term);
        source.getCarmen('term', shard, function(err, data) {
            if (err) return callback(err);
            freqs[term] = data[term].length;
            return termfreq(terms, callback);
        });
    };

    getids([].concat(terms), [], function(err, ids) {
        if (err) return callback(err);
        getzxy(ids, [], callback);
    });
};

// Implements carmen#feature method.
MBTiles.prototype.feature = function(id, callback) {
    this._db.get('SELECT key_name AS id, key_json AS data FROM keymap WHERE key_name = ?', id, function(err, row) {
        if (err) return callback(err);
        try { return callback(null, JSON.parse(row.data)); }
        catch(err) { return callback(err); }
    });
};

MBTiles.prototype.getCarmen = function(type, shard, callback) {
    if (!this._carmen) this._carmen = { term: {}, grid: {} };

    if (this._carmen[type][shard]) return callback(null, this._carmen[type][shard]);

    return this._db.get('SELECT data FROM carmen_' + type + ' WHERE shard = ?', shard, function(err, row) {
        if (err) return callback(err);
        this._carmen[type][shard] = row ? JSON.parse(row.data) : {};
        callback(null, this._carmen[type][shard]);
    }.bind(this));
};

MBTiles.prototype.putCarmen = function(type, shard, data, callback) {
    if (!this._carmen) this._carmen = { term: {}, grid: {} };

    return this._db.run('REPLACE INTO carmen_' + type + ' (shard, data) VALUES (?, ?)', shard, JSON.stringify(data), function(err) {
        if (err) return callback(err);
        this._carmen[type][shard] = data;
        callback(null);
    }.bind(this));
};

// Implements carmen#index method.
MBTiles.prototype.index = function(docs, callback) {
    var source = this;
    var shardlevel = 2;
    var remaining = docs.length;
    var patch = { term: {}, grid: {} };
    var done = function(err) {
        if (err) {
            remaining = -1;
            callback(err);
        } else if (!--remaining) {
            callback(null);
        }
    };
    docs.forEach(function(doc) {
        var docid = doc.id|0;
        intstore.terms(doc.text).reduce(function(memo, id) {
            var shard = intstore.shard(shardlevel, id);
            memo[shard] = memo[shard] || {};
            memo[shard][id] = memo[shard][id] || [];
            memo[shard][id].push(docid);
            return memo;
        }, patch.term);
        var shard = intstore.shard(shardlevel, docid);
        patch.grid[shard] = patch.grid[shard] || {};
        patch.grid[shard][docid] = {
            text: doc.text.split(',').map(intstore.terms),
            zxy: doc.zxy.map(intstore.zxy)
        };
    }.bind(this));
    // Number of term shards.
    remaining += Object.keys(patch.term).length;
    // Number of grid shards.
    remaining += Object.keys(patch.grid).length;
    // Add each doc individually to the keymap table.
    // @TODO API method for get/put features.
    docs.forEach(function(doc) {
        source._db.run('REPLACE INTO keymap (key_name, key_json) VALUES (?, ?)', doc.id, JSON.stringify(doc.doc), done);
    });
    _(patch).each(function(shards, type) {
        _(shards).each(function(data, shard) {
            source.getCarmen(type, shard, function(err, current) {
                switch (type) {
                case 'term':
                    // This merges new entries on top of old ones.
                    // @TODO invalidate old entries (?) can this be done incrementally?
                    _(data).each(function(val, key) {
                        current[key] = current[key] || [];
                        current[key] = _(current[key].concat(val)).uniq();
                    });
                    break;
                case 'grid':
                    _(data).each(function(val, key) { current[key] = val });
                    break;
                }
                source.putCarmen(type, shard, current, done);
            });
        });
    });
};

// Implements carmen#indexable method.
MBTiles.prototype.indexable = function(pointer, callback) {
    pointer = pointer || {};
    pointer.limit = pointer.limit || 10000;
    pointer.offset = pointer.offset || 0;

    // If 'carmen' option is passed in initial pointer, retrieve indexables from
    // carmen table. This option can be used to access the previously indexed
    // documents from an MBTiles database without having to know what search
    // field was used in the past (see comment below).
    if (pointer.table === 'carmen') return this._db.all("SELECT c.id, c.text, c.zxy, k.key_json FROM carmen c JOIN keymap k ON c.id = k.key_name LIMIT ? OFFSET ?", pointer.limit, pointer.offset, function(err, rows) {
        if (err) return callback(err);
        var docs = rows.map(function(row) {
            var doc = {};
            doc.id = row.id;
            doc.doc = JSON.parse(row.key_json);
            doc.text = row.text;
            doc.zxy = row.zxy.split(',');
            return doc;
        });
        pointer.offset += pointer.limit;
        return callback(null, docs, pointer);
    }.bind(this));

    // By default the keymap table contains all indexable documents.
    this.getInfo(function(err, info) {
        this._db.all("SELECT k.key_name, k.key_json, GROUP_CONCAT(zoom_level||'/'||tile_column ||'/'||tile_row,',') AS zxy FROM keymap k JOIN grid_key g ON k.key_name = g.key_name JOIN map m ON g.grid_id = m.grid_id WHERE m.zoom_level=? GROUP BY k.key_name LIMIT ? OFFSET ?;", info.maxzoom, pointer.limit, pointer.offset, function(err, rows) {
            if (err) return callback(err);
            var docs = rows.map(function(row) {
                var doc = {};
                doc.id = row.key_name;
                doc.doc = JSON.parse(row.key_json);
                // @TODO the doc field name for searching probably (?) belongs
                // in `metadata` and should be un-hardcoded in the future.
                doc.text = doc.doc.search || '';
                doc.zxy = row.zxy.split(',');
                return doc;
            });
            pointer.offset += pointer.limit;
            return callback(null, docs, pointer);
        }.bind(this));
    }.bind(this));
};

// Adds carmen schema to startWriting.
MBTiles.prototype.startWriting = _(MBTiles.prototype.startWriting).wrap(function(parent, callback) {
    parent.call(this, function(err) {
        if (err) return callback(err);
        var sql = '\
        CREATE INDEX IF NOT EXISTS map_grid_id ON map (grid_id);\
        CREATE TABLE IF NOT EXISTS carmen_term(shard INTEGER PRIMARY KEY, data BLOB);\
        CREATE TABLE IF NOT EXISTS carmen_grid(shard INTEGER PRIMARY KEY, data BLOB);';
        this._db.exec(sql, function(err) {
            if (err) {
                return callback(err);
            } else {
                return callback();
            }
        });
    }.bind(this));
});

<|endoftext|>


<commit_before>// Requires kalendae.js and moment.js

var module = angular.module('Kalendae', []);

module.factory('kal', ['$window', function($window) {
    return $window.Kalendae;
}]);

module.factory('Moment', ['$window', function($window) {
    return $window.moment;
}]);

module.directive('kalendae', ['$parse', 'kal', 'KalendaeAPI', function($parse, kal, KalendaeAPI) {
    return {
        restrict: 'EA',
        link: function(scope, element, attrs) {
            var model      = $parse(attrs.ngModel);
            var blackout   = scope.$eval(attrs.blackout);

            var type       = attrs.kalendae === 'dropdown' ? kal.Input : kal;
            var datePicker = new type(element[0], {
                mode       : attrs.mode        || 'single',
                months     : attrs.months      || 1,
                useYearNav : attrs.useYearNav  || true,
                direction  : attrs.direction   || 'any',
                closeButton: attrs.closeButton || false,
                blackout   : blackout          || false
            });

            KalendaeAPI.setDatePicker(datePicker);

            // Requires animate.css in order to animate.
            angular.element(datePicker.container).addClass('animated fadeInDown');

            datePicker.subscribe('date-clicked', function(date) {
                var selectedDate = KalendaeAPI.setDate(date);
                model.assign(scope, selectedDate);
                scope.$apply(function() {
                    scope.$eval(attrs.callback);
                });

                // When using the dropdown datepicker, hide after selecting a date.
                attrs.kalendae === 'dropdown' && element[0].blur();
            });

            // On init set date to the current model value.
            var defaultDate = scope.$eval(attrs.ngModel);

            if(defaultDate) {
                KalendaeAPI.setSelected(defaultDate);
                scope.$eval(attrs.callback);
            }

            // When using the dropdown datepicker, hide after selecting a date.
            attrs.kalendae === 'dropdown' && element[0].blur();
        }
    };
}]);

module.factory('KalendaeAPI', ['Moment', function(Moment) {
    return {
        datePicker: null,
        values:null,
        dates:[],
        setDatePicker: function(datePicker) {
            return this.datePicker = datePicker
        },
        setSelected: function(date) {
            this.datePicker.setSelected(date);
        },
        setBlackOutDates: function() {

        },
        setValues: function(values) {
            this.values = values;
        },
        setDate: function(date) {
            return Moment(date).format('MM/DD/YYYY');
        },
        setDates: function(date) {
            this.dates = [];
            for(var key in dates) {
                var date = Moment(dates[key]).format('MM/DD/YYYY');

                this.dates.push(date);
            }

            return this.dates;
        },
        getDates: function() {
            return this.dates;
        },
        getValueByDate: function(date) {
            var valueByDate;
            var date = Moment(date);

            for(var key in this.values) {
                var min_date = this.values[key].min;
                var max_date = this.values[key].max;
                var value    = this.values[key].value;

                if(date.isBefore(max_date) && date.isAfter(min_date)) {
                    valueByDate = value;
                    break;
                }
            }

            return valueByDate;
        }
    };
}]);<commit_msg>Added weekStart and dayAttributeFormat to the available options.<commit_after>// Requires kalendae.js and moment.js

var module = angular.module('Kalendae', []);

module.factory('kal', ['$window', function($window) {
    return $window.Kalendae;
}]);

module.factory('Moment', ['$window', function($window) {
    return $window.moment;
}]);

module.directive('kalendae', ['$parse', 'kal', 'KalendaeAPI', function($parse, kal, KalendaeAPI) {
    return {
        restrict: 'EA',
        link: function(scope, element, attrs) {
            var model      = $parse(attrs.ngModel);
            var blackout   = scope.$eval(attrs.blackout);

            var type       = attrs.kalendae === 'dropdown' ? kal.Input : kal;
            var datePicker = new type(element[0], {
                mode       : attrs.mode        || 'single',
                months     : attrs.months      || 1,
                useYearNav : attrs.useYearNav  || true,
                direction  : attrs.direction   || 'any',
                closeButton: attrs.closeButton || false,
                blackout   : blackout          || false,
                weekStart  : attrs.weekStart   || 1,
                dayAttributeFormat: attrs.dayAttributeFormat || 'YYYY-MM-DD'
            });

            KalendaeAPI.setDatePicker(datePicker);

            // Requires animate.css in order to animate.
            angular.element(datePicker.container).addClass('animated fadeInDown');

            datePicker.subscribe('date-clicked', function(date) {
                var selectedDate = KalendaeAPI.setDate(date);
                model.assign(scope, selectedDate);
                scope.$apply(function() {
                    scope.$eval(attrs.callback);
                });

                // When using the dropdown datepicker, hide after selecting a date.
                attrs.kalendae === 'dropdown' && element[0].blur();
            });

            // On init set date to the current model value.
            var defaultDate = scope.$eval(attrs.ngModel);

            if(defaultDate) {
                KalendaeAPI.setSelected(defaultDate);
                scope.$eval(attrs.callback);
            }

            // When using the dropdown datepicker, hide after selecting a date.
            attrs.kalendae === 'dropdown' && element[0].blur();
        }
    };
}]);

module.factory('KalendaeAPI', ['Moment', function(Moment) {
    return {
        datePicker: null,
        values:null,
        dates:[],
        setDatePicker: function(datePicker) {
            return this.datePicker = datePicker
        },
        setSelected: function(date) {
            this.datePicker.setSelected(date);
        },
        setBlackOutDates: function() {

        },
        setValues: function(values) {
            this.values = values;
        },
        setDate: function(date) {
            return Moment(date).format('MM/DD/YYYY');
        },
        setDates: function(date) {
            this.dates = [];
            for(var key in dates) {
                var date = Moment(dates[key]).format('MM/DD/YYYY');

                this.dates.push(date);
            }

            return this.dates;
        },
        getDates: function() {
            return this.dates;
        },
        getValueByDate: function(date) {
            var valueByDate;
            var date = Moment(date);

            for(var key in this.values) {
                var min_date = this.values[key].min;
                var max_date = this.values[key].max;
                var value    = this.values[key].value;

                if(date.isBefore(max_date) && date.isAfter(min_date)) {
                    valueByDate = value;
                    break;
                }
            }

            return valueByDate;
        }
    };
}]);<|endoftext|>


<commit_before>import { apiRequest } from 'platform/utilities/api';
import environment from 'platform/utilities/environment';

export const CLAIMANT_INFO_ENDPOINT = `${
  environment.API_URL
}/meb_api/v0/claimant_info`;

const CLAIM_STATUS_ENDPOINT = `${environment.API_URL}/meb_api/v0/claim_status`;

export const FETCH_PERSONAL_INFORMATION = 'FETCH_PERSONAL_INFORMATION';
export const FETCH_PERSONAL_INFORMATION_SUCCESS =
  'FETCH_PERSONAL_INFORMATION_SUCCESS';
export const FETCH_PERSONAL_INFORMATION_FAILED =
  'FETCH_PERSONAL_INFORMATION_FAILED';
export const FETCH_MILITARY_INFORMATION_SUCCESS =
  'FETCH_MILITARY_INFORMATION_SUCCESS';
export const FETCH_MILITARY_INFORMATION_FAILED =
  'FETCH_MILITARY_INFORMATION_FAILED';

export const FETCH_CLAIM_STATUS = 'FETCH_CLAIM_STATUS';
export const FETCH_CLAIM_STATUS_SUCCESS = 'FETCH_CLAIM_STATUS_SUCCESS';
export const FETCH_CLAIM_STATUS_FAILURE = 'FETCH_CLAIM_STATUS_FAILURE';

export const CLAIM_STATUS_RESPONSE_ELIGIBLE = 'ELIGIBLE';
export const CLAIM_STATUS_RESPONSE_DENIED = 'DENIED';
export const CLAIM_STATUS_RESPONSE_IN_PROGRESS = 'INPROGRESS';
export const CLAIM_STATUS_RESPONSE_ERROR = 'ERROR';

const ELIGIBILITY_ENDPOINT = `${environment.API_URL}/meb_api/v0/eligibility`;
export const FETCH_ELIGIBILITY = 'FETCH_ELIGIBILITY';
export const FETCH_ELIGIBILITY_SUCCESS = 'FETCH_ELIGIBILITY_SUCCESS';
export const FETCH_ELIGIBILITY_FAILURE = 'FETCH_ELIGIBILITY_FAILURE';
export const ELIGIBILITY = {
  CHAPTER30: 'Chapter30',
  CHAPTER33: 'Chapter33',
  CHAPTER1606: 'Chapter1606',
};

const ONE_MINUTE_IN_THE_FUTURE = new Date(new Date().getTime() + 60000);
const FIVE_SECONDS = 5000;

export function fetchPersonalInformation() {
  return async dispatch => {
    dispatch({ type: FETCH_PERSONAL_INFORMATION });

    return apiRequest(CLAIMANT_INFO_ENDPOINT)
      .then(response =>
        dispatch({
          type: FETCH_PERSONAL_INFORMATION_SUCCESS,
          response,
        }),
      )
      .catch(errors =>
        dispatch({
          type: FETCH_PERSONAL_INFORMATION_FAILED,
          errors,
        }),
      );
  };
}

const poll = ({
  endpoint,
  validate = response => response && response.data,
  interval = FIVE_SECONDS,
  endTime = ONE_MINUTE_IN_THE_FUTURE,
  dispatch,
  timeoutResponse,
  successDispatchType,
  failureDispatchType,
}) => {
  // eslint-disable-next-line consistent-return
  const executePoll = async (resolve, reject) => {
    const response = await apiRequest(endpoint);

    if (validate(response)) {
      return resolve(response.data);
    }
    if (new Date() >= endTime) {
      return resolve(timeoutResponse);
    }

    setTimeout(executePoll, interval, resolve, reject);
  };

  return new Promise(executePoll)
    .then(response => {
      return dispatch({
        type: successDispatchType,
        response,
      });
    })
    .catch(errors => {
      dispatch({
        type: failureDispatchType,
        errors,
      });
    });
};

export function fetchClaimStatus() {
  return async dispatch => {
    dispatch({ type: FETCH_CLAIM_STATUS });
    const timeoutResponse = {
      claimStatus: CLAIM_STATUS_RESPONSE_IN_PROGRESS,
      receivedDate: Date.now(),
    };

    poll({
      endpoint: CLAIM_STATUS_ENDPOINT,
      validate: response =>
        response.data.attributes &&
        response.data.attributes.claimStatus !==
          CLAIM_STATUS_RESPONSE_IN_PROGRESS,
      dispatch,
      timeoutResponse,
      successDispatchType: FETCH_CLAIM_STATUS_SUCCESS,
      failureDispatchType: FETCH_CLAIM_STATUS_FAILURE,
    });
  };
}

export function fetchEligibility() {
  return async dispatch => {
    dispatch({ type: FETCH_ELIGIBILITY });

    return apiRequest(ELIGIBILITY_ENDPOINT)
      .then(response =>
        dispatch({
          type: FETCH_ELIGIBILITY_SUCCESS,
          response,
        }),
      )
      .catch(errors =>
        dispatch({
          type: FETCH_ELIGIBILITY_FAILURE,
          errors,
        }),
      );
  };
}
<commit_msg>Add Null Check to Claim Status Polling (#20227)<commit_after>import { apiRequest } from 'platform/utilities/api';
import environment from 'platform/utilities/environment';

export const CLAIMANT_INFO_ENDPOINT = `${
  environment.API_URL
}/meb_api/v0/claimant_info`;

const CLAIM_STATUS_ENDPOINT = `${environment.API_URL}/meb_api/v0/claim_status`;

export const FETCH_PERSONAL_INFORMATION = 'FETCH_PERSONAL_INFORMATION';
export const FETCH_PERSONAL_INFORMATION_SUCCESS =
  'FETCH_PERSONAL_INFORMATION_SUCCESS';
export const FETCH_PERSONAL_INFORMATION_FAILED =
  'FETCH_PERSONAL_INFORMATION_FAILED';
export const FETCH_MILITARY_INFORMATION_SUCCESS =
  'FETCH_MILITARY_INFORMATION_SUCCESS';
export const FETCH_MILITARY_INFORMATION_FAILED =
  'FETCH_MILITARY_INFORMATION_FAILED';

export const FETCH_CLAIM_STATUS = 'FETCH_CLAIM_STATUS';
export const FETCH_CLAIM_STATUS_SUCCESS = 'FETCH_CLAIM_STATUS_SUCCESS';
export const FETCH_CLAIM_STATUS_FAILURE = 'FETCH_CLAIM_STATUS_FAILURE';

export const CLAIM_STATUS_RESPONSE_ELIGIBLE = 'ELIGIBLE';
export const CLAIM_STATUS_RESPONSE_DENIED = 'DENIED';
export const CLAIM_STATUS_RESPONSE_IN_PROGRESS = 'INPROGRESS';
export const CLAIM_STATUS_RESPONSE_ERROR = 'ERROR';

const ELIGIBILITY_ENDPOINT = `${environment.API_URL}/meb_api/v0/eligibility`;
export const FETCH_ELIGIBILITY = 'FETCH_ELIGIBILITY';
export const FETCH_ELIGIBILITY_SUCCESS = 'FETCH_ELIGIBILITY_SUCCESS';
export const FETCH_ELIGIBILITY_FAILURE = 'FETCH_ELIGIBILITY_FAILURE';
export const ELIGIBILITY = {
  CHAPTER30: 'Chapter30',
  CHAPTER33: 'Chapter33',
  CHAPTER1606: 'Chapter1606',
};

const ONE_MINUTE_IN_THE_FUTURE = new Date(new Date().getTime() + 60000);
const FIVE_SECONDS = 5000;

export function fetchPersonalInformation() {
  return async dispatch => {
    dispatch({ type: FETCH_PERSONAL_INFORMATION });

    return apiRequest(CLAIMANT_INFO_ENDPOINT)
      .then(response =>
        dispatch({
          type: FETCH_PERSONAL_INFORMATION_SUCCESS,
          response,
        }),
      )
      .catch(errors =>
        dispatch({
          type: FETCH_PERSONAL_INFORMATION_FAILED,
          errors,
        }),
      );
  };
}

const poll = ({
  endpoint,
  validate = response => response && response.data,
  interval = FIVE_SECONDS,
  endTime = ONE_MINUTE_IN_THE_FUTURE,
  dispatch,
  timeoutResponse,
  successDispatchType,
  failureDispatchType,
}) => {
  // eslint-disable-next-line consistent-return
  const executePoll = async (resolve, reject) => {
    const response = await apiRequest(endpoint);

    if (validate(response)) {
      return resolve(response.data);
    }
    if (new Date() >= endTime) {
      return resolve(timeoutResponse);
    }

    setTimeout(executePoll, interval, resolve, reject);
  };

  return new Promise(executePoll)
    .then(response => {
      return dispatch({
        type: successDispatchType,
        response,
      });
    })
    .catch(errors => {
      dispatch({
        type: failureDispatchType,
        errors,
      });
    });
};

export function fetchClaimStatus() {
  return async dispatch => {
    dispatch({ type: FETCH_CLAIM_STATUS });
    const timeoutResponse = {
      attributes: {
        claimStatus: CLAIM_STATUS_RESPONSE_IN_PROGRESS,
        receivedDate: Date.now(),
      },
    };

    poll({
      endpoint: CLAIM_STATUS_ENDPOINT,
      validate: response =>
        response.data.attributes &&
        response.data.attributes.claimStatus &&
        response.data.attributes.claimStatus !==
          CLAIM_STATUS_RESPONSE_IN_PROGRESS,
      dispatch,
      timeoutResponse,
      successDispatchType: FETCH_CLAIM_STATUS_SUCCESS,
      failureDispatchType: FETCH_CLAIM_STATUS_FAILURE,
    });
  };
}

export function fetchEligibility() {
  return async dispatch => {
    dispatch({ type: FETCH_ELIGIBILITY });

    return apiRequest(ELIGIBILITY_ENDPOINT)
      .then(response =>
        dispatch({
          type: FETCH_ELIGIBILITY_SUCCESS,
          response,
        }),
      )
      .catch(errors =>
        dispatch({
          type: FETCH_ELIGIBILITY_FAILURE,
          errors,
        }),
      );
  };
}
<|endoftext|>


<commit_before>// Copyright 2016, University of Colorado Boulder

/**
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // modules
  var assertInstanceOf = require( 'PHET_IO/assertions/assertInstanceOf' );
  var phetioInherit = require( 'PHET_IO/phetioInherit' );
  var phetioNamespace = require( 'PHET_IO/phetioNamespace' );
  var TBoolean = require( 'PHET_IO/types/TBoolean' );
  var TNumber = require( 'PHET_IO/types/TNumber' );
  var TObject = require( 'PHET_IO/types/TObject' );
  var TVoid = require( 'PHET_IO/types/TVoid' );

  function TNode( node, phetioID ) {
    TObject.call( this, node, phetioID );
    assertInstanceOf( node, phet.scenery.Node );
  }

  phetioInherit( TObject, 'TNode', TNode, {

    isVisible: {
      returnType: TBoolean,
      parameterTypes: [],
      implementation: function() {
        return this.instance.visible;
      },
      documentation: 'Gets a Boolean value indicating whether the node can be seen and interacted with'
    },

    setVisible: {
      returnType: TVoid,
      parameterTypes: [ TBoolean ],
      implementation: function( visible ) {
        this.instance.visible = visible;
      },
      documentation: 'Set whether the node will be visible (and interactive)'
    },

    setPickable: {
      returnType: TVoid,
      parameterTypes: [ TBoolean ],
      implementation: function( pickable ) {
        this.instance.pickable = pickable;
      },
      documentation: 'Set whether the node will be pickable (and hence interactive)'
    },

    isPickable: {
      returnType: TBoolean,
      parameterTypes: [],
      implementation: function() {
        return this.instance.pickable;
      },
      documentation: 'Gets whether the node is pickable (and hence interactive)'
    },

    setOpacity: {
      returnType: TVoid,
      parameterTypes: [ TNumber( 'unitless' ) ],
      implementation: function( opacity ) {
        this.instance.opacity = opacity;
      },
      documentation: 'Set opacity between 0-1 (inclusive)'
    },

    setRotation: {
      returnType: TVoid,
      parameterTypes: [ TNumber( 'unitless' ) ],
      implementation: function( rotation ) {
        this.instance.rotation = rotation;
      },
      documentation: 'Set the rotation of the node, in radians'
    }
  }, {
    documentation: 'The base type for graphical and potentially interactive objects'
  } );

  phetioNamespace.register( 'TNode', TNode );

  return TNode;
} );

<commit_msg>Made units optional, see https://github.com/phetsims/beers-law-lab/issues/182<commit_after>// Copyright 2016, University of Colorado Boulder

/**
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // modules
  var assertInstanceOf = require( 'PHET_IO/assertions/assertInstanceOf' );
  var phetioInherit = require( 'PHET_IO/phetioInherit' );
  var phetioNamespace = require( 'PHET_IO/phetioNamespace' );
  var TBoolean = require( 'PHET_IO/types/TBoolean' );
  var TNumber = require( 'PHET_IO/types/TNumber' );
  var TObject = require( 'PHET_IO/types/TObject' );
  var TVoid = require( 'PHET_IO/types/TVoid' );

  function TNode( node, phetioID ) {
    TObject.call( this, node, phetioID );
    assertInstanceOf( node, phet.scenery.Node );
  }

  phetioInherit( TObject, 'TNode', TNode, {

    isVisible: {
      returnType: TBoolean,
      parameterTypes: [],
      implementation: function() {
        return this.instance.visible;
      },
      documentation: 'Gets a Boolean value indicating whether the node can be seen and interacted with'
    },

    setVisible: {
      returnType: TVoid,
      parameterTypes: [ TBoolean ],
      implementation: function( visible ) {
        this.instance.visible = visible;
      },
      documentation: 'Set whether the node will be visible (and interactive)'
    },

    setPickable: {
      returnType: TVoid,
      parameterTypes: [ TBoolean ],
      implementation: function( pickable ) {
        this.instance.pickable = pickable;
      },
      documentation: 'Set whether the node will be pickable (and hence interactive)'
    },

    isPickable: {
      returnType: TBoolean,
      parameterTypes: [],
      implementation: function() {
        return this.instance.pickable;
      },
      documentation: 'Gets whether the node is pickable (and hence interactive)'
    },

    setOpacity: {
      returnType: TVoid,
      parameterTypes: [ TNumber( ) ],
      implementation: function( opacity ) {
        this.instance.opacity = opacity;
      },
      documentation: 'Set opacity between 0-1 (inclusive)'
    },

    setRotation: {
      returnType: TVoid,
      parameterTypes: [ TNumber( ) ],
      implementation: function( rotation ) {
        this.instance.rotation = rotation;
      },
      documentation: 'Set the rotation of the node, in radians'
    }
  }, {
    documentation: 'The base type for graphical and potentially interactive objects'
  } );

  phetioNamespace.register( 'TNode', TNode );

  return TNode;
} );

<|endoftext|>