Corpus.purs 17.6 KB
Newer Older
1
module Gargantext.Components.Nodes.Corpus where
2

3 4
import Data.Argonaut (class DecodeJson, decodeJson, encodeJson)
import Data.Argonaut.Parser (jsonParser)
5
import Data.Array as A
6
import Data.Either (Either(..))
7
import Data.List as List
8
import Data.Maybe (Maybe(..), fromMaybe)
9
import Data.Tuple (Tuple(..), fst, snd)
10
import Data.Tuple.Nested ((/\))
11
import DOM.Simple.Console (log2)
12
import Effect (Effect)
13 14
import Effect.Aff (Aff, launchAff_, throwError)
import Effect.Class (liftEffect)
15
import Effect.Exception (error)
16
import Reactix as R
17
import Reactix.DOM.HTML as H
18

19
import Gargantext.Prelude
20

21
import Gargantext.Components.CodeEditor as CE
22
import Gargantext.Components.InputWithEnter (inputWithEnter)
23
import Gargantext.Components.Node (NodePoly(..), HyperdataList)
24
import Gargantext.Components.Nodes.Corpus.Types (CorpusData, FTField, Field(..), FieldType(..), Hash, Hyperdata(..), defaultField, defaultHaskell', defaultPython', defaultJSON', defaultMarkdown')
25
import Gargantext.Data.Array as GDA
26
import Gargantext.Hooks.Loader (useLoader)
27
import Gargantext.Routes (SessionRoute(NodeAPI, Children))
28
import Gargantext.Sessions (Session, get, put, sessionId)
29
import Gargantext.Types (NodeType(..), AffTableResult, ReloadS)
30
import Gargantext.Utils.Crypto as Crypto
31
import Gargantext.Utils.Reactix as R2
Nicolas Pouillard's avatar
Nicolas Pouillard committed
32

33
thisModule :: String
34 35
thisModule = "Gargantext.Components.Nodes.Corpus"

36 37
type Props =
  ( nodeId  :: Int
38 39
  , session :: Session
  )
40

41
type KeyProps =
42 43
  ( key :: String
  | Props
44 45
  )

46
corpusLayout :: Record Props -> R.Element
47 48
corpusLayout props = R.createElement corpusLayoutCpt props []

49
corpusLayoutCpt :: R.Component Props
50
corpusLayoutCpt = R.hooksComponentWithModule thisModule "corpusLayout" cpt
51
  where
52 53 54 55 56 57 58 59 60 61
    cpt { nodeId, session } _ = do
      let sid = sessionId session

      pure $ corpusLayoutWithKey { key: show sid <> "-" <> show nodeId, nodeId, session }


corpusLayoutWithKey :: Record KeyProps -> R.Element
corpusLayoutWithKey props = R.createElement corpusLayoutWithKeyCpt props []

corpusLayoutWithKeyCpt :: R.Component KeyProps
62
corpusLayoutWithKeyCpt = R.hooksComponentWithModule thisModule "corpusLayoutWithKey" cpt
63 64
  where
    cpt { nodeId, session } _ = do
65 66 67 68
      reload <- R.useState' 0

      useLoader {nodeId, reload: fst reload, session} loadCorpusWithReload $
        \corpus -> corpusLayoutView {corpus, nodeId, reload, session}
69

70 71
type ViewProps =
  ( corpus  :: NodePoly Hyperdata
72
  , reload  :: ReloadS
73 74 75
  | Props
  )

76 77
-- We need FTFields with indices because it's the only way to identify the
-- FTField element inside a component (there are no UUIDs and such)
78 79
type Index = Int
type FTFieldWithIndex = Tuple Index FTField
80
type FTFieldsWithIndex = List.List FTFieldWithIndex
81

82 83 84 85
corpusLayoutView :: Record ViewProps -> R.Element
corpusLayoutView props = R.createElement corpusLayoutViewCpt props []

corpusLayoutViewCpt :: R.Component ViewProps
86
corpusLayoutViewCpt = R.hooksComponentWithModule thisModule "corpusLayoutView" cpt
87
  where
88
    cpt {corpus: (NodePoly {hyperdata: Hyperdata {fields}}), nodeId, reload, session} _ = do
89
      let fieldsWithIndex = List.mapWithIndex (\idx -> \t -> Tuple idx t) fields
90
      fieldsS <- R.useState' fieldsWithIndex
