SearchField.purs 23.1 KB
Newer Older
1
module Gargantext.Components.Forest.Tree.Node.Action.Search.SearchField where
2

3 4
import Gargantext.Prelude

5
import Data.Array as A
6
import Data.Maybe (Maybe(..), maybe, fromMaybe)
7
import Data.Newtype (over)
8
import Data.Nullable (null)
9
import Data.Set as Set
10 11
import Data.String.Common (joinWith, replaceAll)
import Data.String.Pattern (Pattern(..), Replacement(..))
12
import Data.Tuple (Tuple(..))
13
import DOM.Simple.Console (log, log2)
14
import Effect (Effect)
15 16
import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
17
import Gargantext.Components.Forest.Tree.Node.Action.Search.Frame (searchIframes)
18 19 20
import Gargantext.Components.Forest.Tree.Node.Action.Search.Types (DataField(..), Database(..), IMT_org(..), Org(..), SearchQuery(..), allOrgs, dataFields, defaultSearchQuery, doc, performSearch, datafield2database, Search)
import Gargantext.Components.GraphQL.Endpoints (getIMTSchools)
import Gargantext.Components.GraphQL.IMT as GQLIMT
21
import Gargantext.Components.InputWithEnter (inputWithEnter)
22
import Gargantext.Components.Lang (Lang(..))
23 24
import Gargantext.Components.ListSelection as ListSelection
import Gargantext.Components.ListSelection.Types as ListSelection
25
import Gargantext.Config.REST (logRESTError)
26
import Gargantext.Config.Utils (handleRESTError)
27
import Gargantext.Hooks.Loader (useLoader)
28
import Gargantext.Sessions (Session)
29
import Gargantext.Types (FrontendError)
30 31
import Gargantext.Types as GT
import Gargantext.Utils.Reactix as R2
32 33 34
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
35

36 37
here :: R2.Here
here = R2.here "Gargantext.Components.Forest.Tree.Node.Action.Search.SearchField"
38

39
defaultSearch :: Search
40
defaultSearch = { databases: Empty
41
                , datafield: Nothing
42 43 44
                , node_id  : Nothing
                , lang     : Nothing
                , term     : ""
45
                , url      : ""
46
                , years    : []
47
                }
48

49
type Props =
50 51
  -- list of databases to search, or parsers to use on uploads
  ( databases :: Array Database
52
  , errors    :: T.Box (Array FrontendError)
53
  , langs     :: Array Lang
54
  -- State hook for a search, how we get data in and out
55
  , onSearch  :: GT.AsyncTaskWithType -> Effect Unit
56
  , search    :: T.Box Search
57
  , session   :: Session
58 59
  )

60 61 62 63 64
searchField :: R2.Component Props
searchField = R.createElement searchFieldCpt
searchFieldCpt :: R.Component Props
searchFieldCpt = here.component "searchField" cpt
  where
65
    cpt { databases, errors, langs, onSearch, search, session } _ = do
66
      selection <- T.useBox ListSelection.MyListsFirst
67

68 69
      pure $
        H.div { className: "search-field" }
70 71 72 73 74
        [ searchInput { search } []
          -- , if length s.term < 3  -- search with love : <3
          --   then
          --     H.div {}[]
          --   else
75
        , datafieldInput { databases, langs, search, session } []
76
        , ListSelection.selection { selection, session } []
77
        , submitButton { errors, onSearch, search, selection, session } []
78 79
        ]
      --pure $ panel params button
80

81 82 83
type ComponentProps =
  ( search :: T.Box Search )

84 85 86 87
componentYears :: R2.Component ComponentProps
componentYears = R.createElement componentYearsCpt
componentYearsCpt :: R.Component ComponentProps
componentYearsCpt = here.component "componentYears" cpt where
88
  cpt { search } _  = do
89 90 91
    { years } <- T.useLive T.unequal search
    let yearsZ = A.zip (A.range 0 (A.length years)) years
    newYear <- T.useBox ""
92
    pure $ H.div {}
93 94 95 96 97 98 99 100 101
      ((yearCpt search <$> yearsZ) <>
      [ H.div {}
        [ H.input { on: { blur: modify newYear
                      , change: modify newYear
                      , input: modify newYear } }
        , H.span { className: "btn btn-primary fa fa-check"
                 , on: { click: clickAdd newYear search }} []
        ]
      ])
