diff --git a/apps/examples/e2e/tests/export-snapshots.spec.ts b/apps/examples/e2e/tests/export-snapshots.spec.ts
index 95314b9a0..8547a97fd 100644
--- a/apps/examples/e2e/tests/export-snapshots.spec.ts
+++ b/apps/examples/e2e/tests/export-snapshots.spec.ts
@@ -179,7 +179,7 @@ test.describe('Export snapshots', () => {
.selectAll()
.deleteShapes()
.createShapes(shapes)
- }, shapes)
+ }, shapes as any)
const downloadEvent = page.waitForEvent('download')
await page.click('[data-testid="main.menu"]')
diff --git a/apps/examples/src/17-shape-meta/ShapeMetaExample.tsx b/apps/examples/src/17-shape-meta/ShapeMetaExample.tsx
new file mode 100644
index 000000000..4e5282198
--- /dev/null
+++ b/apps/examples/src/17-shape-meta/ShapeMetaExample.tsx
@@ -0,0 +1,49 @@
+import { TLShape, Tldraw, track, useEditor } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+export default function ShapeMetaExample() {
+ return (
+
+ {
+ editor.getInitialMetaForShape = (shape) => {
+ return { label: `My ${shape.type} shape` }
+ }
+ }}
+ >
+
+
+
+ )
+}
+
+// By default, the TLShape type's meta property is { [key: string]: any }, but we can type it
+// by unioning the type with a new type that has a meta property of our choosing.
+type ShapeWithMyMeta = TLShape & { meta: { label: string } }
+
+export const ShapeLabelUiWithHelper = track(function ShapeLabelUiWithHelper() {
+ const editor = useEditor()
+ const onlySelectedShape = editor.onlySelectedShape as ShapeWithMyMeta | null
+
+ if (!onlySelectedShape) {
+ return null
+ }
+
+ function onChange(e: React.ChangeEvent) {
+ if (onlySelectedShape) {
+ const { id, type, meta } = onlySelectedShape
+
+ editor.updateShapes([
+ { id, type, meta: { ...meta, label: e.currentTarget.value } },
+ ])
+ }
+ }
+
+ return (
+
+ shape label:
+
+ )
+})
diff --git a/apps/examples/src/index.tsx b/apps/examples/src/index.tsx
index 85c618e7f..75ddffcb9 100644
--- a/apps/examples/src/index.tsx
+++ b/apps/examples/src/index.tsx
@@ -16,6 +16,7 @@ import StoreEventsExample from './13-store-events/StoreEventsExample'
import PersistenceExample from './14-persistence/PersistenceExample'
import ZonesExample from './15-custom-zones/ZonesExample'
import CustomStylesExample from './16-custom-styles/CustomStylesExample'
+import ShapeMetaExample from './17-shape-meta/ShapeMetaExample'
import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample'
import CustomUiExample from './4-custom-ui/CustomUiExample'
@@ -113,6 +114,10 @@ export const allExamples: Example[] = [
path: '/custom-styles',
element: ,
},
+ {
+ path: '/shape-meta',
+ element: ,
+ },
]
const router = createBrowserRouter(allExamples)
diff --git a/apps/vscode/editor/src/utils/bookmarks.ts b/apps/vscode/editor/src/utils/bookmarks.ts
index c39c84682..df6098fc1 100644
--- a/apps/vscode/editor/src/utils/bookmarks.ts
+++ b/apps/vscode/editor/src/utils/bookmarks.ts
@@ -17,6 +17,7 @@ export async function onCreateAssetFromUrl(editor: Editor, url: string): Promise
image: meta.image ?? '',
title: meta.title ?? truncateStringWithEllipsis(url, 32),
},
+ meta: {},
}
} catch (error) {
// Otherwise, fallback to fetching data from the url
@@ -51,6 +52,7 @@ export async function onCreateAssetFromUrl(editor: Editor, url: string): Promise
title: meta.title,
description: meta.description,
},
+ meta: {},
}
}
}
diff --git a/docs/docs/editor.mdx b/docs/docs/editor.mdx
index 182385642..ea5c899d9 100644
--- a/docs/docs/editor.mdx
+++ b/docs/docs/editor.mdx
@@ -224,6 +224,7 @@ editor.updateShapes([
])
```
+
### Delete shapes
```ts
@@ -244,6 +245,46 @@ editor.getShapeById(myShapeId)
editor.setCamera(0, 0, 1)
```
+## Meta
+
+Shapes have a `meta` property that you can fill with your own data. This should feel like a bit of a hack, however it's intended to be an escape hatch for applications where you want to use tldraw's shapes but also want to attach a bit of extra data to the shape.
+
+Note that tldraw's regular shape definitions have an unknown object for the shape's `meta` property. To type your shape's meta, use a union like this:
+
+```ts
+type MyShapeWithMeta = TLGeoShape & { meta: { createdBy: string } }
+
+const shape = editor.getShapeById(myGeoShape.id)
+```
+
+You can update a shape's `meta` property in the same way you would update its props, using `Editor.updateShapes`.
+
+```ts
+editor.updateShapes([{
+ id: myGeoShape.id,
+ type: "geo",
+ meta: {
+ createdBy: "Steve"
+ }
+}])
+```
+
+Like `props`, the data in a shape's `meta` object must be JSON serializable.
+
+In addition to setting meta properties this way, you can also set the default meta data for shapes using the Editor's `getShapeInitialMeta` method.
+
+```tsx
+editor.getShapeInitialMeta = (shape: TLShape) => {
+ if (shape.type === 'text') {
+ return { createdBy: currentUser.id, lastModified: Date.now() }
+ } else {
+ return { createdBy: currentUser.id }
+ }
+}
+```
+
+Whenever new shapes are created using the `createShapes` method, the shape's meta property will be set using the `getShapeInitialMeta` method. By default this method returns an empty object.
+
---
See the [tldraw repository](https://github.com/tldraw/tldraw/tree/main/apps/examples) for an example of how to use tldraw's Editor API to control the editor.
\ No newline at end of file
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md
index 131a5af12..09e2e2d38 100644
--- a/packages/editor/api-report.md
+++ b/packages/editor/api-report.md
@@ -20,6 +20,7 @@ import { EmbedDefinition } from '@tldraw/tlschema';
import { EventEmitter } from 'eventemitter3';
import { getHashForString } from '@tldraw/utils';
import { HistoryEntry } from '@tldraw/store';
+import { JsonObject } from '@tldraw/utils';
import { MatLike } from '@tldraw/primitives';
import { Matrix2d } from '@tldraw/primitives';
import { Matrix2dModel } from '@tldraw/primitives';
@@ -478,6 +479,7 @@ export class Editor extends EventEmitter {
getHandles(shape: T): TLHandle[] | undefined;
getHandlesById(id: T['id']): TLHandle[] | undefined;
getHighestIndexForParent(parentId: TLPageId | TLShapeId): string;
+ getInitialMetaForShape(_shape: TLShape): JsonObject;
getMaskedPageBounds(shape: TLShape): Box2d | undefined;
getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined;
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
@@ -886,6 +888,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -915,6 +918,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -931,6 +935,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | {
@@ -945,6 +950,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -1711,6 +1717,7 @@ export class NoteShapeUtil extends ShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -1734,6 +1741,7 @@ export class NoteShapeUtil extends ShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -2101,6 +2109,7 @@ export class TextShapeUtil extends ShapeUtil {
scale: number;
autoSize: boolean;
};
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
@@ -2124,6 +2133,7 @@ export class TextShapeUtil extends ShapeUtil {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
+ meta: JsonObject;
id: TLShapeId;
typeName: "shape";
} | undefined;
diff --git a/packages/editor/src/lib/components/Shape.tsx b/packages/editor/src/lib/components/Shape.tsx
index 4fe691e82..832dab66a 100644
--- a/packages/editor/src/lib/components/Shape.tsx
+++ b/packages/editor/src/lib/components/Shape.tsx
@@ -148,7 +148,7 @@ const InnerShape = React.memo(
function InnerShape({ shape, util }: { shape: T; util: ShapeUtil }) {
return useStateTracking('InnerShape:' + util.type, () => util.component(shape))
},
- (prev, next) => prev.shape.props === next.shape.props
+ (prev, next) => prev.shape.props === next.shape.props || prev.shape.meta === next.shape.meta
)
const InnerShapeBackground = React.memo(
@@ -161,7 +161,7 @@ const InnerShapeBackground = React.memo(
}) {
return useStateTracking('InnerShape:' + util.type, () => util.backgroundComponent?.(shape))
},
- (prev, next) => prev.shape.props === next.shape.props
+ (prev, next) => prev.shape.props === next.shape.props || prev.shape.meta === next.shape.meta
)
const CulledShape = React.memo(
diff --git a/packages/editor/src/lib/components/ShapeIndicator.tsx b/packages/editor/src/lib/components/ShapeIndicator.tsx
index 1c5b94c6f..7d5c9873e 100644
--- a/packages/editor/src/lib/components/ShapeIndicator.tsx
+++ b/packages/editor/src/lib/components/ShapeIndicator.tsx
@@ -11,7 +11,7 @@ import { OptionalErrorBoundary } from './ErrorBoundary'
class ShapeWithPropsEquality {
constructor(public shape: TLShape | undefined) {}
equals(other: ShapeWithPropsEquality) {
- return this.shape?.props === other?.shape?.props
+ return this.shape?.props === other?.shape?.props || this.shape?.meta === other?.shape?.meta
}
}
diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts
index d196aaa16..c7520c675 100644
--- a/packages/editor/src/lib/editor/Editor.ts
+++ b/packages/editor/src/lib/editor/Editor.ts
@@ -65,6 +65,7 @@ import {
isShapeId,
} from '@tldraw/tlschema'
import {
+ JsonObject,
annotateError,
assert,
compact,
@@ -3971,6 +3972,7 @@ export class Editor extends EventEmitter {
bottomIndex && topIndex !== bottomIndex
? getIndexBetween(topIndex, bottomIndex)
: getIndexAbove(topIndex),
+ meta: {},
})
const newCamera = CameraRecordType.create({
@@ -7094,6 +7096,26 @@ export class Editor extends EventEmitter {
return destination
}
+ /**
+ * Get the initial meta value for a shape.
+ *
+ * @example
+ * ```ts
+ * editor.getInitialMetaForShape = (shape) => {
+ * if (shape.type === 'note') {
+ * return { createdBy: myCurrentUser.id }
+ * }
+ * }
+ * ```
+ *
+ * @param shape - The shape to get the initial meta for.
+ *
+ * @public
+ */
+ getInitialMetaForShape(_shape: TLShape): JsonObject {
+ return {}
+ }
+
/**
* Create shapes.
*
@@ -7253,6 +7275,11 @@ export class Editor extends EventEmitter {
shapeRecordsToCreate.push(shapeRecordToCreate)
}
+ // Add meta properties, if any, to the shapes
+ shapeRecordsToCreate.forEach((shape) => {
+ shape.meta = this.getInitialMetaForShape(shape)
+ })
+
this.store.put(shapeRecordsToCreate)
// If we're also selecting the newly created shapes, attempt to select all of them;
@@ -7564,9 +7591,7 @@ export class Editor extends EventEmitter {
switch (k) {
case 'id':
case 'type':
- case 'typeName': {
continue
- }
default: {
if (v !== (prev as any)[k]) {
if (!newRecord) {
@@ -7574,13 +7599,25 @@ export class Editor extends EventEmitter {
}
if (k === 'props') {
- const nextProps = { ...prev.props } as Record
+ // props property
+ const nextProps = { ...prev.props } as JsonObject
for (const [propKey, propValue] of Object.entries(v as object)) {
- if (propValue === undefined) continue
- nextProps[propKey] = propValue
+ if (propValue !== undefined) {
+ nextProps[propKey] = propValue
+ }
}
newRecord!.props = nextProps
+ } else if (k === 'meta') {
+ // meta property
+ const nextMeta = { ...prev.meta } as JsonObject
+ for (const [metaKey, metaValue] of Object.entries(v as object)) {
+ if (metaValue !== undefined) {
+ nextMeta[metaKey] = metaValue
+ }
+ }
+ newRecord!.meta = nextMeta
} else {
+ // base property
;(newRecord as any)[k] = v
}
}
@@ -8802,6 +8839,7 @@ export class Editor extends EventEmitter {
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
? this.store.get(TLPOINTER_ID)?.lastActivityTimestamp ?? Date.now()
: Date.now(),
+ meta: {},
},
])
}
diff --git a/packages/editor/src/lib/editor/managers/ExternalContentManager.ts b/packages/editor/src/lib/editor/managers/ExternalContentManager.ts
index b8e3f3a90..6d4398439 100644
--- a/packages/editor/src/lib/editor/managers/ExternalContentManager.ts
+++ b/packages/editor/src/lib/editor/managers/ExternalContentManager.ts
@@ -500,6 +500,7 @@ export class ExternalContentManager {
mimeType: file.type,
isAnimated: metadata.isAnimated,
},
+ meta: {},
}
resolve(asset)
@@ -546,6 +547,7 @@ export class ExternalContentManager {
image: meta.image,
title: meta.title,
},
+ meta: {},
}
}
}
diff --git a/packages/editor/src/lib/editor/shapes/line/__snapshots__/LineShapeUtil.test.ts.snap b/packages/editor/src/lib/editor/shapes/line/__snapshots__/LineShapeUtil.test.ts.snap
index 7e4a81449..b2e94530f 100644
--- a/packages/editor/src/lib/editor/shapes/line/__snapshots__/LineShapeUtil.test.ts.snap
+++ b/packages/editor/src/lib/editor/shapes/line/__snapshots__/LineShapeUtil.test.ts.snap
@@ -5,6 +5,7 @@ Object {
"id": "shape:line1",
"index": "a1",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "page:id50",
"props": Object {
diff --git a/packages/editor/src/lib/test/commands/__snapshots__/packShapes.test.ts.snap b/packages/editor/src/lib/test/commands/__snapshots__/packShapes.test.ts.snap
index 6e22955ae..439227edb 100644
--- a/packages/editor/src/lib/test/commands/__snapshots__/packShapes.test.ts.snap
+++ b/packages/editor/src/lib/test/commands/__snapshots__/packShapes.test.ts.snap
@@ -6,6 +6,7 @@ Array [
"id": "shape:boxA",
"index": "a1",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
@@ -34,6 +35,7 @@ Array [
"id": "shape:boxB",
"index": "a2",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
@@ -62,6 +64,7 @@ Array [
"id": "shape:boxC",
"index": "a3",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
@@ -95,6 +98,7 @@ Array [
"id": "shape:boxA",
"index": "a1",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
@@ -123,6 +127,7 @@ Array [
"id": "shape:boxB",
"index": "a2",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
@@ -151,6 +156,7 @@ Array [
"id": "shape:boxC",
"index": "a3",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "wahtever",
"props": Object {
diff --git a/packages/editor/src/lib/test/commands/__snapshots__/zoomToFit.test.ts.snap b/packages/editor/src/lib/test/commands/__snapshots__/zoomToFit.test.ts.snap
index bb137928a..5903ed07c 100644
--- a/packages/editor/src/lib/test/commands/__snapshots__/zoomToFit.test.ts.snap
+++ b/packages/editor/src/lib/test/commands/__snapshots__/zoomToFit.test.ts.snap
@@ -3,6 +3,7 @@
exports[`converts correctly: Zoom to Fit Camera 1`] = `
Object {
"id": "static",
+ "meta": Object {},
"typeName": "camera",
"x": 330.435496777593,
"y": 22.261531457640388,
diff --git a/packages/editor/src/lib/test/commands/getInitialMetaForShape.test.ts b/packages/editor/src/lib/test/commands/getInitialMetaForShape.test.ts
new file mode 100644
index 000000000..b29d9bfb0
--- /dev/null
+++ b/packages/editor/src/lib/test/commands/getInitialMetaForShape.test.ts
@@ -0,0 +1,19 @@
+import { createShapeId } from '@tldraw/tlschema'
+import { TestEditor } from '../TestEditor'
+
+let editor: TestEditor
+
+beforeEach(() => {
+ editor = new TestEditor()
+})
+
+it('Sets shape meta by default to an empty object', () => {
+ editor.createShapes([{ id: createShapeId(), type: 'geo' }], true)
+ expect(editor.onlySelectedShape!.meta).toStrictEqual({})
+})
+
+it('Sets shape meta', () => {
+ editor.getInitialMetaForShape = (shape) => ({ firstThreeCharactersOfId: shape.id.slice(0, 3) })
+ editor.createShapes([{ id: createShapeId(), type: 'geo' }], true)
+ expect(editor.onlySelectedShape!.meta).toStrictEqual({ firstThreeCharactersOfId: 'sha' })
+})
diff --git a/packages/editor/src/lib/test/tools/__snapshots__/resizing.test.ts.snap b/packages/editor/src/lib/test/tools/__snapshots__/resizing.test.ts.snap
index b16aaadaf..aac67b5b6 100644
--- a/packages/editor/src/lib/test/tools/__snapshots__/resizing.test.ts.snap
+++ b/packages/editor/src/lib/test/tools/__snapshots__/resizing.test.ts.snap
@@ -5,6 +5,7 @@ Object {
"id": "shape:lineA",
"index": "a3",
"isLocked": false,
+ "meta": Object {},
"opacity": 1,
"parentId": "shape:boxA",
"props": Object {
diff --git a/packages/editor/src/lib/utils/assets.ts b/packages/editor/src/lib/utils/assets.ts
index af4ae45e7..4425ab3b5 100644
--- a/packages/editor/src/lib/utils/assets.ts
+++ b/packages/editor/src/lib/utils/assets.ts
@@ -170,6 +170,7 @@ export async function getMediaAssetFromFile(file: File): Promise {
mimeType: file.type,
isAnimated: metadata.isAnimated,
},
+ meta: {},
}
resolve(asset)
diff --git a/packages/file-format/src/lib/buildFromV1Document.ts b/packages/file-format/src/lib/buildFromV1Document.ts
index b4590bf21..b790883e1 100644
--- a/packages/file-format/src/lib/buildFromV1Document.ts
+++ b/packages/file-format/src/lib/buildFromV1Document.ts
@@ -70,6 +70,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
mimeType: null,
src: v1Asset.src,
},
+ meta: {},
}
editor.createAssets([placeholderAsset])
tryMigrateAsset(editor, placeholderAsset)
@@ -92,6 +93,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
mimeType: null,
src: v1Asset.src,
},
+ meta: {},
},
])
}
diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md
index 495c6cf5b..da2871c06 100644
--- a/packages/tlschema/api-report.md
+++ b/packages/tlschema/api-report.md
@@ -6,6 +6,7 @@
import { BaseRecord } from '@tldraw/store';
import { Expand } from '@tldraw/utils';
+import { JsonObject } from '@tldraw/utils';
import { Migrations } from '@tldraw/store';
import { RecordId } from '@tldraw/store';
import { RecordType } from '@tldraw/store';
@@ -113,11 +114,12 @@ export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser
export function CLIENT_FIXUP_SCRIPT(persistedStore: SerializedStore): SerializedStore;
// @public
-export function createAssetValidator(type: Type, props: T.Validator): T.ObjectValidator<{
+export function createAssetValidator(type: Type, props: T.Validator): T.ObjectValidator<{
id: TLAssetId;
typeName: 'asset';
type: Type;
props: Props;
+ meta: JsonObject;
}>;
// @public (undocumented)
@@ -131,21 +133,11 @@ export const createPresenceStateDerivation: ($user: Signal<{
export function createShapeId(id?: string): TLShapeId;
// @public (undocumented)
-export function createShapeValidator(type: Type, props?: {
+export function createShapeValidator(type: Type, props?: {
[K in keyof Props]: T.Validatable;
-}): T.ObjectValidator<{
- id: TLShapeId;
- typeName: "shape";
- x: number;
- y: number;
- rotation: number;
- index: string;
- parentId: TLParentId;
- type: Type;
- isLocked: boolean;
- opacity: number;
- props: Props | Record;
-}>;
+}, meta?: {
+ [K in keyof Meta]: T.Validatable ;
+}): T.ObjectValidator>;
// @public
export function createTLSchema({ shapes }: {
@@ -710,6 +702,9 @@ export type SchemaShapeInfo = {
props?: Record any;
}>;
+ meta?: Record any;
+ }>;
};
// @internal (undocumented)
@@ -786,7 +781,8 @@ export type TLAssetPartial = T extends T ? {
id: TLAssetId;
type: T['type'];
props?: Partial;
-} & Partial> : never;
+ meta?: Partial;
+} & Partial> : never;
// @public (undocumented)
export type TLAssetShape = Extract extends BaseRecord<'asset', TLAssetId> {
+ // (undocumented)
+ meta: JsonObject;
// (undocumented)
props: Props;
// (undocumented)
@@ -810,6 +808,8 @@ export interface TLBaseShape extends
// (undocumented)
isLocked: boolean;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
opacity: TLOpacityType;
// (undocumented)
parentId: TLParentId;
@@ -838,6 +838,8 @@ export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>;
// @public
export interface TLCamera extends BaseRecord<'camera', TLCameraId> {
+ // (undocumented)
+ meta: JsonObject;
// (undocumented)
x: number;
// (undocumented)
@@ -912,6 +914,8 @@ export interface TLDocument extends BaseRecord<'document', RecordId>
// (undocumented)
gridSize: number;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
name: string;
}
@@ -1010,6 +1014,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
// (undocumented)
isToolLocked: boolean;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
opacityForNextShape: TLOpacityType;
// (undocumented)
screenBounds: Box2dModel;
@@ -1042,6 +1048,8 @@ export interface TLInstancePageState extends BaseRecord<'instance_page_state', T
// (undocumented)
hoveredId: null | TLShapeId;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
pageId: RecordId;
// (undocumented)
selectedIds: TLShapeId[];
@@ -1075,6 +1083,8 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
// (undocumented)
lastActivityTimestamp: number;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
screenBounds: Box2dModel;
// (undocumented)
scribble: null | TLScribble;
@@ -1103,6 +1113,8 @@ export interface TLPage extends BaseRecord<'page', TLPageId> {
// (undocumented)
index: string;
// (undocumented)
+ meta: JsonObject;
+ // (undocumented)
name: string;
}
@@ -1145,7 +1157,8 @@ export type TLShapePartial = T extends T ? {
id: TLShapeId;
type: T['type'];
props?: Partial;
-} & Partial> : never;
+ meta?: Partial;
+} & Partial> : never;
// @public (undocumented)
export type TLShapeProp = keyof TLShapeProps;
diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts
index f1ee38379..87c79c74e 100644
--- a/packages/tlschema/src/TLStore.ts
+++ b/packages/tlschema/src/TLStore.ts
@@ -74,7 +74,7 @@ export const onValidationFailure: StoreSchemaOptions<
}
function getDefaultPages() {
- return [PageRecordType.create({ name: 'Page 1', index: 'a1' })]
+ return [PageRecordType.create({ name: 'Page 1', index: 'a1', meta: {} })]
}
/** @internal */
diff --git a/packages/tlschema/src/assets/TLBaseAsset.ts b/packages/tlschema/src/assets/TLBaseAsset.ts
index 2cdc46cf7..9360b7ce3 100644
--- a/packages/tlschema/src/assets/TLBaseAsset.ts
+++ b/packages/tlschema/src/assets/TLBaseAsset.ts
@@ -1,4 +1,5 @@
import { BaseRecord } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
import { TLAssetId } from '../records/TLAsset'
@@ -7,6 +8,7 @@ import { TLAssetId } from '../records/TLAsset'
export interface TLBaseAsset extends BaseRecord<'asset', TLAssetId> {
type: Type
props: Props
+ meta: JsonObject
}
/**
@@ -22,7 +24,7 @@ export const assetIdValidator = idValidator('asset')
* @param props - The validator for the asset's props
*
* @public */
-export function createAssetValidator(
+export function createAssetValidator(
type: Type,
props: T.Validator
): T.ObjectValidator<{
@@ -30,11 +32,13 @@ export function createAssetValidator(
typeName: 'asset'
type: Type
props: Props
+ meta: JsonObject
}> {
return T.object({
id: assetIdValidator,
typeName: T.literal('asset'),
type: T.literal(type),
props,
+ meta: T.jsonValue as T.ObjectValidator,
})
}
diff --git a/packages/tlschema/src/createPresenceStateDerivation.ts b/packages/tlschema/src/createPresenceStateDerivation.ts
index 6c174456a..c1e93a2bc 100644
--- a/packages/tlschema/src/createPresenceStateDerivation.ts
+++ b/packages/tlschema/src/createPresenceStateDerivation.ts
@@ -47,6 +47,7 @@ export const createPresenceStateDerivation =
lastActivityTimestamp: pointer.lastActivityTimestamp,
screenBounds: instance.screenBounds,
chatMessage: instance.chatMessage,
+ meta: {},
})
})
}
diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts
index 17e544b5d..7152ec706 100644
--- a/packages/tlschema/src/createTLSchema.ts
+++ b/packages/tlschema/src/createTLSchema.ts
@@ -18,6 +18,7 @@ import { StyleProp } from './styles/StyleProp'
export type SchemaShapeInfo = {
migrations?: Migrations
props?: Record any }>
+ meta?: Record any }>
}
/** @public */
diff --git a/packages/tlschema/src/migrations.test.ts b/packages/tlschema/src/migrations.test.ts
index 88839a10b..8c5ec752e 100644
--- a/packages/tlschema/src/migrations.test.ts
+++ b/packages/tlschema/src/migrations.test.ts
@@ -2,11 +2,15 @@ import { Migrations, Store, createRecordType } from '@tldraw/store'
import fs from 'fs'
import { imageAssetMigrations } from './assets/TLImageAsset'
import { videoAssetMigrations } from './assets/TLVideoAsset'
-import { documentMigrations } from './records/TLDocument'
-import { instanceMigrations, instanceTypeVersions } from './records/TLInstance'
+import { assetMigrations, assetVersions } from './records/TLAsset'
+import { cameraMigrations, cameraVersions } from './records/TLCamera'
+import { documentMigrations, documentVersions } from './records/TLDocument'
+import { instanceMigrations, instanceVersions } from './records/TLInstance'
+import { pageMigrations, pageVersions } from './records/TLPage'
import { instancePageStateMigrations, instancePageStateVersions } from './records/TLPageState'
+import { pointerMigrations, pointerVersions } from './records/TLPointer'
import { instancePresenceMigrations, instancePresenceVersions } from './records/TLPresence'
-import { TLShape, rootShapeMigrations, Versions as rootShapeVersions } from './records/TLShape'
+import { TLShape, rootShapeMigrations, rootShapeVersions } from './records/TLShape'
import { arrowShapeMigrations } from './shapes/TLArrowShape'
import { bookmarkShapeMigrations } from './shapes/TLBookmarkShape'
import { drawShapeMigrations } from './shapes/TLDrawShape'
@@ -896,7 +900,7 @@ describe('user config refactor', () => {
})
test('removes userId from the instance state', () => {
- const { up, down } = instanceMigrations.migrators[instanceTypeVersions.RemoveUserId]
+ const { up, down } = instanceMigrations.migrators[instanceVersions.RemoveUserId]
const prev = {
id: 'instance:123',
@@ -924,8 +928,7 @@ describe('user config refactor', () => {
describe('making instance state independent', () => {
it('adds isPenMode and isGridMode to instance state', () => {
- const { up, down } =
- instanceMigrations.migrators[instanceTypeVersions.AddIsPenModeAndIsGridMode]
+ const { up, down } = instanceMigrations.migrators[instanceVersions.AddIsPenModeAndIsGridMode]
const prev = {
id: 'instance:123',
@@ -1081,7 +1084,7 @@ describe('hoist opacity', () => {
})
test('hoists opacity from propsForNextShape', () => {
- const { up, down } = instanceMigrations.migrators[instanceTypeVersions.HoistOpacity]
+ const { up, down } = instanceMigrations.migrators[instanceVersions.HoistOpacity]
const before = {
isToolLocked: true,
propsForNextShape: {
@@ -1111,7 +1114,7 @@ describe('hoist opacity', () => {
})
describe('Adds highlightedUserIds to instance', () => {
- const { up, down } = instanceMigrations.migrators[instanceTypeVersions.AddHighlightedUserIds]
+ const { up, down } = instanceMigrations.migrators[instanceVersions.AddHighlightedUserIds]
test('up works as expected', () => {
expect(up({})).toEqual({ highlightedUserIds: [] })
@@ -1194,9 +1197,7 @@ describe('Removes overridePermissions from embed', () => {
describe('propsForNextShape -> stylesForNextShape', () => {
test('deletes propsForNextShape and adds stylesForNextShape without trying to bring across contents', () => {
const { up, down } =
- instanceMigrations.migrators[
- instanceTypeVersions.ReplacePropsForNextShapeWithStylesForNextShape
- ]
+ instanceMigrations.migrators[instanceVersions.ReplacePropsForNextShapeWithStylesForNextShape]
const beforeUp = {
isToolLocked: true,
propsForNextShape: {
@@ -1232,6 +1233,30 @@ describe('propsForNextShape -> stylesForNextShape', () => {
})
})
+describe('adds meta ', () => {
+ const metaMigrations = [
+ assetMigrations.migrators[assetVersions.AddMeta],
+ cameraMigrations.migrators[cameraVersions.AddMeta],
+ documentMigrations.migrators[documentVersions.AddMeta],
+ instanceMigrations.migrators[instanceVersions.AddMeta],
+ instancePageStateMigrations.migrators[instancePageStateVersions.AddMeta],
+ instancePresenceMigrations.migrators[instancePresenceVersions.AddMeta],
+ pageMigrations.migrators[pageVersions.AddMeta],
+ pointerMigrations.migrators[pointerVersions.AddMeta],
+ rootShapeMigrations.migrators[rootShapeVersions.AddMeta],
+ ]
+
+ for (const { up, down } of metaMigrations) {
+ test('up works as expected', () => {
+ expect(up({})).toStrictEqual({ meta: {} })
+ })
+
+ test('down works as expected', () => {
+ expect(down({ meta: {} })).toStrictEqual({})
+ })
+ }
+})
+
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
for (const migrator of allMigrators) {
diff --git a/packages/tlschema/src/records/TLAsset.ts b/packages/tlschema/src/records/TLAsset.ts
index 2eddd8db5..8d952c9f9 100644
--- a/packages/tlschema/src/records/TLAsset.ts
+++ b/packages/tlschema/src/records/TLAsset.ts
@@ -23,6 +23,11 @@ export const assetValidator: T.Validator = T.model(
})
)
+/** @internal */
+export const assetVersions = {
+ AddMeta: 1,
+}
+
/** @internal */
export const assetMigrations = defineMigrations({
subTypeKey: 'type',
@@ -31,6 +36,22 @@ export const assetMigrations = defineMigrations({
video: videoAssetMigrations,
bookmark: bookmarkAssetMigrations,
},
+ currentVersion: assetVersions.AddMeta,
+ migrators: {
+ [assetVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
+ },
})
/** @public */
@@ -39,7 +60,8 @@ export type TLAssetPartial = T extends T
id: TLAssetId
type: T['type']
props?: Partial
- } & Partial>
+ meta?: Partial
+ } & Partial>
: never
/** @public */
@@ -47,7 +69,9 @@ export const AssetRecordType = createRecordType('asset', {
migrations: assetMigrations,
validator: assetValidator,
scope: 'document',
-})
+}).withDefaultProperties(() => ({
+ meta: {},
+}))
/** @public */
export type TLAssetId = RecordId>
diff --git a/packages/tlschema/src/records/TLCamera.ts b/packages/tlschema/src/records/TLCamera.ts
index 15796312b..ae068ca2d 100644
--- a/packages/tlschema/src/records/TLCamera.ts
+++ b/packages/tlschema/src/records/TLCamera.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
@@ -11,6 +12,7 @@ export interface TLCamera extends BaseRecord<'camera', TLCameraId> {
x: number
y: number
z: number
+ meta: JsonObject
}
/**
@@ -28,11 +30,34 @@ export const cameraValidator: T.Validator = T.model(
x: T.number,
y: T.number,
z: T.number,
+ meta: T.jsonValue as T.ObjectValidator,
})
)
/** @internal */
-export const cameraMigrations = defineMigrations({})
+export const cameraVersions = {
+ AddMeta: 1,
+}
+
+/** @internal */
+export const cameraMigrations = defineMigrations({
+ currentVersion: cameraVersions.AddMeta,
+ migrators: {
+ [cameraVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
+ },
+})
/** @public */
export const CameraRecordType = createRecordType('camera', {
@@ -44,5 +69,6 @@ export const CameraRecordType = createRecordType('camera', {
x: 0,
y: 0,
z: 1,
+ meta: {},
})
)
diff --git a/packages/tlschema/src/records/TLDocument.ts b/packages/tlschema/src/records/TLDocument.ts
index 596fdb605..3ee1e3e5a 100644
--- a/packages/tlschema/src/records/TLDocument.ts
+++ b/packages/tlschema/src/records/TLDocument.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
/**
@@ -9,6 +10,7 @@ import { T } from '@tldraw/validate'
export interface TLDocument extends BaseRecord<'document', RecordId> {
gridSize: number
name: string
+ meta: JsonObject
}
/** @internal */
@@ -19,18 +21,21 @@ export const documentValidator: T.Validator = T.model(
id: T.literal('document:document' as RecordId),
gridSize: T.number,
name: T.string,
+ meta: T.jsonValue as T.ObjectValidator,
})
)
-const Versions = {
+/** @internal */
+export const documentVersions = {
AddName: 1,
+ AddMeta: 2,
} as const
/** @internal */
export const documentMigrations = defineMigrations({
- currentVersion: Versions.AddName,
+ currentVersion: documentVersions.AddMeta,
migrators: {
- [Versions.AddName]: {
+ [documentVersions.AddName]: {
up: (document: TLDocument) => {
return { ...document, name: '' }
},
@@ -38,6 +43,19 @@ export const documentMigrations = defineMigrations({
return document
},
},
+ [documentVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
},
})
@@ -50,6 +68,7 @@ export const DocumentRecordType = createRecordType('document', {
(): Omit => ({
gridSize: 10,
name: '',
+ meta: {},
})
)
diff --git a/packages/tlschema/src/records/TLInstance.ts b/packages/tlschema/src/records/TLInstance.ts
index f1e194ad3..91b06f5c7 100644
--- a/packages/tlschema/src/records/TLInstance.ts
+++ b/packages/tlschema/src/records/TLInstance.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { Box2dModel, box2dModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
@@ -34,6 +35,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
isChatting: boolean
isPenMode: boolean
isGridMode: boolean
+ meta: JsonObject
}
/** @public */
@@ -71,6 +73,7 @@ export function createInstanceRecordType(stylesById: Map,
})
)
@@ -101,11 +104,13 @@ export function createInstanceRecordType(stylesById: Map {
return { ...instance, exportBackground: true }
},
@@ -138,7 +142,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.RemoveDialog]: {
+ [instanceVersions.RemoveDialog]: {
up: ({ dialog: _, ...instance }: any) => {
return instance
},
@@ -146,7 +150,7 @@ export const instanceMigrations = defineMigrations({
return { ...instance, dialog: null }
},
},
- [Versions.AddToolLockMode]: {
+ [instanceVersions.AddToolLockMode]: {
up: (instance: TLInstance) => {
return { ...instance, isToolLocked: false }
},
@@ -154,7 +158,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.RemoveExtraPropsForNextShape]: {
+ [instanceVersions.RemoveExtraPropsForNextShape]: {
up: ({ propsForNextShape, ...instance }: any) => {
return {
...instance,
@@ -184,7 +188,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.AddLabelColor]: {
+ [instanceVersions.AddLabelColor]: {
up: ({ propsForNextShape, ...instance }: any) => {
return {
...instance,
@@ -204,7 +208,7 @@ export const instanceMigrations = defineMigrations({
}
},
},
- [Versions.AddFollowingUserId]: {
+ [instanceVersions.AddFollowingUserId]: {
up: (instance: TLInstance) => {
return { ...instance, followingUserId: null }
},
@@ -212,7 +216,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.RemoveAlignJustify]: {
+ [instanceVersions.RemoveAlignJustify]: {
up: (instance: any) => {
let newAlign = instance.propsForNextShape.align
if (newAlign === 'justify') {
@@ -231,7 +235,7 @@ export const instanceMigrations = defineMigrations({
return { ...instance }
},
},
- [Versions.AddZoom]: {
+ [instanceVersions.AddZoom]: {
up: (instance: TLInstance) => {
return { ...instance, zoomBrush: null }
},
@@ -239,7 +243,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.AddVerticalAlign]: {
+ [instanceVersions.AddVerticalAlign]: {
up: (instance) => {
return {
...instance,
@@ -257,7 +261,7 @@ export const instanceMigrations = defineMigrations({
}
},
},
- [Versions.AddScribbleDelay]: {
+ [instanceVersions.AddScribbleDelay]: {
up: (instance) => {
if (instance.scribble !== null) {
return { ...instance, scribble: { ...instance.scribble, delay: 0 } }
@@ -272,7 +276,7 @@ export const instanceMigrations = defineMigrations({
return { ...instance }
},
},
- [Versions.RemoveUserId]: {
+ [instanceVersions.RemoveUserId]: {
up: ({ userId: _, ...instance }: any) => {
return instance
},
@@ -280,7 +284,7 @@ export const instanceMigrations = defineMigrations({
return { ...instance, userId: 'user:none' }
},
},
- [Versions.AddIsPenModeAndIsGridMode]: {
+ [instanceVersions.AddIsPenModeAndIsGridMode]: {
up: (instance: TLInstance) => {
return { ...instance, isPenMode: false, isGridMode: false }
},
@@ -288,7 +292,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.HoistOpacity]: {
+ [instanceVersions.HoistOpacity]: {
up: ({ propsForNextShape: { opacity, ...propsForNextShape }, ...instance }: any) => {
return { ...instance, opacityForNextShape: Number(opacity ?? '1'), propsForNextShape }
},
@@ -311,7 +315,7 @@ export const instanceMigrations = defineMigrations({
}
},
},
- [Versions.AddChat]: {
+ [instanceVersions.AddChat]: {
up: (instance: TLInstance) => {
return { ...instance, chatMessage: '', isChatting: false }
},
@@ -319,7 +323,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.AddHighlightedUserIds]: {
+ [instanceVersions.AddHighlightedUserIds]: {
up: (instance: TLInstance) => {
return { ...instance, highlightedUserIds: [] }
},
@@ -327,7 +331,7 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
- [Versions.ReplacePropsForNextShapeWithStylesForNextShape]: {
+ [instanceVersions.ReplacePropsForNextShapeWithStylesForNextShape]: {
up: ({ propsForNextShape: _, ...instance }) => {
return { ...instance, stylesForNextShape: {} }
},
@@ -352,6 +356,19 @@ export const instanceMigrations = defineMigrations({
}
},
},
+ [instanceVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
},
})
diff --git a/packages/tlschema/src/records/TLPage.ts b/packages/tlschema/src/records/TLPage.ts
index b04c70eb2..457495563 100644
--- a/packages/tlschema/src/records/TLPage.ts
+++ b/packages/tlschema/src/records/TLPage.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
@@ -10,6 +11,7 @@ import { idValidator } from '../misc/id-validator'
export interface TLPage extends BaseRecord<'page', TLPageId> {
name: string
index: string
+ meta: JsonObject
}
/** @public */
@@ -26,17 +28,43 @@ export const pageValidator: T.Validator = T.model(
id: pageIdValidator,
name: T.string,
index: T.string,
+ meta: T.jsonValue as T.ObjectValidator,
})
)
+
/** @internal */
-export const pageMigrations = defineMigrations({})
+export const pageVersions = {
+ AddMeta: 1,
+}
+
+/** @internal */
+export const pageMigrations = defineMigrations({
+ currentVersion: pageVersions.AddMeta,
+ migrators: {
+ [pageVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
+ },
+})
/** @public */
export const PageRecordType = createRecordType('page', {
validator: pageValidator,
migrations: pageMigrations,
scope: 'document',
-})
+}).withDefaultProperties(() => ({
+ meta: {},
+}))
/** @public */
export function isPageId(id: string): id is TLPageId {
diff --git a/packages/tlschema/src/records/TLPageState.ts b/packages/tlschema/src/records/TLPageState.ts
index 93062a1db..a633df5c9 100644
--- a/packages/tlschema/src/records/TLPageState.ts
+++ b/packages/tlschema/src/records/TLPageState.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
import { shapeIdValidator } from '../shapes/TLBaseShape'
@@ -24,6 +25,7 @@ export interface TLInstancePageState
editingId: TLShapeId | null
croppingId: TLShapeId | null
focusLayerId: TLShapeId | null
+ meta: JsonObject
}
/** @internal */
@@ -40,22 +42,22 @@ export const instancePageStateValidator: T.Validator = T.mo
editingId: shapeIdValidator.nullable(),
croppingId: shapeIdValidator.nullable(),
focusLayerId: shapeIdValidator.nullable(),
+ meta: T.jsonValue as T.ObjectValidator,
})
)
-const Versions = {
+/** @internal */
+export const instancePageStateVersions = {
AddCroppingId: 1,
RemoveInstanceIdAndCameraId: 2,
+ AddMeta: 3,
} as const
-/** @internal */
-export { Versions as instancePageStateVersions }
-
/** @public */
export const instancePageStateMigrations = defineMigrations({
- currentVersion: Versions.RemoveInstanceIdAndCameraId,
+ currentVersion: instancePageStateVersions.AddMeta,
migrators: {
- [Versions.AddCroppingId]: {
+ [instancePageStateVersions.AddCroppingId]: {
up(instance) {
return { ...instance, croppingId: null }
},
@@ -63,7 +65,7 @@ export const instancePageStateMigrations = defineMigrations({
return instance
},
},
- [Versions.RemoveInstanceIdAndCameraId]: {
+ [instancePageStateVersions.RemoveInstanceIdAndCameraId]: {
up({ instanceId: _, cameraId: __, ...instance }) {
return instance
},
@@ -76,6 +78,19 @@ export const instancePageStateMigrations = defineMigrations({
}
},
},
+ [instancePageStateVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
},
})
@@ -96,6 +111,7 @@ export const InstancePageStateRecordType = createRecordType
erasingIds: [],
hintingIds: [],
focusLayerId: null,
+ meta: {},
})
)
diff --git a/packages/tlschema/src/records/TLPointer.ts b/packages/tlschema/src/records/TLPointer.ts
index b76919d06..ddb740771 100644
--- a/packages/tlschema/src/records/TLPointer.ts
+++ b/packages/tlschema/src/records/TLPointer.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
@@ -11,6 +12,7 @@ export interface TLPointer extends BaseRecord<'pointer', TLPointerId> {
x: number
y: number
lastActivityTimestamp: number
+ meta: JsonObject
}
/** @public */
@@ -25,11 +27,34 @@ export const pointerValidator: T.Validator = T.model(
x: T.number,
y: T.number,
lastActivityTimestamp: T.number,
+ meta: T.jsonValue as T.ObjectValidator,
})
)
/** @internal */
-export const pointerMigrations = defineMigrations({})
+export const pointerVersions = {
+ AddMeta: 1,
+}
+
+/** @internal */
+export const pointerMigrations = defineMigrations({
+ currentVersion: pointerVersions.AddMeta,
+ migrators: {
+ [pointerVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
+ },
+})
/** @public */
export const PointerRecordType = createRecordType('pointer', {
@@ -41,6 +66,7 @@ export const PointerRecordType = createRecordType('pointer', {
x: 0,
y: 0,
lastActivityTimestamp: 0,
+ meta: {},
})
)
diff --git a/packages/tlschema/src/records/TLPresence.ts b/packages/tlschema/src/records/TLPresence.ts
index 67f0246e2..297bd5f6a 100644
--- a/packages/tlschema/src/records/TLPresence.ts
+++ b/packages/tlschema/src/records/TLPresence.ts
@@ -1,4 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
+import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { Box2dModel, box2dModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
@@ -28,6 +29,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
rotation: number
}
chatMessage: string
+ meta: JsonObject
}
/** @public */
@@ -61,21 +63,22 @@ export const instancePresenceValidator: T.Validator = T.mode
brush: box2dModelValidator.nullable(),
scribble: scribbleValidator.nullable(),
chatMessage: T.string,
+ meta: T.jsonValue as T.ObjectValidator,
})
)
-const Versions = {
+/** @internal */
+export const instancePresenceVersions = {
AddScribbleDelay: 1,
RemoveInstanceId: 2,
AddChatMessage: 3,
+ AddMeta: 4,
} as const
-export { Versions as instancePresenceVersions }
-
export const instancePresenceMigrations = defineMigrations({
- currentVersion: Versions.AddChatMessage,
+ currentVersion: instancePresenceVersions.AddMeta,
migrators: {
- [Versions.AddScribbleDelay]: {
+ [instancePresenceVersions.AddScribbleDelay]: {
up: (instance) => {
if (instance.scribble !== null) {
return { ...instance, scribble: { ...instance.scribble, delay: 0 } }
@@ -90,7 +93,7 @@ export const instancePresenceMigrations = defineMigrations({
return { ...instance }
},
},
- [Versions.RemoveInstanceId]: {
+ [instancePresenceVersions.RemoveInstanceId]: {
up: ({ instanceId: _, ...instance }) => {
return instance
},
@@ -98,7 +101,7 @@ export const instancePresenceMigrations = defineMigrations({
return { ...instance, instanceId: TLINSTANCE_ID }
},
},
- [Versions.AddChatMessage]: {
+ [instancePresenceVersions.AddChatMessage]: {
up: (instance) => {
return { ...instance, chatMessage: '' }
},
@@ -106,6 +109,19 @@ export const instancePresenceMigrations = defineMigrations({
return instance
},
},
+ [instancePresenceVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
},
})
@@ -142,4 +158,5 @@ export const InstancePresenceRecordType = createRecordType(
brush: null,
scribble: null,
chatMessage: '',
+ meta: {},
}))
diff --git a/packages/tlschema/src/records/TLShape.ts b/packages/tlschema/src/records/TLShape.ts
index 0fd98e57b..1fba19efc 100644
--- a/packages/tlschema/src/records/TLShape.ts
+++ b/packages/tlschema/src/records/TLShape.ts
@@ -59,7 +59,8 @@ export type TLShapePartial = T extends T
id: TLShapeId
type: T['type']
props?: Partial
- } & Partial>
+ meta?: Partial
+ } & Partial>
: never
/** @public */
@@ -81,16 +82,18 @@ export type TLShapeProp = keyof TLShapeProps
/** @public */
export type TLParentId = TLPageId | TLShapeId
-export const Versions = {
+/** @internal */
+export const rootShapeVersions = {
AddIsLocked: 1,
HoistOpacity: 2,
+ AddMeta: 3,
} as const
/** @internal */
export const rootShapeMigrations = defineMigrations({
- currentVersion: Versions.HoistOpacity,
+ currentVersion: rootShapeVersions.AddMeta,
migrators: {
- [Versions.AddIsLocked]: {
+ [rootShapeVersions.AddIsLocked]: {
up: (record) => {
return {
...record,
@@ -104,7 +107,7 @@ export const rootShapeMigrations = defineMigrations({
}
},
},
- [Versions.HoistOpacity]: {
+ [rootShapeVersions.HoistOpacity]: {
up: ({ props: { opacity, ...props }, ...record }) => {
return {
...record,
@@ -131,6 +134,19 @@ export const rootShapeMigrations = defineMigrations({
}
},
},
+ [rootShapeVersions.AddMeta]: {
+ up: (record) => {
+ return {
+ ...record,
+ meta: {},
+ }
+ },
+ down: ({ meta: _, ...record }) => {
+ return {
+ ...record,
+ }
+ },
+ },
},
})
@@ -182,7 +198,9 @@ export function createShapeRecordType(shapes: Record) {
'shape',
T.union(
'type',
- mapObjectMapValues(shapes, (type, { props }) => createShapeValidator(type, props))
+ mapObjectMapValues(shapes, (type, { props, meta }) =>
+ createShapeValidator(type, props, meta)
+ )
)
),
}).withDefaultProperties(() => ({
@@ -191,5 +209,6 @@ export function createShapeRecordType(shapes: Record) {
rotation: 0,
isLocked: false,
opacity: 1,
+ meta: {},
}))
}
diff --git a/packages/tlschema/src/shapes/TLBaseShape.ts b/packages/tlschema/src/shapes/TLBaseShape.ts
index 22df36eb4..6ec7bfd81 100644
--- a/packages/tlschema/src/shapes/TLBaseShape.ts
+++ b/packages/tlschema/src/shapes/TLBaseShape.ts
@@ -1,5 +1,5 @@
import { BaseRecord } from '@tldraw/store'
-import { Expand } from '@tldraw/utils'
+import { Expand, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
import { idValidator } from '../misc/id-validator'
@@ -17,6 +17,7 @@ export interface TLBaseShape
isLocked: boolean
opacity: TLOpacityType
props: Props
+ meta: JsonObject
}
/** @public */
@@ -31,11 +32,16 @@ export const parentIdValidator = T.string.refine((id) => {
export const shapeIdValidator = idValidator('shape')
/** @public */
-export function createShapeValidator(
+export function createShapeValidator<
+ Type extends string,
+ Props extends JsonObject,
+ Meta extends JsonObject
+>(
type: Type,
- props?: { [K in keyof Props]: T.Validatable }
+ props?: { [K in keyof Props]: T.Validatable },
+ meta?: { [K in keyof Meta]: T.Validatable }
) {
- return T.object({
+ return T.object>({
id: shapeIdValidator,
typeName: T.literal('shape'),
x: T.number,
@@ -46,7 +52,8 @@ export function createShapeValidator(
type: T.literal(type),
isLocked: T.boolean,
opacity: opacityValidator,
- props: props ? T.object(props) : T.unknownObject,
+ props: props ? T.object(props) : (T.jsonValue as T.ObjectValidator),
+ meta: meta ? T.object(meta) : (T.jsonValue as T.ObjectValidator ),
})
}
diff --git a/packages/ui/src/lib/hooks/clipboard/pasteExcalidrawContent.ts b/packages/ui/src/lib/hooks/clipboard/pasteExcalidrawContent.ts
index bc2645182..867559ab7 100644
--- a/packages/ui/src/lib/hooks/clipboard/pasteExcalidrawContent.ts
+++ b/packages/ui/src/lib/hooks/clipboard/pasteExcalidrawContent.ts
@@ -80,6 +80,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
rotation: 0,
isLocked: element.locked,
opacity: getOpacity(element.opacity),
+ meta: {},
} as const
if (element.angle !== 0) {
@@ -298,6 +299,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
mimeType: file.mimeType,
src: file.dataURL,
},
+ meta: {},
})
tldrawContent.shapes.push({
diff --git a/packages/utils/api-report.md b/packages/utils/api-report.md
index e63dc7824..f066408f1 100644
--- a/packages/utils/api-report.md
+++ b/packages/utils/api-report.md
@@ -79,6 +79,20 @@ export function isNonNull(value: T): value is typeof value extends null ? nev
// @public
export function isNonNullish(value: T): value is typeof value extends undefined ? never : typeof value extends null ? never : T;
+// @public (undocumented)
+export type JsonArray = JsonValue[];
+
+// @public (undocumented)
+export type JsonObject = {
+ [key: string]: JsonValue | undefined;
+};
+
+// @public (undocumented)
+export type JsonPrimitive = boolean | null | number | string;
+
+// @public (undocumented)
+export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
+
// @internal (undocumented)
export function last(arr: readonly T[]): T | undefined;
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index c24928273..d905452e9 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -13,6 +13,7 @@ export { annotateError, getErrorAnnotations } from './lib/error'
export { noop, omitFromStackTrace, throttle } from './lib/function'
export { getHashForObject, getHashForString, lns } from './lib/hash'
export { getFirstFromIterable } from './lib/iterable'
+export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
export { lerp, modulate, rng } from './lib/number'
export {
deepCopy,
diff --git a/packages/utils/src/lib/json-value.ts b/packages/utils/src/lib/json-value.ts
new file mode 100644
index 000000000..598c80cfe
--- /dev/null
+++ b/packages/utils/src/lib/json-value.ts
@@ -0,0 +1,8 @@
+/** @public */
+export type JsonValue = JsonPrimitive | JsonArray | JsonObject
+/** @public */
+export type JsonPrimitive = boolean | null | string | number
+/** @public */
+export type JsonArray = JsonValue[]
+/** @public */
+export type JsonObject = { [key: string]: JsonValue | undefined }
diff --git a/packages/validate/api-report.md b/packages/validate/api-report.md
index 341f449db..9b06f4cc7 100644
--- a/packages/validate/api-report.md
+++ b/packages/validate/api-report.md
@@ -4,6 +4,8 @@
```ts
+import { JsonValue } from '@tldraw/utils';
+
// @public
const any: Validator;
@@ -45,6 +47,12 @@ class DictValidator extends Validator;
+// @public
+function jsonDict(): DictValidator;
+
+// @public
+const jsonValue: Validator;
+
// @public
function literal(expectedValue: T): Validator;
@@ -109,6 +117,7 @@ declare namespace T {
literal,
arrayOf,
object,
+ jsonDict,
dict,
union,
model,
@@ -137,7 +146,8 @@ declare namespace T {
boolean,
bigint,
array,
- unknownObject
+ unknownObject,
+ jsonValue
}
}
export { T }
diff --git a/packages/validate/src/lib/validation.ts b/packages/validate/src/lib/validation.ts
index 82465fcda..9e9ea59eb 100644
--- a/packages/validate/src/lib/validation.ts
+++ b/packages/validate/src/lib/validation.ts
@@ -1,4 +1,4 @@
-import { exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/utils'
+import { JsonValue, exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/utils'
/** @public */
export type ValidatorFn = (value: unknown) => T
@@ -466,6 +466,49 @@ export function object(config: {
return new ObjectValidator(config)
}
+function isValidJson(value: any): value is JsonValue {
+ if (
+ value === null ||
+ typeof value === 'number' ||
+ typeof value === 'string' ||
+ typeof value === 'boolean'
+ ) {
+ return true
+ }
+
+ if (Array.isArray(value)) {
+ return value.every(isValidJson)
+ }
+
+ if (typeof value === 'object') {
+ return Object.values(value).every(isValidJson)
+ }
+
+ return false
+}
+
+/**
+ * Validate that a value is valid JSON.
+ *
+ * @public
+ */
+export const jsonValue = new Validator((value): JsonValue => {
+ if (isValidJson(value)) {
+ return value as JsonValue
+ }
+
+ throw new ValidationError(`Expected json serializable value, got ${typeof value}`)
+})
+
+/**
+ * Validate an object has a particular shape.
+ *
+ * @public
+ */
+export function jsonDict(): DictValidator {
+ return dict(string, jsonValue)
+}
+
/**
* Validation that an option is a dict with particular keys and values.
*