Merge pull request #79 from tldraw/fix-parent-on-translate-redo

Fix bug on cloning shapes that are the child of a group
spike-supabase
Steve Ruiz 2021-09-06 14:43:38 +01:00 zatwierdzone przez GitHub
commit 8c2c8d8c93
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 295 dodań i 97 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { TLDrawShapeType, TLDrawStatus } from '~types'
import { GroupShape, TLDrawShapeType, TLDrawStatus } from '~types'
describe('Translate session', () => {
const tlstate = new TLDrawState()
@ -184,15 +184,98 @@ describe('Translate session', () => {
// it.todo('clones a shape with a parent shape')
describe('when translating a child of a group', () => {
it('translates the shape and updates the groups size / point', () => {
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.group(['rect1', 'rect2'], 'groupA')
.select('rect1')
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, false)
.completeSession()
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
tlstate.undo()
expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
tlstate.redo()
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
})
it('clones the shape and updates the parent', () => {
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.group(['rect1', 'rect2'], 'groupA')
.select('rect1')
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, true)
.completeSession()
const children = tlstate.getShape<GroupShape>('groupA').children
const newShapeId = children[children.length - 1]
expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0])
expect(tlstate.getShape<GroupShape>('groupA').children.length).toBe(3)
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
expect(tlstate.getShape(newShapeId).point).toStrictEqual([20, 20])
expect(tlstate.getShape(newShapeId).parentId).toBe('groupA')
tlstate.undo()
expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0])
expect(tlstate.getShape<GroupShape>('groupA').children.length).toBe(2)
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
expect(tlstate.getShape(newShapeId)).toBeUndefined()
tlstate.redo()
expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0])
expect(tlstate.getShape<GroupShape>('groupA').children.length).toBe(3)
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
expect(tlstate.getShape(newShapeId).point).toStrictEqual([20, 20])
expect(tlstate.getShape(newShapeId).parentId).toBe('groupA')
})
})
describe('when translating a shape with children', () => {
it('translates the shapes children', () => {
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.group()
.group(['rect1', 'rect2'], 'groupA')
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, false)
.completeSession()
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect2').point).toStrictEqual([110, 110])
tlstate.undo()
expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100])
tlstate.redo()
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
expect(tlstate.getShape('rect2').point).toStrictEqual([110, 110])
})
it('clones the shapes and children', () => {

Wyświetl plik

@ -8,6 +8,7 @@ import {
TLDrawCommand,
TLDrawStatus,
ArrowShape,
GroupShape,
} from '~types'
import { TLDR } from '~state/tldr'
import type { Patch } from 'rko'
@ -230,7 +231,8 @@ export class TranslateSession implements Session {
complete(data: Data): TLDrawCommand {
const pageId = data.appState.currentPageId
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot
const { initialShapes, initialParentChildren, bindingsToDelete, clones, clonedBindings } =
this.snapshot
const beforeBindings: Patch<Record<string, TLDrawBinding>> = {}
const beforeShapes: Patch<Record<string, TLDrawShape>> = {}
@ -238,21 +240,47 @@ export class TranslateSession implements Session {
const afterBindings: Patch<Record<string, TLDrawBinding>> = {}
const afterShapes: Patch<Record<string, TLDrawShape>> = {}
clones.forEach((clone) => {
beforeShapes[clone.id] = undefined
afterShapes[clone.id] = this.isCloning ? TLDR.getShape(data, clone.id, pageId) : undefined
})
if (this.isCloning) {
// Update the clones
clones.forEach((clone) => {
beforeShapes[clone.id] = undefined
initialShapes.forEach((shape) => {
beforeShapes[shape.id] = { point: shape.point }
afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id, pageId).point }
})
afterShapes[clone.id] = TLDR.getShape(data, clone.id, pageId)
clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId)
})
if (clone.parentId !== pageId) {
beforeShapes[clone.parentId] = {
...beforeShapes[clone.parentId],
children: initialParentChildren[clone.parentId],
}
afterShapes[clone.parentId] = {
...afterShapes[clone.parentId],
children: TLDR.getShape<GroupShape>(data, clone.parentId, pageId).children,
}
}
})
// Update the cloned bindings
clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId)
})
} else {
// If we aren't cloning, then update the initial shapes
initialShapes.forEach((shape) => {
beforeShapes[shape.id] = {
...beforeShapes[shape.id],
point: shape.point,
}
afterShapes[shape.id] = {
...afterShapes[shape.id],
point: TLDR.getShape(data, shape.id, pageId).point,
}
})
}
// Update the deleted bindings and any associated shapes
bindingsToDelete.forEach((binding) => {
beforeBindings[binding.id] = binding
@ -270,6 +298,8 @@ export class TranslateSession implements Session {
afterShapes[id] = { ...afterShapes[id], handles: {} }
// There should be before and after shapes
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
beforeShapes[id]!.handles![handle.id as keyof ArrowShape['handles']] = {
bindingId: binding.id,

Wyświetl plik

@ -1,11 +1,19 @@
import dynamic from 'next/dynamic'
import { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import Head from 'next/head'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
export default function Shhh(): JSX.Element {
return <Editor id="home" />
return (
<>
<Head>
<title>tldraw</title>
</Head>
<Editor id="home" />
</>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {

Wyświetl plik

@ -1,5 +1,6 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import Head from 'next/head'
import { getSession } from 'next-auth/client'
import dynamic from 'next/dynamic'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
@ -9,7 +10,14 @@ interface RoomProps {
}
export default function Room({ id }: RoomProps): JSX.Element {
return <Editor id={id} />
return (
<>
<Head>
<title>tldraw</title>
</Head>
<Editor id={id} />
</>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {

Wyświetl plik

@ -1,12 +1,20 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import Head from 'next/head'
interface RoomProps {
id?: string
}
export default function RandomRoomPage({ id }: RoomProps): JSX.Element {
return <div>Should have routed to room: {id}</div>
return (
<>
<Head>
<title>tldraw</title>
</Head>
<div>Should have routed to room: {id}</div>
</>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {

Wyświetl plik

@ -1,6 +1,14 @@
import dynamic from 'next/dynamic'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
import Head from 'next/head'
export default function Shhh(): JSX.Element {
return <Editor id="home" />
return (
<>
<Head>
<title>tldraw</title>
</Head>
<Editor id="home" />
</>
)
}

Wyświetl plik

@ -3,80 +3,86 @@ import { getSession, signin, signout, useSession } from 'next-auth/client'
import { GetServerSideProps } from 'next'
import Link from 'next/link'
import React from 'react'
import Head from 'next/head'
export default function Sponsorware(): JSX.Element {
const [session, loading] = useSession()
return (
<OuterContent>
<Content
size={{
'@sm': 'small',
}}
>
<h1>tldraw (is sponsorware)</h1>
<p>
Hey, thanks for visiting <Link href="/">tldraw</Link>, a tiny little drawing app by{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
steveruizok
</a>{' '}
and friends .
</p>
<video autoPlay muted playsInline onClick={(e) => e.currentTarget.play()}>
<source src="images/hello.mp4" type="video/mp4" />
</video>
<p>This project is currently: </p>
<ul>
<li>in development</li>
<li>only available for my sponsors</li>
</ul>
<p>
If you&apos;d like to try it out,{' '}
<a
href="https://github.com/sponsors/steveruizok"
target="_blank"
rel="noopener noreferrer"
>
sponsor me on Github
</a>{' '}
(at any level) and sign in below.
</p>
<ButtonGroup>
{session ? (
<>
<Button onClick={() => signout()} variant={'secondary'}>
Sign Out
</Button>
<Detail>
Signed in as {session?.user?.name} ({session?.user?.email}), but it looks like
you&apos;re not yet a sponsor.
<br />
Something wrong? Try <Link href="/">reloading the page</Link> or DM me on{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
Twitter
</a>
.
</Detail>
</>
) : (
<>
<Button onClick={() => signin('github')} variant={'primary'}>
{loading ? 'Loading...' : 'Sign in With Github'}
</Button>
<Detail>Already a sponsor? Just sign in to visit the app.</Detail>
</>
)}
</ButtonGroup>
</Content>
</OuterContent>
<>
<Head>
<title>tldraw</title>
</Head>
<OuterContent>
<Content
size={{
'@sm': 'small',
}}
>
<h1>tldraw (is sponsorware)</h1>
<p>
Hey, thanks for visiting <Link href="/">tldraw</Link>, a tiny little drawing app by{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
steveruizok
</a>{' '}
and friends .
</p>
<video autoPlay muted playsInline onClick={(e) => e.currentTarget.play()}>
<source src="images/hello.mp4" type="video/mp4" />
</video>
<p>This project is currently: </p>
<ul>
<li>in development</li>
<li>only available for my sponsors</li>
</ul>
<p>
If you&apos;d like to try it out,{' '}
<a
href="https://github.com/sponsors/steveruizok"
target="_blank"
rel="noopener noreferrer"
>
sponsor me on Github
</a>{' '}
(at any level) and sign in below.
</p>
<ButtonGroup>
{session ? (
<>
<Button onClick={() => signout()} variant={'secondary'}>
Sign Out
</Button>
<Detail>
Signed in as {session?.user?.name} ({session?.user?.email}), but it looks like
you&apos;re not yet a sponsor.
<br />
Something wrong? Try <Link href="/">reloading the page</Link> or DM me on{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
Twitter
</a>
.
</Detail>
</>
) : (
<>
<Button onClick={() => signin('github')} variant={'primary'}>
{loading ? 'Loading...' : 'Sign in With Github'}
</Button>
<Detail>Already a sponsor? Just sign in to visit the app.</Detail>
</>
)}
</ButtonGroup>
</Content>
</OuterContent>
</>
)
}

Wyświetl plik

@ -1,13 +1,21 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import Head from 'next/head'
interface RoomProps {
id?: string
}
export default function OtherUserPage({ id }: RoomProps): JSX.Element {
return <div>Todo, other user: {id}</div>
return (
<>
<Head>
<title>tldraw</title>
</Head>
<div>Todo, other user: {id}</div>
</>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {

Wyświetl plik

@ -3,6 +3,7 @@ import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import type { Session } from 'next-auth'
import { signOut } from 'next-auth/client'
import Head from 'next/head'
interface UserPageProps {
session: Session
@ -10,12 +11,17 @@ interface UserPageProps {
export default function UserPage({ session }: UserPageProps): JSX.Element {
return (
<div>
<pre>
<code>{JSON.stringify(session.user, null, 2)}</code>
</pre>
<button onClick={() => signOut}>Sign Out</button>
</div>
<>
<Head>
<title>tldraw</title>
</Head>
<div>
<pre>
<code>{JSON.stringify(session.user, null, 2)}</code>
</pre>
<button onClick={() => signOut}>Sign Out</button>
</div>
</>
)
}

Wyświetl plik

@ -1,9 +1,42 @@
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive Mono';
font-style: normal;
font-weight: 420;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {