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>
pull/2924/head^2
Taha 2024-03-07 15:34:46 +00:00 zatwierdzone przez GitHub
rodzic 40c20e5585
commit eb80cf787b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
4 zmienionych plików z 277 dodań i 2 usunięć

Wyświetl plik

@ -2,12 +2,11 @@
title: Bounds Snapping Shape
component: ./BoundsSnappingShape.tsx
category: shapes/tools
priority: 2
priority: 3
---
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.

Wyświetl plik

@ -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.

Wyświetl plik

@ -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" codesee 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 versionin 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.
*/

Wyświetl plik

@ -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
}
}
}
}