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",
"version": "0.0.5.8.4",
"version": "0.0.5.8.5",
"scripts": {
"generate-purs-packages-nix": "./nix/generate-purs-packages.nix",
"generate-psc-packages-nix": "./nix/generate-packages-json.bash",
......
......@@ -16,11 +16,13 @@ type Props =
type Options =
( className :: String
, contentClassName :: String
)
options :: Record Options
options =
{ className: ""
{ className : ""
, contentClassName : ""
}
-- | Component simulating a native <fieldset>
......@@ -36,12 +38,21 @@ component = R.hooksComponent componentName cpt where
cpt props@{ titleSlot
} children = do
-- Computed
className <- pure $ intercalate " "
let
className = intercalate " "
-- provided custom className
[ props.className
-- BEM classNames
, componentName
]
contentClassName = intercalate " "
-- provided custom className
[ props.contentClassName
-- BEM classNames
, componentName <> "__content"
]
-- Render
pure $
......@@ -53,6 +64,6 @@ component = R.hooksComponent componentName cpt where
[ titleSlot ]
,
H.div
{ className: componentName <> "__content" }
{ className: contentClassName}
children
]
......@@ -14,7 +14,9 @@ import Gargantext.Components.Bootstrap.Icon(icon) as Exports
import Gargantext.Components.Bootstrap.IconButton(iconButton) as Exports
import Gargantext.Components.Bootstrap.ProgressBar(progressBar) 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(
div', div_
......
module Gargantext.Components.Bootstrap.Tooltip
( tooltip
, tooltipBind
, TooltipBindingProps, tooltipBind, tooltipBind'
, tooltipContainer
) where
......@@ -90,6 +90,13 @@ tooltipBind =
, "data-tip": true
}
-- | Derived empty state
tooltipBind' :: Record TooltipBindingProps
tooltipBind' =
{ "data-for": ""
, "data-tip": false
}
-------------------------------------------------------------
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)
import Gargantext.Hooks.LinkHandler (useLinkHandler)
import Gargantext.Routes (AppRoute(..))
import Gargantext.Sessions (Session(..), unSessions)
import Gargantext.Utils (nbsp)
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
......@@ -132,9 +131,10 @@ plusCpt = here.component "plus" cpt where
, 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"
]
......@@ -150,7 +150,7 @@ forestLayout :: R2.Leaf Props
forestLayout = R2.leaf forestLayoutCpt
forestLayoutCpt :: R.Memo Props
forestLayoutCpt = R.memo' $ here.component "forestLayout" cpt where
cpt p children = pure $
cpt p _ = pure $
H.div
{ 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 DOM.Simple as DOM
import DOM.Simple.Event as DE
import Data.Array.NonEmpty as NArray
import Data.Foldable (intercalate)
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, null)
import Data.Maybe (Maybe(..), maybe)
import Data.Nullable (null)
import Data.String.Regex as Regex
import Data.Symbol (SProxy(..))
import Data.Tuple.Nested ((/\))
import Effect (Effect)
......@@ -15,24 +18,21 @@ import Effect.Class (liftEffect)
import Gargantext.AsyncTasks as GAT
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Tooltip (tooltipBind)
import Gargantext.Components.Bootstrap.Types (ComponentStatus(..), TooltipEffect(..), Variant(..))
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.Types (FileType(..), UploadFileBlob(..))
import Gargantext.Components.Forest.Tree.Node.Box (nodePopupView)
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.GraphExplorer.API as GraphAPI
import Gargantext.Components.Lang (Lang(EN))
import Gargantext.Components.Nodes.Corpus (loadCorpusWithChild)
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.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Hooks.Loader (useLoader, useLoaderEffect)
import Gargantext.Hooks.Loader (useLoaderEffect)
import Gargantext.Hooks.Version (Version, useVersion)
import Gargantext.Routes as Routes
import Gargantext.Sessions (Session, sessionId)
......@@ -42,14 +42,15 @@ import Gargantext.Utils (nbsp, textEllipsisBreak, (?))
import Gargantext.Utils.Popover as Popover
import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import React.SyntheticEvent (SyntheticEvent_)
import React.SyntheticEvent as E
import React.SyntheticEvent as SE
import Reactix as R
import Reactix.DOM.HTML as H
import Record as Record
import Toestand as T
-- (?) never been able to properly declare PureScript Regex...
foreign import nodeUserRegexp :: Regex.Regex
here :: R2.Here
here = R2.here "Gargantext.Components.Forest.Tree.Node"
......@@ -78,7 +79,6 @@ nodeSpanCpt :: R.Component NodeSpanProps
nodeSpanCpt = here.component "nodeSpan" cpt
where
cpt props@{ boxes: boxes@{ errors
, handed
, reloadMainPage
, reloadRoot
, route
......@@ -119,7 +119,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
dropClass Nothing _ = ""
name' :: String -> GT.NodeType -> Session -> String
name' _ GT.NodeUser session = show session
name' _ GT.NodeUser s = show s
name' n _ _ = n
isSelected = Just route' == Routes.nodeTypeAppRoute nodeType (sessionId session) id
......@@ -128,6 +128,8 @@ nodeSpanCpt = here.component "nodeSpan" cpt
href = url frontends $ GT.NodePath (sessionId session) nodeType (Just id)
name = name' props.name nodeType session
-- Methods
dropHandler :: forall event.
......@@ -151,18 +153,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt
T.Box Boolean
-> SE.SyntheticEvent_ event
-> Effect Unit
onDragOverHandler isDragOver e = do
onDragOverHandler box e = do
-- prevent redirection when file is dropped
-- https://stackoverflow.com/a/6756680/941471
SE.preventDefault e
SE.stopPropagation e
T.write_ true isDragOver
T.write_ true box
onDragLeave :: forall event.
T.Box Boolean
-> SE.SyntheticEvent_ event
-> Effect Unit
onDragLeave isDragOver _ = T.write_ false isDragOver
onDragLeave box _ = T.write_ false box
onTaskFinish ::
GT.NodeID
......@@ -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 //
folderIcon
......@@ -240,15 +263,18 @@ nodeSpanCpt = here.component "nodeSpan" cpt
}
,
nodeIcon
(
{ nodeType
, isLeaf
, callback: const $ T.modify_ (not) folderOpen
, isSelected
}
)
[
case mVersion of
Nothing -> mempty
Just { clientVersion, remoteVersion} ->
B.iconButton
B.iconButton $
{ className: intercalate " "
[ "mainleaf__version-badge"
, clientVersion == remoteVersion ?
......@@ -267,6 +293,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
, href
, id
, name: name' props.name nodeType session
, type: nodeType
}
,
......@@ -305,7 +332,7 @@ nodeSpanCpt = here.component "nodeSpan" cpt
{ boxes
, dispatch
, id
, name: name' props.name nodeType session
, name
, nodeType
, onPopoverClose: const $ onPopoverClose popoverRef
, session
......@@ -325,26 +352,6 @@ nodeSpanCpt = here.component "nodeSpan" cpt
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 =
( nodeType :: GT.NodeType
, callback :: Unit -> Effect Unit
, isLeaf :: Boolean
, isSelected :: Boolean
)
nodeIcon :: R2.Component NodeIconProps
......@@ -363,7 +371,10 @@ nodeIconCpt = here.component "nodeIcon" cpt where
cpt { nodeType
, callback
, isLeaf
} children = pure $
, isSelected
} children = do
-- Render
pure $
H.span
{ className: "mainleaf__node-icon" } $
......@@ -372,6 +383,7 @@ nodeIconCpt = here.component "nodeIcon" cpt where
{ name: GT.getIcon nodeType true
, callback
, status: isLeaf ? Idled $ Enabled
, variant: isSelected ? Primary $ Dark
}
]
<> children
......@@ -408,25 +420,25 @@ folderIconCpt = here.component "folderIcon" cpt where
-----------------------------------------------
type NodeLinkProps =
( callback :: Unit -> Effect Unit
, href :: String
, id :: Int
, name :: GT.Name
, type :: GT.NodeType
)
nodeLink :: R2.Leaf NodeLinkProps
nodeLink = R2.leaf nodeLinkCpt
nodeLinkCpt :: R.Component NodeLinkProps
nodeLinkCpt = here.component "nodeLink" cpt
where
nodeLinkCpt = here.component "nodeLink" cpt where
cpt { callback
, href
, id
, name
, type: nodeType
} _ = do
-- Computed
let
tid = tooltipId name id
......@@ -434,6 +446,7 @@ nodeLinkCpt = here.component "nodeLink" cpt
{ href
} `Record.merge` B.tooltipBind tid
-- Render
pure $
H.div
......@@ -444,10 +457,18 @@ nodeLinkCpt = here.component "nodeLink" cpt
H.a
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 =
......
......@@ -31,15 +31,15 @@ type NodeActionsGraphProps =
nodeActionsGraph :: R2.Component NodeActionsGraphProps
nodeActionsGraph = R.createElement nodeActionsGraphCpt
nodeActionsGraphCpt :: R.Component NodeActionsGraphProps
nodeActionsGraphCpt = here.component "nodeActionsGraph" cpt
where
cpt { id, graphVersions, session, refresh } _ = do
pure $ H.div { className: "node-actions" } [
if graphVersions.gv_graph == Just graphVersions.gv_repo then
H.div {} []
else
nodeActionsGraphCpt = here.component "nodeActionsGraph" cpt where
cpt { id, graphVersions, session, refresh } _ =
let sameVersions = (graphVersions.gv_graph == Just graphVersions.gv_repo)
in pure $
R2.if' (not sameVersions) $
graphUpdateButton { id, session, refresh }
]
type GraphUpdateButtonProps =
( id :: GT.ID
......@@ -99,12 +99,9 @@ type NodeActionsNodeListProps =
nodeActionsNodeList :: Record NodeActionsNodeListProps -> R.Element
nodeActionsNodeList p = R.createElement nodeActionsNodeListCpt p []
nodeActionsNodeListCpt :: R.Component NodeActionsNodeListProps
nodeActionsNodeListCpt = here.component "nodeActionsNodeList" cpt
where
cpt props _ = do
pure $ H.div { className: "node-actions" } [
nodeListUpdateButton props
]
nodeActionsNodeListCpt = here.component "nodeActionsNodeList" cpt where
cpt props _ = pure $ nodeListUpdateButton props
type NodeListUpdateButtonProps =
( listId :: GT.ListId
......
......@@ -132,9 +132,12 @@ graphCpt = here.component "graph" cpt where
Sigma.stopForceAtlas2 sig
case mCamera of
Nothing -> pure unit
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 } ->
......
module Gargantext.Components.GraphExplorer.Button
( Props, centerButton, simpleButton, cameraButton ) where
module Gargantext.Components.GraphExplorer.Buttons
( Props
, centerButton
, simpleButton
, cameraButton
, edgesToggleButton
, louvainToggleButton
, pauseForceAtlasButton
, resetForceAtlasButton
, multiSelectEnabledButton
) where
import Prelude
import DOM.Simple.Console (log2)
import Data.DateTime as DDT
import Data.DateTime.Instant as DDI
import Data.Either (Either(..))
import Data.Enum (fromEnum)
import Data.Maybe (Maybe(..))
import Data.DateTime as DDT
import Data.DateTime.Instant as DDI
import Data.String as DS
import DOM.Simple.Console (log2)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
import Effect.Now as EN
import Reactix as R
import Reactix.DOM.HTML as H
import Gargantext.Components.Bootstrap as B
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.Types (FileFormat(..))
import Gargantext.Components.GraphExplorer.API (cloneGraph)
import Gargantext.Components.GraphExplorer.Resources as Graph
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Components.GraphExplorer.Utils as GEU
import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Sigma as Sigma
import Gargantext.Hooks.Sigmax.Types as SigmaxTypes
import Gargantext.Sessions (Session)
import Gargantext.Utils ((?))
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
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Button"
......@@ -36,9 +50,12 @@ type Props = (
, text :: String
)
-- @WIP
simpleButton :: Record Props -> R.Element
simpleButton props = R.createElement simpleButtonCpt props []
------------------------------------------------------
simpleButtonCpt :: R.Component Props
simpleButtonCpt = here.component "simpleButton" cpt
where
......@@ -48,14 +65,16 @@ simpleButtonCpt = here.component "simpleButton" cpt
} [ R2.small {} [ H.text text ] ]
centerButton :: R.Ref Sigmax.Sigma -> R.Element
centerButton sigmaRef = simpleButton {
onClick: \_ -> do
centerButton sigmaRef = B.button
{ variant: OutlinedButtonVariant Secondary
, callback: \_ -> do
let sigma = R.readRef sigmaRef
Sigmax.dependOnSigma sigma "[centerButton] sigma: Nothing" $ \s ->
Sigma.goToAllCameras s {x: 0.0, y: 0.0, ratio: 1.0, angle: 0.0}
, text: "Center"
}
[ H.text "Center" ]
------------------------------------------------------
type CameraButtonProps =
( id :: Int
......@@ -71,8 +90,10 @@ cameraButton { id
, hyperdataGraph: GET.HyperdataGraph { graph: GET.GraphData hyperdataGraph }
, session
, sigmaRef
, reloadForest } = simpleButton {
onClick: \_ -> do
, reloadForest } = B.button
{ variant: OutlinedButtonVariant Secondary
, callback: \_ -> do
let sigma = R.readRef sigmaRef
Sigmax.dependOnSigma sigma "[cameraButton] sigma: Nothing" $ \s -> do
screen <- Sigma.takeScreenshot s
......@@ -105,5 +126,171 @@ cameraButton { id
Left err -> liftEffect $ log2 "[cameraButton] RESTError" err
Right _ret -> do
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
, controlsCpt
) where
import Prelude
import Data.Array as A
import Data.Foldable (intercalate)
import Data.Int as I
import Data.Maybe (Maybe(..), maybe)
import Data.Sequence as Seq
import Data.Set as Set
import Effect.Timer (setTimeout)
import Prelude
import Reactix as R
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.Bootstrap as B
import Gargantext.Components.GraphExplorer.Buttons (centerButton, cameraButton, edgesToggleButton, louvainToggleButton, pauseForceAtlasButton, multiSelectEnabledButton)
import Gargantext.Components.GraphExplorer.RangeControl (edgeConfluenceControl, edgeWeightControl, nodeSizeControl)
import Gargantext.Components.GraphExplorer.SlideButton (labelSizeButton, mouseSelectorSizeButton)
import Gargantext.Components.GraphExplorer.ToggleButton (multiSelectEnabledButton, edgesToggleButton, louvainToggleButton, pauseForceAtlasButton{-, resetForceAtlasButton-})
import Gargantext.Components.GraphExplorer.Resources as Graph
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.SlideButton (labelSizeButton, mouseSelectorSizeButton)
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax as Sigmax
import Gargantext.Hooks.Sigmax.Types as SigmaxT
......@@ -30,6 +28,9 @@ import Gargantext.Types as GT
import Gargantext.Utils.Range as Range
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
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Controls"
......@@ -51,8 +52,7 @@ type Controls =
, showControls :: T.Box Boolean
, showEdges :: T.Box SigmaxT.ShowEdgesState
, showLouvain :: T.Box Boolean
, showTree :: T.Box Boolean
, sidePanelState :: T.Box GT.SidePanelState
, showSidebar :: T.Box GT.SidePanelState
, sideTab :: T.Box GET.SideTab
, sigmaRef :: R.Ref Sigmax.Sigma
)
......@@ -82,23 +82,29 @@ controlsCpt = here.component "controls" cpt
, reloadForest
, selectedNodeIds
, session
, showControls
, showEdges
, showLouvain
, sidePanelState
, showSidebar
, sideTab
, sigmaRef } _ = do
-- | States
-- |
forceAtlasState' <- T.useLive T.unequal forceAtlasState
graphStage' <- T.useLive T.unequal graphStage
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
showControls' <- T.useLive T.unequal showControls
sidePanelState' <- T.useLive T.unequal sidePanelState
showSidebar' <- T.useLive T.unequal showSidebar
localControls <- initialLocalControls
-- ref to track automatic FA pausing
-- If user pauses FA before auto is triggered, clear the timeoutId
mFAPauseRef <- R.useRef Nothing
-- | Effects
-- |
-- When graph is changed, cleanup the mFAPauseRef so that forceAtlas
-- timeout is retriggered.
R.useEffect' $ do
......@@ -117,8 +123,8 @@ controlsCpt = here.component "controls" cpt
-- Automatic opening of sidebar when a node is selected (but only first time).
R.useEffect' $ do
if sidePanelState' == GT.InitialClosed && (not Set.isEmpty selectedNodeIds') then do
T.write_ GT.Opened sidePanelState
if showSidebar' == GT.InitialClosed && (not Set.isEmpty selectedNodeIds') then do
T.write_ GT.Opened showSidebar
T.write_ GET.SideTabData sideTab
else
pure unit
......@@ -138,6 +144,10 @@ controlsCpt = here.component "controls" cpt
else
pure unit
-- | Computed
-- |
let edgesConfluenceSorted = A.sortWith (_.confluence) $ Seq.toUnfoldable $ SigmaxT.graphEdges graph
let edgeConfluenceMin = maybe 0.0 _.confluence $ A.head edgesConfluenceSorted
let edgeConfluenceMax = maybe 100.0 _.confluence $ A.last edgesConfluenceSorted
......@@ -157,63 +167,147 @@ controlsCpt = here.component "controls" cpt
let nodeSizeMax = maybe 100.0 _.size $ A.last nodesSorted
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 }
[ RH.ul { className: "navbar-nav mx-auto" }
[ -- change type button (?)
navItem [ centerButton sigmaRef ]
-- , navItem [ resetForceAtlasButton { forceAtlasState, sigmaRef } [] ]
, navItem [ pauseForceAtlasButton { state: forceAtlasState } [] ]
, navItem [ edgesToggleButton { state: showEdges } [] ]
, navItem [ louvainToggleButton { state: showLouvain } [] ]
, navItem [ edgeConfluenceControl { range: edgeConfluenceRange
, state: edgeConfluence } [] ]
, navItem [ edgeWeightControl { range: edgeWeightRange
, state: edgeWeight } [] ]
pure $
H.nav
{ className: "graph-toolbar" }
[
H.div
{ className: "flex-shrink-0" }
[
H.div
{ className: "d-flex" }
[
-- 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
-- file upload
-- run demo
-- search button
-- search topics
, navItem [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4
, navItem [ nodeSizeControl { range: nodeSizeRange
, state: nodeSize } [] ]
-- zoom: 0 -100 - calculate ratio
, navItem [ multiSelectEnabledButton { state: multiSelectEnabled } [] ] -- toggle multi node selection
-- save button
, navItem [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ]
, navItem [ cameraButton { id: graphId
, hyperdataGraph: hyperdataGraph
, session: session
, sigmaRef: sigmaRef
, reloadForest } ]
labelSizeButton sigmaRef localControls.labelSize
,
-- labels size: 1-4
nodeSizeControl
{ range: nodeSizeRange
, state: nodeSize }
]
]
where
navItem = RH.li { className: "nav-item" }
-- RH.ul {} [ -- change type button (?)
-- RH.li {} [ centerButton sigmaRef ]
-- , RH.li {} [ pauseForceAtlasButton {state: forceAtlasState} ]
-- , RH.li {} [ edgesToggleButton {state: showEdges} ]
-- , RH.li {} [ louvainToggleButton showLouvain ]
-- , RH.li {} [ edgeConfluenceControl edgeConfluenceRange edgeConfluence ]
-- , RH.li {} [ edgeWeightControl edgeWeightRange edgeWeight ]
]
-- H.ul {} [ -- change type button (?)
-- H.li {} [ centerButton sigmaRef ]
-- , H.li {} [ pauseForceAtlasButton {state: forceAtlasState} ]
-- , H.li {} [ edgesToggleButton {state: showEdges} ]
-- , H.li {} [ louvainToggleButton showLouvain ]
-- , H.li {} [ edgeConfluenceControl edgeConfluenceRange edgeConfluence ]
-- , H.li {} [ edgeWeightControl edgeWeightRange edgeWeight ]
-- -- change level
-- -- file upload
-- -- run demo
-- -- search button
-- -- search topics
-- , RH.li {} [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4
-- , RH.li {} [ nodeSizeControl nodeSizeRange nodeSize ]
-- , H.li {} [ labelSizeButton sigmaRef localControls.labelSize ] -- labels size: 1-4
-- , H.li {} [ nodeSizeControl nodeSizeRange nodeSize ]
-- -- zoom: 0 -100 - calculate ratio
-- , RH.li {} [ multiSelectEnabledButton multiSelectEnabled ] -- toggle multi node selection
-- , H.li {} [ multiSelectEnabledButton multiSelectEnabled ] -- toggle multi node selection
-- -- save button
-- , RH.li {} [ nodeSearchControl { graph: graph
-- , H.li {} [ nodeSearchControl { graph: graph
-- , multiSelectEnabled: multiSelectEnabled
-- , selectedNodeIds: selectedNodeIds } ]
-- , RH.li {} [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ]
-- , RH.li {} [ cameraButton { id: graphId
-- , H.li {} [ mouseSelectorSizeButton sigmaRef localControls.mouseSelectorSize ]
-- , H.li {} [ cameraButton { id: graphId
-- , hyperdataGraph: hyperdataGraph
-- , session: session
-- , sigmaRef: sigmaRef
......@@ -227,9 +321,8 @@ useGraphControls :: { forceAtlasS :: SigmaxT.ForceAtlasState
, hyperdataGraph :: GET.HyperdataGraph
, reloadForest :: T2.ReloadS
, session :: Session
, showTree :: T.Box Boolean
, sidePanel :: T.Box (Maybe (Record GEST.SidePanel))
, sidePanelState :: T.Box GT.SidePanelState }
}
-> R.Hooks (Record Controls)
useGraphControls { forceAtlasS
, graph
......@@ -237,9 +330,8 @@ useGraphControls { forceAtlasS
, hyperdataGraph
, reloadForest
, session
, showTree
, sidePanel
, sidePanelState } = do
} = do
edgeConfluence <- T.useBox $ Range.Closed { min: 0.0, max: 1.0 }
edgeWeight <- T.useBox $ Range.Closed {
min: 0.0
......@@ -253,7 +345,13 @@ useGraphControls { forceAtlasS
sigma <- Sigmax.initSigma
sigmaRef <- R.useRef sigma
{ multiSelectEnabled, removedNodeIds, selectedNodeIds, showControls, sideTab } <- GEST.focusedSidePanel sidePanel
{ multiSelectEnabled
, removedNodeIds
, selectedNodeIds
, showControls
, sideTab
, showSidebar
} <- GEST.focusedSidePanel sidePanel
pure { edgeConfluence
, edgeWeight
......@@ -270,8 +368,7 @@ useGraphControls { forceAtlasS
, showControls
, showEdges
, showLouvain
, sidePanelState
, showTree
, showSidebar
, sideTab
, sigmaRef
, reloadForest
......
module Gargantext.Components.GraphExplorer where
module Gargantext.Components.GraphExplorer.Layout where
import Gargantext.Prelude hiding (max, min)
import Control.Bind ((=<<))
import DOM.Simple.Types (Element)
import Data.Array as A
import Data.FoldableWithIndex (foldMapWithIndex)
......@@ -14,146 +13,97 @@ import Data.Sequence as Seq
import Data.Set as Set
import Data.Tuple (Tuple(..))
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.Sidebar as GES
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.TopBar as GETB
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.Hooks.Loader (useLoader)
import Gargantext.Hooks.Sigmax.Sigma (startForceAtlas2)
import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Routes (SessionRoute(NodeAPI))
import Gargantext.Sessions (Session, get)
import Gargantext.Sessions (Session)
import Gargantext.Types as GT
import Gargantext.Types as Types
import Gargantext.Utils ((?))
import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import Math as Math
import Partial.Unsafe (unsafePartial)
import Reactix as R
import Reactix.DOM.HTML as RH
import Record as Record
import Record.Extra as RX
import Reactix.DOM.HTML as H
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 =
( graph :: SigmaxT.SGraph
, hyperdataGraph :: GET.HyperdataGraph
| LayoutProps
)
type GraphWriteProps =
( mMetaData' :: Maybe GET.MetaData
| Props
, graph :: SigmaxT.SGraph
, hyperdataGraph :: GET.HyperdataGraph
, session :: Session
, boxes :: Boxes
, graphId :: GET.GraphId
)
type LayoutWithKeyProps =
( key :: String
| 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
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Layout"
pure $ R.fragment
[
explorer (RX.pick props :: Record Props) []
,
R2.createPortal' mTopBarHost
[
GETB.topBar { boxes: props.boxes }
]
]
layout :: R2.Leaf Props
layout = R2.leaf layoutCpt
--------------------------------------------------------------
explorer :: R2.Component Props
explorer = R.createElement explorerCpt
explorerCpt :: R.Component Props
explorerCpt = here.component "explorer" cpt
where
cpt props@{ boxes: { graphVersion, handed, reloadForest, showTree, sidePanelGraph, sidePanelState }
layoutCpt :: R.Component Props
layoutCpt = here.component "explorerWriteGraph" cpt where
cpt props@{ boxes
, graph
, mMetaData'
, graphId
, hyperdataGraph
, session
, hyperdataGraph
} _ = 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
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
controls <- Controls.useGraphControls { forceAtlasS
-- Hooks
-----------------
controls <- Controls.useGraphControls
{ forceAtlasS
, graph
, graphId
, hyperdataGraph
, reloadForest
, reloadForest: boxes.reloadForest
, session
, showTree
, sidePanel: sidePanelGraph
, sidePanelState }
, sidePanel: boxes.sidePanelGraph
}
mTopBarHost <- R.unsafeHooksEffect $ R2.getElementById "portal-topbar"
showControls' <- R2.useLive' controls.showControls
-- graphVersionRef <- R.useRef graphVersion'
-- R.useEffect' $ do
......@@ -175,28 +125,78 @@ explorerCpt = here.component "explorer" cpt
-- T.write_ Graph.Init controls.graphStage
-- T.write_ Types.InitialClosed controls.sidePanelState
-- Render
-----------------
pure $
RH.div { className: "graph-meta-container" }
[ RH.div { className: "graph-container" }
[ RH.div { className: "container-fluid " <> hClass handed' }
[ RH.div { id: "controls-container" } [ Controls.controls controls [] ]
, RH.div { className: "row graph-row" }
[ RH.div { ref: graphRef, id: "graph-view", className: "col-md-12" } []
, graphView { boxes: props.boxes
H.div
{ className: "graph-layout" }
[
-- Topbar
R2.createPortal' mTopBarHost
[
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
, elRef: graphRef
, graph
, hyperdataGraph
, mMetaData
} []
]
]
, mMetaData: mMetaDataBox
}
]
]
hClass h = case h of
Types.LeftHanded -> "lefthanded"
Types.RightHanded -> "righthanded"
--------------------------------------------------------------
type GraphProps =
( boxes :: Boxes
......@@ -207,8 +207,8 @@ type GraphProps =
, mMetaData :: T.Box (Maybe GET.MetaData)
)
graphView :: R2.Component GraphProps
graphView = R.createElement graphViewCpt
graphView :: R2.Leaf GraphProps
graphView = R2.leaf graphViewCpt
graphViewCpt :: R.Component GraphProps
graphViewCpt = here.component "graphView" cpt
where
......@@ -217,7 +217,7 @@ graphViewCpt = here.component "graphView" cpt
, elRef
, graph
, hyperdataGraph: GET.HyperdataGraph { mCamera }
, mMetaData } _children = do
, mMetaData } _ = do
edgeConfluence' <- T.useLive T.unequal controls.edgeConfluence
edgeWeight' <- T.useLive T.unequal controls.edgeWeight
mMetaData' <- T.useLive T.unequal mMetaData
......@@ -249,7 +249,10 @@ graphViewCpt = here.component "graphView" cpt
R.useEffect1' multiSelectEnabled' $ do
R.setRef multiSelectEnabledRef multiSelectEnabled'
pure $ Graph.graph { boxes
pure $
Graph.graph
{ boxes
, elRef
, forceAtlas2Settings: Graph.forceAtlas2Settings
, graph
......@@ -264,6 +267,8 @@ graphViewCpt = here.component "graphView" cpt
, transformedGraph
} []
--------------------------------------------------------
convert :: GET.GraphData -> Tuple (Maybe GET.MetaData) SigmaxT.SGraph
convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges}
where
......@@ -310,6 +315,8 @@ convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges}
targetNode = unsafePartial $ fromJust $ Map.lookup e.target nodesMap
color = sourceNode.color
--------------------------------------------------------------
-- | See sigmajs/plugins/sigma.renderers.customShapes/shape-library.js
modeGraphType :: Types.Mode -> String
modeGraphType Types.Authors = "square"
......@@ -317,12 +324,8 @@ modeGraphType Types.Institutes = "equilateral"
modeGraphType Types.Sources = "star"
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 = (
edgeConfluence' :: Range.NumberRange
......
module Gargantext.Components.GraphExplorer.Legend
( Props, legend, legendCpt
( Props, legend
) where
import Prelude hiding (map)
......@@ -7,31 +7,39 @@ import Prelude hiding (map)
import Data.Sequence (Seq)
import Data.Traversable (foldMap)
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.Utils.Reactix as R2
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Legend"
type Props = ( items :: Seq Legend )
legend :: Record Props -> R.Element
legend props = R.createElement legendCpt props []
legend :: R2.Leaf Props
legend = R2.leaf legendCpt
legendCpt :: R.Component Props
legendCpt = here.component "legend" cpt
where
cpt {items} _ = pure $ RH.div {} [foldMap entry items]
entry :: Legend -> R.Element
entry (Legend {id_, label}) =
RH.p {}
[ RH.span { style: { width : 10
, height: 10
, backgroundColor: intColor id_
, display: "inline-block"
legendCpt = here.component "legend" cpt where
cpt { items } _ = pure $
H.ul
{ className: "graph-legend" }
[
flip foldMap items \(Legend { id_, label }) ->
H.li
{ className: "graph-legend__item" }
[
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
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.RangeControl"
type Props = (
caption :: String
type Props =
( caption :: String
, sliderProps :: Record RS.Props
)
rangeControl :: R2.Component Props
rangeControl = R.createElement rangeControlCpt
rangeControl :: R2.Leaf Props
rangeControl = R2.leaf rangeControlCpt
rangeControlCpt :: R.Component Props
rangeControlCpt = here.component "rangeButton" cpt
where
cpt {caption, sliderProps} _ = do
pure $
H.span {className: "range text-center"}
[ H.label {} [ R2.small {} [ H.text caption ] ]
, RS.rangeSlider sliderProps
cpt {caption, sliderProps} _ = pure $
H.span
{ className: "range-control" }
[
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
)
edgeConfluenceControl :: R2.Component EdgeConfluenceControlProps
edgeConfluenceControl = R.createElement edgeConfluenceControlCpt
edgeConfluenceControl :: R2.Leaf EdgeConfluenceControlProps
edgeConfluenceControl = R2.leaf edgeConfluenceControlCpt
edgeConfluenceControlCpt :: R.Component EdgeConfluenceControlProps
edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt
where
cpt { range: Range.Closed { min, max }
, state } _ = do
, state
} _ = do
state' <- T.useLive T.unequal state
pure $ rangeControl {
......@@ -62,21 +70,24 @@ edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt
, height: 5.0
, onChange: \rng -> T.write_ rng state
}
} []
}
--------------------------------------
type EdgeWeightControlProps = (
range :: Range.NumberRange
type EdgeWeightControlProps =
( range :: Range.NumberRange
, state :: T.Box Range.NumberRange
)
edgeWeightControl :: R2.Component EdgeWeightControlProps
edgeWeightControl = R.createElement edgeWeightControlCpt
edgeWeightControl :: R2.Leaf EdgeWeightControlProps
edgeWeightControl = R2.leaf edgeWeightControlCpt
edgeWeightControlCpt :: R.Component EdgeWeightControlProps
edgeWeightControlCpt = here.component "edgeWeightControl" cpt
where
cpt { range: Range.Closed { min, max }
, state } _ = do
, state
} _ = do
state' <- T.useLive T.unequal state
pure $ rangeControl {
......@@ -90,21 +101,24 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt
, height: 5.0
, onChange: \rng -> T.write_ rng state
}
} []
}
--------------------------------------
type NodeSideControlProps = (
range :: Range.NumberRange
type NodeSideControlProps =
( range :: Range.NumberRange
, state :: T.Box Range.NumberRange
)
nodeSizeControl :: R2.Component NodeSideControlProps
nodeSizeControl = R.createElement nodeSizeControlCpt
nodeSizeControl :: R2.Leaf NodeSideControlProps
nodeSizeControl = R2.leaf nodeSizeControlCpt
nodeSizeControlCpt :: R.Component NodeSideControlProps
nodeSizeControlCpt = here.component "nodeSizeControl" cpt
where
cpt { range: Range.Closed { min, max }
, state } _ = do
, state
} _ = do
state' <- T.useLive T.unequal state
pure $ rangeControl {
......@@ -118,4 +132,4 @@ nodeSizeControlCpt = here.component "nodeSizeControl" cpt
, height: 5.0
, 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
import Prelude
import DOM.Simple.Console (log2)
import Data.Foldable (foldl)
import Data.Foldable (foldl, intercalate)
import Data.Sequence as Seq
import Data.Set as Set
import Effect (Effect)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.InputWithAutocomplete (inputWithAutocomplete)
import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Utils (queryMatchesLabel)
......@@ -23,6 +24,7 @@ type Props = (
graph :: SigmaxT.SGraph
, multiSelectEnabled :: T.Box Boolean
, selectedNodeIds :: T.Box SigmaxT.NodeIds
, className :: String
)
-- | Whether a node matches a search string
......@@ -37,28 +39,43 @@ searchNodes :: String -> Seq.Seq (Record SigmaxT.Node) -> Seq.Seq (Record Sigmax
searchNodes "" _ = Seq.empty
searchNodes s nodes = Seq.filter (nodeMatchesSearch s) nodes
nodeSearchControl :: R2.Component Props
nodeSearchControl = R.createElement nodeSearchControlCpt
nodeSearchControl :: R2.Leaf Props
nodeSearchControl = R2.leaf nodeSearchControlCpt
nodeSearchControlCpt :: R.Component Props
nodeSearchControlCpt = here.component "nodeSearchControl" cpt
where
cpt { graph, multiSelectEnabled, selectedNodeIds } _ = do
cpt props@{ graph, multiSelectEnabled, selectedNodeIds } _ = do
search <- T.useBox ""
search' <- T.useLive T.unequal search
multiSelectEnabled' <- T.useLive T.unequal multiSelectEnabled
let doSearch s = triggerSearch graph s multiSelectEnabled' selectedNodeIds
pure $ R.fragment
[ inputWithAutocomplete { autocompleteSearch: autocompleteSearch graph
, classes: "mx-2"
pure $
H.form
{ className: intercalate " "
[ "graph-node-search"
, props.className
]
}
[
inputWithAutocomplete
{ autocompleteSearch: autocompleteSearch graph
, onAutocompleteClick: doSearch
, onEnterPress: doSearch
, state: search } []
, H.div { className: "btn input-group-addon"
, on: { click: \_ -> doSearch search' }
, classes: ""
, state: 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
......
module Gargantext.Components.GraphExplorer.Sidebar
-- (Props, sidebar)
where
( Props, sidebar
, Common
) where
import Gargantext.Prelude
import Control.Parallel (parTraverse)
import Data.Array (head, last, concat)
import Data.Array (concat, head, last, mapWithIndex)
import Data.Array as A
import Data.Either (Either(..))
import Data.Foldable (intercalate)
import Data.Foldable as F
import Data.Int (fromString)
import Data.Map as Map
import Data.Maybe (Maybe(..), fromJust)
import Data.Sequence as Seq
import Data.Set as Set
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
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.Sidebar.Types as GEST
import Gargantext.Components.GraphExplorer.Types as GET
......@@ -32,13 +37,13 @@ import Gargantext.Ends (Frontends)
import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Sessions (Session)
import Gargantext.Types (CTabNgramType, FrontendError(..), NodeID, TabSubType(..), TabType(..), TermList(..), modeTabType)
import Gargantext.Utils (nbsp)
import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import Math as Math
import Partial.Unsafe (unsafePartial)
import Reactix as R
import Reactix.DOM.HTML as H
import Reactix.DOM.HTML as RH
import Record as Record
import Record.Extra as RX
import Toestand as T
......@@ -59,247 +64,439 @@ type Props = (
| Common
)
sidebar :: R2.Component Props
sidebar = R.createElement sidebarCpt
sidebar :: R2.Leaf Props
sidebar = R2.leaf sidebarCpt
sidebarCpt :: R.Component Props
sidebarCpt = here.component "sidebar" cpt
where
cpt props@{ boxes: { sidePanelGraph } } _ = do
-- States
{ sideTab } <- GEST.focusedSidePanel sidePanelGraph
sideTab' <- T.useLive T.unequal sideTab
pure $ RH.div { id: "sp-container" }
[ sideTabNav { sideTab
, sideTabs: [GET.SideTabLegend, GET.SideTabData, GET.SideTabCommunity] } []
, case sideTab' of
GET.SideTabLegend -> sideTabLegend sideTabProps []
GET.SideTabData -> sideTabData sideTabProps []
GET.SideTabCommunity -> sideTabCommunity sideTabProps []
-- Computed
let
sideTabs =
[ GET.SideTabLegend
, GET.SideTabData
, GET.SideTabCommunity
]
where
sideTabProps = RX.pick props :: Record Props
type SideTabNavProps = (
sideTab :: T.Box GET.SideTab
, sideTabs :: Array GET.SideTab
)
sideTabProps = (RX.pick props :: Record Props)
sideTabNav :: R2.Component SideTabNavProps
sideTabNav = R.createElement sideTabNavCpt
sideTabNavCpt :: R.Component SideTabNavProps
sideTabNavCpt = here.component "sideTabNav" cpt
where
cpt { sideTab, sideTabs } _ = do
sideTab' <- T.useLive T.unequal sideTab
-- Render
pure $
pure $ R.fragment [ H.div { className: "text-primary center"} [H.text ""]
, H.div { className: "nav nav-tabs"} (liItem sideTab' <$> sideTabs)
-- , H.div {className: "center"} [ H.text "Doc sideTabs"]
H.div
{ className: "graph-sidebar" }
[
-- 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"
<> if tab == sideTab'
then " active"
else ""
, on: { click: \_ -> T.write_ tab sideTab }
} [ H.text $ show tab ]
sideTabLegend :: R2.Component Props
sideTabLegend = R.createElement sideTabLegendCpt
------------------------------------------------------------
sideTabLegend :: R2.Leaf Props
sideTabLegend = R2.leaf sideTabLegendCpt
sideTabLegendCpt :: R.Component Props
sideTabLegendCpt = here.component "sideTabLegend" cpt
where
cpt { metaData: GET.MetaData { legend } } _ = do
pure $ H.div {}
[ Legend.legend { items: Seq.fromFoldable legend }
, documentation EN
cpt { metaData: GET.MetaData { legend } } _ = pure $
H.div
{ 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 = here.component "sideTabData" cpt
where
cpt props@{ boxes: { sidePanelGraph } } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ RH.div {}
[ selectedNodes (Record.merge { nodesMap: SigmaxT.nodesGraphMap props.graph } props) []
, neighborhood props []
, RH.div { className: "col-md-12", id: "query" }
[ query { frontends: props.frontends
-- Computed
let
hasSelection = not $ Set.isEmpty selectedNodeIds'
-- 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
, nodesMap: SigmaxT.nodesGraphMap props.graph
, searchType: SearchDoc
, selectedNodeIds: selectedNodeIds'
, session: props.session
} []
}
]
]
------------------------------------------------------------
sideTabCommunity :: R2.Component Props
sideTabCommunity = R.createElement sideTabCommunityCpt
sideTabCommunity :: R2.Leaf Props
sideTabCommunity = R2.leaf sideTabCommunityCpt
sideTabCommunityCpt :: R.Component Props
sideTabCommunityCpt = here.component "sideTabCommunity" cpt
where
cpt props@{ boxes: { sidePanelGraph }
, frontends } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ RH.div { className: "col-md-12", id: "query" }
[ selectedNodes (Record.merge { nodesMap: SigmaxT.nodesGraphMap props.graph } props) []
, neighborhood props []
, query { frontends
-- Computed
let
hasSelection = not $ Set.isEmpty selectedNodeIds'
-- 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
, nodesMap: SigmaxT.nodesGraphMap props.graph
, searchType: SearchContact
, selectedNodeIds: selectedNodeIds'
, session: props.session
} []
}
]
]
-------------------------------------------
sideBarTabSeparator :: R.Element
sideBarTabSeparator =
H.div
{ className: "graph-sidebar__separator" }
[
B.icon
{ name: "angle-down" }
]
-------------------------------------------
-- TODO
-- selectedNodes :: Record Props -> Map.Map String Nodes -> R.Element
type SelectedNodesProps = (
nodesMap :: SigmaxT.NodesMap
type SelectedNodesProps =
( nodesMap :: SigmaxT.NodesMap
| Props
)
selectedNodes :: R2.Component SelectedNodesProps
selectedNodes = R.createElement selectedNodesCpt
selectedNodes :: R2.Leaf SelectedNodesProps
selectedNodes = R2.leaf selectedNodesCpt
selectedNodesCpt :: R.Component SelectedNodesProps
selectedNodesCpt = here.component "selectedNodes" cpt
where
selectedNodesCpt = here.component "selectedNodes" cpt where
cpt props@{ boxes: { sidePanelGraph }
, graph
, nodesMap } _ = do
-- States
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ R2.row
[ R2.col 12
[ RH.ul { className: "nav nav-tabs d-flex justify-content-center"
, id: "myTab"
, role: "tablist" }
[ RH.div { className: "tab-content" }
[ RH.div { className: "d-flex flex-wrap justify-content-center"
, role: "tabpanel" }
( Seq.toUnfoldable
$ ( Seq.map (\node -> badge { minSize: node.size -- same size for all badges
, maxSize: node.size
, node
, selectedNodeIds })
(badges graph selectedNodeIds')
)
-- $ ( Seq.map (\node -> badge { maxSize, minSize, node, selectedNodeIds }) badges')
)
, H.br {}
-- Computed
let
commonProps = RX.pick props :: Record Common
-- Behaviors
let
onBadgeClick id _ = T.write_ (Set.singleton id) selectedNodeIds
-- Render
pure $
H.ul
{ className: intercalate " "
[ "graph-selected-nodes"
, "list-group"
]
}
[
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
, 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
, 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 = R.createElement neighborhoodCpt
neighborhoodCpt :: R.Component Props
neighborhoodCpt = here.component "neighborhood" cpt
where
neighborhood :: R2.Leaf Props
neighborhood = R2.leaf neighborhoodCpt
neighborhoodCpt :: R.Memo Props
neighborhoodCpt = R.memo' $ here.component "neighborhood" cpt where
cpt { boxes: { sidePanelGraph }
, graph
} _ = do
{ selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
state <- T.useBox Folded
state' <- T.useLive T.unequal state
-- States
{ selectedNodeIds } <-
GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <-
T.useLive T.unequal selectedNodeIds
showMore /\ showMoreBox <-
R2.useBox' false
let numberOfBadgesToShowWhenFolded = 5
badges' = neighbourBadges graph selectedNodeIds'
termList /\ termListBox <-
R2.useBox' []
termCount /\ termCountBox <-
R2.useBox' 0
-- Computed
let
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))
orderedBadges = A.sortWith (\n -> -n.size) $ Seq.toUnfoldable badges' -- reverse sort (largest size first)
displayBadges = case state' of
Folded -> A.take numberOfBadgesToShowWhenFolded orderedBadges
Unfolded -> orderedBadges
stateText = case state' of
Folded -> "Show more"
Unfolded -> "Show less"
showFoldedTooltip = A.length orderedBadges > numberOfBadgesToShowWhenFolded
pure $ RH.div { className: "tab-content", id: "myTabContent" }
[ RH.div { -- className: "flex-space-around d-flex justify-content-center"
className: "d-flex flex-wrap flex-space-around"
, id: "home"
, role: "tabpanel"
maxTruncateResult = 5
withTruncateResults = (termCount > maxTruncateResult) && (not showMore)
-- Behaviors
let
onBadgeClick id _ = T.write_ (Set.singleton id) selectedNodeIds
-- Effects
R.useEffect1' selectedNodeIds' do
let refreshed = neighbourBadges graph selectedNodeIds'
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
, on: { click: toggleUnfold state} } [ RH.text stateText ]
[
-- Extracted count
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 = (
buttonType :: String
type UpdateTermButtonProps =
( variant :: ButtonVariant
, nodesMap :: SigmaxT.NodesMap
, rType :: TermList
, text :: String
| Common
)
updateTermButton :: R2.Component UpdateTermButtonProps
updateTermButton = R.createElement updateTermButtonCpt
updateTermButton = R2.component updateTermButtonCpt
updateTermButtonCpt :: R.Component UpdateTermButtonProps
updateTermButtonCpt = here.component "updateTermButton" cpt
where
updateTermButtonCpt = here.component "updateTermButton" cpt where
cpt { boxes: { errors
, reloadForest
, sidePanelGraph }
, buttonType
, variant
, graphId
, metaData
, nodesMap
, rType
, session
, text } _ = do
} children = do
-- States
{ removedNodeIds, selectedNodeIds } <- GEST.focusedSidePanel sidePanelGraph
selectedNodeIds' <- T.useLive T.unequal selectedNodeIds
pure $ if Set.isEmpty selectedNodeIds' then
RH.div {} []
else
RH.button { className: "btn btn-sm btn-" <> buttonType
, on: { click: onClickRemove removedNodeIds selectedNodeIds selectedNodeIds' }
} [ RH.text text ]
where
onClickRemove removedNodeIds selectedNodeIds selectedNodeIds' _ = do
-- Behaviors
let
callback _ = do
let nodes = mapMaybe (\id -> Map.lookup id nodesMap)
$ Set.toUnfoldable selectedNodeIds'
sendPatches { errors
......@@ -312,33 +509,30 @@ updateTermButtonCpt = here.component "updateTermButton" cpt
T.write_ selectedNodeIds' removedNodeIds
T.write_ SigmaxT.emptyNodeIds selectedNodeIds
-- Render
pure $
type BadgeProps =
( maxSize :: Number
, minSize :: Number
, 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"
B.button
{ variant
, callback
}
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 graph selectedNodeIds = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds
......@@ -347,6 +541,8 @@ neighbourBadges :: SigmaxT.SGraph -> SigmaxT.NodeIds -> Seq.Seq (Record SigmaxT.
neighbourBadges graph selectedNodeIds = SigmaxT.neighbours graph selectedNodes' where
selectedNodes' = SigmaxT.graphNodes $ SigmaxT.nodesById graph selectedNodeIds
---------------------------------------------------------
type SendPatches =
( errors :: T.Box (Array FrontendError)
, graphId :: NodeID
......@@ -408,6 +604,8 @@ sendPatch termList session (GET.MetaData metaData) node = do
patch_list :: NTC.Replace TermList
patch_list = NTC.Replace { new: termList, old: MapTerm }
---------------------------------------------------------
type Query =
( frontends :: Frontends
, metaData :: GET.MetaData
......@@ -416,15 +614,15 @@ type Query =
, selectedNodeIds :: SigmaxT.NodeIds
, session :: Session )
query :: R2.Component Query
query = R.createElement queryCpt
query :: R2.Leaf Query
query = R2.leaf queryCpt
queryCpt :: R.Component Query
queryCpt = here.component "query" cpt where
cpt props@{ selectedNodeIds } _ = do
pure $ if Set.isEmpty selectedNodeIds
then RH.div {} []
then H.div {} []
else query' props []
query' :: R2.Component Query
......@@ -439,7 +637,7 @@ queryCpt' = here.component "query'" cpt where
, selectedNodeIds
, session } _ = do
pure $ case (head metaData.corpusId) of
Nothing -> RH.div {} []
Nothing -> H.div {} []
Just corpusId ->
CGT.tabs { frontends
, query: SearchQuery { expected: searchType
......@@ -461,8 +659,8 @@ queryCpt' = here.component "query'" cpt where
------------------------------------------------------------------------
{-, RH.div { className: "col-md-12", id: "horizontal-checkbox" }
[ RH.ul {}
{-, H.div { className: "col-md-12", id: "horizontal-checkbox" }
[ H.ul {}
[ checkbox "Pubs"
, checkbox "Projects"
, checkbox "Patents"
......@@ -472,28 +670,88 @@ queryCpt' = here.component "query'" cpt where
-}
--------------------------------------------------------------------------
documentation :: Lang -> R.Element
documentation _ =
H.div {} [ H.h2 {} [ H.text "What is Graph ?"]
, ul [ "Graph is a conveniant tool to explore your documents. "
, "Nodes are terms selected in your Map List. "
<> "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. "
<> "Link strength is proportional to the strenght of terms association."
H.div
{ className: "graph-documentation" }
[
H.div
{ 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
ul ts = H.ul {} $ map (\t -> H.li {} [ H.text t ]) ts
"Use the edge filter so create a subgraph with links in a given range (e.g. keep the strongest association)."
]
]
]
]
{-
TODO DOC
......@@ -504,4 +762,3 @@ Global/local 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.
-}
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 Data.Maybe (Maybe(..), maybe)
import Data.Set as Set
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax.Types as SigmaxT
import Gargantext.Types as GT
import Reactix as R
import Toestand as T
type SidePanel =
(
......@@ -19,6 +19,7 @@ type SidePanel =
, selectedNodeIds :: SigmaxT.NodeIds
, showControls :: Boolean
, sideTab :: GET.SideTab
, showSidebar :: GT.SidePanelState
)
initialSidePanel :: Maybe (Record SidePanel)
......@@ -32,7 +33,9 @@ focusedSidePanel :: T.Box (Maybe (Record SidePanel))
, removedNodeIds :: T.Box SigmaxT.NodeIds
, selectedNodeIds :: T.Box SigmaxT.NodeIds
, showControls :: T.Box Boolean
, sideTab :: T.Box GET.SideTab }
, sideTab :: T.Box GET.SideTab
, showSidebar :: T.Box GT.SidePanelState
}
focusedSidePanel sidePanel = do
mGraph <- T.useFocused
(maybe Nothing _.mGraph)
......@@ -55,6 +58,9 @@ focusedSidePanel sidePanel = do
sideTab <- T.useFocused
(maybe GET.SideTabLegend _.sideTab)
(\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 $ {
mGraph
......@@ -64,4 +70,5 @@ focusedSidePanel sidePanel = do
, selectedNodeIds
, showControls
, sideTab
, showSidebar
}
......@@ -34,20 +34,35 @@ sizeButtonCpt :: R.Component Props
sizeButtonCpt = here.component "sizeButton" cpt where
cpt { state, caption, min, max, onChange } _ = do
defaultValue <- T.useLive T.unequal state
pure $ H.span { className: "range-simple" }
[ H.label {} [ R2.small {} [ H.text caption ] ]
, H.input { type: "range"
, className: "form-control"
pure $
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
, max: show max
, defaultValue
, on: { input: onChange } }]
, on: { input: onChange }
, className: "range-simple__input"
}
]
]
labelSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element
labelSizeButton sigmaRef state =
sizeButton {
state
, caption: "Label Size"
, caption: "Label size"
, min: 1.0
, max: 30.0
, onChange: \e -> do
......@@ -67,7 +82,7 @@ mouseSelectorSizeButton :: R.Ref Sigmax.Sigma -> T.Box Number -> R.Element
mouseSelectorSizeButton sigmaRef state =
sizeButton {
state
, caption: "Selector Size"
, caption: "Selector size"
, min: 1.0
, max: 50.0
, onChange: \e -> do
......
......@@ -3,27 +3,18 @@ module Gargantext.Components.GraphExplorer.ToggleButton
, toggleButton
, toggleButtonCpt
, controlsToggleButton
, edgesToggleButton
, louvainToggleButton
, multiSelectEnabledButton
, sidebarToggleButton
, pauseForceAtlasButton
, resetForceAtlasButton
) where
import Prelude
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 Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
-- @WIP: used?
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.ToggleButton"
......@@ -56,6 +47,8 @@ toggleButtonCpt = here.component "toggleButton" cpt
text on _off true = on
text _on off false = off
----------------------------------------------------------------
type ControlsToggleButtonProps = (
state :: T.Box Boolean
)
......@@ -73,147 +66,3 @@ controlsToggleButtonCpt = here.component "controlsToggleButton" cpt
, onClick: \_ -> T.modify_ not state
, 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 Reactix as R
import Reactix.DOM.HTML as RH
import Toestand as T
import Gargantext.Prelude hiding (max, min)
import Gargantext.Prelude hiding (max,min)
import Gargantext.Components.App.Data (Boxes)
import Data.Maybe (Maybe)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..))
import Gargantext.Components.GraphExplorer.Search (nodeSearchControl)
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 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 "Gargantext.Components.GraphExplorer.TopBar"
type TopBar =
(
boxes :: Boxes
)
topBar :: R2.Leaf Props
topBar = R2.leaf component
topBar :: R2.Leaf TopBar
topBar = R2.leafComponent topBarCpt
topBarCpt :: R.Component TopBar
topBarCpt = here.component "topBar" cpt where
cpt { boxes: { sidePanelGraph
, sidePanelState } } _ = do
{ mGraph, multiSelectEnabled, selectedNodeIds, showControls } <- GEST.focusedSidePanel sidePanelGraph
component :: R.Component Props
component = here.component "topBar" cpt where
cpt { sidePanelGraph } _ = do
-- States
{ mGraph
, multiSelectEnabled
, 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
Just graph -> nodeSearchControl { graph
, multiSelectEnabled
, selectedNodeIds } []
Nothing -> RH.div {} []
-- Render
pure $
H.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" }
[ Toggle.controlsToggleButton { state: showControls } []
, Toggle.sidebarToggleButton { state: sidePanelState } []
, search
, variant: showSidebar' == GT.Opened ?
ButtonVariant Light $
OutlinedButtonVariant Light
}
[
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
, ui_cwTouchPhone :: Maybe String
, ui_cwTouchMail :: Maybe String }
type UserInfoM
= { ui_id :: NotNull Int
= { token :: NotNull String
, ui_id :: NotNull Int
, ui_username :: String
, ui_email :: String
, ui_title :: String
......
......@@ -29,8 +29,8 @@ type Props =
, state :: T.Box String
)
inputWithAutocomplete :: R2.Component Props
inputWithAutocomplete = R.createElement inputWithAutocompleteCpt
inputWithAutocomplete :: R2.Leaf Props
inputWithAutocomplete = R2.leaf inputWithAutocompleteCpt
inputWithAutocompleteCpt :: R.Component Props
inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt
where
......
......@@ -7,9 +7,6 @@ module Gargantext.Components.Nodes.Annuaire.User.Contact
, saveUserInfo
) 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.Lens as L
import Data.Maybe (Maybe(..), fromMaybe)
......@@ -19,6 +16,7 @@ import Effect.Class (liftEffect)
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.GraphQL (getClient)
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.Nodes.Annuaire.User.Contacts.Tabs as Tabs
import Gargantext.Components.Nodes.Annuaire.User.Contacts.Types (ContactData', HyperdataContact(..))
......@@ -26,8 +24,9 @@ import Gargantext.Components.Nodes.Lists.Types as LT
import Gargantext.Config.REST (AffRESTError, logRESTError)
import Gargantext.Ends (Frontends)
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Prelude (Unit, bind, discard, pure, show, ($), (<$>), (<>))
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.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
......@@ -196,11 +195,13 @@ saveContactHyperdata session id = put session (Routes.NodeAPI Node (Just id) "")
saveUserInfo :: Session -> Int -> UserInfo -> AffRESTError Int
saveUserInfo session id ui = do
let token = getToken session
client <- liftEffect $ getClient session
res <- mutation
client
"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_cwLastName: ga ui.ui_cwLastName
, ui_cwOrganization: ui.ui_cwOrganization
......@@ -215,6 +216,7 @@ saveUserInfo session id ui = do
where
ga Nothing = ArgL IgnoreArg
ga (Just val) = ArgR val
getToken (Session { token }) = token
type AnnuaireLayoutProps = ( annuaireId :: Int, session :: Session | ReloadProps )
......
......@@ -5,19 +5,20 @@ module Gargantext.Components.Nodes.Corpus.Phylo
import Gargantext.Prelude
import DOM.Simple (document, querySelector)
import Data.Maybe (Maybe(..))
import FFI.Simple ((..), (.=))
import Data.Maybe (Maybe(..), isJust)
import Data.Tuple.Nested ((/\))
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.PhyloExplorer.API (get)
import Gargantext.Components.PhyloExplorer.Layout (layout)
import Gargantext.Components.PhyloExplorer.Types (PhyloDataSet)
import Gargantext.Config.REST (logRESTError)
import Gargantext.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Hooks.Loader (useLoaderEffect)
import Gargantext.Sessions (Session)
import Gargantext.Types (NodeID)
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
type MainProps =
( nodeId :: NodeID
......@@ -35,60 +36,93 @@ phyloLayoutCpt :: R.Component MainProps
phyloLayoutCpt = here.component "main" cpt where
cpt { nodeId, session } _ = do
-- | States
-- |
state' /\ state <- R2.useBox' Nothing
-- | Computed
-- |
let
errorHandler = logRESTError here "[phylo]"
handler (dataset :: PhyloDataSet) =
content
handler (phyloDataSet :: PhyloDataSet) =
layout
{ nodeId
, dataset
, phyloDataSet
}
useLoader
-- | Hooks
-- |
useLoaderEffect
{ errorHandler
, loader: get session
, 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
--------------------------------------------------------
type ContentProps =
( nodeId :: NodeID
, dataset :: PhyloDataSet
)
content :: R2.Leaf ContentProps
content = R2.leaf contentCpt
-- @XXX: inopinent <div> (see Gargantext.Components.Router) (@TODO?)
R.useEffectOnce' do
mEl <- querySelector document ".main-page__main-route .container"
contentCpt :: R.Component ContentProps
contentCpt = here.component "content" cpt where
cpt { nodeId, dataset } _ = do
-- Hooks
case mEl of
Nothing -> R.nothing
Just el -> R2.addClass el [ "d-none" ]
useFirstEffect' do
-- @XXX: inopinent <div> (see Gargantext.Components.Router) (@TODO?)
R.useEffectOnce do
pure do
mEl <- querySelector document ".main-page__main-route .container"
case mEl of
Nothing -> pure unit
Just el -> do
style <- pure $ (el .. "style")
pure $ (style .= "display") "none"
Nothing -> R.nothing
Just el -> R2.removeClass el [ "d-none" ]
-- @XXX: reset "main-page__main-route" wrapper margin
-- see Gargantext.Components.Router) (@TODO?)
mEl' <- querySelector document ".main-page__main-route"
case mEl' of
Nothing -> pure unit
Just el -> do
style <- pure $ (el .. "style")
pure $ (style .= "padding") "initial"
-- Render
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 $
layout
{ nodeId
, phyloDataSet: dataset
B.cloak
{ isDisplayed: isJust state'
, 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)
import Gargantext.Components.PhyloExplorer.Types (DisplayView(..), PhyloDataSet(..), ExtractedTerm, ExtractedCount, Source, Term, sortSources)
import Gargantext.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Hooks.UpdateEffect (useUpdateEffect1')
import Gargantext.Types (NodeID)
import Gargantext.Types (NodeID, SidePanelState(..))
import Gargantext.Utils (getter, (?))
import Gargantext.Utils.Reactix as R2
import Graphics.D3.Base (d3)
......@@ -46,6 +46,7 @@ layoutCpt = here.component "layout" cpt where
cpt { phyloDataSet: (PhyloDataSet o)
, nodeId
} _ = do
-- States
---------
......@@ -82,7 +83,7 @@ layoutCpt = here.component "layout" cpt where
R2.useBox' false
sideBarDisplayed /\ sideBarDisplayedBox <-
R2.useBox' false
R2.useBox' InitialClosed
extractedTerms /\ extractedTermsBox <-
R2.useBox' (mempty :: Array ExtractedTerm)
......@@ -256,7 +257,7 @@ layoutCpt = here.component "layout" cpt where
{ className: "phylo__sidebar"
-- @XXX: ReactJS lack of "keep-alive" feature workaround solution
-- @link https://github.com/facebook/react/issues/12039
, style: { display: sideBarDisplayed ? "block" $ "none" }
, style: { display: sideBarDisplayed == Opened? "block" $ "none" }
}
[
sideBar
......
......@@ -86,7 +86,7 @@ component = R.hooksComponent componentName cpt where
B.caveat
{ 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
......@@ -212,8 +212,7 @@ component = R.hooksComponent componentName cpt where
{ className: "phylo-selection-tab__separator" }
[
B.icon
{ name: "angle-down"
}
{ name: "angle-down" }
]
,
-- No extracted result
......@@ -291,6 +290,7 @@ component = R.hooksComponent componentName cpt where
]
,
R2.if' (truncateResults) $
B.button
{ variant: ButtonVariant Light
, callback: \_ -> T.modify_ not showMoreBox
......
......@@ -4,15 +4,14 @@ module Gargantext.Components.PhyloExplorer.SideBar
import Gargantext.Prelude
import Data.Foldable (intercalate)
import Data.Maybe (Maybe)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.PhyloExplorer.DetailsTab (detailsTab)
import Gargantext.Components.PhyloExplorer.SelectionTab (selectionTab)
import Gargantext.Components.PhyloExplorer.Types (ExtractedCount, ExtractedTerm, TabView(..))
import Gargantext.Types (NodeID)
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
......@@ -48,60 +47,27 @@ component = R.hooksComponent componentName cpt where
-- States
tabView /\ tabViewBox <- R2.useBox' DetailsTab
-- Computed
let
tabList = [ DetailsTab, SelectionTab ]
-- Render
pure $
H.div
{ className: "phylo-sidebar" }
[
-- Teasers
H.div
{ className: "phylo-sidebar__top-teaser" }
[]
,
-- Menu
H.ul
{ className: intercalate " "
[ "nav nav-tabs"
, "phylo-sidebar__menu"
]
}
[
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" $ ""
]
B.tabs
{ value: tabView
, list: tabList
, callback: flip T.write_ tabViewBox
}
[
H.text "Selection"
]
]
]
,
-- Details tab
R2.if' (tabView == DetailsTab) $
-- Content
case tabView of
DetailsTab ->
detailsTab
{ key: (show props.nodeId) <> "-details"
, docCount: props.docCount
......@@ -111,9 +77,8 @@ component = R.hooksComponent componentName cpt where
, groupCount: props.groupCount
, branchCount: props.branchCount
}
,
-- Selection tab
R2.if' (tabView == SelectionTab) $
SelectionTab ->
selectionTab
{ key: (show props.nodeId) <> "-selection"
, extractedTerms: props.extractedTerms
......@@ -123,9 +88,4 @@ component = R.hooksComponent componentName cpt where
, selectedSource: props.selectedSource
, selectTermCallback: props.selectTermCallback
}
,
-- Teaser
H.div
{ className: "phylo-sidebar__bottom-teaser" }
[]
]
......@@ -9,6 +9,7 @@ import Effect (Effect)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), ComponentStatus(..), Variant(..))
import Gargantext.Components.PhyloExplorer.Types (Term(..), Source(..))
import Gargantext.Types (SidePanelState(..), toggleSidePanelState)
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2
import Reactix (nothing)
......@@ -27,7 +28,7 @@ type Props =
, resultCallback :: Maybe Term -> Effect Unit
, toolBar :: T.Box (Boolean)
, sideBar :: T.Box (Boolean)
, sideBar :: T.Box (SidePanelState)
)
here :: R2.Here
......@@ -71,12 +72,12 @@ component = here.component "main" cpt where
-- Sidebar toggle
B.button
{ className: "phylo-topbar__sidebar"
, callback: \_ -> T.modify_ (not) sideBar
, variant: sideBar' ?
, callback: \_ -> T.modify_ (toggleSidePanelState) sideBar
, variant: sideBar' == Opened ?
ButtonVariant Light $
OutlinedButtonVariant Light
}
[ H.text $ sideBar' ? "Hide sidebar" $ "Show sidebar" ]
[ H.text $ sideBar' == Opened ? "Hide sidebar" $ "Show sidebar" ]
,
-- Source
H.div
......
......@@ -469,6 +469,9 @@ data TabView
derive instance Generic 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
renderScale ref {width,height} (Range.Closed {min, max}) =
H.div { ref, className, width, height, aria } []
where
className = "scale"
className = "range-slider__scale"
aria = { label: "Scale running from " <> show min <> " to " <> show max }
renderScaleSel :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScaleSel ref props (Range.Closed {min, max}) =
H.div { ref, className, style} []
where
className = "scale-sel"
className = "range-slider__scale-sel"
style = {left: computeLeft, width: computeWidth}
percOffsetMin = Range.normalise props.bounds min
percOffsetMax = Range.normalise props.bounds 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 ref (Range.Closed value) bounds set precision =
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
]
......@@ -185,7 +185,7 @@ renderKnob knob ref (Range.Closed value) bounds set precision =
text (Just num) = num
text Nothing = "error"
tabIndex = 0
className = "knob"
className = "range-slider__knob"
aria = { label: labelPrefix knob <> "value: " <> show val }
labelPrefix MinKnob = "Minimum "
labelPrefix MaxKnob = "Maximum "
......@@ -220,4 +220,3 @@ roundRange :: Epsilon -> Bounds -> Range.NumberRange -> Range.NumberRange
roundRange epsilon bounds (Range.Closed initial) = Range.Closed { min, max }
where min = round epsilon bounds initial.min
max = round epsilon bounds initial.max
......@@ -6,17 +6,13 @@ import Data.Array (filter, length)
import Data.Array as A
import Data.Foldable (intercalate)
import Data.Maybe (Maybe(..))
import Data.Tuple.Nested ((/\))
import Data.UUID (UUID)
import Data.UUID as UUID
import Effect (Effect)
import Gargantext.Components.App.Data (Boxes)
import Gargantext.Components.ErrorsView (errorsView)
import Gargantext.Components.Footer (footer)
import Gargantext.Components.Forest as Forest
import Gargantext.Components.GraphExplorer as GraphExplorer
import Gargantext.Components.GraphExplorer.Sidebar as GES
import Gargantext.Components.GraphExplorer.Sidebar.Types as GEST
import Gargantext.Components.Forest (forestLayout)
import Gargantext.Components.Login (login)
import Gargantext.Components.Nodes.Annuaire (annuaireLayout)
import Gargantext.Components.Nodes.Annuaire.User (userLayout)
......@@ -25,7 +21,8 @@ import Gargantext.Components.Nodes.Corpus (corpusLayout)
import Gargantext.Components.Nodes.Corpus.Code (corpusCodeLayout)
import Gargantext.Components.Nodes.Corpus.Dashboard (dashboardLayout)
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.Frame (frameLayout)
import Gargantext.Components.Nodes.Home (homeLayout)
......@@ -42,7 +39,6 @@ import Gargantext.Routes as GR
import Gargantext.Sessions (Session, WithSession)
import Gargantext.Sessions as Sessions
import Gargantext.Types (CorpusId, Handed(..), ListId, NodeID, NodeType(..), SessionId, SidePanelState(..))
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix (getElementById)
import Gargantext.Utils.Reactix as R2
import Reactix as R
......@@ -67,10 +63,11 @@ router :: R2.Leaf Props
router = R2.leafComponent routerCpt
routerCpt :: R.Component Props
routerCpt = here.component "router" cpt where
cpt { boxes: boxes@{ handed, showLogin } } _ = do
cpt { boxes: boxes@{ handed, showLogin, showTree } } _ = do
-- States
handed' <- T.useLive T.unequal handed
handed' <- R2.useLive' handed
showLogin' <- R2.useLive' showLogin
showTree' <- R2.useLive' showTree
-- Effects
let
......@@ -100,75 +97,22 @@ routerCpt = here.component "router" cpt where
, TopBar.topBar { boxes }
, errorsView { errors: boxes.errors } []
, H.div { className: "router__inner" }
[ forest { boxes }
, mainPage { boxes }
, sidePanel { boxes }
[
-- @XXX: ReactJS lack of "keep-alive" feature workaround solution
-- @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.leafComponent mainPageCpt
......@@ -184,7 +128,10 @@ mainPageCpt = here.component "mainPage" cpt where
findTile :: UUID -> Record Tile -> Boolean
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
list <- T.read listBox
newList <- pure $ filter (_ # tile.id # findTile # not) list
......@@ -258,15 +205,13 @@ mainPageCpt = here.component "mainPage" cpt where
]
]
--------------------------------------------------------------
forest :: R2.Leaf Props
forest = R2.leaf forestCpt
forestCpt :: R.Memo Props
forestCpt = R.memo' $ here.component "forest" cpt where
cpt { boxes: boxes@{ showTree } } _ = do
-- States
showTree' <- T.useLive T.unequal showTree
cpt { boxes } _ = do
-- Hooks
resizeHandler <- useResizeHandler
......@@ -279,13 +224,9 @@ forestCpt = R.memo' $ here.component "forest" cpt where
pure $
H.div
{ className: intercalate " "
[ "router__aside"
, showTree' ? "" $ "d-none"
]
}
{ className: "router__aside" }
[
Forest.forestLayout
forestLayout
{ boxes
, frontends: defaultFrontends
}
......@@ -300,6 +241,8 @@ forestCpt = R.memo' $ here.component "forest" cpt where
]
]
--------------------------------------------------------------
sidePanel :: R2.Leaf Props
sidePanel = R2.leafComponent sidePanelCpt
sidePanelCpt :: R.Component Props
......@@ -316,6 +259,8 @@ sidePanelCpt = here.component "sidePanel" cpt where
Opened -> pure $ openedSidePanel (Record.merge { session: s } props) []
_ -> pure $ H.div {} []
--------------------------------------------------------------
type RenderRouteProps =
( route :: AppRoute
| Props
......@@ -360,6 +305,8 @@ renderRouteCpt = here.component "renderRoute" cpt where
GR.UserPage s n -> user (sessionNodeProps s n) []
]
--------------------------------------------------------------
type AuthedProps =
( content :: Session -> R.Element
| SessionProps )
......@@ -383,18 +330,17 @@ authedCpt = here.component "authed" cpt where
where
homeProps = RE.pick props :: Record Props
--------------------------------------------------------------
openedSidePanel :: R2.Component (WithSession Props)
openedSidePanel = R.createElement openedSidePanelCpt
openedSidePanelCpt :: R.Component (WithSession Props)
openedSidePanelCpt = here.component "openedSidePanel" cpt where
cpt { boxes: boxes@{ route
, sidePanelGraph
, sidePanelState
, sidePanelTexts }
, 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
let wrapper = H.div { className: "side-panel" }
......@@ -404,19 +350,6 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where
pure $ wrapper
[ Lists.sidePanel { session
, 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
pure $ wrapper
[ Texts.textsSidePanel { boxes
......@@ -424,6 +357,8 @@ openedSidePanelCpt = here.component "openedSidePanel" cpt where
, sidePanel: sidePanelTexts } [] ]
_ -> pure $ wrapper []
--------------------------------------------------------------
annuaire :: R2.Component SessionNodeProps
annuaire = R.createElement annuaireCpt
annuaireCpt :: R.Component SessionNodeProps
......@@ -435,6 +370,8 @@ annuaireCpt = here.component "annuaire" cpt where
, nodeId
, session } } sessionProps) []
--------------------------------------------------------------
corpus :: R2.Component SessionNodeProps
corpus = R.createElement corpusCpt
corpusCpt :: R.Component SessionNodeProps
......@@ -446,6 +383,8 @@ corpusCpt = here.component "corpus" cpt where
, nodeId
, session } } sessionProps) []
--------------------------------------------------------------
corpusCode :: R2.Component SessionNodeProps
corpusCode = R.createElement corpusCodeCpt
corpusCodeCpt :: R.Component SessionNodeProps
......@@ -465,6 +404,8 @@ corpusCodeCpt = here.component "corpusCode" cpt where
pure $ authed authedProps []
--------------------------------------------------------------
type CorpusDocumentProps =
( corpusId :: CorpusId
, listId :: ListId
......@@ -484,6 +425,8 @@ corpusDocumentCpt = here.component "corpusDocument" cpt
, nodeId
, session } [] } sessionProps )[]
--------------------------------------------------------------
dashboard :: R2.Component SessionNodeProps
dashboard = R.createElement dashboardCpt
dashboardCpt :: R.Component SessionNodeProps
......@@ -494,6 +437,8 @@ dashboardCpt = here.component "dashboard" cpt
pure $ authed (Record.merge { content: \session ->
dashboardLayout { boxes, nodeId, session } [] } sessionProps) []
--------------------------------------------------------------
type DocumentProps = ( listId :: ListId | SessionNodeProps )
document :: R2.Component DocumentProps
......@@ -508,6 +453,8 @@ documentCpt = here.component "document" cpt where
, mCorpusId: Nothing
, session } [] } sessionProps) []
--------------------------------------------------------------
graphExplorer :: R2.Component SessionNodeProps
graphExplorer = R.createElement graphExplorerCpt
graphExplorerCpt :: R.Component SessionNodeProps
......@@ -520,17 +467,19 @@ graphExplorerCpt = here.component "graphExplorer" cpt where
authedProps =
Record.merge
{ content:
\session -> GraphExplorer.explorerLayoutWithKey
\session -> graphLayout
{ boxes
, graphId: nodeId
, key: "graphId-" <> show nodeId
, session }
[]
, session
}
}
sessionProps
pure $ authed authedProps []
--------------------------------------------------------------
phyloExplorer :: R2.Component SessionNodeProps
phyloExplorer = R.createElement phyloExplorerCpt
......@@ -544,7 +493,7 @@ phyloExplorerCpt = here.component "phylo" cpt where
authedProps =
Record.merge
{ content:
\session -> PhyloExplorer.phyloLayout
\session -> phyloLayout
{ boxes
, nodeId
, session
......@@ -554,6 +503,7 @@ phyloExplorerCpt = here.component "phylo" cpt where
pure $ authed authedProps []
--------------------------------------------------------------
home :: R2.Component Props
home = R.createElement homeCpt
......@@ -562,6 +512,8 @@ homeCpt = here.component "home" cpt where
cpt { boxes } _ = do
pure $ homeLayout { boxes }
--------------------------------------------------------------
lists :: R2.Component SessionNodeProps
lists = R.createElement listsCpt
listsCpt :: R.Component SessionNodeProps
......@@ -575,6 +527,8 @@ listsCpt = here.component "lists" cpt where
, session
, sessionUpdate: \_ -> pure unit } [] } sessionProps) []
--------------------------------------------------------------
login' :: Boxes -> R.Element
login' { backend, sessions, showLogin: visible } =
login { backend
......@@ -582,6 +536,8 @@ login' { backend, sessions, showLogin: visible } =
, sessions
, visible }
--------------------------------------------------------------
routeFile :: R2.Component SessionNodeProps
routeFile = R.createElement routeFileCpt
routeFileCpt :: R.Component SessionNodeProps
......@@ -591,6 +547,8 @@ routeFileCpt = here.component "routeFile" cpt where
pure $ authed (Record.merge { content: \session ->
fileLayout { nodeId, session } } sessionProps) []
--------------------------------------------------------------
type RouteFrameProps = (
nodeType :: NodeType
| SessionNodeProps
......@@ -605,6 +563,8 @@ routeFrameCpt = here.component "routeFrame" cpt where
pure $ authed (Record.merge { content: \session ->
frameLayout { nodeId, nodeType, session } } sessionProps) []
--------------------------------------------------------------
team :: R2.Component SessionNodeProps
team = R.createElement teamCpt
teamCpt :: R.Component SessionNodeProps
......@@ -616,6 +576,8 @@ teamCpt = here.component "team" cpt where
, nodeId
, session } } sessionProps) []
--------------------------------------------------------------
texts :: R2.Component SessionNodeProps
texts = R.createElement textsCpt
textsCpt :: R.Component SessionNodeProps
......@@ -630,6 +592,8 @@ textsCpt = here.component "texts" cpt
, nodeId
, session } [] } sessionProps) []
--------------------------------------------------------------
user :: R2.Component SessionNodeProps
user = R.createElement userCpt
userCpt :: R.Component SessionNodeProps
......@@ -643,6 +607,8 @@ userCpt = here.component "user" cpt where
, nodeId
, session } [] } sessionProps) []
--------------------------------------------------------------
type ContactProps = ( annuaireId :: NodeID | SessionNodeProps )
contact :: R2.Component ContactProps
......
......@@ -9,6 +9,7 @@ import Gargantext.Components.Bootstrap.Types (ButtonVariant(..), Variant(..))
import Gargantext.Components.Lang (langSwitcher, allFeLangs)
import Gargantext.Components.Themes (themeSwitcher, allThemes)
import Gargantext.Types (Handed(..))
import Gargantext.Utils ((?))
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
......@@ -64,7 +65,9 @@ topBarCpt = here.component "topBar" cpt
]
,
B.button
{ variant: ButtonVariant Light
{ variant: showTree' ?
ButtonVariant Light $
OutlinedButtonVariant Light
, callback: const $ T.modify_ (not) showTree
, className: "main-topbar__tree-switcher"
}
......
......@@ -10,6 +10,8 @@ import Reactix (nothing, thenNothing)
import Reactix as R
-- | Hook triggered on first mount event only
-- |
-- | /!\ @TODO cleanup function not working
useFirstMount :: R.Hooks (Boolean)
useFirstMount = do
firstMount <- R.useRef true
......
module Gargantext.Utils.Popover where
import Gargantext.Prelude
import DOM.Simple as DOM
import Data.Maybe (maybe)
import Data.Nullable (Nullable, toMaybe)
import DOM.Simple as DOM
import Effect (Effect)
import Effect.Uncurried (EffectFn2, runEffectFn2)
import Reactix as R
import Gargantext.Prelude
import Record as Record
type PopoverRef = R.Ref (Nullable DOM.Element)
......@@ -22,8 +23,11 @@ type Props =
foreign import popoverCpt :: R.Component Props
-- | https://github.com/vaheqelyan/react-awesome-popover
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
......
@import "./base/_reset.scss"
@import "./base/_general.scss"
@import "./base/_form.scss"
@import "./base/_layout.scss"
@import "./base/_nav.scss"
@import "./base/_typography.scss"
@import "./base/_animations.scss"
@import "./base/_bootstrap.scss"
@import "./base/_placeholder.scss"
@import "./base/_utilities.scss"
@import "./base/_range_slider.sass"
......@@ -5,7 +5,6 @@
@import "./_legacy/_tree"
@import "./_legacy/_code_editor"
@import "./_legacy/_styles"
@import "./_legacy/_range_slider"
@import "./_legacy/_annotation"
@import "./_legacy/_folder_view"
@import "./_legacy/_phylo"
@mixin sidePanelCommon
position: absolute
max-height: 600px
//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
/* Grid constants */
$layout-height: calc(100vh - #{ $topbar-height} )
.nav-item
padding-left: 0.8rem
/////////////////////////////////////////////
.graph-row
height: 100vh
.graph-loader
width: 100%
height: 100%
#graph-view
height: 95vh
&__spinner
$size: 100px
$weight: 6px
#tree
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
max-height: 300px
overflow-y: scroll
width: 300px
top: 50px
width: $sidebar-width
height: $sidebar-height
z-index: z-index('graph-layout', 'sidebar')
@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
@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
overflow: visible
height: auto
#graph-tree
.tree
margin-top: 27px
.nopadding
padding: 0 !important
......
......@@ -9,6 +9,11 @@
&__tree-switcher
margin-right: space-x(1)
&__tree-switcher
$fixed-width: 136px
width: $fixed-width
// add hovering effect
&.navbar-dark .navbar-text:hover
color: $navbar-dark-hover-color
......
......@@ -26,7 +26,6 @@
/* Grid constants */
$topbar-height: 56px; // ~ unworthy empirical value (@TODO topbar height calculation)
$graph-margin: 16px;
$layout-height: calc(100vh - #{ $topbar-height} );
......@@ -38,16 +37,6 @@ $left-column-width: 10%;
$center-column-width: 85%;
$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 */
$graph-background-color: $body-bg;
......@@ -448,34 +437,33 @@ $decreasing-color: #11638F;
////////////////////////////////////////////////////////////////
.phylo-topbar {
$margin: space-x(0.5);
$fixed-button-width: 136px;
@include aside-topbar();
padding-left: $margin;
padding-right: $margin;
padding-left: $topbar-item-margin;
padding-right: $topbar-item-margin;
display: flex;
&__toolbar,
&__sidebar {
width: $fixed-button-width;
margin-left: $margin;
margin-right: $margin;
width: $topbar-fixed-button-width;
margin-left: $topbar-item-margin;
margin-right: $topbar-item-margin;
}
&__source {
width: $topbar-input-width;
margin-left: $margin;
margin-right: $margin;
margin-left: $topbar-item-margin;
margin-right: $topbar-item-margin;
}
&__autocomplete {
display: flex;
width: $topbar-input-width;
position: relative;
margin-left: $margin;
margin-right: $margin;
margin-left: $topbar-item-margin;
margin-right: $topbar-item-margin;
}
&__suggestion {
......@@ -506,60 +494,11 @@ $decreasing-color: #11638F;
////////////////////////////////////////////////////////////////
.phylo-sidebar {
$teaser-height: 16px;
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%;
}
@include sidebar;
}
.phylo-details-tab {
$margin-x: $tab-margin-x;
$margin-x: $sidebar-tab-margin-x;
$margin-y: space-x(2);
&__counter {
......@@ -585,7 +524,7 @@ $decreasing-color: #11638F;
}
.phylo-selection-tab {
$margin-x: $tab-margin-x;
$margin-x: $sidebar-tab-margin-x;
$margin-y: space-x(2);
&__highlight {
......@@ -609,7 +548,6 @@ $decreasing-color: #11638F;
&__item {
white-space: normal;
word-break: break-word;
// remove "_reboot.scss" line height
line-height: initial;
&: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 @@
/* */
.btn-primary
color: white
background-color: #005a9aff
border-color: black
.frame
height: 100vh
iframe
......
......@@ -60,10 +60,9 @@ $node-popup-width: 544px
////////////////////////////////////
$leaf-margin-bottom: 3px
.mainleaf
$self: &
$leaf-margin-bottom: 3px
display: flex
align-items: center
......@@ -168,11 +167,6 @@ $leaf-margin-bottom: 3px
@include left-handed
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")
&::before
// margin for the before background
......@@ -221,11 +215,10 @@ $leaf-margin-bottom: 3px
&__settings-icon
// altering component overlay offset to fit it with the ".mainleaf" overlay
// dimension
$aside-icon-offset-y: -5px
$aside-icon-offset-top: -5px
&.b-icon-button--overlay::before
top: $aside-icon-offset-y
bottom: $aside-icon-offset-y
top: $aside-icon-offset-top
//----------------------------
......@@ -266,16 +259,13 @@ $leaf-margin-bottom: 3px
//----------------------------
&--selected
&--selected &
#{ $self }__node-link::before
&__node-link::before
content: ""
background-color: $gray-100
#{ $self }__node-icon
color: $primary
#{ $self }__node-link a
&__node-link a
color: $primary
font-weight: bold
......@@ -291,7 +281,7 @@ $leaf-margin-bottom: 3px
&--blank // for <Cloak> use cases
$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-offset-y: 5px
......@@ -311,6 +301,30 @@ $leaf-margin-bottom: 3px
position: absolute
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 @@
}
/// Mixins
///--------------------------
......@@ -122,3 +121,13 @@
bottom: $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
$space-unit: 8px;
/// Misc
$overlay-radius: 5px;
// Bootstrap color system
// * 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
/// @link https://medium.com/alistapart/sassier-z-index-management-for-complex-layouts-4540717a9488
......@@ -16,7 +32,8 @@ $z-indexes: (
"sidebar",
"spinner",
),
phylo-sidebar: (
"teaser",
graph-layout: (
"toolbar",
"sidebar",
),
);
......@@ -46,3 +46,7 @@
from { transform: rotate(0deg); }
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 @@
label {
font-weight: 600;
margin-bottom: initial; // reset Bootstrap "_reboot.scss"
}
&--sub {
......@@ -83,3 +82,14 @@
color: $input-placeholder-color;
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 @@
h4, h5, h6 {
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 @@
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() {
$border-color: mix-alpha($navbar-dark-hover-color, 5%);
......@@ -28,3 +46,40 @@
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 */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import '../../../node_modules/bootstrap/scss/mixins';
///==========================================
/// Theme variable overrides
///==========================================
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
@import "./abstract";
// Fonts
@import url(https://fonts.googleapis.com/css?family=Comfortaa:200,300,400,700);
$headings-font-family:Comfortaa;
$headings-font-family: "Comfortaa";
// Colors
/*$enable-grid-classes:false;*/
$primary:#FF550B;
$secondary:#303030;
$success:#015668;
$danger:#FF304F;
$info:#0F81C7;
$warning:#0DE2EA;
$light:#e8e8e8;
$dark:#000000;
$theme-colors: (
$primary :#FF550B;
$secondary :#303030;
$success :#015668;
$danger :#FF304F;
$info :#0F81C7;
$warning :#0DE2EA;
$light :#e8e8e8;
$dark :#000000;
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
......@@ -44,89 +33,114 @@ $theme-colors: (
'info': $info,
'warning': $warning,
'light': $light,
'dark': $dark
'dark': $dark,
)
);
// Add more shades to gray
$gray-150: #FAFAFA;
$gray-175: #F0F0F0;
$gray-100: #212529;
$gray-150: #2E2E2E; // (+)
$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;
$gray-300:#000000;
$gray-800:#555555;
$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%);
$list-group-bg:lighten($body-bg,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;
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
@import "../_components";
@import "../_base";
@import "../_legacy";
$input-bg:$gray-300;
$input-disabled-bg: $gray-100;
// 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;}
.table.able {color:#ccccc5}
#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 */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import '../../../node_modules/bootstrap/scss/mixins';
///==========================================
/// Theme variable overrides
///==========================================
// 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
//-------------------------
@import "./abstract";
// Colors
$primary: #005a9a;
$secondary: $blue;
$theme-colors: (
$theme-colors: map.merge(
$theme-colors,
(
"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
.btn-secondary,
......
/*! Themestr.app `Greyson` Bootstrap 4.3.1 theme */
/* 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';
@import '../../../node_modules/bootstrap/scss/mixins';
///==========================================
/// Theme variable overrides
///==========================================
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
@import "./abstract";
// Fonts
@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);
$headings-font-family:Oswald;
$headings-font-family: "Oswald";
// Colors
// $enable-grid-classes:false;
$primary:#2f3c48;
$secondary:#6f7f8c;
$success:#3e4d59;
$danger:#cc330d;
$info:#5c8f94;
$warning:#6e9fa5;
$light:#eceeec;
$dark:#1e2b37;
$theme-colors: (
$primary :#2f3c48;
$secondary :#6f7f8c;
$success :#3e4d59;
$danger :#cc330d;
$info :#5c8f94;
$warning :#6e9fa5;
$light :#eceeec;
$dark :#1e2b37;
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
......@@ -46,29 +38,16 @@ $theme-colors: (
'info': $info,
'warning': $warning,
'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);
$enable-rounded:false;
// (importing Bootstrap)
@import '../../../node_modules/bootstrap/scss/bootstrap';
// Project sheets
//---------------
// Misc
$enable-rounded: false;
@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 */
@use 'sass:map';
// Bootstrap pre-requiring
//------------------------
@import '../../../node_modules/bootstrap/scss/functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import '../../../node_modules/bootstrap/scss/mixins';
///==========================================
/// Theme variable overrides
///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts
@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);
$headings-font-family:Crete Round;
$headings-font-family: "Crete Round";
// Colors
/*$enable-grid-classes:false;*/
$primary:#083358;
$secondary:#F67280;
$success:#0074E4;
$danger:#FF4057;
$info:#74DBEF;
$warning:#FC3C3C;
$light:#F2F2F0;
$dark:#072247;
$theme-colors: (
$primary :#083358;
$secondary :#F67280;
$success :#0074E4;
$danger :#FF4057;
$info :#74DBEF;
$warning :#FC3C3C;
$light :#F2F2F0;
$dark :#072247;
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
......@@ -46,28 +37,13 @@ $theme-colors: (
'info': $info,
'warning': $warning,
'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 */
/* 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';
@import '../../../node_modules/bootstrap/scss/mixins';
///==========================================
/// Theme variable overrides
///==========================================
@import "./abstract";
// Project pre-requiring
//----------------------
@import '../_abstract';
@import '../_modules';
// Theme variable overrides
//-------------------------
// Fonts
@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);
$headings-font-family:Open Sans;
$headings-font-family: "Open Sans";
// Colors
// $enable-grid-classes:false;
$primary:#222222;
$secondary:#666666;
$success:#333333;
$danger:#434343;
$info:#515151;
$warning:#5f5f5f;
$light:#eceeec;
$dark:#111111;
$theme-colors: (
$primary :#222222;
$secondary :#666666;
$success :#333333;
$danger :#434343;
$info :#515151;
$warning :#5f5f5f;
$light :#eceeec;
$dark :#111111;
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
......@@ -47,30 +38,13 @@ $theme-colors: (
'info': $info,
'warning': $warning,
'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