Corpus.purs 15.9 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.Node (NodePoly(..), HyperdataList)
23
import Gargantext.Components.Nodes.Corpus.Types (CorpusData, FTField, Field(..), FieldType(..), Hash, Hyperdata(..), defaultField, defaultHaskell', defaultJSON', defaultMarkdown')
24
import Gargantext.Data.Array as GDA
25
import Gargantext.Hooks.Loader (useLoader)
26
import Gargantext.Routes (SessionRoute(NodeAPI, Children))
27
import Gargantext.Sessions (Session, get, put)
28
import Gargantext.Types (NodeType(..), AffTableResult)
29
import Gargantext.Utils.Crypto as GUC
30
import Gargantext.Utils.Reactix as R2
Nicolas Pouillard's avatar
Nicolas Pouillard committed
31

32 33 34 35
type Props = (
    nodeId  :: Int
  , session :: Session
  )
36

37 38
type Reload = R.State Int

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

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

48
corpusLayoutCpt :: R.Component KeyProps
49 50
corpusLayoutCpt = R.hooksComponent "G.C.N.C.corpusLayout" cpt
  where
51 52 53 54 55
    cpt {nodeId, session} _ = do
      reload <- R.useState' 0

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

type ViewProps = (
58
    corpus  :: NodePoly Hyperdata
59
  , reload  :: Reload
60 61 62
  | Props
  )

63 64
-- 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)
65 66
type Index = Int
type FTFieldWithIndex = Tuple Index FTField
67
type FTFieldsWithIndex = List.List FTFieldWithIndex
68

69 70 71 72 73
corpusLayoutView :: Record ViewProps -> R.Element
corpusLayoutView props = R.createElement corpusLayoutViewCpt props []

corpusLayoutViewCpt :: R.Component ViewProps
corpusLayoutViewCpt = R.hooksComponent "G.C.N.C.corpusLayoutView" cpt
74
  where
75
    cpt {corpus: (NodePoly {hyperdata: Hyperdata {fields}}), nodeId, reload, session} _ = do
76
      let fieldsWithIndex = List.mapWithIndex (\idx -> \t -> Tuple idx t) fields
77
      fieldsS <- R.useState' fieldsWithIndex
78 79 80 81 82 83 84 85 86
      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
87 88

      pure $ H.div {} [
89 90 91 92
        H.div { className: "row" } [
           H.div { className: "btn btn-default " <> (saveEnabled fieldsWithIndex fieldsS)
                 , on: { click: onClickSave {fields: fieldsS, nodeId, reload, session} }
                 } [
93
              H.span { className: "fa fa-floppy-o" } [  ]
94 95
              ]
           ]
96 97 98
        , H.div {} [ fieldsCodeEditor { fields: fieldsS
                                      , nodeId
                                      , session } ]
99
        , H.div { className: "row" } [
100 101 102
           H.div { className: "btn btn-default"
                 , on: { click: onClickAdd fieldsS }
                 } [
103
              H.span { className: "fa fa-plus" } [  ]
104 105
              ]
           ]
106
        ]
107

108
    saveEnabled :: FTFieldsWithIndex -> R.State FTFieldsWithIndex -> String
109 110
    saveEnabled fs (fsS /\ _) = if fs == fsS then "disabled" else "enabled"

111
    onClickSave :: forall e. { fields :: R.State FTFieldsWithIndex
112
                             , nodeId :: Int
113
                             , reload :: R.State Int
114
                             , session :: Session } -> e -> Effect Unit
115
    onClickSave {fields: (fieldsS /\ _), nodeId, reload: (_ /\ setReload), session} _ = do
116 117 118 119 120
      log2 "[corpusLayoutViewCpt] onClickSave fieldsS" fieldsS
      launchAff_ do
        saveCorpus $ { hyperdata: Hyperdata {fields: (\(Tuple _ f) -> f) <$> fieldsS}
                     , nodeId
                     , session }
121
        liftEffect $ setReload $ (+) 1
122

123
    onClickAdd :: forall e. R.State FTFieldsWithIndex -> e -> Effect Unit
124
    onClickAdd (_ /\ setFieldsS) _ = do
125
      setFieldsS $ \fieldsS -> List.snoc fieldsS $ Tuple (List.length fieldsS) defaultField
126 127 128

type FieldsCodeEditorProps =
  (
129
    fields :: R.State FTFieldsWithIndex
130 131 132 133 134 135 136 137 138
    | LoadProps
  )

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

fieldsCodeEditorCpt :: R.Component FieldsCodeEditorProps
fieldsCodeEditorCpt = R.hooksComponent "G.C.N.C.fieldsCodeEditorCpt" cpt
  where
139
    cpt {nodeId, fields: fS@(fields /\ _), session} _ = do
140 141 142
      masterKey <- R.useState' 0

      pure $ H.div {} $ List.toUnfoldable (editors masterKey)
