Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Drag/Drop: Include information about visible parents #87

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions graph/src/main/scala/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -820,18 +820,14 @@ final class GraphLookup(
def ancestorsIdx(nodeIdx: Int) = dfs.toArray(_(nodeIdx), dfs.afterStart, parentsIdx)
def ancestors(nodeId: NodeId) = idToIdxFold(nodeId)(Seq.empty[NodeId])(nodeIdx => ancestorsIdx(nodeIdx).viewMap(nodeIds))

def anyAncestorIsPinned(nodeIds: Iterable[NodeId], userId: NodeId): Boolean = idToIdxFold(userId)(false) { userIdx =>
def starts(f: Int => Unit): Unit = nodeIds.foreach { nodeId =>
idToIdxForeach(nodeId)(f)
}

val isPinnedSet = {
val set = ArraySet.create(n)
pinnedNodeIdx.foreachElement(userIdx)(set.add)
set
}
def anyAncestorOrSelfIsPinned(nodeIdxs: Iterable[Int], userId: NodeId): Boolean = idToIdxFold(userId)(false) { userIdx =>
pinnedNodeIdx.exists(userIdx)(pinnedIdx => nodeIdxs.exists(_ == pinnedIdx)) || anyAncestorIsPinned(nodeIdxs, userIdx)
}

dfs.exists(starts, dfs.withStart, parentsIdx, isFound = isPinnedSet.contains)
def anyAncestorIsPinned(nodeIdxs: Iterable[Int], userId: NodeId): Boolean = idToIdxFold(userId)(false)(anyAncestorIsPinned(nodeIdxs, _))
def anyAncestorIsPinned(nodeIdxs: Iterable[Int], userIdx: Int): Boolean = {
val isPinnedSet = pinnedNodeIdx.toArraySet(userIdx)
dfs.exists(nodeIdxs.foreach, dfs.afterStart, parentsIdx, isFound = isPinnedSet.contains)
}

// IMPORTANT:
Expand Down
15 changes: 7 additions & 8 deletions graph/src/main/scala/GraphChanges.scala
Original file line number Diff line number Diff line change
Expand Up @@ -286,19 +286,18 @@ object GraphChanges {
@inline def moveInto(nodeIds: Iterable[ChildId], newParentIds: Iterable[ParentId], graph:Graph): GraphChanges = {
GraphChanges.moveInto(graph, nodeIds, newParentIds)
}
def movePinnedChannel(channelId: ChildId, targetChannelId: Option[ParentId], graph: Graph, userId: UserId): GraphChanges = graph.idToIdxFold(channelId)(GraphChanges.empty) { channelIdx =>
val directParentsInChannelTree = graph.parentsIdx(channelIdx).collect {
case parentIdx if graph.anyAncestorIsPinned(graph.nodeIds(parentIdx) :: Nil, userId) => ParentId(graph.nodeIds(parentIdx))
}

val disconnect: GraphChanges = GraphChanges.disconnect(Edge.Child)(directParentsInChannelTree, channelId)
val connect: GraphChanges = targetChannelId.fold(GraphChanges.empty) {
targetChannelId => GraphChanges.connect(Edge.Child)(targetChannelId, channelId)
def movePinnedChannel(channelId: ChildId, sourceChannelId: Option[ParentId], targetChannelId: Option[ParentId], graph: Graph, userId: UserId): GraphChanges = graph.idToIdxFold(channelId)(GraphChanges.empty) { channelIdx =>
// target == None means that channel becomes top-level

val disconnect: GraphChanges = GraphChanges.disconnect(Edge.Child)(sourceChannelId, channelId)
val connect: GraphChanges = targetChannelId.fold(GraphChanges.empty) { targetChannelId =>
GraphChanges.connect(Edge.Child)(targetChannelId, channelId)
}
disconnect merge connect
}

@inline def linkOrCopyInto(edge: Edge.LabeledProperty, nodeId: NodeId, graph:Graph): GraphChanges = {
def linkOrCopyInto(edge: Edge.LabeledProperty, nodeId: NodeId, graph:Graph): GraphChanges = {
graph.nodesById(edge.propertyId) match {
case Some(node: Node.Content) if node.role == NodeRole.Neutral =>
val copyNode = node.copy(id = NodeId.fresh)
Expand Down
6 changes: 3 additions & 3 deletions webApp/src/main/scala/dragdrop/DragActions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ object DragActions {
case (payload: SelectedNodes, target: Workspace, ctrl, false) => (graph, userId) => linkOrMoveInto(payload.nodeIds.map(ChildId(_)), ParentId(target.nodeId), graph, ctrl)
case (payload: SelectedNodes, target: Channel, ctrl, false) => (graph, userId) => linkOrMoveInto(payload.nodeIds.map(ChildId(_)), ParentId(target.nodeId), graph, ctrl)

case (payload: Channel, target: Channel, false, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), Some(ParentId(target.nodeId)), graph, userId)
case (payload: Channel, target: Channel, false, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), payload.parentId.map(ParentId(_)), Some(ParentId(target.nodeId)), graph, userId)
case (payload: Channel, target: Channel, true, false) => (graph, userId) => linkOrMoveInto(ChildId(payload.nodeId), Some(ParentId(target.nodeId)), graph, true)
case (payload: Channel, target: Sidebar.type, false, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), None, graph, userId)
case (payload: Channel, target: ContentNode, ctrl, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), Some(ParentId(target.nodeId)), graph, userId)
case (payload: Channel, target: Sidebar.type, false, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), payload.parentId.map(ParentId(_)), None, graph, userId)
case (payload: Channel, target: ContentNode, ctrl, false) => (graph, userId) => movePinnedChannel(ChildId(payload.nodeId), payload.parentId.map(ParentId(_)), Some(ParentId(target.nodeId)), graph, userId)

case (payload: Property, target: ContentNode, false, false) => (graph, userId) => linkOrCopyInto(payload.edge, target.nodeId, graph)

Expand Down
32 changes: 16 additions & 16 deletions webApp/src/main/scala/dragdrop/DragItem.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@ sealed trait DragPayloadAndTarget extends DragPayload with DragTarget
object DragItem {
case object DisableDrag extends DragPayloadAndTarget

sealed trait ContentNode extends DragPayloadAndTarget { def nodeId: NodeId }
case class Message(nodeId: NodeId) extends ContentNode { override def toString = s"Message(${nodeId.shortHumanReadable})" }
case class Task(nodeId: NodeId) extends ContentNode { override def toString = s"Task(${nodeId.shortHumanReadable})" }
case class Note(nodeId: NodeId) extends ContentNode { override def toString = s"Note(${nodeId.shortHumanReadable})" }
case class Project(nodeId: NodeId) extends ContentNode { override def toString = s"Project(${nodeId.shortHumanReadable})" }
sealed trait ContentNode extends DragPayloadAndTarget { def nodeId: NodeId; def parentId: Option[NodeId] }
case class Message(nodeId: NodeId, parentId: Option[NodeId]) extends ContentNode { override def toString = s"Message(${nodeId.shortHumanReadable}, parent = ${parentId.map(_.shortHumanReadable)})" }
case class Task(nodeId: NodeId, parentId: Option[NodeId]) extends ContentNode { override def toString = s"Task(${nodeId.shortHumanReadable}, parent = ${parentId.map(_.shortHumanReadable)})" }
case class Note(nodeId: NodeId, parentId: Option[NodeId]) extends ContentNode { override def toString = s"Note(${nodeId.shortHumanReadable}, parent = ${parentId.map(_.shortHumanReadable)})" }
case class Project(nodeId: NodeId, parentId: Option[NodeId]) extends ContentNode { override def toString = s"Project(${nodeId.shortHumanReadable}, parent = ${parentId.map(_.shortHumanReadable)})" }

sealed trait ContentNodeConnect extends ContentNode {
def propertyName: String
}
case class TaskConnect(nodeId: NodeId, propertyName: String) extends ContentNodeConnect { override def toString = s"TaskConnect(${nodeId.shortHumanReadable}, $propertyName)" }
case class Tag(nodeId: NodeId) extends DragPayloadAndTarget { override def toString = s"Tag(${nodeId.shortHumanReadable})" }
case class TaskConnect(nodeId: NodeId, parentId: Option[NodeId], propertyName: String) extends ContentNodeConnect { override def toString = s"TaskConnect(${nodeId.shortHumanReadable}, parent = ${parentId.map(_.shortHumanReadable)}, $propertyName)" }
case class Tag(nodeId: NodeId, parentId: Option[NodeId]) extends DragPayloadAndTarget { override def toString = s"Tag(${nodeId.shortHumanReadable})" }
case class Property(edge: Edge.LabeledProperty) extends DragPayloadAndTarget { override def toString = s"Property($edge)" }

case class Thread(nodeIds: Seq[NodeId]) extends DragTarget { override def toString = s"Thread(${nodeIds.map(_.shortHumanReadable).mkString(",")})" }
case class Stage(nodeId: NodeId) extends DragPayloadAndTarget { override def toString = s"Stage(${nodeId.shortHumanReadable})" }
case class Stage(nodeId: NodeId, parentId: Option[NodeId]) extends DragPayloadAndTarget { override def toString = s"Stage(${nodeId.shortHumanReadable})" }

case object Sidebar extends DragTarget
case class Channel(nodeId: NodeId) extends DragPayloadAndTarget { override def toString = s"Channel(${nodeId.shortHumanReadable})" }
case class Channel(nodeId: NodeId, parentId: Option[NodeId]) extends DragPayloadAndTarget { override def toString = s"Channel(${nodeId.shortHumanReadable}, parentId: ${parentId.map(_.shortHumanReadable)})" }
case class BreadCrumb(nodeId: NodeId) extends DragPayloadAndTarget { override def toString = s"BreadCrumb(${nodeId.shortHumanReadable})" }
case class Workspace(nodeId: NodeId) extends DragTarget { override def toString = s"Workspace(${nodeId.shortHumanReadable})" }
case class TagBar(nodeId: NodeId) extends DragTarget { override def toString = s"TagBar(${nodeId.shortHumanReadable})" }
Expand All @@ -36,13 +36,13 @@ object DragItem {
case class SelectedNode(nodeId: NodeId) extends DragPayload { override def toString = s"SelectedNode(${nodeId.shortHumanReadable})" }
case class SelectedNodes(nodeIds: Seq[NodeId]) extends DragPayload { override def toString = s"SelectedNodes(${nodeIds.map(_.shortHumanReadable).mkString(",")})" }

def fromNodeRole(nodeId: NodeId, role: NodeRole): Option[DragPayloadAndTarget] = Some(role) collect {
case NodeRole.Message => DragItem.Message(nodeId)
case NodeRole.Task => DragItem.Task(nodeId)
case NodeRole.Note => DragItem.Note(nodeId)
case NodeRole.Project => DragItem.Project(nodeId)
case NodeRole.Tag => DragItem.Tag(nodeId)
case NodeRole.Stage => DragItem.Stage(nodeId)
def fromNodeRole(nodeId: NodeId, parentId: Option[NodeId], role: NodeRole): Option[DragPayloadAndTarget] = Some(role) collect {
case NodeRole.Message => DragItem.Message(nodeId, parentId)
case NodeRole.Task => DragItem.Task(nodeId, parentId)
case NodeRole.Note => DragItem.Note(nodeId, parentId)
case NodeRole.Project => DragItem.Project(nodeId, parentId)
case NodeRole.Tag => DragItem.Tag(nodeId, parentId)
case NodeRole.Stage => DragItem.Stage(nodeId, parentId)
}

val payloadPropName = "_wust_dragpayload"
Expand Down
6 changes: 3 additions & 3 deletions webApp/src/main/scala/state/FocusState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import wust.ids.{NodeId, View}
// when travsering a tree in the dom, we always have a current parent and a chain of ancestors. Needed to check cycles or operate on the parents in the views.
case class TraverseState(
parentId: NodeId,
parentIdChain: List[NodeId] = Nil
tail: List[NodeId] = Nil
) {
def contains(nodeId: NodeId): Boolean = parentId == nodeId || parentIdChain.contains(nodeId)
def step(nodeId: NodeId): TraverseState = TraverseState(nodeId, parentId :: parentIdChain)
def contains(nodeId: NodeId): Boolean = parentId == nodeId || tail.contains(nodeId)
def step(nodeId: NodeId): TraverseState = TraverseState(nodeId, parentId :: tail)
}

// a class for representing a preference to focus something in a certain view. e.g. used for configuring the right sidebar
Expand Down
2 changes: 1 addition & 1 deletion webApp/src/main/scala/state/GlobalStateFactory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ object GlobalStateFactory {
page.parentId.fold(graph) { parentId =>
val userIdx = graph.idToIdx(user.id)
graph.idToIdxFold(parentId)(graph) { pageIdx =>
def anyPageParentIsPinned = graph.anyAncestorIsPinned(List(parentId), user.id)
def anyPageParentIsPinned = graph.anyAncestorOrSelfIsPinned(Array(pageIdx), user.id)
def pageIsInvited = userIdx.fold(false)(userIdx => graph.inviteNodeIdx.contains(userIdx)(pageIdx))
def userIsMemberOfPage: Boolean = userIdx.fold(false)(userIdx => graph.membershipEdgeForNodeIdx.exists(pageIdx)(edgeIdx => graph.edgesIdx.b(edgeIdx) == userIdx))

Expand Down
2 changes: 1 addition & 1 deletion webApp/src/main/scala/views/BreadCrumbs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ object BreadCrumbs {
Components.nodeCardAsOneLineText(node, projectWithIcon = true).apply(
cls := "breadcrumb",
VDomModifier.ifTrue(graph.isDeletedNowInAllParents(nid))(cls := "node-deleted"),
DragItem.fromNodeRole(node.id, node.role).map(drag(_)),
DragItem.fromNodeRole(node.id, Some(parentId), node.role).map(drag(_)),
onClickFocus,
)

Expand Down
2 changes: 1 addition & 1 deletion webApp/src/main/scala/views/ChatView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ object ChatView {
else VDomModifier.empty
} else VDomModifier.empty

val renderedMessage = renderMessage(state, nodeId, directParentIds, isDeletedNow = isDeletedNow, renderedMessageModifier = messageDragOptions(state, nodeId, selectedNodes))
val renderedMessage = renderMessage(state, nodeId, directParentIds, isDeletedNow = isDeletedNow, renderedMessageModifier = messageDragOptions(state, nodeId, directParentIds.headOption, selectedNodes))
val controls = msgControls(state, nodeId, directParentIds.asInstanceOf[Iterable[ParentId]], selectedNodes, isDeletedNow = isDeletedNow, replyAction = replyAction)
val checkbox = msgCheckbox(state, nodeId, selectedNodes, newSelectedNode = SelectedNode(_)(directParentIds), isSelected = isSelected)
val selectByClickingOnRow = {
Expand Down
22 changes: 12 additions & 10 deletions webApp/src/main/scala/views/Components.scala
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,10 @@ object Components {
def checkboxNodeTag(
state: GlobalState,
tagNode: Node,
parentId: Option[NodeId],
tagModifier: VDomModifier = VDomModifier.empty,
pageOnClick: Boolean = false,
dragOptions: NodeId => VDomModifier = nodeId => drag(DragItem.Tag(nodeId), target = DragItem.DisableDrag),
dragOptions: (NodeId, Option[NodeId]) => VDomModifier = (nodeId, parentId) => drag(DragItem.Tag(nodeId, parentId), target = DragItem.DisableDrag),
withAutomation: Boolean = false,
)(implicit ctx: Ctx.Owner): VNode = {

Expand All @@ -481,7 +482,7 @@ object Components {
),
label(), // needed for fomanticui
),
nodeTag(state, tagNode, pageOnClick, dragOptions).apply(tagModifier),
nodeTag(state, tagNode, parentId, pageOnClick, dragOptions).apply(tagModifier),
VDomModifier.ifTrue(withAutomation)(GraphChangesAutomationUI.settingsButton(state, tagNode.id, activeMod = visibility.visible).apply(cls := "singleButtonWithBg", marginLeft.auto)),
)
}
Expand Down Expand Up @@ -509,19 +510,20 @@ object Components {
def nodeTag(
state: GlobalState,
tag: Node,
parentId: Option[NodeId],
pageOnClick: Boolean = false,
dragOptions: NodeId => VDomModifier = nodeId => drag(DragItem.Tag(nodeId), target = DragItem.DisableDrag),
dragOptions: (NodeId, Option[NodeId]) => VDomModifier = (nodeId, parentId) => drag(DragItem.Tag(nodeId, parentId), target = DragItem.DisableDrag),
): VNode = {
val contentString = renderAsOneLineText(tag)
renderNodeTag(state, tag, VDomModifier(contentString, dragOptions(tag.id)), pageOnClick)
renderNodeTag(state, tag, VDomModifier(contentString, dragOptions(tag.id, parentId)), pageOnClick)
}

def removableNodeTagCustom(state: GlobalState, tag: Node, action: () => Unit, pageOnClick:Boolean = false): VNode = {
nodeTag(state, tag, pageOnClick)(removableTagMod(action))
def removableNodeTagCustom(state: GlobalState, tag: Node, parentId: Option[NodeId], action: () => Unit, pageOnClick:Boolean = false): VNode = {
nodeTag(state, tag, parentId, pageOnClick)(removableTagMod(action))
}

def removableNodeTag(state: GlobalState, tag: Node, taggedNodeId: NodeId, pageOnClick:Boolean = false): VNode = {
removableNodeTagCustom(state, tag, () => {
def removableNodeTag(state: GlobalState, tag: Node, parentId: Option[NodeId], taggedNodeId: NodeId, pageOnClick:Boolean = false): VNode = {
removableNodeTagCustom(state, tag, parentId, () => {
state.eventProcessor.changes.onNext(
GraphChanges.disconnect(Edge.Child)(Array(ParentId(tag.id)), ChildId(taggedNodeId))
)
Expand Down Expand Up @@ -1076,7 +1078,7 @@ object Components {
)
}

def automatedNodesOfNode(state: GlobalState, nodeId: NodeId)(implicit ctx: Ctx.Owner): VDomModifier = {
def automatedNodesOfNode(state: GlobalState, nodeId: NodeId, parentId: Option[NodeId])(implicit ctx: Ctx.Owner): VDomModifier = {
val automatedNodes: Rx[Seq[Node]] = Rx {
val graph = state.rawGraph()
graph.idToIdxFold(nodeId)(Seq.empty[Node])(graph.automatedNodes)
Expand All @@ -1089,7 +1091,7 @@ object Components {
div(
div(background := "repeating-linear-gradient(45deg, yellow, yellow 6px, black 6px, black 12px)", height := "3px"),
UI.tooltip("bottom center") := "This node is an active automation template")
Components.nodeTag(state, node, pageOnClick = false, dragOptions = _ => VDomModifier.empty).prepend(renderFontAwesomeIcon(Icons.automate).apply(marginLeft := "3px", marginRight := "3px")
Components.nodeTag(state, node, parentId, pageOnClick = false, dragOptions = (_, _) => VDomModifier.empty).prepend(renderFontAwesomeIcon(Icons.automate).apply(marginLeft := "3px", marginRight := "3px")
)
}
)
Expand Down
2 changes: 1 addition & 1 deletion webApp/src/main/scala/views/DashboardView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ object DashboardView {
marginLeft := "10px",
cls := "node channel-line",

drag(DragItem.Project(project.id)),
drag(DragItem.Project(project.id, Some(focusState.focusedId))),
renderProject(project, renderNode = node => renderAsOneLineText(node).apply(cls := "channel-name"), withIcon = true),

cursor.pointer,
Expand Down
2 changes: 1 addition & 1 deletion webApp/src/main/scala/views/GraphChangesAutomationUI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ object GraphChangesAutomationUI {
state.rawGraph.map(g => VDomModifier.ifNot(g.parentsContains(templateNode.id)(focusedId))(i(color.gray, " * Template is not a direct child of the current node." ))),
),

DragItem.fromNodeRole(templateNode.id, templateNode.role).map(dragItem => Components.drag(target = dragItem)),
DragItem.fromNodeRole(templateNode.id, Some(focusedId), templateNode.role).map(dragItem => Components.drag(target = dragItem)),
Components.sidebarNodeFocusMod(selectedTemplate, templateNode.id),
).prepend(
b(color.gray, templateNode.role.toString)
Expand Down
4 changes: 2 additions & 2 deletions webApp/src/main/scala/views/LeftSidebar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ object LeftSidebar {
onChannelClick(state, nodeId),
onClick foreach { Analytics.sendEvent("sidebar_open", "clickchannel") },
cls := "node",
drag(DragItem.Channel(nodeId)),
drag(DragItem.Channel(nodeId, traverseState.tail.headOption)),
channelModifier
)
)
Expand Down Expand Up @@ -389,7 +389,7 @@ object LeftSidebar {
UI.popup("right center") <-- node.map(_.str),
onChannelClick(state, nodeId),
onClick foreach { Analytics.sendEvent("sidebar_closed", "clickchannel") },
drag(target = DragItem.Channel(nodeId)),
drag(target = DragItem.Channel(nodeId, traverseState.tail.headOption)),
cls := "node",

// for each indent, steal padding on left and right
Expand Down
Loading