From 44f0a3d6d04f8128789b348073e8a47edc5e9d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 14:32:41 +0200 Subject: [PATCH 1/9] Add result type. --- .../AfterCreateUpdateShapeExample.tsx | 27 +++--- .../BeforeDeleteShapeExample.tsx | 39 ++++----- packages/editor/api-report.md | 2 +- packages/editor/src/lib/editor/Editor.ts | 17 ++-- .../BaseBoxShapeTool/children/Pointing.ts | 36 +++++--- .../editor/src/lib/editor/types/misc-types.ts | 40 +++++++++ .../src/lib/defaultExternalContentHandlers.ts | 14 ++- .../lib/shapes/arrow/ArrowShapeUtil.test.ts | 4 +- .../src/lib/shapes/geo/toolStates/Pointing.ts | 46 +++++----- .../lib/shapes/text/toolStates/Pointing.ts | 29 ++++--- .../DebugMenu/DefaultDebugMenuContent.tsx | 3 +- packages/tldraw/src/test/SelectTool.test.ts | 14 ++- .../commands/getInitialMetaForShape.test.ts | 6 +- packages/tldraw/src/test/duplicate.test.ts | 20 ++--- packages/tldraw/src/test/frames.test.ts | 1 + packages/tldraw/src/test/paste.test.ts | 27 +++--- packages/tldraw/src/test/resizing.test.ts | 2 +- .../tldraw/src/test/selection-omnibus.test.ts | 85 +++++++++---------- 18 files changed, 236 insertions(+), 176 deletions(-) diff --git a/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx b/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx index f5414a755..b735e7cba 100644 --- a/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx +++ b/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx @@ -56,17 +56,18 @@ export default function AfterCreateUpdateShapeExample() { // create some shapes to demonstrate the side-effects we added function createDemoShapes(editor: Editor) { - editor - .createShapes( - 'there can only be one red shape'.split(' ').map((word, i) => ({ - id: createShapeId(), - type: 'text', - y: i * 30, - props: { - color: i === 5 ? 'red' : 'black', - text: word, - }, - })) - ) - .zoomToContent({ duration: 0 }) + const result = editor.createShapes( + 'there can only be one red shape'.split(' ').map((word, i) => ({ + id: createShapeId(), + type: 'text', + y: i * 30, + props: { + color: i === 5 ? 'red' : 'black', + text: word, + }, + })) + ) + if (result.ok) { + editor.zoomToContent({ duration: 0 }) + } } diff --git a/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx b/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx index a4ab4db37..617e17a7b 100644 --- a/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx +++ b/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx @@ -24,25 +24,26 @@ export default function BeforeDeleteShapeExample() { // create some shapes to demonstrate the side-effect we added function createDemoShapes(editor: Editor) { - editor - .createShapes([ - { - id: createShapeId(), - type: 'text', - props: { - text: "Red shapes can't be deleted", - color: 'red', - }, + const result = editor.createShapes([ + { + id: createShapeId(), + type: 'text', + props: { + text: "Red shapes can't be deleted", + color: 'red', }, - { - id: createShapeId(), - type: 'text', - y: 30, - props: { - text: 'but other shapes can', - color: 'black', - }, + }, + { + id: createShapeId(), + type: 'text', + y: 30, + props: { + text: 'but other shapes can', + color: 'black', }, - ]) - .zoomToContent({ duration: 0 }) + }, + ]) + if (result.ok) { + editor.zoomToContent({ duration: 0 }) + } } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 99c6779e9..0e5f1915a 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -634,7 +634,7 @@ export class Editor extends EventEmitter { }; createPage(page: Partial): this; createShape(shape: OptionalKeys, 'id'>): this; - createShapes(shapes: OptionalKeys, 'id'>[]): this; + createShapes(shapes: OptionalKeys, 'id'>[]): EditorResult; deleteAssets(assets: TLAsset[] | TLAssetId[]): this; deleteOpenMenu(id: string): this; deletePage(page: TLPage | TLPageId): this; diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 88e44208f..37be641a3 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -129,7 +129,7 @@ import { } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' import { TLHistoryBatchOptions } from './types/history-types' -import { OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types' +import { EditorResult, OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types' import { TLResizeHandle } from './types/selection-types' /** @public */ @@ -6242,12 +6242,14 @@ export class Editor extends EventEmitter { * * @public */ - createShapes(shapes: OptionalKeys, 'id'>[]): this { + createShapes( + shapes: OptionalKeys, 'id'>[] + ): EditorResult { if (!Array.isArray(shapes)) { - throw Error('Editor.createShapes: must provide an array of shapes or shape partials') + return EditorResult.error('not-an-array-of-shapes') } - if (this.getInstanceState().isReadonly) return this - if (shapes.length <= 0) return this + if (this.getInstanceState().isReadonly) return EditorResult.error('readonly-room') + if (shapes.length <= 0) return EditorResult.error('no-shapes-provied') const currentPageShapeIds = this.getCurrentPageShapeIds() @@ -6256,12 +6258,12 @@ export class Editor extends EventEmitter { if (maxShapesReached) { // can't create more shapes than fit on the page alertMaxShapes(this) - return this + return EditorResult.error('max-shapes-reached') } const focusedGroupId = this.getFocusedGroupId() - return this.batch(() => { + this.batch(() => { // 1. Parents // Make sure that each partial will become the child of either the @@ -6419,6 +6421,7 @@ export class Editor extends EventEmitter { this.store.put(shapeRecordsToCreate) }) + return EditorResult.ok() } private animatingShapes = new Map() diff --git a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts index f9339d06d..790d4aab9 100644 --- a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +++ b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts @@ -28,20 +28,24 @@ export class Pointing extends StateNode { this.editor.mark(this.markId) - this.editor - .createShapes([ - { - id, - type: shapeType, - x: originPagePoint.x, - y: originPagePoint.y, - props: { - w: 1, - h: 1, - }, + const result = this.editor.createShapes([ + { + id, + type: shapeType, + x: originPagePoint.x, + y: originPagePoint.y, + props: { + w: 1, + h: 1, }, - ]) - .select(id) + }, + ]) + if (!result.ok) { + this.cancel() + return + } + + this.editor.select(id) this.editor.setCurrentTool('select.resizing', { ...info, target: 'selection', @@ -85,7 +89,7 @@ export class Pointing extends StateNode { this.editor.mark(this.markId) - this.editor.createShapes([ + const result = this.editor.createShapes([ { id, type: shapeType, @@ -93,6 +97,10 @@ export class Pointing extends StateNode { y: originPagePoint.y, }, ]) + if (!result.ok) { + this.cancel() + return + } const shape = this.editor.getShape(id)! const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props'] diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 6851e726d..befae341f 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -14,3 +14,43 @@ export type TLSvgOptions = { darkMode?: boolean preserveAspectRatio: React.SVGAttributes['preserveAspectRatio'] } + +export type TLEditorErrorType = keyof typeof TLEditorErrorTypeMap + +export type TLEditorError = { message: string; type: TLEditorErrorType } + +const TLEditorErrorTypeMap = { + 'not-an-array-of-shapes': { + message: 'createShapes requires an array of shapes', + type: 'not-an-array-of-shapes' as const, + }, + 'no-shapes-provied': { + message: 'No shapes provided', + type: 'no-shapes-provied' as const, + }, + 'readonly-room': { + message: 'Room is readonly', + type: 'readonly-room' as const, + }, + 'max-shapes-reached': { + message: 'Max shapes reached', + type: 'max-shapes-reached' as const, + }, +} + +export type ErrorResult = { ok: false; error: TLEditorError } +export type OkResult = { ok: true } +export type OkResultWithValue = { ok: true; value: T } + +export type EditorResult = ErrorResult | OkResult | OkResultWithValue +export const EditorResult = { + ok(): OkResult { + return { ok: true } + }, + okWithValue(value: T): OkResultWithValue { + return { ok: true, value } + }, + error(errorType: TLEditorErrorType): ErrorResult { + return { ok: false, error: TLEditorErrorTypeMap[errorType] } + }, +} diff --git a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts index d35def4c7..01e0dd333 100644 --- a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts +++ b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts @@ -203,7 +203,10 @@ export function registerDefaultExternalContentHandlers( }, } - editor.createShapes([shapePartial]).select(id) + const result = editor.createShapes([shapePartial]) + if (result.ok) { + editor.select(id) + } }) // files @@ -483,7 +486,10 @@ export async function createShapesForAssets( } // Create the shapes - editor.createShapes(partials).select(...partials.map((p) => p.id)) + const result = editor.createShapes(partials) + if (!result.ok) return + + editor.select(...partials.map((p) => p.id)) // Re-position shapes so that the center of the group is at the provided point centerSelectionAroundPoint(editor, position) @@ -539,7 +545,9 @@ export function createEmptyBookmarkShape( } editor.batch(() => { - editor.createShapes([partial]).select(partial.id) + const result = editor.createShapes([partial]) + if (!result.ok) return + editor.select(partial.id) centerSelectionAroundPoint(editor, position) }) diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts index 2105d6d4a..b378a43ed 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts @@ -294,8 +294,8 @@ describe('Other cases when arrow are moved', () => { { id: ids.box3, type: 'geo', x: 0, y: 300, props: { w: 100, h: 100 } }, { id: ids.box4, type: 'geo', x: 0, y: 600, props: { w: 100, h: 100 } }, ]) - .selectAll() - .groupShapes(editor.getSelectedShapeIds()) + + editor.selectAll().groupShapes(editor.getSelectedShapeIds()) editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350) let arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] diff --git a/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts index 3445efd16..214d4a37f 100644 --- a/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts @@ -26,29 +26,31 @@ export class Pointing extends StateNode { this.editor.mark(this.markId) - this.editor - .createShapes([ - { - id, - type: 'geo', - x: originPagePoint.x, - y: originPagePoint.y, - props: { - w: 1, - h: 1, - geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle), - }, + const result = this.editor.createShapes([ + { + id, + type: 'geo', + x: originPagePoint.x, + y: originPagePoint.y, + props: { + w: 1, + h: 1, + geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle), }, - ]) - .select(id) - .setCurrentTool('select.resizing', { - ...info, - target: 'selection', - handle: 'bottom_right', - isCreating: true, - creationCursorOffset: { x: 1, y: 1 }, - onInteractionEnd: 'geo', - }) + }, + ]) + if (!result.ok) { + this.cancel() + return + } + this.editor.select(id).setCurrentTool('select.resizing', { + ...info, + target: 'selection', + handle: 'bottom_right', + isCreating: true, + creationCursorOffset: { x: 1, y: 1 }, + onInteractionEnd: 'geo', + }) } } diff --git a/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts index 1fc8bb648..6850ca12e 100644 --- a/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts @@ -78,21 +78,24 @@ export class Pointing extends StateNode { this.editor.mark('creating text shape') const id = createShapeId() const { x, y } = this.editor.inputs.currentPagePoint - this.editor - .createShapes([ - { - id, - type: 'text', - x, - y, - props: { - text: '', - autoSize: true, - }, + const result = this.editor.createShapes([ + { + id, + type: 'text', + x, + y, + props: { + text: '', + autoSize: true, }, - ]) - .select(id) + }, + ]) + if (!result.ok) { + this.cancel() + return + } + this.editor.select(id) this.editor.setEditingShape(id) this.editor.setCurrentTool('select') this.editor.root.getCurrent()?.transition('editing_shape') diff --git a/packages/tldraw/src/lib/ui/components/DebugMenu/DefaultDebugMenuContent.tsx b/packages/tldraw/src/lib/ui/components/DebugMenu/DefaultDebugMenuContent.tsx index 68b69382a..8076b9f6a 100644 --- a/packages/tldraw/src/lib/ui/components/DebugMenu/DefaultDebugMenuContent.tsx +++ b/packages/tldraw/src/lib/ui/components/DebugMenu/DefaultDebugMenuContent.tsx @@ -295,6 +295,7 @@ function createNShapes(editor: Editor, n: number) { } editor.batch(() => { - editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id)) + editor.createShapes(shapesToCreate) + editor.setSelectedShapes(shapesToCreate.map((s) => s.id)) }) } diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index c02db8605..f23e8c5ba 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -292,6 +292,7 @@ describe('When double clicking a shape', () => { .deleteShapes(editor.getSelectedShapeIds()) .selectNone() .createShapes([{ id: createShapeId(), type: 'geo' }]) + editor .doubleClick(50, 50, { target: 'shape', shape: editor.getCurrentPageShapes()[0] }) .expectToBeIn('select.editing_shape') }) @@ -305,9 +306,7 @@ describe('When pressing enter on a selected shape', () => { .deleteShapes(editor.getSelectedShapeIds()) .selectNone() .createShapes([{ id, type: 'geo' }]) - .select(id) - .keyUp('Enter') - .expectToBeIn('select.editing_shape') + editor.select(id).keyUp('Enter').expectToBeIn('select.editing_shape') }) }) @@ -336,8 +335,7 @@ describe('When double clicking the selection edge', () => { .deleteShapes(editor.getSelectedShapeIds()) .selectNone() .createShapes([{ id, type: 'text', x: 100, y: 100, props: { scale: 2, text: 'hello' } }]) - .select(id) - .doubleClick(100, 100, { target: 'selection', handle: 'left' }) + editor.select(id).doubleClick(100, 100, { target: 'selection', handle: 'left' }) editor.expectShapeToMatch({ id, props: { scale: 1 } }) }) @@ -355,8 +353,7 @@ describe('When double clicking the selection edge', () => { props: { scale: 2, autoSize: false, w: 200, text: 'hello' }, }, ]) - .select(id) - .doubleClick(100, 100, { target: 'selection', handle: 'left' }) + editor.select(id).doubleClick(100, 100, { target: 'selection', handle: 'left' }) editor.expectShapeToMatch({ id, props: { scale: 2, autoSize: true } }) @@ -378,6 +375,7 @@ describe('When double clicking the selection edge', () => { props: { scale: 2, autoSize: false, w: 200, text: 'hello' }, }, ]) + editor .select(id) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) @@ -402,7 +400,7 @@ describe('When double clicking the selection edge', () => { type: 'geo', }, ]) - .select(id) + editor.select(id) expect(editor.getEditingShapeId()).toBe(null) editor.doubleClick(100, 100, { target: 'selection', handle: 'left' }) diff --git a/packages/tldraw/src/test/commands/getInitialMetaForShape.test.ts b/packages/tldraw/src/test/commands/getInitialMetaForShape.test.ts index 4622ec964..094f5f1da 100644 --- a/packages/tldraw/src/test/commands/getInitialMetaForShape.test.ts +++ b/packages/tldraw/src/test/commands/getInitialMetaForShape.test.ts @@ -9,13 +9,15 @@ beforeEach(() => { it('Sets shape meta by default to an empty object', () => { const id = createShapeId() - editor.createShapes([{ id, type: 'geo' }]).select(id) + editor.createShapes([{ id, type: 'geo' }]) + editor.select(id) expect(editor.getOnlySelectedShape()!.meta).toStrictEqual({}) }) it('Sets shape meta', () => { editor.getInitialMetaForShape = (shape) => ({ firstThreeCharactersOfId: shape.id.slice(0, 3) }) const id = createShapeId() - editor.createShapes([{ id, type: 'geo' }]).select(id) + editor.createShapes([{ id, type: 'geo' }]) + editor.select(id) expect(editor.getOnlySelectedShape()!.meta).toStrictEqual({ firstThreeCharactersOfId: 'sha' }) }) diff --git a/packages/tldraw/src/test/duplicate.test.ts b/packages/tldraw/src/test/duplicate.test.ts index c8461db90..b79d63428 100644 --- a/packages/tldraw/src/test/duplicate.test.ts +++ b/packages/tldraw/src/test/duplicate.test.ts @@ -182,7 +182,8 @@ describe('When duplicating shapes that include arrows', () => { }) it('Preserves the same selection bounds', () => { - editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes).selectAll() + editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes) + editor.selectAll() const boundsBefore = editor.getSelectionRotatedPageBounds()! editor.duplicateShapes(editor.getSelectedShapeIds()) @@ -190,16 +191,13 @@ describe('When duplicating shapes that include arrows', () => { }) it('Preserves the same selection bounds when only duplicating the arrows', () => { - editor - .selectAll() - .deleteShapes(editor.getSelectedShapeIds()) - .createShapes(shapes) - .select( - ...editor - .getCurrentPageShapes() - .filter((s) => editor.isShapeOfType(s, 'arrow')) - .map((s) => s.id) - ) + editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes) + editor.select( + ...editor + .getCurrentPageShapes() + .filter((s) => editor.isShapeOfType(s, 'arrow')) + .map((s) => s.id) + ) const boundsBefore = editor.getSelectionRotatedPageBounds()! editor.duplicateShapes(editor.getSelectedShapeIds()) diff --git a/packages/tldraw/src/test/frames.test.ts b/packages/tldraw/src/test/frames.test.ts index 8631133fe..553e16ea3 100644 --- a/packages/tldraw/src/test/frames.test.ts +++ b/packages/tldraw/src/test/frames.test.ts @@ -177,6 +177,7 @@ describe('frame shapes', () => { editor // Create a frame .createShapes([{ id: frameId, type: 'frame', x: 100, y: 100, props: { w: 100, h: 100 } }]) + editor .select(frameId) // Rotate it by PI/2 .rotateSelection(Math.PI / 2) diff --git a/packages/tldraw/src/test/paste.test.ts b/packages/tldraw/src/test/paste.test.ts index cc1c26d3b..dc9a00dcf 100644 --- a/packages/tldraw/src/test/paste.test.ts +++ b/packages/tldraw/src/test/paste.test.ts @@ -395,7 +395,7 @@ describe('When pasting into frames...', () => { y: 500, }, ]) - .setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 }) + editor.setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 }) // put frame2 inside frame1 editor.reparentShapes([ids.frame2], ids.frame1) @@ -455,20 +455,19 @@ describe('When pasting into frames...', () => { editor.deleteShapes(editor.getSelectedShapeIds()) // create a big frame away from the origin, the size of the viewport - editor - .createShapes([ - { - id: ids.frame1, - type: 'frame', - x: editor.getViewportScreenBounds().w, - y: editor.getViewportScreenBounds().h, - props: { - w: editor.getViewportScreenBounds().w, - h: editor.getViewportScreenBounds().h, - }, + editor.createShapes([ + { + id: ids.frame1, + type: 'frame', + x: editor.getViewportScreenBounds().w, + y: editor.getViewportScreenBounds().h, + props: { + w: editor.getViewportScreenBounds().w, + h: editor.getViewportScreenBounds().h, }, - ]) - .selectAll() + }, + ]) + editor.selectAll() // rotate the frame for hard mode editor.rotateSelection(45) // center on the center of the frame diff --git a/packages/tldraw/src/test/resizing.test.ts b/packages/tldraw/src/test/resizing.test.ts index 651944642..2e7783d37 100644 --- a/packages/tldraw/src/test/resizing.test.ts +++ b/packages/tldraw/src/test/resizing.test.ts @@ -912,7 +912,7 @@ describe('When resizing a shape with children', () => { }, }, ]) - .select(ids.boxB, ids.lineA) + editor.select(ids.boxB, ids.lineA) editor .pointerDown(10, 10, { diff --git a/packages/tldraw/src/test/selection-omnibus.test.ts b/packages/tldraw/src/test/selection-omnibus.test.ts index e02acabf0..7259703fd 100644 --- a/packages/tldraw/src/test/selection-omnibus.test.ts +++ b/packages/tldraw/src/test/selection-omnibus.test.ts @@ -1132,13 +1132,12 @@ describe('Selects inside of groups', () => { describe('when selecting behind selection', () => { beforeEach(() => { - editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 100, y: 0, props: { fill: 'solid' } }, - { id: ids.box2, type: 'geo', x: 0, y: 0 }, - { id: ids.box3, type: 'geo', x: 200, y: 0 }, - ]) - .select(ids.box2, ids.box3) + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 100, y: 0, props: { fill: 'solid' } }, + { id: ids.box2, type: 'geo', x: 0, y: 0 }, + { id: ids.box3, type: 'geo', x: 200, y: 0 }, + ]) + editor.select(ids.box2, ids.box3) }) it('does not select on pointer down, only on pointer up', () => { @@ -1165,13 +1164,12 @@ describe('when selecting behind selection', () => { describe('when shift+selecting', () => { beforeEach(() => { - editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 0, y: 0 }, - { id: ids.box2, type: 'geo', x: 200, y: 0 }, - { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, - ]) - .select(ids.box1) + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 0, y: 0 }, + { id: ids.box2, type: 'geo', x: 200, y: 0 }, + { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, + ]) + editor.select(ids.box1) }) it('adds solid shape to selection on pointer down', () => { @@ -1281,15 +1279,13 @@ describe('when shift+selecting', () => { describe('when shift+selecting a group', () => { beforeEach(() => { - editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 0, y: 0 }, - { id: ids.box2, type: 'geo', x: 200, y: 0 }, - { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, - { id: ids.box4, type: 'geo', x: 600, y: 0 }, - ]) - .groupShapes([ids.box2, ids.box3], ids.group1) - .select(ids.box1) + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 0, y: 0 }, + { id: ids.box2, type: 'geo', x: 200, y: 0 }, + { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, + { id: ids.box4, type: 'geo', x: 600, y: 0 }, + ]) + editor.groupShapes([ids.box2, ids.box3], ids.group1).select(ids.box1) }) it('does not add group to selection when pointing empty space in the group', () => { @@ -1362,14 +1358,14 @@ describe('when shift+selecting a group', () => { describe('When children / descendants of a group are selected', () => { beforeEach(() => { + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 0, y: 0 }, + { id: ids.box2, type: 'geo', x: 200, y: 0 }, + { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, + { id: ids.box4, type: 'geo', x: 600, y: 0 }, + { id: ids.box5, type: 'geo', x: 800, y: 0 }, + ]) editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 0, y: 0 }, - { id: ids.box2, type: 'geo', x: 200, y: 0 }, - { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, - { id: ids.box4, type: 'geo', x: 600, y: 0 }, - { id: ids.box5, type: 'geo', x: 800, y: 0 }, - ]) .groupShapes([ids.box1, ids.box2], ids.group1) .groupShapes([ids.box3, ids.box4], ids.group2) .groupShapes([ids.group1, ids.group2], ids.group3) @@ -1437,14 +1433,14 @@ describe('When children / descendants of a group are selected', () => { describe('When pressing the enter key with groups selected', () => { beforeEach(() => { + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 0, y: 0 }, + { id: ids.box2, type: 'geo', x: 200, y: 0 }, + { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, + { id: ids.box4, type: 'geo', x: 600, y: 0 }, + { id: ids.box5, type: 'geo', x: 800, y: 0 }, + ]) editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 0, y: 0 }, - { id: ids.box2, type: 'geo', x: 200, y: 0 }, - { id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } }, - { id: ids.box4, type: 'geo', x: 600, y: 0 }, - { id: ids.box5, type: 'geo', x: 800, y: 0 }, - ]) .groupShapes([ids.box1, ids.box2], ids.group1) .groupShapes([ids.box3, ids.box4], ids.group2) }) @@ -1545,14 +1541,13 @@ describe('When double clicking an editable shape', () => { describe('shift brushes to add to the selection', () => { beforeEach(() => { editor.user.updateUserPreferences({ isWrapMode: false }) - editor - .createShapes([ - { id: ids.box1, type: 'geo', x: 0, y: 0 }, - { id: ids.box2, type: 'geo', x: 200, y: 0 }, - { id: ids.box3, type: 'geo', x: 400, y: 0 }, - { id: ids.box4, type: 'geo', x: 600, y: 200 }, - ]) - .groupShapes([ids.box3, ids.box4], ids.group1) + editor.createShapes([ + { id: ids.box1, type: 'geo', x: 0, y: 0 }, + { id: ids.box2, type: 'geo', x: 200, y: 0 }, + { id: ids.box3, type: 'geo', x: 400, y: 0 }, + { id: ids.box4, type: 'geo', x: 600, y: 200 }, + ]) + editor.groupShapes([ids.box3, ids.box4], ids.group1) }) it('does not select when brushing into margin', () => { From 760a7358729589207f29c1f69663853f71bbb9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 14:40:14 +0200 Subject: [PATCH 2/9] Also fix create shape. --- packages/editor/api-report.md | 2 +- packages/editor/src/lib/editor/Editor.ts | 5 +-- .../src/lib/shapes/geo/toolStates/Pointing.ts | 6 ++- .../src/lib/shapes/note/NoteShapeTool.test.ts | 9 +++-- .../lib/shapes/note/toolStates/Pointing.ts | 33 +++++++++++----- packages/tldraw/src/test/SelectTool.test.ts | 2 +- .../tldraw/src/test/selection-omnibus.test.ts | 39 +++++++++---------- packages/tldraw/src/test/translating.test.ts | 22 +++++------ 8 files changed, 67 insertions(+), 51 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 0e5f1915a..bfda225fe 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -633,7 +633,7 @@ export class Editor extends EventEmitter { }; }; createPage(page: Partial): this; - createShape(shape: OptionalKeys, 'id'>): this; + createShape(shape: OptionalKeys, 'id'>): EditorResult; createShapes(shapes: OptionalKeys, 'id'>[]): EditorResult; deleteAssets(assets: TLAsset[] | TLAssetId[]): this; deleteOpenMenu(id: string): this; diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 37be641a3..ae5ee9e0b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -6223,9 +6223,8 @@ export class Editor extends EventEmitter { * * @public */ - createShape(shape: OptionalKeys, 'id'>): this { - this.createShapes([shape]) - return this + createShape(shape: OptionalKeys, 'id'>) { + return this.createShapes([shape]) } /** diff --git a/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts index 214d4a37f..6a885c8d8 100644 --- a/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/geo/toolStates/Pointing.ts @@ -75,7 +75,7 @@ export class Pointing extends StateNode { this.editor.mark(this.markId) - this.editor.createShapes([ + const result = this.editor.createShapes([ { id, type: 'geo', @@ -88,6 +88,10 @@ export class Pointing extends StateNode { }, }, ]) + if (!result.ok) { + this.cancel() + return + } const shape = this.editor.getShape(id)! if (!shape) return diff --git a/packages/tldraw/src/lib/shapes/note/NoteShapeTool.test.ts b/packages/tldraw/src/lib/shapes/note/NoteShapeTool.test.ts index 1395e4c99..c02357546 100644 --- a/packages/tldraw/src/lib/shapes/note/NoteShapeTool.test.ts +++ b/packages/tldraw/src/lib/shapes/note/NoteShapeTool.test.ts @@ -190,8 +190,8 @@ describe('Grid placement helpers', () => { }) it('Falls into a sticky pit when empty', () => { + editor.createShape({ type: 'note', x: 0, y: 0 }) editor - .createShape({ type: 'note', x: 0, y: 0 }) .setCurrentTool('note') .pointerMove(324, 104) .click() @@ -204,9 +204,9 @@ describe('Grid placement helpers', () => { }) it('Does not create a new sticky note in a sticky pit if a note is already there', () => { + editor.createShape({ type: 'note', x: 0, y: 0 }) + editor.createShape({ type: 'note', x: 330, y: 8 }) // make a shape kinda there already! editor - .createShape({ type: 'note', x: 0, y: 0 }) - .createShape({ type: 'note', x: 330, y: 8 }) // make a shape kinda there already! .setCurrentTool('note') .pointerMove(300, 104) .click() @@ -241,7 +241,8 @@ describe('Grid placement helpers', () => { }) it('Falls into correct pit below notes with growY', () => { - editor.createShape({ type: 'note', x: 0, y: 0 }).updateShape({ + editor.createShape({ type: 'note', x: 0, y: 0 }) + editor.updateShape({ ...editor.getLastCreatedShape(), props: { growY: 100 }, }) diff --git a/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts index 835fe5fd4..b8fce4738 100644 --- a/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts @@ -40,7 +40,12 @@ export class Pointing extends StateNode { if (offset) { center.sub(offset) } - this.shape = createSticky(this.editor, id, center) + const result = createSticky(this.editor, id, center) + if (!result) { + this.cancel() + return + } + this.shape = result } } @@ -53,7 +58,12 @@ export class Pointing extends StateNode { if (offset) { center.sub(offset) } - this.shape = createSticky(this.editor, id, center) + const result = createSticky(this.editor, id, center) + if (!result) { + this.cancel() + return + } + this.shape = result } this.editor.setCurrentTool('select.translating', { @@ -123,14 +133,17 @@ export function getNotePitOffset(editor: Editor, center: Vec) { } export function createSticky(editor: Editor, id: TLShapeId, center: Vec) { - editor - .createShape({ - id, - type: 'note', - x: center.x, - y: center.y, - }) - .select(id) + const result = editor.createShape({ + id, + type: 'note', + x: center.x, + y: center.y, + }) + if (!result.ok) { + return null + } + + editor.select(id) const shape = editor.getShape(id)! const bounds = editor.getShapeGeometry(shape).bounds diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index f23e8c5ba..45e6d626b 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -563,8 +563,8 @@ describe('When in readonly mode', () => { // This should be end to end, the problem is the blur handler of the react component it('goes into pointing canvas', () => { + editor.createShape({ type: 'note' }) editor - .createShape({ type: 'note' }) .pointerMove(50, 50) .doubleClick() .expectToBeIn('select.editing_shape') diff --git a/packages/tldraw/src/test/selection-omnibus.test.ts b/packages/tldraw/src/test/selection-omnibus.test.ts index 7259703fd..f8897876a 100644 --- a/packages/tldraw/src/test/selection-omnibus.test.ts +++ b/packages/tldraw/src/test/selection-omnibus.test.ts @@ -680,26 +680,25 @@ describe('when a frame has multiple children', () => { let box1: TLGeoShape let box2: TLGeoShape beforeEach(() => { - editor - .createShape({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } }) - .createShape({ - id: ids.box1, - parentId: ids.frame1, - type: 'geo', - x: 25, - y: 25, - }) - .createShape({ - id: ids.box2, - parentId: ids.frame1, - type: 'geo', - x: 50, - y: 50, - props: { - w: 80, - h: 80, - }, - }) + editor.createShape({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } }) + editor.createShape({ + id: ids.box1, + parentId: ids.frame1, + type: 'geo', + x: 25, + y: 25, + }) + editor.createShape({ + id: ids.box2, + parentId: ids.frame1, + type: 'geo', + x: 50, + y: 50, + props: { + w: 80, + h: 80, + }, + }) box1 = editor.getShape(ids.box1)! box2 = editor.getShape(ids.box2)! }) diff --git a/packages/tldraw/src/test/translating.test.ts b/packages/tldraw/src/test/translating.test.ts index 412e68c7a..69d2c107e 100644 --- a/packages/tldraw/src/test/translating.test.ts +++ b/packages/tldraw/src/test/translating.test.ts @@ -1954,9 +1954,9 @@ const defaultPitLocations = [ describe('Note shape grid helper positions / pits', () => { it('Snaps to pits', () => { + editor.createShape({ type: 'note' }) + editor.createShape({ type: 'note', x: 500, y: 500 }) editor - .createShape({ type: 'note' }) - .createShape({ type: 'note', x: 500, y: 500 }) .pointerMove(600, 600) // start translating .pointerDown() @@ -1971,9 +1971,9 @@ describe('Note shape grid helper positions / pits', () => { }) it('Does not snap to pit if shape has a different rotation', () => { + editor.createShape({ type: 'note', rotation: 0.001 }) + editor.createShape({ type: 'note', x: 500, y: 500 }) editor - .createShape({ type: 'note', rotation: 0.001 }) - .createShape({ type: 'note', x: 500, y: 500 }) .pointerMove(600, 600) // start translating .pointerDown() @@ -1989,9 +1989,9 @@ describe('Note shape grid helper positions / pits', () => { }) it('Snaps to pit if shape has the same rotation', () => { + editor.createShape({ type: 'note', rotation: 0.001 }) + editor.createShape({ type: 'note', x: 500, y: 500, rotation: 0.001 }) editor - .createShape({ type: 'note', rotation: 0.001 }) - .createShape({ type: 'note', x: 500, y: 500, rotation: 0.001 }) .pointerMove(600, 600) // start translating .pointerDown() @@ -2008,9 +2008,9 @@ describe('Note shape grid helper positions / pits', () => { }) it('Snaps correctly to the top when the translating shape has growY', () => { + editor.createShape({ type: 'note' }) + editor.createShape({ type: 'note', x: 500, y: 500 }) editor - .createShape({ type: 'note' }) - .createShape({ type: 'note', x: 500, y: 500 }) .updateShape({ ...editor.getLastCreatedShape(), props: { growY: 100 } }) .pointerMove(600, 600) // start translating @@ -2028,10 +2028,10 @@ describe('Note shape grid helper positions / pits', () => { }) it('Snaps correctly to the bottom when the not-translating shape has growY', () => { + editor.createShape({ type: 'note' }) + editor.updateShape({ ...editor.getLastCreatedShape(), props: { growY: 100 } }) + editor.createShape({ type: 'note', x: 500, y: 500 }) editor - .createShape({ type: 'note' }) - .updateShape({ ...editor.getLastCreatedShape(), props: { growY: 100 } }) - .createShape({ type: 'note', x: 500, y: 500 }) .pointerMove(600, 600) // start translating .pointerDown() From c596da4603c8d1d7c7607199bc64c343100601a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:22:10 +0200 Subject: [PATCH 3/9] Refactor. --- packages/editor/api-report.md | 4 +- packages/editor/src/lib/editor/Editor.ts | 22 +++++--- .../editor/src/lib/editor/types/misc-types.ts | 53 ++++++++++--------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index bfda225fe..44e7ad3d4 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -633,8 +633,8 @@ export class Editor extends EventEmitter { }; }; createPage(page: Partial): this; - createShape(shape: OptionalKeys, 'id'>): EditorResult; - createShapes(shapes: OptionalKeys, 'id'>[]): EditorResult; + createShape(shape: OptionalKeys, 'id'>): EditorResult; + createShapes(shapes: OptionalKeys, 'id'>[]): EditorResult; deleteAssets(assets: TLAsset[] | TLAssetId[]): this; deleteOpenMenu(id: string): this; deletePage(page: TLPage | TLPageId): this; diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index ae5ee9e0b..cf2a67fb1 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -129,7 +129,17 @@ import { } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' import { TLHistoryBatchOptions } from './types/history-types' -import { EditorResult, OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types' +import { + CreateShapeError, + EditorResult, + MAX_SHAPES_REACHED_ERROR_ERROR, + NOT_ARRAY_OF_SHAPES_ERROR, + NO_SHAPES_PROVIDED_ERROR, + OptionalKeys, + READONLY_ROOM_ERROR, + RequiredKeys, + TLSvgOptions, +} from './types/misc-types' import { TLResizeHandle } from './types/selection-types' /** @public */ @@ -6243,12 +6253,12 @@ export class Editor extends EventEmitter { */ createShapes( shapes: OptionalKeys, 'id'>[] - ): EditorResult { + ): EditorResult { if (!Array.isArray(shapes)) { - return EditorResult.error('not-an-array-of-shapes') + return EditorResult.error(NOT_ARRAY_OF_SHAPES_ERROR) } - if (this.getInstanceState().isReadonly) return EditorResult.error('readonly-room') - if (shapes.length <= 0) return EditorResult.error('no-shapes-provied') + if (this.getInstanceState().isReadonly) return EditorResult.error(READONLY_ROOM_ERROR) + if (shapes.length <= 0) return EditorResult.error(NO_SHAPES_PROVIDED_ERROR) const currentPageShapeIds = this.getCurrentPageShapeIds() @@ -6257,7 +6267,7 @@ export class Editor extends EventEmitter { if (maxShapesReached) { // can't create more shapes than fit on the page alertMaxShapes(this) - return EditorResult.error('max-shapes-reached') + return EditorResult.error(MAX_SHAPES_REACHED_ERROR_ERROR) } const focusedGroupId = this.getFocusedGroupId() diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index befae341f..c1ddbd368 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -15,34 +15,35 @@ export type TLSvgOptions = { preserveAspectRatio: React.SVGAttributes['preserveAspectRatio'] } -export type TLEditorErrorType = keyof typeof TLEditorErrorTypeMap +// General errors +export const READONLY_ROOM_ERROR = { type: 'readonly-room' as const, message: 'Room is readonly' } -export type TLEditorError = { message: string; type: TLEditorErrorType } - -const TLEditorErrorTypeMap = { - 'not-an-array-of-shapes': { - message: 'createShapes requires an array of shapes', - type: 'not-an-array-of-shapes' as const, - }, - 'no-shapes-provied': { - message: 'No shapes provided', - type: 'no-shapes-provied' as const, - }, - 'readonly-room': { - message: 'Room is readonly', - type: 'readonly-room' as const, - }, - 'max-shapes-reached': { - message: 'Max shapes reached', - type: 'max-shapes-reached' as const, - }, +// Create shape errors +export const NOT_ARRAY_OF_SHAPES_ERROR = { + type: 'not-array' as const, + message: 'Expected an array', +} +export const NO_SHAPES_PROVIDED_ERROR = { + type: 'no-shapes-provided' as const, + message: 'No shapes provided', +} +export const MAX_SHAPES_REACHED_ERROR_ERROR = { + type: 'max-shapes-reached' as const, + message: 'Max shapes reached', } -export type ErrorResult = { ok: false; error: TLEditorError } -export type OkResult = { ok: true } -export type OkResultWithValue = { ok: true; value: T } +export type CreateShapeErrorType = + | (typeof READONLY_ROOM_ERROR)['type'] + | (typeof NOT_ARRAY_OF_SHAPES_ERROR)['type'] + | (typeof NO_SHAPES_PROVIDED_ERROR)['type'] + | (typeof MAX_SHAPES_REACHED_ERROR_ERROR)['type'] +export type CreateShapeError = { type: CreateShapeErrorType; message: string } -export type EditorResult = ErrorResult | OkResult | OkResultWithValue +export type OkResult = { readonly ok: true } +export type OkResultWithValue = { readonly ok: true; readonly value: T } +export type ErrorResult = { readonly ok: false; readonly error: E } + +export type EditorResult = ErrorResult | OkResult | OkResultWithValue export const EditorResult = { ok(): OkResult { return { ok: true } @@ -50,7 +51,7 @@ export const EditorResult = { okWithValue(value: T): OkResultWithValue { return { ok: true, value } }, - error(errorType: TLEditorErrorType): ErrorResult { - return { ok: false, error: TLEditorErrorTypeMap[errorType] } + error(error: E): ErrorResult { + return { ok: false, error } }, } From 10314018aab85ea04ab39f56d42297a1c859a2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:25:15 +0200 Subject: [PATCH 4/9] Rename. --- packages/editor/src/lib/editor/types/misc-types.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index c1ddbd368..6b523b5c8 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -39,19 +39,19 @@ export type CreateShapeErrorType = | (typeof MAX_SHAPES_REACHED_ERROR_ERROR)['type'] export type CreateShapeError = { type: CreateShapeErrorType; message: string } -export type OkResult = { readonly ok: true } -export type OkResultWithValue = { readonly ok: true; readonly value: T } -export type ErrorResult = { readonly ok: false; readonly error: E } +export type Ok = { readonly ok: true } +export type OkWithValue = { readonly ok: true; readonly value: T } +export type Error = { readonly ok: false; readonly error: E } -export type EditorResult = ErrorResult | OkResult | OkResultWithValue +export type EditorResult = Error | Ok | OkWithValue export const EditorResult = { - ok(): OkResult { + ok(): Ok { return { ok: true } }, - okWithValue(value: T): OkResultWithValue { + okWithValue(value: T): OkWithValue { return { ok: true, value } }, - error(error: E): ErrorResult { + error(error: E): Error { return { ok: false, error } }, } From 4ec9af11d724343cd823395829e0fb994fccf1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:31:22 +0200 Subject: [PATCH 5/9] Move to a separate file. --- packages/editor/src/lib/editor/Editor.ts | 20 ++++----- .../lib/editor/types/editor-result-types.ts | 41 +++++++++++++++++++ .../editor/src/lib/editor/types/misc-types.ts | 41 ------------------- 3 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 packages/editor/src/lib/editor/types/editor-result-types.ts diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index cf2a67fb1..b94ef401f 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -120,6 +120,14 @@ import { getStraightArrowInfo } from './shapes/shared/arrow/straight-arrow' import { RootState } from './tools/RootState' import { StateNode, TLStateNodeConstructor } from './tools/StateNode' import { TLContent } from './types/clipboard-types' +import { + CreateShapeError, + EditorResult, + MAX_SHAPES_REACHED_ERROR_ERROR, + NOT_ARRAY_OF_SHAPES_ERROR, + NO_SHAPES_PROVIDED_ERROR, + READONLY_ROOM_ERROR, +} from './types/editor-result-types' import { TLEventMap } from './types/emit-types' import { TLEventInfo, @@ -129,17 +137,7 @@ import { } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' import { TLHistoryBatchOptions } from './types/history-types' -import { - CreateShapeError, - EditorResult, - MAX_SHAPES_REACHED_ERROR_ERROR, - NOT_ARRAY_OF_SHAPES_ERROR, - NO_SHAPES_PROVIDED_ERROR, - OptionalKeys, - READONLY_ROOM_ERROR, - RequiredKeys, - TLSvgOptions, -} from './types/misc-types' +import { OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types' import { TLResizeHandle } from './types/selection-types' /** @public */ diff --git a/packages/editor/src/lib/editor/types/editor-result-types.ts b/packages/editor/src/lib/editor/types/editor-result-types.ts new file mode 100644 index 000000000..ee413e1ad --- /dev/null +++ b/packages/editor/src/lib/editor/types/editor-result-types.ts @@ -0,0 +1,41 @@ +// Result types +export type Ok = { readonly ok: true } +export type OkWithValue = { readonly ok: true; readonly value: T } +export type Error = { readonly ok: false; readonly error: E } + +export type EditorResult = Error | Ok | OkWithValue +export const EditorResult = { + ok(): Ok { + return { ok: true } + }, + okWithValue(value: T): OkWithValue { + return { ok: true, value } + }, + error(error: E): Error { + return { ok: false, error } + }, +} + +// General errors +export const READONLY_ROOM_ERROR = { type: 'readonly-room' as const, message: 'Room is readonly' } + +// Create shape errors +export const NOT_ARRAY_OF_SHAPES_ERROR = { + type: 'not-array' as const, + message: 'Expected an array', +} +export const NO_SHAPES_PROVIDED_ERROR = { + type: 'no-shapes-provided' as const, + message: 'No shapes provided', +} +export const MAX_SHAPES_REACHED_ERROR_ERROR = { + type: 'max-shapes-reached' as const, + message: 'Max shapes reached', +} + +export type CreateShapeErrorType = + | (typeof READONLY_ROOM_ERROR)['type'] + | (typeof NOT_ARRAY_OF_SHAPES_ERROR)['type'] + | (typeof NO_SHAPES_PROVIDED_ERROR)['type'] + | (typeof MAX_SHAPES_REACHED_ERROR_ERROR)['type'] +export type CreateShapeError = { type: CreateShapeErrorType; message: string } diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 6b523b5c8..6851e726d 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -14,44 +14,3 @@ export type TLSvgOptions = { darkMode?: boolean preserveAspectRatio: React.SVGAttributes['preserveAspectRatio'] } - -// General errors -export const READONLY_ROOM_ERROR = { type: 'readonly-room' as const, message: 'Room is readonly' } - -// Create shape errors -export const NOT_ARRAY_OF_SHAPES_ERROR = { - type: 'not-array' as const, - message: 'Expected an array', -} -export const NO_SHAPES_PROVIDED_ERROR = { - type: 'no-shapes-provided' as const, - message: 'No shapes provided', -} -export const MAX_SHAPES_REACHED_ERROR_ERROR = { - type: 'max-shapes-reached' as const, - message: 'Max shapes reached', -} - -export type CreateShapeErrorType = - | (typeof READONLY_ROOM_ERROR)['type'] - | (typeof NOT_ARRAY_OF_SHAPES_ERROR)['type'] - | (typeof NO_SHAPES_PROVIDED_ERROR)['type'] - | (typeof MAX_SHAPES_REACHED_ERROR_ERROR)['type'] -export type CreateShapeError = { type: CreateShapeErrorType; message: string } - -export type Ok = { readonly ok: true } -export type OkWithValue = { readonly ok: true; readonly value: T } -export type Error = { readonly ok: false; readonly error: E } - -export type EditorResult = Error | Ok | OkWithValue -export const EditorResult = { - ok(): Ok { - return { ok: true } - }, - okWithValue(value: T): OkWithValue { - return { ok: true, value } - }, - error(error: E): Error { - return { ok: false, error } - }, -} From de3dc5dffa70af9aa216da23af2e3277db1ab770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:32:43 +0200 Subject: [PATCH 6/9] Make things public. --- .../src/lib/editor/types/editor-result-types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/editor/src/lib/editor/types/editor-result-types.ts b/packages/editor/src/lib/editor/types/editor-result-types.ts index ee413e1ad..b42324f50 100644 --- a/packages/editor/src/lib/editor/types/editor-result-types.ts +++ b/packages/editor/src/lib/editor/types/editor-result-types.ts @@ -1,9 +1,15 @@ // Result types +/** @public */ export type Ok = { readonly ok: true } +/** @public */ export type OkWithValue = { readonly ok: true; readonly value: T } +/** @public */ export type Error = { readonly ok: false; readonly error: E } +/** @public */ export type EditorResult = Error | Ok | OkWithValue + +/** @internal */ export const EditorResult = { ok(): Ok { return { ok: true } @@ -17,25 +23,31 @@ export const EditorResult = { } // General errors +/** @public */ export const READONLY_ROOM_ERROR = { type: 'readonly-room' as const, message: 'Room is readonly' } // Create shape errors +/** @public */ export const NOT_ARRAY_OF_SHAPES_ERROR = { type: 'not-array' as const, message: 'Expected an array', } +/** @public */ export const NO_SHAPES_PROVIDED_ERROR = { type: 'no-shapes-provided' as const, message: 'No shapes provided', } +/** @public */ export const MAX_SHAPES_REACHED_ERROR_ERROR = { type: 'max-shapes-reached' as const, message: 'Max shapes reached', } +/** @public */ export type CreateShapeErrorType = | (typeof READONLY_ROOM_ERROR)['type'] | (typeof NOT_ARRAY_OF_SHAPES_ERROR)['type'] | (typeof NO_SHAPES_PROVIDED_ERROR)['type'] | (typeof MAX_SHAPES_REACHED_ERROR_ERROR)['type'] +/** @public */ export type CreateShapeError = { type: CreateShapeErrorType; message: string } From e2e9c50e03579486a4298dbc18346cb080450313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:51:15 +0200 Subject: [PATCH 7/9] Emit errors. --- packages/editor/api-report.md | 18 ++++++++++++------ packages/editor/src/index.ts | 6 +++++- packages/editor/src/lib/editor/Editor.ts | 5 ++++- .../editor/src/lib/editor/types/emit-types.ts | 8 +++++++- .../tldraw/src/lib/ui/hooks/useEditorEvents.ts | 10 ++++++---- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 44e7ad3d4..226d855fc 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2192,6 +2192,16 @@ export interface TLErrorBoundaryProps { onError?: ((error: unknown) => void) | null; } +// @public (undocumented) +export type TLErrorEvent = { + type: 'max-shapes'; + value: [{ + count: number; + name: string; + pageId: TLPageId; + }]; +}; + // @public (undocumented) export interface TLEventHandlers { // (undocumented) @@ -2235,12 +2245,6 @@ export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEvent // @public (undocumented) export interface TLEventMap { - // (undocumented) - 'max-shapes': [{ - count: number; - name: string; - pageId: TLPageId; - }]; // (undocumented) 'select-all-text': [{ shapeId: TLShapeId; @@ -2256,6 +2260,8 @@ export interface TLEventMap { error: unknown; }]; // (undocumented) + error: [TLErrorEvent]; + // (undocumented) event: [TLEventInfo]; // (undocumented) frame: [number]; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index d89708f3e..4be109566 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -196,7 +196,11 @@ export { type SvgExportDef, } from './lib/editor/types/SvgExportContext' export { type TLContent } from './lib/editor/types/clipboard-types' -export { type TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types' +export { + type TLErrorEvent, + type TLEventMap, + type TLEventMapHandler, +} from './lib/editor/types/emit-types' export { EVENT_NAME_MAP, type TLBaseEventInfo, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index b94ef401f..408e124b7 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8519,7 +8519,10 @@ export class Editor extends EventEmitter { function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) { const name = editor.getPage(pageId)!.name - editor.emit('max-shapes', { name, pageId, count: MAX_SHAPES_PER_PAGE }) + editor.emit('error', { + type: 'max-shapes', + value: [{ name, pageId, count: MAX_SHAPES_PER_PAGE }], + }) } function applyPartialToShape(prev: T, partial?: TLShapePartial): T { diff --git a/packages/editor/src/lib/editor/types/emit-types.ts b/packages/editor/src/lib/editor/types/emit-types.ts index 3104f51e9..5c2ba744e 100644 --- a/packages/editor/src/lib/editor/types/emit-types.ts +++ b/packages/editor/src/lib/editor/types/emit-types.ts @@ -2,11 +2,16 @@ import { HistoryEntry } from '@tldraw/store' import { TLPageId, TLRecord, TLShapeId } from '@tldraw/tlschema' import { TLEventInfo } from './event-types' +/** @public */ +export type TLErrorEvent = { + type: 'max-shapes' + value: [{ name: string; pageId: TLPageId; count: number }] +} + /** @public */ export interface TLEventMap { // Lifecycle / Internal mount: [] - 'max-shapes': [{ name: string; pageId: TLPageId; count: number }] change: [HistoryEntry] update: [] crash: [{ error: unknown }] @@ -16,6 +21,7 @@ export interface TLEventMap { tick: [number] frame: [number] 'select-all-text': [{ shapeId: TLShapeId }] + error: [TLErrorEvent] } /** @public */ diff --git a/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts b/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts index e1a353b53..ce54365e3 100644 --- a/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts +++ b/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts @@ -1,4 +1,4 @@ -import { useEditor } from '@tldraw/editor' +import { TLErrorEvent, useEditor } from '@tldraw/editor' import { useEffect } from 'react' import { useToasts } from '../context/toasts' @@ -8,7 +8,9 @@ export function useEditorEvents() { const { addToast } = useToasts() useEffect(() => { - function handleMaxShapes({ name, count }: { name: string; pageId: string; count: number }) { + function handleMaxShapes(error: TLErrorEvent) { + if (error.type !== 'max-shapes') return + const [{ name, count }] = error.value addToast({ title: 'Maximum Shapes Reached', description: `You've reached the maximum number of shapes allowed on ${name} (${count}). Please delete some shapes or move to a different page to continue.`, @@ -16,9 +18,9 @@ export function useEditorEvents() { }) } - editor.addListener('max-shapes', handleMaxShapes) + editor.addListener('error', handleMaxShapes) return () => { - editor.removeListener('max-shapes', handleMaxShapes) + editor.removeListener('error', handleMaxShapes) } }, [editor, addToast]) } From 999a33d4ec79acaa8717f7ccdbc0e154ef80e5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:55:53 +0200 Subject: [PATCH 8/9] Use the same types. --- packages/editor/api-report.md | 2 +- packages/editor/src/lib/editor/Editor.ts | 2 +- packages/editor/src/lib/editor/types/editor-result-types.ts | 3 +++ packages/editor/src/lib/editor/types/emit-types.ts | 3 ++- packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 226d855fc..78f34f08a 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2194,7 +2194,7 @@ export interface TLErrorBoundaryProps { // @public (undocumented) export type TLErrorEvent = { - type: 'max-shapes'; + type: TLEditorErrorType; value: [{ count: number; name: string; diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 408e124b7..71a417f81 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8520,7 +8520,7 @@ export class Editor extends EventEmitter { function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) { const name = editor.getPage(pageId)!.name editor.emit('error', { - type: 'max-shapes', + type: 'max-shapes-reached', value: [{ name, pageId, count: MAX_SHAPES_PER_PAGE }], }) } diff --git a/packages/editor/src/lib/editor/types/editor-result-types.ts b/packages/editor/src/lib/editor/types/editor-result-types.ts index b42324f50..1679fae3d 100644 --- a/packages/editor/src/lib/editor/types/editor-result-types.ts +++ b/packages/editor/src/lib/editor/types/editor-result-types.ts @@ -22,6 +22,9 @@ export const EditorResult = { }, } +// All errors +export type TLEditorErrorType = CreateShapeErrorType | (typeof READONLY_ROOM_ERROR)['type'] + // General errors /** @public */ export const READONLY_ROOM_ERROR = { type: 'readonly-room' as const, message: 'Room is readonly' } diff --git a/packages/editor/src/lib/editor/types/emit-types.ts b/packages/editor/src/lib/editor/types/emit-types.ts index 5c2ba744e..9cb20ab1e 100644 --- a/packages/editor/src/lib/editor/types/emit-types.ts +++ b/packages/editor/src/lib/editor/types/emit-types.ts @@ -1,10 +1,11 @@ import { HistoryEntry } from '@tldraw/store' import { TLPageId, TLRecord, TLShapeId } from '@tldraw/tlschema' +import { TLEditorErrorType } from './editor-result-types' import { TLEventInfo } from './event-types' /** @public */ export type TLErrorEvent = { - type: 'max-shapes' + type: TLEditorErrorType value: [{ name: string; pageId: TLPageId; count: number }] } diff --git a/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts b/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts index ce54365e3..c1f609760 100644 --- a/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts +++ b/packages/tldraw/src/lib/ui/hooks/useEditorEvents.ts @@ -9,7 +9,7 @@ export function useEditorEvents() { useEffect(() => { function handleMaxShapes(error: TLErrorEvent) { - if (error.type !== 'max-shapes') return + if (error.type !== 'max-shapes-reached') return const [{ name, count }] = error.value addToast({ title: 'Maximum Shapes Reached', From dfc5ceae2124f561f50f1af403496ea2b2479e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 26 Apr 2024 15:57:34 +0200 Subject: [PATCH 9/9] For now we don't need to use all of them. --- packages/editor/api-report.md | 2 +- packages/editor/src/lib/editor/types/editor-result-types.ts | 1 + packages/editor/src/lib/editor/types/emit-types.ts | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 78f34f08a..504f2373c 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2194,7 +2194,7 @@ export interface TLErrorBoundaryProps { // @public (undocumented) export type TLErrorEvent = { - type: TLEditorErrorType; + type: 'max-shapes-reached'; value: [{ count: number; name: string; diff --git a/packages/editor/src/lib/editor/types/editor-result-types.ts b/packages/editor/src/lib/editor/types/editor-result-types.ts index 1679fae3d..acbeaba18 100644 --- a/packages/editor/src/lib/editor/types/editor-result-types.ts +++ b/packages/editor/src/lib/editor/types/editor-result-types.ts @@ -23,6 +23,7 @@ export const EditorResult = { } // All errors +/** @public */ export type TLEditorErrorType = CreateShapeErrorType | (typeof READONLY_ROOM_ERROR)['type'] // General errors diff --git a/packages/editor/src/lib/editor/types/emit-types.ts b/packages/editor/src/lib/editor/types/emit-types.ts index 9cb20ab1e..ea33c2073 100644 --- a/packages/editor/src/lib/editor/types/emit-types.ts +++ b/packages/editor/src/lib/editor/types/emit-types.ts @@ -1,11 +1,10 @@ import { HistoryEntry } from '@tldraw/store' import { TLPageId, TLRecord, TLShapeId } from '@tldraw/tlschema' -import { TLEditorErrorType } from './editor-result-types' import { TLEventInfo } from './event-types' /** @public */ export type TLErrorEvent = { - type: TLEditorErrorType + type: 'max-shapes-reached' value: [{ name: string; pageId: TLPageId; count: number }] }