FolderView.purs 15.8 KB
Newer Older
1 2
module Gargantext.Components.FolderView where

3 4 5
import Gargantext.Prelude

import DOM.Simple (window)
6 7
import Data.Array as A
import Data.Maybe (Maybe(..), fromMaybe)
8
import Data.Traversable (traverse_)
9
import Effect.Aff (Aff)
10 11
import Effect.Class (liftEffect)
import Gargantext.AsyncTasks as GAT
arturo's avatar
arturo committed
12
import Gargantext.Components.App.Store (Boxes)
13 14 15
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.BaseModal (hideModal)
import Gargantext.Components.Bootstrap.Types (Elevation(..), Variant(..))
16 17 18 19 20 21 22 23
import Gargantext.Components.Forest.Tree.Node.Action.Add (AddNodeValue(..), addNode)
import Gargantext.Components.Forest.Tree.Node.Action.Contact as Contact
import Gargantext.Components.Forest.Tree.Node.Action.Delete (deleteNode, unpublishNode)
import Gargantext.Components.Forest.Tree.Node.Action.Link (linkNodeReq)
import Gargantext.Components.Forest.Tree.Node.Action.Merge (mergeNodeReq)
import Gargantext.Components.Forest.Tree.Node.Action.Move (moveNodeReq)
import Gargantext.Components.Forest.Tree.Node.Action.Rename (RenameValue(..), rename)
import Gargantext.Components.Forest.Tree.Node.Action.Share as Share
24
import Gargantext.Components.Forest.Tree.Node.Action.Types (Action(..))
25 26 27 28
import Gargantext.Components.Forest.Tree.Node.Action.Update (updateRequest)
import Gargantext.Components.Forest.Tree.Node.Action.Upload (uploadArbitraryFile, uploadFile)
import Gargantext.Components.Forest.Tree.Node.Box (nodePopupView)
import Gargantext.Components.Forest.Tree.Node.Tools.SubTree.Types (SubTreeOut(..))
29 30
import Gargantext.Components.GraphQL.Endpoints (getNode, getTreeFirstLevel)
import Gargantext.Components.GraphQL.Node (Node)
31
import Gargantext.Components.GraphQL.Tree (TreeFirstLevel, TreeNode)
32
import Gargantext.Config.REST (AffRESTError, logRESTError)
33
import Gargantext.Config.Utils (handleRESTError)
34
import Gargantext.Hooks.LinkHandler (useLinkHandler)
35
import Gargantext.Hooks.Loader (useLoader)
36
import Gargantext.Routes (AppRoute(Home), nodeTypeAppRoute)
37
import Gargantext.Sessions (Session(..), sessionId)
38
import Gargantext.Types (NodeType(..), SessionId)
39
import Gargantext.Types as GT
40
import Gargantext.Utils.Reactix as R2
41
import Gargantext.Utils.Toestand as T2
42 43 44
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
45 46 47 48 49

here :: R2.Here
here = R2.here "Gargantext.Components.FolderView"

type Props =
50
  ( boxes      :: Boxes
51 52
  , nodeId     :: Int
  , session    :: Session
53 54 55 56
  )

data FolderStyle = FolderUp | FolderChild

57
folderView :: R2.Leaf Props
58
folderView = R2.leafComponent folderViewCpt
59 60
folderViewCpt :: R.Component Props
folderViewCpt = here.component "folderViewCpt" cpt where
61
  cpt { boxes, nodeId, session } _ = do
62 63
    reload <- T.useBox T2.newReload
    reload' <- T.useLive T.unequal reload
