@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.
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +71 -242
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js +5 -1
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js +1 -5
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +1 -1
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js +13 -26
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js +4 -37
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +0 -17
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +1 -12
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js +2 -29
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js +1 -12
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js +1 -12
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/store-migrations.js +15 -15
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +71 -242
- package/dist-esm/index.mjs +1 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs +5 -1
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs +1 -5
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +1 -1
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs +13 -26
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs +4 -37
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +1 -12
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs +1 -12
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs +1 -12
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs +15 -15
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/package.json +8 -8
- package/src/__tests__/migrationTestUtils.ts +3 -9
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLBaseBinding.ts +14 -25
- package/src/createTLSchema.ts +2 -8
- package/src/index.ts +0 -9
- package/src/migrations.test.ts +1 -149
- package/src/misc/TLOpacity.ts +5 -1
- package/src/misc/TLRichText.ts +1 -6
- package/src/misc/id-validator.test.ts +50 -0
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +2 -2
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +23 -65
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +5 -100
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +2 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +14 -28
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +10 -34
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +12 -59
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +0 -17
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +1 -14
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +0 -37
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +1 -15
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +2 -16
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/store-migrations.ts +16 -17
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/dist-cjs/misc/b64Vecs.js +0 -224
- package/dist-cjs/misc/b64Vecs.js.map +0 -7
- package/dist-esm/misc/b64Vecs.mjs +0 -204
- package/dist-esm/misc/b64Vecs.mjs.map +0 -7
- package/src/misc/b64Vecs.ts +0 -308
|
@@ -142,7 +142,6 @@ const Versions = createShapePropsMigrationIds('note', {
|
|
|
142
142
|
AddScale: 7,
|
|
143
143
|
AddLabelColor: 8,
|
|
144
144
|
AddRichText: 9,
|
|
145
|
-
AddRichTextAttrs: 10,
|
|
146
145
|
})
|
|
147
146
|
|
|
148
147
|
/**
|
|
@@ -157,8 +156,7 @@ export { Versions as noteShapeVersions }
|
|
|
157
156
|
* Migration sequence for note shapes. Handles schema evolution over time by defining
|
|
158
157
|
* how to upgrade and downgrade note shape data between different versions. Includes
|
|
159
158
|
* migrations for URL properties, text alignment changes, vertical alignment addition,
|
|
160
|
-
* font size adjustments, scaling support, label color, the transition from plain text to rich text
|
|
161
|
-
* and support for attrs property on richText.
|
|
159
|
+
* font size adjustments, scaling support, label color, and the transition from plain text to rich text.
|
|
162
160
|
*
|
|
163
161
|
* @public
|
|
164
162
|
*/
|
|
@@ -253,17 +251,5 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
|
|
|
253
251
|
// delete props.richText
|
|
254
252
|
// },
|
|
255
253
|
},
|
|
256
|
-
{
|
|
257
|
-
id: Versions.AddRichTextAttrs,
|
|
258
|
-
up: (_props) => {
|
|
259
|
-
// noop - attrs is optional so old records are valid
|
|
260
|
-
},
|
|
261
|
-
down: (props) => {
|
|
262
|
-
// Remove attrs from richText when migrating down
|
|
263
|
-
if (props.richText && 'attrs' in props.richText) {
|
|
264
|
-
delete props.richText.attrs
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
254
|
],
|
|
269
255
|
})
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { T } from '@tldraw/validate'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
4
|
+
import { TLRichText, toRichText } from '../misc/TLRichText'
|
|
5
|
+
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
|
6
|
+
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
|
7
|
+
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
|
8
|
+
import { DefaultTextAlignStyle } from '../styles/TLTextAlignStyle'
|
|
9
|
+
import { textShapeMigrations, textShapeProps, textShapeVersions } from './TLTextShape'
|
|
10
|
+
|
|
11
|
+
describe('TLTextShape', () => {
|
|
12
|
+
describe('textShapeProps validation schema', () => {
|
|
13
|
+
it('should validate using comprehensive object validator', () => {
|
|
14
|
+
const fullValidator = T.object(textShapeProps)
|
|
15
|
+
|
|
16
|
+
const validPropsObject = {
|
|
17
|
+
color: 'red' as const,
|
|
18
|
+
size: 's' as const,
|
|
19
|
+
font: 'mono' as const,
|
|
20
|
+
textAlign: 'end' as const,
|
|
21
|
+
w: 250,
|
|
22
|
+
richText: toRichText('Complete validation test') as TLRichText,
|
|
23
|
+
scale: 0.8,
|
|
24
|
+
autoSize: true,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expect(() => fullValidator.validate(validPropsObject)).not.toThrow()
|
|
28
|
+
const result = fullValidator.validate(validPropsObject)
|
|
29
|
+
expect(result).toEqual(validPropsObject)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should validate width as nonZeroNumber', () => {
|
|
33
|
+
// Valid non-zero positive numbers
|
|
34
|
+
const validWidths = [0.1, 1, 50, 100, 1000, 0.001]
|
|
35
|
+
|
|
36
|
+
validWidths.forEach((w) => {
|
|
37
|
+
expect(() => textShapeProps.w.validate(w)).not.toThrow()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Invalid widths (zero, negative numbers, and non-numbers)
|
|
41
|
+
const invalidWidths = [0, -1, -0.1, 'not-number', null, undefined, {}, [], true, false]
|
|
42
|
+
|
|
43
|
+
invalidWidths.forEach((w) => {
|
|
44
|
+
expect(() => textShapeProps.w.validate(w)).toThrow()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should validate scale as nonZeroNumber', () => {
|
|
49
|
+
// Valid non-zero positive numbers
|
|
50
|
+
const validScales = [0.1, 0.5, 1, 1.5, 2, 10]
|
|
51
|
+
|
|
52
|
+
validScales.forEach((scale) => {
|
|
53
|
+
expect(() => textShapeProps.scale.validate(scale)).not.toThrow()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Invalid scales (zero, negative numbers, and non-numbers)
|
|
57
|
+
const invalidScales = [0, -0.5, -1, -2, 'not-number', null, undefined, {}, [], true, false]
|
|
58
|
+
|
|
59
|
+
invalidScales.forEach((scale) => {
|
|
60
|
+
expect(() => textShapeProps.scale.validate(scale)).toThrow()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should use correct default style validators', () => {
|
|
65
|
+
// Verify that the props schema uses the expected style validators
|
|
66
|
+
expect(textShapeProps.color).toBe(DefaultColorStyle)
|
|
67
|
+
expect(textShapeProps.size).toBe(DefaultSizeStyle)
|
|
68
|
+
expect(textShapeProps.font).toBe(DefaultFontStyle)
|
|
69
|
+
expect(textShapeProps.textAlign).toBe(DefaultTextAlignStyle)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should use correct primitive validators', () => {
|
|
73
|
+
// Check that non-style properties use correct T validators
|
|
74
|
+
expect(textShapeProps.w).toBe(T.nonZeroNumber)
|
|
75
|
+
expect(textShapeProps.scale).toBe(T.nonZeroNumber)
|
|
76
|
+
expect(textShapeProps.autoSize).toBe(T.boolean)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('textShapeVersions', () => {
|
|
81
|
+
it('should have all expected migration versions', () => {
|
|
82
|
+
const expectedVersions: Array<keyof typeof textShapeVersions> = [
|
|
83
|
+
'RemoveJustify',
|
|
84
|
+
'AddTextAlign',
|
|
85
|
+
'AddRichText',
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
expectedVersions.forEach((version) => {
|
|
89
|
+
expect(textShapeVersions[version]).toBeDefined()
|
|
90
|
+
expect(typeof textShapeVersions[version]).toBe('string')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('textShapeMigrations', () => {
|
|
96
|
+
it('should have migrations for all version IDs', () => {
|
|
97
|
+
const migrationIds = textShapeMigrations.sequence
|
|
98
|
+
.filter((migration) => 'id' in migration)
|
|
99
|
+
.map((migration) => ('id' in migration ? migration.id : null))
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
|
|
102
|
+
const versionIds = Object.values(textShapeVersions)
|
|
103
|
+
|
|
104
|
+
versionIds.forEach((versionId) => {
|
|
105
|
+
expect(migrationIds).toContain(versionId)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('textShapeMigrations - RemoveJustify migration', () => {
|
|
111
|
+
const { up, down } = getTestMigration(textShapeVersions.RemoveJustify)
|
|
112
|
+
|
|
113
|
+
describe('RemoveJustify up migration', () => {
|
|
114
|
+
it('should convert justify alignment to start', () => {
|
|
115
|
+
const oldRecord = {
|
|
116
|
+
id: 'shape:text1',
|
|
117
|
+
props: {
|
|
118
|
+
color: 'black',
|
|
119
|
+
font: 'draw',
|
|
120
|
+
size: 'm',
|
|
121
|
+
align: 'justify',
|
|
122
|
+
w: 200,
|
|
123
|
+
text: 'Test text',
|
|
124
|
+
scale: 1,
|
|
125
|
+
autoSize: true,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = up(oldRecord)
|
|
130
|
+
expect(result.props.align).toBe('start')
|
|
131
|
+
expect(result.props.color).toBe('black') // Preserve other props
|
|
132
|
+
expect(result.props.text).toBe('Test text')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should preserve non-justify alignments', () => {
|
|
136
|
+
const alignments = ['start', 'middle', 'end']
|
|
137
|
+
|
|
138
|
+
alignments.forEach((align) => {
|
|
139
|
+
const oldRecord = {
|
|
140
|
+
id: 'shape:text1',
|
|
141
|
+
props: {
|
|
142
|
+
color: 'red',
|
|
143
|
+
font: 'sans',
|
|
144
|
+
size: 'l',
|
|
145
|
+
align,
|
|
146
|
+
w: 300,
|
|
147
|
+
text: 'Aligned text',
|
|
148
|
+
scale: 1,
|
|
149
|
+
autoSize: false,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = up(oldRecord)
|
|
154
|
+
expect(result.props.align).toBe(align)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should preserve all other properties during migration', () => {
|
|
159
|
+
const oldRecord = {
|
|
160
|
+
id: 'shape:text1',
|
|
161
|
+
props: {
|
|
162
|
+
color: 'blue',
|
|
163
|
+
font: 'serif',
|
|
164
|
+
size: 'xl',
|
|
165
|
+
align: 'justify',
|
|
166
|
+
w: 400,
|
|
167
|
+
text: 'Justified text becomes start aligned',
|
|
168
|
+
scale: 1.5,
|
|
169
|
+
autoSize: false,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = up(oldRecord)
|
|
174
|
+
expect(result.props.align).toBe('start')
|
|
175
|
+
expect(result.props.color).toBe('blue')
|
|
176
|
+
expect(result.props.font).toBe('serif')
|
|
177
|
+
expect(result.props.size).toBe('xl')
|
|
178
|
+
expect(result.props.w).toBe(400)
|
|
179
|
+
expect(result.props.text).toBe('Justified text becomes start aligned')
|
|
180
|
+
expect(result.props.scale).toBe(1.5)
|
|
181
|
+
expect(result.props.autoSize).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('RemoveJustify down migration', () => {
|
|
186
|
+
it('should be retired (no down migration)', () => {
|
|
187
|
+
expect(() => {
|
|
188
|
+
down({})
|
|
189
|
+
}).toThrow('Migration com.tldraw.shape.text/1 does not have a down function')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('textShapeMigrations - AddTextAlign migration', () => {
|
|
195
|
+
const { up, down } = getTestMigration(textShapeVersions.AddTextAlign)
|
|
196
|
+
|
|
197
|
+
describe('AddTextAlign up migration', () => {
|
|
198
|
+
it('should migrate align to textAlign', () => {
|
|
199
|
+
const oldRecord = {
|
|
200
|
+
id: 'shape:text1',
|
|
201
|
+
props: {
|
|
202
|
+
color: 'black',
|
|
203
|
+
font: 'draw',
|
|
204
|
+
size: 'm',
|
|
205
|
+
align: 'start',
|
|
206
|
+
w: 200,
|
|
207
|
+
text: 'Test text',
|
|
208
|
+
scale: 1,
|
|
209
|
+
autoSize: true,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = up(oldRecord)
|
|
214
|
+
expect(result.props.textAlign).toBe('start')
|
|
215
|
+
expect(result.props.align).toBeUndefined()
|
|
216
|
+
expect(result.props.color).toBe('black') // Preserve other props
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should handle all alignment values', () => {
|
|
220
|
+
const alignments = ['start', 'middle', 'end']
|
|
221
|
+
|
|
222
|
+
alignments.forEach((align) => {
|
|
223
|
+
const oldRecord = {
|
|
224
|
+
id: 'shape:text1',
|
|
225
|
+
props: {
|
|
226
|
+
color: 'red',
|
|
227
|
+
align,
|
|
228
|
+
w: 200,
|
|
229
|
+
text: 'Test',
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = up(oldRecord)
|
|
234
|
+
expect(result.props.textAlign).toBe(align)
|
|
235
|
+
expect(result.props.align).toBeUndefined()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should preserve all other properties during migration', () => {
|
|
240
|
+
const oldRecord = {
|
|
241
|
+
id: 'shape:text1',
|
|
242
|
+
props: {
|
|
243
|
+
color: 'green',
|
|
244
|
+
font: 'mono',
|
|
245
|
+
size: 's',
|
|
246
|
+
align: 'middle',
|
|
247
|
+
w: 150,
|
|
248
|
+
text: 'Migration test',
|
|
249
|
+
scale: 2,
|
|
250
|
+
autoSize: false,
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = up(oldRecord)
|
|
255
|
+
expect(result.props.textAlign).toBe('middle')
|
|
256
|
+
expect(result.props.align).toBeUndefined()
|
|
257
|
+
expect(result.props.color).toBe('green')
|
|
258
|
+
expect(result.props.font).toBe('mono')
|
|
259
|
+
expect(result.props.size).toBe('s')
|
|
260
|
+
expect(result.props.w).toBe(150)
|
|
261
|
+
expect(result.props.text).toBe('Migration test')
|
|
262
|
+
expect(result.props.scale).toBe(2)
|
|
263
|
+
expect(result.props.autoSize).toBe(false)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('AddTextAlign down migration', () => {
|
|
268
|
+
it('should migrate textAlign back to align', () => {
|
|
269
|
+
const newRecord = {
|
|
270
|
+
id: 'shape:text1',
|
|
271
|
+
props: {
|
|
272
|
+
color: 'black',
|
|
273
|
+
font: 'draw',
|
|
274
|
+
size: 'm',
|
|
275
|
+
textAlign: 'start',
|
|
276
|
+
w: 200,
|
|
277
|
+
text: 'Test text',
|
|
278
|
+
scale: 1,
|
|
279
|
+
autoSize: true,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const result = down(newRecord)
|
|
284
|
+
expect(result.props.align).toBe('start')
|
|
285
|
+
expect(result.props.textAlign).toBeUndefined()
|
|
286
|
+
expect(result.props.color).toBe('black') // Preserve other props
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should handle all textAlign values during down migration', () => {
|
|
290
|
+
const alignments = ['start', 'middle', 'end']
|
|
291
|
+
|
|
292
|
+
alignments.forEach((textAlign) => {
|
|
293
|
+
const newRecord = {
|
|
294
|
+
id: 'shape:text1',
|
|
295
|
+
props: {
|
|
296
|
+
color: 'blue',
|
|
297
|
+
textAlign,
|
|
298
|
+
w: 200,
|
|
299
|
+
text: 'Test',
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const result = down(newRecord)
|
|
304
|
+
expect(result.props.align).toBe(textAlign)
|
|
305
|
+
expect(result.props.textAlign).toBeUndefined()
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('textShapeMigrations - AddRichText migration', () => {
|
|
312
|
+
const { up } = getTestMigration(textShapeVersions.AddRichText)
|
|
313
|
+
|
|
314
|
+
describe('AddRichText up migration', () => {
|
|
315
|
+
it('should convert text property to richText', () => {
|
|
316
|
+
const oldRecord = {
|
|
317
|
+
id: 'shape:text1',
|
|
318
|
+
props: {
|
|
319
|
+
color: 'black',
|
|
320
|
+
font: 'draw',
|
|
321
|
+
size: 'm',
|
|
322
|
+
textAlign: 'start',
|
|
323
|
+
w: 200,
|
|
324
|
+
text: 'Simple text content',
|
|
325
|
+
scale: 1,
|
|
326
|
+
autoSize: true,
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = up(oldRecord)
|
|
331
|
+
expect(result.props.richText).toBeDefined()
|
|
332
|
+
expect(result.props.text).toBeUndefined()
|
|
333
|
+
expect(result.props.color).toBe('black') // Preserve other props
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should handle empty text', () => {
|
|
337
|
+
const oldRecord = {
|
|
338
|
+
id: 'shape:text1',
|
|
339
|
+
props: {
|
|
340
|
+
color: 'red',
|
|
341
|
+
font: 'sans',
|
|
342
|
+
size: 'l',
|
|
343
|
+
textAlign: 'middle',
|
|
344
|
+
w: 300,
|
|
345
|
+
text: '',
|
|
346
|
+
scale: 1,
|
|
347
|
+
autoSize: false,
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const result = up(oldRecord)
|
|
352
|
+
expect(result.props.richText).toBeDefined()
|
|
353
|
+
expect(result.props.text).toBeUndefined()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('should handle multi-line text', () => {
|
|
357
|
+
const oldRecord = {
|
|
358
|
+
id: 'shape:text1',
|
|
359
|
+
props: {
|
|
360
|
+
color: 'blue',
|
|
361
|
+
font: 'serif',
|
|
362
|
+
size: 'xl',
|
|
363
|
+
textAlign: 'end',
|
|
364
|
+
w: 400,
|
|
365
|
+
text: 'Line 1\nLine 2\nLine 3',
|
|
366
|
+
scale: 1.2,
|
|
367
|
+
autoSize: true,
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = up(oldRecord)
|
|
372
|
+
expect(result.props.richText).toBeDefined()
|
|
373
|
+
expect(result.props.text).toBeUndefined()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should preserve all other properties during migration', () => {
|
|
377
|
+
const oldRecord = {
|
|
378
|
+
id: 'shape:text1',
|
|
379
|
+
props: {
|
|
380
|
+
color: 'green',
|
|
381
|
+
font: 'mono',
|
|
382
|
+
size: 's',
|
|
383
|
+
textAlign: 'start',
|
|
384
|
+
w: 250,
|
|
385
|
+
text: 'Rich text migration test',
|
|
386
|
+
scale: 0.8,
|
|
387
|
+
autoSize: false,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const result = up(oldRecord)
|
|
392
|
+
expect(result.props.richText).toBeDefined()
|
|
393
|
+
expect(result.props.text).toBeUndefined()
|
|
394
|
+
expect(result.props.color).toBe('green')
|
|
395
|
+
expect(result.props.font).toBe('mono')
|
|
396
|
+
expect(result.props.size).toBe('s')
|
|
397
|
+
expect(result.props.textAlign).toBe('start')
|
|
398
|
+
expect(result.props.w).toBe(250)
|
|
399
|
+
expect(result.props.scale).toBe(0.8)
|
|
400
|
+
expect(result.props.autoSize).toBe(false)
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// Note: The down migration is explicitly not defined (forced client update)
|
|
405
|
+
// so we don't test it
|
|
406
|
+
})
|
|
407
|
+
})
|
|
@@ -104,7 +104,6 @@ const Versions = createShapePropsMigrationIds('text', {
|
|
|
104
104
|
RemoveJustify: 1,
|
|
105
105
|
AddTextAlign: 2,
|
|
106
106
|
AddRichText: 3,
|
|
107
|
-
AddRichTextAttrs: 4,
|
|
108
107
|
})
|
|
109
108
|
|
|
110
109
|
/**
|
|
@@ -117,8 +116,8 @@ const Versions = createShapePropsMigrationIds('text', {
|
|
|
117
116
|
* import { textShapeVersions } from '@tldraw/tlschema'
|
|
118
117
|
*
|
|
119
118
|
* // Check if shape data needs migration
|
|
120
|
-
* if (shapeVersion < textShapeVersions.
|
|
121
|
-
* // Apply rich text
|
|
119
|
+
* if (shapeVersion < textShapeVersions.AddRichText) {
|
|
120
|
+
* // Apply rich text migration
|
|
122
121
|
* }
|
|
123
122
|
* ```
|
|
124
123
|
*/
|
|
@@ -132,7 +131,6 @@ export { Versions as textShapeVersions }
|
|
|
132
131
|
* - RemoveJustify: Replaced 'justify' alignment with 'start'
|
|
133
132
|
* - AddTextAlign: Migrated from 'align' to 'textAlign' property
|
|
134
133
|
* - AddRichText: Converted plain text to rich text format
|
|
135
|
-
* - AddRichTextAttrs: Added support for attrs property on richText
|
|
136
134
|
*
|
|
137
135
|
* @public
|
|
138
136
|
*/
|
|
@@ -169,17 +167,5 @@ export const textShapeMigrations = createShapePropsMigrationSequence({
|
|
|
169
167
|
// delete props.richText
|
|
170
168
|
// },
|
|
171
169
|
},
|
|
172
|
-
{
|
|
173
|
-
id: Versions.AddRichTextAttrs,
|
|
174
|
-
up: (_props) => {
|
|
175
|
-
// noop - attrs is optional so old records are valid
|
|
176
|
-
},
|
|
177
|
-
down: (props) => {
|
|
178
|
-
// Remove attrs from richText when migrating down
|
|
179
|
-
if (props.richText && 'attrs' in props.richText) {
|
|
180
|
-
delete props.richText.attrs
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
170
|
],
|
|
185
171
|
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { T } from '@tldraw/validate'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
4
|
+
import { TLAssetId } from '../records/TLAsset'
|
|
5
|
+
import { videoShapeMigrations, videoShapeProps, videoShapeVersions } from './TLVideoShape'
|
|
6
|
+
|
|
7
|
+
describe('TLVideoShape', () => {
|
|
8
|
+
describe('videoShapeProps validation', () => {
|
|
9
|
+
it('should validate dimensions as nonZeroNumber', () => {
|
|
10
|
+
expect(() => videoShapeProps.w.validate(1)).not.toThrow()
|
|
11
|
+
expect(() => videoShapeProps.h.validate(100)).not.toThrow()
|
|
12
|
+
expect(() => videoShapeProps.w.validate(0)).toThrow()
|
|
13
|
+
expect(() => videoShapeProps.h.validate(-1)).toThrow()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should validate time as number including negatives', () => {
|
|
17
|
+
expect(() => videoShapeProps.time.validate(0)).not.toThrow()
|
|
18
|
+
expect(() => videoShapeProps.time.validate(-5)).not.toThrow()
|
|
19
|
+
expect(() => videoShapeProps.time.validate(30.5)).not.toThrow()
|
|
20
|
+
expect(() => videoShapeProps.time.validate('not-number')).toThrow()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should validate boolean properties', () => {
|
|
24
|
+
expect(() => videoShapeProps.playing.validate(true)).not.toThrow()
|
|
25
|
+
expect(() => videoShapeProps.autoplay.validate(false)).not.toThrow()
|
|
26
|
+
expect(() => videoShapeProps.playing.validate('true')).toThrow()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should validate URLs and assetIds', () => {
|
|
30
|
+
expect(() => videoShapeProps.url.validate('')).not.toThrow()
|
|
31
|
+
expect(() => videoShapeProps.url.validate('https://example.com/video.mp4')).not.toThrow()
|
|
32
|
+
expect(() => videoShapeProps.assetId.validate(null)).not.toThrow()
|
|
33
|
+
expect(() => videoShapeProps.assetId.validate('asset:video123' as TLAssetId)).not.toThrow()
|
|
34
|
+
expect(() => videoShapeProps.altText.validate('Alt text')).not.toThrow()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should validate complete props object', () => {
|
|
38
|
+
const validator = T.object(videoShapeProps)
|
|
39
|
+
const validProps = {
|
|
40
|
+
w: 640,
|
|
41
|
+
h: 480,
|
|
42
|
+
time: 0,
|
|
43
|
+
playing: false,
|
|
44
|
+
autoplay: true,
|
|
45
|
+
url: 'https://example.com/video.mp4',
|
|
46
|
+
assetId: 'asset:video123' as TLAssetId,
|
|
47
|
+
altText: 'Test video',
|
|
48
|
+
}
|
|
49
|
+
expect(() => validator.validate(validProps)).not.toThrow()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('videoShapeVersions', () => {
|
|
54
|
+
it('should have expected migration versions', () => {
|
|
55
|
+
expect(videoShapeVersions.AddUrlProp).toBeDefined()
|
|
56
|
+
expect(videoShapeVersions.MakeUrlsValid).toBeDefined()
|
|
57
|
+
expect(videoShapeVersions.AddAltText).toBeDefined()
|
|
58
|
+
expect(videoShapeVersions.AddAutoplay).toBeDefined()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('videoShapeMigrations', () => {
|
|
63
|
+
it('should add url property in AddUrlProp migration', () => {
|
|
64
|
+
const { up } = getTestMigration(videoShapeVersions.AddUrlProp)
|
|
65
|
+
const oldRecord = {
|
|
66
|
+
props: { w: 640, h: 480, time: 30, playing: true, assetId: 'asset:video123' },
|
|
67
|
+
}
|
|
68
|
+
const result = up(oldRecord)
|
|
69
|
+
expect(result.props.url).toBe('')
|
|
70
|
+
expect(result.props.w).toBe(640)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should clear invalid URLs in MakeUrlsValid migration', () => {
|
|
74
|
+
const { up } = getTestMigration(videoShapeVersions.MakeUrlsValid)
|
|
75
|
+
const oldRecord = {
|
|
76
|
+
props: { url: 'invalid-url', w: 400, h: 300 },
|
|
77
|
+
}
|
|
78
|
+
const result = up(oldRecord)
|
|
79
|
+
expect(result.props.url).toBe('')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should add altText in AddAltText migration', () => {
|
|
83
|
+
const { up, down } = getTestMigration(videoShapeVersions.AddAltText)
|
|
84
|
+
const oldRecord = { props: { w: 800, h: 600 } }
|
|
85
|
+
const result = up(oldRecord)
|
|
86
|
+
expect(result.props.altText).toBe('')
|
|
87
|
+
|
|
88
|
+
// Test down migration removes altText
|
|
89
|
+
const newRecord = { props: { w: 640, h: 480, altText: 'Test' } }
|
|
90
|
+
const downResult = down(newRecord)
|
|
91
|
+
expect(downResult.props.altText).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should add autoplay in AddAutoplay migration', () => {
|
|
95
|
+
const { up, down } = getTestMigration(videoShapeVersions.AddAutoplay)
|
|
96
|
+
const oldRecord = { props: { w: 480, h: 270 } }
|
|
97
|
+
const result = up(oldRecord)
|
|
98
|
+
expect(result.props.autoplay).toBe(true)
|
|
99
|
+
|
|
100
|
+
// Test down migration removes autoplay
|
|
101
|
+
const newRecord = { props: { w: 800, h: 450, autoplay: false } }
|
|
102
|
+
const downResult = down(newRecord)
|
|
103
|
+
expect(downResult.props.autoplay).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should have migrations for all versions', () => {
|
|
107
|
+
expect(videoShapeMigrations.sequence).toBeDefined()
|
|
108
|
+
expect(Array.isArray(videoShapeMigrations.sequence)).toBe(true)
|
|
109
|
+
expect(videoShapeMigrations.sequence.length).toBeGreaterThanOrEqual(4)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
package/src/store-migrations.ts
CHANGED
|
@@ -67,36 +67,35 @@ export const storeMigrations = createMigrationSequence({
|
|
|
67
67
|
sequence: [
|
|
68
68
|
{
|
|
69
69
|
id: Versions.RemoveCodeAndIconShapeTypes,
|
|
70
|
-
scope: '
|
|
71
|
-
up: (
|
|
72
|
-
for (const [id, record] of
|
|
70
|
+
scope: 'store',
|
|
71
|
+
up: (store) => {
|
|
72
|
+
for (const [id, record] of objectMapEntries(store)) {
|
|
73
73
|
if (
|
|
74
74
|
record.typeName === 'shape' &&
|
|
75
|
-
|
|
76
|
-
(record.type === 'icon' || record.type === 'code')
|
|
75
|
+
((record as TLShape).type === 'icon' || (record as TLShape).type === 'code')
|
|
77
76
|
) {
|
|
78
|
-
|
|
77
|
+
delete store[id]
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
},
|
|
82
81
|
},
|
|
83
82
|
{
|
|
84
83
|
id: Versions.AddInstancePresenceType,
|
|
85
|
-
scope: '
|
|
86
|
-
up(
|
|
84
|
+
scope: 'store',
|
|
85
|
+
up(_store) {
|
|
87
86
|
// noop
|
|
88
87
|
// there used to be a down migration for this but we made down migrations optional
|
|
89
|
-
// and we don't use them on
|
|
88
|
+
// and we don't use them on store-level migrations so we can just remove it
|
|
90
89
|
},
|
|
91
90
|
},
|
|
92
91
|
{
|
|
93
92
|
// remove user and presence records and add pointer records
|
|
94
93
|
id: Versions.RemoveTLUserAndPresenceAndAddPointer,
|
|
95
|
-
scope: '
|
|
96
|
-
up: (
|
|
97
|
-
for (const [id, record] of
|
|
94
|
+
scope: 'store',
|
|
95
|
+
up: (store) => {
|
|
96
|
+
for (const [id, record] of objectMapEntries(store)) {
|
|
98
97
|
if (record.typeName.match(/^(user|user_presence)$/)) {
|
|
99
|
-
|
|
98
|
+
delete store[id]
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
},
|
|
@@ -104,11 +103,11 @@ export const storeMigrations = createMigrationSequence({
|
|
|
104
103
|
{
|
|
105
104
|
// remove user document records
|
|
106
105
|
id: Versions.RemoveUserDocument,
|
|
107
|
-
scope: '
|
|
108
|
-
up: (
|
|
109
|
-
for (const [id, record] of
|
|
106
|
+
scope: 'store',
|
|
107
|
+
up: (store) => {
|
|
108
|
+
for (const [id, record] of objectMapEntries(store)) {
|
|
110
109
|
if (record.typeName.match('user_document')) {
|
|
111
|
-
|
|
110
|
+
delete store[id]
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
},
|