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>
{/* 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()
})
})