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