WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit b187bbb

Browse files
committed
Adding button for admins to take over a workspace they follow or is public
This was squashed from commits in #290
1 parent cca096c commit b187bbb

File tree

16 files changed

+240
-19
lines changed

16 files changed

+240
-19
lines changed

backend/app/controllers/api/Workspaces.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ object UpdateWorkspaceName {
4545
implicit val format = Json.format[UpdateWorkspaceName]
4646
}
4747

48+
case class UpdateWorkspaceOwner(owner: String)
49+
object UpdateWorkspaceOwner {
50+
implicit val format = Json.format[UpdateWorkspaceOwner]
51+
}
52+
4853
case class AddItemParameters(uri: Option[String], size: Option[Long], mimeType: Option[String])
4954
object AddItemParameters {
5055
implicit val format = Json.format[AddItemParameters]
@@ -199,6 +204,21 @@ class Workspaces(
199204
}
200205
}
201206

207+
def updateWorkspaceOwner(workspaceId: String) = ApiAction.attempt(parse.json) { req =>
208+
for {
209+
data <- req.body.validate[UpdateWorkspaceOwner].toAttempt
210+
211+
_ = logAction(req.user, workspaceId, s"Updating workspace owner. Data: $data")
212+
_ <- annotation.updateWorkspaceOwner(
213+
req.user.username,
214+
workspaceId,
215+
data.owner
216+
)
217+
} yield {
218+
NoContent
219+
}
220+
}
221+
202222
def deleteWorkspace(workspaceId: String) = ApiAction.attempt { req: UserIdentityRequest[_] =>
203223
logAction(req.user, workspaceId, s"Delete workspace. ID: $workspaceId")
204224

backend/app/model/annotations/Workspace.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ case class WorkspaceMetadata(id: String,
142142
name: String,
143143
isPublic: Boolean,
144144
tagColor: String,
145+
creator: PartialUser,
145146
owner: PartialUser,
146147
followers: List[PartialUser]
147148
)
@@ -150,12 +151,13 @@ object WorkspaceMetadata {
150151
implicit val write: Writes[WorkspaceMetadata] = Json.writes[WorkspaceMetadata]
151152
implicit val read: Reads[WorkspaceMetadata] = Json.reads[WorkspaceMetadata]
152153

153-
def fromNeo4jValue(v: Value, owner: DBUser, followers: List[DBUser]): WorkspaceMetadata = {
154+
def fromNeo4jValue(v: Value, creator: DBUser, owner:DBUser, followers: List[DBUser]): WorkspaceMetadata = {
154155
WorkspaceMetadata(
155156
v.get("id").asString(),
156157
v.get("name").asString(),
157158
v.get("isPublic").asBoolean(),
158159
v.get("tagColor").asString(),
160+
creator.toPartial,
159161
owner.toPartial,
160162
followers.map(_.toPartial)
161163
)
@@ -166,6 +168,7 @@ case class Workspace(id: String,
166168
name: String,
167169
isPublic: Boolean,
168170
tagColor: String,
171+
creator: PartialUser,
169172
owner: PartialUser,
170173
followers: List[PartialUser],
171174
rootNode: TreeEntry[WorkspaceEntry])
@@ -180,6 +183,7 @@ object Workspace {
180183
name = metadata.name,
181184
isPublic = metadata.isPublic,
182185
tagColor = metadata.tagColor,
186+
creator = metadata.creator,
183187
owner = metadata.owner,
184188
followers = metadata.followers,
185189
rootNode = rootNode

backend/app/services/annotations/Annotations.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ trait Annotations {
1717
def getWorkspaceContents(currentUser: String, id: String): Attempt[TreeEntry[WorkspaceEntry]]
1818
def insertWorkspace(username: String, id: String, name: String, isPublic: Boolean, tagColor: String): Attempt[Unit]
1919
def updateWorkspaceName(currentUser: String, id: String, name: String): Attempt[Unit]
20+
def updateWorkspaceOwner(currentUser: String, id: String, owner: String): Attempt[Unit]
2021
def updateWorkspaceIsPublic(currentUser: String, id: String, isPublic: Boolean): Attempt[Unit]
2122
def updateWorkspaceFollowers(currentUser: String, id: String, followers: List[String]): Attempt[Unit]
2223
def deleteWorkspace(currentUser: String, workspace: String): Attempt[Unit]

backend/app/services/annotations/Neo4jAnnotations.scala

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
4848
tx.run(
4949
"""
5050
|MATCH (workspace :Workspace)
51-
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:CREATED]->(workspace) OR workspace.isPublic
51+
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:OWNS]->(workspace) OR workspace.isPublic
5252
|MATCH (creator :User)-[:CREATED]->(workspace)<-[:FOLLOWING]-(follower :User)
53-
|RETURN workspace, creator, collect(distinct follower) as followers
53+
|MATCH (owner :User)-[:OWNS]->(workspace)
54+
|RETURN workspace, creator, owner, collect(distinct follower) as followers
5455
""".stripMargin,
5556
parameters(
5657
"currentUser", currentUser
@@ -59,9 +60,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
5960
summary.list().asScala.toList.map { r =>
6061
val workspace = r.get("workspace")
6162
val creator = DBUser.fromNeo4jValue(r.get("creator"))
63+
val owner = DBUser.fromNeo4jValue(r.get("owner"))
6264
val followers = r.get("followers").asList[DBUser](DBUser.fromNeo4jValue(_)).asScala.toList
6365

64-
WorkspaceMetadata.fromNeo4jValue(workspace, creator, followers)
66+
WorkspaceMetadata.fromNeo4jValue(workspace, creator, owner, followers)
6567
}
6668
}
6769
}
@@ -70,9 +72,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
7072
tx.run(
7173
"""
7274
|MATCH (workspace :Workspace {id: {id} })
73-
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:CREATED]->(workspace) OR workspace.isPublic
75+
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:OWNS]->(workspace) OR workspace.isPublic
7476
|MATCH (creator :User)-[:CREATED]->(workspace)<-[:FOLLOWING]-(follower :User)
75-
|RETURN workspace, creator, collect(distinct follower) as followers
77+
|MATCH (owner :User)-[:OWNS]->(workspace)
78+
|RETURN workspace, creator, owner, collect(distinct follower) as followers
7679
""".stripMargin,
7780
parameters(
7881
"currentUser", currentUser,
@@ -85,9 +88,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
8588
).map { r =>
8689
val workspace = r.head.get("workspace")
8790
val creator = DBUser.fromNeo4jValue(r.head.get("creator"))
91+
val owner = DBUser.fromNeo4jValue(r.head.get("owner"))
8892
val followers = r.head.get("followers").asList[DBUser](DBUser.fromNeo4jValue(_)).asScala.toList
8993

90-
WorkspaceMetadata.fromNeo4jValue(workspace, creator, followers)
94+
WorkspaceMetadata.fromNeo4jValue(workspace, creator, owner, followers)
9195
}
9296
}
9397
}
@@ -96,9 +100,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
96100
tx.run(
97101
"""
98102
|MATCH (workspace: Workspace { id: {id} })
99-
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:CREATED]->(workspace) OR workspace.isPublic
103+
|WHERE (:User { username: {currentUser} })-[:FOLLOWING|:OWNS]->(workspace) OR workspace.isPublic
100104
|
101105
|OPTIONAL MATCH (workspace)<-[:PART_OF]-(node :WorkspaceNode)<-[:CREATED]-(nodeCreator :User)
106+
|
102107
|OPTIONAL MATCH (node)-[:PARENT]->(parentNode :WorkspaceNode)
103108
|
104109
|OPTIONAL MATCH (:Resource {uri: node.uri})<-[todo:TODO|:PROCESSING_EXTERNALLY]-(:Extractor)
@@ -147,6 +152,7 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
147152
|CREATE (w: Workspace {id: {id}, name: {name}, isPublic: {isPublic}, tagColor: {tagColor}})
148153
|CREATE (w)<-[:CREATED]-(u)
149154
|CREATE (w)<-[:FOLLOWING]-(u)
155+
|CREATE (w)<-[:OWNS]-(u)
150156
|
151157
|CREATE (u)-[:CREATED]->(f: WorkspaceNode {id: {rootFolderId}, name: {name}, type: 'folder'})-[:PART_OF]->(w)
152158
|
@@ -170,10 +176,10 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
170176
override def updateWorkspaceFollowers(currentUser: String, id: String, followers: List[String]): Attempt[Unit] = attemptTransaction { tx =>
171177
tx.run(
172178
"""
173-
|MATCH (workspace :Workspace {id: {workspaceId}})<-[:CREATED]-(creator :User {username: {username}})
179+
|MATCH (workspace :Workspace {id: {workspaceId}})<-[:OWNS]-(owner :User {username: {username}})
174180
|
175181
|OPTIONAL MATCH (existingFollower :User)-[existingFollow :FOLLOWING]->(workspace)
176-
| WHERE existingFollower.username <> creator.username
182+
| WHERE existingFollower.username <> owner.username
177183
|
178184
|OPTIONAL MATCH (newFollower :User)
179185
| WHERE newFollower.username IN {followers}
@@ -201,7 +207,7 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
201207
override def updateWorkspaceIsPublic(currentUser: String, id: String, isPublic: Boolean): Attempt[Unit] = attemptTransaction { tx =>
202208
tx.run(
203209
"""
204-
|MATCH (workspace :Workspace {id: {workspaceId}})<-[:CREATED]-(creator :User {username: {username}})
210+
|MATCH (workspace :Workspace {id: {workspaceId}})<-[:OWNS]-(owner :User {username: {username}})
205211
|
206212
|SET workspace.isPublic = {isPublic}
207213
|
@@ -222,7 +228,7 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
222228
override def updateWorkspaceName(currentUser: String, id: String, name: String): Attempt[Unit] = attemptTransaction { tx =>
223229
tx.run(
224230
"""
225-
|MATCH (rootNode :WorkspaceNode)-[:PART_OF]->(workspace :Workspace {id: {workspaceId}})<-[:CREATED]-(creator :User {username: {username}})
231+
|MATCH (rootNode :WorkspaceNode)-[:PART_OF]->(workspace :Workspace {id: {workspaceId}})<-[:OWNS]-(owner :User {username: {username}})
226232
| WHERE NOT exists((rootNode)-[:PARENT]->(:WorkspaceNode))
227233
|
228234
|SET workspace.name = {name}
@@ -241,11 +247,36 @@ class Neo4jAnnotations(driver: Driver, executionContext: ExecutionContext, query
241247
}
242248
}
243249

250+
override def updateWorkspaceOwner(currentUser: String, id: String, owner: String): Attempt[Unit] = attemptTransaction { tx =>
251+
val query = """
252+
MATCH (user: User { username: {username}})
253+
|MATCH (newOwner: User { username: {owner}})
254+
|MATCH (workspace: Workspace {id:{workspaceId}} )<-[ownsRelationship:OWNS]-(currentOwner:User)
255+
|WHERE (:Permission {name: "CanPerformAdminOperations"})<-[:HAS_PERMISSION]-(user)
256+
|CREATE (workspace)<-[:FOLLOWING]-(newOwner)
257+
|CREATE (workspace)<-[:OWNS]-(newOwner)
258+
|DELETE ownsRelationship
259+
""".stripMargin
260+
tx.run(
261+
query,
262+
parameters(
263+
"workspaceId", id,
264+
"owner", owner,
265+
"username", currentUser
266+
)
267+
).flatMap {
268+
case r if r.summary().counters().relationshipsCreated() != 2 =>
269+
Attempt.Left(IllegalStateFailure(s"Error when updating workspace owner, unexpected properties set ${r.summary().counters().propertiesSet()}"))
270+
case _ =>
271+
Attempt.Right(())
272+
}
273+
}
274+
244275
override def deleteWorkspace(currentUser: String, workspace: String): Attempt[Unit] = attemptTransaction { tx =>
245276
tx.run(
246277
"""
247278
|MATCH (user: User { username: {username} })
248-
|MATCH (workspace: Workspace {id: {workspaceId}})<-[:CREATED]-(u:User)
279+
|MATCH (workspace: Workspace {id: {workspaceId}})<-[:OWNS]-(u:User)
249280
|WHERE u.username = {username} OR (workspace.isPublic and (:Permission {name: "CanPerformAdminOperations"})<-[:HAS_PERMISSION]-(user))
250281
|MATCH (workspace)<-[:PART_OF]-(node: WorkspaceNode)
251282
|

backend/app/services/manifest/Neo4jManifest.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class Neo4jManifest(driver: Driver, executionContext: ExecutionContext, queryLog
100100
"""
101101
|MATCH (w:WorkspaceNode {uri: {uri}})-[:PART_OF]->(workspace:Workspace)
102102
|MATCH (creator :User)-[:CREATED]->(workspace)<-[:FOLLOWING]-(follower :User)
103+
|MATCH (owner :User)-[:OWNS]->(workspace)
103104
|return workspace, creator, collect(distinct follower) as followers
104105
|""".stripMargin,
105106
parameters(
@@ -108,9 +109,10 @@ class Neo4jManifest(driver: Driver, executionContext: ExecutionContext, queryLog
108109
summary.list().asScala.toList.map { r =>
109110
val workspace = r.get("workspace")
110111
val creator = DBUser.fromNeo4jValue(r.get("creator"))
112+
val owner = DBUser.fromNeo4jValue(r.get("owner"))
111113
val followers = r.get("followers").asList[DBUser](DBUser.fromNeo4jValue(_)).asScala.toList
112114

113-
WorkspaceMetadata.fromNeo4jValue(workspace, creator, followers)
115+
WorkspaceMetadata.fromNeo4jValue(workspace, creator, owner, followers)
114116
}
115117
}
116118
}

backend/conf/routes

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ PUT /api/workspaces/:workspaceId/followers cont
6161
PUT /api/workspaces/:workspaceId/isPublic controllers.api.Workspaces.updateWorkspaceIsPublic(workspaceId: String)
6262
PUT /api/workspaces/:workspaceId/name controllers.api.Workspaces.updateWorkspaceName(workspaceId: String)
6363
GET /api/workspaces/:workspaceId controllers.api.Workspaces.get(workspaceId: String)
64-
GET /api/workspaces/:workspaceId/nodes controllers.api.Workspaces.getContents(workspaceId: String)
64+
GET /api/workspaces/:workspaceId/nodes controllers.api.Workspaces.getContents(workspaceId: String)
65+
PUT /api/workspaces/:workspaceId/owner controllers.api.Workspaces.updateWorkspaceOwner(workspaceId: String)
6566
POST /api/workspaces/:workspaceId/reprocess controllers.api.Workspaces.reprocess(workspaceId: String, rerunSuccessful: Option[Boolean], rerunFailed: Option[Boolean])
6667
POST /api/workspaces/:workspaceId/nodes controllers.api.Workspaces.addItemToWorkspace(workspaceId: String)
6768
POST /api/workspaces/:workspaceId/remoteUrl controllers.api.Workspaces.addRemoteUrlToWorkspace(workspaceId: String)

backend/test/test/TestAnnotations.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import utils.attempt.{Attempt, Failure, UnsupportedOperationFailure}
1010
class TestAnnotations(usersToWorkspaces: Map[String, List[String]] = Map.empty) extends Annotations {
1111
override def getAllWorkspacesMetadata(currentUser: String): Attempt[List[WorkspaceMetadata]] = {
1212
val workspaces = usersToWorkspaces.getOrElse(currentUser, List.empty).map { id =>
13-
WorkspaceMetadata(id, id, isPublic = false, "", null, List.empty)
13+
WorkspaceMetadata(id, id, isPublic = false, tagColor="", creator=null, owner=null, followers=List.empty)
1414
}
1515

1616
Attempt.Right(workspaces)
@@ -21,6 +21,7 @@ class TestAnnotations(usersToWorkspaces: Map[String, List[String]] = Map.empty)
2121
override def updateWorkspaceFollowers(currentUser: String, id: String, followers: List[String]): Attempt[Unit] = Attempt.Left(UnsupportedOperationFailure(""))
2222
override def updateWorkspaceIsPublic(currentUser: String, id: String, isPublic: Boolean): Attempt[Unit] = Attempt.Left(UnsupportedOperationFailure(""))
2323
override def updateWorkspaceName(currentUser: String, id: String, name: String): Attempt[Unit] = Attempt.Left(UnsupportedOperationFailure(""))
24+
override def updateWorkspaceOwner(currentUser: String, id: String, owner: String): Attempt[Unit] = Attempt.Left(UnsupportedOperationFailure(""))
2425
override def deleteWorkspace(currentUser: String, workspace: String): Attempt[Unit] = Attempt.Left(UnsupportedOperationFailure(""))
2526
override def addFolder(currentUser: String, workspaceId: String, parentFolderId: String, folderName: String): Attempt[String] = Attempt.Left(UnsupportedOperationFailure(""))
2627
override def addResourceToWorkspaceFolder(currentUser: String, fileName: String, uri: Uri, size: Option[Long], mimeType: Option[String], icon: String, workspaceId: String, folderId: String, nodeId: String): Attempt[String] = Attempt.Left(UnsupportedOperationFailure(""))

backend/test/test/integration/Helpers.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ object Helpers extends Matchers with Logging with OptionValues with Inside {
371371
.apply(FakeRequest().withBody(Json.toJson(UpdateWorkspaceName(name))))
372372
}
373373

374+
def setWorkspaceOwner(workspaceId: String, owner: String)(implicit timeout: Timeout, controllers: Controllers): Future[Result] = {
375+
controllers.workspace.updateWorkspaceOwner(workspaceId)
376+
.apply(FakeRequest().withBody(Json.toJson(UpdateWorkspaceOwner(owner))))
377+
}
378+
374379
def getFilters()(implicit controllers: Controllers, timeout: Timeout): List[Filter] = {
375380
contentAsJson(controllers.filters.getFilters().apply(FakeRequest())).as[List[Filter]]
376381
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { updateWorkspaceOwner } from '../../services/WorkspaceApi';
2+
import { getWorkspacesMetadata } from './getWorkspacesMetadata';
3+
import { ThunkAction } from 'redux-thunk';
4+
import { AppAction, AppActionType, WorkspacesAction } from '../../types/redux/GiantActions';
5+
import { GiantState } from '../../types/redux/GiantState';
6+
import { getWorkspace } from './getWorkspace';
7+
8+
export function takeOwnershipOfWorkspace(
9+
id: string,
10+
user: string,
11+
): ThunkAction<void, GiantState, null, WorkspacesAction | AppAction> {
12+
return dispatch => {
13+
return updateWorkspaceOwner(id, user)
14+
.then(res => {
15+
dispatch(getWorkspacesMetadata());
16+
// We need to fire off this second call because "created by"/"owned by" labels as
17+
// displayed in the "current workspace" view is accessed from the currentWorkspace state,
18+
// so we need to refresh it. It's also in the workspacesMetadata state, but we don't want to
19+
// search through that array to find the current workspace: currentWorkspace should always
20+
// be the canonical source of info for that.
21+
dispatch(getWorkspace(id));
22+
})
23+
.catch(error => dispatch(errorUpdatingWorkspaceMetadata(error)));
24+
};
25+
}
26+
27+
function errorUpdatingWorkspaceMetadata(error: Error): AppAction {
28+
return {
29+
type: AppActionType.APP_SHOW_ERROR,
30+
message: 'Failed to take ownership of workspace',
31+
error: error,
32+
};
33+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useState } from 'react';
2+
import Modal from '../UtilComponents/Modal';
3+
import MdSupervisorAccount from "react-icons/lib/md/supervisor-account";
4+
import { Workspace } from '../../types/Workspaces';
5+
import { PartialUser } from '../../types/User';
6+
import {takeOwnershipOfWorkspace} from "../../actions/workspaces/takeOwnershipOfWorkspace";
7+
8+
type Props = {
9+
workspace: Workspace,
10+
isAdmin: Boolean,
11+
currentUser: PartialUser,
12+
takeOwnershipOfWorkspace: typeof takeOwnershipOfWorkspace
13+
}
14+
15+
export default function TakeOwnershipOfWorkspaceModal(props: Props) {
16+
const [open, setOpen] = useState(false);
17+
18+
function onSubmit(e?: React.FormEvent) {
19+
if (e) {
20+
e.preventDefault();
21+
}
22+
23+
if (props.isAdmin) {
24+
props.takeOwnershipOfWorkspace(props.workspace.id, props.currentUser.username)
25+
}
26+
onDismiss();
27+
}
28+
29+
function onDismiss() {
30+
setOpen(false);
31+
}
32+
33+
if (props.currentUser.username === props.workspace.owner.username)
34+
return null
35+
if (!props.isAdmin)
36+
return null
37+
38+
return <React.Fragment>
39+
40+
{/* The component that triggers the modal (pass-through rendering of children) */}
41+
<button
42+
className='btn workspace__button'
43+
onClick={() => setOpen(true)}
44+
title='Take ownership'
45+
>
46+
<MdSupervisorAccount /> Take Ownership
47+
</button>
48+
49+
<Modal isOpen={open} dismiss={onDismiss} panelClassName="modal-action__panel">
50+
<form onSubmit={onSubmit}>
51+
<div className='modal-action__modal'>
52+
<h2>Take over workspace {props.workspace.name}</h2>
53+
<div className='modal-action__buttons'>
54+
<button
55+
className='btn'
56+
onClick={onSubmit}
57+
autoFocus={false}
58+
>
59+
Take Ownership
60+
</button>
61+
<button
62+
className='btn'
63+
onClick={onDismiss}
64+
>
65+
Cancel
66+
</button>
67+
</div>
68+
</div>
69+
</form>
70+
</Modal>
71+
</React.Fragment>;
72+
}

0 commit comments

Comments
 (0)