64 65 66
    useLoader { errorHandler
              , loader: loadFolders
              , path: { nodeId, session, reload: reload'}
67
              , render: \folders -> folderViewMain { boxes
68 69 70
                                                   , folders
                                                   , nodeId
                                                   , reload
71
                                                   , session
72
                                                   } [] }
73
    where
74
      errorHandler = logRESTError here "[folderView]"
75

76
type FolderViewProps =
77
  ( boxes         :: Boxes
78
  , folders       :: TreeFirstLevel
79 80 81
  , nodeId        :: Int
  , reload        :: T.Box T2.Reload
  , session       :: Session
82 83
  )

84 85
folderViewMain :: R2.Component FolderViewProps
folderViewMain = R.createElement folderViewMainCpt
86 87
folderViewMainCpt :: R.Component FolderViewProps
folderViewMainCpt = here.component "folderViewMainCpt" cpt where
88
  cpt props@{ folders: {parent: parentNode, children, root} } _ = do
89
    let folders' = A.sortBy sortFolders children
90 91
    let parent = makeParentFolder root parentNode props
    let childrenEl = makeFolderElements folders' props
92

93
    pure $ H.div {className: "fv folders"} $ parent <> childrenEl
94

95
  makeFolderElements :: Array TreeNode -> Record FolderViewProps -> Array R.Element
96
  makeFolderElements folders' props = makeFolderElementsMap <$> folders' where
97 98
    makeFolderElementsMap :: TreeNode -> R.Element
    makeFolderElementsMap node = folder { boxes: props.boxes
99 100 101 102 103 104 105 106
                                        , nodeId: node.id
                                        , linkId: node.id
                                        , nodeType: node.node_type
                                        , linkNodeType: node.node_type
                                        , parentId: props.nodeId
                                        , reload: props.reload
                                        , session: props.session
                                        , style: FolderChild
107 108
                                        , text: node.name
                                        }
109 110 111

  makeParentFolder :: TreeNode -> Maybe TreeNode -> Record FolderViewProps -> Array R.Element
  makeParentFolder root (Just parent) props =
112 113 114 115 116 117 118 119 120 121 122 123 124 125
    [
      folder
      { boxes: props.boxes
      , nodeId: root.id
      , linkId: parent.id
      , linkNodeType: parent.node_type
      , nodeType: root.node_type
      , parentId: parent.id
      , reload: props.reload
      , session: props.session
      , style: FolderUp
      , text: root.name
      }
    ]
126
  makeParentFolder _ Nothing _ = []
127

128 129
  sortFolders :: TreeNode-> TreeNode -> Ordering
  sortFolders a b = compare a.id b.id
130

131
type FolderProps =
132 133 134 135 136 137 138 139
  ( style         :: FolderStyle
  , text          :: String
  , nodeType      :: GT.NodeType
  , nodeId        :: Int
  , linkNodeType  :: GT.NodeType
  , linkId        :: Int
  , session       :: Session
  , boxes         :: Boxes
140 141
  , parentId      :: Int
  , reload        :: T.Box T2.Reload
142 143
  )

144 145
folder :: R2.Leaf FolderProps
folder = R2.leaf folderCpt
146 147
folderCpt :: R.Component FolderProps
folderCpt = here.component "folderCpt" cpt where
148 149 150
  cpt props@{ boxes
            , nodeId
            , nodeType
151 152
            , linkId
            , linkNodeType
153 154 155 156
            , parentId
            , reload
            , session
            , style
157 158 159 160 161 162
            , text
            } _ = do
    -- States
    isBoxVisible <- T.useBox false

    -- Hooks
163
    { goToRoute } <- useLinkHandler
164

165 166 167 168
    -- Computed
    let sid = sessionId session
    let rootId = treeId session
    let dispatch a = performAction a { boxes, nodeId, parentId, reload, session, isBoxVisible }
169

170
    -- Render
171
    pure $
172 173 174 175

      H.div
      {}
      [
arturo's avatar
arturo committed
176 177 178 179 180 181 182
        H.div
        -- KISS CSS placement (BEM would be better)
        { style:
            { float: "right"
            , position: "relative"
            , right: "-14px"
            }
183
        }
arturo's avatar
arturo committed
184 185 186 187 188 189 190 191 192 193 194 195
        [
          B.iconButton
          { name: "cog"
          , callback: \_ -> T.write_ true isBoxVisible
          , title:
                "Each node of the Tree can perform some actions.\n"
              <> "Click here to execute one of them."
          , variant: Secondary
          , elevation: Level0
          , overlay: false
          }
        ]
196 197 198 199 200 201 202 203 204 205 206 207 208
      ,
        H.button
        { className: "btn btn-primary fv btn"
        , on: { click: \_ -> goToRoute $ route linkId rootId linkNodeType sid }
        }
        [
          H.i
          { className: icon style nodeType }
          []
        ,
          H.br {}
        ,
          H.text text
209
        ]
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
      ,
        -- // Modals //
        B.baseModal
        { isVisibleBox: isBoxVisible
        , noBody: true
        , noHeader: true
        , modalClassName: "forest-tree-node-modal"
        }
        [
          nodePopupView
          { boxes: props.boxes
          , dispatch: dispatch
          , id: props.nodeId
          , nodeType: props.nodeType
          , name: props.text
          , session: props.session
          , closeCallback: \_ -> T.write_ false isBoxVisible
          }
228
        ]
229
      ]
230

231 232 233 234 235 236 237 238 239 240 241
  route :: Int -> Int -> NodeType -> SessionId -> AppRoute
  route lId rootId nType sid
    | rootId == lId    = Home
    | otherwise        = getFolderPath nType sid lId

  icon :: FolderStyle -> GT.NodeType -> String
  icon FolderUp _ = "fa fa-folder-open"
  icon _ nodeType = GT.fldr nodeType false

  getFolderPath :: GT.NodeType -> GT.SessionId -> Int -> AppRoute
  getFolderPath nodeType sid nodeId = fromMaybe Home $ nodeTypeAppRoute nodeType sid nodeId
242

243 244 245
backButton :: R2.Component ()
backButton = R.createElement backButtonCpt
backButtonCpt :: R.Component ()
246
backButtonCpt = here.component "backButton" cpt where
247 248 249
  cpt _ _ = do
    { goToPreviousPage } <- useLinkHandler

arturo's avatar
arturo committed
250
    pure $
251 252
      H.button {
        className: "btn btn-primary"
253
      , on: { click: \_ -> goToPreviousPage unit }
254 255 256
      } [
        H.i { className: "fa fa-arrow-left", title: "Previous view"} []
      ]
257

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
backButtonSmart :: R2.Component (nodeId :: Int, session :: Session)
backButtonSmart = R.createElement backButtonSmartCpt

backButtonSmartCpt :: R.Component (nodeId :: Int, session :: Session)
backButtonSmartCpt = here.component "backButtonSmart" cpt where
  cpt {nodeId, session} _ = do
    reload <- T.useBox T2.newReload
    reload' <- T.useLive T.unequal reload
    useLoader { errorHandler
              , loader: loadNode
              , path: { nodeId, session, reload: reload' }
              , render: \node -> backButtonSmartMain { node, session } []
    }
    where
      errorHandler = logRESTError here "[folderView]"

backButtonSmartMain :: R2.Component (node :: Node, session :: Session)
backButtonSmartMain = R.createElement backButtonSmartMainCpt

backButtonSmartMainCpt :: R.Component (node :: Node, session :: Session)
backButtonSmartMainCpt = here.component "backButtonSmartMain" cpt where
  cpt { node, session } _ = do
    handlers <- useLinkHandler
    let rootId = treeId session

283
    pure $
284 285 286 287 288 289 290 291 292 293 294
      H.button {
        className: "btn btn-primary"
      , on: { click: action rootId node.parent_id handlers }
      } [
        H.i { className: "fa fa-arrow-left", title: "Previous view"} []
      ]
    where
      action rootId pId handlers
        | rootId == pId = handlers.goToRoute Home
        | otherwise = handlers.goToPreviousPage unit

295 296 297
treeId :: Session -> Int
treeId (Session {treeId: tId}) = tId

298 299 300 301 302 303 304
type LoadProps =
  (
    session :: Session,
    nodeId :: Int,
    reload :: T2.Reload
  )

305 306
loadFolders :: Record LoadProps -> AffRESTError TreeFirstLevel
loadFolders {nodeId, session} = getTreeFirstLevel session nodeId
307 308 309

loadNode :: Record LoadProps -> AffRESTError Node
loadNode {nodeId, session} = getNode session nodeId
310 311

type PerformActionProps =
312
  ( boxes         :: Boxes
313 314 315 316
  , nodeId        :: Int
  , parentId      :: Int
  , reload        :: T.Box T2.Reload
  , session       :: Session
317
  , isBoxVisible  :: T.Box Boolean
318 319 320 321 322 323 324 325 326 327 328 329
  )

performAction :: Action -> Record PerformActionProps -> Aff Unit
performAction = performAction' where
  performAction' (DeleteNode nt) p = deleteNode' nt p
  performAction' (DoSearch task) p = doSearch task p
  performAction' (UpdateNode params) p = updateNode params p
  performAction' (RenameNode name) p = renameNode name p
  performAction' (ShareTeam username) p = shareTeam username p
  performAction' (SharePublic { params }) p = sharePublic params p
  performAction' (AddContact params) p = addContact params p
  performAction' (AddNode name nodeType) p = addNode' name nodeType p
330 331 332 333
  performAction' (UploadFile nodeType fileType fileFormat mName contents selection) p =
    uploadFile' nodeType fileType fileFormat mName contents p selection
  performAction' (UploadArbitraryFile fileFormat mName blob selection) p =
    uploadArbitraryFile' fileFormat mName blob p selection
334
  performAction' DownloadNode _ = liftEffect $ here.log "[performAction] DownloadNode"
335 336 337
  performAction' (MoveNode {params}) p = moveNode params p
  performAction' (MergeNode {params}) p = mergeNode params p
  performAction' (LinkNode { nodeType, params }) p = linkNode nodeType params p
338
  performAction' NoAction _ = liftEffect $ here.log "[performAction] NoAction"
339
  performAction' CloseBox p = closeBox p
340
  performAction' _ _ = liftEffect $ here.log "[performAction] unsupported action"
341

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  closeBox { isBoxVisible, nodeId } =
    liftEffect $ do
      T.write_ false isBoxVisible
      -- @XXX ReactJS unreactive ref
      --
      -- /!\ extra care here:
      --
      --  - due to a ReactJS yet another flaw, we have to make an extra closing
      --    modal method call here (bc. even if the `T.Box` change its value
      --    no reactivity will be perfomed, for some unknown reason, and
      --    the modal would so partially close)
      --
      --  - also make an extra assumption here, as the `querySelector` used for
      --    modal close call should be the same as the selector qualifying the
      --    created <base-modal>)
      hideModal window $ "#" <> (show nodeId)
358

359
  refreshFolders p@{ boxes: { reloadForest }, reload } = do
360 361
    liftEffect $ T2.reload reload
    liftEffect $ T2.reload reloadForest
362
    closeBox p
363

364
  deleteNode' nt p@{ nodeId: id, parentId: parent_id, session } = do
365
    case nt of
366
      NodePublic FolderPublic  -> void $ deleteNode session id
367
      NodePublic _             -> void $ unpublishNode session (Just parent_id) id
368
      _                        -> void $ deleteNode session id
369 370
    refreshFolders p

371
  doSearch task { boxes: { tasks }, nodeId: id } = liftEffect $ do
372
    GAT.insert id task tasks
373
    here.log2 "[performAction] DoSearch task:" task
374

375 376
  updateNode params { boxes: { errors, tasks }, nodeId: id, session } = do
    eTask <- updateRequest params session id
377
    handleRESTError errors eTask $ \task -> liftEffect $ do
378
      GAT.insert id task tasks
379
      here.log2 "[performAction] UpdateNode task:" task
380

381 382
  shareTeam username { boxes: { errors }, nodeId: id, session } = do
    eTask <- Share.shareReq session id $ Share.ShareTeamParams { username }
383
    handleRESTError errors eTask $ \_task -> pure unit
384

385
  sharePublic params p@{ boxes: { errors }, session } = traverse_ f params where
386
    f (SubTreeOut { in: inId, out }) = do
387
      eTask <- Share.shareReq session inId $ Share.SharePublicParams { node_id: out }
388
      handleRESTError errors eTask $ \_task -> pure unit
389 390
      refreshFolders p

391 392
  addContact params { nodeId: id, session } =
    void $ Contact.contactReq session id params
393

394 395
  uploadFile' nodeType fileType fileFormat mName contents { boxes: { errors, tasks }, nodeId: id, session } selection = do
    eTask <- uploadFile { contents, fileType, fileFormat, id, nodeType, mName, selection, session }
396
    handleRESTError errors eTask $ \task -> liftEffect $ do
397
      GAT.insert id task tasks
398
      here.log2 "[performAction] UploadFile, uploaded, task:" task
399

400 401
  uploadArbitraryFile' fileFormat mName blob { boxes: { errors, tasks }, nodeId: id, session } selection = do
    eTask <- uploadArbitraryFile session id { blob, fileFormat, mName } selection
402
    handleRESTError errors eTask $ \task -> liftEffect $ do
403
      GAT.insert id task tasks
404
      here.log2 "[performAction] UploadArbitraryFile, uploaded, task:" task
405

406
  moveNode params p@{ boxes: { errors }, session } = traverse_ f params where
407
    f (SubTreeOut { in: in', out }) = do
408
      eTask <- moveNodeReq session in' out
409
      handleRESTError errors eTask $ \_task -> pure unit
410 411
      refreshFolders p

412
  mergeNode params p@{ boxes: { errors }, session } = traverse_ f params where
413
    f (SubTreeOut { in: in', out }) = do
414
      eTask <- mergeNodeReq session in' out
415
      handleRESTError errors eTask $ \_task -> pure unit
416 417
      refreshFolders p

418
  linkNode nodeType params p@{ boxes: { errors }, session } = traverse_ f params where
419
    f (SubTreeOut { in: in', out }) = do
420
      eTask <- linkNodeReq session nodeType in' out
421
      handleRESTError errors eTask $ \_task -> pure unit
422 423
      refreshFolders p

424 425
  renameNode name p@{ boxes: { errors }, nodeId: id, session } = do
    eTask <- rename session id $ RenameValue { text: name }
426
    handleRESTError errors eTask $ \_task -> pure unit
427 428
    refreshFolders p

429 430
  addNode' name nodeType p@{ boxes: { errors }, nodeId: id, session } = do
    eTask <- addNode session id $ AddNodeValue {name, nodeType}
431
    handleRESTError errors eTask $ \_task -> pure unit
432
    refreshFolders p