diff --git a/graph/src/main/scala/GraphChanges.scala b/graph/src/main/scala/GraphChanges.scala index 39ced48de..547ad6125 100644 --- a/graph/src/main/scala/GraphChanges.scala +++ b/graph/src/main/scala/GraphChanges.scala @@ -11,11 +11,14 @@ case class GraphChanges( // we do not really need a connection for deleting (ConnectionId instead), but we want to revert it again. delEdges: collection.Set[Edge] = Set.empty ) { - def withAuthor(userId: UserId, timestamp: EpochMilli = EpochMilli.now): GraphChanges = + def withAuthor(userId: UserId, timestamp: EpochMilli = EpochMilli.now): GraphChanges = { + val existingAuthors: Set[NodeId] = addEdges.collect { case edge: Edge.Author => edge.nodeId }(breakOut) copy( - addEdges = addEdges ++ - addNodes.map(node => Edge.Author(node.id, EdgeData.Author(timestamp), userId)) + addEdges = addEdges ++ addNodes.flatMap { node => + (if (existingAuthors(node.id)) Set.empty[Edge] else Set[Edge](Edge.Author(node.id, EdgeData.Author(timestamp), userId))) + } ) + } def merge(other: GraphChanges): GraphChanges = { GraphChanges.from( diff --git a/sdk/shared/src/main/scala/EventProcessor.scala b/sdk/shared/src/main/scala/EventProcessor.scala index 73e7d5f2f..d696100c2 100644 --- a/sdk/shared/src/main/scala/EventProcessor.scala +++ b/sdk/shared/src/main/scala/EventProcessor.scala @@ -5,7 +5,7 @@ import monix.reactive.{Observable, OverflowStrategy} import monix.reactive.subjects.{PublishSubject, PublishToOneSubject} import wust.api.ApiEvent._ import wust.api._ -import wust.ids.NodeId +import wust.ids.{NodeId, UserId} import wust.graph._ import scala.concurrent.Future @@ -36,7 +36,7 @@ object EventProcessor { //TODO factory and constructor shared responsibility def apply( eventStream: Observable[Seq[ApiEvent]], - enrichChanges: (GraphChanges, Graph) => GraphChanges, + enrichChanges: (GraphChanges, UserId, Graph) => GraphChanges, sendChange: List[GraphChanges] => Future[Boolean], initialAuth: Authentication )(implicit scheduler: Scheduler): EventProcessor = { @@ -62,7 +62,7 @@ object EventProcessor { class EventProcessor private ( eventStream: Observable[Seq[ApiEvent.GraphContent]], authEventStream: Observable[Seq[ApiEvent.AuthContent]], - enrichChanges: (GraphChanges, Graph) => GraphChanges, + enrichChanges: (GraphChanges, UserId, Graph) => GraphChanges, sendChange: List[GraphChanges] => Future[Boolean], val initialAuth: Authentication )(implicit scheduler: Scheduler) { @@ -92,8 +92,8 @@ class EventProcessor private ( val sharedRawGraph = rawGraph.share val rawGraphWithInit = sharedRawGraph.startWith(Seq(Graph.empty)) - val enrichedChanges = changes.withLatestFrom(rawGraphWithInit) { (changes, graph) => - val newChanges = enrichChanges(changes, graph) + val enrichedChanges = changes.withLatestFrom2(currentUser, rawGraphWithInit) { (changes, user, graph) => + val newChanges = enrichChanges(changes, user.id, graph) scribe.info(s"Local Graphchanges: ${newChanges.toPrettyString(graph)}") newChanges } diff --git a/webApp/src/main/scala/state/GlobalStateFactory.scala b/webApp/src/main/scala/state/GlobalStateFactory.scala index 751fc9a9d..b6a9840ec 100644 --- a/webApp/src/main/scala/state/GlobalStateFactory.scala +++ b/webApp/src/main/scala/state/GlobalStateFactory.scala @@ -37,7 +37,7 @@ object GlobalStateFactory { val eventProcessor = EventProcessor( Client.observable.event, - (changes, graph) => GraphChangesAutomation.enrich(graph, urlConfig, EmojiReplacer.replaceChangesToColons(changes)).consistent, + (changes, userId, graph) => GraphChangesAutomation.enrich(userId, graph, urlConfig, EmojiReplacer.replaceChangesToColons(changes)).consistent, Client.api.changeGraph, Client.currentAuth ) diff --git a/webApp/src/main/scala/state/GraphChangesAutomation.scala b/webApp/src/main/scala/state/GraphChangesAutomation.scala index 267c187f4..3dd9a3f29 100644 --- a/webApp/src/main/scala/state/GraphChangesAutomation.scala +++ b/webApp/src/main/scala/state/GraphChangesAutomation.scala @@ -23,7 +23,7 @@ object GraphChangesAutomation { // copy the whole subgraph of the templateNode and append it to newNode. // templateNode is a placeholder and we want make changes such newNode looks like a copy of templateNode. - def copySubGraphOfNode(graph: Graph, newNode: Node, templateNode: Node, newId: NodeId => NodeId = _ => NodeId.fresh, copyTime: EpochMilli = EpochMilli.now): GraphChanges = { + def copySubGraphOfNode(userId: UserId, graph: Graph, newNode: Node, templateNode: Node, newId: NodeId => NodeId = _ => NodeId.fresh, copyTime: EpochMilli = EpochMilli.now): GraphChanges = { scribe.info(s"Copying sub graph of node $newNode with template $templateNode") val templateNodeIdx = graph.idToIdxOrThrow(templateNode.id) @@ -100,10 +100,15 @@ object GraphChangesAutomation { // Go through all edges and create new edges pointing to the replacedNodes, so // that we copy the edge structure that the template node had. graph.edges.foreach { - case _: Edge.Author => () // do not copy authors, we want the new authors of the one who triggered this change. case _: Edge.DerivedFromTemplate => () // do not copy derived info, we get new derive infos for new nodes case edge: Edge.Automated if edge.templateNodeId == templateNode.id => () // do not copy automation edges of template, otherwise the newNode would become a template. case edge: Edge.Child if edge.data.deletedAt.exists(EpochMilli.now.isAfter) => () // do not copy deleted parent edges + case edge: Edge.Author => // need to keep date of authorship, but change author. We will have an author edge for every change that was done to this node + // replace node ids to point to our copied nodes + replacedNodes.get(edge.nodeId) match { + case Some(newSource) => addEdges += edge.copy(nodeId = newSource.id, userId = userId) + case None => () + } case edge => // replace node ids to point to our copied nodes (replacedNodes.get(edge.sourceId), replacedNodes.get(edge.targetId)) match { @@ -121,7 +126,7 @@ object GraphChangesAutomation { // We get the current graph + the new graph change. For each new parent edge in the graph change, // we check if the parent has a template node. If the parent has a template node, we want to // append the subgraph (which is spanned from the template node) to the newly inserted child of the parent. - def enrich(graph: Graph, viewConfig: Var[UrlConfig], changes: GraphChanges): GraphChanges = { + def enrich(userId: UserId, graph: Graph, viewConfig: Var[UrlConfig], changes: GraphChanges): GraphChanges = { scribe.info("Check for automation enrichment of graphchanges: " + changes.toPrettyString(graph)) val addNodes = mutable.HashSet.newBuilder[Node] @@ -153,7 +158,7 @@ object GraphChangesAutomation { val templateNode = graph.nodes(templateNodeIdx) if (templateNode.role == childNode.role) { scribe.info(s"Found fitting template '$templateNode' for '$childNode'") - val changes = copySubGraphOfNode(graph, newNode = childNode, templateNode = templateNode) + val changes = copySubGraphOfNode(userId, graph, newNode = childNode, templateNode = templateNode) // if the automated changes re-add the same child edge were are currently replacing, then we want to take the ordering from the new child edge. // so an automated node can be drag/dropped to the correct position. addEdges ++= changes.addEdges.map { diff --git a/webApp/src/main/scala/views/TableView.scala b/webApp/src/main/scala/views/TableView.scala index afacf24ba..19fdbbddb 100644 --- a/webApp/src/main/scala/views/TableView.scala +++ b/webApp/src/main/scala/views/TableView.scala @@ -190,7 +190,7 @@ object TableView { // now we add these changes with the template node to a temporary graph, because ChangesAutomation needs the template node in the graph val tmpGraph = state.rawGraph.now applyChanges changes // run automation of this template for each row - propertyGroup.infos.foldLeft[GraphChanges](changes)((changes, info) => changes merge GraphChangesAutomation.copySubGraphOfNode(tmpGraph, info.node, templateNode = templateNode)) + propertyGroup.infos.foldLeft[GraphChanges](changes)((changes, info) => changes merge GraphChangesAutomation.copySubGraphOfNode(state.user.now.id, tmpGraph, info.node, templateNode = templateNode)) } else propertyGroup.infos.foldLeft[GraphChanges](GraphChanges.empty)((changes, info) => changes merge changesf(info.node.id)) }, keepPropertyAsDefault), dropdownModifier = cls := "top right", diff --git a/webApp/src/test/scala/GraphChangesAutomationSpec.scala b/webApp/src/test/scala/GraphChangesAutomationSpec.scala index cb2457dbd..1f309f93e 100644 --- a/webApp/src/test/scala/GraphChangesAutomationSpec.scala +++ b/webApp/src/test/scala/GraphChangesAutomationSpec.scala @@ -22,7 +22,7 @@ class GraphChangesAutomationSpec extends FreeSpec with MustMatchers { val copyTime = EpochMilli.now def copySubGraphOfNode(graph: Graph, newNode: Node, templateNode: Node) = GraphChangesAutomation.copySubGraphOfNode( - graph, newNode, templateNode, copyNodeId(_), copyTime + freshNodeId(), graph, newNode, templateNode, copyNodeId(_), copyTime ) "empty template node" in {