Shape with Migrations (#3078)
Adds an example of how to add migrations for a custom shape. closes tld-2246 - [x] `documentation` — Changes to the documentation only[^2] ### Release Notes - Adds a shape with migrations example --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
40c20e5585
commit
eb80cf787b
4 changed files with 277 additions and 2 deletions
|
@ -2,12 +2,11 @@
|
||||||
title: Bounds Snapping Shape
|
title: Bounds Snapping Shape
|
||||||
component: ./BoundsSnappingShape.tsx
|
component: ./BoundsSnappingShape.tsx
|
||||||
category: shapes/tools
|
category: shapes/tools
|
||||||
priority: 2
|
priority: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
Custom shapes with special bounds snapping behaviour.
|
Custom shapes with special bounds snapping behaviour.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
This example shows how to create a shape with custom snapping geometry. When shapes are moved around in snap mode, they will snap to the bounds of other shapes by default. However a shape can return custom snapping geometry to snap to instead. This example creates a playing card shape. The cards are designed to snap together so that the top-left icon remains visible when stacked, similar to a hand of cards in a game.The most relevant code for this customisation is in playing-card-util.tsx.
|
This example shows how to create a shape with custom snapping geometry. When shapes are moved around in snap mode, they will snap to the bounds of other shapes by default. However a shape can return custom snapping geometry to snap to instead. This example creates a playing card shape. The cards are designed to snap together so that the top-left icon remains visible when stacked, similar to a hand of cards in a game.The most relevant code for this customisation is in playing-card-util.tsx.
|
||||||
|
|
12
apps/examples/src/examples/shape-with-migrations/README.md
Normal file
12
apps/examples/src/examples/shape-with-migrations/README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
title: Shape with migrations
|
||||||
|
component: ./ShapeWithMigrationsExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
priority: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
Migrate your shapes and their data between versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Sometimes you'll want to update the way a shape works in your application. When this happens there can be a risk of errors and bugs. For example, users with an old version of a shape in their documents might encounter errors when the editor tries to access a property that doesn't exist. This example shows how you can use our migrations system to preserve your users' data between versions. It uses a snapshot to load a document with a shape that is missing a "color" prop, and uses the migrations method of the shape util to update it.
|
|
@ -0,0 +1,173 @@
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
HTMLContainer,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
TLOnResizeHandler,
|
||||||
|
Tldraw,
|
||||||
|
resizeBox,
|
||||||
|
} from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
import snapshot from './snapshot.json'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
export type IMyShape = TLBaseShape<
|
||||||
|
'myshape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export class MigratedShapeUtil extends BaseBoxShapeUtil<IMyShape> {
|
||||||
|
static override type = 'myshape' as const
|
||||||
|
|
||||||
|
static override props = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
color: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
static override migrations = {
|
||||||
|
firstVersion: 0,
|
||||||
|
currentVersion: 1,
|
||||||
|
migrators: {
|
||||||
|
1: {
|
||||||
|
up(shape: IMyShape) {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
color: 'lightblue',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down(shape: IMyShape) {
|
||||||
|
const { color: _, ...propsWithoutColor } = shape.props
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: propsWithoutColor,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultProps(): IMyShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 300,
|
||||||
|
h: 300,
|
||||||
|
color: 'lightblue',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: IMyShape) {
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
id={shape.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: shape.props.color,
|
||||||
|
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||||
|
}}
|
||||||
|
></HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: IMyShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
|
||||||
|
override onResize: TLOnResizeHandler<IMyShape> = (shape, info) => {
|
||||||
|
return resizeBox(shape, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customShapeUtils = [MigratedShapeUtil]
|
||||||
|
|
||||||
|
export default function ShapeWithMigrationsExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
// Pass in the array of custom shape classes
|
||||||
|
shapeUtils={customShapeUtils}
|
||||||
|
// Use a snapshot to load an old version of the shape
|
||||||
|
snapshot={snapshot}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Introduction:
|
||||||
|
|
||||||
|
Sometimes you'll want to update the way a shape works in your application without
|
||||||
|
breaking older versions of the shape that a user may have stored or persisted in
|
||||||
|
memory.
|
||||||
|
|
||||||
|
This example shows how you can use our migrations system to upgrade (or downgrade)
|
||||||
|
user's data between different versions. Most of the code above is general "custom
|
||||||
|
shape" code—see our custom shape example for more details.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
To define migrations, we can override the migrations property of our shape util. Each migration
|
||||||
|
had two parts: an `up` migration and `down` migration. In this case, the `up` migration adds
|
||||||
|
the `color` prop to the shape, and the `down` migration removes it.
|
||||||
|
|
||||||
|
In some cases (mainly in multiplayer sessions) a peer or server may need to take a later
|
||||||
|
version of a shape and migrate it down to an older version—in this case, it would run the
|
||||||
|
down migrations in order to get it to the needed version.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
Each time the editor's store creates a snapshot (`editor.store.createSnapshot`), it
|
||||||
|
serializes all of the records (the snapshot's `store`) as well as versions of each
|
||||||
|
record that it contains (the snapshot's `scena`). When the editor loads a snapshot,
|
||||||
|
it compares its current schema with the snapshot's schema to determine which migrations
|
||||||
|
to apply to each record.
|
||||||
|
|
||||||
|
In this example, we have a snapshot (snapshot.json) that we created in version 0,
|
||||||
|
however our shape now has a 'color' prop that was added in version 1.
|
||||||
|
|
||||||
|
The snapshot looks something like this:
|
||||||
|
|
||||||
|
```json{
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"shape:BqG5uIAa9ig2-ukfnxwBX": {
|
||||||
|
...,
|
||||||
|
"props": {
|
||||||
|
"w": 300,
|
||||||
|
"h": 300
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
...,
|
||||||
|
"recordVersions": {
|
||||||
|
...,
|
||||||
|
"shape": {
|
||||||
|
"version": 3,
|
||||||
|
"subTypeKey": "type",
|
||||||
|
"subTypeVersions": {
|
||||||
|
...,
|
||||||
|
"myshape": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the shape in the snapshot doesn't have a 'color' prop.
|
||||||
|
|
||||||
|
Note also that the schema's version for this shape is 0.
|
||||||
|
|
||||||
|
When the editor loads the snapshot, it will compare the serialzied schema's version with
|
||||||
|
its current schema's version for the shape, which is 1 as defined in our shape's migrations.
|
||||||
|
Since the serialized version is older than its current version, it will use our migration
|
||||||
|
to bring it up to date: it will run the migration's `up` function, which will add the 'color'
|
||||||
|
prop to the shape.
|
||||||
|
*/
|
|
@ -0,0 +1,91 @@
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"name": "",
|
||||||
|
"meta": {},
|
||||||
|
"id": "document:document",
|
||||||
|
"typeName": "document"
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "page:page",
|
||||||
|
"name": "Page 1",
|
||||||
|
"index": "a1",
|
||||||
|
"typeName": "page"
|
||||||
|
},
|
||||||
|
"shape:BqG5uIAa9ig2-ukfnxwBX": {
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:BqG5uIAa9ig2-ukfnxwBX",
|
||||||
|
"type": "myshape",
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"props": {
|
||||||
|
"w": 300,
|
||||||
|
"h": 300
|
||||||
|
},
|
||||||
|
"typeName": "shape"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"storeVersion": 4,
|
||||||
|
"recordVersions": {
|
||||||
|
"asset": {
|
||||||
|
"version": 1,
|
||||||
|
"subTypeKey": "type",
|
||||||
|
"subTypeVersions": {
|
||||||
|
"image": 3,
|
||||||
|
"video": 3,
|
||||||
|
"bookmark": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"document": {
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"instance": {
|
||||||
|
"version": 24
|
||||||
|
},
|
||||||
|
"instance_page_state": {
|
||||||
|
"version": 5
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"version": 3,
|
||||||
|
"subTypeKey": "type",
|
||||||
|
"subTypeVersions": {
|
||||||
|
"group": 0,
|
||||||
|
"text": 1,
|
||||||
|
"bookmark": 2,
|
||||||
|
"draw": 1,
|
||||||
|
"geo": 8,
|
||||||
|
"note": 5,
|
||||||
|
"line": 4,
|
||||||
|
"frame": 0,
|
||||||
|
"arrow": 3,
|
||||||
|
"highlight": 0,
|
||||||
|
"embed": 4,
|
||||||
|
"image": 3,
|
||||||
|
"video": 2,
|
||||||
|
"myshape": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"instance_presence": {
|
||||||
|
"version": 5
|
||||||
|
},
|
||||||
|
"pointer": {
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue