Issue-178 Add draft mode for IDE #185

Merged
Irev-Dev merged 1 commits from kurt/178 into main 2020-12-29 09:42:56 +01:00
9 changed files with 148 additions and 56 deletions

View File

@@ -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" />

View File

@@ -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,
}} }}
/> />

View File

@@ -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
} }

View File

@@ -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">
* *

View File

@@ -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 } })

View File

@@ -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

View 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

View File

@@ -0,0 +1,7 @@
import DraftPartPage from './DraftPartPage'
export const generated = () => {
return <DraftPartPage />
}
export default { title: 'Pages/DraftPartPage' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import DraftPartPage from './DraftPartPage'
describe('DraftPartPage', () => {
it('renders successfully', () => {
expect(() => {
render(<DraftPartPage />)
}).not.toThrow()
})
})