Skip to content

Commit 3a6fb2b

Browse files
li-xin-yilihebi
andauthored
feat(share): add share button (#81)
Co-authored-by: Hebi Li <lihebi.com@gmail.com>
1 parent 0f6d32c commit 3a6fb2b

File tree

9 files changed

+317
-39
lines changed

9 files changed

+317
-39
lines changed

api/prisma/schema.prisma

100644100755
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ model Repo {
5454
userId String
5555
pods Pod[] @relation("BELONG")
5656
podsId String[]
57+
public Boolean @default(false)
58+
collaboratorIds String[]
59+
5760
5861
@@unique([name, userId])
5962
}

api/src/resolver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
deletePod,
66
deleteRepo,
77
myRepos,
8+
myCollabRepos,
9+
addCollaborator,
810
pod,
911
repo,
1012
repos,
@@ -34,6 +36,7 @@ export const resolvers = {
3436
repo,
3537
pod,
3638
listAllRuntimes,
39+
myCollabRepos,
3740
},
3841
Mutation: {
3942
signup,
@@ -45,6 +48,7 @@ export const resolvers = {
4548
addPod,
4649
updatePod,
4750
deletePod,
51+
addCollaborator,
4852
spawnRuntime:
4953
process.env.RUNTIME_SPAWNER === "k8s"
5054
? spawnRuntime_k8s

api/src/resolver_repo.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const { PrismaClient } = Prisma;
33

44
const prisma = new PrismaClient();
55

6+
// console.log("resolver_repo.ts", Prisma.RepoInclude);
7+
68
async function ensurePodAccess({ id, userId }) {
79
let pod = await prisma.pod.findFirst({
810
where: { id },
@@ -30,7 +32,9 @@ async function ensurePodAccess({ id, userId }) {
3032
// is created on server, which is a time sequence bug
3133
throw new Error("Pod not exists.");
3234
}
33-
if (pod.repo.owner.id !== userId) {
35+
// public repo can be accessed by everyone
36+
// if the user is the owner or one of the collaborators, then it is ok
37+
if (!pod.repo.public && pod.repo.owner.id !== userId && pod.repo.collaboratorIds.indexOf(userId) === -1) {
3438
throw new Error("You do not have access to this pod.");
3539
}
3640
}
@@ -57,12 +61,27 @@ export async function myRepos(_, __, { userId }) {
5761
return repos;
5862
}
5963

60-
export async function repo(_, { id }, { userId }) {
64+
export async function myCollabRepos(_, __, {userId}) {
6165
if (!userId) throw Error("Unauthenticated");
62-
const repo = await prisma.repo.findFirst({
66+
// console.log("myCollabRepos", userId);
67+
const repos = await prisma.repo.findMany({
6368
where: {
64-
id,
65-
owner: { id: userId },
69+
public: false,
70+
collaboratorIds: {
71+
has:userId,
72+
},
73+
},
74+
});
75+
return repos;
76+
}
77+
78+
export async function repo(_, { id }, { userId }) {
79+
const repo = await prisma.repo.findFirst({
80+
where: { OR: [
81+
{ id, public: true },
82+
{ id, owner: { id: userId!} },
83+
{ id, collaboratorIds: { has: userId!} },
84+
]
6685
},
6786
include: {
6887
owner: true,
@@ -77,7 +96,6 @@ export async function repo(_, { id }, { userId }) {
7796
},
7897
},
7998
});
80-
// console.log("Returning repo", repo);
8199
return repo;
82100
}
83101

@@ -90,12 +108,13 @@ export async function pod(_, { id }) {
90108
});
91109
}
92110

93-
export async function createRepo(_, { id, name }, { userId }) {
111+
export async function createRepo(_, { id, name, isPublic}, { userId }) {
94112
if (!userId) throw Error("Unauthenticated");
95113
const repo = await prisma.repo.create({
96114
data: {
97115
id,
98116
name,
117+
public: isPublic,
99118
owner: {
100119
connect: {
101120
id: userId,
@@ -141,6 +160,42 @@ export async function deleteRepo(_, { name }, { userId }) {
141160
return true;
142161
}
143162

163+
export async function addCollaborator(_, { repoId, email }, { userId}) {
164+
// make sure the repo is writable by this user
165+
if (!userId) throw new Error("Not authenticated.");
166+
// 1. find the repo
167+
const repo = await prisma.repo.findFirst({
168+
where: {
169+
id: repoId,
170+
owner: { id: userId },
171+
},
172+
});
173+
if (!repo) throw new Error("Repo not found or you are not the owner.");
174+
if (repo.public) throw new Error("Public repo cannot have collaborators.");
175+
// 2. find the user
176+
const user = await prisma.user.findFirst({
177+
where: {
178+
email,
179+
}
180+
});
181+
if (!user) throw new Error("User not found");
182+
if (user.id === userId) throw new Error("You are already the owner.");
183+
if (repo.collaboratorIds.indexOf(user.id) !== -1) throw new Error("The user is already a collaborator.");
184+
// 3. add the user to the repo
185+
const res = await prisma.repo.update({
186+
where: {
187+
id: repoId,
188+
},
189+
data: {
190+
// public: false,
191+
// name: "test",
192+
collaboratorIds: {push: user.id},
193+
}
194+
})
195+
// console.log(res.collaboratorIds);
196+
return true;
197+
}
198+
144199
export async function addPod(_, { repoId, parent, index, input }, { userId }) {
145200
// make sure the repo is writable by this user
146201
if (!userId) throw new Error("Not authenticated.");
@@ -272,4 +327,4 @@ export async function deletePod(_, { id, toDelete }, { userId }) {
272327
},
273328
});
274329
return true;
275-
}
330+
}

api/src/typedefs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const typeDefs = gql`
1818
id: ID!
1919
name: String!
2020
pods: [Pod]
21+
collaboratorIds: [ID!]
22+
public: Boolean!
2123
}
2224
2325
type Pod {
@@ -83,6 +85,7 @@ export const typeDefs = gql`
8385
myRepos: [Repo]
8486
activeSessions: [String]
8587
listAllRuntimes: [String]
88+
myCollabRepos: [Repo]
8689
}
8790
8891
type Mutation {
@@ -95,7 +98,7 @@ export const typeDefs = gql`
9598
lastname: String
9699
): AuthData
97100
updateUser(email: String, firstname: String, lastname: String): Boolean
98-
createRepo(name: String, id: ID): Repo
101+
createRepo(name: String, id: ID, isPublic: Boolean): Repo
99102
deleteRepo(name: String): Boolean
100103
addPod(repoId: String, parent: String, index: Int, input: PodInput): Boolean
101104
deletePod(id: String, toDelete: [String]): Boolean
@@ -105,5 +108,6 @@ export const typeDefs = gql`
105108
clearPod: Boolean
106109
spawnRuntime(sessionId: String): Boolean
107110
killRuntime(sessionId: String!): Boolean
111+
addCollaborator(repoId: String, email: String): Boolean
108112
}
109113
`;

ui/src/components/Canvas.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { useApolloClient } from "@apollo/client";
5151
import { CanvasContextMenu } from "./CanvasContextMenu";
5252
import ToolBox, { ToolTypes } from "./Toolbox";
5353
import styles from "./canvas.style.js";
54+
import { ShareProjDialog } from "./ShareProjDialog";
5455
import { analyzeCode } from "../lib/parser";
5556

5657
const nanoid = customAlphabet(nolookalikes, 10);
@@ -513,12 +514,14 @@ export function Canvas() {
513514
// const pods = useStore(store, (state) => state.pods);
514515
const getPod = useStore(store, (state) => state.getPod);
515516
const nodesMap = useStore(store, (state) => state.ydoc.getMap<Node>("pods"));
517+
const [showShareDialog, setShowShareDialog] = useState(false);
518+
const repoId = useStore(store, (state) => state.repoId);
519+
const repoName = useStore(store, (state) => state.repoName);
516520

517521
const getRealNodes = useCallback(
518522
(id, level) => {
519523
let res: any[] = [];
520524
let children = getId2children(id) || [];
521-
console.log("getChildren", id, children);
522525
const pod = getPod(id);
523526
if (id !== "ROOT") {
524527
res.push({
@@ -920,8 +923,17 @@ export function Canvas() {
920923
y={points.y}
921924
addCode={() => addNode(client.x, client.y, "code")}
922925
addScope={() => addNode(client.x, client.y, "scope")}
926+
onShareClick={() => {
927+
setShowShareDialog(true);
928+
}}
923929
/>
924930
)}
931+
<ShareProjDialog
932+
open={showShareDialog}
933+
onClose={() => setShowShareDialog(false)}
934+
title={repoName || ""}
935+
id={repoId || ""}
936+
/>
925937
</Box>
926938
</Box>
927939
);

ui/src/components/CanvasContextMenu.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React, { useContext } from "react";
99
import CodeIcon from "@mui/icons-material/Code";
1010
import PostAddIcon from "@mui/icons-material/PostAdd";
1111
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
12+
import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined";
1213

1314
const paneMenuStyle = (left, top) => {
1415
return {
@@ -62,6 +63,12 @@ export function CanvasContextMenu(props) {
6263
{showLineNumbers ? "Hide " : "Show "} Line Numbers
6364
</ListItemText>
6465
</MenuItem>
66+
<MenuItem onClick={props.onShareClick} sx={ItemStyle}>
67+
<ListItemIcon>
68+
<ShareOutlinedIcon />
69+
</ListItemIcon>
70+
<ListItemText> Share with Collaborators </ListItemText>
71+
</MenuItem>
6572
</MenuList>
6673
</Box>
6774
);

ui/src/components/ShareProjDialog.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import DialogTitle from "@mui/material/DialogTitle";
2+
import Dialog from "@mui/material/Dialog";
3+
import DialogActions from "@mui/material/DialogActions";
4+
import DialogContent from "@mui/material/DialogContent";
5+
import DialogContentText from "@mui/material/DialogContentText";
6+
import TextField from "@mui/material/TextField";
7+
import Button from "@mui/material/Button";
8+
import Alert from "@mui/material/Alert";
9+
import { useState } from "react";
10+
import { useQuery, useMutation, gql } from "@apollo/client";
11+
12+
interface ShareProjDialogProps {
13+
open: boolean;
14+
title: String;
15+
onClose: () => void;
16+
id: string;
17+
}
18+
19+
export function ShareProjDialog({
20+
open,
21+
title,
22+
onClose,
23+
id,
24+
}: ShareProjDialogProps) {
25+
const [email, setEmail] = useState("");
26+
const [alert, setAlert] = useState(false);
27+
const [success, setSuccess] = useState(false);
28+
const [errorMsg, setErrorMsg] = useState("");
29+
30+
const query = gql`
31+
mutation addCollaborator($repoId: String, $email: String) {
32+
addCollaborator(repoId: $repoId, email: $email)
33+
}
34+
`;
35+
const [addEmail] = useMutation(query);
36+
37+
const onChange = (e) => {
38+
setEmail(e.target.value);
39+
};
40+
async function onShare() {
41+
if (email === "") {
42+
setAlert(true);
43+
setErrorMsg("Please enter an email address");
44+
return;
45+
}
46+
try {
47+
const { data } = await addEmail({
48+
variables: {
49+
repoId: id,
50+
email,
51+
},
52+
});
53+
setAlert(false);
54+
setSuccess(true);
55+
// show the success message for 1 second before closing the dialog
56+
setTimeout(() => {
57+
onCloseHandler();
58+
}, 1000);
59+
} catch (error: any) {
60+
setSuccess(false); // just in case
61+
setAlert(true);
62+
setErrorMsg(error?.message || "Unknown error");
63+
}
64+
}
65+
66+
function onCloseHandler() {
67+
setEmail("");
68+
setAlert(false);
69+
setSuccess(false);
70+
onClose();
71+
}
72+
73+
return (
74+
<Dialog open={open} onClose={onCloseHandler}>
75+
<DialogTitle> Share Project {title} with</DialogTitle>
76+
{alert && <Alert severity="error"> {errorMsg} </Alert>}
77+
{success && <Alert severity="success"> Invitation Sent </Alert>}
78+
<DialogContent>
79+
<DialogContentText>
80+
Enter the email address of the person you want to share this project
81+
with.
82+
</DialogContentText>
83+
<TextField
84+
autoFocus
85+
margin="dense"
86+
id="name"
87+
label="Email Address"
88+
type="email"
89+
variant="standard"
90+
fullWidth
91+
onChange={onChange}
92+
/>
93+
<DialogActions>
94+
<Button onClick={onCloseHandler}>Cancel</Button>
95+
<Button onClick={onShare}> Share</Button>
96+
</DialogActions>
97+
</DialogContent>
98+
</Dialog>
99+
);
100+
}

ui/src/lib/store.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ if (window.location.protocol === "http:") {
2828
}
2929
console.log("yjs server url: ", serverURL);
3030

31-
export const RepoContext =
32-
createContext<StoreApi<RepoSlice & RuntimeSlice> | null>(null);
31+
export const RepoContext = createContext<StoreApi<
32+
RepoSlice & RuntimeSlice
33+
> | null>(null);
3334

3435
// TODO use a selector to compute and retrieve the status
3536
// TODO this need to cooperate with syncing indicator

0 commit comments

Comments
 (0)