[feature] add meta
property to records (#1627)
This PR adds a `meta` property to shapes and other records. It adds it to: - asset - camera - document - instance - instancePageState - instancePresence - page - pointer - rootShape ## Setting meta This data can generally be added wherever you would normally update the corresponding record. An exception exists for shapes, which can be updated using a partial of the `meta` in the same way that we update shapes with a partial of `props`. ```ts this.updateShapes([{ id: myShape.id, type: "geo", meta: { nemesis: "steve", special: true } ]) ``` ## `Editor.getInitialMetaForShape` The `Editor.getInitialMetaForShape` method is kind of a hack to set the initial meta property for newly created shapes. You can set it externally. Escape hatch! ### Change Type - [x] `minor` — New feature ### Test Plan todo - [ ] Unit Tests (todo) ### Release Notes - todo
This commit is contained in:
parent
3e07f70440
commit
fd29006538
39 changed files with 594 additions and 95 deletions
|
@ -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"]')
|
||||
|
|
49
apps/examples/src/17-shape-meta/ShapeMetaExample.tsx
Normal file
49
apps/examples/src/17-shape-meta/ShapeMetaExample.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { TLShape, Tldraw, track, useEditor } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
export default function ShapeMetaExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="shape_meta_example"
|
||||
autoFocus
|
||||
onMount={(editor) => {
|
||||
editor.getInitialMetaForShape = (shape) => {
|
||||
return { label: `My ${shape.type} shape` }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ShapeLabelUiWithHelper />
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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<HTMLInputElement>) {
|
||||
if (onlySelectedShape) {
|
||||
const { id, type, meta } = onlySelectedShape
|
||||
|
||||
editor.updateShapes<ShapeWithMyMeta>([
|
||||
{ id, type, meta: { ...meta, label: e.currentTarget.value } },
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', zIndex: 300, top: 64, left: 12 }}>
|
||||
shape label: <input value={onlySelectedShape.meta.label} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -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: <CustomStylesExample />,
|
||||
},
|
||||
{
|
||||
path: '/shape-meta',
|
||||
element: <ShapeMetaExample />,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createBrowserRouter(allExamples)
|
||||
|
|
|
@ -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: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MyShapeWithMeta>(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<MyShapeWithMeta>([{
|
||||
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.
|
|
@ -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<TLEventMap> {
|
|||
getHandles<T extends TLShape>(shape: T): TLHandle[] | undefined;
|
||||
getHandlesById<T extends TLShape>(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<TLGeoShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -915,6 +918,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -931,6 +935,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | {
|
||||
|
@ -945,6 +950,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -1711,6 +1717,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -1734,6 +1741,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -2101,6 +2109,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
scale: number;
|
||||
autoSize: boolean;
|
||||
};
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
@ -2124,6 +2133,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
opacity: number;
|
||||
meta: JsonObject;
|
||||
id: TLShapeId;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
|
|
|
@ -148,7 +148,7 @@ const InnerShape = React.memo(
|
|||
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ import {
|
|||
isShapeId,
|
||||
} from '@tldraw/tlschema'
|
||||
import {
|
||||
JsonObject,
|
||||
annotateError,
|
||||
assert,
|
||||
compact,
|
||||
|
@ -3971,6 +3972,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
bottomIndex && topIndex !== bottomIndex
|
||||
? getIndexBetween(topIndex, bottomIndex)
|
||||
: getIndexAbove(topIndex),
|
||||
meta: {},
|
||||
})
|
||||
|
||||
const newCamera = CameraRecordType.create({
|
||||
|
@ -7094,6 +7096,26 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
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<TLEventMap> {
|
|||
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<TLEventMap> {
|
|||
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<TLEventMap> {
|
|||
}
|
||||
|
||||
if (k === 'props') {
|
||||
const nextProps = { ...prev.props } as Record<string, unknown>
|
||||
// props property
|
||||
const nextProps = { ...prev.props } as JsonObject
|
||||
for (const [propKey, propValue] of Object.entries(v as object)) {
|
||||
if (propValue === undefined) continue
|
||||
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<TLEventMap> {
|
|||
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
|
||||
? this.store.get(TLPOINTER_ID)?.lastActivityTimestamp ?? Date.now()
|
||||
: Date.now(),
|
||||
meta: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ Object {
|
|||
"id": "shape:line1",
|
||||
"index": "a1",
|
||||
"isLocked": false,
|
||||
"meta": Object {},
|
||||
"opacity": 1,
|
||||
"parentId": "page:id50",
|
||||
"props": Object {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' })
|
||||
})
|
|
@ -5,6 +5,7 @@ Object {
|
|||
"id": "shape:lineA",
|
||||
"index": "a3",
|
||||
"isLocked": false,
|
||||
"meta": Object {},
|
||||
"opacity": 1,
|
||||
"parentId": "shape:boxA",
|
||||
"props": Object {
|
||||
|
|
|
@ -170,6 +170,7 @@ export async function getMediaAssetFromFile(file: File): Promise<TLAsset> {
|
|||
mimeType: file.type,
|
||||
isAnimated: metadata.isAnimated,
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
|
||||
resolve(asset)
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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<TLRecord>): SerializedStore<TLRecord>;
|
||||
|
||||
// @public
|
||||
export function createAssetValidator<Type extends string, Props extends object>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
|
||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(type: Type, props: T.Validator<Props>): 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 extends string, Props extends object>(type: Type, props?: {
|
||||
export function createShapeValidator<Type extends string, Props extends JsonObject, Meta extends JsonObject>(type: Type, props?: {
|
||||
[K in keyof Props]: T.Validatable<Props[K]>;
|
||||
}): 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<string, unknown>;
|
||||
}>;
|
||||
}, meta?: {
|
||||
[K in keyof Meta]: T.Validatable<Meta[K]>;
|
||||
}): T.ObjectValidator<TLBaseShape<Type, Props>>;
|
||||
|
||||
// @public
|
||||
export function createTLSchema({ shapes }: {
|
||||
|
@ -710,6 +702,9 @@ export type SchemaShapeInfo = {
|
|||
props?: Record<string, {
|
||||
validate: (prop: any) => any;
|
||||
}>;
|
||||
meta?: Record<string, {
|
||||
validate: (prop: any) => any;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
|
@ -786,7 +781,8 @@ export type TLAssetPartial<T extends TLAsset = TLAsset> = T extends T ? {
|
|||
id: TLAssetId;
|
||||
type: T['type'];
|
||||
props?: Partial<T['props']>;
|
||||
} & Partial<Omit<T, 'id' | 'props' | 'type'>> : never;
|
||||
meta?: Partial<T['meta']>;
|
||||
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLAssetShape = Extract<TLShape, {
|
||||
|
@ -797,6 +793,8 @@ export type TLAssetShape = Extract<TLShape, {
|
|||
|
||||
// @public (undocumented)
|
||||
export interface TLBaseAsset<Type extends string, Props> extends BaseRecord<'asset', TLAssetId> {
|
||||
// (undocumented)
|
||||
meta: JsonObject;
|
||||
// (undocumented)
|
||||
props: Props;
|
||||
// (undocumented)
|
||||
|
@ -810,6 +808,8 @@ export interface TLBaseShape<Type extends string, Props extends object> 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<TLDocument>>
|
|||
// (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<TLPage>;
|
||||
// (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 TLShape = TLShape> = T extends T ? {
|
|||
id: TLShapeId;
|
||||
type: T['type'];
|
||||
props?: Partial<T['props']>;
|
||||
} & Partial<Omit<T, 'id' | 'props' | 'type'>> : never;
|
||||
meta?: Partial<T['meta']>;
|
||||
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLShapeProp = keyof TLShapeProps;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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<Type extends string, Props> extends BaseRecord<'asset', TLAssetId> {
|
||||
type: Type
|
||||
props: Props
|
||||
meta: JsonObject
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,7 +24,7 @@ export const assetIdValidator = idValidator<TLAssetId>('asset')
|
|||
* @param props - The validator for the asset's props
|
||||
*
|
||||
* @public */
|
||||
export function createAssetValidator<Type extends string, Props extends object>(
|
||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(
|
||||
type: Type,
|
||||
props: T.Validator<Props>
|
||||
): T.ObjectValidator<{
|
||||
|
@ -30,11 +32,13 @@ export function createAssetValidator<Type extends string, Props extends object>(
|
|||
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<JsonObject>,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ export const createPresenceStateDerivation =
|
|||
lastActivityTimestamp: pointer.lastActivityTimestamp,
|
||||
screenBounds: instance.screenBounds,
|
||||
chatMessage: instance.chatMessage,
|
||||
meta: {},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { StyleProp } from './styles/StyleProp'
|
|||
export type SchemaShapeInfo = {
|
||||
migrations?: Migrations
|
||||
props?: Record<string, { validate: (prop: any) => any }>
|
||||
meta?: Record<string, { validate: (prop: any) => any }>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -23,6 +23,11 @@ export const assetValidator: T.Validator<TLAsset> = 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 TLAsset = TLAsset> = T extends T
|
|||
id: TLAssetId
|
||||
type: T['type']
|
||||
props?: Partial<T['props']>
|
||||
} & Partial<Omit<T, 'type' | 'id' | 'props'>>
|
||||
meta?: Partial<T['meta']>
|
||||
} & Partial<Omit<T, 'type' | 'id' | 'props' | 'meta'>>
|
||||
: never
|
||||
|
||||
/** @public */
|
||||
|
@ -47,7 +69,9 @@ export const AssetRecordType = createRecordType<TLAsset>('asset', {
|
|||
migrations: assetMigrations,
|
||||
validator: assetValidator,
|
||||
scope: 'document',
|
||||
})
|
||||
}).withDefaultProperties(() => ({
|
||||
meta: {},
|
||||
}))
|
||||
|
||||
/** @public */
|
||||
export type TLAssetId = RecordId<TLBaseAsset<any, any>>
|
||||
|
|
|
@ -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<TLCamera> = T.model(
|
|||
x: T.number,
|
||||
y: T.number,
|
||||
z: T.number,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
/** @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<TLCamera>('camera', {
|
||||
|
@ -44,5 +69,6 @@ export const CameraRecordType = createRecordType<TLCamera>('camera', {
|
|||
x: 0,
|
||||
y: 0,
|
||||
z: 1,
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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<TLDocument>> {
|
||||
gridSize: number
|
||||
name: string
|
||||
meta: JsonObject
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -19,18 +21,21 @@ export const documentValidator: T.Validator<TLDocument> = T.model(
|
|||
id: T.literal('document:document' as RecordId<TLDocument>),
|
||||
gridSize: T.number,
|
||||
name: T.string,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
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<TLDocument>('document', {
|
|||
(): Omit<TLDocument, 'id' | 'typeName'> => ({
|
||||
gridSize: 10,
|
||||
name: '',
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -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<string, StyleProp<unkno
|
|||
chatMessage: T.string,
|
||||
isChatting: T.boolean,
|
||||
highlightedUserIds: T.arrayOf(T.string),
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -101,11 +104,13 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
|||
chatMessage: '',
|
||||
isChatting: false,
|
||||
highlightedUserIds: [],
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const Versions = {
|
||||
/** @internal */
|
||||
export const instanceVersions = {
|
||||
AddTransparentExportBgs: 1,
|
||||
RemoveDialog: 2,
|
||||
AddToolLockMode: 3,
|
||||
|
@ -122,15 +127,14 @@ const Versions = {
|
|||
AddChat: 14,
|
||||
AddHighlightedUserIds: 15,
|
||||
ReplacePropsForNextShapeWithStylesForNextShape: 16,
|
||||
AddMeta: 17,
|
||||
} as const
|
||||
|
||||
export { Versions as instanceTypeVersions }
|
||||
|
||||
/** @public */
|
||||
export const instanceMigrations = defineMigrations({
|
||||
currentVersion: Versions.ReplacePropsForNextShapeWithStylesForNextShape,
|
||||
currentVersion: instanceVersions.AddMeta,
|
||||
migrators: {
|
||||
[Versions.AddTransparentExportBgs]: {
|
||||
[instanceVersions.AddTransparentExportBgs]: {
|
||||
up: (instance: TLInstance) => {
|
||||
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,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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<TLPage> = T.model(
|
|||
id: pageIdValidator,
|
||||
name: T.string,
|
||||
index: T.string,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
/** @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<TLPage>('page', {
|
||||
validator: pageValidator,
|
||||
migrations: pageMigrations,
|
||||
scope: 'document',
|
||||
})
|
||||
}).withDefaultProperties(() => ({
|
||||
meta: {},
|
||||
}))
|
||||
|
||||
/** @public */
|
||||
export function isPageId(id: string): id is TLPageId {
|
||||
|
|
|
@ -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<TLInstancePageState> = T.mo
|
|||
editingId: shapeIdValidator.nullable(),
|
||||
croppingId: shapeIdValidator.nullable(),
|
||||
focusLayerId: shapeIdValidator.nullable(),
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
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<TLInstancePageState>
|
|||
erasingIds: [],
|
||||
hintingIds: [],
|
||||
focusLayerId: null,
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -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<TLPointer> = T.model(
|
|||
x: T.number,
|
||||
y: T.number,
|
||||
lastActivityTimestamp: T.number,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
/** @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<TLPointer>('pointer', {
|
||||
|
@ -41,6 +66,7 @@ export const PointerRecordType = createRecordType<TLPointer>('pointer', {
|
|||
x: 0,
|
||||
y: 0,
|
||||
lastActivityTimestamp: 0,
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -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<TLInstancePresence> = T.mode
|
|||
brush: box2dModelValidator.nullable(),
|
||||
scribble: scribbleValidator.nullable(),
|
||||
chatMessage: T.string,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
||||
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<TLInstancePresence>(
|
|||
brush: null,
|
||||
scribble: null,
|
||||
chatMessage: '',
|
||||
meta: {},
|
||||
}))
|
||||
|
|
|
@ -59,7 +59,8 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
|
|||
id: TLShapeId
|
||||
type: T['type']
|
||||
props?: Partial<T['props']>
|
||||
} & Partial<Omit<T, 'type' | 'id' | 'props'>>
|
||||
meta?: Partial<T['meta']>
|
||||
} & Partial<Omit<T, 'type' | 'id' | 'props' | 'meta'>>
|
||||
: 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<string, SchemaShapeInfo>) {
|
|||
'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<string, SchemaShapeInfo>) {
|
|||
rotation: 0,
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
meta: {},
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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<Type extends string, Props extends object>
|
|||
isLocked: boolean
|
||||
opacity: TLOpacityType
|
||||
props: Props
|
||||
meta: JsonObject
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -31,11 +32,16 @@ export const parentIdValidator = T.string.refine((id) => {
|
|||
export const shapeIdValidator = idValidator<TLShapeId>('shape')
|
||||
|
||||
/** @public */
|
||||
export function createShapeValidator<Type extends string, Props extends object>(
|
||||
export function createShapeValidator<
|
||||
Type extends string,
|
||||
Props extends JsonObject,
|
||||
Meta extends JsonObject
|
||||
>(
|
||||
type: Type,
|
||||
props?: { [K in keyof Props]: T.Validatable<Props[K]> }
|
||||
props?: { [K in keyof Props]: T.Validatable<Props[K]> },
|
||||
meta?: { [K in keyof Meta]: T.Validatable<Meta[K]> }
|
||||
) {
|
||||
return T.object({
|
||||
return T.object<TLBaseShape<Type, Props>>({
|
||||
id: shapeIdValidator,
|
||||
typeName: T.literal('shape'),
|
||||
x: T.number,
|
||||
|
@ -46,7 +52,8 @@ export function createShapeValidator<Type extends string, Props extends object>(
|
|||
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<Props>),
|
||||
meta: meta ? T.object(meta) : (T.jsonValue as T.ObjectValidator<Meta>),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -79,6 +79,20 @@ export function isNonNull<T>(value: T): value is typeof value extends null ? nev
|
|||
// @public
|
||||
export function isNonNullish<T>(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<T>(arr: readonly T[]): T | undefined;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
8
packages/utils/src/lib/json-value.ts
Normal file
8
packages/utils/src/lib/json-value.ts
Normal file
|
@ -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 }
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
```ts
|
||||
|
||||
import { JsonValue } from '@tldraw/utils';
|
||||
|
||||
// @public
|
||||
const any: Validator<any>;
|
||||
|
||||
|
@ -45,6 +47,12 @@ class DictValidator<Key extends string, Value> extends Validator<Record<Key, Val
|
|||
// @public
|
||||
const integer: Validator<number>;
|
||||
|
||||
// @public
|
||||
function jsonDict(): DictValidator<string, JsonValue>;
|
||||
|
||||
// @public
|
||||
const jsonValue: Validator<JsonValue>;
|
||||
|
||||
// @public
|
||||
function literal<T extends boolean | number | string>(expectedValue: T): Validator<T>;
|
||||
|
||||
|
@ -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 }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/utils'
|
||||
import { JsonValue, exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/utils'
|
||||
|
||||
/** @public */
|
||||
export type ValidatorFn<T> = (value: unknown) => T
|
||||
|
@ -466,6 +466,49 @@ export function object<Shape extends 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<JsonValue>((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<string, JsonValue> {
|
||||
return dict(string, jsonValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation that an option is a dict with particular keys and values.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue