ElectronicBabylonianLiterature/ebl-frontend

View on GitHub
src/fragmentarium/ui/SearchForm.tsx

Summary

Maintainability
C
1 day
Test Coverage
B
80%
import React, { Component } from 'react'
import LuckyButton from 'fragmentarium/ui/front-page/LuckyButton'
import PioneersButton from 'fragmentarium/ui/PioneersButton'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import {
  Button,
  ButtonToolbar,
  Col,
  Form,
  OverlayTriggerProps,
  Row,
} from 'react-bootstrap'
import { stringify } from 'query-string'
import BibliographySelect from 'bibliography/ui/BibliographySelect'
import HelpTrigger from 'common/HelpTrigger'
import _ from 'lodash'
import produce from 'immer'
import FragmentService from 'fragmentarium/application/FragmentService'
import FragmentSearchService from 'fragmentarium/application/FragmentSearchService'
import replaceTransliteration from 'fragmentarium/domain/replaceTransliteration'
import BibliographyEntry from 'bibliography/domain/BibliographyEntry'
import {
  FragmentQuery,
  PeriodModifierString,
  PeriodString,
  QueryType,
} from 'query/FragmentQuery'
import WordService from 'dictionary/application/WordService'
import { LemmaQueryTypeForm, LemmaSearchForm } from './LemmaSearchForm'
import { PeriodSearchForm, PeriodModifierSearchForm } from './ScriptSearchForm'
import {
  MuseumSearchHelp,
  ReferenceSearchHelp,
  TransliterationSearchHelp,
  LemmaSearchHelp,
  ScriptSearchHelp,
  GenreSearchHelp,
  ProvenanceSearchHelp,
} from './SearchHelp'
import GenreSearchForm from './GenreSearchForm'
import BibliographyService from 'bibliography/application/BibliographyService'
import { ResearchProjects } from 'research-projects/researchProject'
import './SearchForm.sass'
import ProvenanceSearchForm from './ProvenanceSearchForm'

interface State {
  number: string | null
  referenceEntry: {
    id: string
    label: string
  }
  pages: string | null
  lemmas: string | null
  lemmaOperator: QueryType | null
  transliteration: string | null
  scriptPeriod: PeriodString
  scriptPeriodModifier: PeriodModifierString
  genre: string | null
  project: keyof typeof ResearchProjects | null
  isValid: boolean
  site: string | null
}

export type SearchFormProps = {
  fragmentSearchService: FragmentSearchService
  fragmentService: FragmentService
  bibliographyService: BibliographyService
  fragmentQuery?: FragmentQuery
  wordService: WordService
  history: History
  project?: keyof typeof ResearchProjects | null
} & RouteComponentProps

export function isValidNumber(number?: string): boolean {
  return !number || !/^[.*]+$/.test(number.trim())
}

export const helpColSize = 1

function HelpCol({
  ...props
}: Pick<OverlayTriggerProps, 'overlay'>): JSX.Element {
  return (
    <Col
      sm={helpColSize}
      as={Form.Label}
      className="TransliterationSearchForm__label"
    >
      <HelpTrigger {...props} />
    </Col>
  )
}

class SearchForm extends Component<SearchFormProps, State> {
  basepath: string
  constructor(props: SearchFormProps) {
    super(props)
    this.basepath = props.project
      ? `/projects/${props.project.toLowerCase()}/search/`
      : '/fragmentarium/search/'

    const fragmentQuery = this.props.fragmentQuery || {}

    this.state = {
      number: fragmentQuery.number || null,
      referenceEntry: {
        id: fragmentQuery.bibId || '',
        label: fragmentQuery.bibLabel || '',
      },
      pages: fragmentQuery.pages || null,
      lemmas: fragmentQuery.lemmas || '',
      lemmaOperator: fragmentQuery.lemmaOperator || 'line',
      transliteration: fragmentQuery.transliteration || '',
      scriptPeriod: fragmentQuery.scriptPeriod || '',
      scriptPeriodModifier: fragmentQuery.scriptPeriodModifier || '',
      genre: fragmentQuery.genre || '',
      site: fragmentQuery.site || '',
      isValid: isValidNumber(fragmentQuery.number),
      project: fragmentQuery.project || null,
    }

    if (
      this.state.referenceEntry.id &&
      this.state.referenceEntry.label === ''
    ) {
      this.fetchReferenceLabel()
    }
  }

  fetchReferenceLabel = async () => {
    const { bibliographyService } = this.props
    const { id } = this.state.referenceEntry
    const reference = await bibliographyService.find(id)
    this.setState({
      referenceEntry: {
        id: id,
        label: reference.label,
      },
    })
  }

  onChange = (name: string) => (value): void => {
    this.setState((prevState) => ({ ...prevState, [name]: value }))
  }

  onChangeNumber = (value: string): void => {
    this.setState({ number: value, isValid: isValidNumber(value) })
  }

  onChangeBibliographyReference = (event: BibliographyEntry) => {
    const newState = produce(this.state, (draftState) => {
      draftState.referenceEntry.label = event.label || ''
      draftState.referenceEntry.id = event.id || ''
    })
    this.setState(newState)
  }

