From eb80cf787bcf99774bc7def647417417742fbffa Mon Sep 17 00:00:00 2001 From: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:34:46 +0000 Subject: [PATCH] Shape with Migrations (#3078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../examples/bounds-snapping-shape/README.md | 3 +- .../examples/shape-with-migrations/README.md | 12 ++ .../ShapeWithMigrationsExample.tsx | 173 ++++++++++++++++++ .../shape-with-migrations/snapshot.json | 91 +++++++++ 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 apps/examples/src/examples/shape-with-migrations/README.md create mode 100644 apps/examples/src/examples/shape-with-migrations/ShapeWithMigrationsExample.tsx create mode 100644 apps/examples/src/examples/shape-with-migrations/snapshot.json diff --git a/apps/examples/src/examples/bounds-snapping-shape/README.md b/apps/examples/src/examples/bounds-snapping-shape/README.md index 5241bf981..6debd02a7 100644 --- a/apps/examples/src/examples/bounds-snapping-shape/README.md +++ b/apps/examples/src/examples/bounds-snapping-shape/README.md @@ -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. diff --git a/apps/examples/src/examples/shape-with-migrations/README.md b/apps/examples/src/examples/shape-with-migrations/README.md new file mode 100644 index 000000000..d3a1f598d --- /dev/null +++ b/apps/examples/src/examples/shape-with-migrations/README.md @@ -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. diff --git a/apps/examples/src/examples/shape-with-migrations/ShapeWithMigrationsExample.tsx b/apps/examples/src/examples/shape-with-migrations/ShapeWithMigrationsExample.tsx new file mode 100644 index 000000000..65d1c77b8 --- /dev/null +++ b/apps/examples/src/examples/shape-with-migrations/ShapeWithMigrationsExample.tsx @@ -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 { + 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 ( + + ) + } + + indicator(shape: IMyShape) { + return + } + + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } +} + +const customShapeUtils = [MigratedShapeUtil] + +export default function ShapeWithMigrationsExample() { + return ( +
+ +
+ ) +} + +/* +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. +*/ diff --git a/apps/examples/src/examples/shape-with-migrations/snapshot.json b/apps/examples/src/examples/shape-with-migrations/snapshot.json new file mode 100644 index 000000000..e570a1e4d --- /dev/null +++ b/apps/examples/src/examples/shape-with-migrations/snapshot.json @@ -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 + } + } + } +}