91 92 93 94 95 96 97 98 99
      fieldsRef <- R.useRef fields

      -- handle props change of fields
      R.useEffect1' fields $ do
        if R.readRef fieldsRef == fields then
          pure unit
        else do
          R.setRef fieldsRef fields
          snd fieldsS $ const fieldsWithIndex
100 101

      pure $ H.div {} [
102 103 104 105
        H.div { className: "row" } [
           H.div { className: "btn btn-default " <> (saveEnabled fieldsWithIndex fieldsS)
                 , on: { click: onClickSave {fields: fieldsS, nodeId, reload, session} }
                 } [
106
              H.span { className: "fa fa-floppy-o" } [  ]
107 108
              ]
           ]
109 110 111
        , H.div {} [ fieldsCodeEditor { fields: fieldsS
                                      , nodeId
                                      , session } ]
112
        , H.div { className: "row" } [
113 114 115
           H.div { className: "btn btn-default"
                 , on: { click: onClickAdd fieldsS }
                 } [
116
              H.span { className: "fa fa-plus" } [  ]
117 118
              ]
           ]
119
        ]
120

121
    saveEnabled :: FTFieldsWithIndex -> R.State FTFieldsWithIndex -> String
122 123
    saveEnabled fs (fsS /\ _) = if fs == fsS then "disabled" else "enabled"

124
    onClickSave :: forall e. { fields :: R.State FTFieldsWithIndex
125 126 127
                       , nodeId :: Int
                       , reload :: ReloadS
                       , session :: Session } -> e -> Effect Unit
128
    onClickSave {fields: (fieldsS /\ _), nodeId, reload: (_ /\ setReload), session} _ = do
129 130 131 132 133
      log2 "[corpusLayoutViewCpt] onClickSave fieldsS" fieldsS
      launchAff_ do
        saveCorpus $ { hyperdata: Hyperdata {fields: (\(Tuple _ f) -> f) <$> fieldsS}
                     , nodeId
                     , session }
134
        liftEffect $ setReload $ (+) 1
135

136
    onClickAdd :: forall e. R.State FTFieldsWithIndex -> e -> Effect Unit
137
    onClickAdd (_ /\ setFieldsS) _ = do
138
      setFieldsS $ \fieldsS -> List.snoc fieldsS $ Tuple (List.length fieldsS) defaultField
139 140 141

type FieldsCodeEditorProps =
  (
142
    fields :: R.State FTFieldsWithIndex
143 144 145 146 147 148 149
    | LoadProps
  )

fieldsCodeEditor :: Record FieldsCodeEditorProps -> R.Element
fieldsCodeEditor props = R.createElement fieldsCodeEditorCpt props []

fieldsCodeEditorCpt :: R.Component FieldsCodeEditorProps
150
fieldsCodeEditorCpt = R.hooksComponentWithModule thisModule "fieldsCodeEditorCpt" cpt
151
  where
152
    cpt {nodeId, fields: fS@(fields /\ _), session} _ = do
153 154 155
      masterKey <- R.useState' 0

      pure $ H.div {} $ List.toUnfoldable (editors masterKey)
156
      where
157
        editors masterKey =
158
          (\(Tuple idx field) ->
159 160 161 162 163 164 165 166 167 168
            fieldCodeEditorWrapper { canMoveDown: idx < (List.length fields - 1)
                                   , canMoveUp: idx > 0
                                   , field
                                   , key: (show $ fst masterKey) <> "-" <> (show idx)
                                   , onChange: onChange fS idx
                                   , onMoveDown: onMoveDown masterKey fS idx
                                   , onMoveUp: onMoveUp masterKey fS idx
                                   , onRemove: onRemove fS idx
                                   , onRename: onRename fS idx
                                   }) <$> fields
169 170

    onChange :: R.State FTFieldsWithIndex -> Index -> FieldType -> Effect Unit
171 172
    onChange (_ /\ setFields) idx typ = do
      setFields $ \fields ->
173 174
        fromMaybe fields $
          List.modifyAt idx (\(Tuple _ (Field f)) -> Tuple idx (Field $ f { typ = typ })) fields
175

176
    onMoveDown :: ReloadS -> R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
177 178
    onMoveDown (_ /\ setMasterKey) (fs /\ setFields) idx _ = do
      setMasterKey $ (+) 1
179
      setFields $ recomputeIndices <<< (GDA.swapList idx (idx + 1))
180

181
    onMoveUp :: ReloadS -> R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
182 183
    onMoveUp (_ /\ setMasterKey) (_ /\ setFields) idx _ = do
      setMasterKey $ (+) 1
184
      setFields $ recomputeIndices <<< (GDA.swapList idx (idx - 1))
