From eb2696413040589b670af18324e5fc3688a2f678 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 24 May 2023 11:48:31 +0100 Subject: [PATCH] [refactor] restore createTLSchema (#1444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR restores `createTLSchema`. It also: - removes `TldrawEditorConfig.default` - makes `config` a required property of ``, though it's created automatically in ``. - makes `config` a required property of `App` - removes `TLShapeType` and replaces the rare usage with `TLShape["type"]` - adds `TLDefaultShape` for a union of our default shapes - makes `TLShape` a union of `TLDefaultShape` and `TLUnknownShape` ### Change Type - [x] `major` — Breaking Change ### Release Notes - [editor] Simplifies custom shape definition - [tldraw] Updates props for component to require a `TldrawEditorConfig`. --- .../src/4-custom-ui/CustomUiExample.tsx | 6 +- .../src/5-exploded/ExplodedExample.tsx | 12 +- apps/vscode/editor/src/app.tsx | 16 +- apps/vscode/extension/src/file.ts | 4 +- e2e/test/helpers/ui/app.ts | 15 +- packages/editor/api-report.md | 22 +- packages/editor/src/lib/TldrawEditor.tsx | 47 ++-- packages/editor/src/lib/app/App.ts | 16 +- .../shapeutils/TLFrameUtil/TLFrameUtil.tsx | 4 +- .../app/shapeutils/TLIconUtil/TLIconUtil.tsx | 0 .../src/lib/app/shapeutils/TLShapeUtil.ts | 3 +- .../app/statechart/TLArrowTool/TLArrowTool.ts | 4 +- .../TLArrowTool/children/Pointing.ts | 8 +- .../app/statechart/TLLineTool/TLLineTool.ts | 4 +- .../TLLineTool/children/Pointing.ts | 7 +- .../src/lib/config/TldrawEditorConfig.tsx | 202 ++++-------------- packages/editor/src/lib/test/TestApp.test.ts | 7 + packages/editor/src/lib/test/TestApp.ts | 6 +- .../editor/src/lib/test/TldrawEditor.test.tsx | 13 +- .../src/lib/utils/buildFromV1Document.ts | 13 -- packages/file-format/src/lib/file.ts | 2 +- packages/file-format/src/test/file.test.ts | 28 +-- packages/tldraw/api-report.md | 3 +- packages/tldraw/src/lib/Tldraw.tsx | 33 ++- packages/tlschema/api-report.md | 16 +- packages/tlschema/src/createTLSchema.ts | 134 ++++++++++++ packages/tlschema/src/index.ts | 3 +- packages/tlschema/src/records/TLShape.ts | 26 ++- packages/tlsync-client/api-report.md | 2 +- .../src/lib/TLLocalSyncClient.test.ts | 2 +- .../src/lib/hooks/useLocalSyncClient.ts | 4 +- 31 files changed, 377 insertions(+), 285 deletions(-) create mode 100644 packages/editor/src/lib/app/shapeutils/TLIconUtil/TLIconUtil.tsx create mode 100644 packages/editor/src/lib/test/TestApp.test.ts create mode 100644 packages/tlschema/src/createTLSchema.ts diff --git a/apps/examples/src/4-custom-ui/CustomUiExample.tsx b/apps/examples/src/4-custom-ui/CustomUiExample.tsx index 5812871e9..66014805b 100644 --- a/apps/examples/src/4-custom-ui/CustomUiExample.tsx +++ b/apps/examples/src/4-custom-ui/CustomUiExample.tsx @@ -1,13 +1,15 @@ -import { Canvas, TldrawEditor, useApp } from '@tldraw/tldraw' +import { Canvas, TldrawEditor, TldrawEditorConfig, useApp } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import { useEffect } from 'react' import { track } from 'signia-react' import './custom-ui.css' +const config = new TldrawEditorConfig() + export default function Example() { return (
- + diff --git a/apps/examples/src/5-exploded/ExplodedExample.tsx b/apps/examples/src/5-exploded/ExplodedExample.tsx index a17c24bd3..538055310 100644 --- a/apps/examples/src/5-exploded/ExplodedExample.tsx +++ b/apps/examples/src/5-exploded/ExplodedExample.tsx @@ -3,6 +3,7 @@ import { ContextMenu, getUserData, TldrawEditor, + TldrawEditorConfig, TldrawUi, TLInstance, useLocalSyncClient, @@ -12,10 +13,13 @@ import '@tldraw/tldraw/ui.css' const instanceId = TLInstance.createCustomId('example') +const config = new TldrawEditorConfig() + export default function Example() { const userData = getUserData() const syncedStore = useLocalSyncClient({ + config, instanceId, userId: userData.id, universalPersistenceKey: 'exploded-example', @@ -24,7 +28,13 @@ export default function Example() { return (
- + diff --git a/apps/vscode/editor/src/app.tsx b/apps/vscode/editor/src/app.tsx index feccc27fa..92f7ef8e2 100644 --- a/apps/vscode/editor/src/app.tsx +++ b/apps/vscode/editor/src/app.tsx @@ -4,6 +4,7 @@ import { ErrorBoundary, setRuntimeOverrides, TldrawEditor, + TldrawEditorConfig, TLUserId, } from '@tldraw/editor' import { linksUiOverrides } from './utils/links' @@ -24,6 +25,8 @@ import { FullPageMessage } from './FullPageMessage' import { onCreateBookmarkFromUrl } from './utils/bookmarks' import { vscode } from './utils/vscode' +const config = new TldrawEditorConfig() + // @ts-ignore setRuntimeOverrides({ @@ -96,6 +99,7 @@ export const TldrawWrapper = () => { uri: message.data.uri, userId: message.data.userId as TLUserId, isDarkMode: message.data.isDarkMode, + config, }) // We only want to listen for this message once window.removeEventListener('message', handleMessage) @@ -126,20 +130,30 @@ export type TLDrawInnerProps = { uri: string userId: TLUserId isDarkMode: boolean + config: TldrawEditorConfig } -function TldrawInner({ uri, assetSrc, userId, isDarkMode, fileContents }: TLDrawInnerProps) { +function TldrawInner({ + uri, + config, + assetSrc, + userId, + isDarkMode, + fileContents, +}: TLDrawInnerProps) { const instanceId = TAB_ID const syncedStore = useLocalSyncClient({ universalPersistenceKey: uri, instanceId, userId, + config, }) const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc]) return ( { - return browser.execute(() => { - return window.tldrawReady - }) - }) + await Promise.any([ + new Promise((r) => { + browser.waitUntil(() => browser.execute(() => window.tldrawReady)).then(() => r(true)) + }), + new Promise((r) => { + // eslint-disable-next-line no-console + console.log('waitFor failed, using timeout') + setTimeout(() => r(true), 2000) + }), + ]) // Make sure the window is focused... maybe await ui.canvas.click(100, 100) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index b79ef3806..dcad156a7 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -44,7 +44,6 @@ import { sortByIndex } from '@tldraw/indices'; import { StoreSchema } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore'; import { StrokePoint } from '@tldraw/primitives'; -import { T } from '@tldraw/tlvalidate'; import { TLAlignType } from '@tldraw/tlschema'; import { TLArrowheadType } from '@tldraw/tlschema'; import { TLArrowShape } from '@tldraw/tlschema'; @@ -87,7 +86,6 @@ import { TLShapeId } from '@tldraw/tlschema'; import { TLShapePartial } from '@tldraw/tlschema'; import { TLShapeProp } from '@tldraw/tlschema'; import { TLShapeProps } from '@tldraw/tlschema'; -import { TLShapeType } from '@tldraw/tlschema'; import { TLSizeStyle } from '@tldraw/tlschema'; import { TLSizeType } from '@tldraw/tlschema'; import { TLStore } from '@tldraw/tlschema'; @@ -274,7 +272,7 @@ export class App extends EventEmitter { getPageTransform(shape: TLShape): Matrix2d | undefined; getPageTransformById(id: TLShapeId): Matrix2d | undefined; // (undocumented) - getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType): TLPageId | TLShapeId; + getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId; getParentPageId(shape?: TLShape): TLPageId | undefined; getParentShape(shape?: TLShape): TLShape | undefined; getParentsMappedToChildren(ids: TLShapeId[]): Map>; @@ -556,7 +554,7 @@ export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }: // @public (undocumented) export interface AppOptions { - config?: TldrawEditorConfig; + config: TldrawEditorConfig; getContainer: () => HTMLElement; store: TLStore; } @@ -1793,7 +1791,7 @@ export function TldrawEditor(props: TldrawEditorProps): JSX.Element; // @public (undocumented) export class TldrawEditorConfig { - constructor(opts: TldrawEditorConfigOptions); + constructor(opts?: TldrawEditorConfigOptions); // (undocumented) createStore(config: { initialData?: StoreSnapshot; @@ -1801,13 +1799,7 @@ export class TldrawEditorConfig { instanceId: TLInstanceId; }): TLStore; // (undocumented) - static readonly default: TldrawEditorConfig; - // (undocumented) - readonly shapeMigrations: MigrationsForShapes; - // (undocumented) - readonly shapeUtils: UtilsForShapes; - // (undocumented) - readonly shapeValidators: Record>; + readonly shapeUtils: Record>; // (undocumented) readonly storeSchema: StoreSchema; // (undocumented) @@ -1823,7 +1815,7 @@ export interface TldrawEditorProps { // (undocumented) children?: any; components?: Partial; - config?: TldrawEditorConfig; + config: TldrawEditorConfig; instanceId?: TLInstanceId; isDarkMode?: boolean; onCreateAssetFromFile?: (file: File) => Promise; @@ -2044,7 +2036,7 @@ export class TLFrameUtil extends TLBoxUtil { // (undocumented) canEdit: () => boolean; // (undocumented) - canReceiveNewChildrenOfType: (_type: TLShapeType) => boolean; + canReceiveNewChildrenOfType: (_type: TLShape['type']) => boolean; // (undocumented) defaultProps(): TLFrameShape['props']; // (undocumented) @@ -2456,7 +2448,7 @@ export abstract class TLShapeUtil { canCrop: TLShapeUtilFlag; canDropShapes(shape: T, shapes: TLShape[]): boolean; canEdit: TLShapeUtilFlag; - canReceiveNewChildrenOfType(type: TLShapeType): boolean; + canReceiveNewChildrenOfType(type: TLShape['type']): boolean; canResize: TLShapeUtilFlag; canScroll: TLShapeUtilFlag; canUnmount: TLShapeUtilFlag; diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 948845a7c..7a55fba6d 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -1,7 +1,7 @@ import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema' import { Store } from '@tldraw/tlstore' import { annotateError } from '@tldraw/utils' -import React, { useCallback, useSyncExternalStore } from 'react' +import React, { useCallback, useEffect, useState, useSyncExternalStore } from 'react' import { App } from './app/App' import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' import { OptionalErrorBoundary } from './components/ErrorBoundary' @@ -28,12 +28,12 @@ import { useZoomCss } from './hooks/useZoomCss' /** @public */ export interface TldrawEditorProps { children?: any + /** A configuration defining major customizations to the app, such as custom shapes and new tools */ + config: TldrawEditorConfig /** Overrides for the tldraw components */ components?: Partial /** Whether to display the dark mode. */ isDarkMode?: boolean - /** A configuration defining major customizations to the app, such as custom shapes and new tools */ - config?: TldrawEditorConfig /** * Called when the app has mounted. * @@ -133,7 +133,7 @@ export function TldrawEditor(props: TldrawEditorProps) { } function TldrawEditorBeforeLoading({ - config = TldrawEditorConfig.default, + config, userId, instanceId, store, @@ -143,26 +143,43 @@ function TldrawEditorBeforeLoading({ props.assetUrls ?? defaultEditorAssetUrls ) - store ??= config.createStore({ - userId: userId ?? TLUser.createId(), - instanceId: instanceId ?? TLInstance.createId(), + const [_store, _setStore] = useState(() => { + return ( + store ?? + config.createStore({ + userId: userId ?? TLUser.createId(), + instanceId: instanceId ?? TLInstance.createId(), + }) + ) }) - let loadedStore - if (!(store instanceof Store)) { - if (store.error) { + useEffect(() => { + _setStore(() => { + return ( + store ?? + config.createStore({ + userId: userId ?? TLUser.createId(), + instanceId: instanceId ?? TLInstance.createId(), + }) + ) + }) + }, [store, config, userId, instanceId]) + + let loadedStore: TLStore | SyncedStore + if (!(_store instanceof Store)) { + if (_store.error) { // for error handling, we fall back to the default error boundary. // if users want to handle this error differently, they can render // their own error screen before the TldrawEditor component - throw store.error + throw _store.error } - if (!store.store) { + if (!_store.store) { return Connecting... } - loadedStore = store.store + loadedStore = _store.store } else { - loadedStore = store + loadedStore = _store } if (instanceId && loadedStore.props.instanceId !== instanceId) { @@ -209,8 +226,8 @@ function TldrawEditorAfterLoading({ React.useLayoutEffect(() => { const app = new App({ store, - getContainer: () => container, config, + getContainer: () => container, }) setApp(app) diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts index 2394808ce..048a7c042 100644 --- a/packages/editor/src/lib/app/App.ts +++ b/packages/editor/src/lib/app/App.ts @@ -49,7 +49,6 @@ import { TLShapeId, TLShapePartial, TLShapeProp, - TLShapeType, TLSizeStyle, TLStore, TLUnknownShape, @@ -160,7 +159,7 @@ export interface AppOptions { */ store: TLStore /** A configuration defining major customizations to the app, such as custom shapes and new tools */ - config?: TldrawEditorConfig + config: TldrawEditorConfig /** * Should return a containing html element which has all the styles applied to the app. If not * given, the body element will be used. @@ -175,14 +174,15 @@ export function isShapeWithHandles(shape: TLShape) { /** @public */ export class App extends EventEmitter { - constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) { + constructor({ config, store, getContainer }: AppOptions) { super() - if (store.schema !== config.storeSchema) { + this.config = config + + if (store.schema !== this.config.storeSchema) { throw new Error('Store schema does not match schema given to App') } - this.config = config this.store = store this.getContainer = getContainer ?? (() => document.body) @@ -191,7 +191,7 @@ export class App extends EventEmitter { // Set the shape utils this.shapeUtils = Object.fromEntries( - Object.entries(config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)]) + Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)]) ) if (typeof window !== 'undefined' && 'navigator' in window) { @@ -209,7 +209,7 @@ export class App extends EventEmitter { this.root = new RootState(this) if (this.root.children) { - config.tools.forEach((Ctor) => { + this.config.tools.forEach((Ctor) => { this.root.children![Ctor.id] = new Ctor(this) }) } @@ -2864,7 +2864,7 @@ export class App extends EventEmitter { return this } - getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType) { + getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']) { const shapes = this.sortedShapesArray for (let i = shapes.length - 1; i >= 0; i--) { diff --git a/packages/editor/src/lib/app/shapeutils/TLFrameUtil/TLFrameUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLFrameUtil/TLFrameUtil.tsx index 6f1e2b899..1b131ae27 100644 --- a/packages/editor/src/lib/app/shapeutils/TLFrameUtil/TLFrameUtil.tsx +++ b/packages/editor/src/lib/app/shapeutils/TLFrameUtil/TLFrameUtil.tsx @@ -1,5 +1,5 @@ import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives' -import { TLFrameShape, TLShape, TLShapeId, TLShapeType } from '@tldraw/tlschema' +import { TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema' import { last } from '@tldraw/utils' import { SVGContainer } from '../../../components/SVGContainer' import { defaultEmptyAs } from '../../../utils/string' @@ -148,7 +148,7 @@ export class TLFrameUtil extends TLBoxUtil { ) } - override canReceiveNewChildrenOfType = (_type: TLShapeType) => { + override canReceiveNewChildrenOfType = (_type: TLShape['type']) => { return true } diff --git a/packages/editor/src/lib/app/shapeutils/TLIconUtil/TLIconUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLIconUtil/TLIconUtil.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts b/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts index 4df6e1b94..3ad5f7b11 100644 --- a/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts +++ b/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts @@ -5,7 +5,6 @@ import { TLHandle, TLShape, TLShapePartial, - TLShapeType, TLUnknownShape, Vec2dModel, } from '@tldraw/tlschema' @@ -308,7 +307,7 @@ export abstract class TLShapeUtil { * @param type - The shape type. * @public */ - canReceiveNewChildrenOfType(type: TLShapeType) { + canReceiveNewChildrenOfType(type: TLShape['type']) { return false } diff --git a/packages/editor/src/lib/app/statechart/TLArrowTool/TLArrowTool.ts b/packages/editor/src/lib/app/statechart/TLArrowTool/TLArrowTool.ts index 6333b0014..5b6527978 100644 --- a/packages/editor/src/lib/app/statechart/TLArrowTool/TLArrowTool.ts +++ b/packages/editor/src/lib/app/statechart/TLArrowTool/TLArrowTool.ts @@ -1,4 +1,4 @@ -import { TLShapeType, TLStyleType } from '@tldraw/tlschema' +import { TLStyleType } from '@tldraw/tlschema' import { StateNode } from '../StateNode' import { Idle } from './children/Idle' import { Pointing } from './children/Pointing' @@ -8,7 +8,7 @@ export class TLArrowTool extends StateNode { static initial = 'idle' static children = () => [Idle, Pointing] - shapeType: TLShapeType = 'arrow' + shapeType = 'arrow' styles = [ 'color', diff --git a/packages/editor/src/lib/app/statechart/TLArrowTool/children/Pointing.ts b/packages/editor/src/lib/app/statechart/TLArrowTool/children/Pointing.ts index fffb2e1f6..6ff6ab18a 100644 --- a/packages/editor/src/lib/app/statechart/TLArrowTool/children/Pointing.ts +++ b/packages/editor/src/lib/app/statechart/TLArrowTool/children/Pointing.ts @@ -1,4 +1,4 @@ -import { createShapeId, TLArrowShape, TLShapeType } from '@tldraw/tlschema' +import { createShapeId, TLArrowShape } from '@tldraw/tlschema' import { TLArrowUtil } from '../../../shapeutils/TLArrowUtil/TLArrowUtil' import { TLEventHandlers } from '../../../types/event-types' import { StateNode } from '../../StateNode' @@ -7,8 +7,6 @@ import { TLArrowTool } from '../TLArrowTool' export class Pointing extends StateNode { static override id = 'pointing' - shapeType = '' as TLShapeType - shape?: TLArrowShape preciseTimeout = -1 @@ -33,7 +31,7 @@ export class Pointing extends StateNode { this.didTimeout = false - this.shapeType = (this.parent as TLArrowTool).shapeType + const shapeType = (this.parent as TLArrowTool).shapeType this.app.mark('creating') @@ -42,7 +40,7 @@ export class Pointing extends StateNode { this.app.createShapes([ { id, - type: this.shapeType, + type: shapeType, x: currentPagePoint.x, y: currentPagePoint.y, }, diff --git a/packages/editor/src/lib/app/statechart/TLLineTool/TLLineTool.ts b/packages/editor/src/lib/app/statechart/TLLineTool/TLLineTool.ts index ecfb52a1c..6a3365054 100644 --- a/packages/editor/src/lib/app/statechart/TLLineTool/TLLineTool.ts +++ b/packages/editor/src/lib/app/statechart/TLLineTool/TLLineTool.ts @@ -1,4 +1,4 @@ -import { TLShapeType, TLStyleType } from '@tldraw/tlschema' +import { TLStyleType } from '@tldraw/tlschema' import { StateNode } from '../StateNode' import { Idle } from './children/Idle' @@ -9,7 +9,7 @@ export class TLLineTool extends StateNode { static initial = 'idle' static children = () => [Idle, Pointing] - shapeType: TLShapeType = 'line' + shapeType = 'line' styles = ['color', 'opacity', 'dash', 'size', 'spline'] as TLStyleType[] } diff --git a/packages/editor/src/lib/app/statechart/TLLineTool/children/Pointing.ts b/packages/editor/src/lib/app/statechart/TLLineTool/children/Pointing.ts index 997036889..049a11e41 100644 --- a/packages/editor/src/lib/app/statechart/TLLineTool/children/Pointing.ts +++ b/packages/editor/src/lib/app/statechart/TLLineTool/children/Pointing.ts @@ -1,6 +1,6 @@ import { getIndexAbove, sortByIndex } from '@tldraw/indices' import { Matrix2d, Vec2d } from '@tldraw/primitives' -import { TLHandle, TLLineShape, TLShapeId, TLShapeType, createShapeId } from '@tldraw/tlschema' +import { TLHandle, TLLineShape, TLShapeId, createShapeId } from '@tldraw/tlschema' import { last, structuredClone } from '@tldraw/utils' import { TLEventHandlers, TLInterruptEvent } from '../../../types/event-types' import { StateNode } from '../../StateNode' @@ -9,8 +9,6 @@ import { TLLineTool } from '../TLLineTool' export class Pointing extends StateNode { static override id = 'pointing' - shapeType = '' as TLShapeType - shape = {} as TLLineShape markPointId = '' @@ -19,7 +17,6 @@ export class Pointing extends StateNode { const { inputs } = this.app const { currentPagePoint } = inputs - this.shapeType = (this.parent as TLLineTool).shapeType this.markPointId = this.app.mark('creating') let shapeExists = false @@ -85,7 +82,7 @@ export class Pointing extends StateNode { this.app.createShapes([ { id, - type: this.shapeType, + type: (this.parent as TLLineTool).shapeType, x: currentPagePoint.x, y: currentPagePoint.y, }, diff --git a/packages/editor/src/lib/config/TldrawEditorConfig.tsx b/packages/editor/src/lib/config/TldrawEditorConfig.tsx index b70ecfb50..056643124 100644 --- a/packages/editor/src/lib/config/TldrawEditorConfig.tsx +++ b/packages/editor/src/lib/config/TldrawEditorConfig.tsx @@ -1,63 +1,19 @@ import { CLIENT_FIXUP_SCRIPT, - TLAsset, - TLCamera, TLDOCUMENT_ID, - TLDocument, + TLDefaultShape, TLInstance, TLInstanceId, - TLInstancePageState, TLInstancePresence, - TLPage, TLRecord, TLShape, TLStore, TLStoreProps, - TLUnknownShape, TLUser, - TLUserDocument, TLUserId, - TLUserPresence, - arrowShapeTypeMigrations, - arrowShapeTypeValidator, - bookmarkShapeTypeMigrations, - bookmarkShapeTypeValidator, - createIntegrityChecker, - defaultDerivePresenceState, - drawShapeTypeMigrations, - drawShapeTypeValidator, - embedShapeTypeMigrations, - embedShapeTypeValidator, - frameShapeTypeMigrations, - frameShapeTypeValidator, - geoShapeTypeMigrations, - geoShapeTypeValidator, - groupShapeTypeMigrations, - groupShapeTypeValidator, - imageShapeTypeMigrations, - imageShapeTypeValidator, - lineShapeTypeMigrations, - lineShapeTypeValidator, - noteShapeTypeMigrations, - noteShapeTypeValidator, - onValidationFailure, - rootShapeTypeMigrations, - storeMigrations, - textShapeTypeMigrations, - textShapeTypeValidator, - videoShapeTypeMigrations, - videoShapeTypeValidator, + createTLSchema, } from '@tldraw/tlschema' -import { - Migrations, - RecordType, - Store, - StoreSchema, - StoreSnapshot, - createRecordType, - defineMigrations, -} from '@tldraw/tlstore' -import { T } from '@tldraw/tlvalidate' +import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore' import { Signal } from 'signia' import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' @@ -74,135 +30,69 @@ import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil' import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil' import { StateNodeConstructor } from '../app/statechart/StateNode' -/** @public */ -export type ValidatorsForShapes = Record< - T['type'], - { validate: (record: T) => T } -> +// Secret shape types that don't have a shape util yet +type ShapeTypesNotImplemented = 'icon' -/** @public */ -export type MigrationsForShapes = Record - -type CustomShapeInfo = { - util: TLShapeUtilConstructor - validator?: { validate: (record: T) => T } - migrations?: Migrations +const DEFAULT_SHAPE_UTILS: { + [K in Exclude]: TLShapeUtilConstructor +} = { + arrow: TLArrowUtil, + bookmark: TLBookmarkUtil, + draw: TLDrawUtil, + embed: TLEmbedUtil, + frame: TLFrameUtil, + geo: TLGeoUtil, + group: TLGroupUtil, + image: TLImageUtil, + line: TLLineUtil, + note: TLNoteUtil, + text: TLTextUtil, + video: TLVideoUtil, } -type UtilsForShapes = Record> - -type TldrawEditorConfigOptions = { +/** @public */ +export type TldrawEditorConfigOptions = { tools?: readonly StateNodeConstructor[] - shapes?: { [K in T['type']]: CustomShapeInfo } + shapes?: Record< + string, + { + util: TLShapeUtilConstructor + validator?: { validate: (record: T) => T } + migrations?: Migrations + } + > /** @internal */ derivePresenceState?: (store: TLStore) => Signal } /** @public */ export class TldrawEditorConfig { - static readonly default = new TldrawEditorConfig({}) - - readonly storeSchema: StoreSchema - readonly TLShape: RecordType + // Custom tools readonly tools: readonly StateNodeConstructor[] // Custom shape utils - readonly shapeUtils: UtilsForShapes - // Validators for shape subtypes - readonly shapeValidators: Record> - // Migrations for shape subtypes - readonly shapeMigrations: MigrationsForShapes + readonly shapeUtils: Record> - constructor(opts: TldrawEditorConfigOptions) { - const { shapes = [], tools = [], derivePresenceState } = opts + // The record used for TLShape incorporating any custom shapes + readonly TLShape: RecordType + + // The schema used for the store incorporating any custom shapes + readonly storeSchema: StoreSchema + + constructor(opts = {} as TldrawEditorConfigOptions) { + const { shapes = {}, tools = [], derivePresenceState } = opts this.tools = tools this.shapeUtils = { - arrow: TLArrowUtil, - bookmark: TLBookmarkUtil, - draw: TLDrawUtil, - embed: TLEmbedUtil, - frame: TLFrameUtil, - geo: TLGeoUtil, - group: TLGroupUtil, - image: TLImageUtil, - line: TLLineUtil, - note: TLNoteUtil, - text: TLTextUtil, - video: TLVideoUtil, + ...DEFAULT_SHAPE_UTILS, + ...Object.fromEntries(Object.entries(shapes).map(([k, v]) => [k, v.util])), } - this.shapeMigrations = { - arrow: arrowShapeTypeMigrations, - bookmark: bookmarkShapeTypeMigrations, - draw: drawShapeTypeMigrations, - embed: embedShapeTypeMigrations, - frame: frameShapeTypeMigrations, - geo: geoShapeTypeMigrations, - group: groupShapeTypeMigrations, - image: imageShapeTypeMigrations, - line: lineShapeTypeMigrations, - note: noteShapeTypeMigrations, - text: textShapeTypeMigrations, - video: videoShapeTypeMigrations, - } - - this.shapeValidators = { - arrow: arrowShapeTypeValidator, - bookmark: bookmarkShapeTypeValidator, - draw: drawShapeTypeValidator, - embed: embedShapeTypeValidator, - frame: frameShapeTypeValidator, - geo: geoShapeTypeValidator, - group: groupShapeTypeValidator, - image: imageShapeTypeValidator, - line: lineShapeTypeValidator, - note: noteShapeTypeValidator, - text: textShapeTypeValidator, - video: videoShapeTypeValidator, - } - - // Add custom shapes - for (const [type, shape] of Object.entries(shapes)) { - this.shapeUtils[type] = shape.util - this.shapeMigrations[type] = shape.migrations ?? defineMigrations({}) - this.shapeValidators[type] = (shape.validator ?? T.any) as T.Validator - } - - const shapeRecord = createRecordType('shape', { - migrations: defineMigrations({ - currentVersion: rootShapeTypeMigrations.currentVersion, - firstVersion: rootShapeTypeMigrations.firstVersion, - migrators: rootShapeTypeMigrations.migrators, - subTypeKey: 'type', - subTypeMigrations: this.shapeMigrations, - }), - validator: T.model('shape', T.union('type', { ...this.shapeValidators })), - scope: 'document', - }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) - - this.storeSchema = StoreSchema.create( - { - asset: TLAsset, - camera: TLCamera, - document: TLDocument, - instance: TLInstance, - instance_page_state: TLInstancePageState, - page: TLPage, - shape: shapeRecord, - user: TLUser, - user_document: TLUserDocument, - user_presence: TLUserPresence, - instance_presence: TLInstancePresence, - }, - { - snapshotMigrations: storeMigrations, - onValidationFailure, - createIntegrityChecker: createIntegrityChecker, - derivePresenceState: derivePresenceState ?? defaultDerivePresenceState, - } - ) + this.storeSchema = createTLSchema({ + customShapes: shapes, + derivePresenceState: derivePresenceState, + }) this.TLShape = this.storeSchema.types.shape as RecordType< TLShape, diff --git a/packages/editor/src/lib/test/TestApp.test.ts b/packages/editor/src/lib/test/TestApp.test.ts new file mode 100644 index 000000000..c60a0a4c1 --- /dev/null +++ b/packages/editor/src/lib/test/TestApp.test.ts @@ -0,0 +1,7 @@ +import { TestApp } from './TestApp' + +it('loads the test app', () => { + expect(() => { + new TestApp() + }).not.toThrow() +}) diff --git a/packages/editor/src/lib/test/TestApp.ts b/packages/editor/src/lib/test/TestApp.ts index 6f9d17404..1e12fca62 100644 --- a/packages/editor/src/lib/test/TestApp.ts +++ b/packages/editor/src/lib/test/TestApp.ts @@ -55,11 +55,13 @@ export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1') export const TEST_USER_ID = TLUser.createCustomId('testUser1') export class TestApp extends App { - constructor(options = {} as Partial) { + constructor(options = {} as Partial>) { const elm = document.createElement('div') elm.tabIndex = 0 + const config = options.config ?? new TldrawEditorConfig() super({ - store: (options.config ?? TldrawEditorConfig.default).createStore({ + config, + store: config.createStore({ userId: TEST_USER_ID, instanceId: TEST_INSTANCE_ID, }), diff --git a/packages/editor/src/lib/test/TldrawEditor.test.tsx b/packages/editor/src/lib/test/TldrawEditor.test.tsx index 35d30aba6..4af352bb4 100644 --- a/packages/editor/src/lib/test/TldrawEditor.test.tsx +++ b/packages/editor/src/lib/test/TldrawEditor.test.tsx @@ -21,14 +21,17 @@ afterEach(() => { describe('', () => { it('Accepts fresh versions of store and calls `onMount` for each one', async () => { - const initialStore = TldrawEditorConfig.default.createStore({ + const config = new TldrawEditorConfig() + + const initialStore = config.createStore({ instanceId: TLInstance.createCustomId('test'), userId: TLUser.createCustomId('test'), }) + const onMount = jest.fn() const rendered = render( - +
) @@ -40,7 +43,7 @@ describe('', () => { // re-render with the same store: rendered.rerender( - +
) @@ -49,12 +52,12 @@ describe('', () => { expect(onMount).toHaveBeenCalledTimes(1) // re-render with a new store: - const newStore = TldrawEditorConfig.default.createStore({ + const newStore = config.createStore({ instanceId: TLInstance.createCustomId('test'), userId: TLUser.createCustomId('test'), }) rendered.rerender( - +
) diff --git a/packages/editor/src/lib/utils/buildFromV1Document.ts b/packages/editor/src/lib/utils/buildFromV1Document.ts index 702bb79a6..bc2c44e51 100644 --- a/packages/editor/src/lib/utils/buildFromV1Document.ts +++ b/packages/editor/src/lib/utils/buildFromV1Document.ts @@ -1076,19 +1076,6 @@ export interface LegacyTldrawDocument { /* ------------------ Translations ------------------ */ -// const v1ShapeTypesToV2ShapeTypes: Record = { -// [TDShapeType.Rectangle]: 'geo', -// [TDShapeType.Ellipse]: 'geo', -// [TDShapeType.Text]: 'text', -// [TDShapeType.Image]: 'image', -// [TDShapeType.Video]: 'video', -// [TDShapeType.Group]: 'group', -// [TDShapeType.Arrow]: 'arrow', -// [TDShapeType.Sticky]: 'note', -// [TDShapeType.Draw]: 'draw', -// [TDShapeType.Triangle]: 'geo', -// } - const v1ColorsToV2Colors: Record = { [ColorStyle.White]: 'black', [ColorStyle.Black]: 'black', diff --git a/packages/file-format/src/lib/file.ts b/packages/file-format/src/lib/file.ts index ad774be26..cfc96e4d4 100644 --- a/packages/file-format/src/lib/file.ts +++ b/packages/file-format/src/lib/file.ts @@ -208,7 +208,7 @@ export async function parseAndLoadDocument( forceDarkMode?: boolean ) { const parseFileResult = parseTldrawJsonFile({ - config: TldrawEditorConfig.default, + config: new TldrawEditorConfig(), json: document, instanceId: app.instanceId, userId: app.userId, diff --git a/packages/file-format/src/test/file.test.ts b/packages/file-format/src/test/file.test.ts index 85c0d5c89..ef6fc06b3 100644 --- a/packages/file-format/src/test/file.test.ts +++ b/packages/file-format/src/test/file.test.ts @@ -17,14 +17,14 @@ function serialize(file: TldrawFile): string { describe('parseTldrawJsonFile', () => { it('returns an error if the file is not json', () => { - const result = parseTldrawJsonFile(TldrawEditorConfig.default, 'not json') + const result = parseTldrawJsonFile(new TldrawEditorConfig(), 'not json') assert(!result.ok) expect(result.error.type).toBe('notATldrawFile') }) it('returns an error if the file doesnt look like a tldraw file', () => { const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), JSON.stringify({ not: 'a tldraw file' }) ) assert(!result.ok) @@ -33,10 +33,10 @@ describe('parseTldrawJsonFile', () => { it('returns an error if the file version is too old', () => { const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 0, - schema: TldrawEditorConfig.default.storeSchema.serialize(), + schema: new TldrawEditorConfig().storeSchema.serialize(), records: [], }) ) @@ -46,10 +46,10 @@ describe('parseTldrawJsonFile', () => { it('returns an error if the file version is too new', () => { const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 100, - schema: TldrawEditorConfig.default.storeSchema.serialize(), + schema: new TldrawEditorConfig().storeSchema.serialize(), records: [], }) ) @@ -58,10 +58,10 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if migrations fail', () => { - const serializedSchema = TldrawEditorConfig.default.storeSchema.serialize() + const serializedSchema = new TldrawEditorConfig().storeSchema.serialize() serializedSchema.storeVersion = 100 const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 1, schema: serializedSchema, @@ -72,10 +72,10 @@ describe('parseTldrawJsonFile', () => { assert(result.error.type === 'migrationFailed') expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld) - const serializedSchema2 = TldrawEditorConfig.default.storeSchema.serialize() + const serializedSchema2 = new TldrawEditorConfig().storeSchema.serialize() serializedSchema2.recordVersions.shape.version = 100 const result2 = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 1, schema: serializedSchema2, @@ -90,10 +90,10 @@ describe('parseTldrawJsonFile', () => { it('returns an error if a record is invalid', () => { const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 1, - schema: TldrawEditorConfig.default.storeSchema.serialize(), + schema: new TldrawEditorConfig().storeSchema.serialize(), records: [ { typeName: 'shape', @@ -113,10 +113,10 @@ describe('parseTldrawJsonFile', () => { it('returns a store if the file is valid', () => { const result = parseTldrawJsonFile( - TldrawEditorConfig.default, + new TldrawEditorConfig(), serialize({ tldrawFileFormatVersion: 1, - schema: TldrawEditorConfig.default.storeSchema.serialize(), + schema: new TldrawEditorConfig().storeSchema.serialize(), records: [], }) ) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 3625195a4..90a6c20aa 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -8,9 +8,10 @@ import { TldrawEditorProps } from '@tldraw/editor'; import { TldrawUiContextProviderProps } from '@tldraw/ui'; // @public (undocumented) -export function Tldraw(props: Omit & TldrawUiContextProviderProps & { +export function Tldraw(props: Omit & TldrawUiContextProviderProps & { persistenceKey?: string; hideUi?: boolean; + config?: TldrawEditorProps['config']; }): JSX.Element; diff --git a/packages/tldraw/src/lib/Tldraw.tsx b/packages/tldraw/src/lib/Tldraw.tsx index 8697f3585..06f59b7db 100644 --- a/packages/tldraw/src/lib/Tldraw.tsx +++ b/packages/tldraw/src/lib/Tldraw.tsx @@ -1,4 +1,4 @@ -import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor' +import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor' import { DEFAULT_DOCUMENT_NAME, TAB_ID, @@ -6,18 +6,33 @@ import { useLocalSyncClient, } from '@tldraw/tlsync-client' import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui' +import { useEffect, useState } from 'react' /** @public */ export function Tldraw( - props: Omit & + props: Omit & TldrawUiContextProviderProps & { /** The key under which to persist this editor's data to local storage. */ persistenceKey?: string /** Whether to hide the user interface and only display the canvas. */ hideUi?: boolean + /** A custom configuration for this Tldraw editor */ + config?: TldrawEditorProps['config'] } ) { - const { children, persistenceKey = DEFAULT_DOCUMENT_NAME, instanceId = TAB_ID, ...rest } = props + const { + config, + children, + persistenceKey = DEFAULT_DOCUMENT_NAME, + instanceId = TAB_ID, + ...rest + } = props + + const [_config, _setConfig] = useState(() => config ?? new TldrawEditorConfig()) + + useEffect(() => { + _setConfig(config ?? new TldrawEditorConfig()) + }, [config]) const userData = getUserData() @@ -25,13 +40,19 @@ export function Tldraw( const syncedStore = useLocalSyncClient({ instanceId, - userId: userId, + userId, + config: _config, universalPersistenceKey: persistenceKey, - config: props.config, }) return ( - + diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 5ee8b2e5c..4395a9c02 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -103,6 +103,12 @@ export function createShapeValidator( props: Props; }>; +// @public +export function createTLSchema(opts?: { + customShapes?: { [K in T["type"]]: CustomShapeInfo; } | undefined; + derivePresenceState?: ((store: TLStore) => Signal) | undefined; +}): StoreSchema; + // @public (undocumented) export const cursorTypeValidator: T.Validator; @@ -722,6 +728,9 @@ export interface TLDashStyle extends TLBaseStyle { // @public (undocumented) export type TLDashType = SetValue; +// @public +export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape; + // @public export interface TLDocument extends BaseRecord<'document'> { // (undocumented) @@ -1137,7 +1146,7 @@ export type TLScribble = { }; // @public -export type TLShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLUnknownShape | TLVideoShape; +export type TLShape = TLDefaultShape | TLUnknownShape; // @public (undocumented) export type TLShapeId = ID>; @@ -1155,9 +1164,6 @@ export type TLShapeProp = keyof TLShapeProps; // @public (undocumented) export type TLShapeProps = SmooshedUnionObject; -// @public (undocumented) -export type TLShapeType = TLShape['type']; - // @public (undocumented) export interface TLSizeStyle extends TLBaseStyle { // (undocumented) @@ -1252,7 +1258,7 @@ export type TLTextShapeProps = { // @public (undocumented) export type TLUiColorType = SetValue; -// @public (undocumented) +// @public export type TLUnknownShape = TLBaseShape; // @public diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts new file mode 100644 index 000000000..564f6bbb7 --- /dev/null +++ b/packages/tlschema/src/createTLSchema.ts @@ -0,0 +1,134 @@ +import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore' +import { T } from '@tldraw/tlvalidate' +import { Signal } from 'signia' +import { TLRecord } from './TLRecord' +import { TLStore, TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore' +import { defaultDerivePresenceState } from './defaultDerivePresenceState' +import { TLAsset } from './records/TLAsset' +import { TLCamera } from './records/TLCamera' +import { TLDocument } from './records/TLDocument' +import { TLInstance } from './records/TLInstance' +import { TLInstancePageState } from './records/TLInstancePageState' +import { TLInstancePresence } from './records/TLInstancePresence' +import { TLPage } from './records/TLPage' +import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape' +import { TLUser } from './records/TLUser' +import { TLUserDocument } from './records/TLUserDocument' +import { TLUserPresence } from './records/TLUserPresence' +import { storeMigrations } from './schema' +import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape' +import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape' +import { drawShapeTypeMigrations, drawShapeTypeValidator } from './shapes/TLDrawShape' +import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape' +import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape' +import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape' +import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape' +import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape' +import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape' +import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape' +import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape' +import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape' + +type DefaultShapeInfo = { + validator: T.Validator + migrations: Migrations +} + +const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo> } = + { + arrow: { migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator }, + bookmark: { migrations: bookmarkShapeTypeMigrations, validator: bookmarkShapeTypeValidator }, + draw: { migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator }, + embed: { migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator }, + frame: { migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator }, + geo: { migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator }, + group: { migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator }, + image: { migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator }, + line: { migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator }, + note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator }, + text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator }, + video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator }, + } + +type CustomShapeInfo = { + validator?: { validate: (record: T) => T } + migrations?: Migrations +} + +/** + * Create a store schema for a tldraw store that includes all the default shapes together with any custom shapes. + * @public */ +export function createTLSchema( + opts = {} as { + customShapes?: { [K in T['type']]: CustomShapeInfo } + derivePresenceState?: (store: TLStore) => Signal + } +) { + const { customShapes = {}, derivePresenceState } = opts + + const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [ + TLShape['type'], + DefaultShapeInfo + ][] + + const customShapeSubTypeEntries = Object.entries(customShapes) as [ + T['type'], + CustomShapeInfo + ][] + + // Create a shape record that incorporates the defeault shapes and any custom shapes + // into its subtype migrations and validators, so that we can migrate any new custom + // subtypes. Note that migrations AND validators for custom shapes are optional. If + // not provided, we use an empty migrations set and/or an "any" validator. + + const shapeSubTypeMigrationsWithCustomSubTypeMigrations = { + ...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.migrations])), + ...Object.fromEntries( + customShapeSubTypeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})]) + ), + } + + const validatorWithCustomShapeValidators = T.model( + 'shape', + T.union('type', { + ...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.validator])), + ...Object.fromEntries( + customShapeSubTypeEntries.map(([k, v]) => [k, (v.validator as T.Validator) ?? T.any]) + ), + }) + ) + + const shapeRecord = createRecordType('shape', { + migrations: defineMigrations({ + currentVersion: rootShapeTypeMigrations.currentVersion, + firstVersion: rootShapeTypeMigrations.firstVersion, + migrators: rootShapeTypeMigrations.migrators, + subTypeKey: 'type', + subTypeMigrations: shapeSubTypeMigrationsWithCustomSubTypeMigrations, + }), + validator: validatorWithCustomShapeValidators, + scope: 'document', + }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) + + return StoreSchema.create( + { + asset: TLAsset, + camera: TLCamera, + document: TLDocument, + instance: TLInstance, + instance_page_state: TLInstancePageState, + page: TLPage, + shape: shapeRecord, + user: TLUser, + user_document: TLUserDocument, + user_presence: TLUserPresence, + instance_presence: TLInstancePresence, + }, + { + snapshotMigrations: storeMigrations, + onValidationFailure, + createIntegrityChecker: createIntegrityChecker, + derivePresenceState: derivePresenceState ?? defaultDerivePresenceState, + } + ) +} diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index b21a865d0..ccd19d9ba 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -24,6 +24,7 @@ export { type TLVideoAsset, } from './assets/TLVideoAsset' export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation' +export { createTLSchema } from './createTLSchema' export { defaultDerivePresenceState } from './defaultDerivePresenceState' export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup' export { type Box2dModel, type Vec2dModel } from './geometry-types' @@ -58,6 +59,7 @@ export { isShape, isShapeId, rootShapeTypeMigrations, + type TLDefaultShape, type TLNullableShapeProps, type TLParentId, type TLShape, @@ -65,7 +67,6 @@ export { type TLShapePartial, type TLShapeProp, type TLShapeProps, - type TLShapeType, type TLUnknownShape, } from './records/TLShape' export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser' diff --git a/packages/tlschema/src/records/TLShape.ts b/packages/tlschema/src/records/TLShape.ts index 8f8847ee9..c933555fb 100644 --- a/packages/tlschema/src/records/TLShape.ts +++ b/packages/tlschema/src/records/TLShape.ts @@ -17,15 +17,11 @@ import { TLVideoShape } from '../shapes/TLVideoShape' import { SmooshedUnionObject } from '../util-types' import { TLPageId } from './TLPage' -/** @public */ -export type TLUnknownShape = TLBaseShape - /** - * TLShape + * The default set of shapes that are available in the editor. * - * @public - */ -export type TLShape = + * @public */ +export type TLDefaultShape = | TLArrowShape | TLBookmarkShape | TLDrawShape @@ -38,11 +34,21 @@ export type TLShape = | TLNoteShape | TLTextShape | TLVideoShape - | TLUnknownShape | TLIconShape -/** @public */ -export type TLShapeType = TLShape['type'] +/** + * A type for a shape that is available in the editor but whose type is + * unknown—either one of the editor's default shapes or else a custom shape. + * + * @public */ +export type TLUnknownShape = TLBaseShape + +/** + * The set of all shapes that are available in the editor, including unknown shapes. + * + * @public + */ +export type TLShape = TLDefaultShape | TLUnknownShape /** @public */ export type TLShapePartial = T extends T diff --git a/packages/tlsync-client/api-report.md b/packages/tlsync-client/api-report.md index 59c22fa2c..d3c953fe2 100644 --- a/packages/tlsync-client/api-report.md +++ b/packages/tlsync-client/api-report.md @@ -101,7 +101,7 @@ export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId universalPersistenceKey: string; instanceId: TLInstanceId; userId: TLUserId; - config?: TldrawEditorConfig; + config: TldrawEditorConfig; }): SyncedStore; // (No @packageDocumentation comment for this package) diff --git a/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts b/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts index b03c2a205..726bedfb1 100644 --- a/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts +++ b/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts @@ -34,7 +34,7 @@ function testClient( userId: TLUserId = TLUser.createCustomId('test'), channel = new BroadcastChannelMock('test') ) { - const store = TldrawEditorConfig.default.createStore({ + const store = new TldrawEditorConfig().createStore({ userId, instanceId, }) diff --git a/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts b/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts index 7775fff77..aeab97120 100644 --- a/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts +++ b/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts @@ -14,12 +14,12 @@ export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId, - config = TldrawEditorConfig.default, + config, }: { universalPersistenceKey: string instanceId: TLInstanceId userId: TLUserId - config?: TldrawEditorConfig + config: TldrawEditorConfig }): SyncedStore { const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)