@tldraw/tlschema 4.2.2 → 4.2.3

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.
Files changed (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +71 -242
  4. package/dist-cjs/index.js +1 -4
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +5 -1
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +1 -5
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/records/TLAsset.js.map +1 -1
  11. package/dist-cjs/records/TLBinding.js.map +2 -2
  12. package/dist-cjs/records/TLShape.js.map +2 -2
  13. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  14. package/dist-cjs/shapes/TLArrowShape.js +13 -26
  15. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  17. package/dist-cjs/shapes/TLDrawShape.js +4 -37
  18. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLEmbedShape.js +0 -17
  20. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLGeoShape.js +1 -12
  22. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLHighlightShape.js +2 -29
  24. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLNoteShape.js +1 -12
  26. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLTextShape.js +1 -12
  28. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  29. package/dist-cjs/store-migrations.js +15 -15
  30. package/dist-cjs/store-migrations.js.map +2 -2
  31. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  32. package/dist-esm/createTLSchema.mjs.map +2 -2
  33. package/dist-esm/index.d.mts +71 -242
  34. package/dist-esm/index.mjs +1 -5
  35. package/dist-esm/index.mjs.map +2 -2
  36. package/dist-esm/misc/TLOpacity.mjs +5 -1
  37. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  38. package/dist-esm/misc/TLRichText.mjs +1 -5
  39. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  40. package/dist-esm/records/TLAsset.mjs.map +1 -1
  41. package/dist-esm/records/TLBinding.mjs.map +2 -2
  42. package/dist-esm/records/TLShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLArrowShape.mjs +13 -26
  44. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  46. package/dist-esm/shapes/TLDrawShape.mjs +4 -37
  47. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  48. package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
  49. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLGeoShape.mjs +1 -12
  51. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
  53. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLNoteShape.mjs +1 -12
  55. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLTextShape.mjs +1 -12
  57. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  58. package/dist-esm/store-migrations.mjs +15 -15
  59. package/dist-esm/store-migrations.mjs.map +2 -2
  60. package/package.json +8 -8
  61. package/src/__tests__/migrationTestUtils.ts +3 -9
  62. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  63. package/src/assets/TLImageAsset.test.ts +213 -0
  64. package/src/assets/TLVideoAsset.test.ts +105 -0
  65. package/src/bindings/TLArrowBinding.test.ts +55 -0
  66. package/src/bindings/TLBaseBinding.ts +14 -25
  67. package/src/createTLSchema.ts +2 -8
  68. package/src/index.ts +0 -9
  69. package/src/migrations.test.ts +1 -149
  70. package/src/misc/TLOpacity.ts +5 -1
  71. package/src/misc/TLRichText.ts +1 -6
  72. package/src/misc/id-validator.test.ts +50 -0
  73. package/src/records/TLAsset.test.ts +234 -0
  74. package/src/records/TLAsset.ts +2 -2
  75. package/src/records/TLBinding.test.ts +22 -0
  76. package/src/records/TLBinding.ts +23 -65
  77. package/src/records/TLCamera.test.ts +19 -0
  78. package/src/records/TLDocument.test.ts +35 -0
  79. package/src/records/TLInstance.test.ts +201 -0
  80. package/src/records/TLPage.test.ts +110 -0
  81. package/src/records/TLPageState.test.ts +228 -0
  82. package/src/records/TLPointer.test.ts +63 -0
  83. package/src/records/TLPresence.test.ts +190 -0
  84. package/src/records/TLRecord.test.ts +70 -0
  85. package/src/records/TLShape.test.ts +232 -0
  86. package/src/records/TLShape.ts +5 -100
  87. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  88. package/src/shapes/ShapeWithCrop.ts +2 -2
  89. package/src/shapes/TLArrowShape.test.ts +505 -0
  90. package/src/shapes/TLArrowShape.ts +14 -28
  91. package/src/shapes/TLBaseShape.test.ts +142 -0
  92. package/src/shapes/TLBaseShape.ts +10 -34
  93. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  94. package/src/shapes/TLDrawShape.test.ts +177 -0
  95. package/src/shapes/TLDrawShape.ts +12 -59
  96. package/src/shapes/TLEmbedShape.test.ts +286 -0
  97. package/src/shapes/TLEmbedShape.ts +0 -17
  98. package/src/shapes/TLFrameShape.test.ts +71 -0
  99. package/src/shapes/TLGeoShape.test.ts +247 -0
  100. package/src/shapes/TLGeoShape.ts +1 -14
  101. package/src/shapes/TLGroupShape.test.ts +59 -0
  102. package/src/shapes/TLHighlightShape.test.ts +325 -0
  103. package/src/shapes/TLHighlightShape.ts +0 -37
  104. package/src/shapes/TLImageShape.test.ts +534 -0
  105. package/src/shapes/TLLineShape.test.ts +269 -0
  106. package/src/shapes/TLNoteShape.test.ts +1568 -0
  107. package/src/shapes/TLNoteShape.ts +1 -15
  108. package/src/shapes/TLTextShape.test.ts +407 -0
  109. package/src/shapes/TLTextShape.ts +2 -16
  110. package/src/shapes/TLVideoShape.test.ts +112 -0
  111. package/src/store-migrations.ts +16 -17
  112. package/src/styles/TLColorStyle.test.ts +439 -0
  113. package/dist-cjs/misc/b64Vecs.js +0 -224
  114. package/dist-cjs/misc/b64Vecs.js.map +0 -7
  115. package/dist-esm/misc/b64Vecs.mjs +0 -204
  116. package/dist-esm/misc/b64Vecs.mjs.map +0 -7
  117. package/src/misc/b64Vecs.ts +0 -308
@@ -0,0 +1,505 @@
1
+ import { describe, expect, it, test } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { arrowShapeMigrations, arrowShapeProps, arrowShapeVersions } from './TLArrowShape'
4
+
5
+ describe('TLArrowShape', () => {
6
+ describe('arrowShapeMigrations - AddLabelColor migration', () => {
7
+ const { up, down } = getTestMigration(arrowShapeVersions.AddLabelColor)
8
+
9
+ describe('AddLabelColor up migration', () => {
10
+ it('should add labelColor property with default value "black"', () => {
11
+ const oldRecord = {
12
+ id: 'shape:arrow1',
13
+ typeName: 'shape',
14
+ type: 'arrow',
15
+ x: 100,
16
+ y: 200,
17
+ rotation: 0,
18
+ index: 'a1',
19
+ parentId: 'page:main',
20
+ isLocked: false,
21
+ opacity: 1,
22
+ props: {
23
+ color: 'blue',
24
+ fill: 'none',
25
+ dash: 'solid',
26
+ size: 'm',
27
+ arrowheadStart: 'none',
28
+ arrowheadEnd: 'arrow',
29
+ font: 'draw',
30
+ start: { x: 0, y: 0 },
31
+ end: { x: 100, y: 100 },
32
+ },
33
+ meta: {},
34
+ }
35
+
36
+ const result = up(oldRecord)
37
+ expect(result.props.labelColor).toBe('black')
38
+ expect(result.props.color).toBe('blue') // Preserve other props
39
+ })
40
+
41
+ it('should preserve all existing properties during migration', () => {
42
+ const oldRecord = {
43
+ id: 'shape:arrow2',
44
+ typeName: 'shape',
45
+ type: 'arrow',
46
+ x: 50,
47
+ y: 75,
48
+ rotation: 0.5,
49
+ index: 'b1',
50
+ parentId: 'page:test',
51
+ isLocked: true,
52
+ opacity: 0.8,
53
+ props: {
54
+ color: 'red',
55
+ fill: 'solid',
56
+ dash: 'dashed',
57
+ size: 'l',
58
+ arrowheadStart: 'triangle',
59
+ arrowheadEnd: 'diamond',
60
+ font: 'sans',
61
+ start: { x: 25, y: 50 },
62
+ end: { x: 200, y: 150 },
63
+ },
64
+ meta: { custom: 'data' },
65
+ }
66
+
67
+ const result = up(oldRecord)
68
+ expect(result.props.labelColor).toBe('black')
69
+ expect(result.props.color).toBe('red')
70
+ expect(result.props.fill).toBe('solid')
71
+ expect(result.props.dash).toBe('dashed')
72
+ expect(result.props.size).toBe('l')
73
+ expect(result.props.arrowheadStart).toBe('triangle')
74
+ expect(result.props.arrowheadEnd).toBe('diamond')
75
+ expect(result.props.font).toBe('sans')
76
+ expect(result.props.start).toEqual({ x: 25, y: 50 })
77
+ expect(result.props.end).toEqual({ x: 200, y: 150 })
78
+ expect(result.meta).toEqual({ custom: 'data' })
79
+ })
80
+
81
+ test('should not modify labelColor if it already exists', () => {
82
+ const recordWithLabelColor = {
83
+ id: 'shape:arrow3',
84
+ typeName: 'shape',
85
+ type: 'arrow',
86
+ props: {
87
+ labelColor: 'red', // Already has labelColor
88
+ color: 'blue',
89
+ },
90
+ }
91
+
92
+ const result = up(recordWithLabelColor)
93
+ expect(result.props.labelColor).toBe('black') // Migration sets default regardless
94
+ })
95
+ })
96
+
97
+ describe('AddLabelColor down migration', () => {
98
+ it('should be retired (no down migration)', () => {
99
+ // Based on the source code, the down migration is 'retired'
100
+ // The getTestMigration utility should throw when trying to access down migration
101
+ expect(() => {
102
+ // This should throw since the migration is retired
103
+ down({})
104
+ }).toThrow('Migration com.tldraw.shape.arrow/1 does not have a down function')
105
+ })
106
+ })
107
+ })
108
+
109
+ describe('arrowShapeMigrations - AddIsPrecise migration', () => {
110
+ const { up, down } = getTestMigration(arrowShapeVersions.AddIsPrecise)
111
+
112
+ describe('AddIsPrecise up migration', () => {
113
+ it('should add isPrecise property to binding start and end', () => {
114
+ const oldRecord = {
115
+ id: 'shape:arrow1',
116
+ props: {
117
+ start: {
118
+ type: 'binding',
119
+ boundShapeId: 'shape:rect1',
120
+ normalizedAnchor: { x: 0.5, y: 0.5 },
121
+ },
122
+ end: {
123
+ type: 'binding',
124
+ boundShapeId: 'shape:rect2',
125
+ normalizedAnchor: { x: 0.5, y: 0.5 },
126
+ },
127
+ },
128
+ }
129
+
130
+ const result = up(oldRecord)
131
+ expect(result.props.start.isPrecise).toBe(false) // 0.5, 0.5 is not precise
132
+ expect(result.props.end.isPrecise).toBe(false)
133
+ })
134
+
135
+ it('should set isPrecise to true for non-center anchors', () => {
136
+ const oldRecord = {
137
+ id: 'shape:arrow1',
138
+ props: {
139
+ start: {
140
+ type: 'binding',
141
+ boundShapeId: 'shape:rect1',
142
+ normalizedAnchor: { x: 0.25, y: 0.75 }, // Not center
143
+ },
144
+ end: {
145
+ type: 'binding',
146
+ boundShapeId: 'shape:rect2',
147
+ normalizedAnchor: { x: 1, y: 0 }, // Not center
148
+ },
149
+ },
150
+ }
151
+
152
+ const result = up(oldRecord)
153
+ expect(result.props.start.isPrecise).toBe(true)
154
+ expect(result.props.end.isPrecise).toBe(true)
155
+ })
156
+
157
+ it('should not modify non-binding terminals', () => {
158
+ const oldRecord = {
159
+ id: 'shape:arrow1',
160
+ props: {
161
+ start: { type: 'point', x: 0, y: 0 },
162
+ end: { type: 'point', x: 100, y: 100 },
163
+ },
164
+ }
165
+
166
+ const result = up(oldRecord)
167
+ expect(result.props.start.isPrecise).toBeUndefined()
168
+ expect(result.props.end.isPrecise).toBeUndefined()
169
+ })
170
+
171
+ it('should handle mixed binding and point terminals', () => {
172
+ const oldRecord = {
173
+ id: 'shape:arrow1',
174
+ props: {
175
+ start: {
176
+ type: 'binding',
177
+ boundShapeId: 'shape:rect1',
178
+ normalizedAnchor: { x: 0, y: 0 }, // Precise
179
+ },
180
+ end: { type: 'point', x: 100, y: 100 },
181
+ },
182
+ }
183
+
184
+ const result = up(oldRecord)
185
+ expect(result.props.start.isPrecise).toBe(true)
186
+ expect(result.props.end.isPrecise).toBeUndefined()
187
+ })
188
+ })
189
+
190
+ describe('AddIsPrecise down migration', () => {
191
+ it('should remove isPrecise property and adjust normalizedAnchor if not precise', () => {
192
+ const newRecord = {
193
+ id: 'shape:arrow1',
194
+ props: {
195
+ start: {
196
+ type: 'binding',
197
+ boundShapeId: 'shape:rect1',
198
+ normalizedAnchor: { x: 0.25, y: 0.75 },
199
+ isPrecise: false,
200
+ },
201
+ end: {
202
+ type: 'binding',
203
+ boundShapeId: 'shape:rect2',
204
+ normalizedAnchor: { x: 0.1, y: 0.9 },
205
+ isPrecise: true,
206
+ },
207
+ },
208
+ }
209
+
210
+ const result = down(newRecord)
211
+ expect(result.props.start.isPrecise).toBeUndefined()
212
+ expect(result.props.start.normalizedAnchor).toEqual({ x: 0.5, y: 0.5 }) // Reset to center
213
+ expect(result.props.end.isPrecise).toBeUndefined()
214
+ expect(result.props.end.normalizedAnchor).toEqual({ x: 0.1, y: 0.9 }) // Keep precise anchor
215
+ })
216
+
217
+ it('should not modify non-binding terminals', () => {
218
+ const newRecord = {
219
+ id: 'shape:arrow1',
220
+ props: {
221
+ start: { type: 'point', x: 0, y: 0 },
222
+ end: { type: 'point', x: 100, y: 100 },
223
+ },
224
+ }
225
+
226
+ const result = down(newRecord)
227
+ expect(result.props.start).toEqual({ type: 'point', x: 0, y: 0 })
228
+ expect(result.props.end).toEqual({ type: 'point', x: 100, y: 100 })
229
+ })
230
+ })
231
+ })
232
+
233
+ describe('arrowShapeMigrations - AddLabelPosition migration', () => {
234
+ const { up, down } = getTestMigration(arrowShapeVersions.AddLabelPosition)
235
+
236
+ describe('AddLabelPosition up migration', () => {
237
+ it('should add labelPosition property with default value 0.5', () => {
238
+ const oldRecord = {
239
+ id: 'shape:arrow1',
240
+ props: {
241
+ color: 'blue',
242
+ start: { x: 0, y: 0 },
243
+ end: { x: 100, y: 100 },
244
+ },
245
+ }
246
+
247
+ const result = up(oldRecord)
248
+ expect(result.props.labelPosition).toBe(0.5)
249
+ })
250
+
251
+ it('should preserve existing properties during migration', () => {
252
+ const oldRecord = {
253
+ id: 'shape:arrow1',
254
+ props: {
255
+ color: 'red',
256
+ fill: 'solid',
257
+ start: { x: 25, y: 50 },
258
+ end: { x: 200, y: 150 },
259
+ bend: 0.3,
260
+ },
261
+ }
262
+
263
+ const result = up(oldRecord)
264
+ expect(result.props.labelPosition).toBe(0.5)
265
+ expect(result.props.color).toBe('red')
266
+ expect(result.props.fill).toBe('solid')
267
+ expect(result.props.start).toEqual({ x: 25, y: 50 })
268
+ expect(result.props.end).toEqual({ x: 200, y: 150 })
269
+ expect(result.props.bend).toBe(0.3)
270
+ })
271
+ })
272
+
273
+ describe('AddLabelPosition down migration', () => {
274
+ it('should remove labelPosition property', () => {
275
+ const newRecord = {
276
+ id: 'shape:arrow1',
277
+ props: {
278
+ color: 'blue',
279
+ start: { x: 0, y: 0 },
280
+ end: { x: 100, y: 100 },
281
+ labelPosition: 0.7,
282
+ },
283
+ }
284
+
285
+ const result = down(newRecord)
286
+ expect(result.props.labelPosition).toBeUndefined()
287
+ expect(result.props.color).toBe('blue') // Preserve other props
288
+ })
289
+ })
290
+ })
291
+
292
+ describe('arrowShapeMigrations - ExtractBindings migration', () => {
293
+ const migration = arrowShapeMigrations.sequence.find(
294
+ (m) => 'id' in m && m.id === arrowShapeVersions.ExtractBindings
295
+ )
296
+
297
+ it('should be a store-scope migration', () => {
298
+ expect(migration).toBeDefined()
299
+ if (migration && 'scope' in migration) {
300
+ expect(migration.scope).toBe('store')
301
+ }
302
+ })
303
+
304
+ it('should have up function for extracting bindings', () => {
305
+ if (migration && 'up' in migration) {
306
+ expect(migration.up).toBeDefined()
307
+ expect(typeof migration.up).toBe('function')
308
+ }
309
+ })
310
+
311
+ // Note: This migration is complex and modifies the entire store
312
+ // Testing the full migration would require setting up a mock store
313
+ // The migration extracts binding information from arrow terminals
314
+ // and creates separate binding records
315
+ })
316
+
317
+ describe('arrowShapeMigrations - AddScale migration', () => {
318
+ const { up, down } = getTestMigration(arrowShapeVersions.AddScale)
319
+
320
+ describe('AddScale up migration', () => {
321
+ it('should add scale property with default value 1', () => {
322
+ const oldRecord = {
323
+ id: 'shape:arrow1',
324
+ props: {
325
+ color: 'blue',
326
+ start: { x: 0, y: 0 },
327
+ end: { x: 100, y: 100 },
328
+ },
329
+ }
330
+
331
+ const result = up(oldRecord)
332
+ expect(result.props.scale).toBe(1)
333
+ })
334
+
335
+ it('should preserve existing properties during migration', () => {
336
+ const oldRecord = {
337
+ id: 'shape:arrow1',
338
+ props: {
339
+ color: 'red',
340
+ labelPosition: 0.3,
341
+ start: { x: 10, y: 20 },
342
+ end: { x: 200, y: 150 },
343
+ },
344
+ }
345
+
346
+ const result = up(oldRecord)
347
+ expect(result.props.scale).toBe(1)
348
+ expect(result.props.color).toBe('red')
349
+ expect(result.props.labelPosition).toBe(0.3)
350
+ })
351
+ })
352
+
353
+ describe('AddScale down migration', () => {
354
+ it('should remove scale property', () => {
355
+ const newRecord = {
356
+ id: 'shape:arrow1',
357
+ props: {
358
+ color: 'blue',
359
+ start: { x: 0, y: 0 },
360
+ end: { x: 100, y: 100 },
361
+ scale: 1.5,
362
+ },
363
+ }
364
+
365
+ const result = down(newRecord)
366
+ expect(result.props.scale).toBeUndefined()
367
+ expect(result.props.color).toBe('blue') // Preserve other props
368
+ })
369
+ })
370
+ })
371
+
372
+ describe('arrowShapeMigrations - AddElbow migration', () => {
373
+ const { up, down } = getTestMigration(arrowShapeVersions.AddElbow)
374
+
375
+ describe('AddElbow up migration', () => {
376
+ it('should add kind and elbowMidPoint properties with default values', () => {
377
+ const oldRecord = {
378
+ id: 'shape:arrow1',
379
+ props: {
380
+ color: 'blue',
381
+ start: { x: 0, y: 0 },
382
+ end: { x: 100, y: 100 },
383
+ },
384
+ }
385
+
386
+ const result = up(oldRecord)
387
+ expect(result.props.kind).toBe('arc')
388
+ expect(result.props.elbowMidPoint).toBe(0.5)
389
+ })
390
+
391
+ it('should preserve existing properties during migration', () => {
392
+ const oldRecord = {
393
+ id: 'shape:arrow1',
394
+ props: {
395
+ color: 'red',
396
+ scale: 1.2,
397
+ labelPosition: 0.7,
398
+ start: { x: 25, y: 50 },
399
+ end: { x: 200, y: 150 },
400
+ },
401
+ }
402
+
403
+ const result = up(oldRecord)
404
+ expect(result.props.kind).toBe('arc')
405
+ expect(result.props.elbowMidPoint).toBe(0.5)
406
+ expect(result.props.color).toBe('red')
407
+ expect(result.props.scale).toBe(1.2)
408
+ expect(result.props.labelPosition).toBe(0.7)
409
+ })
410
+ })
411
+
412
+ describe('AddElbow down migration', () => {
413
+ it('should remove kind and elbowMidPoint properties', () => {
414
+ const newRecord = {
415
+ id: 'shape:arrow1',
416
+ props: {
417
+ kind: 'elbow',
418
+ elbowMidPoint: 0.3,
419
+ color: 'blue',
420
+ start: { x: 0, y: 0 },
421
+ end: { x: 100, y: 100 },
422
+ },
423
+ }
424
+
425
+ const result = down(newRecord)
426
+ expect(result.props.kind).toBeUndefined()
427
+ expect(result.props.elbowMidPoint).toBeUndefined()
428
+ expect(result.props.color).toBe('blue') // Preserve other props
429
+ })
430
+ })
431
+ })
432
+
433
+ describe('arrowShapeMigrations - AddRichText migration', () => {
434
+ const { up } = getTestMigration(arrowShapeVersions.AddRichText)
435
+
436
+ describe('AddRichText up migration', () => {
437
+ it('should convert text property to richText', () => {
438
+ const oldRecord = {
439
+ id: 'shape:arrow1',
440
+ props: {
441
+ text: 'Simple text label',
442
+ color: 'blue',
443
+ start: { x: 0, y: 0 },
444
+ end: { x: 100, y: 100 },
445
+ },
446
+ }
447
+
448
+ const result = up(oldRecord)
449
+ expect(result.props.richText).toBeDefined()
450
+ expect(result.props.text).toBeUndefined()
451
+ })
452
+
453
+ it('should handle empty text', () => {
454
+ const oldRecord = {
455
+ id: 'shape:arrow1',
456
+ props: {
457
+ text: '',
458
+ color: 'red',
459
+ start: { x: 10, y: 20 },
460
+ end: { x: 200, y: 150 },
461
+ },
462
+ }
463
+
464
+ const result = up(oldRecord)
465
+ expect(result.props.richText).toBeDefined()
466
+ expect(result.props.text).toBeUndefined()
467
+ })
468
+
469
+ it('should preserve other properties during migration', () => {
470
+ const oldRecord = {
471
+ id: 'shape:arrow1',
472
+ props: {
473
+ text: 'Label text',
474
+ kind: 'elbow',
475
+ elbowMidPoint: 0.3,
476
+ color: 'green',
477
+ scale: 1.5,
478
+ },
479
+ }
480
+
481
+ const result = up(oldRecord)
482
+ expect(result.props.richText).toBeDefined()
483
+ expect(result.props.text).toBeUndefined()
484
+ expect(result.props.kind).toBe('elbow')
485
+ expect(result.props.elbowMidPoint).toBe(0.3)
486
+ expect(result.props.color).toBe('green')
487
+ expect(result.props.scale).toBe(1.5)
488
+ })
489
+ })
490
+
491
+ // Note: The down migration is explicitly not defined (forced client update)
492
+ // so we don't test it
493
+ })
494
+
495
+ describe('edge cases and error handling', () => {
496
+ it('should handle zero scale validation correctly', () => {
497
+ // Zero should be invalid for scale (nonZeroNumber)
498
+ expect(() => arrowShapeProps.scale.validate(0)).toThrow()
499
+
500
+ // Very small positive numbers should be valid, but negative numbers should be invalid
501
+ expect(() => arrowShapeProps.scale.validate(0.0001)).not.toThrow()
502
+ expect(() => arrowShapeProps.scale.validate(-0.0001)).toThrow()
503
+ })
504
+ })
505
+ })
@@ -1,10 +1,9 @@
1
1
  import { createMigrationSequence } from '@tldraw/store'
2
- import { structuredClone } from '@tldraw/utils'
3
2
  import { T } from '@tldraw/validate'
4
3
  import { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'
5
4
  import { VecModel, vecModelValidator } from '../misc/geometry-types'
6
5
  import { createBindingId } from '../records/TLBinding'
7
- import { TLShape, TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
6
+ import { TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
8
7
  import { RecordProps, TLPropsMigration, createPropsMigration } from '../recordsWithProps'
9
8
  import { StyleProp } from '../styles/StyleProp'
10
9
  import {
@@ -277,7 +276,6 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
277
276
  AddScale: 5,
278
277
  AddElbow: 6,
279
278
  AddRichText: 7,
280
- AddRichTextAttrs: 8,
281
279
  })
282
280
 
283
281
  function propsMigration(migration: TLPropsMigration) {
@@ -343,8 +341,8 @@ export const arrowShapeMigrations = createMigrationSequence({
343
341
 
344
342
  {
345
343
  id: arrowShapeVersions.ExtractBindings,
346
- scope: 'storage',
347
- up: (storage) => {
344
+ scope: 'store',
345
+ up: (oldStore) => {
348
346
  type OldArrowTerminal =
349
347
  | {
350
348
  type: 'point'
@@ -363,10 +361,11 @@ export const arrowShapeMigrations = createMigrationSequence({
363
361
 
364
362
  type OldArrow = TLBaseShape<'arrow', { start: OldArrowTerminal; end: OldArrowTerminal }>
365
363
 
366
- for (const record of storage.values()) {
367
- if (record.typeName !== 'shape' || (record as TLShape).type !== 'arrow') continue
368
- const arrow = record as OldArrow
369
- const newArrow = structuredClone(arrow)
364
+ const arrows = Object.values(oldStore).filter(
365
+ (r: any): r is OldArrow => r.typeName === 'shape' && r.type === 'arrow'
366
+ )
367
+
368
+ for (const arrow of arrows) {
370
369
  const { start, end } = arrow.props
371
370
  if (start.type === 'binding') {
372
371
  const id = createBindingId()
@@ -385,10 +384,10 @@ export const arrowShapeMigrations = createMigrationSequence({
385
384
  },
386
385
  }
387
386
 
388
- storage.set(id, binding as any)
389
- newArrow.props.start = { x: 0, y: 0 }
387
+ oldStore[id] = binding
388
+ arrow.props.start = { x: 0, y: 0 }
390
389
  } else {
391
- delete newArrow.props.start.type
390
+ delete arrow.props.start.type
392
391
  }
393
392
  if (end.type === 'binding') {
394
393
  const id = createBindingId()
@@ -407,12 +406,11 @@ export const arrowShapeMigrations = createMigrationSequence({
407
406
  },
408
407
  }
409
408
 
410
- storage.set(id, binding as any)
411
- newArrow.props.end = { x: 0, y: 0 }
409
+ oldStore[id] = binding
410
+ arrow.props.end = { x: 0, y: 0 }
412
411
  } else {
413
- delete newArrow.props.end.type
412
+ delete arrow.props.end.type
414
413
  }
415
- storage.set(arrow.id, newArrow)
416
414
  }
417
415
  },
418
416
  },
@@ -447,17 +445,5 @@ export const arrowShapeMigrations = createMigrationSequence({
447
445
  // delete props.richText
448
446
  // },
449
447
  }),
450
- propsMigration({
451
- id: arrowShapeVersions.AddRichTextAttrs,
452
- up: (_props) => {
453
- // noop - attrs is optional so old records are valid
454
- },
455
- down: (props) => {
456
- // Remove attrs from richText when migrating down
457
- if (props.richText && 'attrs' in props.richText) {
458
- delete props.richText.attrs
459
- }
460
- },
461
- }),
462
448
  ],
463
449
  })