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>
|
||||
{/* 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