Issue-178 Add draft mode for IDE #185
@@ -47,6 +47,7 @@ const Routes = () => {
|
|||||||
</Private>
|
</Private>
|
||||||
{/* End ownership enforced routes */}
|
{/* End ownership enforced routes */}
|
||||||
|
|
||||||
|
<Route path="/draft" page={DraftPartPage} name="draftPart" />
|
||||||
<Route path="/u/{userName}" page={UserPage} name="user" />
|
<Route path="/u/{userName}" page={UserPage} name="user" />
|
||||||
<Route path="/u/{userName}/{partTitle}" page={PartPage} name="part" />
|
<Route path="/u/{userName}/{partTitle}" page={PartPage} name="part" />
|
||||||
<Route path="/u/{userName}/{partTitle}/ide" page={IdePartPage} name="ide" />
|
<Route path="/u/{userName}/{partTitle}/ide" page={IdePartPage} name="ide" />
|
||||||
|
|||||||
@@ -1,26 +1,47 @@
|
|||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import CascadeController from 'src/helpers/cascadeController'
|
import CascadeController from 'src/helpers/cascadeController'
|
||||||
import IdeToolbar from 'src/components/IdeToolbar'
|
import IdeToolbar from 'src/components/IdeToolbar'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const defaultExampleCode = `// Welcome to Cascade Studio! Here are some useful functions:
|
||||||
|
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
|
||||||
|
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
|
||||||
|
// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),
|
||||||
|
// FilletEdges(), ChamferEdges(),
|
||||||
|
// Slider(), Button(), Checkbox()
|
||||||
|
|
||||||
|
let holeRadius = Slider("Radius", 30 , 20 , 40);
|
||||||
|
|
||||||
|
let sphere = Sphere(50);
|
||||||
|
let cylinderZ = Cylinder(holeRadius, 200, true);
|
||||||
|
let cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));
|
||||||
|
let cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));
|
||||||
|
|
||||||
|
Translate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));
|
||||||
|
|
||||||
|
Translate([-130, 0, 100], Text3D("Start Hacking"));
|
||||||
|
|
||||||
|
// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!`
|
||||||
|
|
||||||
const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
||||||
const [code, setCode] = useState(part.code)
|
const isDraft = !part
|
||||||
|
const [code, setCode] = useState(isDraft ? defaultExampleCode : part.code)
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const canEdit = currentUser?.sub === part?.user?.id
|
const canEdit = currentUser?.sub === part?.user?.id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Cascade studio attaches "cascade-container" a div outside the react app in 'web/src/index.html', and so we are
|
// Cascade studio attaches "cascade-container" a div outside the react app in 'web/src/index.html', and so we are
|
||||||
// "opening" and "closing" it for the ide part of the app by displaying none or block. Which is why this useEffect
|
// "opening" and "closing" it for the ide part of the app by displaying none or block. Which is why this useEffect
|
||||||
// returns a clean up function that hides the div again.
|
// returns a clean up function that hides the div again.
|
||||||
|
setCode(part?.code || '')
|
||||||
const onCodeChange = (code) => setCode(code)
|
const onCodeChange = (code) => setCode(code)
|
||||||
CascadeController.initialise(onCodeChange, part.code)
|
CascadeController.initialise(onCodeChange, code || '')
|
||||||
const element = document.getElementById('cascade-container')
|
const element = document.getElementById('cascade-container')
|
||||||
element.setAttribute('style', 'display: block; opacity: 100%; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
element.setAttribute('style', 'display: block; opacity: 100%; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
||||||
return () => {
|
return () => {
|
||||||
element.setAttribute('style', 'display: none; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
element.setAttribute('style', 'display: none; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
||||||
}
|
}
|
||||||
}, [part.code])
|
}, [part?.code])
|
||||||
const isChanges = code !== part.code
|
const isChanges = code !== part?.code
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -28,6 +49,8 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
|||||||
<IdeToolbar
|
<IdeToolbar
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
isChanges={isChanges && !loading}
|
isChanges={isChanges && !loading}
|
||||||
|
isDraft={isDraft}
|
||||||
|
code={code}
|
||||||
onSave={() => {
|
onSave={() => {
|
||||||
saveCode({
|
saveCode({
|
||||||
input: {
|
input: {
|
||||||
@@ -42,8 +65,8 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
|||||||
}}
|
}}
|
||||||
onExport={(type) => threejsViewport[`saveShape${type}`]()}
|
onExport={(type) => threejsViewport[`saveShape${type}`]()}
|
||||||
userNamePart={{
|
userNamePart={{
|
||||||
userName: part.user.userName,
|
userName: part?.user?.userName,
|
||||||
partTitle: part.title,
|
partTitle: part?.title,
|
||||||
image: part?.user?.image,
|
image: part?.user?.image,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const UPDATE_PART_MUTATION = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const FORK_PART_MUTATION = gql`
|
export const FORK_PART_MUTATION = gql`
|
||||||
mutation ForkPartMutation($input: CreatePartInput!) {
|
mutation ForkPartMutation($input: CreatePartInput!) {
|
||||||
forkPart(input: $input) {
|
forkPart(input: $input) {
|
||||||
id
|
id
|
||||||
@@ -62,9 +62,9 @@ export const Success = ({ part, refetch }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveCode = ({ input, id, isFork }) => {
|
const saveCode = async ({ input, id, isFork }) => {
|
||||||
if (!isFork) {
|
if (!isFork) {
|
||||||
updatePart({ variables: { id, input } })
|
await updatePart({ variables: { id, input } })
|
||||||
refetch()
|
refetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,43 @@ import OutBound from 'src/components/OutBound'
|
|||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
|
|
||||||
import Button from 'src/components/Button'
|
import Button from 'src/components/Button'
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import Svg from '../Svg/Svg'
|
import Svg from '../Svg/Svg'
|
||||||
import LoginModal from '../LoginModal/LoginModal'
|
import LoginModal from 'src/components/LoginModal'
|
||||||
|
import { FORK_PART_MUTATION } from 'src/components/IdePartCell'
|
||||||
|
|
||||||
const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
|
const IdeToolbar = ({
|
||||||
|
canEdit,
|
||||||
|
isChanges,
|
||||||
|
onSave,
|
||||||
|
onExport,
|
||||||
|
userNamePart,
|
||||||
|
isDraft,
|
||||||
|
code,
|
||||||
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [whichPopup, setWhichPopup] = useState(null)
|
const [whichPopup, setWhichPopup] = useState(null)
|
||||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false)
|
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false)
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
|
const showForkButton = !(canEdit || isDraft)
|
||||||
|
|
||||||
|
const { addMessage } = useFlash()
|
||||||
|
const [forkPart] = useMutation(FORK_PART_MUTATION, {
|
||||||
|
onCompleted: ({ forkPart }) => {
|
||||||
|
navigate(
|
||||||
|
routes.ide({
|
||||||
|
userName: forkPart?.user?.userName,
|
||||||
|
partTitle: forkPart?.title,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
addMessage(`Part created with title: ${forkPart?.title}.`, {
|
||||||
|
classes: 'rw-flash-success',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const handleClick = ({ event, whichPopup }) => {
|
const handleClick = ({ event, whichPopup }) => {
|
||||||
setAnchorEl(event.currentTarget)
|
setAnchorEl(event.currentTarget)
|
||||||
@@ -27,7 +53,17 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (isAuthenticated) onSave()
|
if (isDraft && isAuthenticated)
|
||||||
|
forkPart({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
userId: currentUser.sub,
|
||||||
|
title: 'draft',
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
else if (isAuthenticated) onSave()
|
||||||
else recordedLogin()
|
else recordedLogin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,39 +91,43 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
|
|||||||
id="cadhub-ide-toolbar"
|
id="cadhub-ide-toolbar"
|
||||||
className="flex bg-gradient-to-r from-gray-900 to-indigo-900 pt-1"
|
className="flex bg-gradient-to-r from-gray-900 to-indigo-900 pt-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
{!isDraft && (
|
||||||
<div className="h-8 w-8 ml-4">
|
<>
|
||||||
<ImageUploader
|
<div className="flex items-center">
|
||||||
className="rounded-full object-cover"
|
<div className="h-8 w-8 ml-4">
|
||||||
aspectRatio={1}
|
<ImageUploader
|
||||||
imageUrl={userNamePart?.image}
|
className="rounded-full object-cover"
|
||||||
width={80}
|
aspectRatio={1}
|
||||||
/>
|
imageUrl={userNamePart?.image}
|
||||||
</div>
|
width={80}
|
||||||
<div className="text-indigo-400 ml-2 mr-8">
|
/>
|
||||||
<Link to={routes.user({ userName: userNamePart?.userName })}>
|
</div>
|
||||||
{userNamePart?.userName}
|
<div className="text-indigo-400 ml-2 mr-8">
|
||||||
</Link>
|
<Link to={routes.user({ userName: userNamePart?.userName })}>
|
||||||
</div>
|
{userNamePart?.userName}
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
iconName="arrow-left"
|
||||||
|
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
|
||||||
|
shouldAnimateHover
|
||||||
|
onClick={() => {
|
||||||
|
navigate(routes.part(userNamePart))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Part Profile
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
iconName="arrow-left"
|
iconName={showForkButton ? 'fork' : 'save'}
|
||||||
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
|
|
||||||
shouldAnimateHover
|
|
||||||
onClick={() => {
|
|
||||||
navigate(routes.part(userNamePart))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Part Profile
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
iconName={canEdit ? 'save' : 'fork'}
|
|
||||||
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
|
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
|
||||||
shouldAnimateHover
|
shouldAnimateHover
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
{canEdit ? 'Save' : 'Fork'}
|
{showForkButton ? 'Fork' : 'Save'}
|
||||||
{isChanges && (
|
{isChanges && !isDraft && (
|
||||||
<span className="relative h-4">
|
<span className="relative h-4">
|
||||||
<span className="text-pink-400 text-2xl absolute transform -translate-y-3">
|
<span className="text-pink-400 text-2xl absolute transform -translate-y-3">
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
|
|||||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [createUser] = useMutation(CREATE_PART_MUTATION, {
|
const [createPart] = useMutation(CREATE_PART_MUTATION, {
|
||||||
onCompleted: ({ createPart }) => {
|
onCompleted: ({ createPart }) => {
|
||||||
navigate(
|
navigate(
|
||||||
routes.part({
|
routes.part({
|
||||||
@@ -130,7 +130,7 @@ export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
|
|||||||
})
|
})
|
||||||
const onSave = (id, input) => {
|
const onSave = (id, input) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
createUser({ variables: { input } })
|
createPart({ variables: { input } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateUser({ variables: { id, input } })
|
updateUser({ variables: { id, input } })
|
||||||
|
|||||||
@@ -137,20 +137,12 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
|||||||
<ul className="flex items-center">
|
<ul className="flex items-center">
|
||||||
<li
|
<li
|
||||||
className={getActiveClasses(
|
className={getActiveClasses(
|
||||||
'mr-8 h-10 w-10 rounded-full border-2 border-gray-700 flex items-center justify-center',
|
'mr-8 h-10 w-10 rounded-full border-2 border-indigo-300 flex items-center justify-center'
|
||||||
{ 'border-indigo-300': currentUser }
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isAuthenticated && data?.user?.userName ? (
|
<Link className="h-full w-full" to={routes.draftPart()}>
|
||||||
<Link
|
<Svg name="plus" className="text-indigo-300 w-full h-full" />
|
||||||
className="h-full w-full"
|
</Link>
|
||||||
to={routes.newPart({ userName: data?.user?.userName })}
|
|
||||||
>
|
|
||||||
<Svg name="plus" className="text-indigo-300 w-full h-full" />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Svg name="plus" className="text-gray-700 w-full h-full" />
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<li
|
<li
|
||||||
|
|||||||
18
web/src/pages/DraftPartPage/DraftPartPage.js
Normal file
18
web/src/pages/DraftPartPage/DraftPartPage.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
|
import IdeCascadeStudio from 'src/components/IdeCascadeStudio'
|
||||||
|
import Seo from 'src/components/Seo/Seo'
|
||||||
|
|
||||||
|
const DraftPartPage = () => {
|
||||||
|
return (
|
||||||
|
<MainLayout shouldRemoveFooterInIde>
|
||||||
|
<Seo
|
||||||
|
title="draft part"
|
||||||
|
description="black slate to hack on a new part"
|
||||||
|
lang="en-US"
|
||||||
|
/>
|
||||||
|
<IdeCascadeStudio />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DraftPartPage
|
||||||
7
web/src/pages/DraftPartPage/DraftPartPage.stories.js
Normal file
7
web/src/pages/DraftPartPage/DraftPartPage.stories.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import DraftPartPage from './DraftPartPage'
|
||||||
|
|
||||||
|
export const generated = () => {
|
||||||
|
return <DraftPartPage />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { title: 'Pages/DraftPartPage' }
|
||||||
11
web/src/pages/DraftPartPage/DraftPartPage.test.js
Normal file
11
web/src/pages/DraftPartPage/DraftPartPage.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { render } from '@redwoodjs/testing'
|
||||||
|
|
||||||
|
import DraftPartPage from './DraftPartPage'
|
||||||
|
|
||||||
|
describe('DraftPartPage', () => {
|
||||||
|
it('renders successfully', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(<DraftPartPage />)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user