Commit bb0f1dfa authored by Przemyslaw Kaminski's avatar Przemyslaw Kaminski

Merge branch 'dev' into 395-dev-ps-0.15-update

parents 3644779c 5cf74ca2
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"name": "Gargantext",
"version": "0.0.5.9.5",
"version": "0.0.6",
"scripts": {
"generate-purs-packages-nix": "./nix/generate-purs-packages.nix",
"generate-psc-packages-nix": "./nix/generate-packages-json.bash",
......
......@@ -74,15 +74,31 @@ annotatedFieldInner = R2.leafComponent annotatedFieldInnerCpt
annotatedFieldInnerCpt :: R.Component InnerProps
annotatedFieldInnerCpt = here.component "annotatedFieldInner" cpt where
cpt { menuRef, ngrams, redrawMenu, setTermList, text: fieldText, mode } _ = do
cpt { menuRef
, ngrams
, redrawMenu
, setTermList
, text: fieldText
, mode
} _ = do
-- | States
-- |
_redrawMenu' <- T.useLive T.unequal redrawMenu
-- menu <- T.useBox (Nothing :: Maybe (Record AnnotationMenu))
let wrap (text /\ list) = { list
, onSelect: onAnnotationSelect { menuRef, ngrams, redrawMenu, setTermList }
, text }
-- | Computed
-- |
let
wrap :: Tuple String (List (Tuple NgramsTerm TermList)) -> Record RunProps
wrap (text /\ list)
= { list
, onSelect: onAnnotationSelect { menuRef, ngrams, redrawMenu, setTermList }
, text
}
-- | Render
-- |
pure $
H.div
......
......@@ -78,14 +78,14 @@ componentName = "b-modal"
-- | - a FFI fix has been added to remove left elements
-- | - an overlay has been added to synchronise the close button
-- | - the keyboard shortcut has been removed
-- | @https://stackoverflow.com/questions/50168312/bootstrap-4-close-modal-backdrop-doesnt-disappear
-- | @link https://stackoverflow.com/questions/50168312/bootstrap-4-close-modal-backdrop-doesnt-disappear
-- |
-- | https://getbootstrap.com/docs/4.6/components/modal/
-- | @link https://getbootstrap.com/docs/4.6/components/modal/
baseModal :: forall r. R2.OptComponent Options Props r
baseModal = R2.optComponent component options
component :: R.Component Props
component = R.hooksComponent componentName cpt where
component :: R.Memo Props
component = R.memo' $ R.hooksComponent componentName cpt where
cpt props@{ isVisibleBox
, title
, hasCollapsibleBackground
......@@ -94,12 +94,13 @@ component = R.hooksComponent componentName cpt where
, noBody
, size
} children
= R.unsafeHooksEffect (UUID.genUUID >>= pure <<< UUID.toString)
>>= \uuid -> do
= do
-- | States
-- |
isVisible <- R2.useLive' isVisibleBox
uuid <- R.unsafeHooksEffect (UUID.genUUID >>= pure <<< UUID.toString)
-- | Computed
-- |
let
......
......@@ -93,7 +93,7 @@ component = R.hooksComponent componentName cpt where
R2.select
{ className
, on: { change }
, disabled: elem status [ Disabled ]
, disabled: elem status [ Disabled, Idled ]
, readOnly: elem status [ Idled ]
, type: props.type
, value: props.value
......
......@@ -5,18 +5,22 @@ module Gargantext.Components.Document.Layout
import Gargantext.Prelude
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
import Data.Ord (greaterThan)
import Data.String (length)
import Data.String as String
import Data.Tuple.Nested ((/\))
import Gargantext.Components.Annotation.Field as AnnotatedField
import Gargantext.Components.Annotation.Types as AFT
import Gargantext.Components.AutoUpdate (autoUpdate)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (SpinnerTheme(..))
import Gargantext.Components.Bootstrap.Types (ComponentStatus(..), SpinnerTheme(..))
import Gargantext.Components.Document.Types (DocPath, Document(..), LoadedData, initialState)
import Gargantext.Components.NgramsTable.AutoSync (useAutoSync)
import Gargantext.Components.Node (NodePoly(..))
import Gargantext.Core.NgramsTable.Functions (addNewNgramA, applyNgramsPatches, coreDispatch, findNgramRoot, setTermListA)
import Gargantext.Core.NgramsTable.Types (CoreAction(..), Versioned(..), replace)
import Gargantext.Hooks.FirstEffect (useFirstEffect')
import Gargantext.Utils ((?))
import Gargantext.Utils as U
import Gargantext.Utils.Reactix as R2
import Reactix as R
......@@ -63,12 +67,11 @@ layoutCpt = here.component "main" cpt where
state'@{ ngramsLocalPatch } /\ state <-
R2.useBox' $ initialState { loaded }
mode' /\ mode <- R2.useBox' AFT.EditionMode
mode' /\ mode <- R2.useBox' AFT.AdditionMode
-- | Hooks
-- |
let dispatch = coreDispatch path state
forceAdditionMode' /\ forceAdditionMode <- R2.useBox' false
let dispatch = coreDispatch path state
{ onPending, result } <- useAutoSync { state, action: dispatch }
onPending' <- R2.useLive' onPending
......@@ -77,6 +80,7 @@ layoutCpt = here.component "main" cpt where
-- | Computed
-- |
let
withAutoUpdate = false
ngrams = applyNgramsPatches state' initTable
......@@ -98,6 +102,21 @@ layoutCpt = here.component "main" cpt where
hasAbstract = maybe false (not String.null) doc.abstract
-- | Hooks
-- |
-- (?) Limit large document feature with empirical length value
-- see #423
useFirstEffect' do
let len = maybe 0 (length) doc.abstract
if (len `greaterThan` 4500)
then
T.write_ true forceAdditionMode
*> T.write_ AFT.AdditionMode mode
else
T.write_ false forceAdditionMode
*> T.write_ AFT.EditionMode mode
-- | Behaviors
-- |
let
......@@ -123,7 +142,7 @@ layoutCpt = here.component "main" cpt where
[
-- Viewing mode
B.wad
[ "d-flex", "align-items-center" ]
[ "d-flex", "align-items-center", "width-auto" ]
[
H.label
{ className: "mr-1"
......@@ -136,6 +155,9 @@ layoutCpt = here.component "main" cpt where
B.formSelect
{ value: show mode'
, callback: onModeChange
, status: forceAdditionMode' ?
Idled $
Enabled
}
[
H.option
......@@ -147,6 +169,14 @@ layoutCpt = here.component "main" cpt where
[ H.text "Add and edit terms" ]
]
]
,
R2.when forceAdditionMode' $
B.wad
[ "color-warning", "font-size-100", "mx-2", "inline-block" ]
[
H.text "limited term feature due to abstract length"
]
,
R2.when withAutoUpdate $
-- (?) purpose? would still working with current code?
......@@ -162,6 +192,7 @@ layoutCpt = here.component "main" cpt where
-- , ngramsLocalPatch
-- , performAction: dispatch
-- }
]
,
H.div
......
......@@ -299,7 +299,7 @@ childLoaderCpt = here.component "childLoader" cpt where
closeBox { isBoxVisible } =
liftEffect $ T.write_ false isBoxVisible
refreshTree p@{ reloadTree } = liftEffect $ T2.reload reloadTree *> closeBox p
refreshTree p@{ reloadTree } = liftEffect $ closeBox p *> T2.reload reloadTree
deleteNode' nt p@{ boxes: { forestOpen }, session, tree: (NTree (LNode {id, parent_id}) _) } = do
case nt of
......
module Gargantext.Components.Forest.Tree.Node.Action.ManageTeam where
import Gargantext.Prelude
import Data.Array (filter, null)
import Data.Either (Either(..))
import Effect.Aff (runAff_)
import Effect.Class (liftEffect)
import Gargantext.Components.Forest.Tree.Node.Tools as Tools
import Gargantext.Components.GraphQL.Endpoints (deleteTeamMembership, getTeam)
import Gargantext.Components.GraphQL.Team (TeamMember)
import Gargantext.Config.REST (AffRESTError, logRESTError)
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Sessions (Session)
import Gargantext.Types (ID, NodeType)
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
here :: R2.Here
here = R2.here "Gargantext.Components.Forest.Tree.Node.Action.ManageTeam"
type ActionManageTeam = (
id :: ID
, nodeType :: NodeType
, session :: Session
)
actionManageTeam :: R2.Component ActionManageTeam
actionManageTeam = R.createElement actionManageTeamCpt
actionManageTeamCpt :: R.Component ActionManageTeam
actionManageTeamCpt = here.component "actionManageTeam" cpt where
cpt {id, session} _ = do
useLoader { errorHandler
, loader: loadTeam
, path: { nodeId: id, session }
, render: \team -> teamLayoutWrapper { team
, nodeId: id
, session
} []
}
where
errorHandler = logRESTError here "teamLayout"
type TeamProps =
( nodeId :: ID
, session :: Session
, team :: (Array TeamMember)
)
teamLayoutWrapper :: R2.Component TeamProps
teamLayoutWrapper = R.createElement teamLayoutWrapperCpt
teamLayoutWrapperCpt :: R.Component TeamProps
teamLayoutWrapperCpt = here.component "teamLayoutWrapper" cpt where
cpt {nodeId, session, team} _ = do
teamS <- T.useBox team
team' <- T.useLive T.unequal teamS
error <- T.useBox ""
error' <- T.useLive T.unequal error
pure $ teamLayoutRows {nodeId, session, team: teamS, team', error, error'}
type TeamRowProps =
( nodeId :: ID
, session :: Session
, team :: T.Box (Array TeamMember)
, error :: T.Box String
, team' :: Array TeamMember
, error' :: String
)
teamLayoutRows :: R2.Leaf TeamRowProps
teamLayoutRows = R2.leafComponent teamLayoutRowsCpt
teamLayoutRowsCpt :: R.Component TeamRowProps
teamLayoutRowsCpt = here.component "teamLayoutRows" cpt where
cpt { team, nodeId, session, error, team', error' } _ = do
case null team' of
true -> pure $ H.div { style: {margin: "10px"}}
[ H.h4 {} [H.text "Your team is empty, you can send some invitations."]]
false -> pure $ Tools.panel (map makeTeam team') (H.div {} [H.text error'])
where
makeTeam :: TeamMember -> R.Element
makeTeam { username, shared_folder_id } = H.div {className: "from-group row"} [ H.div { className: "col-8" } [ H.text username ]
, H.a { className: "text-danger col-2 fa fa-times"
, title: "Remove user from team"
, type: "button"
, on: {click: submit shared_folder_id }
} []
]
submit sharedFolderId _ = do
runAff_ callback $ saveDeleteTeam { session, nodeId, sharedFolderId }
callback res =
case res of
Left _ -> do
_ <- liftEffect $ T.write "Only the Team owner can remove users" error
pure unit
Right val ->
case val of
Left _ -> do
pure unit
Right r -> do
T.write_ (filter (\tm -> tm.shared_folder_id /= r) team') team
-------------------------------------------------------------
type LoadProps =
(
session :: Session,
nodeId :: Int
)
loadTeam :: Record LoadProps -> AffRESTError (Array TeamMember)
loadTeam { session, nodeId } = getTeam session nodeId
type DeleteProps =
(
session :: Session,
nodeId :: Int,
sharedFolderId :: Int
)
saveDeleteTeam ∷ Record DeleteProps -> AffRESTError Int
saveDeleteTeam { session, nodeId, sharedFolderId } = deleteTeamMembership session sharedFolderId nodeId
......@@ -22,6 +22,7 @@ import Gargantext.Components.Forest.Tree.Node.Action.Types (Action)
import Gargantext.Components.Forest.Tree.Node.Action.Update (update)
import Gargantext.Components.Forest.Tree.Node.Action.Upload (actionUpload)
import Gargantext.Components.Forest.Tree.Node.Action.WriteNodesDocuments (actionWriteNodesDocuments)
import Gargantext.Components.Forest.Tree.Node.Action.ManageTeam (actionManageTeam)
import Gargantext.Components.Forest.Tree.Node.Box.Types (NodePopupProps, NodePopupS)
import Gargantext.Components.Forest.Tree.Node.Settings (NodeAction(..), SettingsBox(..), glyphiconNodeAction, settingsBox)
import Gargantext.Components.Forest.Tree.Node.Status (Status(..), hasStatus)
......@@ -189,6 +190,7 @@ panelActionCpt = here.component "panelAction" cpt
cpt { action: Download, id, nodeType, session} _ = pure $ actionDownload { id, nodeType, session } []
cpt { action: Upload, dispatch, id, nodeType, session} _ = pure $ actionUpload { dispatch, id, nodeType, session } []
cpt { action: Delete, nodeType, dispatch} _ = pure $ actionDelete { dispatch, nodeType } []
cpt { action: ManageTeam, nodeType, id, session} _ = pure $ actionManageTeam { id, nodeType, session } []
cpt { action: Add xs, dispatch, id, name, nodeType} _ =
pure $ addNodeView {dispatch, id, name, nodeType, nodeTypes: xs} []
cpt { action: Refresh , dispatch, nodeType } _ = pure $ update { dispatch, nodeType } []
......
......@@ -16,6 +16,7 @@ data NodeAction = Documentation NodeType
| Download | Upload | Refresh | Config
| Delete
| Share
| ManageTeam
| Publish { subTreeParams :: SubTreeParams }
| Add (Array NodeType)
| Merge { subTreeParams :: SubTreeParams }
......@@ -37,6 +38,7 @@ instance Eq NodeAction where
eq Clone Clone = true
eq Delete Delete = true
eq Share Share = true
eq ManageTeam ManageTeam = true
eq (Link x) (Link y) = x == y
eq (Add x) (Add y) = x == y
eq (Merge x) (Merge y) = x == y
......@@ -57,6 +59,7 @@ instance Show NodeAction where
show Clone = "Clone"
show Delete = "Delete"
show Share = "Share"
show ManageTeam = "Team"
show Config = "Config"
show (Link _) = "Link to " -- <> show x
show (Add _) = "Add Child" -- foldl (\a b -> a <> show b) "Add " xs
......@@ -78,6 +81,7 @@ glyphiconNodeAction (Merge _) = "random"
glyphiconNodeAction Refresh = "refresh"
glyphiconNodeAction Config = "wrench"
glyphiconNodeAction Share = "user-plus"
glyphiconNodeAction ManageTeam = "users"
glyphiconNodeAction AddingContact = "user-plus"
glyphiconNodeAction (Move _) = "share-square-o"
glyphiconNodeAction (Publish _) = fldr FolderPublic true
......@@ -137,6 +141,7 @@ settingsBox Team =
, NodeFrameVisio
]
, Share
, ManageTeam
, Delete
]
}
......
......@@ -23,8 +23,11 @@ forgotPasswordLayoutCpt = here.component "forgotPasswordLayout" cpt where
useLoader { errorHandler
, loader: loadPassword
, path: { server, uuid }
, render: \{ password } ->
H.p {} [ H.text ("Your new password is: " <> password) ] }
, render: \{ password } ->
H.div { className:"container text-center justify-content-center" } [
H.div {className: "row"} [ H.div {className: "mx-auto"} [ H.img { src: "images/logo.png" } ] ]
, H.div {className: "row"} [ H.div {className: "col"} [ H.text ("Your new password is: " <> password) ] ]
]}
where
errorHandler = logRESTError here "[forgotPasswordLayout]"
......
......@@ -28,9 +28,10 @@ import Gargantext.Components.GraphExplorer.Sidebar.DocList (docListWrapper)
import Gargantext.Components.GraphExplorer.Sidebar.Legend as Legend
import Gargantext.Components.GraphExplorer.Store as GraphStore
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Components.GraphExplorer.Utils as GEU
import Gargantext.Components.Lang (Lang(..))
import Gargantext.Core.NgramsTable.Functions as NTC
import Gargantext.Config.REST (AffRESTError)
import Gargantext.Core.NgramsTable.Functions as NTC
import Gargantext.Core.NgramsTable.Types as CNT
import Gargantext.Data.Array (mapMaybe)
import Gargantext.Ends (Frontends)
......@@ -38,7 +39,7 @@ import Gargantext.Hooks.FirstEffect (useFirstEffect')
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, setter, (?))
import Gargantext.Utils (getter, nbsp, setter, (?))
import Gargantext.Utils.Reactix as R2
import Gargantext.Utils.Toestand as T2
import Partial.Unsafe (unsafePartial)
......@@ -101,15 +102,53 @@ sideTabLegend :: R2.Leaf Props
sideTabLegend = R2.leaf sideTabLegendCpt
sideTabLegendCpt :: R.Component Props
sideTabLegendCpt = here.component "sideTabLegend" cpt
where
cpt { metaData: GET.MetaData { legend } } _ = pure $
sideTabLegendCpt = here.component "sideTabLegend" cpt where
cpt { metaData: GET.MetaData { legend } } _ = do
-- | States
-- |
store <- GraphStore.use
hyperdataGraph
<- R2.useLive' store.hyperdataGraph
-- | Computed
-- |
let
maxItemPerCluster = 4
-- | Hooks
-- |
-- For each provided Cluster (see Legend), extract the greatest nodes
extractedNodeList <- R.useMemo1 hyperdataGraph $ const $
flip A.foldMap legend
( getter _.id_
>>> GEU.takeGreatestNodeByCluster
hyperdataGraph
maxItemPerCluster
)
-- For each provided Cluster (see Legend), count the number of nodes
nodeCountList <- R.useMemo1 hyperdataGraph $ const $
flip A.foldMap legend
( getter _.id_
>>> GEU.countNodeByCluster hyperdataGraph
>>> A.singleton
)
-- | Render
-- |
pure $
H.div
{ className: "graph-sidebar__legend-tab" }
[
Legend.legend
{ items: Seq.fromFoldable legend }
{ legendSeq: Seq.fromFoldable legend
, extractedNodeList
, nodeCountList
, selectedNodeIds: store.selectedNodeIds
}
,
H.hr {}
,
......
......@@ -4,42 +4,196 @@ module Gargantext.Components.GraphExplorer.Sidebar.Legend
import Prelude hiding (map)
import Data.Array as A
import Data.Maybe (isJust, maybe)
import Data.Sequence (Seq)
import Data.Traversable (foldMap)
import Data.Set as Set
import Data.Traversable (foldMap, intercalate)
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax.Types as ST
import Gargantext.Utils (getter, nbsp, (?))
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
import Gargantext.Components.GraphExplorer.Types (Legend(..), intColor)
import Gargantext.Utils.Reactix as R2
import Toestand as T
here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Sidebar.Legend"
type Props = ( items :: Seq Legend )
type Props =
( legendSeq :: Seq GET.Legend
, extractedNodeList :: Array GET.Node
, nodeCountList :: Array GET.ClusterCount
, selectedNodeIds :: T.Box ST.NodeIds
)
legend :: R2.Leaf Props
legend = R2.leaf legendCpt
legendCpt :: R.Component Props
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_ }
cpt { legendSeq
, extractedNodeList
, nodeCountList
, selectedNodeIds
} _ = do
-- | Hooks
-- |
R.useEffectOnce' $ here.info2 "legend" extractedNodeList
-- | Render
-- |
pure $
H.ul
{ className: "graph-legend" }
[
flip foldMap legendSeq \(GET.Legend { id_, label }) ->
H.li
{ className: "graph-legend__item" }
[
H.div
{ className: "graph-legend__code"
, style: { backgroundColor: GET.intColor id_ }
}
[]
,
B.wad
[ "flex-grow-1" ]
[
B.div'
{ className: "graph-legend__title" }
label
,
selectedNodes
{ selectedNodeIds
, extractedNodeList
, clusterId: id_
, nodeCount: getClusterNodeCount nodeCountList id_
}
]
]
]
filterByCluster :: Int -> Array GET.Node -> Array GET.Node
filterByCluster id
= A.filter
( getter _.attributes
>>> getter _.clustDefault
>>> eq id
)
getClusterNodeCount :: Array GET.ClusterCount -> Int -> Int
getClusterNodeCount nodeCountList id
= nodeCountList
# A.find
( getter _.id
>>> eq id
)
>>> maybe 0
( getter _.count
)
---------------------------------------------------------
type SelectedNodesProps =
( extractedNodeList :: Array GET.Node
, selectedNodeIds :: T.Box ST.NodeIds
, clusterId :: Int
, nodeCount :: Int
)
selectedNodes :: R2.Leaf SelectedNodesProps
selectedNodes = R2.leaf selectedNodesCpt
selectedNodesCpt :: R.Component SelectedNodesProps
selectedNodesCpt = here.component "selectedNodes" cpt where
cpt { extractedNodeList
, selectedNodeIds
, clusterId
, nodeCount
} _ = do
-- | States
-- |
selectedNodeIds' <- R2.useLive' selectedNodeIds
-- | Computed
-- |
let
isSelected id
= selectedNodeIds'
# A.fromFoldable
# A.find
( eq id
)
# isJust
countValue
= extractedNodeList
# A.length
# (nodeCount - _)
-- | Behaviors
-- |
let
onBadgeClick id _ = T.write_ (Set.singleton id) selectedNodeIds
-- | Render
-- |
pure $
H.ul
{ className: "graph-legend-nodes" }
[
flip foldMap (filterByCluster clusterId extractedNodeList)
\(GET.Node { label: nodeLabel, id_: nodeId }) ->
H.li
{ className: "graph-legend-nodes__item" }
[
H.a
{ className: intercalate " "
[ "graph-legend-nodes__badge"
, (isSelected nodeId) ?
"graph-legend-nodes__badge--selected" $
""
, "badge badge-light"
]
, on: { click: onBadgeClick nodeId }
}
[ H.text nodeLabel ]
]
,
R2.when (eq countValue 0) $
H.li
{ className: intercalate " "
[ "graph-legend-nodes__item"
, "graph-legend-nodes__item--count"
]
}
[
H.text "0 node"
]
,
R2.when (not $ eq countValue 0) $
H.li
{ className: intercalate " "
[ "graph-legend-nodes__item"
, "graph-legend-nodes__item--count"
]
}
[]
,
H.span
{ className: "graph-legend__caption" }
[ H.text label ]
]
]
[
H.text "+"
,
H.text $ nbsp 1
,
H.text $ show countValue
,
H.text $ nbsp 1
,
H.text $ eq countValue 1 ? "node" $ "nodes"
]
]
......@@ -72,6 +72,12 @@ instance JSON.ReadForeign Cluster where
instance JSON.WriteForeign Cluster where
writeImpl (Cluster cl) = JSON.writeImpl $ Record.rename clustDefaultP clust_defaultP cl
newtype ClusterCount = ClusterCount
{ id :: Int
, count :: Int
}
derive instance Generic ClusterCount _
derive instance Newtype ClusterCount _
newtype Edge = Edge {
confluence :: Number
......
module Gargantext.Components.GraphExplorer.Utils where
import Data.Maybe (Maybe(..))
module Gargantext.Components.GraphExplorer.Utils
( stEdgeToGET, stNodeToGET
, normalizeNodes
, takeGreatestNodeByCluster, countNodeByCluster
) where
import Gargantext.Prelude
import Data.Array as A
import Data.Maybe (Maybe(..))
import Data.Newtype (wrap)
import Gargantext.Components.GraphExplorer.Types as GET
import Gargantext.Hooks.Sigmax.Types as ST
import Gargantext.Utils (getter)
import Gargantext.Utils.Array as GUA
stEdgeToGET :: Record ST.Edge -> GET.Edge
stEdgeToGET { _original } = _original
......@@ -24,6 +29,8 @@ stNodeToGET { id, label, x, y, _original: GET.Node { attributes, size, type_ } }
, y
}
-----------------------------------------------------------------------
normalizeNodes :: Array GET.Node -> Array GET.Node
normalizeNodes ns = map normalizeNode ns
where
......@@ -49,3 +56,37 @@ normalizeNodes ns = map normalizeNode ns
Just ydiv -> 1.0 / ydiv
normalizeNode (GET.Node n@{ x, y }) = GET.Node $ n { x = x * xdivisor
, y = y * ydivisor }
------------------------------------------------------------------------
takeGreatestNodeByCluster :: GET.HyperdataGraph -> Int -> Int -> Array GET.Node
takeGreatestNodeByCluster graphData take clusterId
= graphData
# getter _.graph
>>> getter _.nodes
>>> A.filter
( getter _.attributes
>>> getter _.clustDefault
>>> eq clusterId
)
>>> A.sortWith
( getter _.size
)
>>> A.takeEnd take
>>> A.reverse
countNodeByCluster :: GET.HyperdataGraph -> Int -> GET.ClusterCount
countNodeByCluster graphData clusterId
= graphData
# getter _.graph
>>> getter _.nodes
>>> A.filter
( getter _.attributes
>>> getter _.clustDefault
>>> eq clusterId
)
>>> A.length
>>> { id: clusterId
, count: _
}
>>> wrap
......@@ -14,6 +14,7 @@ import Gargantext.Components.GraphQL.IMT as GQLIMT
import Gargantext.Components.GraphQL.Node (Node)
import Gargantext.Components.GraphQL.Tree (TreeFirstLevel)
import Gargantext.Components.GraphQL.User (User, UserInfo, UserInfoM)
import Gargantext.Components.GraphQL.Team (TeamMember, TeamDeleteM)
import Gargantext.Ends (Backend(..))
import Gargantext.Sessions (Session(..))
import Gargantext.Utils.Reactix as R2
......@@ -77,7 +78,9 @@ type Schema
, users :: { user_id :: Int } ==> Array User
, tree :: { root_id :: Int } ==> TreeFirstLevel
, annuaire_contacts :: { contact_id :: Int } ==> Array AnnuaireContact
, team :: { team_node_id :: Int } ==> Array TeamMember
}
type Mutation
= { update_user_info :: UserInfoM ==> Int }
= { update_user_info :: UserInfoM ==> Int
, delete_team_membership :: TeamDeleteM ==> Array Int }
......@@ -2,22 +2,24 @@ module Gargantext.Components.GraphQL.Endpoints where
import Gargantext.Prelude
import Gargantext.Components.GraphQL.Node (Node, nodeParentQuery, nodesQuery)
import Gargantext.Components.GraphQL.Tree (TreeFirstLevel, treeFirstLevelQuery)
import Gargantext.Components.GraphQL.User (UserInfo, userInfoQuery)
import Data.Array as A
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Gargantext.Components.GraphQL (queryGql)
import Gargantext.Components.GraphQL (getClient, queryGql)
import Gargantext.Components.GraphQL.IMT as GQLIMT
import Gargantext.Components.GraphQL.Node (Node, nodeParentQuery, nodesQuery)
import Gargantext.Components.GraphQL.Team (TeamMember, teamQuery)
import Gargantext.Components.GraphQL.Tree (TreeFirstLevel, treeFirstLevelQuery)
import Gargantext.Components.GraphQL.User (UserInfo, userInfoQuery)
import Gargantext.Config.REST (RESTError(..), AffRESTError)
import Gargantext.Sessions (Session)
import Gargantext.Sessions (Session(..))
import Gargantext.Types (NodeType)
import Gargantext.Utils.Reactix as R2
import Gargnatext.Components.GraphQL.Contact (AnnuaireContact, annuaireContactQuery)
import GraphQL.Client.Args (onlyArgs)
import GraphQL.Client.Query (mutation)
import GraphQL.Client.Variables (withVars)
here :: R2.Here
......@@ -70,3 +72,28 @@ getTreeFirstLevel session id = do
{ tree } <- queryGql session "get tree first level" $ treeFirstLevelQuery `withVars` { id }
liftEffect $ here.log2 "[getTreeFirstLevel] tree first level" tree
pure $ Right tree -- TODO: error handling
getTeam :: Session -> Int -> AffRESTError (Array TeamMember)
getTeam session id = do
{ team } <- queryGql session "get team" $ teamQuery `withVars` { id }
liftEffect $ here.log2 "[getTree] data" team
pure $ Right team
type SharedFolderId = Int
type TeamNodeId = Int
deleteTeamMembership :: Session -> SharedFolderId -> TeamNodeId -> AffRESTError Int
deleteTeamMembership session sharedFolderId teamNodeId = do
let token = getToken session
client <- liftEffect $ getClient session
{ delete_team_membership } <- mutation
client
"delete_team_membership"
{ delete_team_membership: onlyArgs { token: token
, shared_folder_id: sharedFolderId
, team_node_id: teamNodeId } }
pure $ case A.head delete_team_membership of
Nothing -> Left (CustomError $ "Failed to delete team membership. team node id=" <> show teamNodeId <> " shared folder id=" <> show sharedFolderId)
Just _ -> Right sharedFolderId
where
getToken (Session { token }) = token
module Gargantext.Components.GraphQL.Team where
import Gargantext.Prelude
import GraphQL.Client.Args (NotNull, (=>>))
import GraphQL.Client.Variable (Var(..))
type TeamMember
= { username :: String
, shared_folder_id :: Int
}
type TeamDeleteM
= { token :: NotNull String
, shared_folder_id :: Int
, team_node_id :: Int
}
teamQuery = { team: { team_node_id: Var :: _ "id" Int } =>>
{ username: unit
, shared_folder_id: unit }
}
\ No newline at end of file
......@@ -5,9 +5,11 @@ module Gargantext.Components.Login where
import Gargantext.Prelude
import DOM.Simple.Event as DE
import Data.Array (head)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.String as DST
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
import Gargantext.Components.Bootstrap as B
......@@ -23,6 +25,7 @@ import Gargantext.Sessions as Sessions
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Reactix.DOM.HTML as H
import Reactix.SyntheticEvent as RE
import Toestand as T
here :: R2.Here
......@@ -44,6 +47,27 @@ login :: R2.Leaf Props
login = R2.leaf loginCpt
loginCpt :: R.Component Props
loginCpt = here.component "login" cpt where
cpt props@{ visible } _ = do
-- Render
pure $
B.baseModal
{ isVisibleBox: visible
, title: Just "GarganText ecosystem explorer"
, size: ExtraLargeModalSize
}
[
loginContainer
props
]
-- | @XXX React re-rendering issue with `React.Portal`
-- | @link https://github.com/facebook/react/issues/12247
loginContainer :: R2.Leaf Props
loginContainer = R2.leaf loginContainerCpt
loginContainerCpt :: R.Component Props
loginContainerCpt = here.component "container" cpt where
cpt props@{ sessions, visible } _ = do
-- States
mBackend <- R2.useLive' props.backend
......@@ -53,54 +77,101 @@ loginCpt = here.component "login" cpt where
-- Render
pure $
B.baseModal
{ isVisibleBox: visible
, title: Just "GarganText ecosystem explorer"
, size: ExtraLargeModalSize
}
H.div
{}
[
case mBackend of
Nothing -> chooser props
Just backend -> case formType' of
Login -> form { backend, formType, sessions, visible }
ForgotPassword -> forgotPassword { backend, sessions }
Login ->
form
{ backend
, formType
, sessions
, visible
}
ForgotPassword ->
forgotPassword
{ backend
, sessions
}
]
chooser :: R2.Leaf Props
chooser = R2.leafComponent chooserCpt
chooser = R2.leaf chooserCpt
chooserCpt :: R.Component Props
chooserCpt = here.component "chooser" cpt where
cpt { backend, backends, sessions } _ = do
sessions' <- T.useLive T.unequal sessions
pure $
R.fragment $
[ H.h2 { className: "mx-auto" } [ H.text "Workspace manager" ]]
<> activeConnections sessions sessions' <>
[ H.h3 {} [ H.text "Existing places (click to login)" ]
, H.table { className : "table" }
[ H.thead { className: "thead-light" }
[ H.tr {} (map header headers) ]
, H.tbody {} (map (renderBackend backend) backends)
]
, H.input { className: "form-control", type:"text", placeholder } ]
placeholder = "Search for your institute"
H.div
{} $
[
H.h2
{ className: "mx-auto" }
[ H.text "Workspace manager" ]
]
<>
activeConnections sessions sessions'
<>
[
H.h3
{}
[ H.text "Existing places (click to login)" ]
,
H.table
{ className : "table" }
[
H.thead
{ className: "thead-light" }
[
H.tr
{}
(map header headers)
]
,
H.tbody
{}
(map (renderBackend backend) backends)
]
,
H.input
{ className: "form-control"
, type:"text"
, placeholder: "Search for your institute"
}
]
headers = [ "", "GarganText places", "Fonction", "Garg protocol url" ]
header label = H.th {} [ H.text label ]
-- Shown in the chooser
activeConnections :: forall s. T.ReadWrite s Sessions => s -> Sessions -> Array R.Element
activeConnections _ sessions' | Sessions.null sessions' = []
activeConnections sessions sessions' =
[ H.h3 {} [ H.text "Active place(s)" ]
, H.table { className : "table" }
[ H.thead { className: "thead-light" }
[ H.tr {} (map header headers) ]
, H.tbody {} [renderSessions sessions sessions']
]
activeConnections _ sessions' | Sessions.null sessions' = mempty
activeConnections sessions sessions' =
[
H.h3
{}
[ H.text "Active place(s)" ]
,
H.table
{ className : "table" }
[
H.thead
{ className: "thead-light" }
[
H.tr
{}
(map header headers)
]
,
H.tbody
{}
[ renderSessions sessions sessions' ]
]
]
where
headers = [ "", "Active(s) connection(s)", "Fonction", "Clear data/Logout"]
header label = H.th {} [ H.text label ]
where
headers = [ "", "Active(s) connection(s)", "Fonction", "Clear data/Logout"]
header label = H.th {} [ H.text label ]
......@@ -144,7 +215,11 @@ renderBackend cursor backend@(Backend {name, baseUrl, backendType}) =
]
where
className = "fa fa-hand-o-right" -- "glyphitem fa fa-log-in"
click _ = T.write_ (Just backend) cursor
click :: RE.SyntheticEvent DE.Event -> Effect Unit
click e = do
RE.preventDefault e
T.write_ (Just backend) cursor
backendLabel :: String -> String
backendLabel =
......
......@@ -98,7 +98,7 @@ submitButtonCpt = here.component "submitButton" cpt where
let isValid = agreed && (username `notEq` "") && (password `notEq` "")
pure $ H.div { className: "text-center" }
[ loginSubmit isValid $ submitForm { backend, formType, sessions, visible } cell ]
-- Attempts to submit the form
submitForm :: forall s v. T.ReadWrite s Sessions => T.Write v Boolean
=> Record (Props s v) -> T.Box Form -> ChangeEvent -> Effect Unit
......
......@@ -957,7 +957,7 @@ mainNgramsTableCacheOnCpt = here.component "mainNgramsTableCacheOn" cpt where
, spinnerClass: Nothing
}
versionEndpoint { defaultListId, path: { nodeId, tabType, session } } _ = get session $ Routes.GetNgramsTableVersion { listId: defaultListId, tabType } (Just nodeId)
errorHandler = logRESTError here "[mainNgramsTable]"
errorHandler = logRESTError here "[mainNgramsTableCacheOn]"
mkRequest :: PageParams -> GUC.Request
mkRequest path@{ session } = GUC.makeGetRequest session $ url path
where
......@@ -994,7 +994,7 @@ mainNgramsTableCacheOffCpt = here.component "mainNgramsTableCacheOff" cpt where
, path
, render }
errorHandler = logRESTError here "[mainNgramsTable]"
errorHandler = logRESTError here "[mainNgramsTableCacheOff]"
-- NOTE With cache off
loader :: PageParams -> AffRESTError VersionedWithCountNgramsTable
......
......@@ -575,10 +575,12 @@ listsCpt = here.component "lists" cpt where
login' :: Boxes -> R.Element
login' { backend, sessions, showLogin: visible } =
login { backend
, backends: A.fromFoldable defaultBackends
, sessions
, visible }
login
{ backend
, backends: A.fromFoldable defaultBackends
, sessions
, visible
}
--------------------------------------------------------------
......
......@@ -84,6 +84,7 @@ useLoaderEffect { errorHandler, loader: loader', path, state } = do
then pure $ R.nothing
else do
R.setRef oPath path
liftEffect $ T.write_ Nothing state
R2.affEffect "G.H.Loader.useLoaderEffect" $ do
l <- loader' path
case l of
......@@ -102,31 +103,9 @@ useLoaderBox :: forall path st. Eq path => Eq st
=> Record (UseLoaderBox path st)
-> R.Hooks R.Element
useLoaderBox { errorHandler, loader: loader', path, render } = do
state <- T.useBox Nothing
useLoaderBoxEffect { errorHandler, loader: loader', path, state: state }
pure $ loader { render, state } []
type UseLoaderBoxEffect path state =
( errorHandler :: RESTError -> Effect Unit
, loader :: path -> AffRESTError state
, path :: T.Box path
, state :: T.Box (Maybe state)
)
useLoaderBoxEffect :: forall st path. Eq path => Eq st
=> Record (UseLoaderBoxEffect path st)
-> R.Hooks Unit
useLoaderBoxEffect { errorHandler, loader: loader', path, state } = do
path' <- T.useLive T.unequal path
R.useEffect' $ do
R2.affEffect "G.H.Loader.useLoaderBoxEffect" $ do
l <- loader' path'
case l of
Left err -> liftEffect $ errorHandler err
Right l' -> liftEffect $ T.write_ (Just l') state
useLoader { errorHandler, loader: loader', path: path', render }
newtype HashedResponse a = HashedResponse { hash :: Hash, value :: a }
......
......@@ -3,7 +3,7 @@
exports._add = add;
exports._remove = remove;
/**
* @name add
* @function add
* @param {Window} window
* @param {Document} document
* @param {String} sourceQuery
......@@ -22,13 +22,18 @@ function add(window, document, sourceQuery, targetQuery, type) {
}
function startResizing(e) {
var height = e.clientY - target.offsetTop
var width = e.clientX - target.offsetLeft
if (type === 'both' || type === 'horizontal')
target.style.height = (e.clientY - target.offsetTop) + 'px';
target.style.height = height + 'px';
if (type === 'both' || type === 'vertical')
target.style.width = (e.clientX - target.offsetLeft) + 'px';
target.style.width = width + 'px';
// prevent "user-select" highlights
document.body.classList.add('no-user-select');
// prevent event focus losing (eg. while hovering iframe, see #422)
document.body.classList.add('no-pointer-events');
}
function stopResizing(e) {
......@@ -36,10 +41,11 @@ function add(window, document, sourceQuery, targetQuery, type) {
window.removeEventListener('mouseup', stopResizing, false);
document.body.classList.remove('no-user-select');
document.body.classList.remove('no-pointer-events');
}
}
/**
* @name remove
* @function remove
* @param {Document} document
* @param {String} sourceQuery
*/
......
......@@ -17,6 +17,10 @@ $document-container-width: 780px
border-bottom: 1px solid $border-color
margin-bottom: $card-spacer-y
&__main-controls
display: flex
align-items: center
&__side-controls
display: flex
align-items: center
......
......@@ -142,8 +142,19 @@
$legend-code-height: 12px
&__item
display: flex
align-items: baseline
list-style: none
margin-bottom: space-x(0.75)
position: relative
&:not(:first-child)
margin-top: space-x(3)
&__title
color: $gray-800
font-size: 15px
font-weight: bold
margin-bottom: space-x(0.25)
&__code
width: $legend-code-width
......@@ -152,8 +163,32 @@
margin-right: space-x(2.5)
border: 1px solid $gray-500
&__caption
vertical-align: top
.graph-legend-nodes
&__item
display: inline-block
&:first-child
margin-top: space-x(0.75)
&:not(:last-child)
margin-bottom: space-x(0.5)
margin-right: space-x(0.5)
&--count
color: $gray-800
font-size: 12px
font-weight: bold
&__badge
font-size: 13px
white-space: normal
word-break: break-word
&--selected
background-color: darken($light, 10%) // from Bootstrap "_badge.scss"
.graph-documentation
......
......@@ -2,6 +2,9 @@
.no-user-select
user-select: none
.no-pointer-events
pointer-events: none
.side-panel
background-color: #fff
padding: 0.3em
......
......@@ -74,12 +74,18 @@
}
}
// Browser cursor consistency
.custom-select:disabled,
.form-control:disabled {
@include unclickable();
}
// Rectify Bootstrap readonly UI
.custom-select:disabled[readonly],
.form-control[readonly] {
background-color: $input-bg;
// (opinionated rules)
color: $input-placeholder-color;
// color: $input-placeholder-color;
border-style: dashed;
}
......
This diff is collapsed.
......@@ -312,7 +312,7 @@
transition: transform 0s, opacity 0s;
}
@each $name, $value in $theme-colors {
@each $name, $value in $palette-semantic {
&--#{ $name }:after {
background-image:
......
......@@ -23,20 +23,6 @@ $warning :#0DE2EA;
$light :#e8e8e8;
$dark :#000000;
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
'danger': $danger,
'info': $info,
'warning': $warning,
'light': $light,
'dark': $dark,
)
);
$gray-50 : #121212; // (+)
$gray-100: #212529;
$gray-150: #2E2E2E; // (+)
......@@ -50,6 +36,49 @@ $gray-700: #DEE2E6;
$gray-800: #E9ECEF;
$gray-900: #F8F9FA;
// Palettes
$theme-colors: map.merge(
$theme-colors,
(
'primary': $primary,
'secondary': $secondary,
'success': $success,
'danger': $danger,
'info': $info,
'warning': $warning,
'light': $light,
'dark': $dark,
)
);
$palette-semantic: $theme-colors;
$palette-gray: (
'0': $white,
'50': $gray-50,
'100': $gray-100,
'150': $gray-150,
'175': $gray-175,
'200': $gray-200,
'300': $gray-300,
'400': $gray-400,
'500': $gray-500,
'600': $gray-600,
'700': $gray-700,
'800': $gray-800,
'900': $gray-900,
'1000': $black
);
$palette-pastel: (
'green': $pastel-green,
'blue': $pastel-blue,
'yellow': $pastel-yellow,
'red': $pastel-red,
'orange': $pastel-orange,
'purple': $pastel-purple
);
// Components
$annotation-text-color: $gray-600;
$annotation-field-base-alpha: 15%;
......
......@@ -13,13 +13,41 @@
$primary: #005a9a;
$secondary: $blue;
// Palettes
$theme-colors: map.merge(
$theme-colors,
(
"primary": $primary,
"secondary": $secondary,
"primary": $primary,
"secondary": $secondary,
)
);
$palette-semantic: $theme-colors;
$palette-gray: (
'0': $white,
'50': $gray-50,
'100': $gray-100,
'150': $gray-150,
'175': $gray-175,
'200': $gray-200,
'300': $gray-300,
'400': $gray-400,
'500': $gray-500,
'600': $gray-600,
'700': $gray-700,
'800': $gray-800,
'900': $gray-900,
'1000': $black
);
$palette-pastel: (
'green': $pastel-green,
'blue': $pastel-blue,
'yellow': $pastel-yellow,
'red': $pastel-red,
'orange': $pastel-orange,
'purple': $pastel-purple
);
///==========================================
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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