Commit 7cad1696 authored by Przemyslaw Kaminski's avatar Przemyslaw Kaminski

Merge branch 'dev' into dev-arxiv

parents 2a29dcc9 9e3893bb
Pipeline #2699 failed with stage
in 0 seconds
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "Gargantext", "name": "Gargantext",
"version": "0.0.5.8.4", "version": "0.0.5.8.5",
"scripts": { "scripts": {
"generate-purs-packages-nix": "./nix/generate-purs-packages.nix", "generate-purs-packages-nix": "./nix/generate-purs-packages.nix",
"generate-psc-packages-nix": "./nix/generate-packages-json.bash", "generate-psc-packages-nix": "./nix/generate-packages-json.bash",
......
...@@ -16,11 +16,13 @@ type Props = ...@@ -16,11 +16,13 @@ type Props =
type Options = type Options =
( className :: String ( className :: String
, contentClassName :: String
) )
options :: Record Options options :: Record Options
options = options =
{ className: "" { className : ""
, contentClassName : ""
} }
-- | Component simulating a native <fieldset> -- | Component simulating a native <fieldset>
...@@ -36,12 +38,21 @@ component = R.hooksComponent componentName cpt where ...@@ -36,12 +38,21 @@ component = R.hooksComponent componentName cpt where
cpt props@{ titleSlot cpt props@{ titleSlot
} children = do } children = do
-- Computed -- Computed
className <- pure $ intercalate " " let
className = intercalate " "
-- provided custom className -- provided custom className
[ props.className [ props.className
-- BEM classNames -- BEM classNames
, componentName , componentName
] ]
contentClassName = intercalate " "
-- provided custom className
[ props.contentClassName
-- BEM classNames
, componentName <> "__content"
]
-- Render -- Render
pure $ pure $
...@@ -53,6 +64,6 @@ component = R.hooksComponent componentName cpt where ...@@ -53,6 +64,6 @@ component = R.hooksComponent componentName cpt where
[ titleSlot ] [ titleSlot ]
, ,
H.div H.div
{ className: componentName <> "__content" } { className: contentClassName}
children children
] ]
...@@ -14,7 +14,9 @@ import Gargantext.Components.Bootstrap.Icon(icon) as Exports ...@@ -14,7 +14,9 @@ import Gargantext.Components.Bootstrap.Icon(icon) as Exports
import Gargantext.Components.Bootstrap.IconButton(iconButton) as Exports import Gargantext.Components.Bootstrap.IconButton(iconButton) as Exports
import Gargantext.Components.Bootstrap.ProgressBar(progressBar) as Exports import Gargantext.Components.Bootstrap.ProgressBar(progressBar) as Exports
import Gargantext.Components.Bootstrap.Spinner(spinner) as Exports import Gargantext.Components.Bootstrap.Spinner(spinner) as Exports
import Gargantext.Components.Bootstrap.Tooltip(tooltip, tooltipBind, tooltipContainer) as Exports import Gargantext.Components.Bootstrap.Tabs(tabs) as Exports
import Gargantext.Components.Bootstrap.Tooltip(tooltip, TooltipBindingProps, tooltipBind, tooltipBind', tooltipContainer) as Exports
import Gargantext.Components.Bootstrap.Wad(wad, wad', wad_) as Exports
import Gargantext.Components.Bootstrap.Shortcut( import Gargantext.Components.Bootstrap.Shortcut(
div', div_ div', div_
......
module Gargantext.Components.Bootstrap.Tooltip module Gargantext.Components.Bootstrap.Tooltip
( tooltip ( tooltip
, tooltipBind , TooltipBindingProps, tooltipBind, tooltipBind'
, tooltipContainer , tooltipContainer
) where ) where
...@@ -90,6 +90,13 @@ tooltipBind = ...@@ -90,6 +90,13 @@ tooltipBind =
, "data-tip": true , "data-tip": true
} }
-- | Derived empty state
tooltipBind' :: Record TooltipBindingProps
tooltipBind' =
{ "data-for": ""
, "data-tip": false
}
------------------------------------------------------------- -------------------------------------------------------------
type ContainerProps = type ContainerProps =
......
module Gargantext.Components.Bootstrap.Wad
( wad
, wad'
, wad_
) where
import Gargantext.Prelude
import Data.Foldable (intercalate)
import Reactix as R
import Reactix.DOM.HTML as H
componentName :: String
componentName = "b-wad"
-- | Structural Component for a simple Element only serving the purpose to add
-- | some classes in it
-- |
-- | Hence the name: Wad (noun): a small mass, lump, or ball of anything ;
-- | a roll of something
wad :: Array String -> Array R.Element -> R.Element
wad classes children = R.createDOMElement "div" cls children
where
cls = { className: intercalate " " $
[ componentName
] <> classes
}
-- | Shorthand for using <wad> Component without writing its text node
wad' :: Array String -> String -> R.Element
wad' classes text = R.createDOMElement "div" cls chd
where
cls = { className: intercalate " " $
[ componentName
] <> classes
}
chd = [ H.text text ]
-- | Shorthand for using <wad> Component without any child
wad_ :: Array String -> R.Element
wad_ classes = R.createDOMElement "div" cls []
where
cls = { className: intercalate " " $
[ componentName
] <> classes
}
module Gargantext.Components.Bootstrap.Tabs(tabs) where
import Gargantext.Prelude
import Data.Foldable (intercalate)
import Effect (Effect)
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
type Props a =
( value :: a
, callback :: a -> Effect Unit
, list :: Array a
| Options
)
type Options =
( className :: String
)
options :: Record Options
options =
{ className : ""
}
-- | Structural molecular component to the Bootstrap <nav-tabs> + <nav-item>
-- | simplifying a lot of the available UI/UX possibilites (type, disabled
-- | tabs, etc)
-- |
-- | https://getbootstrap.com/docs/4.6/components/navs/#tabs
tabs :: forall r a.
Show a
=> Eq a
=> R2.OptLeaf Options (Props a) r
tabs = R2.optLeaf component options
componentName :: String
componentName = "b-tabs"
component :: forall a.
Show a
=> Eq a
=> R.Component (Props a)
component = R.hooksComponent componentName cpt where
cpt props@{ list, value, callback } _ = do
-- Computed
let
className = intercalate " "
-- provided custom className
[ props.className
-- BEM classNames
, componentName
-- Bootstrap specific classNames
, "nav nav-tabs"
]
-- Render
pure $
H.ul
{ className } $
flip map list \item ->
H.li
{ className: "nav-item"
, on: { click: \_ -> callback item }
}
[
H.a
{ className: intercalate " "
[ "nav-link"
, value == item ? "active" $ ""
]
}
[
H.text $ show item
]
]
...@@ -16,7 +16,6 @@ import Gargantext.Ends (Frontends) ...@@ -16,7 +16,6 @@ import Gargantext.Ends (Frontends)
import Gargantext.Hooks.LinkHandler (useLinkHandler) import Gargantext.Hooks.LinkHandler (useLinkHandler)
import Gargantext.Routes (AppRoute(..)) import Gargantext.Routes (AppRoute(..))
import Gargantext.Sessions (Session(..), unSessions) import Gargantext.Sessions (Session(..), unSessions)
import Gargantext.Utils (nbsp)
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
...@@ -132,9 +131,10 @@ plusCpt = here.component "plus" cpt where ...@@ -132,9 +131,10 @@ plusCpt = here.component "plus" cpt where
, variant: ButtonVariant Light , variant: ButtonVariant Light
} }
[ [
B.icon { name: "universal-access" } B.icon
{ name: "universal-access" }
, ,
H.text $ nbsp 1 B.wad_ [ "d-inline-block", "w-1" ]
, ,
H.text $ "Log in/out" H.text $ "Log in/out"
] ]
...@@ -150,7 +150,7 @@ forestLayout :: R2.Leaf Props ...@@ -150,7 +150,7 @@ forestLayout :: R2.Leaf Props
forestLayout = R2.leaf forestLayoutCpt forestLayout = R2.leaf forestLayoutCpt
forestLayoutCpt :: R.Memo Props forestLayoutCpt :: R.Memo Props
forestLayoutCpt = R.memo' $ here.component "forestLayout" cpt where forestLayoutCpt = R.memo' $ here.component "forestLayout" cpt where
cpt p children = pure $ cpt p _ = pure $
H.div H.div
{ className: "forest-layout" } { className: "forest-layout" }
......
exports.nodeUserRegexp = /(@{1}.*).gargantext.org$/;
module Gargantext.Components.Forest.Tree.Node where module Gargantext.Components.Forest.Tree.Node
( nodeSpan
, blankNodeSpan
) where
import Gargantext.Prelude import Gargantext.Prelude
import DOM.Simple as DOM import Data.Array.NonEmpty as NArray
import DOM.Simple.Event as DE
import Data.Foldable (intercalate) import Data.Foldable (intercalate)
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..), maybe)
import Data.Nullable (Nullable, null) import Data.Nullable (null)
import Data.String.Regex as Regex
import Data.Symbol (SProxy(..)) import Data.Symbol (SProxy(..))
import Data.Tuple.Nested ((/\)) import Data.Tuple.Nested ((/\))
import Effect (Effect) import Effect (Effect)
...@@ -15,24 +18,21 @@ import Effect.Class (liftEffect) ...@@ -15,24 +18,21 @@ import Effect.Class (liftEffect)
import Gargantext.AsyncTasks as GAT import Gargantext.AsyncTasks as GAT
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Tooltip (tooltipBind)
import Gargantext.Components.Bootstrap.Types (ComponentStatus(..), TooltipEffect(..), Variant(..)) import Gargantext.Components.Bootstrap.Types (ComponentStatus(..), TooltipEffect(..), Variant(..))
import Gargantext.Components.Forest.Tree.Node.Action.Types (Action(..)) import Gargantext.Components.Forest.Tree.Node.Action.Types (Action(..))
import Gargantext.Components.Forest.Tree.Node.Action.Upload (DroppedFile(..), fileTypeView) import Gargantext.Components.Forest.Tree.Node.Action.Upload (DroppedFile(..), fileTypeView)
import Gargantext.Components.Forest.Tree.Node.Action.Upload.Types (FileType(..), UploadFileBlob(..)) import Gargantext.Components.Forest.Tree.Node.Action.Upload.Types (FileType(..), UploadFileBlob(..))
import Gargantext.Components.Forest.Tree.Node.Box (nodePopupView) import Gargantext.Components.Forest.Tree.Node.Box (nodePopupView)
import Gargantext.Components.Forest.Tree.Node.Settings (SettingsBox(..), settingsBox) import Gargantext.Components.Forest.Tree.Node.Settings (SettingsBox(..), settingsBox)
import Gargantext.Components.Forest.Tree.Node.Tools.ProgressBar (BarType(..), asyncProgressBar)
import Gargantext.Components.Forest.Tree.Node.Tools.ProgressBar as PB
import Gargantext.Components.Forest.Tree.Node.Tools.Sync (nodeActionsGraph, nodeActionsNodeList) import Gargantext.Components.Forest.Tree.Node.Tools.Sync (nodeActionsGraph, nodeActionsNodeList)
import Gargantext.Components.GraphExplorer.API as GraphAPI import Gargantext.Components.GraphExplorer.API as GraphAPI
import Gargantext.Components.Lang (Lang(EN)) import Gargantext.Components.Lang (Lang(EN))
import Gargantext.Components.Nodes.Corpus (loadCorpusWithChild) import Gargantext.Components.Nodes.Corpus (loadCorpusWithChild)
import Gargantext.Config.REST (logRESTError) import Gargantext.Config.REST (logRESTError)
import Gargantext.Context.Progress (AsyncProps, asyncContext, asyncProgress) import Gargantext.Context.Progress (asyncContext, asyncProgress)
import Gargantext.Ends (Frontends, url) import Gargantext.Ends (Frontends, url)
import Gargantext.Hooks.FirstEffect (useFirstEffect') import Gargantext.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Hooks.Loader (useLoader, useLoaderEffect) import Gargantext.Hooks.Loader (useLoaderEffect)
import Gargantext.Hooks.Version (Version, useVersion) import Gargantext.Hooks.Version (Version, useVersion)
import Gargantext.Routes as Routes import Gargantext.Routes as Routes
import Gargantext.Sessions (Session, sessionId) import Gargantext.Sessions (Session, sessionId)
...@@ -42,14 +42,15 @@ import Gargantext.Utils (nbsp, textEllipsisBreak, (?)) ...@@ -42,14 +42,15 @@ import Gargantext.Utils (nbsp, textEllipsisBreak, (?))
import Gargantext.Utils.Popover as Popover import Gargantext.Utils.Popover as Popover
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2 import Gargantext.Utils.Toestand as T2
import React.SyntheticEvent (SyntheticEvent_)
import React.SyntheticEvent as E
import React.SyntheticEvent as SE import React.SyntheticEvent as SE
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
import Record as Record import Record as Record
import Toestand as T import Toestand as T
-- (?) never been able to properly declare PureScript Regex...
foreign import nodeUserRegexp :: Regex.Regex
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.Forest.Tree.Node" here = R2.here "Gargantext.Components.Forest.Tree.Node"
...@@ -78,7 +79,6 @@ nodeSpanCpt :: R.Component NodeSpanProps ...@@ -78,7 +79,6 @@ nodeSpanCpt :: R.Component NodeSpanProps
nodeSpanCpt = here.component "nodeSpan" cpt nodeSpanCpt = here.component "nodeSpan" cpt
where where
cpt props@{ boxes: boxes@{ errors cpt props@{ boxes: boxes@{ errors
, handed
, reloadMainPage , reloadMainPage
, reloadRoot , reloadRoot
, route , route
...@@ -119,7 +119,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -119,7 +119,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
dropClass Nothing _ = "" dropClass Nothing _ = ""
name' :: String -> GT.NodeType -> Session -> String name' :: String -> GT.NodeType -> Session -> String
name' _ GT.NodeUser session = show session name' _ GT.NodeUser s = show s
name' n _ _ = n name' n _ _ = n
isSelected = Just route' == Routes.nodeTypeAppRoute nodeType (sessionId session) id isSelected = Just route' == Routes.nodeTypeAppRoute nodeType (sessionId session) id
...@@ -128,6 +128,8 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -128,6 +128,8 @@ nodeSpanCpt = here.component "nodeSpan" cpt
href = url frontends $ GT.NodePath (sessionId session) nodeType (Just id) href = url frontends $ GT.NodePath (sessionId session) nodeType (Just id)
name = name' props.name nodeType session
-- Methods -- Methods
dropHandler :: forall event. dropHandler :: forall event.
...@@ -151,18 +153,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -151,18 +153,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt
T.Box Boolean T.Box Boolean
-> SE.SyntheticEvent_ event -> SE.SyntheticEvent_ event
-> Effect Unit -> Effect Unit
onDragOverHandler isDragOver e = do onDragOverHandler box e = do
-- prevent redirection when file is dropped -- prevent redirection when file is dropped
-- https://stackoverflow.com/a/6756680/941471 -- https://stackoverflow.com/a/6756680/941471
SE.preventDefault e SE.preventDefault e
SE.stopPropagation e SE.stopPropagation e
T.write_ true isDragOver T.write_ true box
onDragLeave :: forall event. onDragLeave :: forall event.
T.Box Boolean T.Box Boolean
-> SE.SyntheticEvent_ event -> SE.SyntheticEvent_ event
-> Effect Unit -> Effect Unit
onDragLeave isDragOver _ = T.write_ false isDragOver onDragLeave box _ = T.write_ false box
onTaskFinish :: onTaskFinish ::
GT.NodeID GT.NodeID
...@@ -231,6 +233,27 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -231,6 +233,27 @@ nodeSpanCpt = here.component "nodeSpan" cpt
} }
[ [
-- // Abstract informations //
nodeTooltip
{ id
, nodeType
, name
}
[
case mVersion of
Nothing -> mempty
Just v -> versionComparator v
]
,
R.createPortal
[
fileTypeView
{ dispatch, droppedFile, id, isDragOver, nodeType } []
]
host
,
-- // Leaf informations data // -- // Leaf informations data //
folderIcon folderIcon
...@@ -240,15 +263,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -240,15 +263,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt
} }
, ,
nodeIcon nodeIcon
(
{ nodeType { nodeType
, isLeaf , isLeaf
, callback: const $ T.modify_ (not) folderOpen , callback: const $ T.modify_ (not) folderOpen
, isSelected
} }
)
[ [
case mVersion of case mVersion of
Nothing -> mempty Nothing -> mempty
Just { clientVersion, remoteVersion} -> Just { clientVersion, remoteVersion} ->
B.iconButton B.iconButton $
{ className: intercalate " " { className: intercalate " "
[ "mainleaf__version-badge" [ "mainleaf__version-badge"
, clientVersion == remoteVersion ? , clientVersion == remoteVersion ?
...@@ -267,6 +293,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -267,6 +293,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
, href , href
, id , id
, name: name' props.name nodeType session , name: name' props.name nodeType session
, type: nodeType
} }
, ,
...@@ -305,7 +332,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -305,7 +332,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
{ boxes { boxes
, dispatch , dispatch
, id , id
, name: name' props.name nodeType session , name
, nodeType , nodeType
, onPopoverClose: const $ onPopoverClose popoverRef , onPopoverClose: const $ onPopoverClose popoverRef
, session , session
...@@ -325,26 +352,6 @@ nodeSpanCpt = here.component "nodeSpan" cpt ...@@ -325,26 +352,6 @@ nodeSpanCpt = here.component "nodeSpan" cpt
taskProgress taskProgress
{} {}
] ]
,
-- // Abstract informations //
nodeTooltip
{ id
, nodeType
, name: name' props.name nodeType session
}
[
case mVersion of
Nothing -> mempty
Just v -> versionComparator v
]
,
R.createPortal
[
fileTypeView
{ dispatch, droppedFile, id, isDragOver, nodeType } []
]
host
] ]
...@@ -354,6 +361,7 @@ type NodeIconProps = ...@@ -354,6 +361,7 @@ type NodeIconProps =
( nodeType :: GT.NodeType ( nodeType :: GT.NodeType
, callback :: Unit -> Effect Unit , callback :: Unit -> Effect Unit
, isLeaf :: Boolean , isLeaf :: Boolean
, isSelected :: Boolean
) )
nodeIcon :: R2.Component NodeIconProps nodeIcon :: R2.Component NodeIconProps
...@@ -363,7 +371,10 @@ nodeIconCpt = here.component "nodeIcon" cpt where ...@@ -363,7 +371,10 @@ nodeIconCpt = here.component "nodeIcon" cpt where
cpt { nodeType cpt { nodeType
, callback , callback
, isLeaf , isLeaf
} children = pure $ , isSelected
} children = do
-- Render
pure $
H.span H.span
{ className: "mainleaf__node-icon" } $ { className: "mainleaf__node-icon" } $
...@@ -372,6 +383,7 @@ nodeIconCpt = here.component "nodeIcon" cpt where ...@@ -372,6 +383,7 @@ nodeIconCpt = here.component "nodeIcon" cpt where
{ name: GT.getIcon nodeType true { name: GT.getIcon nodeType true
, callback , callback
, status: isLeaf ? Idled $ Enabled , status: isLeaf ? Idled $ Enabled
, variant: isSelected ? Primary $ Dark
} }
] ]
<> children <> children
...@@ -408,25 +420,25 @@ folderIconCpt = here.component "folderIcon" cpt where ...@@ -408,25 +420,25 @@ folderIconCpt = here.component "folderIcon" cpt where
----------------------------------------------- -----------------------------------------------
type NodeLinkProps = type NodeLinkProps =
( callback :: Unit -> Effect Unit ( callback :: Unit -> Effect Unit
, href :: String , href :: String
, id :: Int , id :: Int
, name :: GT.Name , name :: GT.Name
, type :: GT.NodeType
) )
nodeLink :: R2.Leaf NodeLinkProps nodeLink :: R2.Leaf NodeLinkProps
nodeLink = R2.leaf nodeLinkCpt nodeLink = R2.leaf nodeLinkCpt
nodeLinkCpt :: R.Component NodeLinkProps nodeLinkCpt :: R.Component NodeLinkProps
nodeLinkCpt = here.component "nodeLink" cpt nodeLinkCpt = here.component "nodeLink" cpt where
where
cpt { callback cpt { callback
, href , href
, id , id
, name , name
, type: nodeType
} _ = do } _ = do
-- Computed
let let
tid = tooltipId name id tid = tooltipId name id
...@@ -434,6 +446,7 @@ nodeLinkCpt = here.component "nodeLink" cpt ...@@ -434,6 +446,7 @@ nodeLinkCpt = here.component "nodeLink" cpt
{ href { href
} `Record.merge` B.tooltipBind tid } `Record.merge` B.tooltipBind tid
-- Render
pure $ pure $
H.div H.div
...@@ -444,10 +457,18 @@ nodeLinkCpt = here.component "nodeLink" cpt ...@@ -444,10 +457,18 @@ nodeLinkCpt = here.component "nodeLink" cpt
H.a H.a
aProps aProps
[ [
B.span_ $ textEllipsisBreak 15 name B.span_ $ nodeLinkText nodeType name
] ]
] ]
nodeLinkText :: GT.NodeType -> String -> String
nodeLinkText GT.NodeUser s = s # (truncateNodeUser)
>>> maybe s identity
nodeLinkText _ s = textEllipsisBreak 15 s
truncateNodeUser :: String -> Maybe String
truncateNodeUser = Regex.match (nodeUserRegexp) >=> flip NArray.index 1 >>> join
--------------------------------------------------- ---------------------------------------------------
type NodeTooltipProps = type NodeTooltipProps =
......
...@@ -31,15 +31,15 @@ type NodeActionsGraphProps = ...@@ -31,15 +31,15 @@ type NodeActionsGraphProps =
nodeActionsGraph :: R2.Component NodeActionsGraphProps nodeActionsGraph :: R2.Component NodeActionsGraphProps
nodeActionsGraph = R.createElement nodeActionsGraphCpt nodeActionsGraph = R.createElement nodeActionsGraphCpt
nodeActionsGraphCpt :: R.Component NodeActionsGraphProps nodeActionsGraphCpt :: R.Component NodeActionsGraphProps
nodeActionsGraphCpt = here.component "nodeActionsGraph" cpt nodeActionsGraphCpt = here.component "nodeActionsGraph" cpt where
where cpt { id, graphVersions, session, refresh } _ =
cpt { id, graphVersions, session, refresh } _ = do let sameVersions = (graphVersions.gv_graph == Just graphVersions.gv_repo)
pure $ H.div { className: "node-actions" } [ in pure $
if graphVersions.gv_graph == Just graphVersions.gv_repo then
H.div {} [] R2.if' (not sameVersions) $
else
graphUpdateButton { id, session, refresh } graphUpdateButton { id, session, refresh }
]
type GraphUpdateButtonProps = type GraphUpdateButtonProps =
( id :: GT.ID ( id :: GT.ID
...@@ -99,12 +99,9 @@ type NodeActionsNodeListProps = ...@@ -99,12 +99,9 @@ type NodeActionsNodeListProps =
nodeActionsNodeList :: Record NodeActionsNodeListProps -> R.Element nodeActionsNodeList :: Record NodeActionsNodeListProps -> R.Element
nodeActionsNodeList p = R.createElement nodeActionsNodeListCpt p [] nodeActionsNodeList p = R.createElement nodeActionsNodeListCpt p []
nodeActionsNodeListCpt :: R.Component NodeActionsNodeListProps nodeActionsNodeListCpt :: R.Component NodeActionsNodeListProps
nodeActionsNodeListCpt = here.component "nodeActionsNodeList" cpt nodeActionsNodeListCpt = here.component "nodeActionsNodeList" cpt where
where cpt props _ = pure $ nodeListUpdateButton props
cpt props _ = do
pure $ H.div { className: "node-actions" } [
nodeListUpdateButton props
]
type NodeListUpdateButtonProps = type NodeListUpdateButtonProps =
( listId :: GT.ListId ( listId :: GT.ListId
......
...@@ -132,9 +132,12 @@ graphCpt = here.component "graph" cpt where ...@@ -132,9 +132,12 @@ graphCpt = here.component "graph" cpt where
Sigma.stopForceAtlas2 sig Sigma.stopForceAtlas2 sig
case mCamera of case mCamera of
Nothing -> pure unit
Just (GET.Camera { ratio, x, y }) -> do Just (GET.Camera { ratio, x, y }) -> do
Sigma.updateCamera sig { ratio, x, y } Sigma.updateCamera sig { ratio, x, y }
-- Default camera: slightly de-zoom the graph to avoid
-- nodes sticking to the container borders
Nothing ->
Sigma.updateCamera sig { ratio: 1.1, x: 0.0, y: 0.0 }
-- Reload Sigma on Theme changes -- Reload Sigma on Theme changes
_ <- flip T.listen boxes.theme \{ old, new } -> _ <- flip T.listen boxes.theme \{ old, new } ->
......
module Gargantext.Components.GraphExplorer.Button module Gargantext.Components.GraphExplorer.Buttons
( Props, centerButton, simpleButton, cameraButton ) where ( Props
, centerButton
, simpleButton
, cameraButton
, edgesToggleButton
, louvainToggleButton
, pauseForceAtlasButton
, resetForceAtlasButton
, multiSelectEnabledButton
) where
import Prelude import Prelude
import DOM.Simple.Console (log2)
import Data.DateTime as DDT
import Data.DateTime.Instant as DDI
import Data.Either (Either(..)) import Data.Either (Either(..))
import Data.Enum (fromEnum) import Data.Enum (fromEnum)
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..))
import Data.DateTime as DDT
import Data.DateTime.Instant as DDI
import Data.String as DS import Data.String as DS
import DOM.Simple.Console (log2)
import Effect (Effect) import Effect (Effect)
import Effect.Aff (launchAff_) import Effect.Aff (launchAff_)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Now as EN import Effect.Now as EN
import Reactix as R import Gargantext.Components.Bootstrap as B
import Reactix.DOM.HTML as H import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), ComponentStatus(..), Variant(..))
import Gargantext.Components.Forest.Tree.Node.Action.Upload (uploadArbitraryData) import Gargantext.Components.Forest.Tree.Node.Action.Upload (uploadArbitraryData)
import Gargantext.Components.Forest.Tree.Node.Action.Upload.Types (FileFormat(..)) import Gargantext.Components.Forest.Tree.Node.Action.Upload.Types (FileFormat(..))
import Gargantext.Components.GraphExplorer.API (cloneGraph) import Gargantext.Components.GraphExplorer.API (cloneGraph)
import Gargantext.Components.GraphExplorer.Resources as Graph
import Gargantext.Components.GraphExplorer.Types as GET import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Components.GraphExplorer.Utils as GEU import Gargantext.Components.GraphExplorer.Utils as GEU
import Gargantext.Hooks.Sigmax as Sigmax import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Sigma as Sigma import Gargantext.Hooks.Sigmax.Sigma as Sigma
import Gargantext.Hooks.Sigmax.Types as SigmaxTypes
import Gargantext.Sessions (Session) import Gargantext.Sessions (Session)
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2 import Gargantext.Utils.Toestand as T2
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Button" here = R2.here "Gargantext.Components.GraphExplorer.Button"
...@@ -36,9 +50,12 @@ type Props = ( ...@@ -36,9 +50,12 @@ type Props = (
, text :: String , text :: String
) )
-- @WIP
simpleButton :: Record Props -> R.Element simpleButton :: Record Props -> R.Element
simpleButton props = R.createElement simpleButtonCpt props [] simpleButton props = R.createElement simpleButtonCpt props []
------------------------------------------------------
simpleButtonCpt :: R.Component Props simpleButtonCpt :: R.Component Props
simpleButtonCpt = here.component "simpleButton" cpt simpleButtonCpt = here.component "simpleButton" cpt
where where
...@@ -48,14 +65,16 @@ simpleButtonCpt = here.component "simpleButton" cpt ...@@ -48,14 +65,16 @@ simpleButtonCpt = here.component "simpleButton" cpt
} [ R2.small {} [ H.text text ] ] } [ R2.small {} [ H.text text ] ]
centerButton :: R.Ref Sigmax.Sigma -> R.Element centerButton :: R.Ref Sigmax.Sigma -> R.Element
centerButton sigmaRef = simpleButton { centerButton sigmaRef = B.button
onClick: \_ -> do { variant: OutlinedButtonVariant Secondary
, callback: \_ -> do
let sigma = R.readRef sigmaRef let sigma = R.readRef sigmaRef
Sigmax.dependOnSigma sigma "[centerButton] sigma: Nothing" $ \s -> Sigmax.dependOnSigma sigma "[centerButton] sigma: Nothing" $ \s ->
Sigma.goToAllCameras s {x: 0.0, y: 0.0, ratio: 1.0, angle: 0.0} Sigma.goToAllCameras s {x: 0.0, y: 0.0, ratio: 1.0, angle: 0.0}
, text: "Center"
} }
[ H.text "Center" ]
------------------------------------------------------
type CameraButtonProps = type CameraButtonProps =
( id :: Int ( id :: Int
...@@ -71,8 +90,10 @@ cameraButton { id ...@@ -71,8 +90,10 @@ cameraButton { id
, hyperdataGraph: GET.HyperdataGraph { graph: GET.GraphData hyperdataGraph } , hyperdataGraph: GET.HyperdataGraph { graph: GET.GraphData hyperdataGraph }
, session , session
, sigmaRef , sigmaRef
, reloadForest } = simpleButton { , reloadForest } = B.button
onClick: \_ -> do
{ variant: OutlinedButtonVariant Secondary
, callback: \_ -> do
let sigma = R.readRef sigmaRef let sigma = R.readRef sigmaRef
Sigmax.dependOnSigma sigma "[cameraButton] sigma: Nothing" $ \s -> do Sigmax.dependOnSigma sigma "[cameraButton] sigma: Nothing" $ \s -> do
screen <- Sigma.takeScreenshot s screen <- Sigma.takeScreenshot s
...@@ -105,5 +126,171 @@ cameraButton { id ...@@ -105,5 +126,171 @@ cameraButton { id
Left err -> liftEffect $ log2 "[cameraButton] RESTError" err Left err -> liftEffect $ log2 "[cameraButton] RESTError" err
Right _ret -> do Right _ret -> do
liftEffect $ T2.reload reloadForest liftEffect $ T2.reload reloadForest
, text: "Screenshot"
} }
[ H.text "Screenshot" ]
------------------------------------------------------
type EdgesButtonProps =
( state :: T.Box SigmaxTypes.ShowEdgesState
, stateAtlas :: T.Box SigmaxTypes.ForceAtlasState
)
edgesToggleButton :: R2.Leaf EdgesButtonProps
edgesToggleButton = R2.leaf edgesToggleButtonCpt
edgesToggleButtonCpt :: R.Component EdgesButtonProps
edgesToggleButtonCpt = here.component "edgesToggleButton" cpt
where
cpt { state, stateAtlas } _ = do
-- States
state' <- R2.useLive' state
stateAtlas' <- R2.useLive' stateAtlas
-- Computed
let
cst SigmaxTypes.InitialRunning = Disabled
cst SigmaxTypes.Running = Disabled
cst _ = Enabled
-- Render
pure $
B.button
{ variant: state' == SigmaxTypes.EShow ?
ButtonVariant Secondary $
OutlinedButtonVariant Secondary
, status: cst stateAtlas'
-- TODO: Move this to Graph.purs to the R.useEffect handler which renders nodes/edges
, callback: \_ -> T.modify_ SigmaxTypes.toggleShowEdgesState state
}
[ H.text "Edges" ]
------------------------------------------------------
type LouvainToggleButtonProps =
( state :: T.Box Boolean
)
louvainToggleButton :: R2.Leaf LouvainToggleButtonProps
louvainToggleButton = R2.leaf louvainToggleButtonCpt
louvainToggleButtonCpt :: R.Component LouvainToggleButtonProps
louvainToggleButtonCpt = here.component "louvainToggleButton" cpt
where
cpt { state } _ = do
state' <- R2.useLive' state
pure $
B.button
{ variant: state' ?
ButtonVariant Secondary $
OutlinedButtonVariant Secondary
, callback: \_ -> T.modify_ (not) state
}
[ H.text "Louvain" ]
--------------------------------------------------------------
type ForceAtlasProps =
( state :: T.Box SigmaxTypes.ForceAtlasState
)
pauseForceAtlasButton :: R2.Leaf ForceAtlasProps
pauseForceAtlasButton = R2.leaf pauseForceAtlasButtonCpt
pauseForceAtlasButtonCpt :: R.Component ForceAtlasProps
pauseForceAtlasButtonCpt = here.component "forceAtlasToggleButton" cpt
where
cpt { state } _ = do
-- States
state' <- R2.useLive' state
-- Computed
let
cls SigmaxTypes.InitialRunning = "on-running-animation active"
cls SigmaxTypes.Running = "on-running-animation active"
cls _ = ""
vrt SigmaxTypes.InitialRunning = ButtonVariant Secondary
vrt SigmaxTypes.Running = ButtonVariant Secondary
vrt _ = OutlinedButtonVariant Secondary
icn SigmaxTypes.InitialRunning = "pause"
icn SigmaxTypes.InitialStopped = "play"
icn SigmaxTypes.Running = "pause"
icn SigmaxTypes.Paused = "play"
icn SigmaxTypes.Killed = "play"
-- Render
pure $
B.button
{ variant: vrt state'
, className: cls state'
, callback: \_ -> T.modify_ SigmaxTypes.toggleForceAtlasState state
}
[
B.icon
{ name: icn state'}
]
--------------------------------------------------------
type ResetForceAtlasProps =
( forceAtlasState :: T.Box SigmaxTypes.ForceAtlasState
, sigmaRef :: R.Ref Sigmax.Sigma
)
resetForceAtlasButton :: R2.Leaf ResetForceAtlasProps
resetForceAtlasButton = R2.leaf resetForceAtlasButtonCpt
resetForceAtlasButtonCpt :: R.Component ResetForceAtlasProps
resetForceAtlasButtonCpt = here.component "resetForceAtlasToggleButton" cpt
where
cpt { forceAtlasState, sigmaRef } _ = do
pure $ H.button { className: "btn btn-outline-secondary"
, on: { click: onClick forceAtlasState sigmaRef }
} [ R2.small {} [ H.text "Reset Force Atlas" ] ]
onClick forceAtlasState sigmaRef _ = do
-- TODO Sigma.killForceAtlas2 sigma
-- startForceAtlas2 sigma
Sigmax.dependOnSigma (R.readRef sigmaRef) "[resetForceAtlasButton] no sigma" $ \sigma -> do
Sigma.killForceAtlas2 sigma
Sigma.refreshForceAtlas sigma Graph.forceAtlas2Settings
T.write_ SigmaxTypes.Killed forceAtlasState
------------------------------------------------------------------
type MultiSelectEnabledButtonProps =
( state :: T.Box Boolean
)
multiSelectEnabledButton :: R2.Leaf MultiSelectEnabledButtonProps
multiSelectEnabledButton = R2.leaf multiSelectEnabledButtonCpt
multiSelectEnabledButtonCpt :: R.Component MultiSelectEnabledButtonProps
multiSelectEnabledButtonCpt = here.component "multiSelectEnabledButton" cpt
where
cpt { state } _ = do
state' <- R2.useLive' state
pure $
H.div
{ className: "btn-group"
, role: "group"
}
[
B.button
{ variant: state' ?
OutlinedButtonVariant Secondary $
ButtonVariant Secondary
, callback: \_ -> T.write_ false state
}
[ H.text "Single" ]
,
B.button
{ variant: state' ?
ButtonVariant Secondary $
OutlinedButtonVariant Secondary
, callback: \_ -> T.write_ true state
}
[ H.text "Multiple" ]
]
...@@ -5,23 +5,21 @@ module Gargantext.Components.GraphExplorer.Controls ...@@ -5,23 +5,21 @@ module Gargantext.Components.GraphExplorer.Controls
, controlsCpt , controlsCpt
) where ) where
import Prelude
import Data.Array as A import Data.Array as A
import Data.Foldable (intercalate)
import Data.Int as I import Data.Int as I
import Data.Maybe (Maybe(..), maybe) import Data.Maybe (Maybe(..), maybe)
import Data.Sequence as Seq import Data.Sequence as Seq
import Data.Set as Set import Data.Set as Set
import Effect.Timer (setTimeout) import Effect.Timer (setTimeout)
import Prelude import Gargantext.Components.Bootstrap as B
import Reactix as R import Gargantext.Components.GraphExplorer.Buttons (centerButton, cameraButton, edgesToggleButton, louvainToggleButton, pauseForceAtlasButton, multiSelectEnabledButton)
import Reactix.DOM.HTML as RH
import Toestand as T
import Gargantext.Components.Graph as Graph
import Gargantext.Components.GraphExplorer.Button (centerButton, cameraButton)
import Gargantext.Components.GraphExplorer.RangeControl (edgeConfluenceControl, edgeWeightControl, nodeSizeControl) import Gargantext.Components.GraphExplorer.RangeControl (edgeConfluenceControl, edgeWeightControl, nodeSizeControl)
import Gargantext.Components.GraphExplorer.SlideButton (labelSizeButton, mouseSelectorSizeButton) import Gargantext.Components.GraphExplorer.Resources as Graph
import Gargantext.Components.GraphExplorer.ToggleButton (multiSelectEnabledButton, edgesToggleButton, louvainToggleButton, pauseForceAtlasButton{-, resetForceAtlasButton-})
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.SlideButton (labelSizeButton, mouseSelectorSizeButton)
import Gargantext.Components.GraphExplorer.Types as GET import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax as Sigmax import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Types as SigmaxT import Gargantext.Hooks.Sigmax.Types as SigmaxT
...@@ -30,6 +28,9 @@ import Gargantext.Types as GT ...@@ -30,6 +28,9 @@ import Gargantext.Types as GT
import Gargantext.Utils.Range as Range import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2 import Gargantext.Utils.Toestand as T2
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Controls" here = R2.here "Gargantext.Components.GraphExplorer.Controls"
...@@ -51,8 +52,7 @@ type Controls = ...@@ -51,8 +52,7 @@ type Controls =
, showControls :: T.Box Boolean , showControls :: T.Box Boolean
, showEdges :: T.Box SigmaxT.ShowEdgesState , showEdges :: T.Box SigmaxT.ShowEdgesState
, showLouvain :: T.Box Boolean , showLouvain :: T.Box Boolean
, showTree :: T.Box Boolean , showSidebar :: T.Box GT.SidePanelState
, sidePanelState :: T.Box GT.SidePanelState
, sideTab :: T.Box GET.SideTab , sideTab :: T.Box GET.SideTab
, sigmaRef :: R.Ref Sigmax.Sigma , sigmaRef :: R.Ref Sigmax.Sigma
) )
...@@ -82,23 +82,29 @@ controlsCpt = here.component "controls" cpt ...@@ -82,23 +82,29 @@ controlsCpt = here.component "controls" cpt
, reloadForest , reloadForest
, selectedNodeIds , selectedNodeIds
, session , session
, showControls
, showEdges , showEdges
, showLouvain , showLouvain
, sidePanelState , showSidebar
, sideTab , sideTab
, sigmaRef } _ = do , sigmaRef } _ = do
-- | States
-- |
forceAtlasState' <- T.useLive T.unequal forceAtlasState forceAtlasState' <- T.useLive T.unequal forceAtlasState
graphStage' <- T.useLive T.unequal graphStage graphStage' <- T.useLive T.unequal graphStage
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
showControls' <- T.useLive T.unequal showControls showSidebar' <- T.useLive T.unequal showSidebar
sidePanelState' <- T.useLive T.unequal sidePanelState
localControls <- initialLocalControls localControls <- initialLocalControls
-- ref to track automatic FA pausing -- ref to track automatic FA pausing
-- If user pauses FA before auto is triggered, clear the timeoutId -- If user pauses FA before auto is triggered, clear the timeoutId
mFAPauseRef <- R.useRef Nothing mFAPauseRef <- R.useRef Nothing
-- | Effects
-- |
-- When graph is changed, cleanup the mFAPauseRef so that forceAtlas -- When graph is changed, cleanup the mFAPauseRef so that forceAtlas
-- timeout is retriggered. -- timeout is retriggered.
R.useEffect' $ do R.useEffect' $ do
...@@ -117,8 +123,8 @@ controlsCpt = here.component "controls" cpt ...@@ -117,8 +123,8 @@ controlsCpt = here.component "controls" cpt
-- Automatic opening of sidebar when a node is selected (but only first time). -- Automatic opening of sidebar when a node is selected (but only first time).
R.useEffect' $ do R.useEffect' $ do
if sidePanelState' == GT.InitialClosed && (not Set.isEmpty selectedNodeIds') then do if showSidebar' == GT.InitialClosed && (not Set.isEmpty selectedNodeIds') then do
T.write_ GT.Opened sidePanelState T.write_ GT.Opened showSidebar
T.write_ GET.SideTabData sideTab T.write_ GET.SideTabData sideTab
else else
pure unit pure unit
...@@ -138,6 +144,10 @@ controlsCpt = here.component "controls" cpt ...@@ -138,6 +144,10 @@ controlsCpt = here.component "controls" cpt
else else
pure unit pure unit
-- | Computed
-- |
let edgesConfluenceSorted = A.sortWith (_.confluence) $ Seq.toUnfoldable $ SigmaxT.graphEdges graph let edgesConfluenceSorted = A.sortWith (_.confluence) $ Seq.toUnfoldable $ SigmaxT.graphEdges graph
let edgeConfluenceMin = maybe 0.0 _.confluence $ A.head edgesConfluenceSorted let edgeConfluenceMin = maybe 0.0 _.confluence $ A.head edgesConfluenceSorted
let edgeConfluenceMax = maybe 100.0 _.confluence $ A.last edgesConfluenceSorted let edgeConfluenceMax = maybe 100.0 _.confluence $ A.last edgesConfluenceSorted
...@@ -157,63 +167,147 @@ controlsCpt = here.component "controls" cpt ...@@ -157,63 +167,147 @@ controlsCpt = here.component "controls" cpt
let nodeSizeMax = maybe 100.0 _.size $ A.last nodesSorted let nodeSizeMax = maybe 100.0 _.size $ A.last nodesSorted
let nodeSizeRange = Range.Closed { min: nodeSizeMin, max: nodeSizeMax } let nodeSizeRange = Range.Closed { min: nodeSizeMin, max: nodeSizeMax }
let className = "navbar navbar-expand-lg " <> if showControls' then "" else "d-none" let gap = H.span { className: "graph-toolbar__gap" } []
-- | Render
-- |
pure $ RH.nav { className } pure $
[ RH.ul { className: "navbar-nav mx-auto" }
[ -- change type button (?) H.nav
navItem [ centerButton sigmaRef ] { className: "graph-toolbar" }
-- , navItem [ resetForceAtlasButton { forceAtlasState, sigmaRef } [] ] [
, navItem [ pauseForceAtlasButton { state: forceAtlasState } [] ] H.div
, navItem [ edgesToggleButton { state: showEdges } [] ] { className: "flex-shrink-0" }
, navItem [ louvainToggleButton { state: showLouvain } [] ] [
, navItem [ edgeConfluenceControl { range: edgeConfluenceRange H.div
, state: edgeConfluence } [] ] { className: "d-flex" }
, navItem [ edgeWeightControl { range: edgeWeightRange [
, state: edgeWeight } [] ] -- View Settings
B.fieldset
{ className: "graph-toolbar__section"
, titleSlot: H.text "View settings"
}
[
-- change type button (?)
centerButton sigmaRef
,
gap
,
edgesToggleButton
{ state: showEdges
, stateAtlas: forceAtlasState
}
,
gap
,
louvainToggleButton { state: showLouvain }
]
,
-- Actions
B.fieldset
{ className: "graph-toolbar__section"
, titleSlot: H.text "Actions"
}
[
-- resetForceAtlasButton { forceAtlasState, sigmaRef }
pauseForceAtlasButton { state: forceAtlasState }
,
gap
,
cameraButton
{ id: graphId
, hyperdataGraph: hyperdataGraph
, session: session
, sigmaRef: sigmaRef
, reloadForest
}
]
]
,
-- Selection Settings
B.fieldset
{ className: intercalate " "
[ "graph-toolbar__section"
, "graph-toolbar__section--selection"
]
, titleSlot: H.text "Selection settings"
}
[
-- zoom: 0 -100 - calculate ratio
multiSelectEnabledButton { state: multiSelectEnabled }
,
gap
,
-- toggle multi node selection
-- save button
mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize
]
]
,
-- Controls
B.fieldset
{ className: intercalate " "
[ "graph-toolbar__section"
, "graph-toolbar__section--controls"
, "flex-grow-1 flex-shrink-1"
]
, titleSlot: H.text "Controls"
}
[
H.div
{ className: "d-flex justify-content-between mb-3" }
[
edgeConfluenceControl
{ range: edgeConfluenceRange
, state: edgeConfluence }
,
edgeWeightControl
{ range: edgeWeightRange
, state: edgeWeight }
]
,
H.div
{ className: "d-flex justify-content-between" }
[
-- change level -- change level
-- file upload -- file upload
-- run demo -- run demo
-- search button -- search button
-- search topics -- search topics
, navItem [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4 labelSizeButton sigmaRef localControls.labelSize
, navItem [ nodeSizeControl { range: nodeSizeRange ,
, state: nodeSize } [] ] -- labels size: 1-4
-- zoom: 0 -100 - calculate ratio nodeSizeControl
, navItem [ multiSelectEnabledButton { state: multiSelectEnabled } [] ] -- toggle multi node selection { range: nodeSizeRange
-- save button , state: nodeSize }
, navItem [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ]
, navItem [ cameraButton { id: graphId
, hyperdataGraph: hyperdataGraph
, session: session
, sigmaRef: sigmaRef
, reloadForest } ]
] ]
] ]
where ]
navItem = RH.li { className: "nav-item" }
-- RH.ul {} [ -- change type button (?) -- H.ul {} [ -- change type button (?)
-- RH.li {} [ centerButton sigmaRef ] -- H.li {} [ centerButton sigmaRef ]
-- , RH.li {} [ pauseForceAtlasButton {state: forceAtlasState} ] -- , H.li {} [ pauseForceAtlasButton {state: forceAtlasState} ]
-- , RH.li {} [ edgesToggleButton {state: showEdges} ] -- , H.li {} [ edgesToggleButton {state: showEdges} ]
-- , RH.li {} [ louvainToggleButton showLouvain ] -- , H.li {} [ louvainToggleButton showLouvain ]
-- , RH.li {} [ edgeConfluenceControl edgeConfluenceRange edgeConfluence ] -- , H.li {} [ edgeConfluenceControl edgeConfluenceRange edgeConfluence ]
-- , RH.li {} [ edgeWeightControl edgeWeightRange edgeWeight ] -- , H.li {} [ edgeWeightControl edgeWeightRange edgeWeight ]
-- -- change level -- -- change level
-- -- file upload -- -- file upload
-- -- run demo -- -- run demo
-- -- search button -- -- search button
-- -- search topics -- -- search topics
-- , RH.li {} [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4 -- , H.li {} [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4
-- , RH.li {} [ nodeSizeControl nodeSizeRange nodeSize ] -- , H.li {} [ nodeSizeControl nodeSizeRange nodeSize ]
-- -- zoom: 0 -100 - calculate ratio -- -- zoom: 0 -100 - calculate ratio
-- , RH.li {} [ multiSelectEnabledButton multiSelectEnabled ] -- toggle multi node selection -- , H.li {} [ multiSelectEnabledButton multiSelectEnabled ] -- toggle multi node selection
-- -- save button -- -- save button
-- , RH.li {} [ nodeSearchControl { graph: graph -- , H.li {} [ nodeSearchControl { graph: graph
-- , multiSelectEnabled: multiSelectEnabled -- , multiSelectEnabled: multiSelectEnabled
-- , selectedNodeIds: selectedNodeIds } ] -- , selectedNodeIds: selectedNodeIds } ]
-- , RH.li {} [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ] -- , H.li {} [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ]
-- , RH.li {} [ cameraButton { id: graphId -- , H.li {} [ cameraButton { id: graphId
-- , hyperdataGraph: hyperdataGraph -- , hyperdataGraph: hyperdataGraph
-- , session: session -- , session: session
-- , sigmaRef: sigmaRef -- , sigmaRef: sigmaRef
...@@ -227,9 +321,8 @@ useGraphControls :: { forceAtlasS :: SigmaxT.ForceAtlasState ...@@ -227,9 +321,8 @@ useGraphControls :: { forceAtlasS :: SigmaxT.ForceAtlasState
, hyperdataGraph :: GET.HyperdataGraph , hyperdataGraph :: GET.HyperdataGraph
, reloadForest :: T2.ReloadS , reloadForest :: T2.ReloadS
, session :: Session , session :: Session
, showTree :: T.Box Boolean
, sidePanel :: T.Box (Maybe (Record GEST.SidePanel)) , sidePanel :: T.Box (Maybe (Record GEST.SidePanel))
, sidePanelState :: T.Box GT.SidePanelState } }
-> R.Hooks (Record Controls) -> R.Hooks (Record Controls)
useGraphControls { forceAtlasS useGraphControls { forceAtlasS
, graph , graph
...@@ -237,9 +330,8 @@ useGraphControls { forceAtlasS ...@@ -237,9 +330,8 @@ useGraphControls { forceAtlasS
, hyperdataGraph , hyperdataGraph
, reloadForest , reloadForest
, session , session
, showTree
, sidePanel , sidePanel
, sidePanelState } = do } = do
edgeConfluence <- T.useBox $ Range.Closed { min: 0.0, max: 1.0 } edgeConfluence <- T.useBox $ Range.Closed { min: 0.0, max: 1.0 }
edgeWeight <- T.useBox $ Range.Closed { edgeWeight <- T.useBox $ Range.Closed {
min: 0.0 min: 0.0
...@@ -253,7 +345,13 @@ useGraphControls { forceAtlasS ...@@ -253,7 +345,13 @@ useGraphControls { forceAtlasS
sigma <- Sigmax.initSigma sigma <- Sigmax.initSigma
sigmaRef <- R.useRef sigma sigmaRef <- R.useRef sigma
{ multiSelectEnabled, removedNodeIds, selectedNodeIds, showControls, sideTab } <- GEST.focusedSidePanel sidePanel { multiSelectEnabled
, removedNodeIds
, selectedNodeIds
, showControls
, sideTab
, showSidebar
} <- GEST.focusedSidePanel sidePanel
pure { edgeConfluence pure { edgeConfluence
, edgeWeight , edgeWeight
...@@ -270,8 +368,7 @@ useGraphControls { forceAtlasS ...@@ -270,8 +368,7 @@ useGraphControls { forceAtlasS
, showControls , showControls
, showEdges , showEdges
, showLouvain , showLouvain
, sidePanelState , showSidebar
, showTree
, sideTab , sideTab
, sigmaRef , sigmaRef
, reloadForest , reloadForest
......
module Gargantext.Components.GraphExplorer where module Gargantext.Components.GraphExplorer.Layout where
import Gargantext.Prelude hiding (max, min) import Gargantext.Prelude hiding (max, min)
import Control.Bind ((=<<))
import DOM.Simple.Types (Element) import DOM.Simple.Types (Element)
import Data.Array as A import Data.Array as A
import Data.FoldableWithIndex (foldMapWithIndex) import Data.FoldableWithIndex (foldMapWithIndex)
...@@ -14,146 +13,97 @@ import Data.Sequence as Seq ...@@ -14,146 +13,97 @@ import Data.Sequence as Seq
import Data.Set as Set import Data.Set as Set
import Data.Tuple (Tuple(..)) import Data.Tuple (Tuple(..))
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Graph as Graph import Gargantext.Components.Bootstrap as B
import Gargantext.Components.GraphExplorer.Resources as Graph
import Gargantext.Components.GraphExplorer.Controls as Controls import Gargantext.Components.GraphExplorer.Controls as Controls
import Gargantext.Components.GraphExplorer.Sidebar as GES
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.TopBar as GETB import Gargantext.Components.GraphExplorer.TopBar as GETB
import Gargantext.Components.GraphExplorer.Types as GET import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Config.REST (AffRESTError, logRESTError) import Gargantext.Config (defaultFrontends)
import Gargantext.Data.Louvain as Louvain import Gargantext.Data.Louvain as Louvain
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Hooks.Sigmax.Sigma (startForceAtlas2)
import Gargantext.Hooks.Sigmax.Types as SigmaxT import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Routes (SessionRoute(NodeAPI)) import Gargantext.Sessions (Session)
import Gargantext.Sessions (Session, get) import Gargantext.Types as GT
import Gargantext.Types as Types import Gargantext.Types as Types
import Gargantext.Utils ((?))
import Gargantext.Utils.Range as Range import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import Math as Math import Math as Math
import Partial.Unsafe (unsafePartial) import Partial.Unsafe (unsafePartial)
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as RH import Reactix.DOM.HTML as H
import Record as Record
import Record.Extra as RX
import Toestand as T import Toestand as T
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer"
type BaseProps =
( boxes :: Boxes
, graphId :: GET.GraphId
)
type LayoutProps =
( session :: Session
| BaseProps )
type Props = type Props =
( graph :: SigmaxT.SGraph
, hyperdataGraph :: GET.HyperdataGraph
| LayoutProps
)
type GraphWriteProps =
( mMetaData' :: Maybe GET.MetaData ( mMetaData' :: Maybe GET.MetaData
| Props , graph :: SigmaxT.SGraph
, hyperdataGraph :: GET.HyperdataGraph
, session :: Session
, boxes :: Boxes
, graphId :: GET.GraphId
) )
type LayoutWithKeyProps = here :: R2.Here
( key :: String here = R2.here "Gargantext.Components.GraphExplorer.Layout"
| LayoutProps )
--------------------------------------------------------------
explorerLayoutWithKey :: R2.Component LayoutWithKeyProps
explorerLayoutWithKey = R.createElement explorerLayoutWithKeyCpt
explorerLayoutWithKeyCpt :: R.Component LayoutWithKeyProps
explorerLayoutWithKeyCpt = here.component "explorerLayoutWithKey" cpt where
cpt { boxes, graphId, session } _ = do
pure $ explorerLayout { boxes, graphId, session } []
explorerLayout :: R2.Component LayoutProps
explorerLayout = R.createElement explorerLayoutCpt
explorerLayoutCpt :: R.Component LayoutProps
explorerLayoutCpt = here.component "explorerLayout" cpt where
cpt props@{ boxes: { graphVersion }, graphId, session } _ = do
graphVersion' <- T.useLive T.unequal graphVersion
useLoader { errorHandler
, loader: getNodes session graphVersion'
, path: graphId
, render: handler }
where
errorHandler = logRESTError here "[explorerLayout]"
handler loaded@(GET.HyperdataGraph { graph: hyperdataGraph }) =
explorerWriteGraph (Record.merge props { graph, hyperdataGraph: loaded, mMetaData' }) []
where
Tuple mMetaData' graph = convert hyperdataGraph
explorerWriteGraph :: R2.Component GraphWriteProps
explorerWriteGraph = R.createElement explorerWriteGraphCpt
explorerWriteGraphCpt :: R.Component GraphWriteProps
explorerWriteGraphCpt = here.component "explorerWriteGraph" cpt where
cpt props@{ boxes: { sidePanelGraph }
, graph
, mMetaData' } _ = do
mTopBarHost <- R.unsafeHooksEffect $ R2.getElementById "portal-topbar"
R.useEffectOnce' $ do
T.write_ (Just { mGraph: Just graph
, mMetaData: mMetaData'
, multiSelectEnabled: false
, removedNodeIds: Set.empty
, selectedNodeIds: Set.empty
, showControls: false
, sideTab: GET.SideTabLegend }) sidePanelGraph
pure $ R.fragment layout :: R2.Leaf Props
[ layout = R2.leaf layoutCpt
explorer (RX.pick props :: Record Props) []
,
R2.createPortal' mTopBarHost
[
GETB.topBar { boxes: props.boxes }
]
]
-------------------------------------------------------------- layoutCpt :: R.Component Props
explorer :: R2.Component Props layoutCpt = here.component "explorerWriteGraph" cpt where
explorer = R.createElement explorerCpt cpt props@{ boxes
explorerCpt :: R.Component Props
explorerCpt = here.component "explorer" cpt
where
cpt props@{ boxes: { graphVersion, handed, reloadForest, showTree, sidePanelGraph, sidePanelState }
, graph , graph
, mMetaData'
, graphId , graphId
, hyperdataGraph
, session , session
, hyperdataGraph
} _ = do } _ = do
{ mMetaData } <- GEST.focusedSidePanel sidePanelGraph
_graphVersion' <- T.useLive T.unequal graphVersion
handed' <- T.useLive T.unequal handed
mMetaData' <- T.useLive T.unequal mMetaData
let startForceAtlas = maybe true (\(GET.MetaData { startForceAtlas: sfa }) -> sfa) mMetaData' -- Computed
-----------------
let
topBarPortalKey = "portal-topbar::" <> show graphId
startForceAtlas = maybe true
(\(GET.MetaData { startForceAtlas: sfa }) -> sfa) mMetaData'
let forceAtlasS = if startForceAtlas forceAtlasS = if startForceAtlas
then SigmaxT.InitialRunning then SigmaxT.InitialRunning
else SigmaxT.InitialStopped else SigmaxT.InitialStopped
_dataRef <- R.useRef graph -- States
-----------------
{ mMetaData: mMetaDataBox
, showSidebar
} <- GEST.focusedSidePanel boxes.sidePanelGraph
_graphVersion' <- T.useLive T.unequal boxes.graphVersion
showSidebar' <- R2.useLive' showSidebar
-- _dataRef <- R.useRef graph
graphRef <- R.useRef null graphRef <- R.useRef null
controls <- Controls.useGraphControls { forceAtlasS
-- Hooks
-----------------
controls <- Controls.useGraphControls
{ forceAtlasS
, graph , graph
, graphId , graphId
, hyperdataGraph , hyperdataGraph
, reloadForest , reloadForest: boxes.reloadForest
, session , session
, showTree , sidePanel: boxes.sidePanelGraph
, sidePanel: sidePanelGraph }
, sidePanelState }
mTopBarHost <- R.unsafeHooksEffect $ R2.getElementById "portal-topbar"
showControls' <- R2.useLive' controls.showControls
-- graphVersionRef <- R.useRef graphVersion' -- graphVersionRef <- R.useRef graphVersion'
-- R.useEffect' $ do -- R.useEffect' $ do
...@@ -175,28 +125,78 @@ explorerCpt = here.component "explorer" cpt ...@@ -175,28 +125,78 @@ explorerCpt = here.component "explorer" cpt
-- T.write_ Graph.Init controls.graphStage -- T.write_ Graph.Init controls.graphStage
-- T.write_ Types.InitialClosed controls.sidePanelState -- T.write_ Types.InitialClosed controls.sidePanelState
-- Render
-----------------
pure $ pure $
RH.div { className: "graph-meta-container" }
[ RH.div { className: "graph-container" } H.div
[ RH.div { className: "container-fluid " <> hClass handed' } { className: "graph-layout" }
[ RH.div { id: "controls-container" } [ Controls.controls controls [] ] [
, RH.div { className: "row graph-row" } -- Topbar
[ RH.div { ref: graphRef, id: "graph-view", className: "col-md-12" } [] R2.createPortal' mTopBarHost
, graphView { boxes: props.boxes [
R2.fragmentWithKey topBarPortalKey
[
GETB.topBar
{ sidePanelGraph: props.boxes.sidePanelGraph }
]
]
,
-- Sidebar
H.div
{ className: "graph-layout__sidebar"
-- @XXX: ReactJS lack of "keep-alive" feature workaround solution
-- @link https://github.com/facebook/react/issues/12039
, style: { display: showSidebar' == GT.Opened ? "block" $ "none" }
}
[
case mMetaData' of
Nothing ->
B.caveat
{}
[ H.text "No meta data has been found for this node." ]
Just metaData ->
GES.sidebar
{ boxes
, frontends: defaultFrontends
, graph
, graphId
, metaData
, session
}
]
,
-- Toolbar
H.div
{ className: "graph-layout__toolbar"
-- @XXX: ReactJS lack of "keep-alive" feature workaround solution
-- @link https://github.com/facebook/react/issues/12039
, style: { display: showControls' ? "block" $ "none" }
}
[
Controls.controls controls []
]
,
-- Content
H.div
{ ref: graphRef
, className: "graph-layout__content"
}
[
graphView
{ boxes: props.boxes
, controls , controls
, elRef: graphRef , elRef: graphRef
, graph , graph
, hyperdataGraph , hyperdataGraph
, mMetaData , mMetaData: mMetaDataBox
} [] }
]
]
] ]
] ]
hClass h = case h of --------------------------------------------------------------
Types.LeftHanded -> "lefthanded"
Types.RightHanded -> "righthanded"
type GraphProps = type GraphProps =
( boxes :: Boxes ( boxes :: Boxes
...@@ -207,8 +207,8 @@ type GraphProps = ...@@ -207,8 +207,8 @@ type GraphProps =
, mMetaData :: T.Box (Maybe GET.MetaData) , mMetaData :: T.Box (Maybe GET.MetaData)
) )
graphView :: R2.Component GraphProps graphView :: R2.Leaf GraphProps
graphView = R.createElement graphViewCpt graphView = R2.leaf graphViewCpt
graphViewCpt :: R.Component GraphProps graphViewCpt :: R.Component GraphProps
graphViewCpt = here.component "graphView" cpt graphViewCpt = here.component "graphView" cpt
where where
...@@ -217,7 +217,7 @@ graphViewCpt = here.component "graphView" cpt ...@@ -217,7 +217,7 @@ graphViewCpt = here.component "graphView" cpt
, elRef , elRef
, graph , graph
, hyperdataGraph: GET.HyperdataGraph { mCamera } , hyperdataGraph: GET.HyperdataGraph { mCamera }
, mMetaData } _children = do , mMetaData } _ = do
edgeConfluence' <- T.useLive T.unequal controls.edgeConfluence edgeConfluence' <- T.useLive T.unequal controls.edgeConfluence
edgeWeight' <- T.useLive T.unequal controls.edgeWeight edgeWeight' <- T.useLive T.unequal controls.edgeWeight
mMetaData' <- T.useLive T.unequal mMetaData mMetaData' <- T.useLive T.unequal mMetaData
...@@ -249,7 +249,10 @@ graphViewCpt = here.component "graphView" cpt ...@@ -249,7 +249,10 @@ graphViewCpt = here.component "graphView" cpt
R.useEffect1' multiSelectEnabled' $ do R.useEffect1' multiSelectEnabled' $ do
R.setRef multiSelectEnabledRef multiSelectEnabled' R.setRef multiSelectEnabledRef multiSelectEnabled'
pure $ Graph.graph { boxes pure $
Graph.graph
{ boxes
, elRef , elRef
, forceAtlas2Settings: Graph.forceAtlas2Settings , forceAtlas2Settings: Graph.forceAtlas2Settings
, graph , graph
...@@ -264,6 +267,8 @@ graphViewCpt = here.component "graphView" cpt ...@@ -264,6 +267,8 @@ graphViewCpt = here.component "graphView" cpt
, transformedGraph , transformedGraph
} [] } []
--------------------------------------------------------
convert :: GET.GraphData -> Tuple (Maybe GET.MetaData) SigmaxT.SGraph convert :: GET.GraphData -> Tuple (Maybe GET.MetaData) SigmaxT.SGraph
convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges} convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges}
where where
...@@ -310,6 +315,8 @@ convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges} ...@@ -310,6 +315,8 @@ convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges}
targetNode = unsafePartial $ fromJust $ Map.lookup e.target nodesMap targetNode = unsafePartial $ fromJust $ Map.lookup e.target nodesMap
color = sourceNode.color color = sourceNode.color
--------------------------------------------------------------
-- | See sigmajs/plugins/sigma.renderers.customShapes/shape-library.js -- | See sigmajs/plugins/sigma.renderers.customShapes/shape-library.js
modeGraphType :: Types.Mode -> String modeGraphType :: Types.Mode -> String
modeGraphType Types.Authors = "square" modeGraphType Types.Authors = "square"
...@@ -317,12 +324,8 @@ modeGraphType Types.Institutes = "equilateral" ...@@ -317,12 +324,8 @@ modeGraphType Types.Institutes = "equilateral"
modeGraphType Types.Sources = "star" modeGraphType Types.Sources = "star"
modeGraphType Types.Terms = "def" modeGraphType Types.Terms = "def"
--------------------------------------------------------------
getNodes :: Session -> T2.Reload -> GET.GraphId -> AffRESTError GET.HyperdataGraph
getNodes session graphVersion graphId =
get session $ NodeAPI Types.Graph
(Just graphId)
("?version=" <> (show graphVersion))
type LiveProps = ( type LiveProps = (
edgeConfluence' :: Range.NumberRange edgeConfluence' :: Range.NumberRange
......
module Gargantext.Components.GraphExplorer.Legend module Gargantext.Components.GraphExplorer.Legend
( Props, legend, legendCpt ( Props, legend
) where ) where
import Prelude hiding (map) import Prelude hiding (map)
...@@ -7,31 +7,39 @@ import Prelude hiding (map) ...@@ -7,31 +7,39 @@ import Prelude hiding (map)
import Data.Sequence (Seq) import Data.Sequence (Seq)
import Data.Traversable (foldMap) import Data.Traversable (foldMap)
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as RH import Reactix.DOM.HTML as H
import Gargantext.Components.GraphExplorer.Types (Legend(..), intColor) import Gargantext.Components.GraphExplorer.Types (Legend(..), intColor)
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Legend" here = R2.here "Gargantext.Components.GraphExplorer.Legend"
type Props = ( items :: Seq Legend ) type Props = ( items :: Seq Legend )
legend :: Record Props -> R.Element legend :: R2.Leaf Props
legend props = R.createElement legendCpt props [] legend = R2.leaf legendCpt
legendCpt :: R.Component Props legendCpt :: R.Component Props
legendCpt = here.component "legend" cpt legendCpt = here.component "legend" cpt where
where cpt { items } _ = pure $
cpt {items} _ = pure $ RH.div {} [foldMap entry items]
H.ul
entry :: Legend -> R.Element { className: "graph-legend" }
entry (Legend {id_, label}) = [
RH.p {} flip foldMap items \(Legend { id_, label }) ->
[ RH.span { style: { width : 10
, height: 10 H.li
, backgroundColor: intColor id_ { className: "graph-legend__item" }
, display: "inline-block" [
H.span
{ className: "graph-legend__code"
, style: { backgroundColor: intColor id_ }
} }
} [] []
, RH.text $ " " <> label ,
H.span
{ className: "graph-legend__caption" }
[ H.text label ]
]
] ]
...@@ -18,37 +18,45 @@ import Gargantext.Utils.Reactix as R2 ...@@ -18,37 +18,45 @@ import Gargantext.Utils.Reactix as R2
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.RangeControl" here = R2.here "Gargantext.Components.GraphExplorer.RangeControl"
type Props = ( type Props =
caption :: String ( caption :: String
, sliderProps :: Record RS.Props , sliderProps :: Record RS.Props
) )
rangeControl :: R2.Component Props rangeControl :: R2.Leaf Props
rangeControl = R.createElement rangeControlCpt rangeControl = R2.leaf rangeControlCpt
rangeControlCpt :: R.Component Props rangeControlCpt :: R.Component Props
rangeControlCpt = here.component "rangeButton" cpt rangeControlCpt = here.component "rangeButton" cpt
where where
cpt {caption, sliderProps} _ = do cpt {caption, sliderProps} _ = pure $
pure $
H.span {className: "range text-center"} H.span
[ H.label {} [ R2.small {} [ H.text caption ] ] { className: "range-control" }
, RS.rangeSlider sliderProps [
H.label
{ className: "range-control__label" }
[ H.text caption ]
,
RS.rangeSlider sliderProps
] ]
type EdgeConfluenceControlProps = ( ----------------------------------------
range :: Range.NumberRange
type EdgeConfluenceControlProps =
( range :: Range.NumberRange
, state :: T.Box Range.NumberRange , state :: T.Box Range.NumberRange
) )
edgeConfluenceControl :: R2.Component EdgeConfluenceControlProps edgeConfluenceControl :: R2.Leaf EdgeConfluenceControlProps
edgeConfluenceControl = R.createElement edgeConfluenceControlCpt edgeConfluenceControl = R2.leaf edgeConfluenceControlCpt
edgeConfluenceControlCpt :: R.Component EdgeConfluenceControlProps edgeConfluenceControlCpt :: R.Component EdgeConfluenceControlProps
edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt
where where
cpt { range: Range.Closed { min, max } cpt { range: Range.Closed { min, max }
, state } _ = do , state
} _ = do
state' <- T.useLive T.unequal state state' <- T.useLive T.unequal state
pure $ rangeControl { pure $ rangeControl {
...@@ -62,21 +70,24 @@ edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt ...@@ -62,21 +70,24 @@ edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt
, height: 5.0 , height: 5.0
, onChange: \rng -> T.write_ rng state , onChange: \rng -> T.write_ rng state
} }
} [] }
--------------------------------------
type EdgeWeightControlProps = ( type EdgeWeightControlProps =
range :: Range.NumberRange ( range :: Range.NumberRange
, state :: T.Box Range.NumberRange , state :: T.Box Range.NumberRange
) )
edgeWeightControl :: R2.Component EdgeWeightControlProps edgeWeightControl :: R2.Leaf EdgeWeightControlProps
edgeWeightControl = R.createElement edgeWeightControlCpt edgeWeightControl = R2.leaf edgeWeightControlCpt
edgeWeightControlCpt :: R.Component EdgeWeightControlProps edgeWeightControlCpt :: R.Component EdgeWeightControlProps
edgeWeightControlCpt = here.component "edgeWeightControl" cpt edgeWeightControlCpt = here.component "edgeWeightControl" cpt
where where
cpt { range: Range.Closed { min, max } cpt { range: Range.Closed { min, max }
, state } _ = do , state
} _ = do
state' <- T.useLive T.unequal state state' <- T.useLive T.unequal state
pure $ rangeControl { pure $ rangeControl {
...@@ -90,21 +101,24 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt ...@@ -90,21 +101,24 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt
, height: 5.0 , height: 5.0
, onChange: \rng -> T.write_ rng state , onChange: \rng -> T.write_ rng state
} }
} [] }
--------------------------------------
type NodeSideControlProps = ( type NodeSideControlProps =
range :: Range.NumberRange ( range :: Range.NumberRange
, state :: T.Box Range.NumberRange , state :: T.Box Range.NumberRange
) )
nodeSizeControl :: R2.Component NodeSideControlProps nodeSizeControl :: R2.Leaf NodeSideControlProps
nodeSizeControl = R.createElement nodeSizeControlCpt nodeSizeControl = R2.leaf nodeSizeControlCpt
nodeSizeControlCpt :: R.Component NodeSideControlProps nodeSizeControlCpt :: R.Component NodeSideControlProps
nodeSizeControlCpt = here.component "nodeSizeControl" cpt nodeSizeControlCpt = here.component "nodeSizeControl" cpt
where where
cpt { range: Range.Closed { min, max } cpt { range: Range.Closed { min, max }
, state } _ = do , state
} _ = do
state' <- T.useLive T.unequal state state' <- T.useLive T.unequal state
pure $ rangeControl { pure $ rangeControl {
...@@ -118,4 +132,4 @@ nodeSizeControlCpt = here.component "nodeSizeControl" cpt ...@@ -118,4 +132,4 @@ nodeSizeControlCpt = here.component "nodeSizeControl" cpt
, height: 5.0 , height: 5.0
, onChange: \rng -> T.write_ rng state , onChange: \rng -> T.write_ rng state
} }
} [] }
module Gargantext.Components.GraphExplorer.Resources
-- ( graph, graphCpt
-- , sigmaSettings, SigmaSettings, SigmaOptionalSettings
-- , forceAtlas2Settings, ForceAtlas2Settings, ForceAtlas2OptionalSettings
-- )
where
import Gargantext.Prelude
import DOM.Simple (window)
import DOM.Simple.Types (Element)
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable)
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Components.Themes (darksterTheme)
import Gargantext.Components.Themes as Themes
import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Sigma as Sigma
import Gargantext.Hooks.Sigmax.Types as SigmaxTypes
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as RH
import Record (merge)
import Record as Record
import Toestand as T
here :: R2.Here
here = R2.here "Gargantext.Components.Graph"
data Stage = Init | Ready | Cleanup
derive instance Generic Stage _
derive instance Eq Stage
type Props sigma forceatlas2 =
( boxes :: Boxes
, elRef :: R.Ref (Nullable Element)
, forceAtlas2Settings :: forceatlas2
, graph :: SigmaxTypes.SGraph
, mCamera :: Maybe GET.Camera
, multiSelectEnabledRef :: R.Ref Boolean
, selectedNodeIds :: T.Box SigmaxTypes.NodeIds
, showEdges :: T.Box SigmaxTypes.ShowEdgesState
, sigmaRef :: R.Ref Sigmax.Sigma
, sigmaSettings :: sigma
, stage :: T.Box Stage
, startForceAtlas :: Boolean
, transformedGraph :: SigmaxTypes.SGraph
)
graph :: forall s fa2. R2.Component (Props s fa2)
graph = R.createElement graphCpt
graphCpt :: forall s fa2. R.Memo (Props s fa2)
graphCpt = R.memo' $ here.component "graph" cpt where
cpt props@{ elRef
, showEdges
, sigmaRef
, stage } _ = do
showEdges' <- T.useLive T.unequal showEdges
stage' <- T.useLive T.unequal stage
stageHooks (Record.merge { showEdges', stage' } props)
R.useEffectOnce $ do
pure $ do
here.log "[graphCpt (Cleanup)]"
Sigmax.dependOnSigma (R.readRef sigmaRef) "[graphCpt (Cleanup)] no sigma" $ \sigma -> do
Sigma.stopForceAtlas2 sigma
here.log2 "[graphCpt (Cleanup)] forceAtlas stopped for" sigma
Sigma.kill sigma
here.log "[graphCpt (Cleanup)] sigma killed"
-- NOTE: This div is not empty after sigma initializes.
-- When we change state, we make it empty though.
--pure $ RH.div { ref: elRef, style: {height: "95%"} } []
pure $ case R.readNullableRef elRef of
Nothing -> RH.div {} []
Just el -> R.createPortal [] el
stageHooks { elRef
, mCamera
, multiSelectEnabledRef
, selectedNodeIds
, forceAtlas2Settings: fa2
, graph: graph'
, sigmaRef
, stage
, stage': Init
, startForceAtlas
, boxes
} = do
R.useEffectOnce' $ do
let rSigma = R.readRef sigmaRef
case Sigmax.readSigma rSigma of
Nothing -> do
theme <- T.read boxes.theme
eSigma <- Sigma.sigma {settings: sigmaSettings theme}
case eSigma of
Left err -> here.log2 "[graphCpt] error creating sigma" err
Right sig -> do
Sigmax.writeSigma rSigma $ Just sig
Sigmax.dependOnContainer elRef "[graphCpt (Ready)] container not found" $ \c -> do
_ <- Sigma.addRenderer sig {
"type": "canvas"
, container: c
, additionalContexts: ["mouseSelector"]
}
pure unit
Sigmax.refreshData sig $ Sigmax.sigmafy graph'
Sigmax.dependOnSigma (R.readRef sigmaRef) "[graphCpt (Ready)] no sigma" $ \sigma -> do
-- bind the click event only initially, when ref was empty
Sigmax.bindSelectedNodesClick sigma selectedNodeIds multiSelectEnabledRef
_ <- Sigma.bindMouseSelectorPlugin sigma
pure unit
Sigmax.setEdges sig false
-- here.log2 "[graph] startForceAtlas" startForceAtlas
if startForceAtlas then
Sigma.startForceAtlas2 sig fa2
else
Sigma.stopForceAtlas2 sig
case mCamera of
Just (GET.Camera { ratio, x, y }) -> do
Sigma.updateCamera sig { ratio, x, y }
-- Default camera: slightly de-zoom the graph to avoid
-- nodes sticking to the container borders
Nothing ->
Sigma.updateCamera sig { ratio: 1.1, x: 0.0, y: 0.0 }
-- Reload Sigma on Theme changes
_ <- flip T.listen boxes.theme \{ old, new } ->
if (eq old new) then pure unit
else Sigma.proxySetSettings window sig $ sigmaSettings new
pure unit
Just _sig -> do
pure unit
T.write Ready stage
stageHooks { showEdges'
, sigmaRef
, stage': Ready
, transformedGraph
} = do
let tEdgesMap = SigmaxTypes.edgesGraphMap transformedGraph
let tNodesMap = SigmaxTypes.nodesGraphMap transformedGraph
-- TODO Probably this can be optimized to re-mark selected nodes only when they changed
R.useEffect' $ do
Sigmax.dependOnSigma (R.readRef sigmaRef) "[graphCpt (Ready)] no sigma" $ \sigma -> do
Sigmax.performDiff sigma transformedGraph
Sigmax.updateEdges sigma tEdgesMap
Sigmax.updateNodes sigma tNodesMap
let edgesState = not $ SigmaxTypes.edgeStateHidden showEdges'
here.log2 "[graphCpt] edgesState" edgesState
Sigmax.setEdges sigma edgesState
stageHooks _ = pure unit
type SigmaSettings =
( animationsTime :: Number
, autoRescale :: Boolean
, autoResize :: Boolean
, batchEdgesDrawing :: Boolean
, borderSize :: Number
-- , canvasEdgesBatchSize :: Number
-- , clone :: Boolean
-- , defaultEdgeColor :: String
, defaultEdgeHoverColor :: String
, defaultEdgeType :: String
, defaultHoverLabelBGColor :: String
, defaultHoverLabelColor :: String
, defaultLabelColor :: String
-- , defaultLabelHoverColor :: String
, defaultLabelSize :: Number
, defaultNodeBorderColor :: String
, defaultNodeColor :: String
-- , defaultNodeHoverColor :: String
-- , defaultNodeType :: String
, doubleClickEnabled :: Boolean
-- , doubleClickTimeout :: Number
-- , doubleClickZoomDuration :: Number
-- , doubleClickZoomingRatio :: Number
-- , doubleTapTimeout :: Number
-- , dragTimeout :: Number
, drawEdgeLabels :: Boolean
, drawEdges :: Boolean
, drawLabels :: Boolean
, drawNodes :: Boolean
-- , edgeColor :: String
, edgeHoverColor :: String
, edgeHoverExtremities :: Boolean
, edgeHoverPrecision :: Number
, edgeHoverSizeRatio :: Number
-- , edgesPowRatio :: Number
-- , enableCamera :: Boolean
, enableEdgeHovering :: Boolean
, enableHovering :: Boolean
-- , eventsEnabled :: Boolean
, font :: String
, fontStyle :: String
, hideEdgesOnMove :: Boolean
-- , hoverFont :: String
-- , hoverFontStyle :: String
-- , immutable :: Boolean
-- , labelColor :: String
-- , labelHoverBGColor :: String
-- , labelHoverColor :: String
-- , labelHoverShadow :: String
-- , labelHoverShadowColor :: String
, labelSize :: String
, labelSizeRatio :: Number
, labelThreshold :: Number
, maxEdgeSize :: Number
, maxNodeSize :: Number
-- , minArrowSize :: Number
, minEdgeSize :: Number
, minNodeSize :: Number
, mouseEnabled :: Boolean
-- , mouseInertiaDuration :: Number
-- , mouseInertiaRatio :: Number
, mouseSelectorSize :: Number
-- , mouseWheelEnabled :: Boolean
, mouseZoomDuration :: Number
, nodeBorderColor :: String
-- , nodeHoverColor :: String
--, nodesPowRatio :: Number
, rescaleIgnoreSize :: Boolean
-- , scalingMode :: String
-- , sideMargin :: Number
, singleHover :: Boolean
-- , skipErrors :: Boolean
, touchEnabled :: Boolean
-- , touchInertiaDuration :: Number
-- , touchInertiaRatio :: Number
, twBorderGreyColor :: String
, twEdgeDefaultOpacity :: Number
, twEdgeGreyColor :: String
, twNodeRendBorderColor :: String
, twNodeRendBorderSize :: Number
, twNodesGreyOpacity :: Number
, twSelectedColor :: String
, verbose :: Boolean
-- , webglEdgesBatchSize :: Number
-- , webglOversamplingRatio :: Number
, zoomMax :: Number
, zoomMin :: Number
, zoomingRatio :: Number
)
-- not selected <=> (1-greyness)
-- selected nodes <=> special label
sigmaSettings :: Themes.Theme -> {|SigmaSettings}
sigmaSettings theme =
{ animationsTime : 30000.0
, autoRescale : true
, autoResize : true
, batchEdgesDrawing : true
, borderSize : 1.0 -- for ex, bigger border when hover
, defaultEdgeHoverColor : "#f00"
, defaultEdgeType : "curve" -- 'curve' or 'line' (curve iff ourRendering)
-- , defaultHoverLabelBGColor : "#fff"
-- , defaultHoverLabelColor : "#000"
-- , defaultLabelColor : "#000" -- labels text color
, defaultLabelSize : 15.0 -- (old tina: showLabelsIfZoom)
, defaultNodeBorderColor : "#000" -- <- if nodeBorderColor = 'default'
, defaultNodeColor : "#FFF"
, doubleClickEnabled : false -- indicates whether or not the graph can be zoomed on double-click
, drawEdgeLabels : true
, drawEdges : true
, drawLabels : true
, drawNodes : true
, enableEdgeHovering : false
, edgeHoverExtremities : true
, edgeHoverColor : "edge"
, edgeHoverPrecision : 2.0
, edgeHoverSizeRatio : 2.0
, enableHovering : true
, font : "arial"
, fontStyle : ""
, hideEdgesOnMove : true
, labelSize : "proportional" -- alt : proportional, fixed
-- , labelSize : "fixed"
, labelSizeRatio : 2.0 -- label size in ratio of node size
, labelThreshold : 9.0 -- 5.0 for more labels -- min node cam size to start showing label
, maxEdgeSize : 1.0
, maxNodeSize : 10.0
, minEdgeSize : 0.5 -- in fact used in tina as edge size
, minNodeSize : 1.0
, mouseEnabled : true
, mouseSelectorSize : 15.0
, mouseZoomDuration : 150.0
, nodeBorderColor : "default" -- choices: "default" color vs. "node" color
--, nodesPowRatio : 10.8
, rescaleIgnoreSize : false
, singleHover : true
, touchEnabled : true
, twBorderGreyColor : "rgba(100, 100, 100, 0.9)"
, twEdgeDefaultOpacity : 0.4 -- initial opacity added to src/tgt colors
, twEdgeGreyColor : "rgba(100, 100, 100, 0.25)"
, twNodeRendBorderColor : "#FFF"
, twNodeRendBorderSize : 2.5 -- node borders (only iff ourRendering)
, twNodesGreyOpacity : 5.5 -- smaller value: more grey
, twSelectedColor : "node" -- "node" for a label bg like the node color, "default" for white background
, verbose : true
, zoomMax : 1.7
, zoomMin : 0.0
, zoomingRatio : 1.4
} `merge` themeSettings theme
where
themeSettings t
| eq t darksterTheme =
{ defaultHoverLabelBGColor: "#FFF"
, defaultHoverLabelColor : "#000"
, defaultLabelColor: "#FFF"
}
| otherwise =
{ defaultHoverLabelBGColor: "#FFF"
, defaultHoverLabelColor : "#000"
, defaultLabelColor: "#000"
}
type ForceAtlas2Settings =
( adjustSizes :: Boolean
, barnesHutOptimize :: Boolean
-- , barnesHutTheta :: Number
, batchEdgesDrawing :: Boolean
, edgeWeightInfluence :: Number
-- , fixedY :: Boolean
, hideEdgesOnMove :: Boolean
, gravity :: Number
, includeHiddenEdges :: Boolean
, includeHiddenNodes :: Boolean
, iterationsPerRender :: Number
, linLogMode :: Boolean
, outboundAttractionDistribution :: Boolean
, scalingRatio :: Number
, skipHidden :: Boolean
, slowDown :: Number
, startingIterations :: Number
, strongGravityMode :: Boolean
-- , timeout :: Number
-- , worker :: Boolean
)
forceAtlas2Settings :: {|ForceAtlas2Settings}
forceAtlas2Settings =
{ adjustSizes : true
, barnesHutOptimize : true
, batchEdgesDrawing : true
, edgeWeightInfluence : 1.0
-- fixedY : false
, gravity : 1.0
, hideEdgesOnMove : true
, includeHiddenEdges : false
, includeHiddenNodes : true
, iterationsPerRender : 100.0 -- 10.0
, linLogMode : false -- false
, outboundAttractionDistribution : false
, scalingRatio : 1000.0
, skipHidden : false
, slowDown : 1.0
, startingIterations : 10.0
, strongGravityMode : false
}
...@@ -4,10 +4,11 @@ module Gargantext.Components.GraphExplorer.Search ...@@ -4,10 +4,11 @@ module Gargantext.Components.GraphExplorer.Search
import Prelude import Prelude
import DOM.Simple.Console (log2) import DOM.Simple.Console (log2)
import Data.Foldable (foldl) import Data.Foldable (foldl, intercalate)
import Data.Sequence as Seq import Data.Sequence as Seq
import Data.Set as Set import Data.Set as Set
import Effect (Effect) import Effect (Effect)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.InputWithAutocomplete (inputWithAutocomplete) import Gargantext.Components.InputWithAutocomplete (inputWithAutocomplete)
import Gargantext.Hooks.Sigmax.Types as SigmaxT import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Utils (queryMatchesLabel) import Gargantext.Utils (queryMatchesLabel)
...@@ -23,6 +24,7 @@ type Props = ( ...@@ -23,6 +24,7 @@ type Props = (
graph :: SigmaxT.SGraph graph :: SigmaxT.SGraph
, multiSelectEnabled :: T.Box Boolean , multiSelectEnabled :: T.Box Boolean
, selectedNodeIds :: T.Box SigmaxT.NodeIds , selectedNodeIds :: T.Box SigmaxT.NodeIds
, className :: String
) )
-- | Whether a node matches a search string -- | Whether a node matches a search string
...@@ -37,28 +39,43 @@ searchNodes :: String -> Seq.Seq (Record SigmaxT.Node) -> Seq.Seq (Record Sigmax ...@@ -37,28 +39,43 @@ searchNodes :: String -> Seq.Seq (Record SigmaxT.Node) -> Seq.Seq (Record Sigmax
searchNodes "" _ = Seq.empty searchNodes "" _ = Seq.empty
searchNodes s nodes = Seq.filter (nodeMatchesSearch s) nodes searchNodes s nodes = Seq.filter (nodeMatchesSearch s) nodes
nodeSearchControl :: R2.Component Props nodeSearchControl :: R2.Leaf Props
nodeSearchControl = R.createElement nodeSearchControlCpt nodeSearchControl = R2.leaf nodeSearchControlCpt
nodeSearchControlCpt :: R.Component Props nodeSearchControlCpt :: R.Component Props
nodeSearchControlCpt = here.component "nodeSearchControl" cpt nodeSearchControlCpt = here.component "nodeSearchControl" cpt
where where
cpt { graph, multiSelectEnabled, selectedNodeIds } _ = do cpt props@{ graph, multiSelectEnabled, selectedNodeIds } _ = do
search <- T.useBox "" search <- T.useBox ""
search' <- T.useLive T.unequal search search' <- T.useLive T.unequal search
multiSelectEnabled' <- T.useLive T.unequal multiSelectEnabled multiSelectEnabled' <- T.useLive T.unequal multiSelectEnabled
let doSearch s = triggerSearch graph s multiSelectEnabled' selectedNodeIds let doSearch s = triggerSearch graph s multiSelectEnabled' selectedNodeIds
pure $ R.fragment pure $
[ inputWithAutocomplete { autocompleteSearch: autocompleteSearch graph
, classes: "mx-2" H.form
{ className: intercalate " "
[ "graph-node-search"
, props.className
]
}
[
inputWithAutocomplete
{ autocompleteSearch: autocompleteSearch graph
, onAutocompleteClick: doSearch , onAutocompleteClick: doSearch
, onEnterPress: doSearch , onEnterPress: doSearch
, state: search } [] , classes: ""
, H.div { className: "btn input-group-addon" , state: search
, on: { click: \_ -> doSearch search' }
} }
[ H.span { className: "fa fa-search" } [] ] ,
B.button
{ callback: \_ -> doSearch search'
, type: "submit"
, className: "graph-node-search__submit"
}
[
B.icon { name: "search"}
]
] ]
autocompleteSearch :: SigmaxT.SGraph -> String -> Array String autocompleteSearch :: SigmaxT.SGraph -> String -> Array String
......
module Gargantext.Components.GraphExplorer.Sidebar module Gargantext.Components.GraphExplorer.Sidebar
-- (Props, sidebar) ( Props, sidebar
where , Common
) where
import Gargantext.Prelude import Gargantext.Prelude
import Control.Parallel (parTraverse) import Control.Parallel (parTraverse)
import Data.Array (head, last, concat) import Data.Array (concat, head, last, mapWithIndex)
import Data.Array as A import Data.Array as A
import Data.Either (Either(..)) import Data.Either (Either(..))
import Data.Foldable (intercalate)
import Data.Foldable as F import Data.Foldable as F
import Data.Int (fromString) import Data.Int (fromString)
import Data.Map as Map import Data.Map as Map
import Data.Maybe (Maybe(..), fromJust) import Data.Maybe (Maybe(..), fromJust)
import Data.Sequence as Seq import Data.Sequence as Seq
import Data.Set as Set import Data.Set as Set
import Data.Tuple.Nested ((/\))
import Effect (Effect) import Effect (Effect)
import Effect.Aff (launchAff_) import Effect.Aff (launchAff_)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..))
import Gargantext.Components.GraphExplorer.Legend as Legend import Gargantext.Components.GraphExplorer.Legend as Legend
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.Types as GET import Gargantext.Components.GraphExplorer.Types as GET
...@@ -32,13 +37,13 @@ import Gargantext.Ends (Frontends) ...@@ -32,13 +37,13 @@ import Gargantext.Ends (Frontends)
import Gargantext.Hooks.Sigmax.Types as SigmaxT import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Sessions (Session) import Gargantext.Sessions (Session)
import Gargantext.Types (CTabNgramType, FrontendError(..), NodeID, TabSubType(..), TabType(..), TermList(..), modeTabType) import Gargantext.Types (CTabNgramType, FrontendError(..), NodeID, TabSubType(..), TabType(..), TermList(..), modeTabType)
import Gargantext.Utils (nbsp)
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2 import Gargantext.Utils.Toestand as T2
import Math as Math import Math as Math
import Partial.Unsafe (unsafePartial) import Partial.Unsafe (unsafePartial)
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
import Reactix.DOM.HTML as RH
import Record as Record import Record as Record
import Record.Extra as RX import Record.Extra as RX
import Toestand as T import Toestand as T
...@@ -59,247 +64,439 @@ type Props = ( ...@@ -59,247 +64,439 @@ type Props = (
| Common | Common
) )
sidebar :: R2.Component Props sidebar :: R2.Leaf Props
sidebar = R.createElement sidebarCpt sidebar = R2.leaf sidebarCpt
sidebarCpt :: R.Component Props sidebarCpt :: R.Component Props
sidebarCpt = here.component "sidebar" cpt sidebarCpt = here.component "sidebar" cpt
where where
cpt props@{ boxes: { sidePanelGraph } } _ = do cpt props@{ boxes: { sidePanelGraph } } _ = do
-- States
{ sideTab } <- GEST.focusedSidePanel sidePanelGraph { sideTab } <- GEST.focusedSidePanel sidePanelGraph
sideTab' <- T.useLive T.unequal sideTab sideTab' <- T.useLive T.unequal sideTab
pure $ RH.div { id: "sp-container" } -- Computed
[ sideTabNav { sideTab let
, sideTabs: [GET.SideTabLegend, GET.SideTabData, GET.SideTabCommunity] } [] sideTabs =
, case sideTab' of [ GET.SideTabLegend
GET.SideTabLegend -> sideTabLegend sideTabProps [] , GET.SideTabData
GET.SideTabData -> sideTabData sideTabProps [] , GET.SideTabCommunity
GET.SideTabCommunity -> sideTabCommunity sideTabProps []
] ]
where
sideTabProps = RX.pick props :: Record Props
type SideTabNavProps = ( sideTabProps = (RX.pick props :: Record Props)
sideTab :: T.Box GET.SideTab
, sideTabs :: Array GET.SideTab
)
sideTabNav :: R2.Component SideTabNavProps -- Render
sideTabNav = R.createElement sideTabNavCpt pure $
sideTabNavCpt :: R.Component SideTabNavProps
sideTabNavCpt = here.component "sideTabNav" cpt
where
cpt { sideTab, sideTabs } _ = do
sideTab' <- T.useLive T.unequal sideTab
pure $ R.fragment [ H.div { className: "text-primary center"} [H.text ""] H.div
, H.div { className: "nav nav-tabs"} (liItem sideTab' <$> sideTabs) { className: "graph-sidebar" }
-- , H.div {className: "center"} [ H.text "Doc sideTabs"] [
-- Menu
B.tabs
{ value: sideTab'
, list: sideTabs
, callback: flip T.write_ sideTab
}
,
case sideTab' of
GET.SideTabLegend -> sideTabLegend sideTabProps
GET.SideTabData -> sideTabData sideTabProps
GET.SideTabCommunity -> sideTabCommunity sideTabProps
] ]
where
liItem :: GET.SideTab -> GET.SideTab -> R.Element ------------------------------------------------------------
liItem sideTab' tab =
H.div { className : "nav-item nav-link" sideTabLegend :: R2.Leaf Props
<> if tab == sideTab' sideTabLegend = R2.leaf sideTabLegendCpt
then " active"
else ""
, on: { click: \_ -> T.write_ tab sideTab }
} [ H.text $ show tab ]
sideTabLegend :: R2.Component Props
sideTabLegend = R.createElement sideTabLegendCpt
sideTabLegendCpt :: R.Component Props sideTabLegendCpt :: R.Component Props
sideTabLegendCpt = here.component "sideTabLegend" cpt sideTabLegendCpt = here.component "sideTabLegend" cpt
where where
cpt { metaData: GET.MetaData { legend } } _ = do cpt { metaData: GET.MetaData { legend } } _ = pure $
pure $ H.div {}
[ Legend.legend { items: Seq.fromFoldable legend } H.div
, documentation EN { className: "graph-sidebar__legend-tab" }
[
Legend.legend
{ items: Seq.fromFoldable legend }
,
H.hr {}
,
documentation EN
] ]
sideTabData :: R2.Component Props ------------------------------------------------------------
sideTabData = R.createElement sideTabDataCpt
sideTabData :: R2.Leaf Props
sideTabData = R2.leaf sideTabDataCpt
sideTabDataCpt :: R.Component Props sideTabDataCpt :: R.Component Props
sideTabDataCpt = here.component "sideTabData" cpt sideTabDataCpt = here.component "sideTabData" cpt
where where
cpt props@{ boxes: { sidePanelGraph } } _ = do cpt props@{ boxes: { sidePanelGraph } } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph { selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ RH.div {} -- Computed
[ selectedNodes (Record.merge { nodesMap: SigmaxT.nodesGraphMap props.graph } props) [] let
, neighborhood props [] hasSelection = not $ Set.isEmpty selectedNodeIds'
, RH.div { className: "col-md-12", id: "query" }
[ query { frontends: props.frontends -- Render
pure $
H.div
{ className: "graph-sidebar__data-tab" }
[
case hasSelection of
-- No result
false ->
B.caveat
{}
[
H.text "Select one or more nodes to get their informations"
]
-- Nodes have been selected
true ->
R.fragment
[
selectedNodes $
{ nodesMap: SigmaxT.nodesGraphMap props.graph
} `Record.merge` props
,
sideBarTabSeparator
,
neighborhood
props
,
sideBarTabSeparator
,
query
{ frontends: props.frontends
, metaData: props.metaData , metaData: props.metaData
, nodesMap: SigmaxT.nodesGraphMap props.graph , nodesMap: SigmaxT.nodesGraphMap props.graph
, searchType: SearchDoc , searchType: SearchDoc
, selectedNodeIds: selectedNodeIds' , selectedNodeIds: selectedNodeIds'
, session: props.session , session: props.session
} [] }
] ]
] ]
------------------------------------------------------------
sideTabCommunity :: R2.Component Props sideTabCommunity :: R2.Leaf Props
sideTabCommunity = R.createElement sideTabCommunityCpt sideTabCommunity = R2.leaf sideTabCommunityCpt
sideTabCommunityCpt :: R.Component Props sideTabCommunityCpt :: R.Component Props
sideTabCommunityCpt = here.component "sideTabCommunity" cpt sideTabCommunityCpt = here.component "sideTabCommunity" cpt
where where
cpt props@{ boxes: { sidePanelGraph } cpt props@{ boxes: { sidePanelGraph }
, frontends } _ = do , frontends } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph { selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ RH.div { className: "col-md-12", id: "query" } -- Computed
[ selectedNodes (Record.merge { nodesMap: SigmaxT.nodesGraphMap props.graph } props) [] let
, neighborhood props [] hasSelection = not $ Set.isEmpty selectedNodeIds'
, query { frontends
-- Render
pure $
H.div
{ className: "graph-sidebar__community-tab" }
[
case hasSelection of
-- No result
false ->
B.caveat
{}
[
H.text "Select one or more nodes to get their informations"
]
-- Nodes have been selection
true ->
R.fragment
[
selectedNodes $
{ nodesMap: SigmaxT.nodesGraphMap props.graph
} `Record.merge` props
,
sideBarTabSeparator
,
neighborhood
props
,
sideBarTabSeparator
,
query
{ frontends
, metaData: props.metaData , metaData: props.metaData
, nodesMap: SigmaxT.nodesGraphMap props.graph , nodesMap: SigmaxT.nodesGraphMap props.graph
, searchType: SearchContact , searchType: SearchContact
, selectedNodeIds: selectedNodeIds' , selectedNodeIds: selectedNodeIds'
, session: props.session , session: props.session
} [] }
] ]
]
-------------------------------------------
sideBarTabSeparator :: R.Element
sideBarTabSeparator =
H.div
{ className: "graph-sidebar__separator" }
[
B.icon
{ name: "angle-down" }
]
------------------------------------------- -------------------------------------------
-- TODO -- TODO
-- selectedNodes :: Record Props -> Map.Map String Nodes -> R.Element -- selectedNodes :: Record Props -> Map.Map String Nodes -> R.Element
type SelectedNodesProps = ( type SelectedNodesProps =
nodesMap :: SigmaxT.NodesMap ( nodesMap :: SigmaxT.NodesMap
| Props | Props
) )
selectedNodes :: R2.Component SelectedNodesProps selectedNodes :: R2.Leaf SelectedNodesProps
selectedNodes = R.createElement selectedNodesCpt selectedNodes = R2.leaf selectedNodesCpt
selectedNodesCpt :: R.Component SelectedNodesProps selectedNodesCpt :: R.Component SelectedNodesProps
selectedNodesCpt = here.component "selectedNodes" cpt selectedNodesCpt = here.component "selectedNodes" cpt where
where
cpt props@{ boxes: { sidePanelGraph } cpt props@{ boxes: { sidePanelGraph }
, graph , graph
, nodesMap } _ = do , nodesMap } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph { selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ R2.row -- Computed
[ R2.col 12 let
[ RH.ul { className: "nav nav-tabs d-flex justify-content-center" commonProps = RX.pick props :: Record Common
, id: "myTab"
, role: "tablist" } -- Behaviors
[ RH.div { className: "tab-content" } let
[ RH.div { className: "d-flex flex-wrap justify-content-center" onBadgeClick id _ = T.write_ (Set.singleton id) selectedNodeIds
, role: "tabpanel" }
( Seq.toUnfoldable -- Render
$ ( Seq.map (\node -> badge { minSize: node.size -- same size for all badges pure $
, maxSize: node.size
, node H.ul
, selectedNodeIds }) { className: intercalate " "
(badges graph selectedNodeIds') [ "graph-selected-nodes"
) , "list-group"
-- $ ( Seq.map (\node -> badge { maxSize, minSize, node, selectedNodeIds }) badges') ]
) }
, H.br {} [
H.li
{ className: "list-group-item" }
[
H.ul
{} $
Seq.toUnfoldable $
flip Seq.map (badges graph selectedNodeIds') \node ->
H.li
{ className: "graph-selected-nodes__item" }
[
H.a
{ className: intercalate " "
[ "graph-selected-nodes__badge"
, "badge badge-info"
]
, on: { click: onBadgeClick node.id }
}
[ H.text node.label ]
] ]
]
,
H.li
{ className: intercalate " "
[ "list-group-item"
, "graph-selected-nodes__actions"
] ]
, RH.div { className: "tab-content flex-space-between" } }
[ updateTermButton (Record.merge { buttonType: "primary" [
updateTermButton
( commonProps `Record.merge`
{ variant: ButtonVariant Success
, rType: CandidateTerm , rType: CandidateTerm
, nodesMap , nodesMap
, text: "Move as candidate" } commonProps) [] }
, H.br {} )
, updateTermButton (Record.merge { buttonType: "danger" [ H.text "Move as candidate" ]
,
updateTermButton
( commonProps `Record.merge`
{ variant: ButtonVariant Danger
, nodesMap , nodesMap
, rType: StopTerm , rType: StopTerm
, text: "Move as stop" } commonProps) [] }
] )
[ H.text "Move as stop" ]
] ]
] ]
where
commonProps = RX.pick props :: Record Common
data TagCloudState = Folded | Unfolded ---------------------------------------------------------
derive instance Eq TagCloudState
flipFold :: TagCloudState -> TagCloudState
flipFold Folded = Unfolded
flipFold Unfolded = Folded
neighborhood :: R2.Component Props neighborhood :: R2.Leaf Props
neighborhood = R.createElement neighborhoodCpt neighborhood = R2.leaf neighborhoodCpt
neighborhoodCpt :: R.Component Props neighborhoodCpt :: R.Memo Props
neighborhoodCpt = here.component "neighborhood" cpt neighborhoodCpt = R.memo' $ here.component "neighborhood" cpt where
where
cpt { boxes: { sidePanelGraph } cpt { boxes: { sidePanelGraph }
, graph , graph
} _ = do } _ = do
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph -- States
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds { selectedNodeIds } <-
state <- T.useBox Folded GEST.focusedSidePanel sidePanelGraph
state' <- T.useLive T.unequal state
selectedNodeIds' <-
T.useLive T.unequal selectedNodeIds
showMore /\ showMoreBox <-
R2.useBox' false
let numberOfBadgesToShowWhenFolded = 5 termList /\ termListBox <-
badges' = neighbourBadges graph selectedNodeIds' R2.useBox' []
termCount /\ termCountBox <-
R2.useBox' 0
-- Computed
let
minSize = F.foldl Math.min 0.0 (Seq.map _.size (SigmaxT.graphNodes graph)) minSize = F.foldl Math.min 0.0 (Seq.map _.size (SigmaxT.graphNodes graph))
maxSize = F.foldl Math.max 0.0 (Seq.map _.size (SigmaxT.graphNodes graph)) maxSize = F.foldl Math.max 0.0 (Seq.map _.size (SigmaxT.graphNodes graph))
orderedBadges = A.sortWith (\n -> -n.size) $ Seq.toUnfoldable badges' -- reverse sort (largest size first)
displayBadges = case state' of maxTruncateResult = 5
Folded -> A.take numberOfBadgesToShowWhenFolded orderedBadges
Unfolded -> orderedBadges withTruncateResults = (termCount > maxTruncateResult) && (not showMore)
stateText = case state' of
Folded -> "Show more"
Unfolded -> "Show less" -- Behaviors
showFoldedTooltip = A.length orderedBadges > numberOfBadgesToShowWhenFolded let
onBadgeClick id _ = T.write_ (Set.singleton id) selectedNodeIds
pure $ RH.div { className: "tab-content", id: "myTabContent" }
[ RH.div { -- className: "flex-space-around d-flex justify-content-center" -- Effects
className: "d-flex flex-wrap flex-space-around" R.useEffect1' selectedNodeIds' do
, id: "home" let refreshed = neighbourBadges graph selectedNodeIds'
, role: "tabpanel" let count = Seq.length refreshed
let ordered = A.sortWith (\n -> -n.size) $ Seq.toUnfoldable refreshed
T.write_ count termCountBox
T.write_ ordered termListBox
T.write_ false showMoreBox
-- Render
pure $
H.ul
{ className: intercalate " "
[ "graph-neighborhood"
, "list-group"
]
} }
((\node -> badge { maxSize, minSize, node, selectedNodeIds }) <$> displayBadges) <> [
RH.a { className: "" -- with empty class name, bootstrap renders this blue -- Extracted count
, on: { click: toggleUnfold state} } [ RH.text stateText ] H.li
{ className: "list-group-item" }
[
-- @XXX: Bootstrap CSS w/ one <li> deduped the list-style-type bullet
H.div
{ className: "graph-neighborhood__counter" }
[
B.wad'
[ "text-info", "d-inline" ] $
show termCount
,
H.text $ nbsp 1 <> "terms"
]
]
,
-- Word cloud
H.li
{ className: "list-group-item" }
[
H.ul
{} $
flip mapWithIndex termList \index node ->
R2.if'
(
withTruncateResults == false
|| index < maxTruncateResult
) $
H.li
{ className: "graph-neighborhood__badge" }
[
H.a
{ className: "badge badge-light"
-- adjust font accordingly
, style:
{ fontSize: badgeSize
minSize
maxSize
node.size
, lineHeight: badgeSize
minSize
maxSize
node.size
}
, on: { click: onBadgeClick node.id }
}
[ H.text node.label ]
]
,
R2.if' (withTruncateResults) $
B.button
{ variant: ButtonVariant Light
, callback: \_ -> T.modify_ (not) showMoreBox
, block: true
, className: "graph-neighborhood__show-more"
}
[
H.text "Show more"
]
]
] ]
where
toggleUnfold state = T.modify_ flipFold state
---------------------------------------------------------
type UpdateTermButtonProps = ( type UpdateTermButtonProps =
buttonType :: String ( variant :: ButtonVariant
, nodesMap :: SigmaxT.NodesMap , nodesMap :: SigmaxT.NodesMap
, rType :: TermList , rType :: TermList
, text :: String
| Common | Common
) )
updateTermButton :: R2.Component UpdateTermButtonProps updateTermButton :: R2.Component UpdateTermButtonProps
updateTermButton = R.createElement updateTermButtonCpt updateTermButton = R2.component updateTermButtonCpt
updateTermButtonCpt :: R.Component UpdateTermButtonProps updateTermButtonCpt :: R.Component UpdateTermButtonProps
updateTermButtonCpt = here.component "updateTermButton" cpt updateTermButtonCpt = here.component "updateTermButton" cpt where
where
cpt { boxes: { errors cpt { boxes: { errors
, reloadForest , reloadForest
, sidePanelGraph } , sidePanelGraph }
, buttonType , variant
, graphId , graphId
, metaData , metaData
, nodesMap , nodesMap
, rType , rType
, session , session
, text } _ = do } children = do
-- States
{ removedNodeIds, selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph { removedNodeIds, selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ if Set.isEmpty selectedNodeIds' then -- Behaviors
RH.div {} [] let
else callback _ = do
RH.button { className: "btn btn-sm btn-" <> buttonType
, on: { click: onClickRemove removedNodeIds selectedNodeIds selectedNodeIds' }
} [ RH.text text ]
where
onClickRemove removedNodeIds selectedNodeIds selectedNodeIds' _ = do
let nodes = mapMaybe (\id -> Map.lookup id nodesMap) let nodes = mapMaybe (\id -> Map.lookup id nodesMap)
$ Set.toUnfoldable selectedNodeIds' $ Set.toUnfoldable selectedNodeIds'
sendPatches { errors sendPatches { errors
...@@ -312,33 +509,30 @@ updateTermButtonCpt = here.component "updateTermButton" cpt ...@@ -312,33 +509,30 @@ updateTermButtonCpt = here.component "updateTermButton" cpt
T.write_ selectedNodeIds' removedNodeIds T.write_ selectedNodeIds' removedNodeIds
T.write_ SigmaxT.emptyNodeIds selectedNodeIds T.write_ SigmaxT.emptyNodeIds selectedNodeIds
-- Render
pure $
type BadgeProps = B.button
( maxSize :: Number { variant
, minSize :: Number , callback
, node :: Record SigmaxT.Node
, selectedNodeIds :: T.Box SigmaxT.NodeIds )
badge :: R2.Leaf BadgeProps
badge = R2.leafComponent badgeCpt
badgeCpt :: R.Component BadgeProps
badgeCpt = here.component "badge" cpt where
cpt { maxSize, minSize, node: { id, label, size }, selectedNodeIds } _ = do
let minFontSize = 1.0 -- "em"
let maxFontSize = 3.0 -- "em"
let sizeScaled = (size - minSize) / (maxSize - minSize) -- in [0; 1] range
let scale' = Math.log (sizeScaled + 1.0) / (Math.log 2.0) -- in [0; 1] range
let scale = minFontSize + scale' * (maxFontSize - minFontSize)
let style = {
fontSize: show scale <> "em"
} }
children
---------------------------------------------------------
badgeSize :: Number -> Number -> Number -> String
badgeSize minSize maxSize size =
let
minFontSize = 10.0
maxFontSize = 24.0
sizeScaled = (size - minSize) / (maxSize - minSize) -- in [0; 1] range
scale' = Math.log (sizeScaled + 1.0) / (Math.log 2.0) -- in [0; 1] range
scale = minFontSize + scale' * (maxFontSize - minFontSize)
in
show scale <> "px"
pure $ RH.a { className: "badge badge-pill badge-light"
, on: { click: onClick }
} [ RH.h6 { style } [ RH.text label ] ]
where
onClick _ = do
T.write_ (Set.singleton id) selectedNodeIds
badges :: SigmaxT.SGraph -> SigmaxT.NodeIds -> Seq.Seq (Record SigmaxT.Node) badges :: SigmaxT.SGraph -> SigmaxT.NodeIds -> Seq.Seq (Record SigmaxT.Node)
badges graph selectedNodeIds = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds badges graph selectedNodeIds = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds
...@@ -347,6 +541,8 @@ neighbourBadges :: SigmaxT.SGraph -> SigmaxT.NodeIds -> Seq.Seq (Record SigmaxT. ...@@ -347,6 +541,8 @@ neighbourBadges :: SigmaxT.SGraph -> SigmaxT.NodeIds -> Seq.Seq (Record SigmaxT.
neighbourBadges graph selectedNodeIds = SigmaxT.neighbours graph selectedNodes' where neighbourBadges graph selectedNodeIds = SigmaxT.neighbours graph selectedNodes' where
selectedNodes' = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds selectedNodes' = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds
---------------------------------------------------------
type SendPatches = type SendPatches =
( errors :: T.Box (Array FrontendError) ( errors :: T.Box (Array FrontendError)
, graphId :: NodeID , graphId :: NodeID
...@@ -408,6 +604,8 @@ sendPatch termList session (GET.MetaData metaData) node = do ...@@ -408,6 +604,8 @@ sendPatch termList session (GET.MetaData metaData) node = do
patch_list :: NTC.Replace TermList patch_list :: NTC.Replace TermList
patch_list = NTC.Replace { new: termList, old: MapTerm } patch_list = NTC.Replace { new: termList, old: MapTerm }
---------------------------------------------------------
type Query = type Query =
( frontends :: Frontends ( frontends :: Frontends
, metaData :: GET.MetaData , metaData :: GET.MetaData
...@@ -416,15 +614,15 @@ type Query = ...@@ -416,15 +614,15 @@ type Query =
, selectedNodeIds :: SigmaxT.NodeIds , selectedNodeIds :: SigmaxT.NodeIds
, session :: Session ) , session :: Session )
query :: R2.Component Query query :: R2.Leaf Query
query = R.createElement queryCpt query = R2.leaf queryCpt
queryCpt :: R.Component Query queryCpt :: R.Component Query
queryCpt = here.component "query" cpt where queryCpt = here.component "query" cpt where
cpt props@{ selectedNodeIds } _ = do cpt props@{ selectedNodeIds } _ = do
pure $ if Set.isEmpty selectedNodeIds pure $ if Set.isEmpty selectedNodeIds
then RH.div {} [] then H.div {} []
else query' props [] else query' props []
query' :: R2.Component Query query' :: R2.Component Query
...@@ -439,7 +637,7 @@ queryCpt' = here.component "query'" cpt where ...@@ -439,7 +637,7 @@ queryCpt' = here.component "query'" cpt where
, selectedNodeIds , selectedNodeIds
, session } _ = do , session } _ = do
pure $ case (head metaData.corpusId) of pure $ case (head metaData.corpusId) of
Nothing -> RH.div {} [] Nothing -> H.div {} []
Just corpusId -> Just corpusId ->
CGT.tabs { frontends CGT.tabs { frontends
, query: SearchQuery { expected: searchType , query: SearchQuery { expected: searchType
...@@ -461,8 +659,8 @@ queryCpt' = here.component "query'" cpt where ...@@ -461,8 +659,8 @@ queryCpt' = here.component "query'" cpt where
------------------------------------------------------------------------ ------------------------------------------------------------------------
{-, RH.div { className: "col-md-12", id: "horizontal-checkbox" } {-, H.div { className: "col-md-12", id: "horizontal-checkbox" }
[ RH.ul {} [ H.ul {}
[ checkbox "Pubs" [ checkbox "Pubs"
, checkbox "Projects" , checkbox "Projects"
, checkbox "Patents" , checkbox "Patents"
...@@ -472,28 +670,88 @@ queryCpt' = here.component "query'" cpt where ...@@ -472,28 +670,88 @@ queryCpt' = here.component "query'" cpt where
-} -}
-------------------------------------------------------------------------- --------------------------------------------------------------------------
documentation :: Lang -> R.Element documentation :: Lang -> R.Element
documentation _ = documentation _ =
H.div {} [ H.h2 {} [ H.text "What is Graph ?"]
, ul [ "Graph is a conveniant tool to explore your documents. " H.div
, "Nodes are terms selected in your Map List. " { className: "graph-documentation" }
<> "Node size is proportional to the number of documents with the associated term. " [
, "Edges between nodes represent proximities of terms according to a specific distance between your documents. " H.div
<> "Link strength is proportional to the strenght of terms association." { className: "graph-documentation__text-section" }
[
H.p
{}
[
B.b_ "What is a graph? "
,
H.text "Graph is a conveniant tool to explore your documents."
]
,
H.p
{}
[
H.text $
"Nodes are terms selected in your Map List. "
<>
"Node size is proportional to the number of documents with the associated term. "
]
,
H.p
{}
[
H.text $
"Edges between nodes represent proximities of terms according to a specific distance between your documents. "
<>
"Link strength is proportional to the strenght of terms association."
] ]
, H.h3 {} [ H.text "Basic Interactions:"]
, ul [ "Click on a node to select/unselect and get its information. "
, "In case of multiple selection, the button unselect clears all selections. "
<> "Use your mouse scroll to zoom in and out in the graph. "
, "Use the node filter to create a subgraph with nodes of a given size "
<>"range (e.g. display only generic terms). "
, "Use the edge filter so create a subgraph with links in a given range (e.g. keep the strongest association)."
] ]
,
H.div
{ className: "graph-documentation__text-section" }
[
H.ul
{}
[
H.li
{}
[
H.text $
"Click on a node to select/unselect and get its information."
] ]
,
H.li
{}
[
H.text $
"In case of multiple selection, the button unselect clears all selections. "
<>
"Use your mouse scroll to zoom in and out in the graph. "
]
,
H.li
{}
[
H.text $
"Use the node filter to create a subgraph with nodes of a given size "
<>
"range (e.g. display only generic terms). "
]
,
H.li
{}
[
H.text $
where "Use the edge filter so create a subgraph with links in a given range (e.g. keep the strongest association)."
ul ts = H.ul {} $ map (\t -> H.li {} [ H.text t ]) ts ]
]
]
]
{- {-
TODO DOC TODO DOC
...@@ -504,4 +762,3 @@ Global/local view: ...@@ -504,4 +762,3 @@ Global/local view:
The 'change level' button allows to change between global view and node centered view, The 'change level' button allows to change between global view and node centered view,
To explore the neighborhood of a selection click on the 'change level' button. To explore the neighborhood of a selection click on the 'change level' button.
-} -}
module Gargantext.Components.GraphExplorer.Sidebar.Types where module Gargantext.Components.GraphExplorer.Sidebar.Types where
import Data.Maybe (Maybe(..), maybe)
import Data.Set as Set
import Reactix as R
import Toestand as T
import Gargantext.Prelude import Gargantext.Prelude
import Data.Maybe (Maybe(..), maybe)
import Data.Set as Set
import Gargantext.Components.GraphExplorer.Types as GET import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax.Types as SigmaxT import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Types as GT
import Reactix as R
import Toestand as T
type SidePanel = type SidePanel =
( (
...@@ -19,6 +19,7 @@ type SidePanel = ...@@ -19,6 +19,7 @@ type SidePanel =
, selectedNodeIds :: SigmaxT.NodeIds , selectedNodeIds :: SigmaxT.NodeIds
, showControls :: Boolean , showControls :: Boolean
, sideTab :: GET.SideTab , sideTab :: GET.SideTab
, showSidebar :: GT.SidePanelState
) )
initialSidePanel :: Maybe (Record SidePanel) initialSidePanel :: Maybe (Record SidePanel)
...@@ -32,7 +33,9 @@ focusedSidePanel :: T.Box (Maybe (Record SidePanel)) ...@@ -32,7 +33,9 @@ focusedSidePanel :: T.Box (Maybe (Record SidePanel))
, removedNodeIds :: T.Box SigmaxT.NodeIds , removedNodeIds :: T.Box SigmaxT.NodeIds
, selectedNodeIds :: T.Box SigmaxT.NodeIds , selectedNodeIds :: T.Box SigmaxT.NodeIds
, showControls :: T.Box Boolean , showControls :: T.Box Boolean
, sideTab :: T.Box GET.SideTab } , sideTab :: T.Box GET.SideTab
, showSidebar :: T.Box GT.SidePanelState
}
focusedSidePanel sidePanel = do focusedSidePanel sidePanel = do
mGraph <- T.useFocused mGraph <- T.useFocused
(maybe Nothing _.mGraph) (maybe Nothing _.mGraph)
...@@ -55,6 +58,9 @@ focusedSidePanel sidePanel = do ...@@ -55,6 +58,9 @@ focusedSidePanel sidePanel = do
sideTab <- T.useFocused sideTab <- T.useFocused
(maybe GET.SideTabLegend _.sideTab) (maybe GET.SideTabLegend _.sideTab)
(\val -> maybe Nothing (\sp -> Just $ sp { sideTab = val })) sidePanel (\val -> maybe Nothing (\sp -> Just $ sp { sideTab = val })) sidePanel
showSidebar <- T.useFocused
(maybe GT.InitialClosed _.showSidebar)
(\val -> maybe Nothing (\sp -> Just $ sp { showSidebar = val })) sidePanel
pure $ { pure $ {
mGraph mGraph
...@@ -64,4 +70,5 @@ focusedSidePanel sidePanel = do ...@@ -64,4 +70,5 @@ focusedSidePanel sidePanel = do
, selectedNodeIds , selectedNodeIds
, showControls , showControls
, sideTab , sideTab
, showSidebar
} }
...@@ -34,20 +34,35 @@ sizeButtonCpt :: R.Component Props ...@@ -34,20 +34,35 @@ sizeButtonCpt :: R.Component Props
sizeButtonCpt = here.component "sizeButton" cpt where sizeButtonCpt = here.component "sizeButton" cpt where
cpt { state, caption, min, max, onChange } _ = do cpt { state, caption, min, max, onChange } _ = do
defaultValue <- T.useLive T.unequal state defaultValue <- T.useLive T.unequal state
pure $ H.span { className: "range-simple" }
[ H.label {} [ R2.small {} [ H.text caption ] ] pure $
, H.input { type: "range"
, className: "form-control" H.span
{ className: "range-simple" }
[
H.label
{ className: "range-simple__label" }
[ H.text caption ]
,
H.span
{ className: "range-simple__field" }
[
H.input
{ type: "range"
, min: show min , min: show min
, max: show max , max: show max
, defaultValue , defaultValue
, on: { input: onChange } }] , on: { input: onChange }
, className: "range-simple__input"
}
]
]
labelSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element labelSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element
labelSizeButton sigmaRef state = labelSizeButton sigmaRef state =
sizeButton { sizeButton {
state state
, caption: "Label Size" , caption: "Label size"
, min: 1.0 , min: 1.0
, max: 30.0 , max: 30.0
, onChange: \e -> do , onChange: \e -> do
...@@ -67,7 +82,7 @@ mouseSelectorSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element ...@@ -67,7 +82,7 @@ mouseSelectorSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element
mouseSelectorSizeButton sigmaRef state = mouseSelectorSizeButton sigmaRef state =
sizeButton { sizeButton {
state state
, caption: "Selector Size" , caption: "Selector size"
, min: 1.0 , min: 1.0
, max: 50.0 , max: 50.0
, onChange: \e -> do , onChange: \e -> do
......
...@@ -3,27 +3,18 @@ module Gargantext.Components.GraphExplorer.ToggleButton ...@@ -3,27 +3,18 @@ module Gargantext.Components.GraphExplorer.ToggleButton
, toggleButton , toggleButton
, toggleButtonCpt , toggleButtonCpt
, controlsToggleButton , controlsToggleButton
, edgesToggleButton
, louvainToggleButton
, multiSelectEnabledButton
, sidebarToggleButton
, pauseForceAtlasButton
, resetForceAtlasButton
) where ) where
import Prelude import Prelude
import Effect (Effect) import Effect (Effect)
import Gargantext.Components.Graph as Graph
import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Sigma as Sigma
import Gargantext.Hooks.Sigmax.Types as SigmaxTypes
import Gargantext.Types as GT
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
import Toestand as T import Toestand as T
-- @WIP: used?
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.ToggleButton" here = R2.here "Gargantext.Components.GraphExplorer.ToggleButton"
...@@ -56,6 +47,8 @@ toggleButtonCpt = here.component "toggleButton" cpt ...@@ -56,6 +47,8 @@ toggleButtonCpt = here.component "toggleButton" cpt
text on _off true = on text on _off true = on
text _on off false = off text _on off false = off
----------------------------------------------------------------
type ControlsToggleButtonProps = ( type ControlsToggleButtonProps = (
state :: T.Box Boolean state :: T.Box Boolean
) )
...@@ -73,147 +66,3 @@ controlsToggleButtonCpt = here.component "controlsToggleButton" cpt ...@@ -73,147 +66,3 @@ controlsToggleButtonCpt = here.component "controlsToggleButton" cpt
, onClick: \_ -> T.modify_ not state , onClick: \_ -> T.modify_ not state
, style: "light" , style: "light"
} [] } []
type EdgesButtonProps = (
state :: T.Box SigmaxTypes.ShowEdgesState
)
edgesToggleButton :: R2.Component EdgesButtonProps
edgesToggleButton = R.createElement edgesToggleButtonCpt
edgesToggleButtonCpt :: R.Component EdgesButtonProps
edgesToggleButtonCpt = here.component "edgesToggleButton" cpt
where
cpt { state } _ = do
state' <- T.useLive T.unequal state
pure $ H.button { className: "btn btn-outline-secondary " <> cls state'
, on: { click: onClick state }
} [ R2.small {} [ H.text (text state') ] ]
text s = if SigmaxTypes.edgeStateHidden s then "Show edges" else "Hide edges"
cls SigmaxTypes.EShow = ""
cls _ = "active"
-- TODO: Move this to Graph.purs to the R.useEffect handler which renders nodes/edges
onClick state _ = T.modify_ SigmaxTypes.toggleShowEdgesState state
type LouvainToggleButtonProps = (
state :: T.Box Boolean
)
louvainToggleButton :: R2.Component LouvainToggleButtonProps
louvainToggleButton = R.createElement louvainToggleButtonCpt
louvainToggleButtonCpt :: R.Component LouvainToggleButtonProps
louvainToggleButtonCpt = here.component "louvainToggleButton" cpt
where
cpt { state } _ = do
pure $ toggleButton {
state: state
, onMessage: "Louvain off"
, offMessage: "Louvain on"
, onClick: \_ -> T.modify_ not state
, style: "secondary"
} []
type MultiSelectEnabledButtonProps = (
state :: T.Box Boolean
)
multiSelectEnabledButton :: R2.Component MultiSelectEnabledButtonProps
multiSelectEnabledButton = R.createElement multiSelectEnabledButtonCpt
multiSelectEnabledButtonCpt :: R.Component MultiSelectEnabledButtonProps
multiSelectEnabledButtonCpt = here.component "lmultiSelectEnabledButton" cpt
where
cpt { state } _ = do
pure $ toggleButton {
state: state
, onMessage: "Single-node"
, offMessage: "Multi-node"
, onClick: \_ -> T.modify_ not state
, style : "primary"
} []
type ForceAtlasProps = (
state :: T.Box SigmaxTypes.ForceAtlasState
)
pauseForceAtlasButton :: R2.Component ForceAtlasProps
pauseForceAtlasButton = R.createElement pauseForceAtlasButtonCpt
pauseForceAtlasButtonCpt :: R.Component ForceAtlasProps
pauseForceAtlasButtonCpt = here.component "forceAtlasToggleButton" cpt
where
cpt { state } _ = do
state' <- T.useLive T.unequal state
pure $ H.button { className: "btn btn-outline-secondary " <> cls state'
, on: { click: onClick state }
} [ R2.small {} [ H.text (text state') ] ]
cls SigmaxTypes.InitialRunning = "active"
cls SigmaxTypes.Running = "active"
cls _ = ""
text SigmaxTypes.InitialRunning = "Pause"
text SigmaxTypes.InitialStopped = "Start"
text SigmaxTypes.Running = "Pause"
text SigmaxTypes.Paused = "Start"
text SigmaxTypes.Killed = "Start"
onClick state _ = T.modify_ SigmaxTypes.toggleForceAtlasState state
type ResetForceAtlasProps = (
forceAtlasState :: T.Box SigmaxTypes.ForceAtlasState
, sigmaRef :: R.Ref Sigmax.Sigma
)
resetForceAtlasButton :: R2.Component ResetForceAtlasProps
resetForceAtlasButton = R.createElement resetForceAtlasButtonCpt
resetForceAtlasButtonCpt :: R.Component ResetForceAtlasProps
resetForceAtlasButtonCpt = here.component "resetForceAtlasToggleButton" cpt
where
cpt { forceAtlasState, sigmaRef } _ = do
pure $ H.button { className: "btn btn-outline-secondary"
, on: { click: onClick forceAtlasState sigmaRef }
} [ R2.small {} [ H.text "Reset Force Atlas" ] ]
onClick forceAtlasState sigmaRef _ = do
-- TODO Sigma.killForceAtlas2 sigma
-- startForceAtlas2 sigma
Sigmax.dependOnSigma (R.readRef sigmaRef) "[resetForceAtlasButton] no sigma" $ \sigma -> do
Sigma.killForceAtlas2 sigma
Sigma.refreshForceAtlas sigma Graph.forceAtlas2Settings
T.write_ SigmaxTypes.Killed forceAtlasState
type SidebarToggleButtonProps = (
state :: T.Box GT.SidePanelState
)
sidebarToggleButton :: R2.Component SidebarToggleButtonProps
sidebarToggleButton = R.createElement sidebarToggleButtonCpt
sidebarToggleButtonCpt :: R.Component SidebarToggleButtonProps
sidebarToggleButtonCpt = here.component "sidebarToggleButton" cpt
where
cpt { state } _ = do
state' <- T.useLive T.unequal state
pure $ H.div { className: "btn btn-outline-light " <> cls state'
, on: { click: onClick state }
} [ R2.small {} [ H.text (text onMessage offMessage state') ] ]
cls GT.Opened = "active"
cls _ = ""
onMessage = "Hide Sidebar"
offMessage = "Show Sidebar"
text on _off GT.Opened = on
text _on off GT.InitialClosed = off
text _on off GT.Closed = off
onClick state = \_ ->
T.modify_ GT.toggleSidePanelState state
-- case s of
-- GET.InitialClosed -> GET.Opened GET.SideTabLegend
-- GET.Closed -> GET.Opened GET.SideTabLegend
-- (GET.Opened _) -> GET.Closed) state
module Gargantext.Components.GraphExplorer.TopBar where module Gargantext.Components.GraphExplorer.TopBar (topBar) where
import Data.Maybe (Maybe(..)) import Gargantext.Prelude hiding (max, min)
import Reactix as R
import Reactix.DOM.HTML as RH
import Toestand as T
import Gargantext.Prelude hiding (max,min) import Data.Maybe (Maybe)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..))
import Gargantext.Components.GraphExplorer.Search (nodeSearchControl) import Gargantext.Components.GraphExplorer.Search (nodeSearchControl)
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.ToggleButton as Toggle import Gargantext.Types as GT
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
type Props =
( sidePanelGraph :: T.Box (Maybe (Record GEST.SidePanel))
)
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.TopBar" here = R2.here "Gargantext.Components.GraphExplorer.TopBar"
type TopBar = topBar :: R2.Leaf Props
( topBar = R2.leaf component
boxes :: Boxes
)
topBar :: R2.Leaf TopBar component :: R.Component Props
topBar = R2.leafComponent topBarCpt component = here.component "topBar" cpt where
topBarCpt :: R.Component TopBar cpt { sidePanelGraph } _ = do
topBarCpt = here.component "topBar" cpt where -- States
cpt { boxes: { sidePanelGraph { mGraph
, sidePanelState } } _ = do , multiSelectEnabled
{ mGraph, multiSelectEnabled, selectedNodeIds, showControls } <- GEST.focusedSidePanel sidePanelGraph , selectedNodeIds
, showControls
, showSidebar
} <- GEST.focusedSidePanel sidePanelGraph
mGraph' <- T.useLive T.unequal mGraph mGraph' <- R2.useLive' mGraph
showControls' <- R2.useLive' showControls
showSidebar' <- R2.useLive' showSidebar
let search = case mGraph' of -- Render
Just graph -> nodeSearchControl { graph pure $
, multiSelectEnabled
, selectedNodeIds } [] H.div
Nothing -> RH.div {} [] { className: "graph-topbar" }
[
-- Toolbar toggle
B.button
{ className: "graph-topbar__toolbar"
, callback: \_ -> T.modify_ (not) showControls
, variant: showControls' ?
ButtonVariant Light $
OutlinedButtonVariant Light
}
[
H.text $ showControls' ? "Hide toolbar" $ "Show toolbar"
]
,
-- Sidebar toggle
B.button
{ className: "graph-topbar__sidebar"
, callback: \_ -> T.modify_ GT.toggleSidePanelState showSidebar
pure $ RH.form { className: "graph-topbar d-flex" } , variant: showSidebar' == GT.Opened ?
[ Toggle.controlsToggleButton { state: showControls } [] ButtonVariant Light $
, Toggle.sidebarToggleButton { state: sidePanelState } [] OutlinedButtonVariant Light
, search }
[
H.text $ showSidebar' == GT.Opened ?
"Hide sidebar" $
"Show sidebar"
]
,
-- Search
R2.fromMaybe_ mGraph' \graph ->
nodeSearchControl
{ graph
, multiSelectEnabled
, selectedNodeIds
, className: "graph-topbar__search"
}
] ]
...@@ -26,7 +26,8 @@ type UserInfo ...@@ -26,7 +26,8 @@ type UserInfo
, ui_cwTouchPhone :: Maybe String , ui_cwTouchPhone :: Maybe String
, ui_cwTouchMail :: Maybe String } , ui_cwTouchMail :: Maybe String }
type UserInfoM type UserInfoM
= { ui_id :: NotNull Int = { token :: NotNull String
, ui_id :: NotNull Int
, ui_username :: String , ui_username :: String
, ui_email :: String , ui_email :: String
, ui_title :: String , ui_title :: String
......
...@@ -29,8 +29,8 @@ type Props = ...@@ -29,8 +29,8 @@ type Props =
, state :: T.Box String , state :: T.Box String
) )
inputWithAutocomplete :: R2.Component Props inputWithAutocomplete :: R2.Leaf Props
inputWithAutocomplete = R.createElement inputWithAutocompleteCpt inputWithAutocomplete = R2.leaf inputWithAutocompleteCpt
inputWithAutocompleteCpt :: R.Component Props inputWithAutocompleteCpt :: R.Component Props
inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt
where where
......
...@@ -7,9 +7,6 @@ module Gargantext.Components.Nodes.Annuaire.User.Contact ...@@ -7,9 +7,6 @@ module Gargantext.Components.Nodes.Annuaire.User.Contact
, saveUserInfo , saveUserInfo
) where ) where
import Gargantext.Components.GraphQL.User (UserInfo, _ui_cwCity, _ui_cwCountry, _ui_cwFirstName, _ui_cwLabTeamDeptsFirst, _ui_cwLastName, _ui_cwOffice, _ui_cwOrganizationFirst, _ui_cwRole, _ui_cwTouchMail, _ui_cwTouchPhone)
import Gargantext.Prelude (Unit, bind, discard, pure, show, ($), (<$>), (<>))
import Data.Either (Either(..)) import Data.Either (Either(..))
import Data.Lens as L import Data.Lens as L
import Data.Maybe (Maybe(..), fromMaybe) import Data.Maybe (Maybe(..), fromMaybe)
...@@ -19,6 +16,7 @@ import Effect.Class (liftEffect) ...@@ -19,6 +16,7 @@ import Effect.Class (liftEffect)
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.GraphQL (getClient) import Gargantext.Components.GraphQL (getClient)
import Gargantext.Components.GraphQL.Endpoints (getUserInfo) import Gargantext.Components.GraphQL.Endpoints (getUserInfo)
import Gargantext.Components.GraphQL.User (UserInfo, _ui_cwCity, _ui_cwCountry, _ui_cwFirstName, _ui_cwLabTeamDeptsFirst, _ui_cwLastName, _ui_cwOffice, _ui_cwOrganizationFirst, _ui_cwRole, _ui_cwTouchMail, _ui_cwTouchPhone)
import Gargantext.Components.InputWithEnter (inputWithEnter) import Gargantext.Components.InputWithEnter (inputWithEnter)
import Gargantext.Components.Nodes.Annuaire.User.Contacts.Tabs as Tabs import Gargantext.Components.Nodes.Annuaire.User.Contacts.Tabs as Tabs
import Gargantext.Components.Nodes.Annuaire.User.Contacts.Types (ContactData', HyperdataContact(..)) import Gargantext.Components.Nodes.Annuaire.User.Contacts.Types (ContactData', HyperdataContact(..))
...@@ -26,8 +24,9 @@ import Gargantext.Components.Nodes.Lists.Types as LT ...@@ -26,8 +24,9 @@ import Gargantext.Components.Nodes.Lists.Types as LT
import Gargantext.Config.REST (AffRESTError, logRESTError) import Gargantext.Config.REST (AffRESTError, logRESTError)
import Gargantext.Ends (Frontends) import Gargantext.Ends (Frontends)
import Gargantext.Hooks.Loader (useLoader) import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Prelude (Unit, bind, discard, pure, show, ($), (<$>), (<>))
import Gargantext.Routes as Routes import Gargantext.Routes as Routes
import Gargantext.Sessions (Session, get, put, sessionId) import Gargantext.Sessions (Session(..), get, put, sessionId)
import Gargantext.Types (NodeType(..)) import Gargantext.Types (NodeType(..))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2 import Gargantext.Utils.Toestand as T2
...@@ -196,11 +195,13 @@ saveContactHyperdata session id = put session (Routes.NodeAPI Node (Just id) "") ...@@ -196,11 +195,13 @@ saveContactHyperdata session id = put session (Routes.NodeAPI Node (Just id) "")
saveUserInfo :: Session -> Int -> UserInfo -> AffRESTError Int saveUserInfo :: Session -> Int -> UserInfo -> AffRESTError Int
saveUserInfo session id ui = do saveUserInfo session id ui = do
let token = getToken session
client <- liftEffect $ getClient session client <- liftEffect $ getClient session
res <- mutation res <- mutation
client client
"update user_info" "update user_info"
{ update_user_info: onlyArgs { ui_id: id { update_user_info: onlyArgs { token: token
, ui_id: id
, ui_cwFirstName: ga ui.ui_cwFirstName , ui_cwFirstName: ga ui.ui_cwFirstName
, ui_cwLastName: ga ui.ui_cwLastName , ui_cwLastName: ga ui.ui_cwLastName
, ui_cwOrganization: ui.ui_cwOrganization , ui_cwOrganization: ui.ui_cwOrganization
...@@ -215,6 +216,7 @@ saveUserInfo session id ui = do ...@@ -215,6 +216,7 @@ saveUserInfo session id ui = do
where where
ga Nothing = ArgL IgnoreArg ga Nothing = ArgL IgnoreArg
ga (Just val) = ArgR val ga (Just val) = ArgR val
getToken (Session { token }) = token
type AnnuaireLayoutProps = ( annuaireId :: Int, session :: Session | ReloadProps ) type AnnuaireLayoutProps = ( annuaireId :: Int, session :: Session | ReloadProps )
......
...@@ -5,19 +5,20 @@ module Gargantext.Components.Nodes.Corpus.Phylo ...@@ -5,19 +5,20 @@ module Gargantext.Components.Nodes.Corpus.Phylo
import Gargantext.Prelude import Gargantext.Prelude
import DOM.Simple (document, querySelector) import DOM.Simple (document, querySelector)
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..), isJust)
import FFI.Simple ((..), (.=)) import Data.Tuple.Nested ((/\))
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.PhyloExplorer.API (get) import Gargantext.Components.PhyloExplorer.API (get)
import Gargantext.Components.PhyloExplorer.Layout (layout) import Gargantext.Components.PhyloExplorer.Layout (layout)
import Gargantext.Components.PhyloExplorer.Types (PhyloDataSet) import Gargantext.Components.PhyloExplorer.Types (PhyloDataSet)
import Gargantext.Config.REST (logRESTError) import Gargantext.Config.REST (logRESTError)
import Gargantext.Hooks.FirstEffect (useFirstEffect') import Gargantext.Hooks.Loader (useLoaderEffect)
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Sessions (Session) import Gargantext.Sessions (Session)
import Gargantext.Types (NodeID) import Gargantext.Types (NodeID)
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H
type MainProps = type MainProps =
( nodeId :: NodeID ( nodeId :: NodeID
...@@ -35,60 +36,93 @@ phyloLayoutCpt :: R.Component MainProps ...@@ -35,60 +36,93 @@ phyloLayoutCpt :: R.Component MainProps
phyloLayoutCpt = here.component "main" cpt where phyloLayoutCpt = here.component "main" cpt where
cpt { nodeId, session } _ = do cpt { nodeId, session } _ = do
-- | States
-- |
state' /\ state <- R2.useBox' Nothing
-- | Computed
-- |
let let
errorHandler = logRESTError here "[phylo]" errorHandler = logRESTError here "[phylo]"
handler (dataset :: PhyloDataSet) = handler (phyloDataSet :: PhyloDataSet) =
content layout
{ nodeId { nodeId
, dataset , phyloDataSet
} }
useLoader
-- | Hooks
-- |
useLoaderEffect
{ errorHandler { errorHandler
, loader: get session , loader: get session
, path: nodeId , path: nodeId
, render: handler , state
} }
-- @XXX: Runtime odd behavior
-- cannot use the `useEffect` + its cleanup function within the
-- same `Effect`, otherwise the below cleanup example will be
-- execute at mount
-------------------------------------------------------- -- @XXX: inopinent <div> (see Gargantext.Components.Router) (@TODO?)
R.useEffectOnce' do
type ContentProps = mEl <- querySelector document ".main-page__main-route .container"
( nodeId :: NodeID
, dataset :: PhyloDataSet
)
content :: R2.Leaf ContentProps
content = R2.leaf contentCpt
contentCpt :: R.Component ContentProps case mEl of
contentCpt = here.component "content" cpt where Nothing -> R.nothing
cpt { nodeId, dataset } _ = do Just el -> R2.addClass el [ "d-none" ]
-- Hooks
useFirstEffect' do R.useEffectOnce do
-- @XXX: inopinent <div> (see Gargantext.Components.Router) (@TODO?) pure do
mEl <- querySelector document ".main-page__main-route .container" mEl <- querySelector document ".main-page__main-route .container"
case mEl of case mEl of
Nothing -> pure unit Nothing -> R.nothing
Just el -> do Just el -> R2.removeClass el [ "d-none" ]
style <- pure $ (el .. "style")
pure $ (style .= "display") "none"
-- @XXX: reset "main-page__main-route" wrapper margin -- @XXX: reset "main-page__main-route" wrapper margin
-- see Gargantext.Components.Router) (@TODO?) -- see Gargantext.Components.Router) (@TODO?)
mEl' <- querySelector document ".main-page__main-route" R.useEffectOnce' do
case mEl' of mEl <- querySelector document ".main-page__main-route"
Nothing -> pure unit
Just el -> do case mEl of
style <- pure $ (el .. "style") Nothing -> R.nothing
pure $ (style .= "padding") "initial" Just el -> R2.addClass el [ "p-0" ]
-- Render R.useEffectOnce do
pure do
mEl <- querySelector document ".main-page__main-route"
case mEl of
Nothing -> R.nothing
Just el -> R2.removeClass el [ "p-0" ]
-- | Render
-- |
pure $ pure $
layout B.cloak
{ nodeId { isDisplayed: isJust state'
, phyloDataSet: dataset , idlingPhaseDuration: Just 150
, cloakSlot:
-- mimicking `PhyloExplorer.layout` preloading template
H.div
{ className: "phylo" }
[
H.div
{ className: "phylo__spinner-wrapper" }
[
B.spinner
{ className: "phylo__spinner" }
]
]
, defaultSlot:
R2.fromMaybe_ state' handler
} }
module Gargantext.Components.Nodes.Corpus.Graph
( graphLayout
) where
import Gargantext.Prelude
import DOM.Simple (document, querySelector)
import Data.Maybe (Maybe(..), isJust)
import Data.Set as Set
import Data.Tuple (Tuple(..))
import Data.Tuple.Nested ((/\))
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.GraphExplorer.Layout (convert, layout)
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Config.REST (AffRESTError, logRESTError)
import Gargantext.Hooks.Loader (useLoaderEffect)
import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Routes (SessionRoute(NodeAPI))
import Gargantext.Sessions (Session, get)
import Gargantext.Types as Types
import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
type Props =
( key :: String
, session :: Session
, boxes :: Boxes
, graphId :: GET.GraphId
)
here :: R2.Here
here = R2.here "Gargantext.Components.Nodes.Corpus.Graph"
graphLayout :: R2.Leaf Props
graphLayout = R2.leaf graphLayoutCpt
graphLayoutCpt :: R.Component Props
graphLayoutCpt = here.component "explorerLayout" cpt where
cpt props@{ boxes: { graphVersion }, graphId, session } _ = do
-- | States
-- |
graphVersion' <- T.useLive T.unequal graphVersion
state' /\ state <- R2.useBox' Nothing
-- | Hooks
-- |
useLoaderEffect
{ errorHandler
, loader: getNodes session graphVersion'
, path: graphId
, state
}
-- @XXX: Runtime odd behavior
-- cannot use the `useEffect` + its cleanup function within the
-- same `Effect`, otherwise the below cleanup example will be
-- execute at mount
-- @XXX: inopinent <div> (see Gargantext.Components.Router) (@TODO?)
R.useEffectOnce' do
mEl <- querySelector document ".main-page__main-route .container"
case mEl of
Nothing -> R.nothing
Just el -> R2.addClass el [ "d-none" ]
R.useEffectOnce do
pure do
mEl <- querySelector document ".main-page__main-route .container"
case mEl of
Nothing -> R.nothing
Just el -> R2.removeClass el [ "d-none" ]
-- @XXX: reset "main-page__main-route" wrapper margin
-- see Gargantext.Components.Router) (@TODO?)
R.useEffectOnce' do
mEl <- querySelector document ".main-page__main-route"
case mEl of
Nothing -> R.nothing
Just el -> R2.addClass el [ "p-0" ]
R.useEffectOnce do
pure do
mEl <- querySelector document ".main-page__main-route"
case mEl of
Nothing -> R.nothing
Just el -> R2.removeClass el [ "p-0" ]
-- | Render
-- |
pure $
B.cloak
{ isDisplayed: isJust state'
, idlingPhaseDuration: Just 150
, cloakSlot:
H.div
{ className: "graph-loader" }
[
B.spinner
{ className: "graph-loader__spinner" }
]
, defaultSlot:
R2.fromMaybe_ state' handler
}
where
errorHandler = logRESTError here "[explorerLayout]"
handler loaded@(GET.HyperdataGraph { graph: hyperdataGraph }) =
content { graph
, hyperdataGraph: loaded
, mMetaData'
, session
, boxes: props.boxes
, graphId
}
where
Tuple mMetaData' graph = convert hyperdataGraph
--------------------------------------------------------
type ContentProps =
( mMetaData' :: Maybe GET.MetaData
, graph :: SigmaxT.SGraph
, hyperdataGraph :: GET.HyperdataGraph
, session :: Session
, boxes :: Boxes
, graphId :: GET.GraphId
)
content :: R2.Leaf ContentProps
content = R2.leaf contentCpt
contentCpt :: R.Component ContentProps
contentCpt = here.component "content" cpt where
cpt props@{ boxes, mMetaData', graph } _ = do
-- Hooks
R.useEffectOnce' $
-- Hydrate Boxes
flip T.write_ boxes.sidePanelGraph $ Just
{ mGraph: Just graph
, mMetaData: mMetaData'
, multiSelectEnabled: false
, removedNodeIds: Set.empty
, selectedNodeIds: Set.empty
, showControls: false
, sideTab: GET.SideTabLegend
, showSidebar: Types.InitialClosed
}
-- Render
pure $
layout
props
--------------------------------------------------------------
getNodes :: Session -> T2.Reload -> GET.GraphId -> AffRESTError GET.HyperdataGraph
getNodes session graphVersion graphId =
get session $ NodeAPI Types.Graph
(Just graphId)
("?version=" <> (show graphVersion))
...@@ -21,7 +21,7 @@ import Gargantext.Components.PhyloExplorer.TopBar (topBar) ...@@ -21,7 +21,7 @@ import Gargantext.Components.PhyloExplorer.TopBar (topBar)
import Gargantext.Components.PhyloExplorer.Types (DisplayView(..), PhyloDataSet(..), ExtractedTerm, ExtractedCount, Source, Term, sortSources) import Gargantext.Components.PhyloExplorer.Types (DisplayView(..), PhyloDataSet(..), ExtractedTerm, ExtractedCount, Source, Term, sortSources)
import Gargantext.Hooks.FirstEffect (useFirstEffect') import Gargantext.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Hooks.UpdateEffect (useUpdateEffect1') import Gargantext.Hooks.UpdateEffect (useUpdateEffect1')
import Gargantext.Types (NodeID) import Gargantext.Types (NodeID, SidePanelState(..))
import Gargantext.Utils (getter, (?)) import Gargantext.Utils (getter, (?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Graphics.D3.Base (d3) import Graphics.D3.Base (d3)
...@@ -46,6 +46,7 @@ layoutCpt = here.component "layout" cpt where ...@@ -46,6 +46,7 @@ layoutCpt = here.component "layout" cpt where
cpt { phyloDataSet: (PhyloDataSet o) cpt { phyloDataSet: (PhyloDataSet o)
, nodeId , nodeId
} _ = do } _ = do
-- States -- States
--------- ---------
...@@ -82,7 +83,7 @@ layoutCpt = here.component "layout" cpt where ...@@ -82,7 +83,7 @@ layoutCpt = here.component "layout" cpt where
R2.useBox' false R2.useBox' false
sideBarDisplayed /\ sideBarDisplayedBox <- sideBarDisplayed /\ sideBarDisplayedBox <-
R2.useBox' false R2.useBox' InitialClosed
extractedTerms /\ extractedTermsBox <- extractedTerms /\ extractedTermsBox <-
R2.useBox' (mempty :: Array ExtractedTerm) R2.useBox' (mempty :: Array ExtractedTerm)
...@@ -256,7 +257,7 @@ layoutCpt = here.component "layout" cpt where ...@@ -256,7 +257,7 @@ layoutCpt = here.component "layout" cpt where
{ className: "phylo__sidebar" { className: "phylo__sidebar"
-- @XXX: ReactJS lack of "keep-alive" feature workaround solution -- @XXX: ReactJS lack of "keep-alive" feature workaround solution
-- @link https://github.com/facebook/react/issues/12039 -- @link https://github.com/facebook/react/issues/12039
, style: { display: sideBarDisplayed ? "block" $ "none" } , style: { display: sideBarDisplayed == Opened? "block" $ "none" }
} }
[ [
sideBar sideBar
......
...@@ -86,7 +86,7 @@ component = R.hooksComponent componentName cpt where ...@@ -86,7 +86,7 @@ component = R.hooksComponent componentName cpt where
B.caveat B.caveat
{ className: "phylo-selection-tab__nil" } { className: "phylo-selection-tab__nil" }
[ [
H.text "No selection has been made" H.text "Select term, branch or source to get their informations"
] ]
, ,
-- Selected source -- Selected source
...@@ -212,8 +212,7 @@ component = R.hooksComponent componentName cpt where ...@@ -212,8 +212,7 @@ component = R.hooksComponent componentName cpt where
{ className: "phylo-selection-tab__separator" } { className: "phylo-selection-tab__separator" }
[ [
B.icon B.icon
{ name: "angle-down" { name: "angle-down" }
}
] ]
, ,
-- No extracted result -- No extracted result
...@@ -291,6 +290,7 @@ component = R.hooksComponent componentName cpt where ...@@ -291,6 +290,7 @@ component = R.hooksComponent componentName cpt where
] ]
, ,
R2.if' (truncateResults) $ R2.if' (truncateResults) $
B.button B.button
{ variant: ButtonVariant Light { variant: ButtonVariant Light
, callback: \_ -> T.modify_ not showMoreBox , callback: \_ -> T.modify_ not showMoreBox
......
...@@ -4,15 +4,14 @@ module Gargantext.Components.PhyloExplorer.SideBar ...@@ -4,15 +4,14 @@ module Gargantext.Components.PhyloExplorer.SideBar
import Gargantext.Prelude import Gargantext.Prelude
import Data.Foldable (intercalate)
import Data.Maybe (Maybe) import Data.Maybe (Maybe)
import Data.Tuple.Nested ((/\)) import Data.Tuple.Nested ((/\))
import Effect (Effect) import Effect (Effect)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.PhyloExplorer.DetailsTab (detailsTab) import Gargantext.Components.PhyloExplorer.DetailsTab (detailsTab)
import Gargantext.Components.PhyloExplorer.SelectionTab (selectionTab) import Gargantext.Components.PhyloExplorer.SelectionTab (selectionTab)
import Gargantext.Components.PhyloExplorer.Types (ExtractedCount, ExtractedTerm, TabView(..)) import Gargantext.Components.PhyloExplorer.Types (ExtractedCount, ExtractedTerm, TabView(..))
import Gargantext.Types (NodeID) import Gargantext.Types (NodeID)
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
...@@ -48,60 +47,27 @@ component = R.hooksComponent componentName cpt where ...@@ -48,60 +47,27 @@ component = R.hooksComponent componentName cpt where
-- States -- States
tabView /\ tabViewBox <- R2.useBox' DetailsTab tabView /\ tabViewBox <- R2.useBox' DetailsTab
-- Computed
let
tabList = [ DetailsTab, SelectionTab ]
-- Render -- Render
pure $ pure $
H.div H.div
{ className: "phylo-sidebar" } { className: "phylo-sidebar" }
[ [
-- Teasers
H.div
{ className: "phylo-sidebar__top-teaser" }
[]
,
-- Menu -- Menu
H.ul B.tabs
{ className: intercalate " " { value: tabView
[ "nav nav-tabs" , list: tabList
, "phylo-sidebar__menu" , callback: flip T.write_ tabViewBox
]
}
[
H.li
{ className: "nav-item"
, on: { click: \_ -> T.write_ DetailsTab tabViewBox }
}
[
H.a
{ className: intercalate " "
[ "nav-link"
, tabView == DetailsTab ? "active" $ ""
]
}
[
H.text "Details"
]
]
,
H.li
{ className: "nav-item"
, on: { click: \_ -> T.write_ SelectionTab tabViewBox }
}
[
H.a
{ className: intercalate " "
[ "nav-link"
, tabView == SelectionTab ? "active" $ ""
]
} }
[
H.text "Selection"
]
]
]
, ,
-- Details tab -- Content
R2.if' (tabView == DetailsTab) $ case tabView of
DetailsTab ->
detailsTab detailsTab
{ key: (show props.nodeId) <> "-details" { key: (show props.nodeId) <> "-details"
, docCount: props.docCount , docCount: props.docCount
...@@ -111,9 +77,8 @@ component = R.hooksComponent componentName cpt where ...@@ -111,9 +77,8 @@ component = R.hooksComponent componentName cpt where
, groupCount: props.groupCount , groupCount: props.groupCount
, branchCount: props.branchCount , branchCount: props.branchCount
} }
,
-- Selection tab SelectionTab ->
R2.if' (tabView == SelectionTab) $
selectionTab selectionTab
{ key: (show props.nodeId) <> "-selection" { key: (show props.nodeId) <> "-selection"
, extractedTerms: props.extractedTerms , extractedTerms: props.extractedTerms
...@@ -123,9 +88,4 @@ component = R.hooksComponent componentName cpt where ...@@ -123,9 +88,4 @@ component = R.hooksComponent componentName cpt where
, selectedSource: props.selectedSource , selectedSource: props.selectedSource
, selectTermCallback: props.selectTermCallback , selectTermCallback: props.selectTermCallback
} }
,
-- Teaser
H.div
{ className: "phylo-sidebar__bottom-teaser" }
[]
] ]
...@@ -9,6 +9,7 @@ import Effect (Effect) ...@@ -9,6 +9,7 @@ import Effect (Effect)
import Gargantext.Components.Bootstrap as B import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), ComponentStatus(..), Variant(..)) import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), ComponentStatus(..), Variant(..))
import Gargantext.Components.PhyloExplorer.Types (Term(..), Source(..)) import Gargantext.Components.PhyloExplorer.Types (Term(..), Source(..))
import Gargantext.Types (SidePanelState(..), toggleSidePanelState)
import Gargantext.Utils ((?)) import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix (nothing) import Reactix (nothing)
...@@ -27,7 +28,7 @@ type Props = ...@@ -27,7 +28,7 @@ type Props =
, resultCallback :: Maybe Term -> Effect Unit , resultCallback :: Maybe Term -> Effect Unit
, toolBar :: T.Box (Boolean) , toolBar :: T.Box (Boolean)
, sideBar :: T.Box (Boolean) , sideBar :: T.Box (SidePanelState)
) )
here :: R2.Here here :: R2.Here
...@@ -71,12 +72,12 @@ component = here.component "main" cpt where ...@@ -71,12 +72,12 @@ component = here.component "main" cpt where
-- Sidebar toggle -- Sidebar toggle
B.button B.button
{ className: "phylo-topbar__sidebar" { className: "phylo-topbar__sidebar"
, callback: \_ -> T.modify_ (not) sideBar , callback: \_ -> T.modify_ (toggleSidePanelState) sideBar
, variant: sideBar' ? , variant: sideBar' == Opened ?
ButtonVariant Light $ ButtonVariant Light $
OutlinedButtonVariant Light OutlinedButtonVariant Light
} }
[ H.text $ sideBar' ? "Hide sidebar" $ "Show sidebar" ] [ H.text $ sideBar' == Opened ? "Hide sidebar" $ "Show sidebar" ]
, ,
-- Source -- Source
H.div H.div
......
...@@ -469,6 +469,9 @@ data TabView ...@@ -469,6 +469,9 @@ data TabView
derive instance Generic TabView _ derive instance Generic TabView _
derive instance Eq TabView derive instance Eq TabView
instance Show TabView where
show DetailsTab = "Legend"
show SelectionTab = "Data"
----------------------------------------------------------- -----------------------------------------------------------
......
...@@ -158,14 +158,14 @@ renderScale :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange ...@@ -158,14 +158,14 @@ renderScale :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange
renderScale ref {width,height} (Range.Closed {min, max}) = renderScale ref {width,height} (Range.Closed {min, max}) =
H.div { ref, className, width, height, aria } [] H.div { ref, className, width, height, aria } []
where where
className = "scale" className = "range-slider__scale"
aria = { label: "Scale running from " <> show min <> " to " <> show max } aria = { label: "Scale running from " <> show min <> " to " <> show max }
renderScaleSel :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element renderScaleSel :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScaleSel ref props (Range.Closed {min, max}) = renderScaleSel ref props (Range.Closed {min, max}) =
H.div { ref, className, style} [] H.div { ref, className, style} []
where where
className = "scale-sel" className = "range-slider__scale-sel"
style = {left: computeLeft, width: computeWidth} style = {left: computeLeft, width: computeWidth}
percOffsetMin = Range.normalise props.bounds min percOffsetMin = Range.normalise props.bounds min
percOffsetMax = Range.normalise props.bounds max percOffsetMax = Range.normalise props.bounds max
...@@ -176,7 +176,7 @@ renderScaleSel ref props (Range.Closed {min, max}) = ...@@ -176,7 +176,7 @@ renderScaleSel ref props (Range.Closed {min, max}) =
renderKnob :: Knob -> R.Ref (Nullable DOM.Element) -> Range.NumberRange -> Bounds -> T.Box (Maybe Knob) -> Int -> R.Element renderKnob :: Knob -> R.Ref (Nullable DOM.Element) -> Range.NumberRange -> Bounds -> T.Box (Maybe Knob) -> Int -> R.Element
renderKnob knob ref (Range.Closed value) bounds set precision = renderKnob knob ref (Range.Closed value) bounds set precision =
H.div { ref, tabIndex, className, aria, on: { mouseDown: onMouseDown }, style } [ H.div { ref, tabIndex, className, aria, on: { mouseDown: onMouseDown }, style } [
H.div { className: "button" } H.div { className: "range-slider__placeholder" }
[ [
H.text $ text $ toFixed precision val H.text $ text $ toFixed precision val
] ]
...@@ -185,7 +185,7 @@ renderKnob knob ref (Range.Closed value) bounds set precision = ...@@ -185,7 +185,7 @@ renderKnob knob ref (Range.Closed value) bounds set precision =
text (Just num) = num text (Just num) = num
text Nothing = "error" text Nothing = "error"
tabIndex = 0 tabIndex = 0
className = "knob" className = "range-slider__knob"
aria = { label: labelPrefix knob <> "value: " <> show val } aria = { label: labelPrefix knob <> "value: " <> show val }
labelPrefix MinKnob = "Minimum " labelPrefix MinKnob = "Minimum "
labelPrefix MaxKnob = "Maximum " labelPrefix MaxKnob = "Maximum "
...@@ -220,4 +220,3 @@ roundRange :: Epsilon -> Bounds -> Range.NumberRange -> Range.NumberRange ...@@ -220,4 +220,3 @@ roundRange :: Epsilon -> Bounds -> Range.NumberRange -> Range.NumberRange
roundRange epsilon bounds (Range.Closed initial) = Range.Closed { min, max } roundRange epsilon bounds (Range.Closed initial) = Range.Closed { min, max }
where min = round epsilon bounds initial.min where min = round epsilon bounds initial.min
max = round epsilon bounds initial.max max = round epsilon bounds initial.max
...@@ -6,17 +6,13 @@ import Data.Array (filter, length) ...@@ -6,17 +6,13 @@ import Data.Array (filter, length)
import Data.Array as A import Data.Array as A
import Data.Foldable (intercalate) import Data.Foldable (intercalate)
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..))
import Data.Tuple.Nested ((/\))
import Data.UUID (UUID) import Data.UUID (UUID)
import Data.UUID as UUID import Data.UUID as UUID
import Effect (Effect) import Effect (Effect)
import Gargantext.Components.App.Data (Boxes) import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.ErrorsView (errorsView) import Gargantext.Components.ErrorsView (errorsView)
import Gargantext.Components.Footer (footer) import Gargantext.Components.Footer (footer)
import Gargantext.Components.Forest as Forest import Gargantext.Components.Forest (forestLayout)
import Gargantext.Components.GraphExplorer as GraphExplorer
import Gargantext.Components.GraphExplorer.Sidebar as GES
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.Login (login) import Gargantext.Components.Login (login)
import Gargantext.Components.Nodes.Annuaire (annuaireLayout) import Gargantext.Components.Nodes.Annuaire (annuaireLayout)
import Gargantext.Components.Nodes.Annuaire.User (userLayout) import Gargantext.Components.Nodes.Annuaire.User (userLayout)
...@@ -25,7 +21,8 @@ import Gargantext.Components.Nodes.Corpus (corpusLayout) ...@@ -25,7 +21,8 @@ import Gargantext.Components.Nodes.Corpus (corpusLayout)
import Gargantext.Components.Nodes.Corpus.Code (corpusCodeLayout) import Gargantext.Components.Nodes.Corpus.Code (corpusCodeLayout)
import Gargantext.Components.Nodes.Corpus.Dashboard (dashboardLayout) import Gargantext.Components.Nodes.Corpus.Dashboard (dashboardLayout)
import Gargantext.Components.Nodes.Corpus.Document (documentMainLayout) import Gargantext.Components.Nodes.Corpus.Document (documentMainLayout)
import Gargantext.Components.Nodes.Corpus.Phylo as PhyloExplorer import Gargantext.Components.Nodes.Corpus.Graph (graphLayout)
import Gargantext.Components.Nodes.Corpus.Phylo (phyloLayout)
import Gargantext.Components.Nodes.File (fileLayout) import Gargantext.Components.Nodes.File (fileLayout)
import Gargantext.Components.Nodes.Frame (frameLayout) import Gargantext.Components.Nodes.Frame (frameLayout)
import Gargantext.Components.Nodes.Home (homeLayout) import Gargantext.Components.Nodes.Home (homeLayout)
...@@ -42,7 +39,6 @@ import Gargantext.Routes as GR ...@@ -42,7 +39,6 @@ import Gargantext.Routes as GR
import Gargantext.Sessions (Session, WithSession) import Gargantext.Sessions (Session, WithSession)
import Gargantext.Sessions as Sessions import Gargantext.Sessions as Sessions
import Gargantext.Types (CorpusId, Handed(..), ListId, NodeID, NodeType(..), SessionId, SidePanelState(..)) import Gargantext.Types (CorpusId, Handed(..), ListId, NodeID, NodeType(..), SessionId, SidePanelState(..))
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix (getElementById) import Gargantext.Utils.Reactix (getElementById)
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
...@@ -67,10 +63,11 @@ router :: R2.Leaf Props ...@@ -67,10 +63,11 @@ router :: R2.Leaf Props
router = R2.leafComponent routerCpt router = R2.leafComponent routerCpt
routerCpt :: R.Component Props routerCpt :: R.Component Props
routerCpt = here.component "router" cpt where routerCpt = here.component "router" cpt where
cpt { boxes: boxes@{ handed, showLogin } } _ = do cpt { boxes: boxes@{ handed, showLogin, showTree } } _ = do
-- States -- States
handed' <- T.useLive T.unequal handed handed' <- R2.useLive' handed
showLogin' <- R2.useLive' showLogin showLogin' <- R2.useLive' showLogin
showTree' <- R2.useLive' showTree
-- Effects -- Effects
let let
...@@ -100,75 +97,22 @@ routerCpt = here.component "router" cpt where ...@@ -100,75 +97,22 @@ routerCpt = here.component "router" cpt where
, TopBar.topBar { boxes } , TopBar.topBar { boxes }
, errorsView { errors: boxes.errors } [] , errorsView { errors: boxes.errors } []
, H.div { className: "router__inner" } , H.div { className: "router__inner" }
[ forest { boxes } [
, mainPage { boxes } -- @XXX: ReactJS lack of "keep-alive" feature workaround solution
, sidePanel { boxes } -- @link https://github.com/facebook/react/issues/12039
-- ↓
-- @XXX: ReactJS "display: none" don't exec effect cleaning function
-- (therefore cannot use the simple "display: none" workaround
-- to keep below component alive)
R2.if' (showTree') $ forest { boxes }
,
mainPage { boxes }
,
sidePanel { boxes }
] ]
] ]
-- router :: R2.Leaf Props --------------------------------------------------------------
-- router = R2.leaf routerCpt
-- routerCpt :: R.Memo Props
-- routerCpt = R.memo' $ here.component "router" cpt where
-- cpt { boxes: boxes@{ handed, showLogin } } _ = do
-- -- States
-- handed' <- T.useLive T.unequal handed
-- showLogin' <- R2.useLive' showLogin
-- -- Render
-- pure $
-- router'
-- { handed: handed'
-- , showLogin: showLogin'
-- , boxes
-- , key: "lol"
-- }
-- type CloneProps =
-- ( showLogin :: Boolean
-- , handed :: Handed
-- , key :: String
-- | Props
-- )
-- router' :: R2.Leaf CloneProps
-- router' = R2.leaf routerCpt'
-- routerCpt' :: R.Memo CloneProps
-- routerCpt' = R.memo' $ here.component "__clone__" cpt where
-- cpt { boxes, handed, showLogin } _ = do
-- -- Effects
-- let
-- handedClassName = case _ of
-- LeftHanded -> "left-handed"
-- RightHanded -> "right-handed"
-- R.useEffect1' handed $
-- getElementById "app" >>= case _ of
-- Nothing -> pure unit
-- Just app -> do
-- R2.removeClass app
-- [ handedClassName LeftHanded
-- , handedClassName RightHanded
-- ]
-- R2.addClass app [ handedClassName handed ]
-- -- Render
-- -- pure $ R2.fragmentWithKey "lol"
-- pure $ H.div {}
-- [
-- -- loginModal { boxes }
-- R2.if' showLogin $
-- login' boxes
-- , TopBar.topBar { boxes }
-- , errorsView { errors: boxes.errors } []
-- , H.div { className: "router-inner" }
-- [ forest { boxes }
-- , mainPage { boxes }
-- , sidePanel { boxes }
-- ]
-- ]
mainPage :: R2.Leaf Props mainPage :: R2.Leaf Props
mainPage = R2.leafComponent mainPageCpt mainPage = R2.leafComponent mainPageCpt
...@@ -184,7 +128,10 @@ mainPageCpt = here.component "mainPage" cpt where ...@@ -184,7 +128,10 @@ mainPageCpt = here.component "mainPage" cpt where
findTile :: UUID -> Record Tile -> Boolean findTile :: UUID -> Record Tile -> Boolean
findTile id tile = eq id $ get (Proxy :: Proxy "id") tile findTile id tile = eq id $ get (Proxy :: Proxy "id") tile
deleteTile :: Record Tile -> T.Box (Array (Record Tile)) -> (Unit -> Effect Unit) deleteTile ::
Record Tile
-> T.Box (Array (Record Tile))
-> (Unit -> Effect Unit)
deleteTile tile listBox = const do deleteTile tile listBox = const do
list <- T.read listBox list <- T.read listBox
newList <- pure $ filter (_ # tile.id # findTile # not) list newList <- pure $ filter (_ # tile.id # findTile # not) list
...@@ -258,15 +205,13 @@ mainPageCpt = here.component "mainPage" cpt where ...@@ -258,15 +205,13 @@ mainPageCpt = here.component "mainPage" cpt where
] ]
] ]
--------------------------------------------------------------
forest :: R2.Leaf Props forest :: R2.Leaf Props
forest = R2.leaf forestCpt forest = R2.leaf forestCpt
forestCpt :: R.Memo Props forestCpt :: R.Memo Props
forestCpt = R.memo' $ here.component "forest" cpt where forestCpt = R.memo' $ here.component "forest" cpt where
cpt { boxes: boxes@{ showTree } } _ = do cpt { boxes } _ = do
-- States
showTree' <- T.useLive T.unequal showTree
-- Hooks -- Hooks
resizeHandler <- useResizeHandler resizeHandler <- useResizeHandler
...@@ -279,13 +224,9 @@ forestCpt = R.memo' $ here.component "forest" cpt where ...@@ -279,13 +224,9 @@ forestCpt = R.memo' $ here.component "forest" cpt where
pure $ pure $
H.div H.div
{ className: intercalate " " { className: "router__aside" }
[ "router__aside"
, showTree' ? "" $ "d-none"
]
}
[ [
Forest.forestLayout forestLayout
{ boxes { boxes
, frontends: defaultFrontends , frontends: defaultFrontends
} }
...@@ -300,6 +241,8 @@ forestCpt = R.memo' $ here.component "forest" cpt where ...@@ -300,6 +241,8 @@ forestCpt = R.memo' $ here.component "forest" cpt where
] ]
] ]
--------------------------------------------------------------
sidePanel :: R2.Leaf Props sidePanel :: R2.Leaf Props
sidePanel = R2.leafComponent sidePanelCpt sidePanel = R2.leafComponent sidePanelCpt
sidePanelCpt :: R.Component Props sidePanelCpt :: R.Component Props
...@@ -316,6 +259,8 @@ sidePanelCpt = here.component "sidePanel" cpt where ...@@ -316,6 +259,8 @@ sidePanelCpt = here.component "sidePanel" cpt where
Opened -> pure $ openedSidePanel (Record.merge { session: s } props) [] Opened -> pure $ openedSidePanel (Record.merge { session: s } props) []
_ -> pure $ H.div {} [] _ -> pure $ H.div {} []
--------------------------------------------------------------
type RenderRouteProps = type RenderRouteProps =
( route :: AppRoute ( route :: AppRoute
| Props | Props
...@@ -360,6 +305,8 @@ renderRouteCpt = here.component "renderRoute" cpt where ...@@ -360,6 +305,8 @@ renderRouteCpt = here.component "renderRoute" cpt where
GR.UserPage s n -> user (sessionNodeProps s n) [] GR.UserPage s n -> user (sessionNodeProps s n) []
] ]
--------------------------------------------------------------
type AuthedProps = type AuthedProps =
( content :: Session -> R.Element ( content :: Session -> R.Element
| SessionProps ) | SessionProps )
...@@ -383,18 +330,17 @@ authedCpt = here.component "authed" cpt where ...@@ -383,18 +330,17 @@ authedCpt = here.component "authed" cpt where
where where
homeProps = RE.pick props :: Record Props homeProps = RE.pick props :: Record Props
--------------------------------------------------------------
openedSidePanel :: R2.Component (WithSession Props) openedSidePanel :: R2.Component (WithSession Props)
openedSidePanel = R.createElement openedSidePanelCpt openedSidePanel = R.createElement openedSidePanelCpt
openedSidePanelCpt :: R.Component (WithSession Props) openedSidePanelCpt :: R.Component (WithSession Props)
openedSidePanelCpt = here.component "openedSidePanel" cpt where openedSidePanelCpt = here.component "openedSidePanel" cpt where
cpt { boxes: boxes@{ route cpt { boxes: boxes@{ route
, sidePanelGraph
, sidePanelState , sidePanelState
, sidePanelTexts } , sidePanelTexts }
, session } _ = do , session } _ = do
{ mGraph, mMetaData } <- GEST.focusedSidePanel sidePanelGraph
mGraph' <- T.useLive T.unequal mGraph
mGraphMetaData' <- T.useLive T.unequal mMetaData
route' <- T.useLive T.unequal route route' <- T.useLive T.unequal route
let wrapper = H.div { className: "side-panel" } let wrapper = H.div { className: "side-panel" }
...@@ -404,19 +350,6 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where ...@@ -404,19 +350,6 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where
pure $ wrapper pure $ wrapper
[ Lists.sidePanel { session [ Lists.sidePanel { session
, sidePanelState } [] ] , sidePanelState } [] ]
GR.PGraphExplorer _s g -> do
case (mGraph' /\ mGraphMetaData') of
(Nothing /\ _) -> pure $ wrapper []
(_ /\ Nothing) -> pure $ wrapper []
(Just graph /\ Just metaData) -> do
pure $ wrapper
[ GES.sidebar { boxes
, frontends: defaultFrontends
, graph
, graphId: g
, metaData
, session
} [] ]
GR.NodeTexts _s _n -> do GR.NodeTexts _s _n -> do
pure $ wrapper pure $ wrapper
[ Texts.textsSidePanel { boxes [ Texts.textsSidePanel { boxes
...@@ -424,6 +357,8 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where ...@@ -424,6 +357,8 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where
, sidePanel: sidePanelTexts } [] ] , sidePanel: sidePanelTexts } [] ]
_ -> pure $ wrapper [] _ -> pure $ wrapper []
--------------------------------------------------------------
annuaire :: R2.Component SessionNodeProps annuaire :: R2.Component SessionNodeProps
annuaire = R.createElement annuaireCpt annuaire = R.createElement annuaireCpt
annuaireCpt :: R.Component SessionNodeProps annuaireCpt :: R.Component SessionNodeProps
...@@ -435,6 +370,8 @@ annuaireCpt = here.component "annuaire" cpt where ...@@ -435,6 +370,8 @@ annuaireCpt = here.component "annuaire" cpt where
, nodeId , nodeId
, session } } sessionProps) [] , session } } sessionProps) []
--------------------------------------------------------------
corpus :: R2.Component SessionNodeProps corpus :: R2.Component SessionNodeProps
corpus = R.createElement corpusCpt corpus = R.createElement corpusCpt
corpusCpt :: R.Component SessionNodeProps corpusCpt :: R.Component SessionNodeProps
...@@ -446,6 +383,8 @@ corpusCpt = here.component "corpus" cpt where ...@@ -446,6 +383,8 @@ corpusCpt = here.component "corpus" cpt where
, nodeId , nodeId
, session } } sessionProps) [] , session } } sessionProps) []
--------------------------------------------------------------
corpusCode :: R2.Component SessionNodeProps corpusCode :: R2.Component SessionNodeProps
corpusCode = R.createElement corpusCodeCpt corpusCode = R.createElement corpusCodeCpt
corpusCodeCpt :: R.Component SessionNodeProps corpusCodeCpt :: R.Component SessionNodeProps
...@@ -465,6 +404,8 @@ corpusCodeCpt = here.component "corpusCode" cpt where ...@@ -465,6 +404,8 @@ corpusCodeCpt = here.component "corpusCode" cpt where
pure $ authed authedProps [] pure $ authed authedProps []
--------------------------------------------------------------
type CorpusDocumentProps = type CorpusDocumentProps =
( corpusId :: CorpusId ( corpusId :: CorpusId
, listId :: ListId , listId :: ListId
...@@ -484,6 +425,8 @@ corpusDocumentCpt = here.component "corpusDocument" cpt ...@@ -484,6 +425,8 @@ corpusDocumentCpt = here.component "corpusDocument" cpt
, nodeId , nodeId
, session } [] } sessionProps )[] , session } [] } sessionProps )[]
--------------------------------------------------------------
dashboard :: R2.Component SessionNodeProps dashboard :: R2.Component SessionNodeProps
dashboard = R.createElement dashboardCpt dashboard = R.createElement dashboardCpt
dashboardCpt :: R.Component SessionNodeProps dashboardCpt :: R.Component SessionNodeProps
...@@ -494,6 +437,8 @@ dashboardCpt = here.component "dashboard" cpt ...@@ -494,6 +437,8 @@ dashboardCpt = here.component "dashboard" cpt
pure $ authed (Record.merge { content: \session -> pure $ authed (Record.merge { content: \session ->
dashboardLayout { boxes, nodeId, session } [] } sessionProps) [] dashboardLayout { boxes, nodeId, session } [] } sessionProps) []
--------------------------------------------------------------
type DocumentProps = ( listId :: ListId | SessionNodeProps ) type DocumentProps = ( listId :: ListId | SessionNodeProps )
document :: R2.Component DocumentProps document :: R2.Component DocumentProps
...@@ -508,6 +453,8 @@ documentCpt = here.component "document" cpt where ...@@ -508,6 +453,8 @@ documentCpt = here.component "document" cpt where
, mCorpusId: Nothing , mCorpusId: Nothing
, session } [] } sessionProps) [] , session } [] } sessionProps) []
--------------------------------------------------------------
graphExplorer :: R2.Component SessionNodeProps graphExplorer :: R2.Component SessionNodeProps
graphExplorer = R.createElement graphExplorerCpt graphExplorer = R.createElement graphExplorerCpt
graphExplorerCpt :: R.Component SessionNodeProps graphExplorerCpt :: R.Component SessionNodeProps
...@@ -520,17 +467,19 @@ graphExplorerCpt = here.component "graphExplorer" cpt where ...@@ -520,17 +467,19 @@ graphExplorerCpt = here.component "graphExplorer" cpt where
authedProps = authedProps =
Record.merge Record.merge
{ content: { content:
\session -> GraphExplorer.explorerLayoutWithKey \session -> graphLayout
{ boxes { boxes
, graphId: nodeId , graphId: nodeId
, key: "graphId-" <> show nodeId , key: "graphId-" <> show nodeId
, session } , session
[] }
} }
sessionProps sessionProps
pure $ authed authedProps [] pure $ authed authedProps []
--------------------------------------------------------------
phyloExplorer :: R2.Component SessionNodeProps phyloExplorer :: R2.Component SessionNodeProps
phyloExplorer = R.createElement phyloExplorerCpt phyloExplorer = R.createElement phyloExplorerCpt
...@@ -544,7 +493,7 @@ phyloExplorerCpt = here.component "phylo" cpt where ...@@ -544,7 +493,7 @@ phyloExplorerCpt = here.component "phylo" cpt where
authedProps = authedProps =
Record.merge Record.merge
{ content: { content:
\session -> PhyloExplorer.phyloLayout \session -> phyloLayout
{ boxes { boxes
, nodeId , nodeId
, session , session
...@@ -554,6 +503,7 @@ phyloExplorerCpt = here.component "phylo" cpt where ...@@ -554,6 +503,7 @@ phyloExplorerCpt = here.component "phylo" cpt where
pure $ authed authedProps [] pure $ authed authedProps []
--------------------------------------------------------------
home :: R2.Component Props home :: R2.Component Props
home = R.createElement homeCpt home = R.createElement homeCpt
...@@ -562,6 +512,8 @@ homeCpt = here.component "home" cpt where ...@@ -562,6 +512,8 @@ homeCpt = here.component "home" cpt where
cpt { boxes } _ = do cpt { boxes } _ = do
pure $ homeLayout { boxes } pure $ homeLayout { boxes }
--------------------------------------------------------------
lists :: R2.Component SessionNodeProps lists :: R2.Component SessionNodeProps
lists = R.createElement listsCpt lists = R.createElement listsCpt
listsCpt :: R.Component SessionNodeProps listsCpt :: R.Component SessionNodeProps
...@@ -575,6 +527,8 @@ listsCpt = here.component "lists" cpt where ...@@ -575,6 +527,8 @@ listsCpt = here.component "lists" cpt where
, session , session
, sessionUpdate: \_ -> pure unit } [] } sessionProps) [] , sessionUpdate: \_ -> pure unit } [] } sessionProps) []
--------------------------------------------------------------
login' :: Boxes -> R.Element login' :: Boxes -> R.Element
login' { backend, sessions, showLogin: visible } = login' { backend, sessions, showLogin: visible } =
login { backend login { backend
...@@ -582,6 +536,8 @@ login' { backend, sessions, showLogin: visible } = ...@@ -582,6 +536,8 @@ login' { backend, sessions, showLogin: visible } =
, sessions , sessions
, visible } , visible }
--------------------------------------------------------------
routeFile :: R2.Component SessionNodeProps routeFile :: R2.Component SessionNodeProps
routeFile = R.createElement routeFileCpt routeFile = R.createElement routeFileCpt
routeFileCpt :: R.Component SessionNodeProps routeFileCpt :: R.Component SessionNodeProps
...@@ -591,6 +547,8 @@ routeFileCpt = here.component "routeFile" cpt where ...@@ -591,6 +547,8 @@ routeFileCpt = here.component "routeFile" cpt where
pure $ authed (Record.merge { content: \session -> pure $ authed (Record.merge { content: \session ->
fileLayout { nodeId, session } } sessionProps) [] fileLayout { nodeId, session } } sessionProps) []
--------------------------------------------------------------
type RouteFrameProps = ( type RouteFrameProps = (
nodeType :: NodeType nodeType :: NodeType
| SessionNodeProps | SessionNodeProps
...@@ -605,6 +563,8 @@ routeFrameCpt = here.component "routeFrame" cpt where ...@@ -605,6 +563,8 @@ routeFrameCpt = here.component "routeFrame" cpt where
pure $ authed (Record.merge { content: \session -> pure $ authed (Record.merge { content: \session ->
frameLayout { nodeId, nodeType, session } } sessionProps) [] frameLayout { nodeId, nodeType, session } } sessionProps) []
--------------------------------------------------------------
team :: R2.Component SessionNodeProps team :: R2.Component SessionNodeProps
team = R.createElement teamCpt team = R.createElement teamCpt
teamCpt :: R.Component SessionNodeProps teamCpt :: R.Component SessionNodeProps
...@@ -616,6 +576,8 @@ teamCpt = here.component "team" cpt where ...@@ -616,6 +576,8 @@ teamCpt = here.component "team" cpt where
, nodeId , nodeId
, session } } sessionProps) [] , session } } sessionProps) []
--------------------------------------------------------------
texts :: R2.Component SessionNodeProps texts :: R2.Component SessionNodeProps
texts = R.createElement textsCpt texts = R.createElement textsCpt
textsCpt :: R.Component SessionNodeProps textsCpt :: R.Component SessionNodeProps
...@@ -630,6 +592,8 @@ textsCpt = here.component "texts" cpt ...@@ -630,6 +592,8 @@ textsCpt = here.component "texts" cpt
, nodeId , nodeId
, session } [] } sessionProps) [] , session } [] } sessionProps) []
--------------------------------------------------------------
user :: R2.Component SessionNodeProps user :: R2.Component SessionNodeProps
user = R.createElement userCpt user = R.createElement userCpt
userCpt :: R.Component SessionNodeProps userCpt :: R.Component SessionNodeProps
...@@ -643,6 +607,8 @@ userCpt = here.component "user" cpt where ...@@ -643,6 +607,8 @@ userCpt = here.component "user" cpt where
, nodeId , nodeId
, session } [] } sessionProps) [] , session } [] } sessionProps) []
--------------------------------------------------------------
type ContactProps = ( annuaireId :: NodeID | SessionNodeProps ) type ContactProps = ( annuaireId :: NodeID | SessionNodeProps )
contact :: R2.Component ContactProps contact :: R2.Component ContactProps
......
...@@ -9,6 +9,7 @@ import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..)) ...@@ -9,6 +9,7 @@ import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..))
import Gargantext.Components.Lang (langSwitcher, allFeLangs) import Gargantext.Components.Lang (langSwitcher, allFeLangs)
import Gargantext.Components.Themes (themeSwitcher, allThemes) import Gargantext.Components.Themes (themeSwitcher, allThemes)
import Gargantext.Types (Handed(..)) import Gargantext.Types (Handed(..))
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
...@@ -64,7 +65,9 @@ topBarCpt = here.component "topBar" cpt ...@@ -64,7 +65,9 @@ topBarCpt = here.component "topBar" cpt
] ]
, ,
B.button B.button
{ variant: ButtonVariant Light { variant: showTree' ?
ButtonVariant Light $
OutlinedButtonVariant Light
, callback: const $ T.modify_ (not) showTree , callback: const $ T.modify_ (not) showTree
, className: "main-topbar__tree-switcher" , className: "main-topbar__tree-switcher"
} }
......
...@@ -10,6 +10,8 @@ import Reactix (nothing, thenNothing) ...@@ -10,6 +10,8 @@ import Reactix (nothing, thenNothing)
import Reactix as R import Reactix as R
-- | Hook triggered on first mount event only -- | Hook triggered on first mount event only
-- |
-- | /!\ @TODO cleanup function not working
useFirstMount :: R.Hooks (Boolean) useFirstMount :: R.Hooks (Boolean)
useFirstMount = do useFirstMount = do
firstMount <- R.useRef true firstMount <- R.useRef true
......
module Gargantext.Utils.Popover where module Gargantext.Utils.Popover where
import Gargantext.Prelude
import DOM.Simple as DOM
import Data.Maybe (maybe) import Data.Maybe (maybe)
import Data.Nullable (Nullable, toMaybe) import Data.Nullable (Nullable, toMaybe)
import DOM.Simple as DOM
import Effect (Effect) import Effect (Effect)
import Effect.Uncurried (EffectFn2, runEffectFn2) import Effect.Uncurried (EffectFn2, runEffectFn2)
import Reactix as R import Reactix as R
import Record as Record
import Gargantext.Prelude
type PopoverRef = R.Ref (Nullable DOM.Element) type PopoverRef = R.Ref (Nullable DOM.Element)
...@@ -22,8 +23,11 @@ type Props = ...@@ -22,8 +23,11 @@ type Props =
foreign import popoverCpt :: R.Component Props foreign import popoverCpt :: R.Component Props
-- | https://github.com/vaheqelyan/react-awesome-popover
popover :: Record Props -> Array R.Element -> R.Element popover :: Record Props -> Array R.Element -> R.Element
popover = R.rawCreateElement popoverCpt popover props children = R.rawCreateElement popoverCpt props' children
where
props' = Record.merge props { className: "awesome-popover" }
foreign import _setState :: forall a. EffectFn2 DOM.Element a Unit foreign import _setState :: forall a. EffectFn2 DOM.Element a Unit
......
@import "./base/_reset.scss"
@import "./base/_general.scss"
@import "./base/_form.scss" @import "./base/_form.scss"
@import "./base/_layout.scss" @import "./base/_layout.scss"
@import "./base/_nav.scss" @import "./base/_nav.scss"
@import "./base/_typography.scss" @import "./base/_typography.scss"
@import "./base/_animations.scss" @import "./base/_animations.scss"
@import "./base/_bootstrap.scss"
@import "./base/_placeholder.scss" @import "./base/_placeholder.scss"
@import "./base/_utilities.scss"
@import "./base/_range_slider.sass"
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
@import "./_legacy/_tree" @import "./_legacy/_tree"
@import "./_legacy/_code_editor" @import "./_legacy/_code_editor"
@import "./_legacy/_styles" @import "./_legacy/_styles"
@import "./_legacy/_range_slider"
@import "./_legacy/_annotation" @import "./_legacy/_annotation"
@import "./_legacy/_folder_view" @import "./_legacy/_folder_view"
@import "./_legacy/_phylo" @import "./_legacy/_phylo"
@mixin sidePanelCommon /* Grid constants */
position: absolute
max-height: 600px $layout-height: calc(100vh - #{ $topbar-height} )
//overflow-y: scroll
top: 170px
//z-index: 1
#graph-explorer
margin-left: 20rem
margin-right: 20rem
padding-top: 0px
.graph-container
#sp-container
@include sidePanelCommon
border: 1px white solid
background-color: white
width: 28%
z-index: 15
#myTab
marginBottom: 18px
marginTop: 18px
#myTabContent
borderBottom: 1px solid black
paddingBottom : 19px
#horizontal-checkbox
ul
display: inline
float : left
.lefthanded #sp-container
left: 0%
.righthanded #sp-container
left: 70%
.graph-tree
@include sidePanelCommon
background-color: #fff
z-index: 1
.lefthanded .graph-tree
left: 80%
.righthanded .graph-tree
left: 0%
/* #toggle-container
/* position: fixed
/* z-index: 999 // needs to appear above solid menu bar
/* right: 25%
/* top: 10px
/* width: 50%
/* .container-fluid
/* padding-top: 90px
#controls-container
// position: fixed
position: absolute
// needs to appear above graph elements
z-index: 900
backdrop-filter: blur(4px)
background: rgba(255,255,255,75%)
// overflow: auto
left: 0
right: 0
top: 60px
.nav-item /////////////////////////////////////////////
padding-left: 0.8rem
.graph-row .graph-loader
height: 100vh width: 100%
height: 100%
#graph-view &__spinner
height: 95vh $size: 100px
$weight: 6px
#tree
position: absolute position: absolute
z-index: 1 font-size: $weight
height: $size
width: $size
// (?) `centered` mixin will not work here, due to Bootstrap process
// interfering with the transform rule
top: calc( 50% - #{ $size / 2 } )
left: calc( 50% - #{ $size / 2 } )
.input-with-autocomplete
.completions /////////////////////////////////////////////
.graph-layout
position: relative
width: 100%
height: $layout-height
&__sidebar
position: fixed position: fixed
max-height: 300px width: $sidebar-width
overflow-y: scroll height: $sidebar-height
width: 300px z-index: z-index('graph-layout', 'sidebar')
top: 50px
@include right-handed
right: 0
border-left: 1px solid $border-color
@include left-handed
left: 0
border-right: 1px solid $border-color
&__toolbar
position: absolute
z-index: z-index('graph-layout', 'toolbar')
background-color: $body-bg
width: 100%
border-bottom: 1px solid $border-color
.b-fieldset
background-color: $body-bg
&__content
width: 100%
height: 100%
/////////////////////////////////////////////
.graph-topbar .graph-topbar
@include aside-topbar @include aside-topbar
display: flex
padding-left: $topbar-item-margin
padding-right: $topbar-item-margin
&__toolbar,
&__sidebar
width: $topbar-fixed-button-width
margin-left: $topbar-item-margin
margin-right: $topbar-item-margin
&__search
width: $topbar-input-width
margin-left: $topbar-item-margin
margin-right: $topbar-item-margin
[type="submit"]
display: none
/////////////////////////////////////////////
.graph-sidebar
@include sidebar
$margin-x: $sidebar-tab-margin-x
$margin-y: space-x(2)
&__legend-tab
padding: $margin-y $margin-x
&__data-tab
padding: $margin-y $margin-x
&__community-tab
padding: $margin-y $margin-x
&__separator
margin-top: $margin-y
margin-bottom: $margin-y
color: $gray-500
text-align: center
.graph-legend
$legend-code-width: 24px
$legend-code-height: 12px
&__item
list-style: none
margin-bottom: space-x(0.75)
&__code
width: $legend-code-width
height: $legend-code-height
display: inline-block
margin-right: space-x(2.5)
border: 1px solid $gray-500
&__caption
vertical-align: top
.graph-documentation
&__text-section
margin-bottom: space-x(3)
font-size: 15px
line-height: 1.5
p
margin-bottom: space-x(1)
li
list-style-type: circle
padding-left: space-x(0.5)
margin-left: space-x(3)
line-height: 1.4
&:not(:last-child)
margin-bottom: space-x(1.5)
.graph-selected-nodes
&__item
&:not(:last-child)
margin-bottom: space-x(0.5)
&__badge
font-size: $font-size-base
white-space: normal
word-break: break-word
&__actions
$gutter: space-x(2)
display: flex
justify-content: space-around
.b-button
width: calc(50% - #{ space-x(2) / 2 } )
.graph-neighborhood
&__counter
font-weight: bold
&__badge
white-space: normal
word-break: break-word
&:not(:last-child)
margin-bottom: space-x(0.75)
&__show-more
margin-top: space-x(2)
/////////////////////////////////////////////
.graph-toolbar
$self: &
$section-margin: space-x(2)
$item-margin: space-x(1)
display: flex
padding: #{ $section-margin / 2 }
&__gap
width: $item-margin
display: inline-block
&__section
margin: #{ $section-margin / 2 }
// Selection settings
&--selection
.b-fieldset__content
display: flex
#{ $self }__gap
width: #{ $item-margin * 2 }
.range-simple
flex-grow: 1
// Controls
&--controls
.b-fieldset__content
position: relative
#{ $self }__gap
width: #{ $item-margin * 2 }
.range-control,
.range-simple
flex-basis: calc(50% - #{ $item-margin * 2 })
// Atlas button animation
.on-running-animation .b-icon
animation-name: pulse
animation-duration: 0.5s
animation-timing-function: ease
animation-direction: alternate
animation-iteration-count: infinite
animation-play-state: running
...@@ -92,9 +92,6 @@ li#rename ...@@ -92,9 +92,6 @@ li#rename
overflow: visible overflow: visible
height: auto height: auto
#graph-tree
.tree
margin-top: 27px
.nopadding .nopadding
padding: 0 !important padding: 0 !important
......
...@@ -9,6 +9,11 @@ ...@@ -9,6 +9,11 @@
&__tree-switcher &__tree-switcher
margin-right: space-x(1) margin-right: space-x(1)
&__tree-switcher
$fixed-width: 136px
width: $fixed-width
// add hovering effect // add hovering effect
&.navbar-dark .navbar-text:hover &.navbar-dark .navbar-text:hover
color: $navbar-dark-hover-color color: $navbar-dark-hover-color
......
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
/* Grid constants */ /* Grid constants */
$topbar-height: 56px; // ~ unworthy empirical value (@TODO topbar height calculation)
$graph-margin: 16px; $graph-margin: 16px;
$layout-height: calc(100vh - #{ $topbar-height} ); $layout-height: calc(100vh - #{ $topbar-height} );
...@@ -38,16 +37,6 @@ $left-column-width: 10%; ...@@ -38,16 +37,6 @@ $left-column-width: 10%;
$center-column-width: 85%; $center-column-width: 85%;
$right-column-width: 5%; $right-column-width: 5%;
/* Topbar constants */
$topbar-input-width: 304px;
/* Sidebar constant */
$sidebar-width: 480px;
$sidebar-height: calc(100vh - #{ $topbar-height });
$tab-margin-x: space-x(2.5);
/* Colors */ /* Colors */
$graph-background-color: $body-bg; $graph-background-color: $body-bg;
...@@ -448,34 +437,33 @@ $decreasing-color: #11638F; ...@@ -448,34 +437,33 @@ $decreasing-color: #11638F;
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
.phylo-topbar { .phylo-topbar {
$margin: space-x(0.5);
$fixed-button-width: 136px; $fixed-button-width: 136px;
@include aside-topbar(); @include aside-topbar();
padding-left: $margin; padding-left: $topbar-item-margin;
padding-right: $margin; padding-right: $topbar-item-margin;
display: flex; display: flex;
&__toolbar, &__toolbar,
&__sidebar { &__sidebar {
width: $fixed-button-width; width: $topbar-fixed-button-width;
margin-left: $margin; margin-left: $topbar-item-margin;
margin-right: $margin; margin-right: $topbar-item-margin;
} }
&__source { &__source {
width: $topbar-input-width; width: $topbar-input-width;
margin-left: $margin; margin-left: $topbar-item-margin;
margin-right: $margin; margin-right: $topbar-item-margin;
} }
&__autocomplete { &__autocomplete {
display: flex; display: flex;
width: $topbar-input-width; width: $topbar-input-width;
position: relative; position: relative;
margin-left: $margin; margin-left: $topbar-item-margin;
margin-right: $margin; margin-right: $topbar-item-margin;
} }
&__suggestion { &__suggestion {
...@@ -506,60 +494,11 @@ $decreasing-color: #11638F; ...@@ -506,60 +494,11 @@ $decreasing-color: #11638F;
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
.phylo-sidebar { .phylo-sidebar {
$teaser-height: 16px; @include sidebar;
background-color: $body-bg;
height: 100%;
// avoiding ugly scrollbar
scrollbar-width: none;
overflow-y: scroll;
overflow-x: visible;
&::-webkit-scrollbar {
display: none;
}
// adjust nav menu gutter (@TODO: generic?)
&__menu .nav-item {
&:first-child {
margin-left: space-x(2);
}
&:last-child {
margin-right: space-x(2);
}
}
// UX best pratice: when a lengthy column is overflowy hidden
// (with a scroll), a teaser UI element shows to the user that a scroll
// is possible
&__top-teaser {
@include top-teaser;
z-index: z-index('phylo-sidebar', 'teaser');
pointer-events: none;
position: sticky;
top: 0;
height: $teaser-height;
width: 100%;
}
&__bottom-teaser {
@include bottom-teaser;
z-index: z-index('phylo-sidebar', 'teaser');
pointer-events: none;
position: sticky;
bottom: 0;
height: $teaser-height;
width: 100%;
}
} }
.phylo-details-tab { .phylo-details-tab {
$margin-x: $tab-margin-x; $margin-x: $sidebar-tab-margin-x;
$margin-y: space-x(2); $margin-y: space-x(2);
&__counter { &__counter {
...@@ -585,7 +524,7 @@ $decreasing-color: #11638F; ...@@ -585,7 +524,7 @@ $decreasing-color: #11638F;
} }
.phylo-selection-tab { .phylo-selection-tab {
$margin-x: $tab-margin-x; $margin-x: $sidebar-tab-margin-x;
$margin-y: space-x(2); $margin-y: space-x(2);
&__highlight { &__highlight {
...@@ -609,7 +548,6 @@ $decreasing-color: #11638F; ...@@ -609,7 +548,6 @@ $decreasing-color: #11638F;
&__item { &__item {
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
// remove "_reboot.scss" line height
line-height: initial; line-height: initial;
&:not(:last-child) { &:not(:last-child) {
......
.range
width: 400px
/* some space for the right knob */
padding-right: 30px
.range-slider
position: relative
width: 85%
.scale
position: absolute
width: 100%
height: 3px
margin-top: 2px
background-color: #d8d8d8
.scale-sel
position: absolute
background-color: rgb(39, 196, 112)
width: 100%
height: 7px
.knob
position: absolute
cursor: pointer
-moz-user-select: none
-webkit-user-select: none
-ms-user-select: none
user-select: none
-o-user-select: none
margin-top: -4px
z-index: 1
box-shadow: 1px 1px 3px grey
.button
margin-top: -3px
background: #eee
width: 30px
height: 20px
.range-simple
input
width: 85%
...@@ -69,12 +69,6 @@ ...@@ -69,12 +69,6 @@
/* */ /* */
.btn-primary
color: white
background-color: #005a9aff
border-color: black
.frame .frame
height: 100vh height: 100vh
iframe iframe
......
...@@ -60,10 +60,9 @@ $node-popup-width: 544px ...@@ -60,10 +60,9 @@ $node-popup-width: 544px
//////////////////////////////////// ////////////////////////////////////
$leaf-margin-bottom: 3px
.mainleaf .mainleaf
$self: & $self: &
$leaf-margin-bottom: 3px
display: flex display: flex
align-items: center align-items: center
...@@ -168,11 +167,6 @@ $leaf-margin-bottom: 3px ...@@ -168,11 +167,6 @@ $leaf-margin-bottom: 3px
@include left-handed @include left-handed
margin-left: space-x(1) margin-left: space-x(1)
// @TODO: handle default <a> color (override "_reboot.scss" rules or
// whole file?)
a
color: $body-color
// preparing "before" content (see "&--selected", "&--file-dropped") // preparing "before" content (see "&--selected", "&--file-dropped")
&::before &::before
// margin for the before background // margin for the before background
...@@ -221,11 +215,10 @@ $leaf-margin-bottom: 3px ...@@ -221,11 +215,10 @@ $leaf-margin-bottom: 3px
&__settings-icon &__settings-icon
// altering component overlay offset to fit it with the ".mainleaf" overlay // altering component overlay offset to fit it with the ".mainleaf" overlay
// dimension // dimension
$aside-icon-offset-y: -5px $aside-icon-offset-top: -5px
&.b-icon-button--overlay::before &.b-icon-button--overlay::before
top: $aside-icon-offset-y top: $aside-icon-offset-top
bottom: $aside-icon-offset-y
//---------------------------- //----------------------------
...@@ -266,16 +259,13 @@ $leaf-margin-bottom: 3px ...@@ -266,16 +259,13 @@ $leaf-margin-bottom: 3px
//---------------------------- //----------------------------
&--selected &--selected &
#{ $self }__node-link::before &__node-link::before
content: "" content: ""
background-color: $gray-100 background-color: $gray-100
#{ $self }__node-icon &__node-link a
color: $primary
#{ $self }__node-link a
color: $primary color: $primary
font-weight: bold font-weight: bold
...@@ -291,7 +281,7 @@ $leaf-margin-bottom: 3px ...@@ -291,7 +281,7 @@ $leaf-margin-bottom: 3px
&--blank // for <Cloak> use cases &--blank // for <Cloak> use cases
$blank-color: $gray-100 $blank-color: $gray-100
$blank-link-width: 144px $blank-link-width: 120px // roughly max size of a truncated node link text
$blank-link-height: 12px $blank-link-height: 12px
$blank-link-offset-y: 5px $blank-link-offset-y: 5px
...@@ -311,6 +301,30 @@ $leaf-margin-bottom: 3px ...@@ -311,6 +301,30 @@ $leaf-margin-bottom: 3px
position: absolute position: absolute
top: $blank-link-offset-y top: $blank-link-offset-y
// (?) Chrome engine adds extra height to the overlay embedded settings icon
// → set an empirical icon overlay position
@media screen and (-webkit-min-device-pixel-ratio:0)
$icon-overlay-bottom: -5px
.awesome-popover
.b-icon-button--overlay::before
bottom: $icon-overlay-bottom
// (?) FireFox engine adds extra height to the embedded settings
// → set an empirical fixed height on its wrapper to avoid
// an height flickering alteration (on mainleaf hover) ;
// and modify the icon overlay position
@-moz-document url-prefix()
$simulated-mainleaf-overlay-height: 22.5px
$icon-overlay-bottom: -3px
.awesome-popover
max-height: $simulated-mainleaf-overlay-height
.b-icon-button--overlay::before
bottom: $icon-overlay-bottom
//////////////////////////////////////// ////////////////////////////////////////
......
...@@ -38,7 +38,6 @@ ...@@ -38,7 +38,6 @@
} }
/// Mixins /// Mixins
///-------------------------- ///--------------------------
...@@ -122,3 +121,13 @@ ...@@ -122,3 +121,13 @@
bottom: $value; bottom: $value;
left: $value; left: $value;
} }
/// Hidden vertical scrollbar
@mixin hidden-scrollbar() {
scrollbar-width: none;
overflow-y: scroll;
&::-webkit-scrollbar {
display: none;
}
}
@use 'sass:map';
/// Global spacing value /// Global spacing value
$space-unit: 8px; $space-unit: 8px;
/// Misc // Bootstrap color system
$overlay-radius: 5px; // * with added shades of gray
$white: #FFFFFF;
$gray-100: #F8F9FA;
$gray-150: #FAFAFA; // (+)
$gray-175: #F0F0F0; // (+)
$gray-200: #E9ECEF;
$gray-300: #DEE2E6;
$gray-400: #CED4DA;
$gray-500: #ADB5BD;
$gray-600: #6C757D;
$gray-700: #495057;
$gray-800: #343A40;
$gray-900: #212529;
$black: #000000;
/// Z-Index Management /// Z-Index Management
/// @link https://medium.com/alistapart/sassier-z-index-management-for-complex-layouts-4540717a9488 /// @link https://medium.com/alistapart/sassier-z-index-management-for-complex-layouts-4540717a9488
...@@ -16,7 +32,8 @@ $z-indexes: ( ...@@ -16,7 +32,8 @@ $z-indexes: (
"sidebar", "sidebar",
"spinner", "spinner",
), ),
phylo-sidebar: ( graph-layout: (
"teaser", "toolbar",
"sidebar",
), ),
); );
...@@ -46,3 +46,7 @@ ...@@ -46,3 +46,7 @@
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@keyframes pulse {
from { transform: scale(1); }
to { transform: scale(1.1); }
}
/* Bootstrap 4.x Generic Custom Styles */
// originally added to "table" tag on "_reboot.scss"
.table {
border-collapse: collapse;
}
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
label { label {
font-weight: 600; font-weight: 600;
margin-bottom: initial; // reset Bootstrap "_reboot.scss"
} }
&--sub { &--sub {
...@@ -83,3 +82,14 @@ ...@@ -83,3 +82,14 @@
color: $input-placeholder-color; color: $input-placeholder-color;
border-style: dashed; border-style: dashed;
} }
// Custom autocomplete (need UI/UX rework)
.input-with-autocomplete {
.completions {
position: fixed;
max-height: 300px;
overflow-y: scroll;
width: 300px;
top: 50px;
}
}
/// Opinionated box-sizing
/// @link https://www.paulirish.com/2012/box-sizing-border-box-ftw/
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
/// Make images easier to work with
/// @link https://hankchizljaw.com/wrote/a-modern-css-reset/
img {
max-width: 100%;
display: block;
}
/// Remove hyperlink very first interaction (not including in "_reset.scss"
/// as this interaction came as an intricated-native feature)
// a:focus,a:visited,a:active{
// outline: none;
// }
// Colors based on Chromium engine
// (every browser renders its input range on its own, here we are trying
// to add some consistency)
$range-bg-color: $gray-175
$range-border-color: $gray-400
$range-bg-progress-color: $secondary
$range-height: 8px
$range-radius: 3px
$knob-size: 14px
.range-control
display: inline-flex
flex-direction: column
vertical-align: top
&__label
font-size: 14px
color: $gray-700
margin-bottom: space-x(0.5)
// enhance hover area of ".range-slider" placeholder
// (cf ".range-slider")
&:hover .range-slider .range-slider__placeholder
display: block
///////////////////////////////////////////////
.range-slider
$self: &
$placeholder-offset-y: -32px
position: relative
width: 100%
height: $range-height
&__scale
position: absolute
width: inherit
height: inherit
background-color: $range-bg-color
border: 1px solid $range-border-color
border-radius: $range-radius
&__scale-sel
position: absolute
background-color: $range-bg-progress-color
width: inherit
height: inherit
border-radius: $range-radius
&__knob
@include unselectable
@include clickable
z-index: 1
background-color: $range-bg-progress-color
width: $knob-size
height: $knob-size
border-radius: 50%
position: absolute
box-shadow: 0 0 1px 0 $range-bg-progress-color
// alignement with the bar
transform: translateX(-5px) translateY(-3px)
&:hover
background-color: darken($range-bg-progress-color, 15%)
&__placeholder
display: none
background-color: $black
color: $white
position: absolute
padding: space-x(0.5) space-x(1)
text-align: center
font-size: 14px
font-weight: bold
border-radius: 5px
box-shadow: 0 0 1px 0 $black
opacity: 0.8
transform: translateX(-25%) translateY($placeholder-offset-y)
&:hover &
&__placeholder
display: block
////////////////////////////////////////////
.range-simple
display: inline-flex
flex-direction: column
vertical-align: top
position: relative
&__label
font-size: 14px
color: $gray-700
&__field
position: relative
////////////////////////////////////////////
// Cross browser rules
//
// (?) Range consistency issue between browsers UI engine: mostly rely on
// Chromium UI and adapt it to other engine
//
// Some examples:
// @link https://brennaobrien.com/blog/2014/05/style-input-type-range-in-every-browser.html
input[type=range]
@include clickable
width: 100%
// Styling FireFox input range, mimicking Chromium one
//
// (?) * unable to differenciate selector betwenn background range and progress
// one → set only background range
// * unable to position the input ideally → have to rely on
// absolute + position
input[type=range]
-webkit-appearance: none
input[type=range]::-webkit-slider-runnable-track
$chromium-offset-y: 4px
height: $range-height
border-radius: $range-radius
border: 1px solid $range-border-color
background: $range-bg-color
width: 100%
position: absolute
top: $chromium-offset-y
input[type=range]::-webkit-slider-thumb
$chromium-offset-y: -4px
-webkit-appearance: none
transform: translateY($chromium-offset-y)
border: none
height: $knob-size
width: $knob-size
border-radius: 50%
background-color: $range-bg-progress-color
box-shadow: 0 0 1px 0 $range-bg-progress-color
// Styling FireFox input range, mimicking Chromium one
//
// (?) * unable to differenciate selector betwenn background range and progress
// one → set only background range
// * unable to position the input ideally → have to rely on
// translation
input[type=range]::-moz-range-track
$firefox-height: #{ $range-height - 2px }
$firefox-offset-y: -2px
height: $firefox-height
border-radius: $range-radius
border: 1px solid $range-border-color
background: $range-bg-color
transform: translateY($firefox-offset-y)
input[type=range]::-moz-range-thumb
$firefox-offset-y: -2px
border: none
height: $knob-size
width: $knob-size
border-radius: 50%
background-color: $range-bg-progress-color
box-shadow: 0 0 2px 0 $range-bg-progress-color
transform: translateY($firefox-offset-y)
// hide the outline behind the border
input[type=range]:-moz-focusring
outline: 1px solid white
outline-offset: -1px
// Most simple reset ever found
// @link https://meiert.com/en/blog/stop-using-resets/#comment-240548
html{
box-sizing: border-box;
height: 100%
}
*,
:after,
:before{
margin: 0;
padding: 0;
border: 0;
outline: 0;
box-sizing: inherit
}
::-moz-focus-inner{
border: 0;
}
body{
position:relative;
}
a{
text-decoration: none;
color: inherit
}
a:focus{
outline: 0;
}
ul{
list-style:none;
}
...@@ -2,3 +2,31 @@ ...@@ -2,3 +2,31 @@
h4, h5, h6 { h4, h5, h6 {
font-weight: bold; font-weight: bold;
} }
/// from "_reboot.scss"
body {
margin: 0;
font-family: $font-family-base;
@include font-size($font-size-base);
font-weight: $font-weight-base;
line-height: $line-height-base;
color: $body-color;
text-align: left;
background-color: $body-bg;
}
/// from "_reboot.scss"
pre,
code,
kbd,
samp {
font-family: $font-family-monospace;
}
/// For this project: underline every links
/// (legacy choice)
a:hover,
a:active,
a:focus {
text-decoration: underline;
}
/// Utility classes
///
/// Pattern strategies:
///
/// [ ] "!important" keyword → «using a sledgehammer to hit a nail
/// in the wall»
/// [x] rule precedence based on higher selector (ie. "#id")
/// [ ] adding an extra cascade layer → not fully browser compliant
///
///
/// @https://sebastiandedeyne.com/why-we-use-important-with-tailwind/
/// @https://css-tricks.com/css-cascade-layers/
#app {
// Width
.w-0 { width: 0; }
.w-1 { width: space-x(1); }
.w-2 { width: space-x(2); }
.w-3 { width: space-x(3); }
.w-4 { width: space-x(4); }
.w-5 { width: space-x(5); }
.w-auto { width: auto; }
.w-fluid { width: 100%; }
.w-inherit { width: inherit; }
// Height
.h-0 { height: 0; }
.h-1 { height: space-x(1); }
.h-2 { height: space-x(2); }
.h-3 { height: space-x(3); }
.h-4 { height: space-x(4); }
.h-5 { height: space-x(5); }
.h-auto { height: auto; }
.h-fluid { height: 100%; }
.h-inherit { height: inherit; }
// Margin
.m-0 { margin: 0; }
.m-1 { margin: space-x(1); }
.m-2 { margin: space-x(2); }
.m-3 { margin: space-x(3); }
.m-4 { margin: space-x(4); }
.m-5 { margin: space-x(5); }
.mt-0 { margin-top: 0; }
.mr-0 { margin-right: 0; }
.mb-0 { margin-bottom: 0; }
.ml-0 { margin-left: 0; }
.mx-0 { margin-left: 0; margin-right: 0; }
.my-0 { margin-top: 0; margin-bottom: 0; }
.mt-1 { margin-top: space-x(1); }
.mr-1 { margin-right: space-x(1); }
.mb-1 { margin-bottom: space-x(1); }
.ml-1 { margin-left: space-x(1); }
.mx-1 { margin-left: space-x(1); margin-right: space-x(1); }
.my-1 { margin-top: space-x(1); margin-bottom: space-x(1); }
.mt-2 { margin-top: space-x(2); }
.mr-2 { margin-right: space-x(2); }
.mb-2 { margin-bottom: space-x(2); }
.ml-2 { margin-left: space-x(2); }
.mx-2 { margin-right: space-x(2); margin-left: space-x(2); }
.my-2 { margin-top: space-x(2); margin-bottom: space-x(2); }
.mt-3 { margin-top: space-x(3); }
.mr-3 { margin-right: space-x(3); }
.mb-3 { margin-bottom: space-x(3); }
.ml-3 { margin-left: space-x(3); }
.mx-3 { margin-right: space-x(3); margin-left: space-x(3); }
.my-3 { margin-bottom: space-x(3); margin-top: space-x(3); }
// Padding
.p-0 { padding: 0; }
.p-1 { padding: space-x(1); }
.p-2 { padding: space-x(2); }
.p-3 { padding: space-x(3); }
.p-4 { padding: space-x(4); }
.p-5 { padding: space-x(5); }
.pt-0 { padding-top: 0; }
.pr-0 { padding-right: 0; }
.pb-0 { padding-bottom: 0; }
.pl-0 { padding-left: 0; }
.px-0 { padding-left: 0; padding-right: 0; }
.py-0 { padding-top: 0; padding-bottom: 0; }
.pt-1 { padding-top: space-x(1); }
.pr-1 { padding-right: space-x(1); }
.pb-1 { padding-bottom: space-x(1); }
.pl-1 { padding-left: space-x(1); }
.px-1 { padding-left: space-x(1); padding-right: space-x(1); }
.py-1 { padding-top: space-x(1); padding-bottom: space-x(1); }
.pt-2 { padding-top: space-x(2); }
.pr-2 { padding-right: space-x(2); }
.pb-2 { padding-bottom: space-x(2); }
.pl-2 { padding-left: space-x(2); }
.px-2 { padding-right: space-x(2); padding-left: space-x(2); }
.py-2 { padding-top: space-x(2); padding-bottom: space-x(2); }
.pt-3 { padding-top: space-x(3); }
.pr-3 { padding-right: space-x(3); }
.pb-3 { padding-bottom: space-x(3); }
.pl-3 { padding-left: space-x(3); }
.px-3 { padding-right: space-x(3); padding-left: space-x(3); }
.py-3 { padding-bottom: space-x(3); padding-top: space-x(3); }
// Display
$displays:
none,
inline,
inline-block,
block,
table,
table-row,
table-cell,
flex,
inline-flex;
@each $value in $displays {
.d-#{$value} { display: $value; }
}
// Position
$positions:
static,
relative,
absolute,
fixed,
sticky;
@each $value in $positions {
.position-#{$value} { position: $value; }
}
// Overflow
$overflows: auto, hidden !default;
@each $value in $overflows {
.overflow-#{$value} { overflow: $value; }
}
// Typography
.text-primary { color: $primary; }
.text-secondary { color: $secondary; }
.text-info { color: $info; }
.text-success { color: $success; }
.text-warning { color: $warning; }
.text-danger { color: $danger; }
.text-dark { color: $dark; }
.text-light { color: $light; }
.text-bold { font-weight: bold; }
.text-italic { font-style: italic; }
.text-underline { text-decoration: underline; }
.text-justify { text-align: justify; }
.text-wrap { white-space: normal; }
.text-nowrap { white-space: nowrap; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-lowercase { text-transform: lowercase; }
.text-uppercase { text-transform: uppercase; }
.text-capitalize { text-transform: capitalize; }
.font-italic { font-style: italic; }
.text-decoration-none { text-decoration: none; }
.text-decoration-underline { text-decoration: underline; }
// Align
.align-baseline { vertical-align: baseline; }
.align-top { vertical-align: top; }
.align-middle { vertical-align: middle; }
.align-bottom { vertical-align: bottom; }
.align-text-bottom { vertical-align: text-bottom; }
.align-text-top { vertical-align: text-top; }
// Background
.bg-white { background-color: $white; }
.bg-black { background-color: $black; }
.bg-transparent { background-color: transparent; }
.bg-primary { background-color: $primary; }
.bg-secondary { background-color: $secondary; }
.bg-info { background-color: $info; }
.bg-success { background-color: $success; }
.bg-warning { background-color: $warning; }
.bg-danger { background-color: $danger; }
.bg-dark { background-color: $dark; }
.bg-light { background-color: $light; }
// Border
.border-0 { border: 0; }
.border-top-0 { border-top: 0; }
.border-right-0 { border-right: 0; }
.border-bottom-0 { border-bottom: 0; }
.border-left-0 { border-left: 0; }
// Border radius
.rounded-circle { border-radius: 50%; }
.rounded-0 { border-radius: 0; }
// Clearfix
.clearfix { @include clearfix; }
// Flex
.flex-row { flex-direction: row; }
.flex-column { flex-direction: column; }
.flex-row-reverse { flex-direction: row-reverse; }
.flex-column-reverse { flex-direction: column-reverse; }
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.flex-wrap-reverse { flex-wrap: wrap-reverse; }
.flex-fill { flex: 1 1 auto; }
.flex-grow-0 { flex-grow: 0; }
.flex-grow-1 { flex-grow: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.flex-shrink-1 { flex-shrink: 1; }
.justify-content-start { justify-content: flex-start; }
.justify-content-end { justify-content: flex-end; }
.justify-content-center { justify-content: center; }
.justify-content-between { justify-content: space-between; }
.justify-content-around { justify-content: space-around; }
.align-items-start { align-items: flex-start; }
.align-items-end { align-items: flex-end; }
.align-items-center { align-items: center; }
.align-items-baseline { align-items: baseline; }
.align-items-stretch { align-items: stretch; }
.align-content-start { align-content: flex-start; }
.align-content-end { align-content: flex-end; }
.align-content-center { align-content: center; }
.align-content-between { align-content: space-between; }
.align-content-around { align-content: space-around; }
.align-content-stretch { align-content: stretch; }
.align-self-auto { align-self: auto; }
.align-self-start { align-self: flex-start; }
.align-self-end { align-self: flex-end; }
.align-self-center { align-self: center; }
.align-self-baseline { align-self: baseline; }
.align-self-stretch { align-self: stretch; }
// Float
.float-left { float: left; }
.float-right { float: right; }
.float-none { float: none; }
// Visibility
.visible { visibility: visible; }
.hidden { visibility: hidden; }
}
...@@ -248,3 +248,16 @@ ...@@ -248,3 +248,16 @@
text-align: center; text-align: center;
} }
} }
/// Tabs
///-----------------------------------------------------------------------------
.b-tabs {
.nav-item:first-child {
margin-left: space-x(2);
}
.nav-item:last-child {
margin-right: space-x(2);
}
}
// Form
$form-group-margin-bottom: space-x(3);
/// Topbar
$topbar-height: 56px; // ~ unworthy empirical value (@TODO topbar height calculation)
$topbar-input-width: 304px;
$topbar-item-margin: space-x(0.5);
$topbar-fixed-button-width: 136px;
/// Sidebar
$sidebar-width: 480px;
$sidebar-height: calc(100vh - #{ $topbar-height });
$sidebar-tab-margin-x: space-x(2.5);
/// Misc
$overlay-radius: 5px;
@mixin aside-topbar() { @mixin aside-topbar() {
$border-color: mix-alpha($navbar-dark-hover-color, 5%); $border-color: mix-alpha($navbar-dark-hover-color, 5%);
...@@ -28,3 +46,40 @@ ...@@ -28,3 +46,40 @@
mix-alpha($body-bg, 100%) 45% mix-alpha($body-bg, 100%) 45%
); );
} }
@mixin sidebar() {
@include hidden-scrollbar;
$teaser-height: 16px;
background-color: $body-bg;
height: 100%;
position: relative;
&::before {
@include top-teaser;
content: "";
z-index: 1;
pointer-events: none;
position: sticky;
top: 0;
height: $teaser-height;
width: 100%;
display: block;
}
&::after {
@include bottom-teaser;
content: "";
z-index: 1;
pointer-events: none;
position: sticky;
bottom: 0;
height: $teaser-height;
width: 100%;
display: block;
}
}
/// Customising Bootstrap 4
/// (part 1.1: importing Bootstrap abstracts)
///
/// @link https://uxplanet.org/how-to-customize-bootstrap-b8078a011203
/// @link https://getbootstrap.com/docs/4.1/getting-started/theming/
@import '../../../node_modules/bootstrap/scss/functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import '../../../node_modules/bootstrap/scss/mixins';
/// (part 1.2: overriding Bootstrap abstrackt + importing project abstracts)
///
// (?) Normally "abstract" and "modules" have to be MANUALLY IMPORTED each
// times their content is being used
//
// However, with Bootstrap non "@use" API-ready, and multiple themes
// management, we have to stick the old "@import"
//
// It also implies that, if tomorrow, transition to "@import" to "@use" API
// has to be made, every manual import must be included (eg. by checking
// the `modules/form` file, and check where its mixin `inputError` has been
// being used
@import '../_abstract';
@import '../_modules';
/// Customising Bootstrap 4
/// (part 2.1: importing Bootstrap styles)
///
/// @link https://uxplanet.org/how-to-customize-bootstrap-b8078a011203
/// @link https://getbootstrap.com/docs/4.1/getting-started/theming/
// As we want to remove some modules from the builded bootstrap, we have to
// manually import each of its part
//
// Which part were filtered?
// * `/functions` → already imported in our bootstrap custom abstract part
// * `/variables` → already imported in our bootstrap custom abstract part
// * `/mixins` → already imported in our bootstrap custom abstract part
// * `/reboot` → due to smelly decision on making rules directly within
// HTML tag, the removal of this whole is the best decision
// (if necessary, pick another reset sheet which does not
// lead to incongruous battle with !important keyworkd
// everywhere)
// * `/utilities` → due to the `!important` keyword systematically added to
// to some utilities' section (eg. "spacing"), we have to
// manually manage the Bootstrap utilities by filtering
// some sections
// ↳
// * `/align`
// * `/background`
// * `/borders`
// * `/clearfix`
// * `/display`
// * `/flex`
// * `/float`
// * `/overflow`
// * `/position`
// * `/sizing`
// * `/spacing`
// * `/text`
// * `/visibility`
//
@import "../../../node_modules/bootstrap/scss/root";
@import "../../../node_modules/bootstrap/scss/type";
@import "../../../node_modules/bootstrap/scss/images";
@import "../../../node_modules/bootstrap/scss/code";
@import "../../../node_modules/bootstrap/scss/grid";
@import "../../../node_modules/bootstrap/scss/tables";
@import "../../../node_modules/bootstrap/scss/forms";
@import "../../../node_modules/bootstrap/scss/buttons";
@import "../../../node_modules/bootstrap/scss/transitions";
@import "../../../node_modules/bootstrap/scss/dropdown";
@import "../../../node_modules/bootstrap/scss/button-group";
@import "../../../node_modules/bootstrap/scss/input-group";
@import "../../../node_modules/bootstrap/scss/custom-forms";
@import "../../../node_modules/bootstrap/scss/nav";
@import "../../../node_modules/bootstrap/scss/navbar";
@import "../../../node_modules/bootstrap/scss/card";
@import "../../../node_modules/bootstrap/scss/breadcrumb";
@import "../../../node_modules/bootstrap/scss/pagination";
@import "../../../node_modules/bootstrap/scss/badge";
@import "../../../node_modules/bootstrap/scss/jumbotron";
@import "../../../node_modules/bootstrap/scss/alert";
@import "../../../node_modules/bootstrap/scss/progress";
@import "../../../node_modules/bootstrap/scss/media";
@import "../../../node_modules/bootstrap/scss/list-group";
@import "../../../node_modules/bootstrap/scss/close";
@import "../../../node_modules/bootstrap/scss/toasts";
@import "../../../node_modules/bootstrap/scss/modal";
@import "../../../node_modules/bootstrap/scss/tooltip";
@import "../../../node_modules/bootstrap/scss/popover";
@import "../../../node_modules/bootstrap/scss/carousel";
@import "../../../node_modules/bootstrap/scss/spinners";
@import "../../../node_modules/bootstrap/scss/print";
@import "../../../node_modules/bootstrap/scss/utilities/embed";
@import "../../../node_modules/bootstrap/scss/utilities/interactions";
@import "../../../node_modules/bootstrap/scss/utilities/screenreaders";
@import "../../../node_modules/bootstrap/scss/utilities/shadows";
@import "../../../node_modules/bootstrap/scss/utilities/stretched-link";
/// (part 2.2: overriding Bootstrap styles + import project styles)
@import '../_components';
@import '../_base';
@import '../_legacy';
/*! Themestr.app `Darkster` Bootstrap 4.3.1 theme */ /*! Themestr.app `Darkster` Bootstrap 4.3.1 theme */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions'; ///==========================================
@import '../../../node_modules/bootstrap/scss/variables'; /// Theme variable overrides
@import '../../../node_modules/bootstrap/scss/mixins'; ///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts // Fonts
@import url(https://fonts.googleapis.com/css?family=Comfortaa:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Comfortaa:200,300,400,700);
$headings-font-family:Comfortaa; $headings-font-family: "Comfortaa";
// Colors // Colors
$primary :#FF550B;
/*$enable-grid-classes:false;*/ $secondary :#303030;
$primary:#FF550B; $success :#015668;
$secondary:#303030; $danger :#FF304F;
$success:#015668; $info :#0F81C7;
$danger:#FF304F; $warning :#0DE2EA;
$info:#0F81C7; $light :#e8e8e8;
$warning:#0DE2EA; $dark :#000000;
$light:#e8e8e8;
$dark:#000000; $theme-colors: map.merge(
$theme-colors,
$theme-colors: ( (
'primary': $primary, 'primary': $primary,
'secondary': $secondary, 'secondary': $secondary,
'success': $success, 'success': $success,
...@@ -44,89 +33,114 @@ $theme-colors: ( ...@@ -44,89 +33,114 @@ $theme-colors: (
'info': $info, 'info': $info,
'warning': $warning, 'warning': $warning,
'light': $light, 'light': $light,
'dark': $dark 'dark': $dark,
)
); );
// Add more shades to gray $gray-100: #212529;
$gray-150: #FAFAFA; $gray-150: #2E2E2E; // (+)
$gray-175: #F0F0F0; $gray-175: #333333; // (+)
$gray-200: #343A40;
$gray-300: #495057;
$gray-400: #6C757D;
$gray-500: #ADB5BD;
$gray-600: #CED4DA;
$gray-700: #DEE2E6;
$gray-800: #E9ECEF;
$gray-900: #F8F9FA;
// Layout
$form-group-margin-bottom: space-x(3);
// Misc...
$enable-shadows:true; $enable-shadows:true;
$gray-300:#000000;
$gray-800:#555555;
$body-bg:$black; $body-bg:$black;
$body-color:#cccccc; $body-color:#cccccc;
$link-color:#f0f0f0; $link-color:#f0f0f0;
$link-hover-color:darken($link-color,20%);
$font-size-base:1.1rem; $font-size-base:1.1rem;
$table-accent-bg: rgba($white,.05); $list-group-bg:lighten($body-bg,5%);
$table-hover-bg:rgba($white,.075);
$table-border-color:rgba($white, 0.3);
$table-dark-border-color: $table-border-color;
$table-dark-color:$white;
$input-bg:$gray-300;
$input-disabled-bg: #ccc;
$dropdown-bg:$gray-800;
$dropdown-divider-bg:rgba($black,.15);
$dropdown-link-color:$body-color;
$dropdown-link-hover-color:$white;
$dropdown-link-hover-bg:$body-bg;
$nav-tabs-border-color:rgba($white, 0.3);
$nav-tabs-link-hover-border-color:$nav-tabs-border-color;
$nav-tabs-link-active-bg:transparent;
$nav-tabs-link-active-border-color:$nav-tabs-border-color;
$navbar-dark-hover-color:$white;
$navbar-light-hover-color:$gray-800;
$navbar-light-active-color:$gray-800;
$pagination-color:$white;
$pagination-bg:transparent;
$pagination-border-color:rgba($black, 0.6);
$pagination-hover-color:$white;
$pagination-hover-bg:transparent;
$pagination-hover-border-color:rgba($black, 0.6);
$pagination-active-bg:transparent;
$pagination-active-border-color:rgba($black, 0.6);
$pagination-disabled-bg:transparent;
$pagination-disabled-border-color:rgba($black, 0.6);
$jumbotron-bg:darken($gray-900, 5%);
$card-border-color:rgba($black, 0.6); $card-border-color:rgba($black, 0.6);
$card-cap-bg:lighten($gray-800, 10%); $card-cap-bg:lighten($gray-800, 10%);
$card-bg:lighten($body-bg, 5%); $card-bg:lighten($body-bg, 5%);
$modal-content-bg:lighten($body-bg,5%); $input-bg:$gray-300;
$modal-header-border-color:rgba(0,0,0,.2); $input-disabled-bg: $gray-100;
$progress-bg:darken($gray-900,5%);
$progress-bar-color:$gray-600;
$list-group-bg:lighten($body-bg,5%);
$list-group-border-color:rgba($black,0.6);
$list-group-hover-bg:lighten($body-bg,10%);
$list-group-active-color:$white;
$list-group-active-bg:$list-group-hover-bg;
$list-group-active-border-color:$list-group-border-color;
$list-group-disabled-color:$gray-800;
$list-group-disabled-bg:$black;
$list-group-action-color:$white;
$breadcrumb-active-color:$gray-500;
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import "../_components";
@import "../_base";
@import "../_legacy";
// Misc...
// $enable-shadows:true;
// $body-bg:$black;
// $body-color:#cccccc;
// $link-color:#f0f0f0;
// $link-hover-color:darken($link-color,20%);
// $font-size-base:1.1rem;
// $table-accent-bg: rgba($white,.05);
// $table-hover-bg:rgba($white,.075);
// $table-border-color:rgba($white, 0.3);
// $table-dark-border-color: $table-border-color;
// $table-dark-color:$white;
// $input-bg:$gray-300;
// $input-disabled-bg: #ccc;
// $dropdown-bg:$gray-800;
// $dropdown-divider-bg:rgba($black,.15);
// $dropdown-link-color:$body-color;
// $dropdown-link-hover-color:$white;
// $dropdown-link-hover-bg:$body-bg;
// $nav-tabs-border-color:rgba($white, 0.3);
// $nav-tabs-link-hover-border-color:$nav-tabs-border-color;
// $nav-tabs-link-active-bg:transparent;
// $nav-tabs-link-active-border-color:$nav-tabs-border-color;
// $navbar-dark-hover-color:$white;
// $navbar-light-hover-color:$gray-800;
// $navbar-light-active-color:$gray-800;
// $pagination-color:$white;
// $pagination-bg:transparent;
// $pagination-border-color:rgba($black, 0.6);
// $pagination-hover-color:$white;
// $pagination-hover-bg:transparent;
// $pagination-hover-border-color:rgba($black, 0.6);
// $pagination-active-bg:transparent;
// $pagination-active-border-color:rgba($black, 0.6);
// $pagination-disabled-bg:transparent;
// $pagination-disabled-border-color:rgba($black, 0.6);
// $jumbotron-bg:darken($gray-900, 5%);
// $card-border-color:rgba($black, 0.6);
// $card-cap-bg:lighten($gray-800, 10%);
// $card-bg:lighten($body-bg, 5%);
// $modal-content-bg:lighten($body-bg,5%);
// $modal-header-border-color:rgba(0,0,0,.2);
// $progress-bg:darken($gray-900,5%);
// $progress-bar-color:$gray-600;
// $list-group-bg:lighten($body-bg,5%);
// $list-group-border-color:rgba($black,0.6);
// $list-group-hover-bg:lighten($body-bg,10%);
// $list-group-active-color:$white;
// $list-group-active-bg:$list-group-hover-bg;
// $list-group-active-border-color:$list-group-border-color;
// $list-group-disabled-color:$gray-800;
// $list-group-disabled-bg:$black;
// $list-group-action-color:$white;
// $breadcrumb-active-color:$gray-500;
///==========================================
/// Custom styles specific to the theme
///==========================================
@import './_base';
// Custom rules specific to the theme
//-----------------------------------
.navbar-dark.bg-primary {background-color:#111111 !important;} .navbar-dark.bg-primary {background-color:#111111 !important;}
.table.able {color:#ccccc5} .table.able {color:#ccccc5}
#sp-container .table tr td a { color: #005a9aff; } #sp-container .table tr td a { color: #005a9aff; }
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
background-color: $gray-300;
color: $gray-700
}
.nav.nav-tabs li a.nav-link.active:hover {
color: $gray-900;
}
.form-control,
.form-control:focus{
background-color: $gray-300;
color: $gray-700;
}
.card-header {
background-color: $gray-400;
}
/*! Bootstrap `Default` https://getbootstrap.com/docs/4.6 */ /*! Bootstrap `Default` https://getbootstrap.com/docs/4.6 */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions'; ///==========================================
@import '../../../node_modules/bootstrap/scss/variables'; /// Theme variable overrides
@import '../../../node_modules/bootstrap/scss/mixins'; ///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
// (?) Normally "abstract" and "modules" have to be MANUALLY IMPORTED each
// times their content is being used
//
// However, with Bootstrap non "@use" API-ready, and multiple themes
// management, we have to stick the old "@import"
//
// It also implies that, if tomorrow, transition to "@import" to "@use" API
// has to be made, every manual import must be included (eg. by checking
// the `modules/form` file, and check where its mixin `inputError` has been
// being used
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Colors // Colors
$primary: #005a9a; $primary: #005a9a;
$secondary: $blue; $secondary: $blue;
$theme-colors: ( $theme-colors: map.merge(
$theme-colors,
(
"primary": $primary, "primary": $primary,
"secondary": $secondary "secondary": $secondary,
)
); );
// Add more shades to gray
$gray-150: #FAFAFA;
$gray-175: #F0F0F0;
// Layout
$form-group-margin-bottom: space-x(3);
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import '../_components';
@import '../_base';
@import '../_legacy';
///==========================================
/// Custom styles specific to the theme
///==========================================
// Custom rules specific to the theme @import './_base';
//-----------------------------------
// Rectify "secondary" button tonal contrast // Rectify "secondary" button tonal contrast
.btn-secondary, .btn-secondary,
......
/*! Themestr.app `Greyson` Bootstrap 4.3.1 theme */ /*! Themestr.app `Greyson` Bootstrap 4.3.1 theme */
/* https://github.com/ThemesGuide/bootstrap-themes/blob/master/greyson/ */ /* https://github.com/ThemesGuide/bootstrap-themes/blob/master/greyson/ */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions'; ///==========================================
@import '../../../node_modules/bootstrap/scss/variables'; /// Theme variable overrides
@import '../../../node_modules/bootstrap/scss/mixins'; ///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts // Fonts
@import url(https://fonts.googleapis.com/css?family=Muli:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Muli:200,300,400,700);
$font-family-base:Muli; $font-family-base: "Muli";
@import url(https://fonts.googleapis.com/css?family=Oswald:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Oswald:200,300,400,700);
$headings-font-family:Oswald; $headings-font-family: "Oswald";
// Colors // Colors
// $enable-grid-classes:false; $primary :#2f3c48;
$primary:#2f3c48; $secondary :#6f7f8c;
$secondary:#6f7f8c; $success :#3e4d59;
$success:#3e4d59; $danger :#cc330d;
$danger:#cc330d; $info :#5c8f94;
$info:#5c8f94; $warning :#6e9fa5;
$warning:#6e9fa5; $light :#eceeec;
$light:#eceeec; $dark :#1e2b37;
$dark:#1e2b37;
$theme-colors: map.merge(
$theme-colors: ( $theme-colors,
(
'primary': $primary, 'primary': $primary,
'secondary': $secondary, 'secondary': $secondary,
'success': $success, 'success': $success,
...@@ -46,29 +38,16 @@ $theme-colors: ( ...@@ -46,29 +38,16 @@ $theme-colors: (
'info': $info, 'info': $info,
'warning': $warning, 'warning': $warning,
'light': $light, 'light': $light,
'dark': $dark 'dark': $dark,
)
); );
// Add more shades to gray // Misc
$gray-150: #FAFAFA; $enable-rounded: false;
$gray-175: #F0F0F0;
// Layout
$form-group-margin-bottom: space-x(3);
$enable-rounded:false;
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import "../_components";
@import "../_base";
@import "../_legacy";
///==========================================
/// Custom styles specific to the theme
///==========================================
// Custom rules specific to the theme @import './_base';
//-----------------------------------
/*! Themestr.app `Herbie` Bootstrap 4.3.1 theme */ /*! Themestr.app `Herbie` Bootstrap 4.3.1 theme */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions'; ///==========================================
@import '../../../node_modules/bootstrap/scss/variables'; /// Theme variable overrides
@import '../../../node_modules/bootstrap/scss/mixins'; ///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts // Fonts
@import url(https://fonts.googleapis.com/css?family=Nunito:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Nunito:200,300,400,700);
$font-family-base:Nunito; $font-family-base: "Nunito";
@import url(https://fonts.googleapis.com/css?family=Crete+Round:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Crete+Round:200,300,400,700);
$headings-font-family:Crete Round; $headings-font-family: "Crete Round";
// Colors // Colors
$primary :#083358;
/*$enable-grid-classes:false;*/ $secondary :#F67280;
$primary:#083358; $success :#0074E4;
$secondary:#F67280; $danger :#FF4057;
$success:#0074E4; $info :#74DBEF;
$danger:#FF4057; $warning :#FC3C3C;
$info:#74DBEF; $light :#F2F2F0;
$warning:#FC3C3C; $dark :#072247;
$light:#F2F2F0;
$dark:#072247; $theme-colors: map.merge(
$theme-colors,
$theme-colors: ( (
'primary': $primary, 'primary': $primary,
'secondary': $secondary, 'secondary': $secondary,
'success': $success, 'success': $success,
...@@ -46,28 +37,13 @@ $theme-colors: ( ...@@ -46,28 +37,13 @@ $theme-colors: (
'info': $info, 'info': $info,
'warning': $warning, 'warning': $warning,
'light': $light, 'light': $light,
'dark': $dark 'dark': $dark,
)
); );
// Add more shades to gray
$gray-150: #FAFAFA;
$gray-175: #F0F0F0;
// Layout
$form-group-margin-bottom: space-x(3);
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import "../_components";
@import "../_base";
@import "../_legacy";
///==========================================
/// Custom styles specific to the theme
///==========================================
// Custom rules specific to the theme @import './_base';
//-----------------------------------
/*! Themestr.app `Monotony` Bootstrap 4.3.1 theme */ /*! Themestr.app `Monotony` Bootstrap 4.3.1 theme */
/* https://github.com/ThemesGuide/bootstrap-themes/blob/master/monotony/ */ /* https://github.com/ThemesGuide/bootstrap-themes/blob/master/monotony/ */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions'; ///==========================================
@import '../../../node_modules/bootstrap/scss/variables'; /// Theme variable overrides
@import '../../../node_modules/bootstrap/scss/mixins'; ///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts // Fonts
@import url(https://fonts.googleapis.com/css?family=Montserrat:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Montserrat:200,300,400,700);
$font-family-base:Montserrat; $font-family-base: "Montserrat";
@import url(https://fonts.googleapis.com/css?family=Open+Sans:200,300,400,700); @import url(https://fonts.googleapis.com/css?family=Open+Sans:200,300,400,700);
$headings-font-family:Open Sans; $headings-font-family: "Open Sans";
// Colors // Colors
$primary :#222222;
// $enable-grid-classes:false; $secondary :#666666;
$primary:#222222; $success :#333333;
$secondary:#666666; $danger :#434343;
$success:#333333; $info :#515151;
$danger:#434343; $warning :#5f5f5f;
$info:#515151; $light :#eceeec;
$warning:#5f5f5f; $dark :#111111;
$light:#eceeec;
$dark:#111111; $theme-colors: map.merge(
$theme-colors,
$theme-colors: ( (
'primary': $primary, 'primary': $primary,
'secondary': $secondary, 'secondary': $secondary,
'success': $success, 'success': $success,
...@@ -47,30 +38,13 @@ $theme-colors: ( ...@@ -47,30 +38,13 @@ $theme-colors: (
'info': $info, 'info': $info,
'warning': $warning, 'warning': $warning,
'light': $light, 'light': $light,
'dark': $dark 'dark': $dark,
)
); );
// Add more shades to gray
$gray-150: #FAFAFA;
$gray-175: #F0F0F0;
// Layout
$form-group-margin-bottom: space-x(3);
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import '../_abstract';
@import '../_modules';
@import "../_components";
@import "../_base";
@import "../_legacy";
///==========================================
/// Custom styles specific to the theme
///==========================================
// Custom rules specific to the theme @import './_base';
//-----------------------------------
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment