Add project forking #533
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "forkedFromId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD FOREIGN KEY ("forkedFromId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -53,9 +53,12 @@ model Project {
|
||||
deleted Boolean @default(false)
|
||||
cadPackage CadPackage @default(openscad)
|
||||
socialCard SocialCard?
|
||||
forkedFromId String?
|
||||
forkedFrom Project? @relation("Fork", fields: [forkedFromId], references: [id])
|
||||
|
||||
Comment Comment[]
|
||||
Reaction ProjectReaction[]
|
||||
childForks Project[] @relation("Fork")
|
||||
Comment Comment[]
|
||||
Reaction ProjectReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ export const schema = gql`
|
||||
socialCard: SocialCard
|
||||
Comment: [Comment]!
|
||||
Reaction(userId: String): [ProjectReaction]!
|
||||
forkedFromId: String
|
||||
forkedFrom: Project
|
||||
childForks: [Project]!
|
||||
}
|
||||
|
||||
enum CadPackage {
|
||||
@@ -37,6 +40,11 @@ export const schema = gql`
|
||||
cadPackage: CadPackage!
|
||||
}
|
||||
|
||||
input ForkProjectInput {
|
||||
userId: String!
|
||||
forkedFromId: String
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
title: String
|
||||
description: String
|
||||
@@ -47,7 +55,7 @@ export const schema = gql`
|
||||
|
||||
type Mutation {
|
||||
createProject(input: CreateProjectInput!): Project!
|
||||
forkProject(input: CreateProjectInput!): Project!
|
||||
forkProject(input: ForkProjectInput!): Project!
|
||||
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
||||
updateProjectImages(
|
||||
id: String!
|
||||
|
||||
@@ -78,13 +78,26 @@ export const createProject = async ({ input }: CreateProjectArgs) => {
|
||||
}
|
||||
|
||||
export const forkProject = async ({ input }) => {
|
||||
// Only difference between create and fork project is that fork project will generate a unique title
|
||||
// (for the user) if there is a conflict
|
||||
requireAuth()
|
||||
const projectData = await db.project.findUnique({
|
||||
where: {
|
||||
id: input.forkedFromId,
|
||||
},
|
||||
})
|
||||
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
||||
const title = await generateUniqueString(input.title, isUniqueCallback)
|
||||
// TODO change the description to `forked from userName/projectName ${rest of description}`
|
||||
let title = projectData.title
|
||||
|
||||
title = await generateUniqueString(title, isUniqueCallback)
|
||||
|
||||
const { code, description, cadPackage } = projectData
|
||||
return db.project.create({
|
||||
data: foreignKeyReplacement({ ...input, title }),
|
||||
data: foreignKeyReplacement({
|
||||
...input,
|
||||
title,
|
||||
code,
|
||||
description,
|
||||
cadPackage,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,24 @@ import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import NavPlusButton from 'src/components/NavPlusButton'
|
||||
import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
|
||||
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
const FORK_PROJECT_MUTATION = gql`
|
||||
mutation ForkProjectMutation($input: ForkProjectInput!) {
|
||||
forkProject(input: $input) {
|
||||
id
|
||||
title
|
||||
description
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopButton = ({
|
||||
onClick,
|
||||
children,
|
||||
@@ -111,7 +124,6 @@ const IdeHeader = ({
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{/* <TopButton>Fork</TopButton> */}
|
||||
<div className="h-8 w-8">
|
||||
<NavPlusButton />
|
||||
</div>
|
||||
@@ -130,6 +142,26 @@ function DefaultTopButtons({
|
||||
handleRender,
|
||||
|
|
||||
canEdit,
|
||||
}) {
|
||||
const { currentUser } = useAuth()
|
||||
const [createFork] = useMutation(FORK_PROJECT_MUTATION, {
|
||||
onCompleted: () => {},
|
||||
})
|
||||
const handleFork = () => {
|
||||
const prom = createFork({
|
||||
variables: {
|
||||
input: {
|
||||
userId: currentUser.sub,
|
||||
forkedFromId: project.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
// toast.promise(prom, {
|
||||
// loading: 'Saving Image/s',
|
||||
// success: <b>Image/s saved!</b>,
|
||||
// error: <b>Problem saving.</b>,
|
||||
// })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{canEdit && !projectTitle && (
|
||||
@@ -212,6 +244,15 @@ function DefaultTopButtons({
|
||||
)
|
||||
}}
|
||||
</Popover>
|
||||
{currentUser?.id && (
|
||||
<TopButton
|
||||
onClick={handleFork}
|
||||
name="Fork"
|
||||
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<Svg name="fork-new" className="w-6 h-6 text-ch-blue-400" />
|
||||
</TopButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
Navigate to either the IDE or the details views to match wherever you are now.
TODO regularize these params when the Header component gets regularized.
This seems a little brittle to me, as I would hope that the component would be able be able to know what page it's on without having to look at the route. What if someone names their project "idea" than it would match on "user/idea" I had a look at seeing what it would take to pass this information to the component instead and realised that it could be done but it would be adding more mess to the messy situation that is this component so I did a big refactor aimed at this branch.
I've put you as a review, but the diff is not super meaningful as it's really just huge chunks of code moved around 🤷
https://github.com/Irev-Dev/cadhub/pull/534