@tldraw/tlschema 4.1.0-canary.8e597b345c40 → 4.1.0-canary.95d46c96eb30
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 +4412 -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 +4412 -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 +82 -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
|
@@ -10,17 +10,58 @@ import { JsonObject } from '@tldraw/utils'
|
|
|
10
10
|
import { T } from '@tldraw/validate'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Document record containing global settings and metadata for a tldraw document.
|
|
14
|
+
* There is exactly one document record per tldraw instance with a fixed ID.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const document: TLDocument = {
|
|
19
|
+
* id: 'document:document',
|
|
20
|
+
* typeName: 'document',
|
|
21
|
+
* gridSize: 20, // Grid snap size in pixels
|
|
22
|
+
* name: 'My Drawing', // Document name
|
|
23
|
+
* meta: {
|
|
24
|
+
* createdAt: Date.now(),
|
|
25
|
+
* author: 'user123',
|
|
26
|
+
* version: '1.0.0'
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* // Update document settings
|
|
31
|
+
* editor.updateDocumentSettings({
|
|
32
|
+
* name: 'Updated Drawing',
|
|
33
|
+
* gridSize: 25
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
14
36
|
*
|
|
15
37
|
* @public
|
|
16
38
|
*/
|
|
17
39
|
export interface TLDocument extends BaseRecord<'document', RecordId<TLDocument>> {
|
|
40
|
+
/** Grid snap size in pixels. Used for shape positioning and alignment */
|
|
18
41
|
gridSize: number
|
|
42
|
+
/** Human-readable name of the document */
|
|
19
43
|
name: string
|
|
44
|
+
/** User-defined metadata for the document */
|
|
20
45
|
meta: JsonObject
|
|
21
46
|
}
|
|
22
47
|
|
|
23
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Validator for TLDocument records that ensures runtime type safety.
|
|
50
|
+
* Enforces the fixed document ID and validates all document properties.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Validation happens automatically when document is stored
|
|
55
|
+
* try {
|
|
56
|
+
* const validatedDocument = documentValidator.validate(documentData)
|
|
57
|
+
* store.put([validatedDocument])
|
|
58
|
+
* } catch (error) {
|
|
59
|
+
* console.error('Document validation failed:', error.message)
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
24
65
|
export const documentValidator: T.Validator<TLDocument> = T.model(
|
|
25
66
|
'document',
|
|
26
67
|
T.object({
|
|
@@ -32,19 +73,72 @@ export const documentValidator: T.Validator<TLDocument> = T.model(
|
|
|
32
73
|
})
|
|
33
74
|
)
|
|
34
75
|
|
|
35
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Type guard to check if a record is a TLDocument.
|
|
78
|
+
* Useful for filtering or type narrowing when working with mixed record types.
|
|
79
|
+
*
|
|
80
|
+
* @param record - The record to check
|
|
81
|
+
* @returns True if the record is a document, false otherwise
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // Type guard usage
|
|
86
|
+
* function processRecord(record: UnknownRecord) {
|
|
87
|
+
* if (isDocument(record)) {
|
|
88
|
+
* // record is now typed as TLDocument
|
|
89
|
+
* console.log(`Document: ${record.name}, Grid: ${record.gridSize}px`)
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* // Filter documents from mixed records
|
|
94
|
+
* const allRecords = store.allRecords()
|
|
95
|
+
* const documents = allRecords.filter(isDocument) // Should be exactly one
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
36
100
|
export function isDocument(record?: UnknownRecord): record is TLDocument {
|
|
37
101
|
if (!record) return false
|
|
38
102
|
return record.typeName === 'document'
|
|
39
103
|
}
|
|
40
104
|
|
|
41
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Migration version identifiers for document record schema evolution.
|
|
107
|
+
* Each version represents a breaking change that requires data migration.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* // Check if document needs migration
|
|
112
|
+
* const needsNameMigration = currentVersion < documentVersions.AddName
|
|
113
|
+
* const needsMetaMigration = currentVersion < documentVersions.AddMeta
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
42
118
|
export const documentVersions = createMigrationIds('com.tldraw.document', {
|
|
43
119
|
AddName: 1,
|
|
44
120
|
AddMeta: 2,
|
|
45
121
|
} as const)
|
|
46
122
|
|
|
47
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Migration sequence for evolving document record structure over time.
|
|
125
|
+
* Handles converting document records from older schema versions to current format.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* // Migration is applied automatically when loading old documents
|
|
130
|
+
* const migratedStore = migrator.migrateStoreSnapshot({
|
|
131
|
+
* schema: oldSchema,
|
|
132
|
+
* store: oldStoreSnapshot
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* // The migrations:
|
|
136
|
+
* // v1: Added 'name' property with empty string default
|
|
137
|
+
* // v2: Added 'meta' property with empty object default
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
48
142
|
export const documentMigrations = createRecordMigrationSequence({
|
|
49
143
|
sequenceId: 'com.tldraw.document',
|
|
50
144
|
recordType: 'document',
|
|
@@ -67,7 +161,32 @@ export const documentMigrations = createRecordMigrationSequence({
|
|
|
67
161
|
],
|
|
68
162
|
})
|
|
69
163
|
|
|
70
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Record type definition for TLDocument with validation and default properties.
|
|
166
|
+
* Configures the document as a document-scoped record that persists across sessions.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* // Create a document record (usually done automatically)
|
|
171
|
+
* const documentRecord = DocumentRecordType.create({
|
|
172
|
+
* id: TLDOCUMENT_ID,
|
|
173
|
+
* name: 'My Drawing',
|
|
174
|
+
* gridSize: 20,
|
|
175
|
+
* meta: { createdAt: Date.now() }
|
|
176
|
+
* })
|
|
177
|
+
*
|
|
178
|
+
* // Create with defaults
|
|
179
|
+
* const defaultDocument = DocumentRecordType.create({
|
|
180
|
+
* id: TLDOCUMENT_ID
|
|
181
|
+
* // gridSize: 10, name: '', meta: {} are applied as defaults
|
|
182
|
+
* })
|
|
183
|
+
*
|
|
184
|
+
* // Store the document
|
|
185
|
+
* store.put([documentRecord])
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @public
|
|
189
|
+
*/
|
|
71
190
|
export const DocumentRecordType = createRecordType<TLDocument>('document', {
|
|
72
191
|
validator: documentValidator,
|
|
73
192
|
scope: 'document',
|
|
@@ -79,6 +198,27 @@ export const DocumentRecordType = createRecordType<TLDocument>('document', {
|
|
|
79
198
|
})
|
|
80
199
|
)
|
|
81
200
|
|
|
82
|
-
|
|
83
|
-
|
|
201
|
+
/**
|
|
202
|
+
* The fixed ID for the singleton document record in every tldraw store.
|
|
203
|
+
* All document records use this same ID: 'document:document'
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* // Get the document from store
|
|
208
|
+
* const document = store.get(TLDOCUMENT_ID)
|
|
209
|
+
*
|
|
210
|
+
* // Update document settings
|
|
211
|
+
* store.put([{
|
|
212
|
+
* ...document,
|
|
213
|
+
* name: 'Updated Name',
|
|
214
|
+
* gridSize: 25
|
|
215
|
+
* }])
|
|
216
|
+
*
|
|
217
|
+
* // Access via editor
|
|
218
|
+
* const documentSettings = editor.getDocumentSettings()
|
|
219
|
+
* editor.updateDocumentSettings({ name: 'New Name' })
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
84
224
|
export const TLDOCUMENT_ID: RecordId<TLDocument> = DocumentRecordType.createId('document')
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { StyleProp } from '../styles/StyleProp'
|
|
3
|
+
import {
|
|
4
|
+
createInstanceRecordType,
|
|
5
|
+
instanceIdValidator,
|
|
6
|
+
instanceMigrations,
|
|
7
|
+
instanceVersions,
|
|
8
|
+
pluckPreservingValues,
|
|
9
|
+
shouldKeyBePreservedBetweenSessions,
|
|
10
|
+
TLInstance,
|
|
11
|
+
TLINSTANCE_ID,
|
|
12
|
+
} from './TLInstance'
|
|
13
|
+
|
|
14
|
+
// Mock style prop for testing
|
|
15
|
+
const mockColorStyle = {
|
|
16
|
+
type: 'color',
|
|
17
|
+
defaultValue: 'black',
|
|
18
|
+
getDefaultValue: () => 'black',
|
|
19
|
+
} as unknown as StyleProp<string>
|
|
20
|
+
|
|
21
|
+
const mockStylesMap = new Map([['color', mockColorStyle]])
|
|
22
|
+
createInstanceRecordType(mockStylesMap)
|
|
23
|
+
|
|
24
|
+
describe('shouldKeyBePreservedBetweenSessions', () => {
|
|
25
|
+
it('should preserve user preferences', () => {
|
|
26
|
+
const userPreferences = [
|
|
27
|
+
'isFocusMode',
|
|
28
|
+
'isDebugMode',
|
|
29
|
+
'isToolLocked',
|
|
30
|
+
'exportBackground',
|
|
31
|
+
'isGridMode',
|
|
32
|
+
'isReadonly',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
userPreferences.forEach((key) => {
|
|
36
|
+
expect(
|
|
37
|
+
shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
|
|
38
|
+
).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should not preserve temporary state', () => {
|
|
43
|
+
const temporaryState = [
|
|
44
|
+
'currentPageId',
|
|
45
|
+
'opacityForNextShape',
|
|
46
|
+
'stylesForNextShape',
|
|
47
|
+
'followingUserId',
|
|
48
|
+
'brush',
|
|
49
|
+
'cursor',
|
|
50
|
+
'scribbles',
|
|
51
|
+
'zoomBrush',
|
|
52
|
+
'chatMessage',
|
|
53
|
+
'isChatting',
|
|
54
|
+
'isPenMode',
|
|
55
|
+
'isHoveringCanvas',
|
|
56
|
+
'openMenus',
|
|
57
|
+
'isChangingStyle',
|
|
58
|
+
'duplicateProps',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
temporaryState.forEach((key) => {
|
|
62
|
+
expect(
|
|
63
|
+
shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
|
|
64
|
+
).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('pluckPreservingValues', () => {
|
|
70
|
+
it('should return null for null or undefined input', () => {
|
|
71
|
+
expect(pluckPreservingValues(null)).toBe(null)
|
|
72
|
+
expect(pluckPreservingValues(undefined)).toBe(null)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should filter properties according to preservation rules', () => {
|
|
76
|
+
const fullInstance: TLInstance = {
|
|
77
|
+
id: TLINSTANCE_ID,
|
|
78
|
+
typeName: 'instance',
|
|
79
|
+
currentPageId: 'page:page1' as any,
|
|
80
|
+
opacityForNextShape: 0.5,
|
|
81
|
+
stylesForNextShape: {},
|
|
82
|
+
followingUserId: null,
|
|
83
|
+
highlightedUserIds: [],
|
|
84
|
+
brush: null,
|
|
85
|
+
cursor: { type: 'default', rotation: 0 },
|
|
86
|
+
scribbles: [],
|
|
87
|
+
isFocusMode: true,
|
|
88
|
+
isDebugMode: false,
|
|
89
|
+
isToolLocked: true,
|
|
90
|
+
exportBackground: true,
|
|
91
|
+
screenBounds: { x: 0, y: 0, w: 1920, h: 1080 },
|
|
92
|
+
insets: [false, false, false, false],
|
|
93
|
+
zoomBrush: null,
|
|
94
|
+
chatMessage: '',
|
|
95
|
+
isChatting: false,
|
|
96
|
+
isPenMode: false,
|
|
97
|
+
isGridMode: true,
|
|
98
|
+
isFocused: true,
|
|
99
|
+
devicePixelRatio: 2,
|
|
100
|
+
isCoarsePointer: false,
|
|
101
|
+
isHoveringCanvas: null,
|
|
102
|
+
openMenus: [],
|
|
103
|
+
isChangingStyle: false,
|
|
104
|
+
isReadonly: false,
|
|
105
|
+
meta: {},
|
|
106
|
+
duplicateProps: null,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const preserved = pluckPreservingValues(fullInstance)
|
|
110
|
+
|
|
111
|
+
// Should preserve user preferences
|
|
112
|
+
expect(preserved?.isFocusMode).toBe(true)
|
|
113
|
+
expect(preserved?.isDebugMode).toBe(false)
|
|
114
|
+
expect(preserved?.isToolLocked).toBe(true)
|
|
115
|
+
expect(preserved?.exportBackground).toBe(true)
|
|
116
|
+
expect(preserved?.isGridMode).toBe(true)
|
|
117
|
+
|
|
118
|
+
// Should not preserve temporary state
|
|
119
|
+
expect(preserved?.currentPageId).toBeUndefined()
|
|
120
|
+
expect(preserved?.opacityForNextShape).toBeUndefined()
|
|
121
|
+
expect(preserved?.brush).toBeUndefined()
|
|
122
|
+
expect(preserved?.cursor).toBeUndefined()
|
|
123
|
+
expect(preserved?.chatMessage).toBeUndefined()
|
|
124
|
+
expect(preserved?.openMenus).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('instanceIdValidator', () => {
|
|
129
|
+
it('should validate correct instance IDs and reject invalid ones', () => {
|
|
130
|
+
expect(() => instanceIdValidator.validate('instance:instance')).not.toThrow()
|
|
131
|
+
expect(() => instanceIdValidator.validate('instance:test')).not.toThrow()
|
|
132
|
+
expect(() => instanceIdValidator.validate(TLINSTANCE_ID)).not.toThrow()
|
|
133
|
+
|
|
134
|
+
expect(() => instanceIdValidator.validate('invalid')).toThrow()
|
|
135
|
+
expect(() => instanceIdValidator.validate('page:instance')).toThrow()
|
|
136
|
+
expect(() => instanceIdValidator.validate('')).toThrow()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('createInstanceRecordType', () => {
|
|
141
|
+
it('should create a valid record type with correct configuration', () => {
|
|
142
|
+
const recordType = createInstanceRecordType(mockStylesMap)
|
|
143
|
+
expect(recordType.typeName).toBe('instance')
|
|
144
|
+
expect(recordType.scope).toBe('session')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('instanceMigrations', () => {
|
|
149
|
+
it('should have correct migration configuration', () => {
|
|
150
|
+
expect(instanceMigrations.sequenceId).toBe('com.tldraw.instance')
|
|
151
|
+
expect(Array.isArray(instanceMigrations.sequence)).toBe(true)
|
|
152
|
+
expect(instanceMigrations.sequence.length).toBe(25)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should migrate HoistOpacity correctly', () => {
|
|
156
|
+
const migration = instanceMigrations.sequence.find(
|
|
157
|
+
(m) => m.id === instanceVersions.HoistOpacity
|
|
158
|
+
)!
|
|
159
|
+
const oldRecord: any = {
|
|
160
|
+
id: TLINSTANCE_ID,
|
|
161
|
+
typeName: 'instance',
|
|
162
|
+
propsForNextShape: {
|
|
163
|
+
opacity: '0.5',
|
|
164
|
+
color: 'red',
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
const result = migration.up(oldRecord)
|
|
168
|
+
|
|
169
|
+
expect((result as any).opacityForNextShape).toBe(0.5)
|
|
170
|
+
expect((result as any).propsForNextShape.opacity).toBeUndefined()
|
|
171
|
+
expect((result as any).propsForNextShape.color).toBe('red')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should migrate RemoveDialog correctly', () => {
|
|
175
|
+
const migration = instanceMigrations.sequence.find(
|
|
176
|
+
(m) => m.id === instanceVersions.RemoveDialog
|
|
177
|
+
)!
|
|
178
|
+
const oldRecord: any = {
|
|
179
|
+
id: TLINSTANCE_ID,
|
|
180
|
+
typeName: 'instance',
|
|
181
|
+
dialog: 'some-dialog',
|
|
182
|
+
otherProp: 'keep-me',
|
|
183
|
+
}
|
|
184
|
+
const result = migration.up(oldRecord)
|
|
185
|
+
|
|
186
|
+
expect((result as any).dialog).toBeUndefined()
|
|
187
|
+
expect((result as any).otherProp).toBe('keep-me')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should have bidirectional migrations where applicable', () => {
|
|
191
|
+
const addInsetMigration = instanceMigrations.sequence.find(
|
|
192
|
+
(m) => m.id === instanceVersions.AddInset
|
|
193
|
+
)!
|
|
194
|
+
expect(addInsetMigration.down).toBeDefined()
|
|
195
|
+
|
|
196
|
+
const removeCameraMigration = instanceMigrations.sequence.find(
|
|
197
|
+
(m) => m.id === instanceVersions.RemoveCanMoveCamera
|
|
198
|
+
)!
|
|
199
|
+
expect(removeCameraMigration.down).toBeDefined()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -17,9 +17,25 @@ import { pageIdValidator, TLPageId } from './TLPage'
|
|
|
17
17
|
import { TLShapeId } from './TLShape'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* TLInstance
|
|
20
|
+
* State that is particular to a single browser tab. The TLInstance record stores
|
|
21
|
+
* all session-specific state including cursor position, selected tools, UI preferences,
|
|
22
|
+
* and temporary interaction state.
|
|
21
23
|
*
|
|
22
|
-
*
|
|
24
|
+
* Each browser tab has exactly one TLInstance record that persists for the duration
|
|
25
|
+
* of the session and tracks the user's current interaction state.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const instance: TLInstance = {
|
|
30
|
+
* id: 'instance:instance',
|
|
31
|
+
* typeName: 'instance',
|
|
32
|
+
* currentPageId: 'page:page1',
|
|
33
|
+
* cursor: { type: 'default', rotation: 0 },
|
|
34
|
+
* screenBounds: { x: 0, y: 0, w: 1920, h: 1080 },
|
|
35
|
+
* isFocusMode: false,
|
|
36
|
+
* isGridMode: true
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
23
39
|
*
|
|
24
40
|
* @public
|
|
25
41
|
*/
|
|
@@ -68,7 +84,14 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
|
|
68
84
|
} | null
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Configuration object defining which TLInstance properties should be preserved
|
|
89
|
+
* when loading snapshots across browser sessions. Properties marked as `true`
|
|
90
|
+
* represent user preferences that should persist, while `false` indicates
|
|
91
|
+
* temporary state that should reset.
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
72
95
|
export const shouldKeyBePreservedBetweenSessions = {
|
|
73
96
|
// This object defines keys that should be preserved across calls to loadSnapshot()
|
|
74
97
|
|
|
@@ -108,7 +131,15 @@ export const shouldKeyBePreservedBetweenSessions = {
|
|
|
108
131
|
duplicateProps: false, //
|
|
109
132
|
} as const satisfies { [K in keyof TLInstance]: boolean }
|
|
110
133
|
|
|
111
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Extracts only the properties from a TLInstance that should be preserved
|
|
136
|
+
* between browser sessions, filtering out temporary state.
|
|
137
|
+
*
|
|
138
|
+
* @param val - The TLInstance to filter, or null/undefined
|
|
139
|
+
* @returns A partial TLInstance containing only preservable properties, or null
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
112
143
|
export function pluckPreservingValues(val?: TLInstance | null): null | Partial<TLInstance> {
|
|
113
144
|
return val
|
|
114
145
|
? (filterEntries(val, (key) => {
|
|
@@ -117,12 +148,51 @@ export function pluckPreservingValues(val?: TLInstance | null): null | Partial<T
|
|
|
117
148
|
: null
|
|
118
149
|
}
|
|
119
150
|
|
|
120
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* A unique identifier for TLInstance records.
|
|
153
|
+
*
|
|
154
|
+
* TLInstance IDs are always the constant 'instance:instance' since there
|
|
155
|
+
* is exactly one instance record per browser tab.
|
|
156
|
+
*
|
|
157
|
+
* @public
|
|
158
|
+
*/
|
|
121
159
|
export type TLInstanceId = RecordId<TLInstance>
|
|
122
160
|
|
|
123
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Validator for TLInstanceId values. Ensures the ID follows the correct
|
|
163
|
+
* format for instance records.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* const isValid = instanceIdValidator.isValid('instance:instance') // true
|
|
168
|
+
* const isValid2 = instanceIdValidator.isValid('invalid') // false
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
124
173
|
export const instanceIdValidator = idValidator<TLInstanceId>('instance')
|
|
125
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Creates the record type definition for TLInstance records, including validation
|
|
177
|
+
* and default properties. The function takes a map of available style properties
|
|
178
|
+
* to configure validation for the stylesForNextShape field.
|
|
179
|
+
*
|
|
180
|
+
* @param stylesById - Map of style property IDs to their corresponding StyleProp definitions
|
|
181
|
+
* @returns A configured RecordType for TLInstance records
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* const stylesMap = new Map([['color', DefaultColorStyle]])
|
|
186
|
+
* const InstanceRecordType = createInstanceRecordType(stylesMap)
|
|
187
|
+
*
|
|
188
|
+
* const instance = InstanceRecordType.create({
|
|
189
|
+
* id: 'instance:instance',
|
|
190
|
+
* currentPageId: 'page:page1'
|
|
191
|
+
* })
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @public
|
|
195
|
+
*/
|
|
126
196
|
export function createInstanceRecordType(stylesById: Map<string, StyleProp<unknown>>) {
|
|
127
197
|
const stylesForNextShapeValidators = {} as Record<string, T.Validator<unknown>>
|
|
128
198
|
for (const [id, style] of stylesById) {
|
|
@@ -241,7 +311,15 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
|
|
241
311
|
)
|
|
242
312
|
}
|
|
243
313
|
|
|
244
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* Migration version identifiers for TLInstance records. Each version represents
|
|
316
|
+
* a schema change that requires data transformation when loading older documents.
|
|
317
|
+
*
|
|
318
|
+
* The versions track the evolution of the instance record structure over time,
|
|
319
|
+
* enabling backward and forward compatibility.
|
|
320
|
+
*
|
|
321
|
+
* @public
|
|
322
|
+
*/
|
|
245
323
|
export const instanceVersions = createMigrationIds('com.tldraw.instance', {
|
|
246
324
|
AddTransparentExportBgs: 1,
|
|
247
325
|
RemoveDialog: 2,
|
|
@@ -272,7 +350,22 @@ export const instanceVersions = createMigrationIds('com.tldraw.instance', {
|
|
|
272
350
|
|
|
273
351
|
// TODO: rewrite these to use mutation
|
|
274
352
|
|
|
275
|
-
/**
|
|
353
|
+
/**
|
|
354
|
+
* Migration sequence for TLInstance records. Defines how to transform instance
|
|
355
|
+
* records between different schema versions, ensuring data compatibility when
|
|
356
|
+
* loading documents created with different versions of tldraw.
|
|
357
|
+
*
|
|
358
|
+
* Each migration includes an 'up' function to migrate forward and optionally
|
|
359
|
+
* a 'down' function for reverse migration.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* // Migrations are applied automatically when loading documents
|
|
364
|
+
* const migratedInstance = instanceMigrations.migrate(oldInstance, targetVersion)
|
|
365
|
+
* ```
|
|
366
|
+
*
|
|
367
|
+
* @public
|
|
368
|
+
*/
|
|
276
369
|
export const instanceMigrations = createRecordMigrationSequence({
|
|
277
370
|
sequenceId: 'com.tldraw.instance',
|
|
278
371
|
recordType: 'instance',
|
|
@@ -524,5 +617,20 @@ export const instanceMigrations = createRecordMigrationSequence({
|
|
|
524
617
|
],
|
|
525
618
|
})
|
|
526
619
|
|
|
527
|
-
/**
|
|
620
|
+
/**
|
|
621
|
+
* The constant ID used for the singleton TLInstance record.
|
|
622
|
+
*
|
|
623
|
+
* Since each browser tab has exactly one instance, this constant ID
|
|
624
|
+
* is used universally across the application.
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* const instance = store.get(TLINSTANCE_ID)
|
|
629
|
+
* if (instance) {
|
|
630
|
+
* console.log('Current page:', instance.currentPageId)
|
|
631
|
+
* }
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
634
|
+
* @public
|
|
635
|
+
*/
|
|
528
636
|
export const TLINSTANCE_ID = 'instance:instance' as TLInstanceId
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isPageId,
|
|
4
|
+
pageIdValidator,
|
|
5
|
+
pageMigrations,
|
|
6
|
+
PageRecordType,
|
|
7
|
+
pageValidator,
|
|
8
|
+
pageVersions,
|
|
9
|
+
TLPageId,
|
|
10
|
+
} from './TLPage'
|
|
11
|
+
|
|
12
|
+
describe('pageIdValidator', () => {
|
|
13
|
+
it('should validate correct page IDs', () => {
|
|
14
|
+
expect(() => pageIdValidator.validate('page:main')).not.toThrow()
|
|
15
|
+
expect(() => pageIdValidator.validate('page:page1')).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should reject invalid page IDs', () => {
|
|
19
|
+
expect(() => pageIdValidator.validate('invalid')).toThrow()
|
|
20
|
+
expect(() => pageIdValidator.validate('shape:page1')).toThrow()
|
|
21
|
+
expect(() => pageIdValidator.validate('')).toThrow()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('pageValidator', () => {
|
|
26
|
+
it('should validate valid page records', () => {
|
|
27
|
+
const validPage = {
|
|
28
|
+
typeName: 'page',
|
|
29
|
+
id: 'page:test' as TLPageId,
|
|
30
|
+
name: 'Test Page',
|
|
31
|
+
index: 'a1' as any,
|
|
32
|
+
meta: {},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expect(() => pageValidator.validate(validPage)).not.toThrow()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should reject pages with invalid typeName', () => {
|
|
39
|
+
const invalidPage = {
|
|
40
|
+
typeName: 'not-page',
|
|
41
|
+
id: 'page:test' as TLPageId,
|
|
42
|
+
name: 'Test',
|
|
43
|
+
index: 'a1' as any,
|
|
44
|
+
meta: {},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
expect(() => pageValidator.validate(invalidPage)).toThrow()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should reject pages with missing required fields', () => {
|
|
51
|
+
const incompletePages = [
|
|
52
|
+
{
|
|
53
|
+
typeName: 'page',
|
|
54
|
+
id: 'page:test' as TLPageId,
|
|
55
|
+
// missing name, index, meta
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
typeName: 'page',
|
|
59
|
+
id: 'page:test' as TLPageId,
|
|
60
|
+
name: 'Test',
|
|
61
|
+
// missing index, meta
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
incompletePages.forEach((page) => {
|
|
66
|
+
expect(() => pageValidator.validate(page)).toThrow()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('pageMigrations', () => {
|
|
72
|
+
it('should apply AddMeta migration correctly', () => {
|
|
73
|
+
const addMetaMigration = pageMigrations.sequence.find((m) => m.id === pageVersions.AddMeta)!
|
|
74
|
+
|
|
75
|
+
const oldRecord: any = {
|
|
76
|
+
typeName: 'page',
|
|
77
|
+
id: 'page:test',
|
|
78
|
+
name: 'Test Page',
|
|
79
|
+
index: 'a1',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addMetaMigration.up(oldRecord)
|
|
83
|
+
expect(oldRecord.meta).toEqual({})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('PageRecordType', () => {
|
|
88
|
+
it('should create page records with defaults', () => {
|
|
89
|
+
const page = PageRecordType.create({
|
|
90
|
+
id: 'page:test' as TLPageId,
|
|
91
|
+
name: 'Test Page',
|
|
92
|
+
index: 'a1' as any,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(page.meta).toEqual({})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('isPageId', () => {
|
|
100
|
+
it('should return true for valid page IDs', () => {
|
|
101
|
+
expect(isPageId('page:main')).toBe(true)
|
|
102
|
+
expect(isPageId('page:page1')).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should return false for invalid page IDs', () => {
|
|
106
|
+
expect(isPageId('shape:main')).toBe(false)
|
|
107
|
+
expect(isPageId('invalid')).toBe(false)
|
|
108
|
+
expect(isPageId('')).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
})
|