185

186
    onRemove :: R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
187 188
    onRemove (_ /\ setFields) idx _ = do
      setFields $ \fields ->
189
        fromMaybe fields $ List.deleteAt idx fields
190

191
    onRename :: R.State FTFieldsWithIndex -> Index -> String -> Effect Unit
192 193
    onRename (_ /\ setFields) idx newName = do
      setFields $ \fields ->
194
        fromMaybe fields $ List.modifyAt idx (\(Tuple _ (Field f)) -> Tuple idx (Field $ f { name = newName })) fields
195

196 197
    recomputeIndices :: FTFieldsWithIndex -> FTFieldsWithIndex
    recomputeIndices = List.mapWithIndex $ \idx -> \(Tuple _ t) -> Tuple idx t
198

199
hash :: FTFieldWithIndex -> Hash
200
hash (Tuple idx f) = Crypto.hash $ "--idx--" <> (show idx) <> "--field--" <> (show f)
201

202 203
type FieldCodeEditorProps =
  (
204 205 206
    canMoveDown :: Boolean
  , canMoveUp :: Boolean
  , field :: FTField
207
  , key :: String
208
  , onChange :: FieldType -> Effect Unit
209 210
  , onMoveDown :: Unit -> Effect Unit
  , onMoveUp :: Unit -> Effect Unit
211
  , onRemove :: Unit -> Effect Unit
212
  , onRename :: String -> Effect Unit
213 214 215 216 217 218
  )

fieldCodeEditorWrapper :: Record FieldCodeEditorProps -> R.Element
fieldCodeEditorWrapper props = R.createElement fieldCodeEditorWrapperCpt props []

fieldCodeEditorWrapperCpt :: R.Component FieldCodeEditorProps
219
fieldCodeEditorWrapperCpt = R.hooksComponentWithModule thisModule "fieldCodeEditorWrapperCpt" cpt
220
  where
221 222
    cpt props@{canMoveDown, canMoveUp, field: Field {name, typ}, onMoveDown, onMoveUp, onRemove, onRename} _ = do
      pure $ H.div { className: "row panel panel-default" } [
223
        H.div { className: "panel-heading" } [
224 225 226 227 228 229
          H.div { className: "code-editor-heading" } [
              renameable {onRename, text: name}
            , H.div { className: "buttons-right" } [
                H.div { className: "btn btn-danger"
                      , on: { click: \_ -> onRemove unit }
                      } [
230
                  H.span { className: "fa fa-trash" } [  ]
231 232
                  ]
                ]
233 234
              , moveDownButton canMoveDown
              , moveUpButton canMoveUp
235 236
            ]
         ]
237 238 239 240
        , H.div { className: "panel-body" } [
           fieldCodeEditor props
           ]
        ]
241 242 243 244 245 246
      where
        moveDownButton false = H.div {} []
        moveDownButton true =
          H.div { className: "btn btn-default"
                , on: { click: \_ -> onMoveDown unit }
                } [
247
            H.span { className: "fa fa-arrow-down" } [  ]
248 249 250 251 252 253
            ]
        moveUpButton false = H.div {} []
        moveUpButton true =
          H.div { className: "btn btn-default"
                , on: { click: \_ -> onMoveUp unit }
                } [
254
            H.span { className: "fa fa-arrow-up" } [  ]
255
            ]
256

257 258 259 260 261 262 263 264 265 266
type RenameableProps =
  (
    onRename :: String -> Effect Unit
  , text :: String
  )

renameable :: Record RenameableProps -> R.Element
renameable props = R.createElement renameableCpt props []

renameableCpt :: R.Component RenameableProps
267
renameableCpt = R.hooksComponentWithModule thisModule "renameableCpt" cpt
268 269 270 271
  where
    cpt {onRename, text} _ = do
      isEditing <- R.useState' false
      state <- R.useState' text
272 273 274 275 276 277 278 279 280
      textRef <- R.useRef text

      -- handle props change of text
      R.useEffect1' text $ do
        if R.readRef textRef == text then
          pure unit
        else do
          R.setRef textRef text
          snd state $ const text
281 282

      pure $ H.div { className: "renameable" } [
283
        renameableText { isEditing, onRename, state }
284
      ]
285 286 287 288 289 290 291 292 293 294 295 296

type RenameableTextProps =
  (
    isEditing :: R.State Boolean
  , onRename :: String -> Effect Unit
  , state :: R.State String
  )

renameableText :: Record RenameableTextProps -> R.Element
renameableText props = R.createElement renameableTextCpt props []

