Add project forking #533

Merged
franknoirot merged 7 commits from frank/add-project-forking into main 2021-09-28 14:51:36 +02:00
8 changed files with 230 additions and 263 deletions
Showing only changes of commit d4bfcb4eb8 - Show all commits

View File

@@ -1,21 +1,17 @@
import { useAuth } from '@redwoodjs/auth' import { useAuth } from '@redwoodjs/auth'
import { useLocation } from '@redwoodjs/router' import { useMutation } from '@redwoodjs/web'
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { Link, navigate, routes } from '@redwoodjs/router' import { Link, navigate, routes } from '@redwoodjs/router'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding' import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding'
import ExternalScript from 'src/components/EncodedUrl/ExternalScript' import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
import Svg from 'src/components/Svg/Svg' import Svg from 'src/components/Svg/Svg'
import NavPlusButton from 'src/components/NavPlusButton' import { toast } from '@redwoodjs/web/toast'
import ProfileSlashLogin from 'src/components/ProfileSlashLogin' import CaptureButton from 'src/components/CaptureButton/CaptureButton'
import { useMutation } from '@redwoodjs/web' import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Gravatar from 'src/components/Gravatar/Gravatar' import Gravatar from 'src/components/Gravatar/Gravatar'
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle' import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
import { toast } from '@redwoodjs/web/toast'
import { ReactNode } from 'react'
const FORK_PROJECT_MUTATION = gql` const FORK_PROJECT_MUTATION = gql`
mutation ForkProjectMutation($input: ForkProjectInput!) { mutation ForkProjectMutation($input: ForkProjectInput!) {
@@ -55,101 +51,21 @@ const TopButton = ({
) )
} }
interface IdeHeaderProps { export default function IdeHeader({
handleRender: () => void
projectTitle?: string
projectOwner?: string
projectOwnerId?: string
projectOwnerImage?: string
projectId?: string
children?: ReactNode
}
const IdeHeader = ({
handleRender, handleRender,
projectOwner, context,
projectTitle, }: {
projectOwnerImage, handleRender?: () => void
projectId, context: 'ide' | 'profile'
projectOwnerId,
children,
}: IdeHeaderProps) => {
const { currentUser } = useAuth()
const { project } = useIdeContext()
const canEdit =
(currentUser &&
currentUser?.sub === (project?.user?.id || projectOwnerId)) ||
currentUser?.roles.includes('admin')
const _projectId = projectId || project?.id
const _projectOwner = project?.user?.userName || projectOwner
return (
<div className="h-16 w-full bg-ch-gray-900 flex justify-between items-center text-lg">
<div className="h-full text-gray-300 flex items-center">
<div className="w-14 h-16 flex items-center justify-center bg-ch-gray-900">
<Link to={routes.home()}>
<Svg className="w-12 p-0.5" name="favicon" />
</Link>
</div>
{_projectId && (
<>
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
<Gravatar
image={project?.user?.image || projectOwnerImage}
className="w-10"
/>
<Link
to={routes.user({
userName: _projectOwner,
})}
>
{_projectOwner}
</Link>
</span>
<EditableProjectTitle
id={_projectId}
userName={_projectOwner}
projectTitle={project?.title || projectTitle}
canEdit={canEdit}
shouldRouteToIde={!projectTitle}
/>
</>
)}
</div>
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
{!children ? (
<DefaultTopButtons
project={project}
projectTitle={projectTitle}
projectId={projectId}
_projectOwner={_projectOwner}
handleRender={handleRender}
canEdit={canEdit}
/>
) : (
children
)}
<div className="h-8 w-8">
<NavPlusButton />
</div>
<ProfileSlashLogin />
</div>
</div>
)
}
export default IdeHeader
function DefaultTopButtons({
project,
projectTitle,
projectId,
_projectOwner,
handleRender,
canEdit,
}) { }) {
const { currentUser } = useAuth() const { currentUser } = useAuth()
const { pathname } = useLocation() const { project } = useIdeContext()
const isProfile = context === 'profile'
const canEdit =
(currentUser && currentUser?.sub === project?.user?.id) ||
currentUser?.roles?.includes('admin')
const projectOwner = project?.user?.userName
const [createFork] = useMutation(FORK_PROJECT_MUTATION, { const [createFork] = useMutation(FORK_PROJECT_MUTATION, {
onCompleted: ({ forkProject }) => { onCompleted: ({ forkProject }) => {
@@ -157,9 +73,7 @@ function DefaultTopButtons({
userName: forkProject?.user?.userName, userName: forkProject?.user?.userName,
projectTitle: forkProject?.title, projectTitle: forkProject?.title,
} }
navigate( navigate(!isProfile ? routes.ide(params) : routes.project(params))
pathname.includes('/ide') ? routes.ide(params) : routes.project(params)
)
}, },
}) })
const handleFork = () => { const handleFork = () => {
@@ -167,7 +81,7 @@ function DefaultTopButtons({
variables: { variables: {
input: { input: {
userId: currentUser.sub, userId: currentUser.sub,
forkedFromId: project?.id || projectId, forkedFromId: project?.id,
}, },
}, },
}) })
@@ -180,7 +94,32 @@ function DefaultTopButtons({
return ( return (
<> <>
{canEdit && !projectTitle && ( <div className="flex justify-between flex-grow h-full">
<div className="flex h-full items-center text-gray-300">
{project?.id && (
<>
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
<Gravatar image={project?.user?.image} className="w-10" />
<Link
to={routes.user({
userName: projectOwner,
})}
>
{projectOwner}
</Link>
</span>
<EditableProjectTitle
id={project?.id}
userName={projectOwner}
projectTitle={project?.title}
canEdit={canEdit}
shouldRouteToIde={!isProfile}
/>
</>
)}
</div>
<div className="grid grid-flow-col-dense gap-4 items-center mr-4">
{canEdit && !isProfile && (
<CaptureButton <CaptureButton
canEdit={canEdit} canEdit={canEdit}
projectTitle={project?.title} projectTitle={project?.title}
@@ -197,7 +136,7 @@ function DefaultTopButtons({
)} )}
/> />
)} )}
{!projectTitle && ( {!isProfile && (
<TopButton <TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300" className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={handleRender} onClick={handleRender}
franknoirot commented 2021-09-28 00:23:45 +02:00 (Migrated from github.com)
Review

Navigate to either the IDE or the details views to match wherever you are now.

Navigate to either the IDE or the details views to match wherever you are now.
franknoirot commented 2021-09-28 00:25:37 +02:00 (Migrated from github.com)
Review

TODO regularize these params when the Header component gets regularized.

TODO regularize these params when the Header component gets regularized.
Irev-Dev commented 2021-09-28 12:32:39 +02:00 (Migrated from github.com)
Review

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

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
@@ -209,11 +148,16 @@ function DefaultTopButtons({
/> />
</TopButton> </TopButton>
)} )}
{projectTitle && ( {isProfile && (
<TopButton <TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300" className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={() => onClick={() =>
navigate(routes.ide({ userName: _projectOwner, projectTitle })) navigate(
routes.ide({
userName: projectOwner,
projectTitle: project.title,
})
)
} }
name="Editor" name="Editor"
> >
@@ -224,7 +168,7 @@ function DefaultTopButtons({
{({ open }) => { {({ open }) => {
return ( return (
<> <>
<Popover.Button className="h-full w-full outline-none"> <Popover.Button className="h-full outline-none">
<TopButton <TopButton
Tag="div" Tag="div"
name="Share" name="Share"
@@ -269,6 +213,8 @@ function DefaultTopButtons({
<Svg name="fork-new" className="w-6 h-6 text-ch-blue-400" /> <Svg name="fork-new" className="w-6 h-6 text-ch-blue-400" />
</TopButton> </TopButton>
)} )}
</div>
</div>
</> </>
) )
} }

View File

@@ -1,17 +1,17 @@
import { useEffect, useState } from 'react' import { useState } from 'react'
import IdeContainer from 'src/components/IdeContainer/IdeContainer' import IdeContainer from 'src/components/IdeContainer/IdeContainer'
import { useRender } from './useRender' import { useRender } from './useRender'
import OutBound from 'src/components/OutBound/OutBound'
import IdeSideBar from 'src/components/IdeSideBar/IdeSideBar' import IdeSideBar from 'src/components/IdeSideBar/IdeSideBar'
import IdeHeader from 'src/components/IdeHeader/IdeHeader' import TopNav from 'src/components/TopNav/TopNav'
import Svg from 'src/components/Svg/Svg'
import { useIdeInit } from 'src/components/EncodedUrl/helpers' import { useIdeInit } from 'src/components/EncodedUrl/helpers'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode' import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import { ShortcutsModalContext } from 'src/components/EditorMenu/AllShortcutsModal' import { ShortcutsModalContext } from 'src/components/EditorMenu/AllShortcutsModal'
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
interface Props { interface Props {
cadPackage: string cadPackage: CadPackageType
} }
const IdeWrapper = ({ cadPackage }: Props) => { const IdeWrapper = ({ cadPackage }: Props) => {
@@ -33,7 +33,9 @@ const IdeWrapper = ({ cadPackage }: Props) => {
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<ShortcutsModalContext.Provider value={shortcutModalContextValues}> <ShortcutsModalContext.Provider value={shortcutModalContextValues}>
<nav className="flex"> <nav className="flex">
<IdeHeader handleRender={onRender} /> <TopNav>
<IdeHeader handleRender={onRender} context="ide" />
</TopNav>
</nav> </nav>
<div className="h-full flex flex-grow bg-ch-gray-900"> <div className="h-full flex flex-grow bg-ch-gray-900">
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View File

@@ -2,6 +2,8 @@ import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast' import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth' import { useAuth } from '@redwoodjs/auth'
import { useIdeState } from 'src/helpers/hooks/useIdeState'
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
import ProjectProfile from 'src/components/ProjectProfile/ProjectProfile' import ProjectProfile from 'src/components/ProjectProfile/ProjectProfile'
import { QUERY as PROJECT_REACTION_QUERY } from 'src/components/ProjectReactionsCell' import { QUERY as PROJECT_REACTION_QUERY } from 'src/components/ProjectReactionsCell'
@@ -120,15 +122,10 @@ export const Empty = () => <div className="h-full">Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div> export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ export const Success = ({ userProject, refetch }) => {
userProject,
variables: { isEditable },
refetch,
}) => {
const { currentUser } = useAuth() const { currentUser } = useAuth()
const [updateProject, { loading, error }] = useMutation( const [state, thunkDispatch] = useIdeState()
UPDATE_PROJECT_MUTATION, const [updateProject] = useMutation(UPDATE_PROJECT_MUTATION, {
{
onCompleted: ({ updateProject }) => { onCompleted: ({ updateProject }) => {
navigate( navigate(
routes.project({ routes.project({
@@ -138,8 +135,7 @@ export const Success = ({
) )
toast.success('Project updated.') toast.success('Project updated.')
}, },
} })
)
const [createProject] = useMutation(CREATE_PROJECT_MUTATION, { const [createProject] = useMutation(CREATE_PROJECT_MUTATION, {
onCompleted: ({ createProject }) => { onCompleted: ({ createProject }) => {
navigate( navigate(
@@ -160,7 +156,7 @@ export const Success = ({
refetch() refetch()
} }
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, { const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
onCompleted: ({ deleteProject }) => { onCompleted: () => {
navigate(routes.home()) navigate(routes.home())
toast.success('Project deleted.') toast.success('Project deleted.')
}, },
@@ -206,15 +202,27 @@ export const Success = ({
}) })
return ( return (
<IdeContext.Provider
value={{
state,
thunkDispatch,
project: {
...userProject?.Project,
user: {
id: userProject.id,
image: userProject.image,
userName: userProject.userName,
},
},
}}
>
<ProjectProfile <ProjectProfile
userProject={userProject} userProject={userProject}
onSave={onSave} onSave={onSave}
onDelete={onDelete} onDelete={onDelete}
loading={loading}
error={error}
isEditable={isEditable}
onReaction={onReaction} onReaction={onReaction}
onComment={onComment} onComment={onComment}
/> />
</IdeContext.Provider>
) )
} }

View File

@@ -10,6 +10,7 @@ import Button from 'src/components/Button/Button'
import ProjectReactionsCell from '../ProjectReactionsCell' import ProjectReactionsCell from '../ProjectReactionsCell'
import { countEmotes } from 'src/helpers/emote' import { countEmotes } from 'src/helpers/emote'
import { getActiveClasses } from 'get-active-classes' import { getActiveClasses } from 'get-active-classes'
import TopNav from 'src/components/TopNav/TopNav'
import IdeHeader from 'src/components/IdeHeader/IdeHeader' import IdeHeader from 'src/components/IdeHeader/IdeHeader'
import CadPackage from 'src/components/CadPackage/CadPackage' import CadPackage from 'src/components/CadPackage/CadPackage'
import Gravatar from 'src/components/Gravatar/Gravatar' import Gravatar from 'src/components/Gravatar/Gravatar'
@@ -50,7 +51,7 @@ const ProjectProfile = ({
projectTitle: project?.title, projectTitle: project?.title,
}) })
) )
}, [currentUser]) }, [currentUser, project?.title, userProject.userName])
useIdeInit(project?.cadPackage, project?.code, 'viewer') useIdeInit(project?.cadPackage, project?.code, 'viewer')
const [newDescription, setNewDescription] = useState(project?.description) const [newDescription, setNewDescription] = useState(project?.description)
const onDescriptionChange = (description) => setNewDescription(description()) const onDescriptionChange = (description) => setNewDescription(description())
@@ -70,14 +71,9 @@ const ProjectProfile = ({
<> <>
<div className="h-screen flex flex-col text-lg font-fira-sans"> <div className="h-screen flex flex-col text-lg font-fira-sans">
<div className="flex"> <div className="flex">
<IdeHeader <TopNav>
handleRender={() => {}} <IdeHeader context="profile" />
projectOwner={userProject?.userName} </TopNav>
projectOwnerImage={userProject?.image}
projectOwnerId={userProject?.id}
projectTitle={project?.title}
projectId={project?.id}
/>
</div> </div>
<div className="relative flex-grow h-full"> <div className="relative flex-grow h-full">
<div className="grid grid-cols-1 md:auto-cols-preview-layout grid-flow-row-dense absolute inset-0 h-full"> <div className="grid grid-cols-1 md:auto-cols-preview-layout grid-flow-row-dense absolute inset-0 h-full">

View File

@@ -0,0 +1,31 @@
import { Link, routes } from '@redwoodjs/router'
import Svg from 'src/components/Svg/Svg'
import NavPlusButton from 'src/components/NavPlusButton'
import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
import { ReactNode } from 'react'
interface IdeHeaderProps {
children?: ReactNode
}
const TopNav = ({ children }: IdeHeaderProps) => {
return (
<div className="h-16 w-full bg-ch-gray-900 flex justify-between items-center text-lg">
<div className="h-full text-gray-300 flex items-center">
<div className="w-14 h-16 flex items-center justify-center bg-ch-gray-900">
<Link to={routes.home()}>
<Svg className="w-12 p-0.5" name="favicon" />
</Link>
</div>
</div>
{children}
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
<div className="h-8 w-8">
<NavPlusButton />
</div>
<ProfileSlashLogin />
</div>
</div>
)
}
export default TopNav

View File

@@ -1,9 +1,8 @@
import { useEffect, useReducer } from 'react' import { useEffect, useReducer } from 'react'
import { useAuth } from '@redwoodjs/auth' import { useAuth } from '@redwoodjs/auth'
import { Link, navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import ProjectsOfUser from 'src/components/ProjectsOfUserCell' import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
import IdeHeader from 'src/components/IdeHeader/IdeHeader' import TopNav from 'src/components/TopNav/TopNav'
import Svg from 'src/components/Svg/Svg'
import { import {
fieldComponents, fieldComponents,
fieldReducer, fieldReducer,
@@ -27,13 +26,7 @@ function buildFieldsConfig(fieldsConfig, user, hasPermissionToEdit) {
) )
} }
const UserProfile = ({ const UserProfile = ({ user, isEditing, onSave }: UserProfileType) => {
user,
isEditing,
loading,
onSave,
error,
}: UserProfileType) => {
const { currentUser } = useAuth() const { currentUser } = useAuth()
const hasPermissionToEdit = currentUser?.sub === user.id const hasPermissionToEdit = currentUser?.sub === user.id
useEffect(() => { useEffect(() => {
@@ -60,14 +53,7 @@ const UserProfile = ({
<> <>
<div className="md:h-screen flex flex-col text-lg font-fira-sans"> <div className="md:h-screen flex flex-col text-lg font-fira-sans">
<div className="flex"> <div className="flex">
<IdeHeader <TopNav />
handleRender={() => {}}
projectOwner={user?.userName}
projectOwnerImage={user?.image}
projectOwnerId={user?.id}
>
<span></span>
</IdeHeader>
</div> </div>
<div className="relative flex-grow h-full"> <div className="relative flex-grow h-full">
<div className="grid md:grid-cols-profile-layout grid-flow-row-dense absolute inset-0"> <div className="grid md:grid-cols-profile-layout grid-flow-row-dense absolute inset-0">

View File

@@ -4,6 +4,7 @@ import { Toaster } from '@redwoodjs/web/toast'
import { useIdeState } from 'src/helpers/hooks/useIdeState' import { useIdeState } from 'src/helpers/hooks/useIdeState'
import type { Project } from 'src/components/IdeProjectCell/IdeProjectCell' import type { Project } from 'src/components/IdeProjectCell/IdeProjectCell'
import { IdeContext } from 'src/helpers/hooks/useIdeContext' import { IdeContext } from 'src/helpers/hooks/useIdeContext'
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
interface Props { interface Props {
cadPackage: string cadPackage: string
@@ -21,7 +22,9 @@ const DevIdePage = ({ cadPackage, project }: Props) => {
/> />
<Toaster timeout={9000} /> <Toaster timeout={9000} />
<IdeContext.Provider value={{ state, thunkDispatch, project }}> <IdeContext.Provider value={{ state, thunkDispatch, project }}>
<IdeWrapper cadPackage={cadPackage.toLocaleLowerCase()} /> <IdeWrapper
cadPackage={cadPackage.toLocaleLowerCase() as CadPackageType}
/>
</IdeContext.Provider> </IdeContext.Provider>
</div> </div>
) )

View File

@@ -2,14 +2,11 @@ import { useAuth } from '@redwoodjs/auth'
import ProjectCell from 'src/components/ProjectCell' import ProjectCell from 'src/components/ProjectCell'
import Seo from 'src/components/Seo/Seo' import Seo from 'src/components/Seo/Seo'
import { useIdeState } from 'src/helpers/hooks/useIdeState'
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
import { Toaster } from '@redwoodjs/web/toast' import { Toaster } from '@redwoodjs/web/toast'
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages' import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
const ProjectPage = ({ userName, projectTitle }) => { const ProjectPage = ({ userName, projectTitle }) => {
const { currentUser } = useAuth() const { currentUser } = useAuth()
const [state, thunkDispatch] = useIdeState()
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId( const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
userName, userName,
projectTitle projectTitle
@@ -23,13 +20,11 @@ const ProjectPage = ({ userName, projectTitle }) => {
lang="en-US" lang="en-US"
/> />
<Toaster timeout={1500} /> <Toaster timeout={1500} />
<IdeContext.Provider value={{ state, thunkDispatch, project: null }}>
<ProjectCell <ProjectCell
userName={userName} userName={userName}
projectTitle={projectTitle} projectTitle={projectTitle}
currentUserId={currentUser?.sub} currentUserId={currentUser?.sub}
/> />
</IdeContext.Provider>
</> </>
) )
} }