@tldraw/tlschema 4.1.0-next.542f014c3fac → 4.1.0-next.9f145d10c7d0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/TLStore.js +3 -10
- package/dist-cjs/TLStore.js.map +2 -2
- package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
- package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
- package/dist-cjs/assets/TLImageAsset.js.map +2 -2
- package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
- package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +4416 -223
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLColor.js.map +2 -2
- package/dist-cjs/misc/TLCursor.js.map +2 -2
- package/dist-cjs/misc/TLHandle.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/misc/TLScribble.js.map +2 -2
- package/dist-cjs/misc/geometry-types.js.map +2 -2
- package/dist-cjs/misc/id-validator.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +2 -2
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLCamera.js.map +2 -2
- package/dist-cjs/records/TLDocument.js.map +2 -2
- package/dist-cjs/records/TLInstance.js.map +2 -2
- package/dist-cjs/records/TLPage.js.map +2 -2
- package/dist-cjs/records/TLPageState.js.map +2 -2
- package/dist-cjs/records/TLPointer.js.map +2 -2
- package/dist-cjs/records/TLPresence.js.map +2 -2
- package/dist-cjs/records/TLRecord.js.map +1 -1
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/recordsWithProps.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLImageShape.js.map +2 -2
- package/dist-cjs/shapes/TLLineShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-cjs/styles/TLColorStyle.js.map +2 -2
- package/dist-cjs/styles/TLDashStyle.js.map +2 -2
- package/dist-cjs/styles/TLFillStyle.js.map +2 -2
- package/dist-cjs/styles/TLFontStyle.js.map +2 -2
- package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
- package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
- package/dist-cjs/translations/translations.js +1 -1
- package/dist-cjs/translations/translations.js.map +2 -2
- package/dist-cjs/util-types.js.map +1 -1
- package/dist-esm/TLStore.mjs +3 -10
- package/dist-esm/TLStore.mjs.map +2 -2
- package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
- package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
- package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
- package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
- package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +4416 -223
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLColor.mjs.map +2 -2
- package/dist-esm/misc/TLCursor.mjs.map +2 -2
- package/dist-esm/misc/TLHandle.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/misc/TLScribble.mjs.map +2 -2
- package/dist-esm/misc/geometry-types.mjs.map +2 -2
- package/dist-esm/misc/id-validator.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +2 -2
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLCamera.mjs.map +2 -2
- package/dist-esm/records/TLDocument.mjs.map +2 -2
- package/dist-esm/records/TLInstance.mjs.map +2 -2
- package/dist-esm/records/TLPage.mjs.map +2 -2
- package/dist-esm/records/TLPageState.mjs.map +2 -2
- package/dist-esm/records/TLPointer.mjs.map +2 -2
- package/dist-esm/records/TLPresence.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/recordsWithProps.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
- package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
- package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
- package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
- package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
- package/dist-esm/translations/translations.mjs +1 -1
- package/dist-esm/translations/translations.mjs.map +2 -2
- package/package.json +5 -5
- package/src/TLStore.test.ts +644 -0
- package/src/TLStore.ts +205 -20
- package/src/assets/TLBaseAsset.ts +90 -7
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLBookmarkAsset.ts +52 -2
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLImageAsset.ts +60 -2
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/assets/TLVideoAsset.ts +93 -4
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLArrowBinding.ts +132 -10
- package/src/bindings/TLBaseBinding.ts +140 -3
- package/src/createPresenceStateDerivation.test.ts +158 -0
- package/src/createPresenceStateDerivation.ts +71 -2
- package/src/createTLSchema.test.ts +181 -0
- package/src/createTLSchema.ts +164 -7
- package/src/index.ts +32 -0
- package/src/misc/TLColor.ts +50 -6
- package/src/misc/TLCursor.ts +110 -8
- package/src/misc/TLHandle.ts +86 -6
- package/src/misc/TLOpacity.ts +51 -2
- package/src/misc/TLRichText.ts +56 -3
- package/src/misc/TLScribble.ts +105 -5
- package/src/misc/geometry-types.ts +30 -2
- package/src/misc/id-validator.test.ts +50 -0
- package/src/misc/id-validator.ts +20 -1
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +165 -8
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +277 -11
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLCamera.ts +118 -7
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLDocument.ts +148 -8
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLInstance.ts +117 -9
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPage.ts +106 -8
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPageState.ts +88 -7
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPointer.ts +105 -7
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLPresence.ts +99 -5
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLRecord.ts +43 -1
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +289 -12
- package/src/recordsWithProps.test.ts +188 -0
- package/src/recordsWithProps.ts +131 -2
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +64 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +188 -10
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +103 -4
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLBookmarkShape.ts +58 -4
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +97 -6
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +57 -4
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLFrameShape.ts +59 -4
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +103 -7
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLGroupShape.ts +52 -4
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +79 -4
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLImageShape.ts +105 -5
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLLineShape.ts +128 -8
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +97 -4
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +94 -4
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/shapes/TLVideoShape.ts +99 -4
- package/src/store-migrations.test.ts +88 -0
- package/src/store-migrations.ts +47 -1
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/src/styles/TLColorStyle.ts +228 -10
- package/src/styles/TLDashStyle.ts +54 -2
- package/src/styles/TLFillStyle.ts +54 -2
- package/src/styles/TLFontStyle.ts +72 -3
- package/src/styles/TLHorizontalAlignStyle.ts +55 -2
- package/src/styles/TLSizeStyle.ts +54 -2
- package/src/styles/TLTextAlignStyle.ts +52 -2
- package/src/styles/TLVerticalAlignStyle.ts +52 -2
- package/src/translations/translations.test.ts +378 -35
- package/src/translations/translations.ts +157 -10
- package/src/util-types.ts +51 -1
package/src/records/TLPage.ts
CHANGED
|
@@ -10,7 +10,23 @@ import { T } from '@tldraw/validate'
|
|
|
10
10
|
import { idValidator } from '../misc/id-validator'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* A page within a tldraw document. Pages are containers for shapes and provide
|
|
14
|
+
* a way to organize content into separate canvases. Each document can have multiple
|
|
15
|
+
* pages, and users can navigate between them.
|
|
16
|
+
*
|
|
17
|
+
* Pages have a name for identification, an index for ordering, and can store
|
|
18
|
+
* custom metadata.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const page: TLPage = {
|
|
23
|
+
* id: 'page:page1',
|
|
24
|
+
* typeName: 'page',
|
|
25
|
+
* name: 'Page 1',
|
|
26
|
+
* index: 'a1',
|
|
27
|
+
* meta: { description: 'Main design page' }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
14
30
|
*
|
|
15
31
|
* @public
|
|
16
32
|
*/
|
|
@@ -20,13 +36,47 @@ export interface TLPage extends BaseRecord<'page', TLPageId> {
|
|
|
20
36
|
meta: JsonObject
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A unique identifier for TLPage records.
|
|
41
|
+
*
|
|
42
|
+
* Page IDs follow the format 'page:' followed by a unique string identifier.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const pageId: TLPageId = 'page:main'
|
|
47
|
+
* const pageId2: TLPageId = createShapeId() // generates 'page:abc123'
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
24
52
|
export type TLPageId = RecordId<TLPage>
|
|
25
53
|
|
|
26
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Validator for TLPageId values. Ensures the ID follows the correct
|
|
56
|
+
* format for page records ('page:' prefix).
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const isValid = pageIdValidator.isValid('page:main') // true
|
|
61
|
+
* const isValid2 = pageIdValidator.isValid('shape:abc') // false
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
27
66
|
export const pageIdValidator = idValidator<TLPageId>('page')
|
|
28
67
|
|
|
29
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Runtime validator for TLPage records. Validates the structure and types
|
|
70
|
+
* of all page properties to ensure data integrity.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const page = { id: 'page:1', typeName: 'page', name: 'My Page', index: 'a1', meta: {} }
|
|
75
|
+
* const isValid = pageValidator.isValid(page) // true
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
30
80
|
export const pageValidator: T.Validator<TLPage> = T.model(
|
|
31
81
|
'page',
|
|
32
82
|
T.object({
|
|
@@ -38,12 +88,28 @@ export const pageValidator: T.Validator<TLPage> = T.model(
|
|
|
38
88
|
})
|
|
39
89
|
)
|
|
40
90
|
|
|
41
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Migration version identifiers for TLPage records. Each version represents
|
|
93
|
+
* a schema change that requires data transformation when loading older documents.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
42
97
|
export const pageVersions = createMigrationIds('com.tldraw.page', {
|
|
43
98
|
AddMeta: 1,
|
|
44
99
|
})
|
|
45
100
|
|
|
46
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Migration sequence for TLPage records. Defines how to transform page
|
|
103
|
+
* records between different schema versions, ensuring data compatibility.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* // Migrations are applied automatically when loading documents
|
|
108
|
+
* const migratedPage = pageMigrations.migrate(oldPage, targetVersion)
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
47
113
|
export const pageMigrations = createRecordMigrationSequence({
|
|
48
114
|
sequenceId: 'com.tldraw.page',
|
|
49
115
|
recordType: 'page',
|
|
@@ -57,7 +123,24 @@ export const pageMigrations = createRecordMigrationSequence({
|
|
|
57
123
|
],
|
|
58
124
|
})
|
|
59
125
|
|
|
60
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* The RecordType definition for TLPage records. Defines validation, scope,
|
|
128
|
+
* and default properties for page records in the tldraw store.
|
|
129
|
+
*
|
|
130
|
+
* Pages are scoped to the document level, meaning they persist across sessions
|
|
131
|
+
* and are shared in collaborative environments.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const page = PageRecordType.create({
|
|
136
|
+
* id: 'page:main',
|
|
137
|
+
* name: 'Main Page',
|
|
138
|
+
* index: 'a1'
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
61
144
|
export const PageRecordType = createRecordType<TLPage>('page', {
|
|
62
145
|
validator: pageValidator,
|
|
63
146
|
scope: 'document',
|
|
@@ -65,7 +148,22 @@ export const PageRecordType = createRecordType<TLPage>('page', {
|
|
|
65
148
|
meta: {},
|
|
66
149
|
}))
|
|
67
150
|
|
|
68
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Type guard to check if a string is a valid TLPageId.
|
|
153
|
+
*
|
|
154
|
+
* @param id - The string to check
|
|
155
|
+
* @returns True if the ID is a valid page ID, false otherwise
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* if (isPageId('page:main')) {
|
|
160
|
+
* // TypeScript knows this is a TLPageId
|
|
161
|
+
* console.log('Valid page ID')
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @public
|
|
166
|
+
*/
|
|
69
167
|
export function isPageId(id: string): id is TLPageId {
|
|
70
168
|
return PageRecordType.isId(id)
|
|
71
169
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
instancePageStateMigrations,
|
|
4
|
+
InstancePageStateRecordType,
|
|
5
|
+
instancePageStateValidator,
|
|
6
|
+
instancePageStateVersions,
|
|
7
|
+
TLInstancePageStateId,
|
|
8
|
+
} from './TLPageState'
|
|
9
|
+
|
|
10
|
+
describe('instancePageStateValidator', () => {
|
|
11
|
+
it('should reject invalid typeName', () => {
|
|
12
|
+
const invalidPageState = {
|
|
13
|
+
typeName: 'not-instance-page-state',
|
|
14
|
+
id: 'instance_page_state:test' as TLInstancePageStateId,
|
|
15
|
+
pageId: 'page:test' as any,
|
|
16
|
+
selectedShapeIds: [],
|
|
17
|
+
hintingShapeIds: [],
|
|
18
|
+
erasingShapeIds: [],
|
|
19
|
+
hoveredShapeId: null,
|
|
20
|
+
editingShapeId: null,
|
|
21
|
+
croppingShapeId: null,
|
|
22
|
+
focusedGroupId: null,
|
|
23
|
+
meta: {},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
expect(() => instancePageStateValidator.validate(invalidPageState)).toThrow()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should reject invalid id format', () => {
|
|
30
|
+
const invalidPageState = {
|
|
31
|
+
typeName: 'instance_page_state',
|
|
32
|
+
id: 'not-valid-id' as TLInstancePageStateId,
|
|
33
|
+
pageId: 'page:test' as any,
|
|
34
|
+
selectedShapeIds: [],
|
|
35
|
+
hintingShapeIds: [],
|
|
36
|
+
erasingShapeIds: [],
|
|
37
|
+
hoveredShapeId: null,
|
|
38
|
+
editingShapeId: null,
|
|
39
|
+
croppingShapeId: null,
|
|
40
|
+
focusedGroupId: null,
|
|
41
|
+
meta: {},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(() => instancePageStateValidator.validate(invalidPageState)).toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should reject missing required fields', () => {
|
|
48
|
+
const incompletePageState = {
|
|
49
|
+
typeName: 'instance_page_state',
|
|
50
|
+
id: 'instance_page_state:test' as TLInstancePageStateId,
|
|
51
|
+
pageId: 'page:test' as any,
|
|
52
|
+
// missing required array fields
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(() => instancePageStateValidator.validate(incompletePageState)).toThrow()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('instancePageStateMigrations', () => {
|
|
60
|
+
it('should migrate AddCroppingId correctly', () => {
|
|
61
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
62
|
+
(m) => m.id === instancePageStateVersions.AddCroppingId
|
|
63
|
+
)!
|
|
64
|
+
const oldRecord: any = { id: 'instance_page_state:test', typeName: 'instance_page_state' }
|
|
65
|
+
migration.up(oldRecord)
|
|
66
|
+
|
|
67
|
+
expect(oldRecord.croppingShapeId).toBe(null)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should migrate RemoveInstanceIdAndCameraId correctly', () => {
|
|
71
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
72
|
+
(m) => m.id === instancePageStateVersions.RemoveInstanceIdAndCameraId
|
|
73
|
+
)!
|
|
74
|
+
const oldRecord: any = {
|
|
75
|
+
id: 'instance_page_state:test',
|
|
76
|
+
typeName: 'instance_page_state',
|
|
77
|
+
instanceId: 'instance:removed',
|
|
78
|
+
cameraId: 'camera:removed',
|
|
79
|
+
otherProp: 'keep-me',
|
|
80
|
+
}
|
|
81
|
+
migration.up(oldRecord)
|
|
82
|
+
|
|
83
|
+
expect(oldRecord.instanceId).toBeUndefined()
|
|
84
|
+
expect(oldRecord.cameraId).toBeUndefined()
|
|
85
|
+
expect(oldRecord.otherProp).toBe('keep-me')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should migrate AddMeta correctly', () => {
|
|
89
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
90
|
+
(m) => m.id === instancePageStateVersions.AddMeta
|
|
91
|
+
)!
|
|
92
|
+
const oldRecord: any = { id: 'instance_page_state:test', typeName: 'instance_page_state' }
|
|
93
|
+
migration.up(oldRecord)
|
|
94
|
+
|
|
95
|
+
expect(oldRecord.meta).toEqual({})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should handle RenameProperties migration (noop)', () => {
|
|
99
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
100
|
+
(m) => m.id === instancePageStateVersions.RenameProperties
|
|
101
|
+
)!
|
|
102
|
+
const oldRecord: any = {
|
|
103
|
+
id: 'instance_page_state:test',
|
|
104
|
+
typeName: 'instance_page_state',
|
|
105
|
+
selectedIds: ['shape:1'],
|
|
106
|
+
}
|
|
107
|
+
const originalRecord = { ...oldRecord }
|
|
108
|
+
migration.up(oldRecord)
|
|
109
|
+
|
|
110
|
+
// Should be a noop
|
|
111
|
+
expect(oldRecord).toEqual(originalRecord)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should migrate RenamePropertiesAgain correctly', () => {
|
|
115
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
116
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
117
|
+
)!
|
|
118
|
+
const oldRecord: any = {
|
|
119
|
+
id: 'instance_page_state:test',
|
|
120
|
+
typeName: 'instance_page_state',
|
|
121
|
+
selectedIds: ['shape:1', 'shape:2'],
|
|
122
|
+
hintingIds: ['shape:3'],
|
|
123
|
+
erasingIds: ['shape:4'],
|
|
124
|
+
hoveredId: 'shape:5',
|
|
125
|
+
editingId: 'shape:6',
|
|
126
|
+
croppingId: 'shape:7',
|
|
127
|
+
focusLayerId: 'shape:8',
|
|
128
|
+
}
|
|
129
|
+
migration.up(oldRecord)
|
|
130
|
+
|
|
131
|
+
expect(oldRecord.selectedShapeIds).toEqual(['shape:1', 'shape:2'])
|
|
132
|
+
expect(oldRecord.hintingShapeIds).toEqual(['shape:3'])
|
|
133
|
+
expect(oldRecord.erasingShapeIds).toEqual(['shape:4'])
|
|
134
|
+
expect(oldRecord.hoveredShapeId).toBe('shape:5')
|
|
135
|
+
expect(oldRecord.editingShapeId).toBe('shape:6')
|
|
136
|
+
expect(oldRecord.croppingShapeId).toBe('shape:7')
|
|
137
|
+
expect(oldRecord.focusedGroupId).toBe('shape:8')
|
|
138
|
+
|
|
139
|
+
// Old properties should be removed
|
|
140
|
+
expect(oldRecord.selectedIds).toBeUndefined()
|
|
141
|
+
expect(oldRecord.hintingIds).toBeUndefined()
|
|
142
|
+
expect(oldRecord.erasingIds).toBeUndefined()
|
|
143
|
+
expect(oldRecord.hoveredId).toBeUndefined()
|
|
144
|
+
expect(oldRecord.editingId).toBeUndefined()
|
|
145
|
+
expect(oldRecord.croppingId).toBeUndefined()
|
|
146
|
+
expect(oldRecord.focusLayerId).toBeUndefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should handle down migration for RenamePropertiesAgain', () => {
|
|
150
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
151
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
152
|
+
)!
|
|
153
|
+
expect(migration.down).toBeDefined()
|
|
154
|
+
|
|
155
|
+
const record: any = {
|
|
156
|
+
id: 'instance_page_state:test',
|
|
157
|
+
typeName: 'instance_page_state',
|
|
158
|
+
selectedShapeIds: ['shape:1', 'shape:2'],
|
|
159
|
+
hintingShapeIds: ['shape:3'],
|
|
160
|
+
erasingShapeIds: ['shape:4'],
|
|
161
|
+
hoveredShapeId: 'shape:5',
|
|
162
|
+
editingShapeId: 'shape:6',
|
|
163
|
+
croppingShapeId: 'shape:7',
|
|
164
|
+
focusedGroupId: 'shape:8',
|
|
165
|
+
}
|
|
166
|
+
migration.down!(record)
|
|
167
|
+
|
|
168
|
+
expect(record.selectedIds).toEqual(['shape:1', 'shape:2'])
|
|
169
|
+
expect(record.hintingIds).toEqual(['shape:3'])
|
|
170
|
+
expect(record.erasingIds).toEqual(['shape:4'])
|
|
171
|
+
expect(record.hoveredId).toBe('shape:5')
|
|
172
|
+
expect(record.editingId).toBe('shape:6')
|
|
173
|
+
expect(record.croppingId).toBe('shape:7')
|
|
174
|
+
expect(record.focusLayerId).toBe('shape:8')
|
|
175
|
+
|
|
176
|
+
// New properties should be removed
|
|
177
|
+
expect(record.selectedShapeIds).toBeUndefined()
|
|
178
|
+
expect(record.hintingShapeIds).toBeUndefined()
|
|
179
|
+
expect(record.erasingShapeIds).toBeUndefined()
|
|
180
|
+
expect(record.hoveredShapeId).toBeUndefined()
|
|
181
|
+
expect(record.editingShapeId).toBeUndefined()
|
|
182
|
+
expect(record.croppingShapeId).toBeUndefined()
|
|
183
|
+
expect(record.focusedGroupId).toBeUndefined()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should handle croppingShapeId fallback in RenamePropertiesAgain', () => {
|
|
187
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
188
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
189
|
+
)!
|
|
190
|
+
|
|
191
|
+
// Test with existing croppingShapeId
|
|
192
|
+
const recordWithCroppingShapeId: any = {
|
|
193
|
+
croppingShapeId: 'shape:existing',
|
|
194
|
+
croppingId: 'shape:fallback',
|
|
195
|
+
}
|
|
196
|
+
migration.up(recordWithCroppingShapeId)
|
|
197
|
+
expect(recordWithCroppingShapeId.croppingShapeId).toBe('shape:existing')
|
|
198
|
+
|
|
199
|
+
// Test with only croppingId
|
|
200
|
+
const recordWithCroppingId: any = {
|
|
201
|
+
croppingId: 'shape:fallback',
|
|
202
|
+
}
|
|
203
|
+
migration.up(recordWithCroppingId)
|
|
204
|
+
expect(recordWithCroppingId.croppingShapeId).toBe('shape:fallback')
|
|
205
|
+
|
|
206
|
+
// Test with neither
|
|
207
|
+
const recordWithNeither: any = {}
|
|
208
|
+
migration.up(recordWithNeither)
|
|
209
|
+
expect(recordWithNeither.croppingShapeId).toBe(null)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('InstancePageStateRecordType', () => {
|
|
214
|
+
it('should have correct ephemeral keys configuration', () => {
|
|
215
|
+
// Non-ephemeral keys (persistent)
|
|
216
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.pageId).toBe(false)
|
|
217
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.selectedShapeIds).toBe(false)
|
|
218
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.editingShapeId).toBe(false)
|
|
219
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.croppingShapeId).toBe(false)
|
|
220
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.meta).toBe(false)
|
|
221
|
+
|
|
222
|
+
// Ephemeral keys (temporary)
|
|
223
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.hintingShapeIds).toBe(true)
|
|
224
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.erasingShapeIds).toBe(true)
|
|
225
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.hoveredShapeId).toBe(true)
|
|
226
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.focusedGroupId).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -13,9 +13,25 @@ import { pageIdValidator, TLPage } from './TLPage'
|
|
|
13
13
|
import { TLShapeId } from './TLShape'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* State that is unique to a particular page within a particular browser tab.
|
|
17
|
+
* This record tracks all page-specific interaction state including selected shapes,
|
|
18
|
+
* editing state, hover state, and other transient UI state that is tied to
|
|
19
|
+
* both a specific page and a specific browser session.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
21
|
+
* Each combination of page and browser tab has its own TLInstancePageState record.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const pageState: TLInstancePageState = {
|
|
26
|
+
* id: 'instance_page_state:page1',
|
|
27
|
+
* typeName: 'instance_page_state',
|
|
28
|
+
* pageId: 'page:page1',
|
|
29
|
+
* selectedShapeIds: ['shape:rect1', 'shape:circle2'],
|
|
30
|
+
* hoveredShapeId: 'shape:text3',
|
|
31
|
+
* editingShapeId: null,
|
|
32
|
+
* focusedGroupId: null
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
19
35
|
*
|
|
20
36
|
* @public
|
|
21
37
|
*/
|
|
@@ -32,7 +48,24 @@ export interface TLInstancePageState
|
|
|
32
48
|
meta: JsonObject
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Runtime validator for TLInstancePageState records. Validates the structure
|
|
53
|
+
* and types of all instance page state properties to ensure data integrity.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const pageState = {
|
|
58
|
+
* id: 'instance_page_state:page1',
|
|
59
|
+
* typeName: 'instance_page_state',
|
|
60
|
+
* pageId: 'page:page1',
|
|
61
|
+
* selectedShapeIds: ['shape:rect1'],
|
|
62
|
+
* // ... other properties
|
|
63
|
+
* }
|
|
64
|
+
* const isValid = instancePageStateValidator.isValid(pageState) // true
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
36
69
|
export const instancePageStateValidator: T.Validator<TLInstancePageState> = T.model(
|
|
37
70
|
'instance_page_state',
|
|
38
71
|
T.object({
|
|
@@ -50,7 +83,13 @@ export const instancePageStateValidator: T.Validator<TLInstancePageState> = T.mo
|
|
|
50
83
|
})
|
|
51
84
|
)
|
|
52
85
|
|
|
53
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Migration version identifiers for TLInstancePageState records. Each version
|
|
88
|
+
* represents a schema change that requires data transformation when loading
|
|
89
|
+
* older documents.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
54
93
|
export const instancePageStateVersions = createMigrationIds('com.tldraw.instance_page_state', {
|
|
55
94
|
AddCroppingId: 1,
|
|
56
95
|
RemoveInstanceIdAndCameraId: 2,
|
|
@@ -59,7 +98,19 @@ export const instancePageStateVersions = createMigrationIds('com.tldraw.instance
|
|
|
59
98
|
RenamePropertiesAgain: 5,
|
|
60
99
|
} as const)
|
|
61
100
|
|
|
62
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Migration sequence for TLInstancePageState records. Defines how to transform
|
|
103
|
+
* instance page state records between different schema versions, ensuring data
|
|
104
|
+
* compatibility when loading documents created with different versions.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* // Migrations are applied automatically when loading documents
|
|
109
|
+
* const migrated = instancePageStateMigrations.migrate(oldState, targetVersion)
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
63
114
|
export const instancePageStateMigrations = createRecordMigrationSequence({
|
|
64
115
|
sequenceId: 'com.tldraw.instance_page_state',
|
|
65
116
|
recordType: 'instance_page_state',
|
|
@@ -132,7 +183,25 @@ export const instancePageStateMigrations = createRecordMigrationSequence({
|
|
|
132
183
|
],
|
|
133
184
|
})
|
|
134
185
|
|
|
135
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* The RecordType definition for TLInstancePageState records. Defines validation,
|
|
188
|
+
* scope, and default properties for instance page state records.
|
|
189
|
+
*
|
|
190
|
+
* Instance page states are scoped to the session level, meaning they are
|
|
191
|
+
* specific to a browser tab and don't persist across sessions or sync
|
|
192
|
+
* in collaborative environments.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const pageState = InstancePageStateRecordType.create({
|
|
197
|
+
* id: 'instance_page_state:page1',
|
|
198
|
+
* pageId: 'page:page1',
|
|
199
|
+
* selectedShapeIds: ['shape:rect1']
|
|
200
|
+
* })
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @public
|
|
204
|
+
*/
|
|
136
205
|
export const InstancePageStateRecordType = createRecordType<TLInstancePageState>(
|
|
137
206
|
'instance_page_state',
|
|
138
207
|
{
|
|
@@ -164,5 +233,17 @@ export const InstancePageStateRecordType = createRecordType<TLInstancePageState>
|
|
|
164
233
|
})
|
|
165
234
|
)
|
|
166
235
|
|
|
167
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* A unique identifier for TLInstancePageState records.
|
|
238
|
+
*
|
|
239
|
+
* Instance page state IDs follow the format 'instance_page_state:' followed
|
|
240
|
+
* by a unique identifier, typically related to the page ID.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* const stateId: TLInstancePageStateId = 'instance_page_state:page1'
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* @public
|
|
248
|
+
*/
|
|
168
249
|
export type TLInstancePageStateId = RecordId<TLInstancePageState>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { pointerMigrations, pointerValidator, pointerVersions, TLPOINTER_ID } from './TLPointer'
|
|
3
|
+
|
|
4
|
+
describe('pointerValidator', () => {
|
|
5
|
+
it('should validate valid pointer records', () => {
|
|
6
|
+
const validPointer = {
|
|
7
|
+
typeName: 'pointer',
|
|
8
|
+
id: TLPOINTER_ID,
|
|
9
|
+
x: 100,
|
|
10
|
+
y: 200,
|
|
11
|
+
lastActivityTimestamp: Date.now(),
|
|
12
|
+
meta: {},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
expect(() => pointerValidator.validate(validPointer)).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should reject pointers with invalid typeName', () => {
|
|
19
|
+
const invalidPointer = {
|
|
20
|
+
typeName: 'not-pointer',
|
|
21
|
+
id: TLPOINTER_ID,
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
lastActivityTimestamp: 0,
|
|
25
|
+
meta: {},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(() => pointerValidator.validate(invalidPointer)).toThrow()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject pointers with missing required fields', () => {
|
|
32
|
+
const incompletePointer = {
|
|
33
|
+
typeName: 'pointer',
|
|
34
|
+
id: TLPOINTER_ID,
|
|
35
|
+
x: 0,
|
|
36
|
+
// missing y, lastActivityTimestamp, meta
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(() => pointerValidator.validate(incompletePointer)).toThrow()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('pointerMigrations', () => {
|
|
44
|
+
it('should apply AddMeta migration correctly', () => {
|
|
45
|
+
const addMetaMigration = pointerMigrations.sequence.find(
|
|
46
|
+
(m) => m.id === pointerVersions.AddMeta
|
|
47
|
+
)!
|
|
48
|
+
|
|
49
|
+
const oldRecord: any = {
|
|
50
|
+
typeName: 'pointer',
|
|
51
|
+
id: TLPOINTER_ID,
|
|
52
|
+
x: 100,
|
|
53
|
+
y: 200,
|
|
54
|
+
lastActivityTimestamp: 123456,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addMetaMigration.up(oldRecord)
|
|
58
|
+
expect(oldRecord.meta).toEqual({})
|
|
59
|
+
expect(oldRecord.x).toBe(100)
|
|
60
|
+
expect(oldRecord.y).toBe(200)
|
|
61
|
+
expect(oldRecord.lastActivityTimestamp).toBe(123456)
|
|
62
|
+
})
|
|
63
|
+
})
|