renameableTextCpt :: R.Component RenameableTextProps
297
renameableTextCpt = R.hooksComponentWithModule thisModule "renameableTextCpt" cpt
298 299 300 301 302 303
  where
    cpt {isEditing: (false /\ setIsEditing), state: (text /\ _)} _ = do
      pure $ H.div {} [
        H.span { className: "text" } [ H.text text ]
        , H.span { className: "btn btn-default"
                 , on: { click: \_ -> setIsEditing $ const true } } [
304
           H.span { className: "fa fa-pencil" } []
305
           ]
306
        ]
307 308
    cpt {isEditing: (true /\ setIsEditing), onRename, state: (text /\ setText)} _ = do
      pure $ H.div {} [
309 310 311 312 313 314 315 316 317
          inputWithEnter {
               onEnter: submit
             , onValueChanged: setText <<< const
             , autoFocus: false
             , className: "form-control text"
             , defaultValue: text
             , placeholder: ""
             , type: "text"
             }
318
        , H.span { className: "btn btn-default"
319
                 , on: { click: submit } } [
320
           H.span { className: "fa fa-floppy-o" } []
321
           ]
322
        ]
323 324 325 326
      where
        submit _ = do
          setIsEditing $ const false
          onRename text
327

328 329 330 331
fieldCodeEditor :: Record FieldCodeEditorProps -> R.Element
fieldCodeEditor props = R.createElement fieldCodeEditorCpt props []

fieldCodeEditorCpt :: R.Component FieldCodeEditorProps
332
fieldCodeEditorCpt = R.hooksComponentWithModule thisModule "fieldCodeEditorCpt" cpt
333
  where
334 335
    cpt {field: Field {typ: typ@(Haskell {haskell})}, onChange} _ = do
      pure $ CE.codeEditor {code: haskell, defaultCodeType: CE.Haskell, onChange: changeCode onChange typ}
336 337 338 339

    cpt {field: Field {typ: typ@(Python {python})}, onChange} _ = do
      pure $ CE.codeEditor {code: python, defaultCodeType: CE.Python, onChange: changeCode onChange typ}

340 341
    cpt {field: Field {typ: typ@(JSON j)}, onChange} _ = do
      pure $ CE.codeEditor {code, defaultCodeType: CE.JSON, onChange: changeCode onChange typ}
342 343
      where
        code = R2.stringify (encodeJson j) 2
344

345 346 347
    cpt {field: Field {typ: typ@(Markdown {text})}, onChange} _ = do
      pure $ CE.codeEditor {code: text, defaultCodeType: CE.Markdown, onChange: changeCode onChange typ}

348
-- Performs the matrix of code type changes
349 350 351 352 353
-- (FieldType -> Effect Unit) is the callback function for fields array
-- FieldType is the current element that we will modify
-- CE.CodeType is the editor code type (might have been the cause of the trigger)
-- CE.Code is the editor code (might have been the cause of the trigger)
changeCode :: (FieldType -> Effect Unit) -> FieldType -> CE.CodeType -> CE.Code -> Effect Unit
354 355 356
changeCode onc (Haskell hs)        CE.Haskell  c = onc $ Haskell $ hs { haskell = c }
changeCode onc (Haskell hs)        CE.Python   c = onc $ Python   $ defaultPython'   { python  = c }
changeCode onc (Haskell {haskell}) CE.JSON     c = onc $ JSON     $ defaultJSON'     { desc = haskell }
357
changeCode onc (Haskell {haskell}) CE.Markdown c = onc $ Markdown $ defaultMarkdown' { text = haskell }
358 359 360 361 362 363 364 365 366 367 368

changeCode onc (Python hs)       CE.Python   c = onc $ Python  $ hs { python  = c }
changeCode onc (Python hs)       CE.Haskell  c = onc $ Haskell $ defaultHaskell' { haskell = c }
changeCode onc (Python {python}) CE.JSON     c = onc $ JSON     $ defaultJSON' { desc = python }
changeCode onc (Python {python}) CE.Markdown c = onc $ Markdown $ defaultMarkdown' { text = python }

changeCode onc (Markdown md) CE.Haskell  c = onc $ Haskell  $ defaultHaskell'  { haskell = c }
changeCode onc (Markdown md) CE.Python   c = onc $ Python   $ defaultPython'   { python  = c }
changeCode onc (Markdown md) CE.JSON     c = onc $ Markdown $ defaultMarkdown' { text    = c }
changeCode onc (Markdown md) CE.Markdown c = onc $ Markdown $ md               { text    = c }

