Issue-178 Add draft mode for IDE #185
@@ -47,6 +47,7 @@ const Routes = () => {
|
||||
</Private>
|
||||
{/* End ownership enforced routes */}
|
||||
|
||||
<Route path="/draft" page={DraftPartPage} name="draftPart" />
|
||||
<Route path="/u/{userName}" page={UserPage} name="user" />
|
||||
<Route path="/u/{userName}/{partTitle}" page={PartPage} name="part" />
|
||||
<Route path="/u/{userName}/{partTitle}/ide" page={IdePartPage} name="ide" />
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import CascadeController from 'src/helpers/cascadeController'
|
||||
import IdeToolbar from 'src/components/IdeToolbar'
|
||||
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 [code, setCode] = useState(part.code)
|
||||
const isDraft = !part
|
||||
const [code, setCode] = useState(isDraft ? defaultExampleCode : part.code)
|
||||
const { currentUser } = useAuth()
|
||||
const canEdit = currentUser?.sub === part?.user?.id
|
||||
useEffect(() => {
|
||||
// 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
|
||||
// returns a clean up function that hides the div again.
|
||||
setCode(part?.code || '')
|
||||
const onCodeChange = (code) => setCode(code)
|
||||
CascadeController.initialise(onCodeChange, part.code)
|
||||
CascadeController.initialise(onCodeChange, code || '')
|
||||
const element = document.getElementById('cascade-container')
|
||||
element.setAttribute('style', 'display: block; opacity: 100%; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
||||
return () => {
|
||||
element.setAttribute('style', 'display: none; overflow: hidden; height: calc(100vh - 8rem)') // eslint-disable-line
|
||||
}
|
||||
}, [part.code])
|
||||
const isChanges = code !== part.code
|
||||
}, [part?.code])
|
||||
const isChanges = code !== part?.code
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,6 +49,8 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
||||
<IdeToolbar
|
||||
canEdit={canEdit}
|
||||
isChanges={isChanges && !loading}
|
||||
isDraft={isDraft}
|
||||
code={code}
|
||||
onSave={() => {
|
||||
saveCode({
|
||||
input: {
|
||||
@@ -42,8 +65,8 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
|
||||
}}
|
||||
onExport={(type) => threejsViewport[`saveShape${type}`]()}
|
||||
userNamePart={{
|
||||
userName: part.user.userName,
|
||||
partTitle: part.title,
|
||||
userName: part?.user?.userName,
|
||||
partTitle: part?.title,
|
||||
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!) {
|
||||
forkPart(input: $input) {
|
||||
id
|
||||
@@ -62,9 +62,9 @@ export const Success = ({ part, refetch }) => {
|
||||
},
|
||||
})
|
||||
|
||||
const saveCode = ({ input, id, isFork }) => {
|
||||
const saveCode = async ({ input, id, isFork }) => {
|
||||
if (!isFork) {
|
||||
updatePart({ variables: { id, input } })
|
||||
await updatePart({ variables: { id, input } })
|
||||
refetch()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,17 +4,43 @@ import OutBound from 'src/components/OutBound'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
|
||||
import Button from 'src/components/Button'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
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 [whichPopup, setWhichPopup] = useState(null)
|
||||
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 }) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
@@ -27,7 +53,17 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isAuthenticated) onSave()
|
||||
if (isDraft && isAuthenticated)
|
||||
forkPart({
|
||||
variables: {
|
||||
input: {
|
||||
userId: currentUser.sub,
|
||||
title: 'draft',
|
||||
code,
|
||||
},
|
||||
},
|
||||
})
|
||||
else if (isAuthenticated) onSave()
|
||||
else recordedLogin()
|
||||
}
|
||||
|
||||
@@ -55,39 +91,43 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
|
||||
id="cadhub-ide-toolbar"
|
||||
className="flex bg-gradient-to-r from-gray-900 to-indigo-900 pt-1"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="h-8 w-8 ml-4">
|
||||
<ImageUploader
|
||||
className="rounded-full object-cover"
|
||||
aspectRatio={1}
|
||||
imageUrl={userNamePart?.image}
|
||||
width={80}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-indigo-400 ml-2 mr-8">
|
||||
<Link to={routes.user({ userName: userNamePart?.userName })}>
|
||||
{userNamePart?.userName}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isDraft && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div className="h-8 w-8 ml-4">
|
||||
<ImageUploader
|
||||
className="rounded-full object-cover"
|
||||
aspectRatio={1}
|
||||
imageUrl={userNamePart?.image}
|
||||
width={80}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-indigo-400 ml-2 mr-8">
|
||||
<Link to={routes.user({ userName: userNamePart?.userName })}>
|
||||
{userNamePart?.userName}
|
||||
</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
|
||||
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
|
||||
iconName={canEdit ? 'save' : 'fork'}
|
||||
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={handleSave}
|
||||
>
|
||||
{canEdit ? 'Save' : 'Fork'}
|
||||
{isChanges && (
|
||||
{showForkButton ? 'Fork' : 'Save'}
|
||||
{isChanges && !isDraft && (
|
||||
<span className="relative h-4">
|
||||
<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' })
|
||||
},
|
||||
})
|
||||
const [createUser] = useMutation(CREATE_PART_MUTATION, {
|
||||
const [createPart] = useMutation(CREATE_PART_MUTATION, {
|
||||
onCompleted: ({ createPart }) => {
|
||||
navigate(
|
||||
routes.part({
|
||||
@@ -130,7 +130,7 @@ export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
|
||||
})
|
||||
const onSave = (id, input) => {
|
||||
if (!id) {
|
||||
createUser({ variables: { input } })
|
||||
createPart({ variables: { input } })
|
||||
return
|
||||
}
|
||||
updateUser({ variables: { id, input } })
|
||||
|
||||
@@ -137,20 +137,12 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<ul className="flex items-center">
|
||||
<li
|
||||
className={getActiveClasses(
|
||||
'mr-8 h-10 w-10 rounded-full border-2 border-gray-700 flex items-center justify-center',
|
||||
{ 'border-indigo-300': currentUser }
|
||||
'mr-8 h-10 w-10 rounded-full border-2 border-indigo-300 flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{isAuthenticated && data?.user?.userName ? (
|
||||
<Link
|
||||
className="h-full w-full"
|
||||
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" />
|
||||
)}
|
||||
<Link className="h-full w-full" to={routes.draftPart()}>
|
||||
<Svg name="plus" className="text-indigo-300 w-full h-full" />
|
||||
</Link>
|
||||
</li>
|
||||
{isAuthenticated ? (
|
||||
<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