102
      where
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
        clickAdd newYear search _ = do
          newYear' <- T.read newYear
          T.modify_ (\s@{ years } -> s { years = A.snoc years newYear' }) search
        clickRemove idx search _ =
          T.modify_ (\s@{ years } -> s { years = left idx years <> right (A.length years - idx) years }) search
          where
            left 0 years = []
            left idx years = A.take idx years
            right 0 years = []
            right len years = A.takeEnd (len - 1) years
        modify newYear e = T.write_ (R.unsafeEventValue e) newYear
        yearCpt search (Tuple idx year) =
          H.div {}
            [ H.span {} [ H.text year ]
            , H.span { className: "btn btn-danger fa fa-times"
                     , on: { click: clickRemove idx search } } [] ]
119

120
type ComponentIMTProps =
121 122
  ( session :: Session
  | ComponentProps )
123

124 125 126 127
componentIMT :: R2.Component ComponentIMTProps
componentIMT = R.createElement componentIMTCpt
componentIMTCpt :: R.Component ComponentIMTProps
componentIMTCpt = here.component "componentIMT" cpt where
128
  cpt { search, session } _  = do
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
    useLoader { errorHandler
              , loader: \_ -> getIMTSchools session
              , path: unit
              , render: \schools -> componentWithIMTOrgs { schools, search } [] }
    where
      errorHandler = logRESTError here "[componentIMT]"

type ComponentWithIMTOrgsProps =
  ( schools :: Array GQLIMT.School
  , search :: T.Box Search)

componentWithIMTOrgs :: R2.Component ComponentWithIMTOrgsProps
componentWithIMTOrgs = R.createElement componentWithIMTOrgsCpt
componentWithIMTOrgsCpt :: R.Component ComponentWithIMTOrgsProps
componentWithIMTOrgsCpt = here.component "componentWithIMTOrgs" cpt where
  cpt { schools, search } _ = do
    search' <- T.useLive T.unequal search
146

147 148 149 150 151 152 153 154 155 156 157 158
    let allIMTOrgs = [All_IMT] <> (IMT_org <$> schools)
        liCpt org =
          H.li {}
          [ H.input { type: "checkbox"
                    , checked: isIn org search'.datafield
                    , on: { change: \_ -> ( T.modify_ (_ { datafield = updateFilter org allIMTOrgs search'.datafield }) search)
                          }
                    }
          , case org of
               All_IMT -> H.i {} [H.text  $ " " <> show org]
               (IMT_org { school_shortName }) -> H.text $ " " <> school_shortName
          ]
159

160 161
    pure $ R.fragment
      [ H.ul {} $ map liCpt $ allIMTOrgs
162
        --, filterInput fi
163
      ]
164

165 166 167 168 169
componentCNRS :: R2.Component ComponentProps
componentCNRS = R.createElement componentCNRSCpt
componentCNRSCpt :: R.Component ComponentProps
componentCNRSCpt = here.component "componentCNRS" cpt
  where
170
    cpt _ _ = do
171 172 173 174
      pure $ R.fragment
        [ H.div {} []
          --, filterInput fi
        ]
175

176

177 178 179 180
isExternal :: Maybe DataField -> Boolean
isExternal (Just (External _)) = true
isExternal _ = false

181 182 183 184 185 186 187 188 189
isArxiv :: Maybe DataField -> Boolean
isArxiv (Just
        ( External
          ( Just Arxiv
          )
        )
      )   = true
isArxiv _ = false

190
isHAL :: Maybe DataField -> Boolean
191
isHAL (Just
192
        ( External
193 194 195 196
          ( Just (HAL _ )
          )
        )
      ) = true
197 198
isHAL _ = false

199
isIsTex :: Maybe DataField -> Boolean
200 201 202 203 204 205
isIsTex ( Just
          ( External
            ( Just ( IsTex)
            )
          )
        ) = true
206 207
isIsTex _ = false

208

209
isIMT :: Maybe DataField -> Boolean
210
isIMT ( Just
211
        ( External
212 213
          ( Just
            ( HAL
214 215 216 217 218 219
              ( Just ( IMT _)
              )
            )
          )
        )
      ) = true
220 221 222
isIMT _ = false

isCNRS :: Maybe DataField -> Boolean
223 224 225 226 227 228 229 230 231 232
isCNRS ( Just
         ( External
          ( Just
            ( HAL
              ( Just ( CNRS _)
              )
            )
          )
        )
      ) = true
233 234
isCNRS _ = false

235 236 237
needsLang :: Maybe DataField -> Boolean
needsLang (Just Gargantext) = true
needsLang (Just Web)        = true
238 239 240 241 242 243 244
needsLang ( Just
            ( External
              ( Just (HAL _)
              )
            )
          ) = true
needsLang _                 = false
245 246 247


isIn :: IMT_org -> Maybe DataField -> Boolean
248 249 250 251 252 253 254 255 256 257 258
isIn org ( Just
           ( External
             ( Just
               ( HAL
                 ( Just
                   ( IMT imtOrgs )
                 )
               )
             )
           )
         ) = Set.member org imtOrgs
259 260
isIn _ _ = false

261 262
updateFilter :: IMT_org -> Array IMT_org -> Maybe DataField -> Maybe DataField
updateFilter org allIMTorgs (Just (External (Just (HAL (Just (IMT imtOrgs)))))) =
263
 (Just (External (Just (HAL (Just $ IMT imtOrgs')))))
264 265
  where
    imtOrgs' = if Set.member org imtOrgs
266 267 268
                  then
                    if org == All_IMT
                       then Set.empty
269
                       else Set.delete All_IMT $ Set.delete org imtOrgs
270 271 272 273
                  else
                    if org == All_IMT
                       then Set.fromFoldable allIMTorgs
                       else Set.insert org imtOrgs
274

275
updateFilter org allIMTorgs _ = (Just (External (Just (HAL (Just (IMT imtOrgs'))))))
276
  where
277 278 279
    imtOrgs' = if org == All_IMT
                  then Set.fromFoldable allIMTorgs
                  else Set.fromFoldable [org]
280

281 282
------------------------------------------------------------------------

283 284 285 286 287 288 289 290 291 292 293 294
type LangNavProps =
  ( langs  :: Array Lang
  , search :: T.Box Search )

langNav :: R2.Component LangNavProps
langNav = R.createElement langNavCpt
langNavCpt :: R.Component LangNavProps
langNavCpt = here.component "langNav" cpt
  where
    cpt { langs, search } _ = do
      search' <- T.useLive T.unequal search

295 296 297 298
      pure $ R.fragment
        [ H.div {className: "text-primary center"} [H.text "with lang"]
        , H.div {className: "nav nav-tabs"} ((liItem search') <$> langs)
        ]
299
        where
300 301 302 303
          liItem :: Search -> Lang -> R.Element
          liItem { lang } lang' =
            H.div { className : "nav-item nav-link" <> if (Just lang') == lang then " active" else ""
                  , on: { click: \_ -> T.modify_ (_ { lang = Just lang' }) search }
304 305
                  }
            [ H.text (show lang') ]
306 307 308 309

------------------------------------------------------------------------

type DataFieldNavProps =
310
  ( search :: T.Box Search )
311 312 313 314 315 316

dataFieldNav :: R2.Component DataFieldNavProps
dataFieldNav = R.createElement dataFieldNavCpt
dataFieldNavCpt :: R.Component DataFieldNavProps
dataFieldNavCpt = here.component "dataFieldNav" cpt
  where
317
    cpt { search } _ = do
318 319
      search'@{ datafield } <- T.useLive T.unequal search

320 321
      pure $ R.fragment
        [ H.div { className: "text-primary center"} [H.text "with DataField"]
322 323
        , H.div { className: "nav nav-tabs" } ((liItem search') <$> dataFields)
        , H.div { className: "center" } [ H.text
324 325 326
                                        $ maybe "TODO: add Doc Instance" doc datafield
                                      ]
        ]
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
      where
        liItem :: Search -> DataField -> R.Element
        liItem { datafield } df' =
          H.div { className : "nav-item nav-link"
                            <> if isActive  --(Just df') == datafield
                                then " active"
                                else ""
              , on: { click: \_ -> T.modify_ (_ { datafield = Just df'
                                                , databases = datafield2database df'
                                                }) search
                    }
              -- just one database query for now
              -- a list a selected database needs more ergonomy
              } [ H.text (show df') ]
          where
            isActive = show (Just df') == show datafield
343 344

------------------------------------------------------------------------
345

346 347
type DatabaseInputProps = (
    databases :: Array Database
348
  , search    :: T.Box Search
349
  )
350

351 352
databaseInput :: R2.Component DatabaseInputProps
databaseInput = R.createElement databaseInputCpt
353
databaseInputCpt :: R.Component DatabaseInputProps
354
databaseInputCpt = here.component "databaseInput" cpt
355
  where
356
    cpt { databases
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
        , search } _ = do
      search' <- T.useLive T.unequal search

      let db = case search'.datafield of
            (Just (External (Just x))) -> Just x
            _                          -> Nothing

          liItem :: Database -> R.Element
          liItem  db' = H.option { className : "text-primary center"
                                 , value: show db' } [ H.text (show db') ]

          change e = do
            let value = read $ R.unsafeEventValue e
            T.modify_ (_ { datafield = Just $ External value
                         , databases = fromMaybe Empty value
                         }) search

374
      pure $
375 376
        H.div { className: "form-group" }
        [ H.div {className: "text-primary center"} [ H.text "in database" ]
377
        , R2.select { className: "form-control"
378 379
                    , defaultValue: defaultValue search'.datafield
                    , on: { change }
380 381 382 383
                    } (liItem <$> databases)
        , H.div {className:"center"} [ H.text $ maybe "" doc db ]
        ]

384
    defaultValue datafield = show $ maybe Empty datafield2database datafield
385 386


387 388 389
type OrgInputProps =
  ( orgs :: Array Org
  | ComponentProps)
390

391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
orgInput :: R2.Component OrgInputProps
orgInput = R.createElement orgInputCpt
orgInputCpt :: R.Component OrgInputProps
orgInputCpt = here.component "orgInput" cpt
  where
    cpt { orgs, search } _ = do
      let change e = do
            let value = R.unsafeEventValue e
            T.modify_ (_ { datafield = Just $ External $ Just $ HAL $ read value }) search

      pure $ H.div { className: "form-group" }
        [ H.div {className: "text-primary center"} [H.text "filter with organization: "]
        , R2.select { className: "form-control"
                    , on: { change }
                    } (liItem <$> orgs)
        ]

    liItem :: Org -> R.Element
    liItem  org = H.option {className : "text-primary center"} [ H.text (show org) ]
410

411
{-
412 413
filterInput :: R.State String -> R.Element
filterInput (term /\ setTerm) =
414 415 416 417
  H.div { className: "form-group" }
        [ H.input { defaultValue: term
                  , className: "form-control"
                  , type: "text"
418
                  , on: { change: setTerm <<< const <<< R.unsafeEventValue }
419 420 421 422
                  , "required pattern": "[[0-9]+[ ]+]*"
                  -- TODO                          ^FIXME not sure about the regex comprehension: that should match "123 2334 44545" only (Integers separated by one space)
                  -- form validation with CSS
                  -- DOC: https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
423
                  , placeholder : "Filter with struct_Ids as integer"
424 425 426
                  }
         ]
-}
427

428 429
type DatafieldInputProps =
  ( databases :: Array Database
430 431 432
  , langs     :: Array Lang
  , search    :: T.Box Search
  , session   :: Session )
433 434 435 436 437

datafieldInput :: R2.Component DatafieldInputProps
datafieldInput = R.createElement datafieldInputCpt
datafieldInputCpt :: R.Component DatafieldInputProps
datafieldInputCpt = here.component "datafieldInput" cpt where
438
  cpt { databases, langs, search, session } _ = do
439 440
    search' <- T.useLive T.unequal search
    iframeRef <- R.useRef null
441

442
    pure $ H.div {}
443
      [ dataFieldNav { search } []
444

445 446 447
      , if isExternal search'.datafield
        then databaseInput { databases, search } []
        else H.div {} []
448

449 450 451
      , if isHAL search'.datafield
        then orgInput { orgs: allOrgs, search } []
        else H.div {} []
452

453
      , if isIMT search'.datafield
454
        then componentIMT { search, session } []
455
        else H.div {} []
456 457

      , if isHAL search'.datafield
458
        then componentYears { search } []
459
        else H.div {} []
460

461 462 463
      , if isCNRS search'.datafield
        then componentCNRS { search } []
        else H.div {} []
464

465 466 467
      , if needsLang search'.datafield
        then langNav { langs, search } []
        else H.div {} []
468

469 470 471
      , H.div {} [ searchIframes { iframeRef, search } [] ]
      ]

472 473
type SearchInputProps =
  (
474
    search :: T.Box Search
475 476
  )

477 478
searchInput :: R2.Component SearchInputProps
searchInput = R.createElement searchInputCpt
479
searchInputCpt :: R.Component SearchInputProps
480
searchInputCpt = here.component "searchInput" cpt
481
  where
482 483
    cpt { search } _ = do
      { term } <- T.useLive T.unequal search
484 485
      valueRef <- R.useRef term

486 487 488 489 490 491 492 493 494 495
      pure $ H.div { className: "" }
        [ inputWithEnter { onBlur: onBlur valueRef search
                         , onEnter: onEnter valueRef search
                         , onValueChanged: onValueChanged valueRef
                         , autoFocus: false
                         , className: "form-control"
                         , defaultValue: R.readRef valueRef
                         , placeholder: "Your query here"
                         , type: "text" }
        ]
496 497 498 499 500 501 502 503 504 505

      -- pure $
      --   H.div { className : "" }
      --   [ H.input { className: "form-control"
      --             , defaultValue: search.term
      --             , on: { input : onInput valueRef setSearch }
      --             , placeholder: "Your Query here"
      --             , type: "text"
      --             }
      --   ]
506
    onBlur valueRef search value = do
507
      R.setRef valueRef value
508 509 510
      T.modify_ (_ { term = value }) search
    onEnter valueRef search _ = do
      T.modify_ (_ { term = R.readRef valueRef }) search
511 512 513 514

    onValueChanged valueRef value = do
      R.setRef valueRef value
      -- setSearch $ _ { term = value }
515 516

type SubmitButtonProps =
517 518 519 520
  ( errors    :: T.Box (Array FrontendError)
  , onSearch  :: GT.AsyncTaskWithType -> Effect Unit
  , search    :: T.Box Search
  , selection :: T.Box ListSelection.Selection
521
  , session   :: Session
522 523
  )

524 525
submitButton :: R2.Component SubmitButtonProps
submitButton = R.createElement submitButtonComponent
526
submitButtonComponent :: R.Component SubmitButtonProps
527
submitButtonComponent = here.component "submitButton" cpt
528
  where
529
    cpt { errors, onSearch, search, selection, session } _ = do
530
      search' <- T.useLive T.unequal search
531
      selection' <- T.useLive T.unequal selection
532

533 534
      pure $
        H.button { className: "btn btn-primary"
535
                 , "type"   : "button"
536
                 , on       : { click: doSearch onSearch errors session selection' search' }
537
                 , style    : { width: "100%" }
538 539
                 }
        [ H.text "Launch Search" ]
540

541 542 543
    doSearch onSearch errors session selection search = \_ -> do
      log2 "[submitButton] searching" search
      triggerSearch { onSearch, errors, session, selection, search }
544 545 546 547
      --case search.term of
      --  "" -> setSearch $ const defaultSearch
      --  _  -> setSearch $ const q

548 549 550 551 552 553 554 555 556 557 558 559 560
type TriggerSearch =
  ( errors    :: T.Box (Array FrontendError)
  , onSearch  :: GT.AsyncTaskWithType -> Effect Unit
  , search    :: T.Box Search
  , selection :: T.Box ListSelection.Selection
  , session   :: Session
  )

triggerSearch :: { onSearch :: (GT.AsyncTaskWithType -> Effect Unit)
                 , errors :: T.Box (Array FrontendError)
                 , session :: Session
                 , selection :: ListSelection.Selection
                 , search :: Search }
561
              -> Effect Unit
562
triggerSearch { onSearch, errors, session, selection, search } =
563 564
  launchAff_ $ do
    liftEffect $ do
565
      let here' = "[triggerSearch] Searching "
566 567 568 569
      here.log2 (here' <> "databases: ") (show search.databases)
      here.log2 (here' <> "datafield: ") (show search.datafield)
      here.log2 (here' <> "term: ")            search.term
      here.log2 (here' <> "lang: ")      (show search.lang)
570

571
    case search.node_id of
572
      Nothing -> liftEffect $ here.log "[triggerSearch] node_id is Nothing, don't know what to do"
573
      Just id -> do
574
        liftEffect $ here.log2 "[triggerSearch] searchQuery" $ searchQuery selection search
575
        eTask <- performSearch session id $ searchQuery selection search
576
        handleRESTError errors eTask $ \task -> liftEffect $ do
577
          here.log2 "[triggerSearch] task" task
578
          onSearch task
579

580 581 582
    --liftEffect $ do
    --  log2 "Return:" r
    --  modalShow "addCorpus"
583

584 585
searchQuery :: ListSelection.Selection -> Search -> SearchQuery
searchQuery selection { datafield: Nothing, term } =
Alexandre Delanoë's avatar
Alexandre Delanoë committed
586 587
                      over SearchQuery (_ { query = term
                                          , selection = selection }) defaultSearchQuery
588 589 590 591 592 593
-- TODO Simplify both HAL Nothing and HAL (Just IMT) cases
searchQuery selection { databases
                      , datafield: datafield@(Just (External (Just (HAL Nothing))))
                      , lang
                      , term
                      , node_id
Alexandre Delanoë's avatar
Alexandre Delanoë committed
594 595 596 597 598 599 600
                      , years } = over SearchQuery (_ { databases = databases
                                                      , datafield = datafield
                                                      , lang      = lang
                                                      , node_id   = node_id
                                                      , query     = queryHAL term Nothing lang years
                                                      , selection = selection
                                                      }) defaultSearchQuery
601 602 603 604 605
searchQuery selection { databases
                      , datafield: datafield@(Just (External (Just (HAL (Just (IMT imtOrgs))))))
                      , lang
                      , term
                      , node_id
Alexandre Delanoë's avatar
Alexandre Delanoë committed
606 607 608 609 610 611 612 613
                      , years } = over SearchQuery (_ { databases = databases
                                , datafield = datafield
                                , lang      = lang
                                , node_id   = node_id
                                , query     = queryHAL term (Just imtOrgs) lang years
                                , selection = selection
                                }) defaultSearchQuery

614
searchQuery selection { databases, datafield, lang, term, node_id } =
Alexandre Delanoë's avatar
Alexandre Delanoë committed
615 616 617 618 619 620 621
                                    over SearchQuery (_ { databases = databases
                                                        , datafield = datafield
                                                        , lang      = lang
                                                        , node_id   = node_id
                                                        , query     = term
                                                        , selection = selection
                                                        }) defaultSearchQuery
622

623 624
queryHAL :: String -> Maybe (Set.Set IMT_org) -> Maybe Lang -> Array String -> String
queryHAL term mIMTOrgs lang years =
625 626 627
  joinWith " AND " $ filterOutEmptyString [ titleAbstract
                                          , structQuery
                                          , yearQuery ]
628
  where
629 630 631 632 633 634
    -- this uses solr query syntax
    -- https://yonik.com/solr/query-syntax/
    titleAbstract = case term of
      "" -> ""
      _ -> "(" <> langPrefix <> "_title_t:" <> termMulti <>
           " OR " <> langPrefix <> "_abstract_t:" <> termMulti <> ")"
635 636 637
    langPrefix = case lang of
      Just FR -> "fr"
      _       -> "en"
638
    -- TODO: Escape double quotes
639 640 641
    --termEscaped = "\"" <> (replaceAll (Pattern "\"") (Replacement "\\\"") term) <> "\""
    termEscaped = "\"" <> term <> "\""
    termMulti = "(" <> term <> ")"
642 643 644
    structQuery = case mIMTOrgs of
      Nothing -> ""
      Just imtOrgs -> if Set.isEmpty imtOrgs then
645 646
        ""
      else
647 648
        " (" <> (structIds imtOrgs) <> ")"
    yearQuery = joinWith " AND " $ (\year -> "producedDateY_i:" <> year) <$> years
649 650 651
    joinFunc :: IMT_org -> String
    joinFunc All_IMT = ""
    joinFunc (IMT_org { school_id }) = "structId_i:" <> school_id
652
    structIds :: Set.Set IMT_org -> String
653 654
    structIds imtOrgs = joinWith " OR " $ filterOutEmptyString $ joinFunc <$> Set.toUnfoldable imtOrgs
    filterOutEmptyString = A.filter (_ /= "")