143
      where
144
        editors masterKey =
145
          (\(Tuple idx field) ->
146 147 148 149 150 151 152 153 154 155
            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
156 157

    onChange :: R.State FTFieldsWithIndex -> Index -> FieldType -> Effect Unit
158 159
    onChange (_ /\ setFields) idx typ = do
      setFields $ \fields ->
160 161
        fromMaybe fields $
          List.modifyAt idx (\(Tuple _ (Field f)) -> Tuple idx (Field $ f { typ = typ })) fields
162

163 164 165
    onMoveDown :: R.State Int -> R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
    onMoveDown (_ /\ setMasterKey) (fs /\ setFields) idx _ = do
      setMasterKey $ (+) 1
166
      setFields $ recomputeIndices <<< (GDA.swapList idx (idx + 1))
167

168 169 170
    onMoveUp :: R.State Int -> R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
    onMoveUp (_ /\ setMasterKey) (_ /\ setFields) idx _ = do
      setMasterKey $ (+) 1
171
      setFields $ recomputeIndices <<< (GDA.swapList idx (idx - 1))
172

173
    onRemove :: R.State FTFieldsWithIndex -> Index -> Unit -> Effect Unit
174 175
    onRemove (_ /\ setFields) idx _ = do
      setFields $ \fields ->
176
        fromMaybe fields $ List.deleteAt idx fields
177

178
    onRename :: R.State FTFieldsWithIndex -> Index -> String -> Effect Unit
179 180
    onRename (_ /\ setFields) idx newName = do
      setFields $ \fields ->
181
        fromMaybe fields $ List.modifyAt idx (\(Tuple _ (Field f)) -> Tuple idx (Field $ f { name = newName })) fields
182

183 184
    recomputeIndices :: FTFieldsWithIndex -> FTFieldsWithIndex
    recomputeIndices = List.mapWithIndex $ \idx -> \(Tuple _ t) -> Tuple idx t
185

186 187 188
hash :: FTFieldWithIndex -> Hash
hash (Tuple idx f) = GUC.md5 $ "--idx--" <> (show idx) <> "--field--" <> (show f)

189 190
type FieldCodeEditorProps =
  (
191 192 193
    canMoveDown :: Boolean
  , canMoveUp :: Boolean
  , field :: FTField
194
  , key :: String
195
  , onChange :: FieldType -> Effect Unit
196 197
  , onMoveDown :: Unit -> Effect Unit
  , onMoveUp :: Unit -> Effect Unit
198
  , onRemove :: Unit -> Effect Unit
199
  , onRename :: String -> Effect Unit
200 201 202 203 204 205 206
  )

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

fieldCodeEditorWrapperCpt :: R.Component FieldCodeEditorProps
fieldCodeEditorWrapperCpt = R.hooksComponent "G.C.N.C.fieldCodeEditorWrapperCpt" cpt
207
  where
208 209
    cpt props@{canMoveDown, canMoveUp, field: Field {name, typ}, onMoveDown, onMoveUp, onRemove, onRename} _ = do
      pure $ H.div { className: "row panel panel-default" } [
210
        H.div { className: "panel-heading" } [
211 212 213 214 215 216
          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 }
                      } [
217
                  H.span { className: "fa fa-trash" } [  ]
218 219
                  ]
                ]
220 221
              , moveDownButton canMoveDown
              , moveUpButton canMoveUp
222 223
            ]
         ]
224 225 226 227
        , H.div { className: "panel-body" } [
           fieldCodeEditor props
           ]
        ]
228 229 230 231 232 233
      where
        moveDownButton false = H.div {} []
        moveDownButton true =
          H.div { className: "btn btn-default"
                , on: { click: \_ -> onMoveDown unit }
                } [
234
            H.span { className: "fa fa-arrow-down" } [  ]
235 236 237 238 239 240
            ]
        moveUpButton false = H.div {} []
        moveUpButton true =
          H.div { className: "btn btn-default"
                , on: { click: \_ -> onMoveUp unit }
                } [
241
            H.span { className: "fa fa-arrow-up" } [  ]
242
            ]
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
type RenameableProps =
  (
    onRename :: String -> Effect Unit
  , text :: String
  )

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

renameableCpt :: R.Component RenameableProps
renameableCpt = R.hooksComponent "G.C.N.C.renameableCpt" cpt
  where
    cpt {onRename, text} _ = do
      isEditing <- R.useState' false
      state <- R.useState' text
259 260 261 262 263 264 265 266 267
      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
268 269

      pure $ H.div { className: "renameable" } [
270
        renameableText {isEditing, onRename, state}
271
      ]
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290

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
renameableTextCpt = R.hooksComponent "G.C.N.C.renameableTextCpt" cpt
  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 } } [
291
           H.span { className: "fa fa-pencil" } []
292
           ]
293
        ]
