DocsTable.purs 21 KB
Newer Older
1
-- TODO: this module should be replaced by FacetsTable
2
module Gargantext.Components.DocsTable where
3

4 5
import DOM.Simple.Console (log2)
import DOM.Simple.Event as DE
6
import Data.Argonaut (class EncodeJson, jsonEmptyObject, (:=), (~>))
7
import Data.Array as A
8
import Data.Generic.Rep (class Generic)
9
import Data.Lens ((^.))
10 11
import Data.Lens.At (at)
import Data.Lens.Record (prop)
12
import Data.Map    as Map
13
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
14
import Data.Newtype (class Newtype)
15
import Data.Ord.Down (Down(..))
16
import Data.Set (Set)
17
import Data.Set    as Set
18
import Data.String as Str
19
import Data.Symbol (SProxy(..))
20
import Data.Tuple (Tuple(..))
21
import Effect (Effect)
22
import Effect.Aff (Aff, launchAff_)
23
import Effect.Class (liftEffect)
24 25
import Gargantext.Components.Category (rating)
import Gargantext.Components.Category.Types (Star(..))
26
import Gargantext.Components.DocsTable.Types (DocumentsView(..), Hyperdata(..), LocalUserScore, Query, Response(..), Year, sampleData)
27
import Gargantext.Components.Nodes.Lists.Types as NT
28
import Gargantext.Components.Nodes.Texts.Types as TextsT
29 30
import Gargantext.Components.Table             as TT
import Gargantext.Components.Table.Types       as TT
James Laver's avatar
James Laver committed
31
import Gargantext.Ends (Frontends, url)
32
import Gargantext.Hooks.Loader (useLoader, useLoaderWithCacheAPI, HashedResponse(..))
33
import Gargantext.Prelude
34
import Gargantext.Prelude (class Ord, Unit, bind, const, discard, identity, mempty, otherwise, pure, show, unit, ($), (/=), (<$>), (<<<), (<>), (==))
35
import Gargantext.Routes (SessionRoute(NodeAPI))
36
import Gargantext.Routes as Routes
Nicolas Pouillard's avatar
Nicolas Pouillard committed
37
import Gargantext.Sessions (Session, sessionId, get, delete)
38
import Gargantext.Types (ListId, NodeID, NodeType(..), OrderBy(..), SidePanelState(..), TableResult, TabSubType, TabType, showTabType')
Nicolas Pouillard's avatar
Nicolas Pouillard committed
39
import Gargantext.Utils (sortWith)
40
import Gargantext.Utils.CacheAPI as GUC
41
import Gargantext.Utils.QueryString (joinQueryStrings, mQueryParam, mQueryParamS, queryParam, queryParamS)
42
import Gargantext.Utils.Reactix as R2
43
import Gargantext.Utils.Toestand as T2
44
import Prelude
45 46
import Reactix as R
import Reactix.DOM.HTML as H
47
import Simple.JSON as JSON
48
import Toestand as T
49

50 51
here :: R2.Here
here = R2.here "Gargantext.Components.DocsTable"
52 53 54

type TotalRecords = Int

James Laver's avatar
James Laver committed
55 56 57
type Path a =
  ( corpusId  :: Int
  , listId    :: Int
58
  , frontends :: Frontends
James Laver's avatar
James Laver committed
59 60
  , session   :: Session
  , tabType   :: TabSubType a
61 62
  )

63 64 65 66 67 68 69 70 71 72
type CommonProps =
  ( cacheState     :: T.Box NT.CacheState
  , frontends      :: Frontends
  , listId         :: Int
  , mCorpusId      :: Maybe Int
  , nodeId         :: Int
  , session        :: Session
  , sidePanel      :: T.Box (Maybe (Record TextsT.SidePanel))
  , sidePanelState :: T.Box SidePanelState
  , tabType        :: TabType
73
  -- ^ tabType is not ideal here since it is too much entangled with tabs and
74
  -- ngramtable. Let's see how this evolves.  )
75 76
  , totalRecords   :: Int
  , yearFilter     :: T.Box (Maybe Year)
77 78 79 80 81 82 83 84
  )

type LayoutProps =
  (
    chart      :: R.Element
  , showSearch :: Boolean
  | CommonProps
  -- , path      :: Record (Path a)
85 86
  )

James Laver's avatar
James Laver committed
87
type PageLayoutProps =
88 89 90 91 92
  (
    key    :: String  -- NOTE Necessary to clear the component when cache state changes
  , params :: TT.Params
  , query  :: Query
  | CommonProps
93
  )
94

95
_documentIdsDeleted  = prop (SProxy :: SProxy "documentIdsDeleted")
96
_localCategories     = prop (SProxy :: SProxy "localCategories")
97

98 99 100 101
docViewLayout :: Record LayoutProps -> R.Element
docViewLayout props = R.createElement docViewLayoutCpt props []

docViewLayoutCpt :: R.Component LayoutProps
102
docViewLayoutCpt = here.component "docViewLayout" cpt
103 104
  where
    cpt layout _children = do
105
      query <- T.useBox ""
106
      let params = TT.initialParams
107
      pure $ docView { layout, params, query } []
108

109 110
type Props = (
    layout :: Record LayoutProps
111
  , params :: TT.Params
112
  , query  :: T.Box Query
113
  )
114

115 116
docView :: R2.Component Props
docView = R.createElement docViewCpt
117 118

docViewCpt :: R.Component Props
119
docViewCpt = here.component "docView" cpt where
120 121 122 123
  cpt { layout: { cacheState
                , chart
                , frontends
                , listId
124
                , mCorpusId
125 126 127
                , nodeId
                , session
                , showSearch
128 129
                , sidePanel
                , sidePanelState
130 131
                , tabType
                , totalRecords
132
                , yearFilter
133 134 135 136
                }
      , params
      , query
      } _ = do
137
    cacheState' <- T.useLive T.unequal cacheState
138
    query' <- T.useLive T.unequal query
139

140
    pure $ H.div { className: "doc-table-doc-view container1" }
141
      [ R2.row
142
        [ chart
143
        , if showSearch then searchBar { query } [] else H.div {} []
144
        , H.div {className: "col-md-12"}
145
          [ pageLayout { cacheState
146
                       , frontends
147
                       , key: "docView-" <> (show cacheState')
148
                       , listId
149
                       , mCorpusId
150 151
                       , nodeId
                       , params
152
                       , query: query'
153
                       , session
154 155
                       , sidePanel
                       , sidePanelState
156 157
                       , tabType
                       , totalRecords
158
                       , yearFilter
159
                       } [] ] ] ]
160

161 162 163 164 165 166 167 168
type SearchBarProps =
  ( query :: T.Box Query )

searchBar :: R2.Component SearchBarProps
searchBar = R.createElement searchBarCpt

searchBarCpt :: R.Component SearchBarProps
searchBarCpt = here.component "searchBar" cpt
169
  where
170 171 172 173
    cpt { query } _children = do
      query' <- T.useLive T.unequal query
      queryText <- T.useBox query'
      queryText' <- T.useLive T.unequal queryText
174

175 176
      pure $ H.div {className: "col-md-12 row"}
        [ H.div {className: "col-md-3"} []
177
        , H.div {className: "col-md-1"} [if query' /= "" then (clearButton query) else H.div {} []]
178
        , H.div {className: "col-md-3 form-group"}
179 180 181 182 183 184
          [ H.input { className: "form-control"
                    , defaultValue: query'
                    , on: { change: onSearchChange queryText
                          , keyUp: onSearchKeyup query queryText' }
                    , placeholder: query'
                    , type: "text" }
185
          ]
186
        , H.div {className: "col-md-1"} [ searchButton query queryText' ]
187 188
        ]

189 190 191
    onSearchChange :: forall e. T.Box Query -> e -> Effect Unit
    onSearchChange queryText e =
      T.write_ (R.unsafeEventValue e) queryText
192

193 194
    onSearchKeyup :: T.Box Query -> Query -> DE.KeyboardEvent -> Effect Unit
    onSearchKeyup query queryText e =
195
      if DE.key e == "Enter" then
196
        T.write_ queryText query
197
      else
198
        pure unit
199

200 201 202 203 204
    searchButton query queryText' =
      H.button { className: "btn btn-primary"
               , on: { click: \e -> T.write_ queryText' query }
               , type: "submit" }
        [ H.span {className: "fa fa-search"} [] ]
205

206
    clearButton query =
207
      H.button { className: "btn btn-danger"
208 209
               , on: { click: \e -> T.write_ "" query } }
        [ H.span {className: "fa fa-times"} [] ]
210 211 212 213

mock :: Boolean
mock = false

214
type PageParams = {
215 216 217 218 219 220 221
    listId      :: Int
  , mCorpusId   :: Maybe Int
  , nodeId      :: Int
  , tabType     :: TabType
  , query       :: Query
  , params      :: TT.Params
  , yearFilter  :: Maybe Year
222
  }
223

224
getPageHash :: Session -> PageParams -> Aff String
225
getPageHash session { nodeId, tabType } = do
226
  (get session $ tableHashRoute nodeId tabType) :: Aff String
227

228 229 230 231 232 233 234
convOrderBy :: Maybe (TT.OrderByDirection TT.ColumnName) -> Maybe OrderBy
convOrderBy (Just (TT.ASC  (TT.ColumnName "Date")))  = Just DateAsc
convOrderBy (Just (TT.DESC (TT.ColumnName "Date")))  = Just DateDesc
convOrderBy (Just (TT.ASC  (TT.ColumnName "Title"))) = Just TitleAsc
convOrderBy (Just (TT.DESC (TT.ColumnName "Title"))) = Just TitleDesc
convOrderBy (Just (TT.ASC  (TT.ColumnName "Source"))) = Just SourceAsc
convOrderBy (Just (TT.DESC (TT.ColumnName "Source"))) = Just SourceDesc
235
convOrderBy _ = Nothing
236

237 238 239
res2corpus :: Response -> DocumentsView
res2corpus (Response r) =
  DocumentsView { _id : r.cid
240 241
  , category   : r.category
  , date       : (\(Hyperdata hr) -> hr.pub_year) r.hyperdata
242
  , ngramCount : r.ngramCount
243 244 245 246
  , score      : r.score
  , source     : (\(Hyperdata hr) -> hr.source) r.hyperdata
  , title      : (\(Hyperdata hr) -> hr.title) r.hyperdata
  , url        : ""
247 248
}

249 250 251 252 253 254 255
filterDocs :: Query -> Array Response -> Array Response
filterDocs query docs = A.filter filterFunc docs
  where
    filterFunc :: Response -> Boolean
    filterFunc (Response { hyperdata: Hyperdata { title } }) =
      isJust $ Str.indexOf (Str.Pattern $ Str.toLower query) $ Str.toLower title

256 257 258 259 260 261
filterDocsByYear :: Year -> Array Response -> Array Response
filterDocsByYear year docs = A.filter filterFunc docs
  where
    filterFunc :: Response -> Boolean
    filterFunc (Response { hyperdata: Hyperdata { pub_year } }) = eq year $ show pub_year

262 263
pageLayout :: R2.Component PageLayoutProps
pageLayout = R.createElement pageLayoutCpt
264

265
pageLayoutCpt :: R.Component PageLayoutProps
266
pageLayoutCpt = here.component "pageLayout" cpt where
267 268 269
  cpt props@{ cacheState
            , frontends
            , listId
270
            , mCorpusId
271 272 273 274
            , nodeId
            , params
            , query
            , session
275
            , sidePanel
276 277 278
            , tabType
            , yearFilter
            } _ = do
279
    cacheState' <- T.useLive T.unequal cacheState
280
    yearFilter' <- T.useLive T.unequal yearFilter
281

282
    let path = { listId, mCorpusId, nodeId, params, query, tabType, yearFilter: yearFilter' }
283 284 285
        handleResponse :: HashedResponse (TableResult Response) -> Tuple Int (Array DocumentsView)
        handleResponse (HashedResponse { hash, value: res }) = ret
          where
286 287 288 289 290 291 292 293

            filters = filterDocs query
                    >>> \res' -> case yearFilter' of
                      Nothing -> res'
                      Just year -> filterDocsByYear year res'

            docs = res2corpus <$> filters res.docs

294 295 296 297
            ret = if mock then
                      --Tuple 0 (take limit $ drop offset sampleData)
                      Tuple 0 sampleData
                    else
298
                      Tuple res.count docs
299

300 301
    case cacheState' of
      NT.CacheOn -> do
302 303 304
        let paint (Tuple count docs) = page { documents: docs
                                            , layout: props { totalRecords = count }
                                            , params } []
305
            mkRequest :: PageParams -> GUC.Request
Nicolas Pouillard's avatar
Nicolas Pouillard committed
306
            mkRequest p = GUC.makeGetRequest session $ tableRoute p
307 308 309 310 311 312 313 314

        useLoaderWithCacheAPI {
            cacheEndpoint: getPageHash session
          , handleResponse
          , mkRequest
          , path
          , renderer: paint
          }
315
      NT.CacheOff -> do
316
        localCategories <- T.useBox (Map.empty :: LocalUserScore)
317 318
        paramsS <- T.useBox params
        paramsS' <- T.useLive T.unequal paramsS
Nicolas Pouillard's avatar
Nicolas Pouillard committed
319
        let loader p = do
320
              let route = tableRouteWithPage (p { params = paramsS', query = query })
321 322
              res <- get session $ route
              liftEffect $ do
323 324
                here.log2 "table route" route
                here.log2 "table res" res
325
              pure $ handleResponse res
326
            render (Tuple count documents) = pagePaintRaw { documents
327
                                                          , layout: props { params = paramsS'
328 329
                                                                          , totalRecords = count }
                                                          , localCategories
330
                                                          , params: paramsS } []
331
        useLoader (path { params = paramsS' }) loader render
332

333 334
type PageProps = (
    documents :: Array DocumentsView
335
  , layout :: Record PageLayoutProps
336
  , params :: TT.Params
337
  )
338

339 340
page :: R2.Component PageProps
page = R.createElement pageCpt
341

342
pageCpt :: R.Component PageProps
343
pageCpt = here.component "pageCpt" cpt where
344
  cpt { documents, layout, params } _ = do
345 346
    paramsS <- T.useBox params

347
    pure $ pagePaint { documents, layout, params: paramsS } []
348 349 350 351

type PagePaintProps = (
    documents :: Array DocumentsView
  , layout :: Record PageLayoutProps
352
  , params :: T.Box TT.Params
353 354
)

355 356
pagePaint :: R2.Component PagePaintProps
pagePaint = R.createElement pagePaintCpt
357 358

pagePaintCpt :: R.Component PagePaintProps
359
pagePaintCpt = here.component "pagePaintCpt" cpt
360 361
  where
    cpt { documents, layout, params } _ = do
362 363
      params' <- T.useLive T.unequal params

364
      localCategories <- T.useBox (Map.empty :: LocalUserScore)
365
      pure $ pagePaintRaw { documents: A.fromFoldable (filteredRows params')
366 367 368 369
                          , layout
                          , localCategories
                          , params } []
        where
370 371
          orderWith { orderBy } =
            case convOrderBy orderBy of
372 373 374 375 376 377 378
              Just DateAsc    -> sortWith \(DocumentsView { date })   -> date
              Just DateDesc   -> sortWith \(DocumentsView { date })   -> Down date
              Just SourceAsc  -> sortWith \(DocumentsView { source }) -> Str.toLower source
              Just SourceDesc -> sortWith \(DocumentsView { source }) -> Down $ Str.toLower source
              Just TitleAsc   -> sortWith \(DocumentsView { title })  -> Str.toLower title
              Just TitleDesc  -> sortWith \(DocumentsView { title })  -> Down $ Str.toLower title
              _               -> identity -- the server ordering is enough here
379
          filteredRows params' = TT.filterRows { params: params' } $ (orderWith params') $ A.toUnfoldable documents
380 381 382


type PagePaintRawProps = (
383 384 385 386
    documents       :: Array DocumentsView
  , layout          :: Record PageLayoutProps
  , localCategories :: T.Box LocalUserScore
  , params          :: T.Box TT.Params
387 388
  )

389 390
pagePaintRaw :: R2.Component PagePaintRawProps
pagePaintRaw = R.createElement pagePaintRawCpt
391 392

pagePaintRawCpt :: R.Component PagePaintRawProps
393
pagePaintRawCpt = here.component "pagePaintRawCpt" cpt where
394
  cpt { documents
395
      , layout: { frontends
396
                , listId
397
                , mCorpusId
398 399
                , nodeId
                , session
400 401
                , sidePanel
                , sidePanelState
402
                , totalRecords }
403 404
      , localCategories
      , params } _ = do
405
    reload <- T.useBox T2.newReload
406 407 408 409
    mCurrentDocId <- T.useFocused
          (maybe Nothing _.mCurrentDocId)
          (\val -> maybe Nothing (\sp -> Just $ sp { mCurrentDocId = val })) sidePanel
    mCurrentDocId' <- T.useLive T.unequal mCurrentDocId
410

411 412
    localCategories' <- T.useLive T.unequal localCategories

413
    pure $ TT.table
414
      { colNames
415
      , container: TT.defaultContainer { title: "Documents" }
416
      , params
417
      , rows: rows reload localCategories' mCurrentDocId'
418
      , syncResetButton : [ H.div {} [] ]
419 420 421
      , totalRecords
      , wrapColElts
      }
422
      where
423
        sid = sessionId session
424
        gi Star_1  = "fa fa-star"
425
        gi _       = "fa fa-star-empty"
426
        trashClassName Star_0 _ = "trash"
427 428
        trashClassName _ true = "active"
        trashClassName _ false = ""
429
        corpusDocument
430
          | Just cid <- mCorpusId = Routes.CorpusDocument sid cid listId
431
          | otherwise = Routes.Document sid listId
432
        colNames = TT.ColumnName <$> [ "Show", "Tag", "Date", "Title", "Source", "Score" ]
433
        wrapColElts = const identity
434
        rows reload localCategories' mCurrentDocId' = row <$> A.toUnfoldable documents
435
          where
436
            row dv@(DocumentsView r@{ _id, category }) =
437
              { row:
438
                TT.makeRow [ -- H.div {} [ H.a { className, style, on: {click: click Favorite} } [] ]
439
                            H.div { className: "" }
440 441 442 443 444 445 446
                                  [ docChooser { listId
                                               , mCorpusId
                                               , nodeId: r._id
                                               , sidePanel
                                               , sidePanelState
                                               , tableReload: reload } []
                                  ]
447
                          --, H.div { className: "column-tag flex" } [ caroussel { category: cat, nodeId, row: dv, session, setLocalCategories } [] ]
448
                          , H.div { className: "column-tag flex" }
449 450 451 452 453
                                  [ rating { nodeId
                                           , row: dv
                                           , score: cat
                                           , setLocalCategories: \lc -> T.modify_ lc localCategories
                                           , session } [] ]
454
                --, H.input { type: "checkbox", defaultValue: checked, on: {click: click Trash} }
455
                -- TODO show date: Year-Month-Day only
456
                , H.div { className: tClassName } [ R2.showText r.date ]
457 458 459 460
                , H.div { className: tClassName }
                        [ H.a { href: url frontends $ corpusDocument r._id, target: "_blank"}
                              [ H.text r.title ]
                        ]
461
                , H.div { className: tClassName } [ H.text $ if r.source == "" then "Source" else r.source ]
462
                , H.div {} [ H.text $ maybe "-" show r.ngramCount ]
463
                ]
464 465
              , delete: true }
              where
466
                cat         = fromMaybe category (localCategories' ^. at _id)
467
                -- checked    = Star_1 == cat
468
                selected   = mCurrentDocId' == Just r._id
469
                tClassName = trashClassName cat selected
470
                className  = gi cat
471

472
type DocChooser = (
473 474 475 476 477 478
    listId         :: ListId
  , mCorpusId      :: Maybe NodeID
  , nodeId         :: NodeID
  , sidePanel      :: T.Box (Maybe (Record TextsT.SidePanel))
  , sidePanelState :: T.Box SidePanelState
  , tableReload    :: T2.ReloadS
479 480 481 482 483 484
  )

docChooser :: R2.Component DocChooser
docChooser = R.createElement docChooserCpt

docChooserCpt :: R.Component DocChooser
485
docChooserCpt = here.component "docChooser" cpt
486 487 488 489 490 491 492
  where
    cpt { mCorpusId: Nothing } _ = do
      pure $ H.div {} []

    cpt { listId
        , mCorpusId: Just corpusId
        , nodeId
493 494
        , sidePanel
        , sidePanelState
495
        , tableReload } _ = do
496 497 498 499
      mCurrentDocId <- T.useFocused
            (maybe Nothing _.mCurrentDocId)
            (\val -> maybe Nothing (\sp -> Just $ sp { mCurrentDocId = val })) sidePanel
      mCurrentDocId' <- T.useLive T.unequal mCurrentDocId
500

501 502
      let selected = mCurrentDocId' == Just nodeId
          eyeClass = if selected then "fa-eye" else "fa-eye-slash"
503

504
      pure $ H.div { className: "btn" } [
505
        H.span { className: "fa " <> eyeClass
506
               , on: { click: onClick selected } } []
507 508
      ]
      where
509
        onClick selected _ = do
510 511 512
          -- log2 "[docChooser] onClick, listId" listId
          -- log2 "[docChooser] onClick, corpusId" corpusId
          -- log2 "[docChooser] onClick, nodeId" nodeId
513 514
          -- R2.callTrigger triggerAnnotatedDocIdChange { corpusId, listId, nodeId }
          -- T2.reload tableReload
515 516 517 518 519 520 521 522 523
          if selected then do
            T.write_ Nothing sidePanel
            T.write_ Closed sidePanelState
          else do
            T.write_ (Just { corpusId: corpusId
                          , listId: listId
                          , mCurrentDocId: Just nodeId
                          , nodeId: nodeId }) sidePanel
            T.write_ Opened sidePanelState
524
          log2 "[docChooser] sidePanel opened" sidePanelState
525

526

527 528 529
newtype SearchQuery = SearchQuery {
    parent_id :: Int
  , query :: Array String
530
  }
531 532 533
derive instance Generic SearchQuery _
derive instance Newtype SearchQuery _
derive newtype instance JSON.ReadForeign SearchQuery
534 535


536 537
documentsRoute :: Int -> SessionRoute
documentsRoute nodeId = NodeAPI Node (Just nodeId) "documents"
538

539 540
tableRoute :: forall row. { listId :: Int, nodeId :: Int, tabType :: TabType | row} -> SessionRoute
tableRoute { listId, nodeId, tabType } = NodeAPI Node (Just nodeId) $ "table" <> "?tabType=" <> (showTabType' tabType) <> "&list=" <> (show listId)
541 542 543 544

tableHashRoute :: Int -> TabType -> SessionRoute
tableHashRoute nodeId tabType = NodeAPI Node (Just nodeId) $ "table/hash" <> "?tabType=" <> (showTabType' tabType)

Nicolas Pouillard's avatar
Nicolas Pouillard committed
545 546 547
tableRouteWithPage :: forall row.
                      { listId :: Int
                      , nodeId :: Int
548
                      , params :: TT.Params
Nicolas Pouillard's avatar
Nicolas Pouillard committed
549 550
                      , query ::  Query
                      , tabType :: TabType
551
                      , yearFilter :: Maybe Year
Nicolas Pouillard's avatar
Nicolas Pouillard committed
552
                      | row } -> SessionRoute
553 554
tableRouteWithPage { listId, nodeId, params: { limit, offset, orderBy, searchType }, query, tabType, yearFilter } =
  NodeAPI Node (Just nodeId) $ "table" <> joinQueryStrings [tt, lst, lmt, odb, ofs, st, q, y]
555 556 557 558
  where
    lmt = queryParam "limit" limit
    lst = queryParam "list" listId
    ofs = queryParam "offset" offset
559
    odb = mQueryParamS "orderBy" TT.orderByToForm orderBy
560 561 562
    st  = queryParam "searchType" searchType
    tt  = queryParamS "tabType" (showTabType' tabType)
    q   = queryParamS "query" query
563
    y   = mQueryParam "year" yearFilter
564

565
deleteAllDocuments :: Session -> Int -> Aff (Array Int)
566
deleteAllDocuments session = delete session <<< documentsRoute
567 568 569 570 571 572

-- TODO: not optimal but Data.Set lacks some function (Set.alter)
toggleSet :: forall a. Ord a => a -> Set a -> Set a
toggleSet a s
  | Set.member a s = Set.delete a s
  | otherwise      = Set.insert a s