  flattenState(state: State): FragmentQuery {
    const cleanedTransliteration = _.trimEnd(state.transliteration || '')
    const stateWithoutNull = _.omitBy(
      {
        number: state.number,
        lemmas: state.lemmas,
        lemmaOperator: state.lemmas ? state.lemmaOperator : '',
        bibId: state.referenceEntry.id,
        label: state.referenceEntry.label,
        pages: state.pages,
        transliteration: replaceTransliteration(cleanedTransliteration),
        scriptPeriodModifier: state.scriptPeriod
          ? state.scriptPeriodModifier
          : '',
        scriptPeriod: state.scriptPeriod,
        genre: state.genre,
        site: state.site ? state.site.split(' ')[0] : '',
        project: state.project,
      },
      (value) => !value
    )
    return _.omit(stateWithoutNull, 'isValid')
  }
  search = (event: React.MouseEvent<HTMLElement>) => {
    event.preventDefault()
    const updatedState = this.flattenState(this.state)
    this.onChange('transliteration')(updatedState.transliteration)
    this.props.history.push(`${this.basepath}?${stringify(updatedState)}`)
  }

  render(): JSX.Element {
    const rows = this.state.number?.split('\n').length ?? 0
    const numberSearchForm = (
      <Form.Group as={Row} controlId="number">
        <HelpCol overlay={MuseumSearchHelp()} />
        <Col>
          <Form.Control
            type="text"
            name="number"
            value={this.state.number || ''}
            placeholder="Museum, accession, CDLI, or excavation number"
            aria-label="Number"
            onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void =>
              this.onChangeNumber(event.target.value)
            }
            isInvalid={!this.state.isValid}
          />
          <Form.Control.Feedback type="invalid">
            At least one of prefix, number or suffix must be specified.
          </Form.Control.Feedback>
        </Col>
      </Form.Group>
    )
    const bibliographySearchForm = (
      <Form.Group as={Row} controlId="reference">
        <HelpCol overlay={ReferenceSearchHelp()} />
        <Col>
          <BibliographySelect
            isClearable={true}
            ariaLabel="Select bibliography reference"
            value={this.state.referenceEntry}
            onChange={this.onChangeBibliographyReference}
            searchBibliography={(query) =>
              this.props.fragmentService.searchBibliography(query)
            }
          />
        </Col>
        <Col>
          <Form.Control
            type="text"
            name="pages"
            placeholder="Page"
            aria-label="Pages"
            value={this.state.pages || ''}
            onChange={(event: React.ChangeEvent<HTMLInputElement>): void =>
              this.onChange('pages')(event.target.value)
            }
          />
        </Col>
      </Form.Group>
    )
    const periodSearchForm = (
      <Form.Group as={Row} controlId="period">
        <HelpCol overlay={ScriptSearchHelp()} />
        <Col>
          <PeriodModifierSearchForm
            onChange={this.onChange('scriptPeriodModifier')}
            value={this.state.scriptPeriodModifier}
          />
        </Col>
        <Col>
          <PeriodSearchForm
            fragmentService={this.props.fragmentService}
            onChange={this.onChange('scriptPeriod')}
            value={this.state.scriptPeriod}
          />
        </Col>
      </Form.Group>
    )
    const provenanceSearchForm = (
      <Form.Group as={Row} controlId="site">
        <HelpCol overlay={ProvenanceSearchHelp()} />
        <Col>
          <ProvenanceSearchForm
            fragmentService={this.props.fragmentService}
            onChange={this.onChange('site')}
            value={this.state.site}
          />
        </Col>
      </Form.Group>
    )
    const genreSearchForm = (
      <Form.Group as={Row} controlId="genre">
        <HelpCol overlay={GenreSearchHelp()} />
        <Col>
          <GenreSearchForm
            fragmentService={this.props.fragmentService}
            onChange={this.onChange('genre')}
            value={this.state.genre}
          />
        </Col>
      </Form.Group>
    )
    const lemmaSearchForm = (
      <Form.Group as={Row} controlId="lemmas">
        <HelpCol overlay={LemmaSearchHelp()} />
        <Col>
          <LemmaSearchForm
            wordService={this.props.wordService}
            onChange={this.onChange}
            lemmas={this.state.lemmas ?? ''}
          />
        </Col>
        <Col sm={3}>
          <LemmaQueryTypeForm
            value={this.state.lemmaOperator || 'line'}
            onChange={this.onChange('lemmaOperator')}
          />
        </Col>
      </Form.Group>
    )
    const transliterationSearchForm = (
      <Form.Group as={Row} controlId="transliteration">
        <HelpCol overlay={TransliterationSearchHelp()} />
        <Col sm={12 - helpColSize}>
          <Form.Control
            as="textarea"
            value={this.state.transliteration || ''}
            rows={Math.max(2, rows)}
            placeholder="Transliterations"
            aria-label="Transliteration"
            name="transliteration"
            onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void =>
              this.onChange('transliteration')(event.target.value)
            }
          />
        </Col>
      </Form.Group>
    )
    return (
      <>
        <Form>
          {numberSearchForm}
          {bibliographySearchForm}
          {periodSearchForm}
          {provenanceSearchForm}
          {genreSearchForm}
          {lemmaSearchForm}
          {transliterationSearchForm}
        </Form>
        <ButtonToolbar>
          <Col
            sm={{ offset: helpColSize }}
            className="SearchForm__ButtonToolbar"
          >
            <Button
              className="w-25 m-1"
              onClick={this.search}
              variant="primary"
              disabled={!this.state.isValid}
            >
              {this.props.project
                ? `Search in ${this.props.project}`
                : 'Search'}
            </Button>
            {!this.props.project && (
              <>
                <LuckyButton
                  fragmentSearchService={this.props.fragmentSearchService}
                />
                <PioneersButton
                  fragmentSearchService={this.props.fragmentSearchService}
                />
              </>
            )}
          </Col>
        </ButtonToolbar>
      </>
    )
  }
}
export default withRouter(SearchForm)