294 295 296 297 298 299 300 301 302 303
    cpt {isEditing: (true /\ setIsEditing), onRename, state: (text /\ setText)} _ = do
      pure $ H.div {} [
        H.input { defaultValue: text
                , className: "form-control text"
                , on: { change: \e -> setText $ const $ R2.unsafeEventValue e } }
        , H.span { className: "btn btn-default"
                 , on: { click: \_ -> do
                            setIsEditing $ const false
                            onRename text
                       } } [
304
           H.span { className: "fa fa-floppy-o" } []
305
           ]
306 307
        ]

308 309 310 311 312
fieldCodeEditor :: Record FieldCodeEditorProps -> R.Element
fieldCodeEditor props = R.createElement fieldCodeEditorCpt props []

fieldCodeEditorCpt :: R.Component FieldCodeEditorProps
fieldCodeEditorCpt = R.hooksComponent "G.C.N.C.fieldCodeEditorCpt" cpt
313
  where
314 315 316 317
    cpt {field: Field {typ: typ@(Haskell {haskell})}, onChange} _ = do
      pure $ CE.codeEditor {code: haskell, defaultCodeType: CE.Haskell, onChange: changeCode onChange typ}
    cpt {field: Field {typ: typ@(JSON j)}, onChange} _ = do
      pure $ CE.codeEditor {code, defaultCodeType: CE.JSON, onChange: changeCode onChange typ}
318 319
      where
        code = R2.stringify (encodeJson j) 2
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    cpt {field: Field {typ: typ@(Markdown {text})}, onChange} _ = do
      pure $ CE.codeEditor {code: text, defaultCodeType: CE.Markdown, onChange: changeCode onChange typ}

-- Perofrms the matrix of code type changes
-- (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
changeCode onc (Haskell hs) CE.Haskell c = onc $ Haskell $ hs { haskell = c }
changeCode onc (Haskell {haskell}) CE.JSON c = onc $ JSON $ defaultJSON' { desc = haskell }
changeCode onc (Haskell {haskell}) CE.Markdown c = onc $ Markdown $ defaultMarkdown' { text = haskell }
changeCode onc (JSON j@{desc}) CE.Haskell c = onc $ Haskell $ defaultHaskell' { haskell = haskell }
  where
    haskell = R2.stringify (encodeJson j) 2
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
changeCode onc (Markdown md) CE.Haskell c = onc $ Haskell $ defaultHaskell' { haskell = 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 }
347

348 349 350 351
type LoadProps = (
    nodeId  :: Int
  , session :: Session
  )
352

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

356 357
-- Just to make reloading effective
loadCorpusWithReload :: {reload :: Int  | LoadProps} -> Aff (NodePoly Hyperdata)
358
loadCorpusWithReload {nodeId, session} = loadCorpus' {nodeId, session}
359

360 361 362 363 364 365
type SaveProps = (
  hyperdata :: Hyperdata
  | LoadProps
  )

saveCorpus :: Record SaveProps -> Aff Unit
366 367 368
saveCorpus {hyperdata, nodeId, session} = do
  id_ <- (put session (NodeAPI Corpus (Just nodeId) "") hyperdata) :: Aff Int
  pure unit
369

370 371
loadCorpus :: Record LoadProps -> Aff CorpusData
loadCorpus {nodeId, session} = do
372 373
  -- fetch corpus via lists parentId
  (NodePoly {parentId: corpusId} :: NodePoly {}) <- get session nodePolyRoute
374 375 376
  corpusNode     <-  get session $ corpusNodeRoute     corpusId ""
  defaultListIds <- (get session $ defaultListIdsRoute corpusId)
                    :: forall a. DecodeJson a => AffTableResult (NodePoly a)
377
  case (A.head defaultListIds.docs :: Maybe (NodePoly HyperdataList)) of
378 379 380 381 382
    Just (NodePoly { id: defaultListId }) ->
      pure {corpusId, corpusNode, defaultListId}
    Nothing ->
      throwError $ error "Missing default list"
  where
383
    nodePolyRoute       = NodeAPI Corpus (Just nodeId) ""
384 385
    corpusNodeRoute     = NodeAPI Corpus <<< Just
    defaultListIdsRoute = Children NodeList 0 1 Nothing <<< Just
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404


loadCorpusWithChild :: Record LoadProps -> Aff CorpusData
loadCorpusWithChild {nodeId:childId, session} = do
  -- 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 }) ->
      pure {corpusId, corpusNode, defaultListId}
    Nothing ->
      throwError $ error "Missing default list"
  where
    corpusNodeRoute     = NodeAPI Corpus <<< Just
    listNodeRoute       = NodeAPI Node <<< Just
    defaultListIdsRoute = Children NodeList 0 1 Nothing <<< Just

405 406 407 408 409 410 411 412 413 414 415

type LoadWithReloadProps =
  (
    reload :: Int
  | LoadProps
  )


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