369 370 371
changeCode onc (JSON j@{desc}) CE.Haskell c = onc $ Haskell $ defaultHaskell' { haskell = haskell }
  where
    haskell = R2.stringify (encodeJson j) 2
372 373 374
changeCode onc (JSON j@{desc}) CE.Python c = onc $ Python $ defaultPython' { python = toCode }
  where
    toCode = R2.stringify (encodeJson j) 2
375 376 377 378 379 380 381 382 383
changeCode onc (JSON j) CE.JSON c = do
  case jsonParser c of
    Left err -> log2 "[fieldCodeEditor'] cannot parse json" c
    Right j' -> case decodeJson j' of
      Left err -> log2 "[fieldCodeEditor'] cannot decode json" j'
      Right j'' -> onc $ JSON j''
changeCode onc (JSON j) CE.Markdown c = onc $ Markdown $ defaultMarkdown' { text = text }
  where
    text = R2.stringify (encodeJson j) 2
384 385 386



387

388 389
type LoadProps =
  ( nodeId  :: Int
390 391
  , session :: Session
  )
392

393
loadCorpus' :: Record LoadProps -> Aff (NodePoly Hyperdata)
394
loadCorpus' {nodeId, session} = get session $ NodeAPI Corpus (Just nodeId) ""
395

396 397
-- Just to make reloading effective
loadCorpusWithReload :: {reload :: Int  | LoadProps} -> Aff (NodePoly Hyperdata)
398
loadCorpusWithReload {nodeId, session} = loadCorpus' {nodeId, session}
399

400 401 402 403 404 405
type SaveProps = (
  hyperdata :: Hyperdata
  | LoadProps
  )

saveCorpus :: Record SaveProps -> Aff Unit
406 407 408
saveCorpus {hyperdata, nodeId, session} = do
  id_ <- (put session (NodeAPI Corpus (Just nodeId) "") hyperdata) :: Aff Int
  pure unit
409

410 411
loadCorpus :: Record LoadProps -> Aff CorpusData
loadCorpus {nodeId, session} = do
412 413
  -- fetch corpus via lists parentId
  (NodePoly {parentId: corpusId} :: NodePoly {}) <- get session nodePolyRoute
414 415 416
  corpusNode     <-  get session $ corpusNodeRoute     corpusId ""
  defaultListIds <- (get session $ defaultListIdsRoute corpusId)
                    :: forall a. DecodeJson a => AffTableResult (NodePoly a)
417
  case (A.head defaultListIds.docs :: Maybe (NodePoly HyperdataList)) of
418 419 420 421 422
    Just (NodePoly { id: defaultListId }) ->
      pure {corpusId, corpusNode, defaultListId}
    Nothing ->
      throwError $ error "Missing default list"
  where
423
    nodePolyRoute       = NodeAPI Corpus (Just nodeId) ""
424 425
    corpusNodeRoute     = NodeAPI Corpus <<< Just
    defaultListIdsRoute = Children NodeList 0 1 Nothing <<< Just
426 427 428


loadCorpusWithChild :: Record LoadProps -> Aff CorpusData
429
loadCorpusWithChild { nodeId: childId, session } = do
430 431 432 433 434 435 436
  -- fetch corpus via lists parentId
  (NodePoly {parentId: corpusId} :: NodePoly {}) <- get session $ listNodeRoute childId ""
  corpusNode     <-  get session $ corpusNodeRoute     corpusId ""
  defaultListIds <- (get session $ defaultListIdsRoute corpusId)
                    :: forall a. DecodeJson a => AffTableResult (NodePoly a)
  case (A.head defaultListIds.docs :: Maybe (NodePoly HyperdataList)) of
    Just (NodePoly { id: defaultListId }) ->
437
      pure { corpusId, corpusNode, defaultListId }
438 439 440 441 442 443 444
    Nothing ->
      throwError $ error "Missing default list"
  where
    corpusNodeRoute     = NodeAPI Corpus <<< Just
    listNodeRoute       = NodeAPI Node <<< Just
    defaultListIdsRoute = Children NodeList 0 1 Nothing <<< Just

445 446 447 448 449 450 451 452 453 454 455

type LoadWithReloadProps =
  (
    reload :: Int
  | LoadProps
  )


-- Just to make reloading effective
loadCorpusWithChildAndReload :: Record LoadWithReloadProps -> Aff CorpusData
loadCorpusWithChildAndReload {nodeId, reload, session} = loadCorpusWithChild {nodeId, session}