@tldraw/tlschema 5.3.0-internal.fba91ed55f6c → 5.3.0-next.91d2a20b898c
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/index.d.ts +0 -185
- package/dist-cjs/index.js +1 -11
- package/dist-cjs/index.js.map +2 -2
- package/dist-esm/index.d.mts +0 -185
- package/dist-esm/index.mjs +1 -21
- package/dist-esm/index.mjs.map +2 -2
- package/package.json +5 -5
- package/src/index.ts +0 -16
- package/dist-cjs/records/TLComment.js +0 -172
- package/dist-cjs/records/TLComment.js.map +0 -7
- package/dist-esm/records/TLComment.mjs +0 -152
- package/dist-esm/records/TLComment.mjs.map +0 -7
- package/src/records/TLComment.test.ts +0 -182
- package/src/records/TLComment.ts +0 -305
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/tlschema",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (schema).",
|
|
4
|
-
"version": "5.3.0-
|
|
4
|
+
"version": "5.3.0-next.91d2a20b898c",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"vitest": "^4.1.7"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@tldraw/state": "5.3.0-
|
|
55
|
-
"@tldraw/store": "5.3.0-
|
|
56
|
-
"@tldraw/utils": "5.3.0-
|
|
57
|
-
"@tldraw/validate": "5.3.0-
|
|
54
|
+
"@tldraw/state": "5.3.0-next.91d2a20b898c",
|
|
55
|
+
"@tldraw/store": "5.3.0-next.91d2a20b898c",
|
|
56
|
+
"@tldraw/utils": "5.3.0-next.91d2a20b898c",
|
|
57
|
+
"@tldraw/validate": "5.3.0-next.91d2a20b898c"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"react": "^18.2.0 || ^19.2.1",
|
package/src/index.ts
CHANGED
|
@@ -116,22 +116,6 @@ export {
|
|
|
116
116
|
type TLUnknownBinding,
|
|
117
117
|
} from './records/TLBinding'
|
|
118
118
|
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
|
|
119
|
-
export {
|
|
120
|
-
commentRecordConfig,
|
|
121
|
-
commentSchemaRecords,
|
|
122
|
-
commentThreadRecordConfig,
|
|
123
|
-
createComment,
|
|
124
|
-
createCommentId,
|
|
125
|
-
createCommentThread,
|
|
126
|
-
createCommentThreadId,
|
|
127
|
-
isCommentId,
|
|
128
|
-
isCommentThreadId,
|
|
129
|
-
type TLComment,
|
|
130
|
-
type TLCommentAnchor,
|
|
131
|
-
type TLCommentId,
|
|
132
|
-
type TLCommentThread,
|
|
133
|
-
type TLCommentThreadId,
|
|
134
|
-
} from './records/TLComment'
|
|
135
119
|
export {
|
|
136
120
|
createCustomRecordId,
|
|
137
121
|
createCustomRecordMigrationIds,
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var TLComment_exports = {};
|
|
20
|
-
__export(TLComment_exports, {
|
|
21
|
-
commentRecordConfig: () => commentRecordConfig,
|
|
22
|
-
commentSchemaRecords: () => commentSchemaRecords,
|
|
23
|
-
commentThreadRecordConfig: () => commentThreadRecordConfig,
|
|
24
|
-
createComment: () => createComment,
|
|
25
|
-
createCommentId: () => createCommentId,
|
|
26
|
-
createCommentThread: () => createCommentThread,
|
|
27
|
-
createCommentThreadId: () => createCommentThreadId,
|
|
28
|
-
isCommentId: () => isCommentId,
|
|
29
|
-
isCommentThreadId: () => isCommentThreadId
|
|
30
|
-
});
|
|
31
|
-
module.exports = __toCommonJS(TLComment_exports);
|
|
32
|
-
var import_store = require("@tldraw/store");
|
|
33
|
-
var import_validate = require("@tldraw/validate");
|
|
34
|
-
var import_id_validator = require("../misc/id-validator");
|
|
35
|
-
var import_TLRichText = require("../misc/TLRichText");
|
|
36
|
-
var import_TLCustomRecord = require("./TLCustomRecord");
|
|
37
|
-
const commentAnchorValidator = import_validate.T.union("type", {
|
|
38
|
-
shape: import_validate.T.object({
|
|
39
|
-
type: import_validate.T.literal("shape"),
|
|
40
|
-
shapeId: (0, import_id_validator.idValidator)("shape"),
|
|
41
|
-
x: import_validate.T.number,
|
|
42
|
-
y: import_validate.T.number,
|
|
43
|
-
isPrecise: import_validate.T.boolean
|
|
44
|
-
}),
|
|
45
|
-
point: import_validate.T.object({
|
|
46
|
-
type: import_validate.T.literal("point"),
|
|
47
|
-
x: import_validate.T.number,
|
|
48
|
-
y: import_validate.T.number
|
|
49
|
-
}),
|
|
50
|
-
region: import_validate.T.object({
|
|
51
|
-
type: import_validate.T.literal("region"),
|
|
52
|
-
x: import_validate.T.number,
|
|
53
|
-
y: import_validate.T.number,
|
|
54
|
-
w: import_validate.T.number,
|
|
55
|
-
h: import_validate.T.number
|
|
56
|
-
}),
|
|
57
|
-
page: import_validate.T.object({
|
|
58
|
-
type: import_validate.T.literal("page")
|
|
59
|
-
}),
|
|
60
|
-
"text-range": import_validate.T.object({
|
|
61
|
-
type: import_validate.T.literal("text-range"),
|
|
62
|
-
shapeId: (0, import_id_validator.idValidator)("shape"),
|
|
63
|
-
from: import_validate.T.number,
|
|
64
|
-
to: import_validate.T.number
|
|
65
|
-
})
|
|
66
|
-
});
|
|
67
|
-
function createCommentGuardMigrations(typeName, extra = []) {
|
|
68
|
-
return (0, import_store.createRecordMigrationSequence)({
|
|
69
|
-
sequenceId: `com.tldraw.${typeName}`,
|
|
70
|
-
recordType: typeName,
|
|
71
|
-
retroactive: true,
|
|
72
|
-
sequence: [
|
|
73
|
-
{
|
|
74
|
-
id: `com.tldraw.${typeName}/1`,
|
|
75
|
-
up: (record) => record
|
|
76
|
-
},
|
|
77
|
-
...extra
|
|
78
|
-
]
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
const commentThreadRecordConfig = {
|
|
82
|
-
scope: "document",
|
|
83
|
-
migrations: createCommentGuardMigrations("comment-thread", [
|
|
84
|
-
{
|
|
85
|
-
// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).
|
|
86
|
-
id: "com.tldraw.comment-thread/2",
|
|
87
|
-
up: (record) => {
|
|
88
|
-
const anchor = record.anchor;
|
|
89
|
-
if (anchor?.type === "shape" && anchor.x === void 0) {
|
|
90
|
-
;
|
|
91
|
-
record.anchor = { ...anchor, x: 1, y: 0, isPrecise: false };
|
|
92
|
-
}
|
|
93
|
-
return record;
|
|
94
|
-
},
|
|
95
|
-
down: (record) => {
|
|
96
|
-
const anchor = record.anchor;
|
|
97
|
-
if (anchor?.type === "shape") {
|
|
98
|
-
;
|
|
99
|
-
record.anchor = { type: "shape", shapeId: anchor.shapeId };
|
|
100
|
-
}
|
|
101
|
-
return record;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
]),
|
|
105
|
-
validator: import_validate.T.object({
|
|
106
|
-
id: (0, import_id_validator.idValidator)("comment-thread"),
|
|
107
|
-
typeName: import_validate.T.literal("comment-thread"),
|
|
108
|
-
pageId: (0, import_id_validator.idValidator)("page"),
|
|
109
|
-
anchor: commentAnchorValidator,
|
|
110
|
-
createdBy: import_validate.T.string,
|
|
111
|
-
createdAt: import_validate.T.number,
|
|
112
|
-
resolved: import_validate.T.object({ at: import_validate.T.number, by: import_validate.T.string }).nullable(),
|
|
113
|
-
meta: import_validate.T.jsonValue
|
|
114
|
-
})
|
|
115
|
-
};
|
|
116
|
-
const commentRecordConfig = {
|
|
117
|
-
scope: "document",
|
|
118
|
-
migrations: createCommentGuardMigrations("comment"),
|
|
119
|
-
validator: import_validate.T.object({
|
|
120
|
-
id: (0, import_id_validator.idValidator)("comment"),
|
|
121
|
-
typeName: import_validate.T.literal("comment"),
|
|
122
|
-
threadId: (0, import_id_validator.idValidator)("comment-thread"),
|
|
123
|
-
pageId: (0, import_id_validator.idValidator)("page"),
|
|
124
|
-
authorId: import_validate.T.string,
|
|
125
|
-
createdAt: import_validate.T.number,
|
|
126
|
-
editedAt: import_validate.T.number.nullable(),
|
|
127
|
-
body: import_TLRichText.richTextValidator,
|
|
128
|
-
meta: import_validate.T.jsonValue
|
|
129
|
-
})
|
|
130
|
-
};
|
|
131
|
-
const commentSchemaRecords = {
|
|
132
|
-
"comment-thread": commentThreadRecordConfig,
|
|
133
|
-
comment: commentRecordConfig
|
|
134
|
-
};
|
|
135
|
-
function createCommentThreadId(id) {
|
|
136
|
-
return (0, import_TLCustomRecord.createCustomRecordId)("comment-thread", id);
|
|
137
|
-
}
|
|
138
|
-
function createCommentId(id) {
|
|
139
|
-
return (0, import_TLCustomRecord.createCustomRecordId)("comment", id);
|
|
140
|
-
}
|
|
141
|
-
function isCommentThreadId(id) {
|
|
142
|
-
return (0, import_TLCustomRecord.isCustomRecordId)("comment-thread", id);
|
|
143
|
-
}
|
|
144
|
-
function isCommentId(id) {
|
|
145
|
-
return (0, import_TLCustomRecord.isCustomRecordId)("comment", id);
|
|
146
|
-
}
|
|
147
|
-
function createCommentThread(props) {
|
|
148
|
-
return {
|
|
149
|
-
id: createCommentThreadId(),
|
|
150
|
-
typeName: "comment-thread",
|
|
151
|
-
pageId: props.pageId,
|
|
152
|
-
anchor: props.anchor,
|
|
153
|
-
createdBy: props.createdBy,
|
|
154
|
-
createdAt: props.now ?? Date.now(),
|
|
155
|
-
resolved: null,
|
|
156
|
-
meta: props.meta ?? {}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function createComment(props) {
|
|
160
|
-
return {
|
|
161
|
-
id: createCommentId(),
|
|
162
|
-
typeName: "comment",
|
|
163
|
-
threadId: props.threadId,
|
|
164
|
-
pageId: props.pageId,
|
|
165
|
-
authorId: props.authorId,
|
|
166
|
-
createdAt: props.now ?? Date.now(),
|
|
167
|
-
editedAt: null,
|
|
168
|
-
body: props.body,
|
|
169
|
-
meta: props.meta ?? {}
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
//# sourceMappingURL=TLComment.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/records/TLComment.ts"],
|
|
4
|
-
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's page bounds, so\n * the pin keeps its spot as the shape moves and resizes. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n * - `text-range` \u2014 pinned to a character range inside a shape's text\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| { type: 'region'; x: number; y: number; w: number; h: number }\n\t| { type: 'page' }\n\t| { type: 'text-range'; shapeId: TLShapeId; from: number; to: number }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n\t'text-range': T.object({\n\t\ttype: T.literal('text-range'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tfrom: T.number,\n\t\tto: T.number,\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads and comments sync. Register both types together \u2014 a comment without its thread type\n * (or vice versa) will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { createRecordMigrationSequence } from "@tldraw/store";
|
|
2
|
-
import { T } from "@tldraw/validate";
|
|
3
|
-
import { idValidator } from "../misc/id-validator.mjs";
|
|
4
|
-
import { richTextValidator } from "../misc/TLRichText.mjs";
|
|
5
|
-
import { createCustomRecordId, isCustomRecordId } from "./TLCustomRecord.mjs";
|
|
6
|
-
const commentAnchorValidator = T.union("type", {
|
|
7
|
-
shape: T.object({
|
|
8
|
-
type: T.literal("shape"),
|
|
9
|
-
shapeId: idValidator("shape"),
|
|
10
|
-
x: T.number,
|
|
11
|
-
y: T.number,
|
|
12
|
-
isPrecise: T.boolean
|
|
13
|
-
}),
|
|
14
|
-
point: T.object({
|
|
15
|
-
type: T.literal("point"),
|
|
16
|
-
x: T.number,
|
|
17
|
-
y: T.number
|
|
18
|
-
}),
|
|
19
|
-
region: T.object({
|
|
20
|
-
type: T.literal("region"),
|
|
21
|
-
x: T.number,
|
|
22
|
-
y: T.number,
|
|
23
|
-
w: T.number,
|
|
24
|
-
h: T.number
|
|
25
|
-
}),
|
|
26
|
-
page: T.object({
|
|
27
|
-
type: T.literal("page")
|
|
28
|
-
}),
|
|
29
|
-
"text-range": T.object({
|
|
30
|
-
type: T.literal("text-range"),
|
|
31
|
-
shapeId: idValidator("shape"),
|
|
32
|
-
from: T.number,
|
|
33
|
-
to: T.number
|
|
34
|
-
})
|
|
35
|
-
});
|
|
36
|
-
function createCommentGuardMigrations(typeName, extra = []) {
|
|
37
|
-
return createRecordMigrationSequence({
|
|
38
|
-
sequenceId: `com.tldraw.${typeName}`,
|
|
39
|
-
recordType: typeName,
|
|
40
|
-
retroactive: true,
|
|
41
|
-
sequence: [
|
|
42
|
-
{
|
|
43
|
-
id: `com.tldraw.${typeName}/1`,
|
|
44
|
-
up: (record) => record
|
|
45
|
-
},
|
|
46
|
-
...extra
|
|
47
|
-
]
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
const commentThreadRecordConfig = {
|
|
51
|
-
scope: "document",
|
|
52
|
-
migrations: createCommentGuardMigrations("comment-thread", [
|
|
53
|
-
{
|
|
54
|
-
// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).
|
|
55
|
-
id: "com.tldraw.comment-thread/2",
|
|
56
|
-
up: (record) => {
|
|
57
|
-
const anchor = record.anchor;
|
|
58
|
-
if (anchor?.type === "shape" && anchor.x === void 0) {
|
|
59
|
-
;
|
|
60
|
-
record.anchor = { ...anchor, x: 1, y: 0, isPrecise: false };
|
|
61
|
-
}
|
|
62
|
-
return record;
|
|
63
|
-
},
|
|
64
|
-
down: (record) => {
|
|
65
|
-
const anchor = record.anchor;
|
|
66
|
-
if (anchor?.type === "shape") {
|
|
67
|
-
;
|
|
68
|
-
record.anchor = { type: "shape", shapeId: anchor.shapeId };
|
|
69
|
-
}
|
|
70
|
-
return record;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
]),
|
|
74
|
-
validator: T.object({
|
|
75
|
-
id: idValidator("comment-thread"),
|
|
76
|
-
typeName: T.literal("comment-thread"),
|
|
77
|
-
pageId: idValidator("page"),
|
|
78
|
-
anchor: commentAnchorValidator,
|
|
79
|
-
createdBy: T.string,
|
|
80
|
-
createdAt: T.number,
|
|
81
|
-
resolved: T.object({ at: T.number, by: T.string }).nullable(),
|
|
82
|
-
meta: T.jsonValue
|
|
83
|
-
})
|
|
84
|
-
};
|
|
85
|
-
const commentRecordConfig = {
|
|
86
|
-
scope: "document",
|
|
87
|
-
migrations: createCommentGuardMigrations("comment"),
|
|
88
|
-
validator: T.object({
|
|
89
|
-
id: idValidator("comment"),
|
|
90
|
-
typeName: T.literal("comment"),
|
|
91
|
-
threadId: idValidator("comment-thread"),
|
|
92
|
-
pageId: idValidator("page"),
|
|
93
|
-
authorId: T.string,
|
|
94
|
-
createdAt: T.number,
|
|
95
|
-
editedAt: T.number.nullable(),
|
|
96
|
-
body: richTextValidator,
|
|
97
|
-
meta: T.jsonValue
|
|
98
|
-
})
|
|
99
|
-
};
|
|
100
|
-
const commentSchemaRecords = {
|
|
101
|
-
"comment-thread": commentThreadRecordConfig,
|
|
102
|
-
comment: commentRecordConfig
|
|
103
|
-
};
|
|
104
|
-
function createCommentThreadId(id) {
|
|
105
|
-
return createCustomRecordId("comment-thread", id);
|
|
106
|
-
}
|
|
107
|
-
function createCommentId(id) {
|
|
108
|
-
return createCustomRecordId("comment", id);
|
|
109
|
-
}
|
|
110
|
-
function isCommentThreadId(id) {
|
|
111
|
-
return isCustomRecordId("comment-thread", id);
|
|
112
|
-
}
|
|
113
|
-
function isCommentId(id) {
|
|
114
|
-
return isCustomRecordId("comment", id);
|
|
115
|
-
}
|
|
116
|
-
function createCommentThread(props) {
|
|
117
|
-
return {
|
|
118
|
-
id: createCommentThreadId(),
|
|
119
|
-
typeName: "comment-thread",
|
|
120
|
-
pageId: props.pageId,
|
|
121
|
-
anchor: props.anchor,
|
|
122
|
-
createdBy: props.createdBy,
|
|
123
|
-
createdAt: props.now ?? Date.now(),
|
|
124
|
-
resolved: null,
|
|
125
|
-
meta: props.meta ?? {}
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function createComment(props) {
|
|
129
|
-
return {
|
|
130
|
-
id: createCommentId(),
|
|
131
|
-
typeName: "comment",
|
|
132
|
-
threadId: props.threadId,
|
|
133
|
-
pageId: props.pageId,
|
|
134
|
-
authorId: props.authorId,
|
|
135
|
-
createdAt: props.now ?? Date.now(),
|
|
136
|
-
editedAt: null,
|
|
137
|
-
body: props.body,
|
|
138
|
-
meta: props.meta ?? {}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
export {
|
|
142
|
-
commentRecordConfig,
|
|
143
|
-
commentSchemaRecords,
|
|
144
|
-
commentThreadRecordConfig,
|
|
145
|
-
createComment,
|
|
146
|
-
createCommentId,
|
|
147
|
-
createCommentThread,
|
|
148
|
-
createCommentThreadId,
|
|
149
|
-
isCommentId,
|
|
150
|
-
isCommentThreadId
|
|
151
|
-
};
|
|
152
|
-
//# sourceMappingURL=TLComment.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/records/TLComment.ts"],
|
|
4
|
-
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's page bounds, so\n * the pin keeps its spot as the shape moves and resizes. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n * - `text-range` \u2014 pinned to a character range inside a shape's text\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| { type: 'region'; x: number; y: number; w: number; h: number }\n\t| { type: 'page' }\n\t| { type: 'text-range'; shapeId: TLShapeId; from: number; to: number }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n\t'text-range': T.object({\n\t\ttype: T.literal('text-range'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tfrom: T.number,\n\t\tto: T.number,\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads and comments sync. Register both types together \u2014 a comment without its thread type\n * (or vice versa) will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createTLSchema } from '../createTLSchema'
|
|
3
|
-
import { toRichText } from '../misc/TLRichText'
|
|
4
|
-
import {
|
|
5
|
-
commentRecordConfig,
|
|
6
|
-
commentSchemaRecords,
|
|
7
|
-
commentThreadRecordConfig,
|
|
8
|
-
createComment,
|
|
9
|
-
createCommentId,
|
|
10
|
-
createCommentThread,
|
|
11
|
-
createCommentThreadId,
|
|
12
|
-
isCommentId,
|
|
13
|
-
isCommentThreadId,
|
|
14
|
-
TLCommentAnchor,
|
|
15
|
-
} from './TLComment'
|
|
16
|
-
import { TLPageId } from './TLPage'
|
|
17
|
-
import { TLShapeId } from './TLShape'
|
|
18
|
-
|
|
19
|
-
const pageId = 'page:page1' as TLPageId
|
|
20
|
-
const shapeId = 'shape:box1' as TLShapeId
|
|
21
|
-
|
|
22
|
-
const anchors: TLCommentAnchor[] = [
|
|
23
|
-
{ type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
|
|
24
|
-
{ type: 'point', x: 100, y: 200 },
|
|
25
|
-
{ type: 'region', x: 0, y: 0, w: 300, h: 150 },
|
|
26
|
-
{ type: 'page' },
|
|
27
|
-
{ type: 'text-range', shapeId, from: 3, to: 12 },
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
describe('TLCommentThread', () => {
|
|
31
|
-
it.each(anchors)('creates a valid thread anchored to %j', (anchor) => {
|
|
32
|
-
const thread = createCommentThread({ pageId, anchor, createdBy: 'user1', now: 1000 })
|
|
33
|
-
expect(commentThreadRecordConfig.validator.validate(thread)).toEqual(thread)
|
|
34
|
-
expect(thread).toMatchObject({
|
|
35
|
-
typeName: 'comment-thread',
|
|
36
|
-
pageId,
|
|
37
|
-
anchor,
|
|
38
|
-
createdAt: 1000,
|
|
39
|
-
resolved: null,
|
|
40
|
-
meta: {},
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('validates resolution state', () => {
|
|
45
|
-
const thread = createCommentThread({
|
|
46
|
-
pageId,
|
|
47
|
-
anchor: { type: 'page' },
|
|
48
|
-
createdBy: 'user1',
|
|
49
|
-
now: 1000,
|
|
50
|
-
})
|
|
51
|
-
const resolved = { ...thread, resolved: { at: 2000, by: 'user2' } }
|
|
52
|
-
expect(commentThreadRecordConfig.validator.validate(resolved)).toEqual(resolved)
|
|
53
|
-
expect(() =>
|
|
54
|
-
commentThreadRecordConfig.validator.validate({ ...thread, resolved: { at: 2000 } })
|
|
55
|
-
).toThrow()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('rejects unknown anchor kinds and malformed anchors', () => {
|
|
59
|
-
const thread = createCommentThread({
|
|
60
|
-
pageId,
|
|
61
|
-
anchor: { type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
|
|
62
|
-
createdBy: 'user1',
|
|
63
|
-
now: 1000,
|
|
64
|
-
})
|
|
65
|
-
expect(() =>
|
|
66
|
-
commentThreadRecordConfig.validator.validate({
|
|
67
|
-
...thread,
|
|
68
|
-
anchor: { type: 'volume', level: 11 },
|
|
69
|
-
})
|
|
70
|
-
).toThrow()
|
|
71
|
-
expect(() =>
|
|
72
|
-
commentThreadRecordConfig.validator.validate({
|
|
73
|
-
...thread,
|
|
74
|
-
// shape anchors must reference a shape id
|
|
75
|
-
anchor: { type: 'shape', shapeId: 'page:nope', x: 1, y: 0, isPrecise: false },
|
|
76
|
-
})
|
|
77
|
-
).toThrow()
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe('TLComment', () => {
|
|
82
|
-
const threadId = createCommentThreadId('thread1')
|
|
83
|
-
|
|
84
|
-
it('creates a valid comment with a rich text body', () => {
|
|
85
|
-
const comment = createComment({
|
|
86
|
-
threadId,
|
|
87
|
-
pageId,
|
|
88
|
-
authorId: 'user1',
|
|
89
|
-
body: toRichText('hello'),
|
|
90
|
-
now: 1000,
|
|
91
|
-
})
|
|
92
|
-
expect(commentRecordConfig.validator.validate(comment)).toEqual(comment)
|
|
93
|
-
expect(comment).toMatchObject({
|
|
94
|
-
typeName: 'comment',
|
|
95
|
-
threadId,
|
|
96
|
-
pageId,
|
|
97
|
-
createdAt: 1000,
|
|
98
|
-
editedAt: null,
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('accepts an editedAt timestamp after edits', () => {
|
|
103
|
-
const comment = createComment({
|
|
104
|
-
threadId,
|
|
105
|
-
pageId,
|
|
106
|
-
authorId: 'user1',
|
|
107
|
-
body: toRichText('hello'),
|
|
108
|
-
now: 1000,
|
|
109
|
-
})
|
|
110
|
-
const edited = { ...comment, editedAt: 2000, body: toRichText('hello, edited') }
|
|
111
|
-
expect(commentRecordConfig.validator.validate(edited)).toEqual(edited)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('rejects a threadId that is not a comment-thread id', () => {
|
|
115
|
-
const comment = createComment({
|
|
116
|
-
threadId,
|
|
117
|
-
pageId,
|
|
118
|
-
authorId: 'user1',
|
|
119
|
-
body: toRichText('hello'),
|
|
120
|
-
now: 1000,
|
|
121
|
-
})
|
|
122
|
-
expect(() =>
|
|
123
|
-
commentRecordConfig.validator.validate({ ...comment, threadId: 'comment:not-a-thread' })
|
|
124
|
-
).toThrow()
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('schema registration', () => {
|
|
129
|
-
it('registers both record types via commentSchemaRecords', () => {
|
|
130
|
-
const schema = createTLSchema({ records: commentSchemaRecords })
|
|
131
|
-
// custom record types aren't part of the default TLRecord union, so index untyped
|
|
132
|
-
const types = schema.types as Record<string, { scope: string } | undefined>
|
|
133
|
-
expect(types['comment-thread']?.scope).toBe('document')
|
|
134
|
-
expect(types['comment']?.scope).toBe('document')
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
describe('isCommentThreadId / isCommentId', () => {
|
|
139
|
-
it('isCommentThreadId accepts comment-thread ids and rejects everything else', () => {
|
|
140
|
-
expect(isCommentThreadId(createCommentThreadId())).toBe(true)
|
|
141
|
-
expect(isCommentThreadId(createCommentId())).toBe(false)
|
|
142
|
-
expect(isCommentThreadId('shape:box1')).toBe(false)
|
|
143
|
-
expect(isCommentThreadId('not-an-id')).toBe(false)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('isCommentId accepts comment ids and rejects everything else, including comment-thread ids', () => {
|
|
147
|
-
expect(isCommentId(createCommentId())).toBe(true)
|
|
148
|
-
expect(isCommentId(createCommentThreadId())).toBe(false)
|
|
149
|
-
expect(isCommentId('shape:box1')).toBe(false)
|
|
150
|
-
expect(isCommentId('not-an-id')).toBe(false)
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
describe('comment-thread migrations', () => {
|
|
155
|
-
const migration = (commentThreadRecordConfig.migrations as any).sequence.find(
|
|
156
|
-
(m: any) => m.id === 'com.tldraw.comment-thread/2'
|
|
157
|
-
) as { up(r: any): any; down(r: any): any }
|
|
158
|
-
|
|
159
|
-
it('stamps existing shape anchors with the top-right position (x:1, y:0)', () => {
|
|
160
|
-
expect(migration.up({ anchor: { type: 'shape', shapeId } })).toEqual({
|
|
161
|
-
anchor: { type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('leaves shape anchors that already have x/y unchanged', () => {
|
|
166
|
-
const anchor = { type: 'shape', shapeId, x: 0.25, y: 0.75, isPrecise: true }
|
|
167
|
-
expect(migration.up({ anchor })).toEqual({ anchor })
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('leaves non-shape anchors untouched', () => {
|
|
171
|
-
const anchor = { type: 'point', x: 5, y: 6 }
|
|
172
|
-
expect(migration.up({ anchor })).toEqual({ anchor })
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('down-migrates by dropping x/y', () => {
|
|
176
|
-
expect(
|
|
177
|
-
migration.down({ anchor: { type: 'shape', shapeId, x: 0.3, y: 0.4, isPrecise: true } })
|
|
178
|
-
).toEqual({
|
|
179
|
-
anchor: { type: 'shape', shapeId },
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
})
|