kopia lustrzana https://github.com/Tldraw/Tldraw
Merge pull request #79 from tldraw/fix-parent-on-translate-redo
Fix bug on cloning shapes that are the child of a groupspike-supabase
commit
8c2c8d8c93
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'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'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'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'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Ładowanie…
Reference in New Issue