@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
package/src/index.ts
CHANGED
|
@@ -99,8 +99,6 @@ export {
|
|
|
99
99
|
type TLBindingId,
|
|
100
100
|
type TLBindingUpdate,
|
|
101
101
|
type TLDefaultBinding,
|
|
102
|
-
type TLGlobalBindingPropsMap,
|
|
103
|
-
type TLIndexedBindings,
|
|
104
102
|
type TLUnknownBinding,
|
|
105
103
|
} from './records/TLBinding'
|
|
106
104
|
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
|
|
@@ -148,11 +146,7 @@ export {
|
|
|
148
146
|
isShape,
|
|
149
147
|
isShapeId,
|
|
150
148
|
rootShapeMigrations,
|
|
151
|
-
type ExtractShapeByProps,
|
|
152
|
-
type TLCreateShapePartial,
|
|
153
149
|
type TLDefaultShape,
|
|
154
|
-
type TLGlobalShapePropsMap,
|
|
155
|
-
type TLIndexedShapes,
|
|
156
150
|
type TLParentId,
|
|
157
151
|
type TLShape,
|
|
158
152
|
type TLShapeId,
|
|
@@ -191,7 +185,6 @@ export {
|
|
|
191
185
|
type TLBookmarkShapeProps,
|
|
192
186
|
} from './shapes/TLBookmarkShape'
|
|
193
187
|
export {
|
|
194
|
-
compressLegacySegments,
|
|
195
188
|
drawShapeMigrations,
|
|
196
189
|
drawShapeProps,
|
|
197
190
|
type TLDrawShape,
|
|
@@ -314,5 +307,3 @@ registerTldrawLibraryVersion(
|
|
|
314
307
|
(globalThis as any).TLDRAW_LIBRARY_VERSION,
|
|
315
308
|
(globalThis as any).TLDRAW_LIBRARY_MODULES
|
|
316
309
|
)
|
|
317
|
-
|
|
318
|
-
export { b64Vecs } from './misc/b64Vecs'
|
package/src/migrations.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { instancePresenceVersions } from './records/TLPresence'
|
|
|
16
16
|
import { TLShape, rootShapeVersions } from './records/TLShape'
|
|
17
17
|
import { arrowShapeVersions } from './shapes/TLArrowShape'
|
|
18
18
|
import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
|
|
19
|
-
import {
|
|
19
|
+
import { drawShapeVersions } from './shapes/TLDrawShape'
|
|
20
20
|
import { embedShapeVersions } from './shapes/TLEmbedShape'
|
|
21
21
|
import { frameShapeVersions } from './shapes/TLFrameShape'
|
|
22
22
|
import { geoShapeVersions } from './shapes/TLGeoShape'
|
|
@@ -1444,32 +1444,6 @@ describe('Add rich text', () => {
|
|
|
1444
1444
|
}
|
|
1445
1445
|
})
|
|
1446
1446
|
|
|
1447
|
-
describe('Add rich text attrs', () => {
|
|
1448
|
-
const migrations = [
|
|
1449
|
-
['text shape', getTestMigration(textShapeVersions.AddRichTextAttrs)],
|
|
1450
|
-
['geo shape', getTestMigration(geoShapeVersions.AddRichTextAttrs)],
|
|
1451
|
-
['note shape', getTestMigration(noteShapeVersions.AddRichTextAttrs)],
|
|
1452
|
-
['arrow shape', getTestMigration(arrowShapeVersions.AddRichTextAttrs)],
|
|
1453
|
-
] as const
|
|
1454
|
-
|
|
1455
|
-
for (const [shapeName, { up, down }] of migrations) {
|
|
1456
|
-
it(`works for ${shapeName}`, () => {
|
|
1457
|
-
const shape = { props: { richText: toRichText('hello, world') } }
|
|
1458
|
-
const shapeWithAttrs = {
|
|
1459
|
-
props: { richText: { ...toRichText('hello, world'), attrs: { test: 'value' } } },
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// Up migration should be a noop
|
|
1463
|
-
expect(up(shape)).toEqual(shape)
|
|
1464
|
-
expect(up(shapeWithAttrs)).toEqual(shapeWithAttrs)
|
|
1465
|
-
|
|
1466
|
-
// Down migration should remove attrs
|
|
1467
|
-
expect(down(shapeWithAttrs)).toEqual(shape)
|
|
1468
|
-
expect(down(shape)).toEqual(shape)
|
|
1469
|
-
})
|
|
1470
|
-
}
|
|
1471
|
-
})
|
|
1472
|
-
|
|
1473
1447
|
describe('Make urls valid for all the assets', () => {
|
|
1474
1448
|
const migrations = [
|
|
1475
1449
|
['bookmark asset', getTestMigration(bookmarkAssetVersions.MakeUrlsValid)],
|
|
@@ -2268,128 +2242,6 @@ describe('TLVideoAsset AddAutoplay', () => {
|
|
|
2268
2242
|
})
|
|
2269
2243
|
})
|
|
2270
2244
|
|
|
2271
|
-
describe('Add scaleX, scaleY, and new base64 format to draw shape', () => {
|
|
2272
|
-
const { up, down } = getTestMigration(drawShapeVersions.Base64)
|
|
2273
|
-
|
|
2274
|
-
test('up works as expected', () => {
|
|
2275
|
-
const legacySegments = [
|
|
2276
|
-
{
|
|
2277
|
-
type: 'free',
|
|
2278
|
-
points: [
|
|
2279
|
-
{ x: 0, y: 0, z: 0.5 },
|
|
2280
|
-
{ x: 10, y: 10, z: 0.6 },
|
|
2281
|
-
{ x: 20, y: 20, z: 0.7 },
|
|
2282
|
-
],
|
|
2283
|
-
},
|
|
2284
|
-
{
|
|
2285
|
-
type: 'straight',
|
|
2286
|
-
points: [
|
|
2287
|
-
{ x: 20, y: 20, z: 0.7 },
|
|
2288
|
-
{ x: 30, y: 30, z: 0.8 },
|
|
2289
|
-
],
|
|
2290
|
-
},
|
|
2291
|
-
]
|
|
2292
|
-
expect(
|
|
2293
|
-
up({
|
|
2294
|
-
props: {
|
|
2295
|
-
segments: legacySegments,
|
|
2296
|
-
},
|
|
2297
|
-
})
|
|
2298
|
-
).toEqual({
|
|
2299
|
-
props: {
|
|
2300
|
-
scaleX: 1,
|
|
2301
|
-
scaleY: 1,
|
|
2302
|
-
segments: compressLegacySegments(legacySegments as any),
|
|
2303
|
-
},
|
|
2304
|
-
})
|
|
2305
|
-
})
|
|
2306
|
-
|
|
2307
|
-
test('down works as expected', () => {
|
|
2308
|
-
const legacySegments = [
|
|
2309
|
-
{
|
|
2310
|
-
type: 'free',
|
|
2311
|
-
points: [
|
|
2312
|
-
{ x: 0, y: 0, z: 0.5 },
|
|
2313
|
-
{ x: 10, y: 10, z: 0.6 },
|
|
2314
|
-
],
|
|
2315
|
-
},
|
|
2316
|
-
]
|
|
2317
|
-
const compressed = compressLegacySegments(legacySegments as any)
|
|
2318
|
-
const result = down({
|
|
2319
|
-
props: {
|
|
2320
|
-
scaleX: 1,
|
|
2321
|
-
scaleY: 1,
|
|
2322
|
-
segments: compressed,
|
|
2323
|
-
},
|
|
2324
|
-
})
|
|
2325
|
-
expect(result.props.scaleX).toBeUndefined()
|
|
2326
|
-
expect(result.props.scaleY).toBeUndefined()
|
|
2327
|
-
expect(Array.isArray(result.props.segments[0].points)).toBe(true)
|
|
2328
|
-
expect(result.props.segments[0].points.length).toBe(2)
|
|
2329
|
-
})
|
|
2330
|
-
})
|
|
2331
|
-
|
|
2332
|
-
describe('Add scaleX, scaleY, and new base64 format to highlight shape', () => {
|
|
2333
|
-
const { up, down } = getTestMigration(highlightShapeVersions.Base64)
|
|
2334
|
-
|
|
2335
|
-
test('up works as expected', () => {
|
|
2336
|
-
const legacySegments = [
|
|
2337
|
-
{
|
|
2338
|
-
type: 'free',
|
|
2339
|
-
points: [
|
|
2340
|
-
{ x: 0, y: 0, z: 0.5 },
|
|
2341
|
-
{ x: 10, y: 10, z: 0.6 },
|
|
2342
|
-
{ x: 20, y: 20, z: 0.7 },
|
|
2343
|
-
],
|
|
2344
|
-
},
|
|
2345
|
-
{
|
|
2346
|
-
type: 'straight',
|
|
2347
|
-
points: [
|
|
2348
|
-
{ x: 20, y: 20, z: 0.7 },
|
|
2349
|
-
{ x: 30, y: 30, z: 0.8 },
|
|
2350
|
-
],
|
|
2351
|
-
},
|
|
2352
|
-
]
|
|
2353
|
-
expect(
|
|
2354
|
-
up({
|
|
2355
|
-
props: {
|
|
2356
|
-
segments: legacySegments,
|
|
2357
|
-
},
|
|
2358
|
-
})
|
|
2359
|
-
).toEqual({
|
|
2360
|
-
props: {
|
|
2361
|
-
scaleX: 1,
|
|
2362
|
-
scaleY: 1,
|
|
2363
|
-
segments: compressLegacySegments(legacySegments as any),
|
|
2364
|
-
},
|
|
2365
|
-
})
|
|
2366
|
-
})
|
|
2367
|
-
|
|
2368
|
-
test('down works as expected', () => {
|
|
2369
|
-
const legacySegments = [
|
|
2370
|
-
{
|
|
2371
|
-
type: 'free',
|
|
2372
|
-
points: [
|
|
2373
|
-
{ x: 0, y: 0, z: 0.5 },
|
|
2374
|
-
{ x: 10, y: 10, z: 0.6 },
|
|
2375
|
-
],
|
|
2376
|
-
},
|
|
2377
|
-
]
|
|
2378
|
-
const compressed = compressLegacySegments(legacySegments as any)
|
|
2379
|
-
const result = down({
|
|
2380
|
-
props: {
|
|
2381
|
-
scaleX: 1,
|
|
2382
|
-
scaleY: 1,
|
|
2383
|
-
segments: compressed,
|
|
2384
|
-
},
|
|
2385
|
-
})
|
|
2386
|
-
expect(result.props.scaleX).toBeUndefined()
|
|
2387
|
-
expect(result.props.scaleY).toBeUndefined()
|
|
2388
|
-
expect(Array.isArray(result.props.segments[0].points)).toBe(true)
|
|
2389
|
-
expect(result.props.segments[0].points.length).toBe(2)
|
|
2390
|
-
})
|
|
2391
|
-
})
|
|
2392
|
-
|
|
2393
2245
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
|
2394
2246
|
|
|
2395
2247
|
// check that all migrator fns were called at least once
|
package/src/misc/TLOpacity.ts
CHANGED
|
@@ -53,4 +53,8 @@ export type TLOpacityType = number
|
|
|
53
53
|
*
|
|
54
54
|
* @public
|
|
55
55
|
*/
|
|
56
|
-
export const opacityValidator = T.
|
|
56
|
+
export const opacityValidator = T.number.check((n) => {
|
|
57
|
+
if (n < 0 || n > 1) {
|
|
58
|
+
throw new T.ValidationError('Opacity must be between 0 and 1')
|
|
59
|
+
}
|
|
60
|
+
})
|
package/src/misc/TLRichText.ts
CHANGED
|
@@ -12,12 +12,7 @@ import { T } from '@tldraw/validate'
|
|
|
12
12
|
* const isValid = richTextValidator.check(richText) // true
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
export const richTextValidator = T.object({
|
|
17
|
-
type: T.string,
|
|
18
|
-
content: T.arrayOf(T.unknown),
|
|
19
|
-
attrs: T.any.optional(),
|
|
20
|
-
})
|
|
15
|
+
export const richTextValidator = T.object({ type: T.string, content: T.arrayOf(T.unknown) })
|
|
21
16
|
|
|
22
17
|
/**
|
|
23
18
|
* Type representing rich text content in tldraw. Rich text follows a document-based
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RecordId, UnknownRecord } from '@tldraw/store'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { idValidator } from './id-validator'
|
|
4
|
+
|
|
5
|
+
// Mock record types for testing
|
|
6
|
+
interface MockShapeRecord extends UnknownRecord {
|
|
7
|
+
typeName: 'shape'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MockPageRecord extends UnknownRecord {
|
|
11
|
+
typeName: 'page'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type MockShapeId = RecordId<MockShapeRecord>
|
|
15
|
+
type MockPageId = RecordId<MockPageRecord>
|
|
16
|
+
|
|
17
|
+
describe('idValidator', () => {
|
|
18
|
+
it('should validate correct IDs with proper prefix', () => {
|
|
19
|
+
const shapeValidator = idValidator<MockShapeId>('shape')
|
|
20
|
+
const pageValidator = idValidator<MockPageId>('page')
|
|
21
|
+
|
|
22
|
+
expect(shapeValidator.validate('shape:abc123')).toBe('shape:abc123')
|
|
23
|
+
expect(shapeValidator.validate('shape:')).toBe('shape:')
|
|
24
|
+
expect(pageValidator.validate('page:main')).toBe('page:main')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should reject IDs with wrong prefix', () => {
|
|
28
|
+
const shapeValidator = idValidator<MockShapeId>('shape')
|
|
29
|
+
|
|
30
|
+
expect(() => shapeValidator.validate('page:abc123')).toThrow(
|
|
31
|
+
'shape ID must start with "shape:"'
|
|
32
|
+
)
|
|
33
|
+
expect(() => shapeValidator.validate('asset:xyz789')).toThrow(
|
|
34
|
+
'shape ID must start with "shape:"'
|
|
35
|
+
)
|
|
36
|
+
expect(() => shapeValidator.validate('abc123')).toThrow('shape ID must start with "shape:"')
|
|
37
|
+
expect(() => shapeValidator.validate('')).toThrow('shape ID must start with "shape:"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should work with different prefixes independently', () => {
|
|
41
|
+
const shapeValidator = idValidator<MockShapeId>('shape')
|
|
42
|
+
const pageValidator = idValidator<MockPageId>('page')
|
|
43
|
+
|
|
44
|
+
expect(shapeValidator.isValid('shape:abc123')).toBe(true)
|
|
45
|
+
expect(shapeValidator.isValid('page:abc123')).toBe(false)
|
|
46
|
+
|
|
47
|
+
expect(pageValidator.isValid('shape:abc123')).toBe(false)
|
|
48
|
+
expect(pageValidator.isValid('page:abc123')).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
3
|
+
import {
|
|
4
|
+
assetMigrations,
|
|
5
|
+
AssetRecordType,
|
|
6
|
+
assetValidator,
|
|
7
|
+
assetVersions,
|
|
8
|
+
TLAssetId,
|
|
9
|
+
} from './TLAsset'
|
|
10
|
+
|
|
11
|
+
describe('TLAsset', () => {
|
|
12
|
+
describe('assetValidator', () => {
|
|
13
|
+
it('should validate different asset types using discriminated union', () => {
|
|
14
|
+
const imageAsset = {
|
|
15
|
+
id: 'asset:test_image',
|
|
16
|
+
typeName: 'asset' as const,
|
|
17
|
+
type: 'image' as const,
|
|
18
|
+
props: {
|
|
19
|
+
w: 100,
|
|
20
|
+
h: 100,
|
|
21
|
+
name: 'test.jpg',
|
|
22
|
+
isAnimated: false,
|
|
23
|
+
mimeType: 'image/jpeg',
|
|
24
|
+
src: 'https://example.com/test.jpg',
|
|
25
|
+
},
|
|
26
|
+
meta: {},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const videoAsset = {
|
|
30
|
+
id: 'asset:test_video',
|
|
31
|
+
typeName: 'asset' as const,
|
|
32
|
+
type: 'video' as const,
|
|
33
|
+
props: {
|
|
34
|
+
w: 640,
|
|
35
|
+
h: 480,
|
|
36
|
+
name: 'test.mp4',
|
|
37
|
+
isAnimated: true,
|
|
38
|
+
mimeType: 'video/mp4',
|
|
39
|
+
src: 'https://example.com/test.mp4',
|
|
40
|
+
},
|
|
41
|
+
meta: {},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bookmarkAsset = {
|
|
45
|
+
id: 'asset:test_bookmark',
|
|
46
|
+
typeName: 'asset' as const,
|
|
47
|
+
type: 'bookmark' as const,
|
|
48
|
+
props: {
|
|
49
|
+
title: 'Test Site',
|
|
50
|
+
description: 'A test bookmark',
|
|
51
|
+
image: 'https://example.com/image.png',
|
|
52
|
+
favicon: 'https://example.com/favicon.ico',
|
|
53
|
+
src: 'https://example.com',
|
|
54
|
+
},
|
|
55
|
+
meta: {},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(() => assetValidator.validate(imageAsset)).not.toThrow()
|
|
59
|
+
expect(() => assetValidator.validate(videoAsset)).not.toThrow()
|
|
60
|
+
expect(() => assetValidator.validate(bookmarkAsset)).not.toThrow()
|
|
61
|
+
|
|
62
|
+
expect(assetValidator.validate(imageAsset).type).toBe('image')
|
|
63
|
+
expect(assetValidator.validate(videoAsset).type).toBe('video')
|
|
64
|
+
expect(assetValidator.validate(bookmarkAsset).type).toBe('bookmark')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should reject invalid asset types', () => {
|
|
68
|
+
const invalidAsset = {
|
|
69
|
+
id: 'asset:invalid',
|
|
70
|
+
typeName: 'asset' as const,
|
|
71
|
+
type: 'invalid_type' as any,
|
|
72
|
+
props: {
|
|
73
|
+
w: 100,
|
|
74
|
+
h: 100,
|
|
75
|
+
name: 'test.jpg',
|
|
76
|
+
},
|
|
77
|
+
meta: {},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(() => assetValidator.validate(invalidAsset)).toThrow()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should require valid asset structure', () => {
|
|
84
|
+
// Wrong typeName
|
|
85
|
+
const wrongTypeName = {
|
|
86
|
+
id: 'asset:wrong_typename',
|
|
87
|
+
typeName: 'shape' as const,
|
|
88
|
+
type: 'image' as const,
|
|
89
|
+
props: {
|
|
90
|
+
w: 100,
|
|
91
|
+
h: 100,
|
|
92
|
+
name: 'test.jpg',
|
|
93
|
+
isAnimated: false,
|
|
94
|
+
mimeType: 'image/jpeg',
|
|
95
|
+
src: 'https://example.com/test.jpg',
|
|
96
|
+
},
|
|
97
|
+
meta: {},
|
|
98
|
+
}
|
|
99
|
+
expect(() => assetValidator.validate(wrongTypeName)).toThrow()
|
|
100
|
+
|
|
101
|
+
// Missing meta
|
|
102
|
+
const noMeta = {
|
|
103
|
+
id: 'asset:no_meta',
|
|
104
|
+
typeName: 'asset' as const,
|
|
105
|
+
type: 'image' as const,
|
|
106
|
+
props: {
|
|
107
|
+
w: 100,
|
|
108
|
+
h: 100,
|
|
109
|
+
name: 'test.jpg',
|
|
110
|
+
isAnimated: false,
|
|
111
|
+
mimeType: 'image/jpeg',
|
|
112
|
+
src: 'https://example.com/test.jpg',
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
expect(() => assetValidator.validate(noMeta)).toThrow()
|
|
116
|
+
|
|
117
|
+
// Wrong ID prefix
|
|
118
|
+
const wrongId = {
|
|
119
|
+
id: 'shape:wrong_prefix',
|
|
120
|
+
typeName: 'asset' as const,
|
|
121
|
+
type: 'image' as const,
|
|
122
|
+
props: {
|
|
123
|
+
w: 100,
|
|
124
|
+
h: 100,
|
|
125
|
+
name: 'test.jpg',
|
|
126
|
+
isAnimated: false,
|
|
127
|
+
mimeType: 'image/jpeg',
|
|
128
|
+
src: 'https://example.com/test.jpg',
|
|
129
|
+
},
|
|
130
|
+
meta: {},
|
|
131
|
+
}
|
|
132
|
+
expect(() => assetValidator.validate(wrongId)).toThrow()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('assetMigrations', () => {
|
|
137
|
+
it('should have correct migration structure', () => {
|
|
138
|
+
expect(assetMigrations.sequenceId).toBe('com.tldraw.asset')
|
|
139
|
+
expect(assetMigrations.sequence).toHaveLength(1)
|
|
140
|
+
expect(assetMigrations.sequence[0].id).toBe(assetVersions.AddMeta)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('AddMeta migration', () => {
|
|
145
|
+
const { up } = getTestMigration(assetVersions.AddMeta)
|
|
146
|
+
|
|
147
|
+
it('should add empty meta property', () => {
|
|
148
|
+
const assetWithoutMeta = {
|
|
149
|
+
id: 'asset:test',
|
|
150
|
+
typeName: 'asset',
|
|
151
|
+
type: 'image',
|
|
152
|
+
props: {
|
|
153
|
+
w: 100,
|
|
154
|
+
h: 100,
|
|
155
|
+
name: 'test.jpg',
|
|
156
|
+
isAnimated: false,
|
|
157
|
+
mimeType: 'image/jpeg',
|
|
158
|
+
src: 'https://example.com/test.jpg',
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const result = up(assetWithoutMeta)
|
|
163
|
+
expect(result.meta).toEqual({})
|
|
164
|
+
expect(result.props).toEqual(assetWithoutMeta.props)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should overwrite existing meta property', () => {
|
|
168
|
+
const assetWithMeta = {
|
|
169
|
+
id: 'asset:test',
|
|
170
|
+
typeName: 'asset',
|
|
171
|
+
type: 'image',
|
|
172
|
+
props: {
|
|
173
|
+
w: 100,
|
|
174
|
+
h: 100,
|
|
175
|
+
name: 'test.jpg',
|
|
176
|
+
isAnimated: false,
|
|
177
|
+
mimeType: 'image/jpeg',
|
|
178
|
+
src: 'https://example.com/test.jpg',
|
|
179
|
+
},
|
|
180
|
+
meta: { existing: 'data' },
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = up(assetWithMeta)
|
|
184
|
+
expect(result.meta).toEqual({}) // Migration always sets to empty object
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should preserve other properties during migration', () => {
|
|
188
|
+
const assetWithExtraProps = {
|
|
189
|
+
id: 'asset:extra_props',
|
|
190
|
+
typeName: 'asset',
|
|
191
|
+
type: 'image',
|
|
192
|
+
props: {
|
|
193
|
+
w: 200,
|
|
194
|
+
h: 150,
|
|
195
|
+
name: 'extra.png',
|
|
196
|
+
isAnimated: false,
|
|
197
|
+
mimeType: 'image/png',
|
|
198
|
+
src: 'https://example.com/extra.png',
|
|
199
|
+
},
|
|
200
|
+
customProperty: 'should be preserved',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = up(assetWithExtraProps)
|
|
204
|
+
expect(result.meta).toEqual({})
|
|
205
|
+
expect(result.customProperty).toBe('should be preserved')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('AssetRecordType', () => {
|
|
210
|
+
it('should have correct configuration', () => {
|
|
211
|
+
expect(AssetRecordType.typeName).toBe('asset')
|
|
212
|
+
expect(AssetRecordType.scope).toBe('document')
|
|
213
|
+
expect(AssetRecordType.validator).toBe(assetValidator)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should create records with default meta property', () => {
|
|
217
|
+
const assetRecord = AssetRecordType.create({
|
|
218
|
+
id: 'asset:test' as TLAssetId,
|
|
219
|
+
type: 'image',
|
|
220
|
+
props: {
|
|
221
|
+
w: 100,
|
|
222
|
+
h: 100,
|
|
223
|
+
name: 'test.jpg',
|
|
224
|
+
isAnimated: false,
|
|
225
|
+
mimeType: 'image/jpeg',
|
|
226
|
+
src: 'https://example.com/test.jpg',
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
expect(assetRecord.meta).toEqual({})
|
|
231
|
+
expect(assetRecord.typeName).toBe('asset')
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
})
|
package/src/records/TLAsset.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { TLBaseAsset } from '../assets/TLBaseAsset'
|
|
|
9
9
|
import { bookmarkAssetValidator, TLBookmarkAsset } from '../assets/TLBookmarkAsset'
|
|
10
10
|
import { imageAssetValidator, TLImageAsset } from '../assets/TLImageAsset'
|
|
11
11
|
import { TLVideoAsset, videoAssetValidator } from '../assets/TLVideoAsset'
|
|
12
|
-
import {
|
|
12
|
+
import { TLShape } from './TLShape'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Union type representing all possible asset types in tldraw.
|
|
@@ -222,4 +222,4 @@ export type TLAssetId = RecordId<TLBaseAsset<any, any>>
|
|
|
222
222
|
*
|
|
223
223
|
* @public
|
|
224
224
|
*/
|
|
225
|
-
export type TLAssetShape =
|
|
225
|
+
export type TLAssetShape = Extract<TLShape, { props: { assetId: TLAssetId } }>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createBindingId, rootBindingMigrations } from './TLBinding'
|
|
3
|
+
|
|
4
|
+
describe('TLBinding', () => {
|
|
5
|
+
describe('createBindingId function', () => {
|
|
6
|
+
it('should generate IDs starting with binding:', () => {
|
|
7
|
+
const id = createBindingId()
|
|
8
|
+
expect(id.startsWith('binding:')).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should use custom ID when provided', () => {
|
|
12
|
+
expect(createBindingId('test')).toBe('binding:test')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('rootBindingMigrations', () => {
|
|
17
|
+
it('should have correct structure', () => {
|
|
18
|
+
expect(rootBindingMigrations.sequenceId).toBe('com.tldraw.binding')
|
|
19
|
+
expect(Array.isArray(rootBindingMigrations.sequence)).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
package/src/records/TLBinding.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createRecordMigrationSequence,
|
|
6
6
|
createRecordType,
|
|
7
7
|
} from '@tldraw/store'
|
|
8
|
-
import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
|
|
8
|
+
import { Expand, mapObjectMapValues, uniqueId } from '@tldraw/utils'
|
|
9
9
|
import { T } from '@tldraw/validate'
|
|
10
10
|
import { TLArrowBinding } from '../bindings/TLArrowBinding'
|
|
11
11
|
import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
|
|
@@ -56,42 +56,10 @@ export type TLDefaultBinding = TLArrowBinding
|
|
|
56
56
|
*/
|
|
57
57
|
export type TLUnknownBinding = TLBaseBinding<string, object>
|
|
58
58
|
|
|
59
|
-
/** @public */
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
61
|
-
export interface TLGlobalBindingPropsMap {}
|
|
62
|
-
|
|
63
|
-
/** @public */
|
|
64
|
-
// prettier-ignore
|
|
65
|
-
export type TLIndexedBindings = {
|
|
66
|
-
// We iterate over a union of augmented keys and default binding types.
|
|
67
|
-
// This allows us to include (or conditionally exclude or override) the default bindings in one go.
|
|
68
|
-
//
|
|
69
|
-
// In the `as` clause we are filtering out disabled bindings.
|
|
70
|
-
[K in keyof TLGlobalBindingPropsMap | TLDefaultBinding['type'] as K extends TLDefaultBinding['type']
|
|
71
|
-
? K extends keyof TLGlobalBindingPropsMap
|
|
72
|
-
? // if it extends a nullish value the user has disabled this binding type so we filter it out with never
|
|
73
|
-
TLGlobalBindingPropsMap[K] extends null | undefined
|
|
74
|
-
? never
|
|
75
|
-
: K
|
|
76
|
-
: K
|
|
77
|
-
: K]: K extends TLDefaultBinding['type']
|
|
78
|
-
? // if it's a default binding type we need to check if it's been overridden
|
|
79
|
-
K extends keyof TLGlobalBindingPropsMap
|
|
80
|
-
? // if it has been overriden then use the custom binding definition
|
|
81
|
-
TLBaseBinding<K, TLGlobalBindingPropsMap[K]>
|
|
82
|
-
: // if it has not been overriden then reuse existing type aliases for better type display
|
|
83
|
-
Extract<TLDefaultBinding, { type: K }>
|
|
84
|
-
: // use the custom binding definition
|
|
85
|
-
TLBaseBinding<K, TLGlobalBindingPropsMap[K & keyof TLGlobalBindingPropsMap]>
|
|
86
|
-
}
|
|
87
|
-
|
|
88
59
|
/**
|
|
89
|
-
* The set of all bindings that are available in the editor.
|
|
60
|
+
* The set of all bindings that are available in the editor, including unknown bindings.
|
|
90
61
|
* Bindings represent relationships between shapes, such as arrows connecting to other shapes.
|
|
91
62
|
*
|
|
92
|
-
* You can use this type without a type argument to work with any binding, or pass
|
|
93
|
-
* a specific binding type string (e.g., `'arrow'`) to narrow down to that specific binding type.
|
|
94
|
-
*
|
|
95
63
|
* @example
|
|
96
64
|
* ```ts
|
|
97
65
|
* // Check binding type and handle accordingly
|
|
@@ -105,17 +73,11 @@ export type TLIndexedBindings = {
|
|
|
105
73
|
* break
|
|
106
74
|
* }
|
|
107
75
|
* }
|
|
108
|
-
*
|
|
109
|
-
* // Narrow to a specific binding type by passing the type as a generic argument
|
|
110
|
-
* function getArrowSourceId(binding: TLBinding<'arrow'>) {
|
|
111
|
-
* return binding.fromId // TypeScript knows this is a TLArrowBinding
|
|
112
|
-
* }
|
|
113
76
|
* ```
|
|
114
77
|
*
|
|
115
78
|
* @public
|
|
116
79
|
*/
|
|
117
|
-
export type TLBinding
|
|
118
|
-
TLIndexedBindings[K]
|
|
80
|
+
export type TLBinding = TLDefaultBinding | TLUnknownBinding
|
|
119
81
|
|
|
120
82
|
/**
|
|
121
83
|
* Type for updating existing bindings with partial properties.
|
|
@@ -137,17 +99,15 @@ export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBinding
|
|
|
137
99
|
*
|
|
138
100
|
* @public
|
|
139
101
|
*/
|
|
140
|
-
export type TLBindingUpdate<T extends TLBinding = TLBinding> =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
: never
|
|
102
|
+
export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
|
|
103
|
+
id: TLBindingId
|
|
104
|
+
type: T['type']
|
|
105
|
+
typeName?: T['typeName']
|
|
106
|
+
fromId?: T['fromId']
|
|
107
|
+
toId?: T['toId']
|
|
108
|
+
props?: Partial<T['props']>
|
|
109
|
+
meta?: Partial<T['meta']>
|
|
110
|
+
}>
|
|
151
111
|
|
|
152
112
|
/**
|
|
153
113
|
* Type for creating new bindings with required fromId and toId.
|
|
@@ -173,17 +133,15 @@ export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
|
|
|
173
133
|
*
|
|
174
134
|
* @public
|
|
175
135
|
*/
|
|
176
|
-
export type TLBindingCreate<T extends TLBinding = TLBinding> =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
: never
|
|
136
|
+
export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
|
|
137
|
+
id?: TLBindingId
|
|
138
|
+
type: T['type']
|
|
139
|
+
typeName?: T['typeName']
|
|
140
|
+
fromId: T['fromId']
|
|
141
|
+
toId: T['toId']
|
|
142
|
+
props?: Partial<T['props']>
|
|
143
|
+
meta?: Partial<T['meta']>
|
|
144
|
+
}>
|
|
187
145
|
|
|
188
146
|
/**
|
|
189
147
|
* Branded string type for binding record identifiers.
|
|
@@ -208,7 +166,7 @@ export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
|
|
|
208
166
|
*
|
|
209
167
|
* @public
|
|
210
168
|
*/
|
|
211
|
-
export type TLBindingId = RecordId<
|
|
169
|
+
export type TLBindingId = RecordId<TLUnknownBinding>
|
|
212
170
|
|
|
213
171
|
/**
|
|
214
172
|
* Migration version identifiers for the root binding record schema.
|
|
@@ -417,7 +375,7 @@ export function createBindingPropsMigrationIds<S extends string, T extends Recor
|
|
|
417
375
|
* @internal
|
|
418
376
|
*/
|
|
419
377
|
export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
|
|
420
|
-
return createRecordType('binding', {
|
|
378
|
+
return createRecordType<TLBinding>('binding', {
|
|
421
379
|
scope: 'document',
|
|
422
380
|
validator: T.model(
|
|
423
381
|
'binding',
|