Issue-178 Add draft mode for IDE

resolves #178
Initially the UI forced users to create a "part" before they got access
to the ide, now we're letting users go straight to hacking in the ide
and saving can come later. Better at getting users to the code earlier
This commit is contained in:
Kurt Hutten
2020-12-29 18:53:49 +11:00
parent 7f5b48a959
commit c84dcda4a1
9 changed files with 148 additions and 56 deletions

View File

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

View File

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

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!) {
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
}

View File

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

View File

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

View File

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

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