Skip to content

Commit ea752a1

Browse files
authored
feat: Fork a Repo (#312)
1 parent dec6133 commit ea752a1

File tree

4 files changed

+103
-1
lines changed

4 files changed

+103
-1
lines changed

api/src/resolver_repo.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ async function deleteEdge(_, { source, target }, { userId }) {
216216
return true;
217217
}
218218

219-
async function createRepo(_, { id, name, isPublic }, { userId }) {
219+
async function createRepo(_, {}, { userId }) {
220220
if (!userId) throw Error("Unauthenticated");
221221
const repo = await prisma.repo.create({
222222
data: {
@@ -497,6 +497,68 @@ async function deletePods(_, { ids }: { ids: string[] }, { userId }) {
497497
return true;
498498
}
499499

500+
async function copyRepo(_, { repoId }, { userId }) {
501+
// Find the repo
502+
const repo = await prisma.repo.findFirst({
503+
where: {
504+
id: repoId,
505+
},
506+
include: {
507+
pods: {
508+
include: {
509+
parent: true,
510+
},
511+
},
512+
},
513+
});
514+
if (!repo) throw new Error("Repo not found");
515+
516+
// Create a new repo
517+
const { id } = await createRepo(_, {}, { userId });
518+
// update the repo name
519+
await prisma.repo.update({
520+
where: {
521+
id,
522+
},
523+
data: {
524+
name: repo.name ? `Copy of ${repo.name}` : `Copy of ${repo.id}`,
525+
},
526+
});
527+
528+
// Create new id for each pod
529+
const sourcePods = repo.pods;
530+
const idMap = await sourcePods.reduce(async (acc, pod) => {
531+
const map = await acc;
532+
const newId = await nanoid();
533+
map.set(pod.id, newId);
534+
return map;
535+
}, Promise.resolve(new Map()));
536+
537+
// Update the parent/child relationship with their new ids
538+
const targetPods = sourcePods.map((pod) => {
539+
return {
540+
...pod,
541+
id: idMap.get(pod.id),
542+
parent: pod.parent ? { id: idMap.get(pod.parent.id) } : undefined,
543+
repoId: id,
544+
parentId: pod.parentId ? idMap.get(pod.parentId) : undefined,
545+
};
546+
});
547+
548+
// Add all nodes without parent/child relationship to the new repo.
549+
// TODO: it updates the parent/child relationship automatically somehow,maybe because the parentId? Try to figure out why, then refactor addPods method.
550+
await prisma.pod.createMany({
551+
data: targetPods.map((pod) => ({
552+
...pod,
553+
id: pod.id,
554+
index: 0,
555+
parent: undefined,
556+
})),
557+
});
558+
559+
return id;
560+
}
561+
500562
export default {
501563
Query: {
502564
myRepos,
@@ -509,6 +571,7 @@ export default {
509571
createRepo,
510572
updateRepo,
511573
deleteRepo,
574+
copyRepo,
512575
updatePod,
513576
deletePods,
514577
addEdge,

api/src/typedefs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export const typeDefs = gql`
126126
createRepo: Repo
127127
updateRepo(id: ID!, name: String!): Boolean
128128
deleteRepo(id: ID!): Boolean
129+
copyRepo(repoId: String!): ID!
129130
deletePods(ids: [String]): Boolean
130131
addPods(repoId: String!, pods: [PodInput]): Boolean
131132
updatePod(id: String!, repoId: String!, input: PodInput): Boolean

ui/src/components/Header.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type HeaderProps = {
3131
currentPage?: string | null;
3232
breadcrumbItem?: React.ReactNode;
3333
shareButton?: React.ReactNode;
34+
forkButton?: React.ReactNode;
3435
};
3536

3637
export const Header: React.FC<HeaderProps> = ({
@@ -39,6 +40,7 @@ export const Header: React.FC<HeaderProps> = ({
3940
currentPage = null,
4041
breadcrumbItem = null,
4142
shareButton = null,
43+
forkButton = null,
4244
}) => {
4345
const [anchorElNav, setAnchorElNav] = useState(null);
4446
const [anchorElUser, setAnchorElUser] = useState(null);
@@ -96,6 +98,16 @@ export const Header: React.FC<HeaderProps> = ({
9698
{breadcrumbItem}
9799
</Breadcrumbs>
98100

101+
<Box
102+
sx={{
103+
display: { xs: "none", md: "flex" },
104+
alignItems: "center",
105+
paddingRight: "10px",
106+
}}
107+
>
108+
{forkButton}
109+
</Box>
110+
99111
<Box
100112
sx={{
101113
display: { xs: "none", md: "flex" },

ui/src/pages/repo.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from "@mui/material/Link";
55
import Alert from "@mui/material/Alert";
66
import AlertTitle from "@mui/material/AlertTitle";
77
import ShareIcon from "@mui/icons-material/Share";
8+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
89
import Button from "@mui/material/Button";
910
import { gql, useApolloClient, useMutation } from "@apollo/client";
1011

@@ -139,6 +140,18 @@ function RepoWrapper({ children, id }) {
139140
if (!store) throw new Error("Missing BearContext.Provider in the tree");
140141

141142
const setShareOpen = useStore(store, (state) => state.setShareOpen);
143+
const navigate = useNavigate();
144+
const [copyRepo] = useMutation(
145+
gql`
146+
mutation CopyRepo($id: String!) {
147+
copyRepo(repoId: $id)
148+
}
149+
`,
150+
{ variables: { id } }
151+
);
152+
// if(result.data.copyRepo){
153+
// navigate(`/repo/${result.data.copyRepo}`);
154+
// }
142155

143156
const DrawerWidth = 240;
144157

@@ -180,6 +193,19 @@ function RepoWrapper({ children, id }) {
180193
Share
181194
</Button>
182195
}
196+
forkButton={
197+
<Button
198+
endIcon={<ContentCopyIcon />}
199+
onClick={async () => {
200+
const result = await copyRepo();
201+
const newRepoId = result.data.copyRepo;
202+
window.open(`/repo/${newRepoId}`);
203+
}}
204+
variant="contained"
205+
>
206+
Make a copy
207+
</Button>
208+
}
183209
/>
184210
<Box
185211
sx={{

0 commit comments

Comments
 (0)