@tldraw/tlschema 4.6.0-next.1f489710ee41 → 4.6.0-next.4dde09fa17ab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/TLStore.js +13 -0
- package/dist-cjs/TLStore.js.map +2 -2
- package/dist-cjs/createPresenceStateDerivation.js +6 -4
- package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
- package/dist-cjs/createTLSchema.js +8 -1
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +211 -28
- package/dist-cjs/index.js +9 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/records/TLRecord.js.map +1 -1
- package/dist-cjs/records/TLUser.js +101 -0
- package/dist-cjs/records/TLUser.js.map +7 -0
- package/dist-cjs/shapes/TLNoteShape.js +13 -2
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-esm/TLStore.mjs +13 -0
- package/dist-esm/TLStore.mjs.map +2 -2
- package/dist-esm/createPresenceStateDerivation.mjs +6 -4
- package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs +8 -1
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +211 -28
- package/dist-esm/index.mjs +17 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/records/TLUser.mjs +85 -0
- package/dist-esm/records/TLUser.mjs.map +7 -0
- package/dist-esm/shapes/TLNoteShape.mjs +13 -2
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/package.json +6 -6
- package/src/TLStore.test.ts +5 -0
- package/src/TLStore.ts +95 -1
- package/src/createPresenceStateDerivation.test.ts +33 -20
- package/src/createPresenceStateDerivation.ts +20 -25
- package/src/createTLSchema.ts +52 -0
- package/src/index.ts +13 -1
- package/src/migrations.test.ts +22 -0
- package/src/records/TLRecord.ts +2 -0
- package/src/records/TLUser.ts +148 -0
- package/src/shapes/TLNoteShape.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/shapes/TLNoteShape.ts"],
|
|
4
|
-
"sourcesContent": ["import { T } from '@tldraw/validate'\nimport { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'\nimport { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'\nimport { RecordProps } from '../recordsWithProps'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultFontStyle, TLDefaultFontStyle } from '../styles/TLFontStyle'\nimport {\n\tDefaultHorizontalAlignStyle,\n\tTLDefaultHorizontalAlignStyle,\n} from '../styles/TLHorizontalAlignStyle'\nimport { DefaultSizeStyle, TLDefaultSizeStyle } from '../styles/TLSizeStyle'\nimport {\n\tDefaultVerticalAlignStyle,\n\tTLDefaultVerticalAlignStyle,\n} from '../styles/TLVerticalAlignStyle'\nimport { TLBaseShape } from './TLBaseShape'\n\n/**\n * Properties for a note shape. Note shapes represent sticky notes or text annotations\n * with rich formatting capabilities and various styling options.\n *\n * @public\n * @example\n * ```ts\n * const noteProps: TLNoteShapeProps = {\n * color: 'yellow',\n * labelColor: 'black',\n * size: 'm',\n * font: 'draw',\n * fontSizeAdjustment: 0,\n * align: 'middle',\n * verticalAlign: 'middle',\n * growY: 0,\n * url: '',\n * richText: toRichText('Hello **world**!'),\n * scale: 1\n * }\n * ```\n */\nexport interface TLNoteShapeProps {\n\t/** Background color style of the note */\n\tcolor: TLDefaultColorStyle\n\t/** Text color style for the note content */\n\tlabelColor: TLDefaultColorStyle\n\t/** Size style determining the font size and note dimensions */\n\tsize: TLDefaultSizeStyle\n\t/** Font family style for the note text */\n\tfont: TLDefaultFontStyle\n\t/** Adjustment to the base font size (positive increases, negative decreases) */\n\tfontSizeAdjustment: number\n\t/** Horizontal alignment of text within the note */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment of text within the note */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Additional height growth for the note beyond its base size */\n\tgrowY: number\n\t/** Optional URL associated with the note for linking */\n\turl: string\n\t/** Rich text content with formatting like bold, italic, etc. */\n\trichText: TLRichText\n\t/** Scale factor applied to the note shape for display */\n\tscale: number\n}\n\n/**\n * A note shape representing a sticky note or text annotation on the canvas.\n * Note shapes support rich text formatting, various styling options, and can\n * be used for annotations, reminders, or general text content.\n *\n * @public\n * @example\n * ```ts\n * const noteShape: TLNoteShape = {\n * id: 'shape:note1',\n * type: 'note',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:main',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * color: 'light-blue',\n * labelColor: 'black',\n * size: 's',\n * font: 'sans',\n * fontSizeAdjustment: 2,\n * align: 'start',\n * verticalAlign: 'start',\n * growY: 50,\n * url: 'https://example.com',\n * richText: toRichText('Important **note**!'),\n * scale: 1\n * },\n * meta: {},\n * typeName: 'shape'\n * }\n * ```\n */\nexport type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>\n\n/**\n * Validation schema for note shape properties. Defines the runtime validation rules\n * for all properties of note shapes, ensuring data integrity and type safety.\n *\n * @public\n * @example\n * ```ts\n * import { noteShapeProps } from '@tldraw/tlschema'\n *\n * // Used internally by the validation system\n * const validator = T.object(noteShapeProps)\n * const validatedProps = validator.validate(someNoteProps)\n * ```\n */\nexport const noteShapeProps: RecordProps<TLNoteShape> = {\n\tcolor: DefaultColorStyle,\n\tlabelColor: DefaultLabelColorStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\tfontSizeAdjustment: T.positiveNumber,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\tgrowY: T.positiveNumber,\n\turl: T.linkUrl,\n\trichText: richTextValidator,\n\tscale: T.nonZeroNumber,\n}\n\nconst Versions = createShapePropsMigrationIds('note', {\n\tAddUrlProp: 1,\n\tRemoveJustify: 2,\n\tMigrateLegacyAlign: 3,\n\tAddVerticalAlign: 4,\n\tMakeUrlsValid: 5,\n\tAddFontSizeAdjustment: 6,\n\tAddScale: 7,\n\tAddLabelColor: 8,\n\tAddRichText: 9,\n\tAddRichTextAttrs: 10,\n})\n\n/**\n * Version identifiers for note shape migrations. These version numbers track\n * significant schema changes over time, enabling proper data migration between versions.\n *\n * @public\n */\nexport { Versions as noteShapeVersions }\n\n/**\n * Migration sequence for note shapes. Handles schema evolution over time by defining\n * how to upgrade and downgrade note shape data between different versions. Includes\n * migrations for URL properties, text alignment changes, vertical alignment addition,\n * font size adjustments, scaling support, label color, the transition from plain text to rich text,\n * and support for attrs property on richText.\n *\n * @public\n */\nexport const noteShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.AddUrlProp,\n\t\t\tup: (props) => {\n\t\t\t\tprops.url = ''\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.RemoveJustify,\n\t\t\tup: (props) => {\n\t\t\t\tif (props.align === 'justify') {\n\t\t\t\t\tprops.align = 'start'\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tprops.align = 'start-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tprops.align = 'end-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tprops.align = 'middle-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddVerticalAlign,\n\t\t\tup: (props) => {\n\t\t\t\tprops.verticalAlign = 'middle'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MakeUrlsValid,\n\t\t\tup: (props) => {\n\t\t\t\tif (!T.linkUrl.isValid(props.url)) {\n\t\t\t\t\tprops.url = ''\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFontSizeAdjustment,\n\t\t\tup: (props) => {\n\t\t\t\tprops.fontSizeAdjustment = 0\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.fontSizeAdjustment\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddScale,\n\t\t\tup: (props) => {\n\t\t\t\tprops.scale = 1\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.scale\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.labelColor\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichText,\n\t\t\tup: (props) => {\n\t\t\t\tprops.richText = toRichText(props.text)\n\t\t\t\tdelete props.text\n\t\t\t},\n\t\t\t// N.B. Explicitly no down state so that we force clients to update.\n\t\t\t// down: (props) => {\n\t\t\t// \tdelete props.richText\n\t\t\t// },\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichTextAttrs,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop - attrs is optional so old records are valid\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\t// Remove attrs from richText when migrating down\n\t\t\t\tif (props.richText && 'attrs' in props.richText) {\n\t\t\t\t\tdelete props.richText.attrs\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t],\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAqB,mBAAmB,kBAAkB;AAC1D,SAAS,8BAA8B,yCAAyC;AAEhF;AAAA,EACC;AAAA,EACA;AAAA,OAEM;AACP,SAAS,wBAA4C;AACrD;AAAA,EACC;AAAA,OAEM;AACP,SAAS,wBAA4C;AACrD;AAAA,EACC;AAAA,OAEM;
|
|
4
|
+
"sourcesContent": ["import { T } from '@tldraw/validate'\nimport { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'\nimport { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'\nimport { RecordProps } from '../recordsWithProps'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultFontStyle, TLDefaultFontStyle } from '../styles/TLFontStyle'\nimport {\n\tDefaultHorizontalAlignStyle,\n\tTLDefaultHorizontalAlignStyle,\n} from '../styles/TLHorizontalAlignStyle'\nimport { DefaultSizeStyle, TLDefaultSizeStyle } from '../styles/TLSizeStyle'\nimport {\n\tDefaultVerticalAlignStyle,\n\tTLDefaultVerticalAlignStyle,\n} from '../styles/TLVerticalAlignStyle'\nimport { TLBaseShape } from './TLBaseShape'\n\n/**\n * Properties for a note shape. Note shapes represent sticky notes or text annotations\n * with rich formatting capabilities and various styling options.\n *\n * @public\n * @example\n * ```ts\n * const noteProps: TLNoteShapeProps = {\n * color: 'yellow',\n * labelColor: 'black',\n * size: 'm',\n * font: 'draw',\n * fontSizeAdjustment: 0,\n * align: 'middle',\n * verticalAlign: 'middle',\n * growY: 0,\n * url: '',\n * richText: toRichText('Hello **world**!'),\n * scale: 1\n * }\n * ```\n */\nexport interface TLNoteShapeProps {\n\t/** Background color style of the note */\n\tcolor: TLDefaultColorStyle\n\t/** Text color style for the note content */\n\tlabelColor: TLDefaultColorStyle\n\t/** Size style determining the font size and note dimensions */\n\tsize: TLDefaultSizeStyle\n\t/** Font family style for the note text */\n\tfont: TLDefaultFontStyle\n\t/** Adjustment to the base font size (positive increases, negative decreases) */\n\tfontSizeAdjustment: number\n\t/** Horizontal alignment of text within the note */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment of text within the note */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Additional height growth for the note beyond its base size */\n\tgrowY: number\n\t/** Optional URL associated with the note for linking */\n\turl: string\n\t/** Rich text content with formatting like bold, italic, etc. */\n\trichText: TLRichText\n\t/** Scale factor applied to the note shape for display */\n\tscale: number\n\t/** User ID of the person who first edited the note text */\n\ttextFirstEditedBy: string | null\n}\n\n/**\n * A note shape representing a sticky note or text annotation on the canvas.\n * Note shapes support rich text formatting, various styling options, and can\n * be used for annotations, reminders, or general text content.\n *\n * @public\n * @example\n * ```ts\n * const noteShape: TLNoteShape = {\n * id: 'shape:note1',\n * type: 'note',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:main',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * color: 'light-blue',\n * labelColor: 'black',\n * size: 's',\n * font: 'sans',\n * fontSizeAdjustment: 2,\n * align: 'start',\n * verticalAlign: 'start',\n * growY: 50,\n * url: 'https://example.com',\n * richText: toRichText('Important **note**!'),\n * scale: 1\n * },\n * meta: {},\n * typeName: 'shape'\n * }\n * ```\n */\nexport type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>\n\n/**\n * Validation schema for note shape properties. Defines the runtime validation rules\n * for all properties of note shapes, ensuring data integrity and type safety.\n *\n * @public\n * @example\n * ```ts\n * import { noteShapeProps } from '@tldraw/tlschema'\n *\n * // Used internally by the validation system\n * const validator = T.object(noteShapeProps)\n * const validatedProps = validator.validate(someNoteProps)\n * ```\n */\nexport const noteShapeProps: RecordProps<TLNoteShape> = {\n\tcolor: DefaultColorStyle,\n\tlabelColor: DefaultLabelColorStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\tfontSizeAdjustment: T.positiveNumber,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\tgrowY: T.positiveNumber,\n\turl: T.linkUrl,\n\trichText: richTextValidator,\n\tscale: T.nonZeroNumber,\n\ttextFirstEditedBy: T.string.nullable(),\n}\n\nconst Versions = createShapePropsMigrationIds('note', {\n\tAddUrlProp: 1,\n\tRemoveJustify: 2,\n\tMigrateLegacyAlign: 3,\n\tAddVerticalAlign: 4,\n\tMakeUrlsValid: 5,\n\tAddFontSizeAdjustment: 6,\n\tAddScale: 7,\n\tAddLabelColor: 8,\n\tAddRichText: 9,\n\tAddRichTextAttrs: 10,\n\tAddFirstEditedBy: 11,\n})\n\n/**\n * Version identifiers for note shape migrations. These version numbers track\n * significant schema changes over time, enabling proper data migration between versions.\n *\n * @public\n */\nexport { Versions as noteShapeVersions }\n\n/**\n * Migration sequence for note shapes. Handles schema evolution over time by defining\n * how to upgrade and downgrade note shape data between different versions. Includes\n * migrations for URL properties, text alignment changes, vertical alignment addition,\n * font size adjustments, scaling support, label color, the transition from plain text to rich text,\n * and support for attrs property on richText.\n *\n * @public\n */\nexport const noteShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.AddUrlProp,\n\t\t\tup: (props) => {\n\t\t\t\tprops.url = ''\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.RemoveJustify,\n\t\t\tup: (props) => {\n\t\t\t\tif (props.align === 'justify') {\n\t\t\t\t\tprops.align = 'start'\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tprops.align = 'start-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tprops.align = 'end-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tprops.align = 'middle-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddVerticalAlign,\n\t\t\tup: (props) => {\n\t\t\t\tprops.verticalAlign = 'middle'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MakeUrlsValid,\n\t\t\tup: (props) => {\n\t\t\t\tif (!T.linkUrl.isValid(props.url)) {\n\t\t\t\t\tprops.url = ''\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFontSizeAdjustment,\n\t\t\tup: (props) => {\n\t\t\t\tprops.fontSizeAdjustment = 0\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.fontSizeAdjustment\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddScale,\n\t\t\tup: (props) => {\n\t\t\t\tprops.scale = 1\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.scale\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.labelColor\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichText,\n\t\t\tup: (props) => {\n\t\t\t\tprops.richText = toRichText(props.text)\n\t\t\t\tdelete props.text\n\t\t\t},\n\t\t\t// N.B. Explicitly no down state so that we force clients to update.\n\t\t\t// down: (props) => {\n\t\t\t// \tdelete props.richText\n\t\t\t// },\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichTextAttrs,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop - attrs is optional so old records are valid\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\t// Remove attrs from richText when migrating down\n\t\t\t\tif (props.richText && 'attrs' in props.richText) {\n\t\t\t\t\tdelete props.richText.attrs\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFirstEditedBy,\n\t\t\tup: (props) => {\n\t\t\t\tprops.textFirstEditedBy = null\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.textFirstEditedBy\n\t\t\t},\n\t\t},\n\t],\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
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": "4.6.0-next.
|
|
4
|
+
"version": "4.6.0-next.4dde09fa17ab",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
|
|
42
42
|
"postpack": "../../internal/scripts/postpack.sh",
|
|
43
43
|
"pack-tarball": "yarn pack",
|
|
44
|
-
"lint": "
|
|
44
|
+
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"kleur": "^4.1.5",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"vitest": "^3.2.4"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@tldraw/state": "4.6.0-next.
|
|
54
|
-
"@tldraw/store": "4.6.0-next.
|
|
55
|
-
"@tldraw/utils": "4.6.0-next.
|
|
56
|
-
"@tldraw/validate": "4.6.0-next.
|
|
53
|
+
"@tldraw/state": "4.6.0-next.4dde09fa17ab",
|
|
54
|
+
"@tldraw/store": "4.6.0-next.4dde09fa17ab",
|
|
55
|
+
"@tldraw/utils": "4.6.0-next.4dde09fa17ab",
|
|
56
|
+
"@tldraw/validate": "4.6.0-next.4dde09fa17ab"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"react": "^18.2.0 || ^19.2.1",
|
package/src/TLStore.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { computed } from '@tldraw/state'
|
|
1
2
|
import { Store } from '@tldraw/store'
|
|
2
3
|
import { annotateError, IndexKey, structuredClone } from '@tldraw/utils'
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -330,6 +331,10 @@ describe('createIntegrityChecker', () => {
|
|
|
330
331
|
props: {
|
|
331
332
|
defaultName: 'Test Store',
|
|
332
333
|
assets: mockAssetStore,
|
|
334
|
+
users: {
|
|
335
|
+
currentUser: computed('currentUser', () => null),
|
|
336
|
+
resolve: () => computed('resolve', () => null),
|
|
337
|
+
},
|
|
333
338
|
onMount: vi.fn(),
|
|
334
339
|
},
|
|
335
340
|
})
|
package/src/TLStore.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Signal } from '@tldraw/state'
|
|
1
|
+
import { Signal, computed } from '@tldraw/state'
|
|
2
2
|
import {
|
|
3
3
|
SerializedStore,
|
|
4
4
|
Store,
|
|
@@ -15,6 +15,7 @@ import { PageRecordType, TLPageId } from './records/TLPage'
|
|
|
15
15
|
import { InstancePageStateRecordType, TLInstancePageStateId } from './records/TLPageState'
|
|
16
16
|
import { PointerRecordType, TLPOINTER_ID } from './records/TLPointer'
|
|
17
17
|
import { TLRecord } from './records/TLRecord'
|
|
18
|
+
import { TLUser } from './records/TLUser'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Redacts the source of an asset record for error reporting.
|
|
@@ -200,6 +201,97 @@ export interface TLAssetStore {
|
|
|
200
201
|
remove?(assetIds: TLAssetId[]): Promise<void>
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Interface for resolving user information in tldraw.
|
|
206
|
+
*
|
|
207
|
+
* A `TLUserStore` sits alongside the main {@link TLStore} and provides user
|
|
208
|
+
* resolution for attribution labels and display names. Implement this interface
|
|
209
|
+
* to connect tldraw to your auth/user system.
|
|
210
|
+
*
|
|
211
|
+
* `currentUser` and `resolve` are reactive {@link @tldraw/state#Signal | Signals}
|
|
212
|
+
* so that the editor can automatically track changes to user data and
|
|
213
|
+
* re-render when a user's name, color, or avatar updates.
|
|
214
|
+
*
|
|
215
|
+
* Implementations should cache signals returned by `resolve` — e.g. return the
|
|
216
|
+
* same `Signal` for repeated calls with the same `userId` — to avoid
|
|
217
|
+
* unnecessary re-computation.
|
|
218
|
+
*
|
|
219
|
+
* @public
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* const currentUser = computed('currentUser', () =>
|
|
223
|
+
* UserRecordType.create({
|
|
224
|
+
* id: createUserId(myAuth.userId),
|
|
225
|
+
* name: myAuth.displayName,
|
|
226
|
+
* color: myAuth.color,
|
|
227
|
+
* })
|
|
228
|
+
* )
|
|
229
|
+
*
|
|
230
|
+
* const userStore: TLUserStore = {
|
|
231
|
+
* currentUser,
|
|
232
|
+
* resolve(userId) {
|
|
233
|
+
* return computed('resolve-' + userId, () =>
|
|
234
|
+
* myUserCache.get(userId) ?? null
|
|
235
|
+
* )
|
|
236
|
+
* },
|
|
237
|
+
* }
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export interface TLUserStore {
|
|
241
|
+
/**
|
|
242
|
+
* A signal resolving to the currently authenticated user,
|
|
243
|
+
* or `null` for anonymous / unknown.
|
|
244
|
+
* Read when stamping attribution on shape create/update.
|
|
245
|
+
*/
|
|
246
|
+
currentUser: Signal<TLUser | null>
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Return a signal resolving an arbitrary user ID to display info.
|
|
250
|
+
* Called when rendering attribution labels for shapes that may have been
|
|
251
|
+
* created or edited by someone else.
|
|
252
|
+
* The signal's value should be `null` if the user cannot be resolved.
|
|
253
|
+
*/
|
|
254
|
+
resolve?(userId: string): Signal<TLUser | null>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a cached {@link TLUserStore.resolve} implementation.
|
|
259
|
+
*
|
|
260
|
+
* Wraps a reactive lookup function so that each `userId` gets a single
|
|
261
|
+
* stable {@link @tldraw/state#Signal | Signal} that is reused across calls.
|
|
262
|
+
* The `resolveFn` is evaluated inside a `computed`, so any `.get()` calls
|
|
263
|
+
* it makes are automatically tracked.
|
|
264
|
+
*
|
|
265
|
+
* @param resolveFn - A function that resolves a raw user-ID string to a
|
|
266
|
+
* {@link TLUser} or `null`. Called reactively inside a `computed`.
|
|
267
|
+
* @returns A function suitable for use as `TLUserStore.resolve`.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* const users: TLUserStore = {
|
|
272
|
+
* currentUser: currentUserSignal,
|
|
273
|
+
* resolve: createCachedUserResolve(
|
|
274
|
+
* (userId) => usersAtom.get()[createUserId(userId)] ?? null
|
|
275
|
+
* ),
|
|
276
|
+
* }
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* @public
|
|
280
|
+
*/
|
|
281
|
+
export function createCachedUserResolve(
|
|
282
|
+
resolveFn: (userId: string) => TLUser | null
|
|
283
|
+
): (userId: string) => Signal<TLUser | null> {
|
|
284
|
+
const cache = new Map<string, Signal<TLUser | null>>()
|
|
285
|
+
return (userId: string) => {
|
|
286
|
+
let signal = cache.get(userId)
|
|
287
|
+
if (!signal) {
|
|
288
|
+
signal = computed('resolve-user-' + userId, () => resolveFn(userId))
|
|
289
|
+
cache.set(userId, signal)
|
|
290
|
+
}
|
|
291
|
+
return signal
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
203
295
|
/**
|
|
204
296
|
* Configuration properties for a tldraw store, defining its behavior and integrations.
|
|
205
297
|
* These props are passed when creating a new store instance.
|
|
@@ -228,6 +320,8 @@ export interface TLStoreProps {
|
|
|
228
320
|
defaultName: string
|
|
229
321
|
/** Asset store implementation for handling file uploads and storage */
|
|
230
322
|
assets: Required<TLAssetStore>
|
|
323
|
+
/** User store implementation for user resolution and attribution */
|
|
324
|
+
users: Required<TLUserStore>
|
|
231
325
|
/**
|
|
232
326
|
* Called when an {@link @tldraw/editor#Editor} connected to this store is mounted.
|
|
233
327
|
* Can optionally return a cleanup function that will be called when unmounted.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { atom } from '@tldraw/state'
|
|
1
|
+
import { atom, computed } from '@tldraw/state'
|
|
2
2
|
import { Store } from '@tldraw/store'
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
import {
|
|
5
5
|
createPresenceStateDerivation,
|
|
6
6
|
getDefaultUserPresence,
|
|
7
|
-
TLPresenceUserInfo,
|
|
8
7
|
} from './createPresenceStateDerivation'
|
|
9
8
|
import { createTLSchema } from './createTLSchema'
|
|
10
9
|
import { TLINSTANCE_ID } from './records/TLInstance'
|
|
11
10
|
import { TLPageId } from './records/TLPage'
|
|
12
11
|
import { InstancePageStateRecordType } from './records/TLPageState'
|
|
13
12
|
import { TLRecord } from './records/TLRecord'
|
|
13
|
+
import { createUserId, TLUser, UserRecordType } from './records/TLUser'
|
|
14
14
|
import { createIntegrityChecker, TLStoreProps } from './TLStore'
|
|
15
15
|
|
|
16
16
|
describe('createPresenceStateDerivation', () => {
|
|
17
17
|
let store: Store<TLRecord, TLStoreProps>
|
|
18
|
-
let userSignal: ReturnType<typeof atom<
|
|
18
|
+
let userSignal: ReturnType<typeof atom<TLUser>>
|
|
19
19
|
|
|
20
20
|
beforeEach(() => {
|
|
21
21
|
const schema = createTLSchema()
|
|
@@ -28,6 +28,10 @@ describe('createPresenceStateDerivation', () => {
|
|
|
28
28
|
resolve: vi.fn().mockResolvedValue('resolved-url'),
|
|
29
29
|
remove: vi.fn().mockResolvedValue(undefined),
|
|
30
30
|
},
|
|
31
|
+
users: {
|
|
32
|
+
currentUser: computed('currentUser', () => null),
|
|
33
|
+
resolve: () => computed('resolve', () => null),
|
|
34
|
+
},
|
|
31
35
|
onMount: vi.fn(),
|
|
32
36
|
},
|
|
33
37
|
})
|
|
@@ -36,11 +40,14 @@ describe('createPresenceStateDerivation', () => {
|
|
|
36
40
|
const checker = createIntegrityChecker(store)
|
|
37
41
|
checker()
|
|
38
42
|
|
|
39
|
-
userSignal = atom(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
userSignal = atom(
|
|
44
|
+
'user',
|
|
45
|
+
UserRecordType.create({
|
|
46
|
+
id: createUserId('user123'),
|
|
47
|
+
name: 'Test User',
|
|
48
|
+
color: '#007AFF',
|
|
49
|
+
})
|
|
50
|
+
)
|
|
44
51
|
})
|
|
45
52
|
|
|
46
53
|
afterEach(() => {
|
|
@@ -53,16 +60,18 @@ describe('createPresenceStateDerivation', () => {
|
|
|
53
60
|
const presenceSignal = derivation(store)
|
|
54
61
|
const presence = presenceSignal.get()
|
|
55
62
|
|
|
56
|
-
expect(presence!.userId).toBe('user123')
|
|
63
|
+
expect(presence!.userId).toBe('user:user123')
|
|
57
64
|
expect(presence!.userName).toBe('Test User')
|
|
58
65
|
expect(presence!.color).toBe('#007AFF')
|
|
59
66
|
|
|
60
67
|
// Update user signal
|
|
61
|
-
userSignal.set(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
userSignal.set(
|
|
69
|
+
UserRecordType.create({
|
|
70
|
+
id: createUserId('user456'),
|
|
71
|
+
name: 'Updated User',
|
|
72
|
+
color: '#FF6B6B',
|
|
73
|
+
})
|
|
74
|
+
)
|
|
66
75
|
|
|
67
76
|
const updatedPresence = presenceSignal.get()
|
|
68
77
|
expect(updatedPresence!.userName).toBe('Updated User')
|
|
@@ -111,6 +120,10 @@ describe('getDefaultUserPresence', () => {
|
|
|
111
120
|
resolve: vi.fn().mockResolvedValue('resolved-url'),
|
|
112
121
|
remove: vi.fn().mockResolvedValue(undefined),
|
|
113
122
|
},
|
|
123
|
+
users: {
|
|
124
|
+
currentUser: computed('currentUser', () => null),
|
|
125
|
+
resolve: () => computed('resolve', () => null),
|
|
126
|
+
},
|
|
114
127
|
onMount: vi.fn(),
|
|
115
128
|
},
|
|
116
129
|
})
|
|
@@ -126,21 +139,21 @@ describe('getDefaultUserPresence', () => {
|
|
|
126
139
|
|
|
127
140
|
describe('basic functionality', () => {
|
|
128
141
|
it('should return presence state with user info and default values', () => {
|
|
129
|
-
const user
|
|
130
|
-
id: 'test-user',
|
|
142
|
+
const user = UserRecordType.create({
|
|
143
|
+
id: createUserId('test-user'),
|
|
131
144
|
name: 'Test User',
|
|
132
145
|
color: '#00FF00',
|
|
133
|
-
}
|
|
146
|
+
})
|
|
134
147
|
|
|
135
148
|
const presence = getDefaultUserPresence(store, user)
|
|
136
149
|
|
|
137
|
-
expect(presence!.userId).toBe('test-user')
|
|
150
|
+
expect(presence!.userId).toBe('user:test-user')
|
|
138
151
|
expect(presence!.userName).toBe('Test User')
|
|
139
152
|
expect(presence!.color).toBe('#00FF00')
|
|
140
153
|
})
|
|
141
154
|
|
|
142
155
|
it('should use defaults for missing user fields', () => {
|
|
143
|
-
const minimalUser
|
|
156
|
+
const minimalUser = UserRecordType.create({ id: createUserId('minimal') })
|
|
144
157
|
const presence = getDefaultUserPresence(store, minimalUser)
|
|
145
158
|
|
|
146
159
|
expect(presence!.userName).toBe('')
|
|
@@ -151,7 +164,7 @@ describe('getDefaultUserPresence', () => {
|
|
|
151
164
|
describe('error conditions', () => {
|
|
152
165
|
it('should return null when required records are missing', () => {
|
|
153
166
|
store.remove([TLINSTANCE_ID])
|
|
154
|
-
const user
|
|
167
|
+
const user = UserRecordType.create({ id: createUserId('test') })
|
|
155
168
|
expect(getDefaultUserPresence(store, user)).toBe(null)
|
|
156
169
|
})
|
|
157
170
|
})
|
|
@@ -4,25 +4,18 @@ import { TLINSTANCE_ID } from './records/TLInstance'
|
|
|
4
4
|
import { InstancePageStateRecordType } from './records/TLPageState'
|
|
5
5
|
import { TLPOINTER_ID } from './records/TLPointer'
|
|
6
6
|
import { InstancePresenceRecordType, TLInstancePresence } from './records/TLPresence'
|
|
7
|
+
import { TLUser } from './records/TLUser'
|
|
7
8
|
import { TLStore } from './TLStore'
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export interface TLPresenceUserInfo {
|
|
14
|
-
/**
|
|
15
|
-
* id - A unique identifier for the user. This should be the same across all devices and sessions.
|
|
16
|
-
*/
|
|
17
|
-
id: string
|
|
18
|
-
/**
|
|
19
|
-
* The user's display name.
|
|
20
|
-
*/
|
|
21
|
-
name?: string | null
|
|
10
|
+
/** @public */
|
|
11
|
+
export interface CreatePresenceStateDerivationOpts {
|
|
12
|
+
/** Custom instance ID. If not provided, one is generated from the store ID. */
|
|
13
|
+
instanceId?: TLInstancePresence['id']
|
|
22
14
|
/**
|
|
23
|
-
*
|
|
15
|
+
* Override how presence state is built from the store and current user.
|
|
16
|
+
* Defaults to {@link getDefaultUserPresence}.
|
|
24
17
|
*/
|
|
25
|
-
|
|
18
|
+
getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
/**
|
|
@@ -33,8 +26,8 @@ export interface TLPresenceUserInfo {
|
|
|
33
26
|
* position, selected shapes, camera position, and user metadata that gets synchronized in
|
|
34
27
|
* multiplayer scenarios.
|
|
35
28
|
*
|
|
36
|
-
* @param $user - A reactive signal containing the user information
|
|
37
|
-
* @param
|
|
29
|
+
* @param $user - A reactive signal containing the user information, or `null` when anonymous
|
|
30
|
+
* @param opts - Optional configuration for instance ID and presence derivation
|
|
38
31
|
* @returns A function that takes a store and returns a computed signal of the user's presence state
|
|
39
32
|
*
|
|
40
33
|
* @example
|
|
@@ -42,7 +35,7 @@ export interface TLPresenceUserInfo {
|
|
|
42
35
|
* import { createPresenceStateDerivation } from '@tldraw/tlschema'
|
|
43
36
|
* import { atom } from '@tldraw/state'
|
|
44
37
|
*
|
|
45
|
-
* const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })
|
|
38
|
+
* const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} })
|
|
46
39
|
* const presenceDerivation = createPresenceStateDerivation(userSignal)
|
|
47
40
|
*
|
|
48
41
|
* // Use with a store to get reactive presence state
|
|
@@ -53,15 +46,17 @@ export interface TLPresenceUserInfo {
|
|
|
53
46
|
* @public
|
|
54
47
|
*/
|
|
55
48
|
export function createPresenceStateDerivation(
|
|
56
|
-
$user: Signal<
|
|
57
|
-
|
|
49
|
+
$user: Signal<TLUser | null>,
|
|
50
|
+
opts?: CreatePresenceStateDerivationOpts
|
|
58
51
|
) {
|
|
52
|
+
const { instanceId, getUserPresence: _getUserPresence } = opts ?? {}
|
|
53
|
+
const getUserPresence = _getUserPresence ?? getDefaultUserPresence
|
|
59
54
|
return (store: TLStore): Signal<TLInstancePresence | null> => {
|
|
60
55
|
return computed('instancePresence', () => {
|
|
61
56
|
const user = $user.get()
|
|
62
57
|
if (!user) return null
|
|
63
58
|
|
|
64
|
-
const state =
|
|
59
|
+
const state = getUserPresence(store, user)
|
|
65
60
|
if (!state) return null
|
|
66
61
|
|
|
67
62
|
return InstancePresenceRecordType.create({
|
|
@@ -98,7 +93,7 @@ export type TLPresenceStateInfo = Parameters<(typeof InstancePresenceRecordType)
|
|
|
98
93
|
* ```ts
|
|
99
94
|
* import { getDefaultUserPresence } from '@tldraw/tlschema'
|
|
100
95
|
*
|
|
101
|
-
* const user = { id: 'user-123', name: 'Alice', color: '#ff0000' }
|
|
96
|
+
* const user = { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} }
|
|
102
97
|
* const presenceInfo = getDefaultUserPresence(store, user)
|
|
103
98
|
*
|
|
104
99
|
* if (presenceInfo) {
|
|
@@ -122,7 +117,7 @@ export type TLPresenceStateInfo = Parameters<(typeof InstancePresenceRecordType)
|
|
|
122
117
|
*
|
|
123
118
|
* @public
|
|
124
119
|
*/
|
|
125
|
-
export function getDefaultUserPresence(store: TLStore, user:
|
|
120
|
+
export function getDefaultUserPresence(store: TLStore, user: TLUser) {
|
|
126
121
|
const instance = store.get(TLINSTANCE_ID)
|
|
127
122
|
const pageState = store.get(InstancePageStateRecordType.createId(instance?.currentPageId))
|
|
128
123
|
const camera = store.get(CameraRecordType.createId(instance?.currentPageId))
|
|
@@ -136,14 +131,14 @@ export function getDefaultUserPresence(store: TLStore, user: TLPresenceUserInfo)
|
|
|
136
131
|
brush: instance.brush,
|
|
137
132
|
scribbles: instance.scribbles,
|
|
138
133
|
userId: user.id,
|
|
139
|
-
userName: user.name
|
|
134
|
+
userName: user.name,
|
|
140
135
|
followingUserId: instance.followingUserId,
|
|
141
136
|
camera: {
|
|
142
137
|
x: camera.x,
|
|
143
138
|
y: camera.y,
|
|
144
139
|
z: camera.z,
|
|
145
140
|
},
|
|
146
|
-
color: user.color
|
|
141
|
+
color: user.color || '#FF0000',
|
|
147
142
|
currentPageId: instance.currentPageId,
|
|
148
143
|
cursor: {
|
|
149
144
|
x: pointer.x,
|
package/src/createTLSchema.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LegacyMigrations, MigrationSequence, StoreSchema, StoreValidator } from '@tldraw/store'
|
|
2
2
|
import { objectMapValues } from '@tldraw/utils'
|
|
3
|
+
import { T } from '@tldraw/validate'
|
|
3
4
|
import { bookmarkAssetMigrations } from './assets/TLBookmarkAsset'
|
|
4
5
|
import { imageAssetMigrations } from './assets/TLImageAsset'
|
|
5
6
|
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
getShapePropKeysByStyle,
|
|
27
28
|
rootShapeMigrations,
|
|
28
29
|
} from './records/TLShape'
|
|
30
|
+
import { UserRecordType, createUserRecordType, userMigrations } from './records/TLUser'
|
|
29
31
|
import { RecordProps, TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
|
|
30
32
|
import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
|
|
31
33
|
import { TLBaseShape } from './shapes/TLBaseShape'
|
|
@@ -193,6 +195,38 @@ export const defaultBindingSchemas = {
|
|
|
193
195
|
arrow: { migrations: arrowBindingMigrations, props: arrowBindingProps },
|
|
194
196
|
} satisfies { [T in TLDefaultBinding['type']]: SchemaPropsInfo }
|
|
195
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Configuration for extending the user record type with custom metadata
|
|
200
|
+
* validators and migration sequences.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { T } from '@tldraw/validate'
|
|
205
|
+
*
|
|
206
|
+
* const userSchema: UserSchemaInfo = {
|
|
207
|
+
* meta: {
|
|
208
|
+
* isAdmin: T.boolean,
|
|
209
|
+
* department: T.string,
|
|
210
|
+
* },
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @public
|
|
215
|
+
*/
|
|
216
|
+
export interface UserSchemaInfo {
|
|
217
|
+
/**
|
|
218
|
+
* Validators for custom metadata fields on user records. Each field is
|
|
219
|
+
* treated as optional — user records without these fields remain valid,
|
|
220
|
+
* but when present, values are validated against the provided validators.
|
|
221
|
+
*/
|
|
222
|
+
meta?: Record<string, T.Validatable<any>>
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Additional migration sequences for evolving custom user data over time.
|
|
226
|
+
*/
|
|
227
|
+
migrations?: readonly MigrationSequence[]
|
|
228
|
+
}
|
|
229
|
+
|
|
196
230
|
/**
|
|
197
231
|
* Creates a complete TLSchema for use with tldraw stores. This schema defines the structure,
|
|
198
232
|
* validation, and migration sequences for all record types in a tldraw application.
|
|
@@ -204,6 +238,7 @@ export const defaultBindingSchemas = {
|
|
|
204
238
|
* @param options - Configuration options for the schema
|
|
205
239
|
* - shapes - Shape schema configurations. Defaults to defaultShapeSchemas if not provided
|
|
206
240
|
* - bindings - Binding schema configurations. Defaults to defaultBindingSchemas if not provided
|
|
241
|
+
* - user - Custom user record configuration with meta validators and migrations
|
|
207
242
|
* - records - Custom record type configurations. These are additional record types beyond
|
|
208
243
|
* the built-in shapes, bindings, assets, etc.
|
|
209
244
|
* - migrations - Additional migration sequences to include in the schema
|
|
@@ -229,6 +264,16 @@ export const defaultBindingSchemas = {
|
|
|
229
264
|
* },
|
|
230
265
|
* })
|
|
231
266
|
*
|
|
267
|
+
* // Create schema with custom user metadata
|
|
268
|
+
* const schemaWithCustomUser = createTLSchema({
|
|
269
|
+
* user: {
|
|
270
|
+
* meta: {
|
|
271
|
+
* isAdmin: T.boolean,
|
|
272
|
+
* department: T.string,
|
|
273
|
+
* },
|
|
274
|
+
* },
|
|
275
|
+
* })
|
|
276
|
+
*
|
|
232
277
|
* // Create schema with custom record types
|
|
233
278
|
* const schemaWithCustomRecords = createTLSchema({
|
|
234
279
|
* records: {
|
|
@@ -256,11 +301,13 @@ export const defaultBindingSchemas = {
|
|
|
256
301
|
export function createTLSchema({
|
|
257
302
|
shapes = defaultShapeSchemas,
|
|
258
303
|
bindings = defaultBindingSchemas,
|
|
304
|
+
user,
|
|
259
305
|
records = {},
|
|
260
306
|
migrations,
|
|
261
307
|
}: {
|
|
262
308
|
shapes?: Record<string, SchemaPropsInfo>
|
|
263
309
|
bindings?: Record<string, SchemaPropsInfo>
|
|
310
|
+
user?: UserSchemaInfo
|
|
264
311
|
records?: Record<string, CustomRecordInfo>
|
|
265
312
|
migrations?: readonly MigrationSequence[]
|
|
266
313
|
} = {}): TLSchema {
|
|
@@ -277,6 +324,7 @@ export function createTLSchema({
|
|
|
277
324
|
const ShapeRecordType = createShapeRecordType(shapes)
|
|
278
325
|
const BindingRecordType = createBindingRecordType(bindings)
|
|
279
326
|
const InstanceRecordType = createInstanceRecordType(stylesById)
|
|
327
|
+
const CustomUserRecordType = user ? createUserRecordType(user) : UserRecordType
|
|
280
328
|
|
|
281
329
|
// Create RecordTypes for custom records
|
|
282
330
|
const builtInTypeNames = new Set([
|
|
@@ -291,6 +339,7 @@ export function createTLSchema({
|
|
|
291
339
|
'pointer',
|
|
292
340
|
'shape',
|
|
293
341
|
'store',
|
|
342
|
+
'user',
|
|
294
343
|
])
|
|
295
344
|
const customRecordTypes: Record<string, { createId: any }> = {}
|
|
296
345
|
for (const [typeName, config] of Object.entries(records)) {
|
|
@@ -314,6 +363,7 @@ export function createTLSchema({
|
|
|
314
363
|
instance_presence: InstancePresenceRecordType,
|
|
315
364
|
pointer: PointerRecordType,
|
|
316
365
|
shape: ShapeRecordType,
|
|
366
|
+
user: CustomUserRecordType,
|
|
317
367
|
...customRecordTypes,
|
|
318
368
|
},
|
|
319
369
|
{
|
|
@@ -329,6 +379,7 @@ export function createTLSchema({
|
|
|
329
379
|
pointerMigrations,
|
|
330
380
|
rootShapeMigrations,
|
|
331
381
|
|
|
382
|
+
userMigrations,
|
|
332
383
|
bookmarkAssetMigrations,
|
|
333
384
|
imageAssetMigrations,
|
|
334
385
|
videoAssetMigrations,
|
|
@@ -337,6 +388,7 @@ export function createTLSchema({
|
|
|
337
388
|
...processPropsMigrations<TLBinding>('binding', bindings),
|
|
338
389
|
...processCustomRecordMigrations(records),
|
|
339
390
|
|
|
391
|
+
...(user?.migrations ?? []),
|
|
340
392
|
...(migrations ?? []),
|
|
341
393
|
],
|
|
342
394
|
onValidationFailure,
|
package/src/index.ts
CHANGED
|
@@ -51,8 +51,8 @@ export {
|
|
|
51
51
|
export {
|
|
52
52
|
createPresenceStateDerivation,
|
|
53
53
|
getDefaultUserPresence,
|
|
54
|
+
type CreatePresenceStateDerivationOpts,
|
|
54
55
|
type TLPresenceStateInfo,
|
|
55
|
-
type TLPresenceUserInfo,
|
|
56
56
|
} from './createPresenceStateDerivation'
|
|
57
57
|
export {
|
|
58
58
|
createTLSchema,
|
|
@@ -60,6 +60,7 @@ export {
|
|
|
60
60
|
defaultShapeSchemas,
|
|
61
61
|
type SchemaPropsInfo,
|
|
62
62
|
type TLSchema,
|
|
63
|
+
type UserSchemaInfo,
|
|
63
64
|
} from './createTLSchema'
|
|
64
65
|
export {
|
|
65
66
|
boxModelValidator,
|
|
@@ -173,6 +174,15 @@ export {
|
|
|
173
174
|
type TLShapePartial,
|
|
174
175
|
type TLUnknownShape,
|
|
175
176
|
} from './records/TLShape'
|
|
177
|
+
export {
|
|
178
|
+
createUserId,
|
|
179
|
+
createUserRecordType,
|
|
180
|
+
isUserId,
|
|
181
|
+
userIdValidator,
|
|
182
|
+
UserRecordType,
|
|
183
|
+
type TLUser,
|
|
184
|
+
type TLUserId,
|
|
185
|
+
} from './records/TLUser'
|
|
176
186
|
export {
|
|
177
187
|
type RecordProps,
|
|
178
188
|
type RecordPropsType,
|
|
@@ -308,6 +318,7 @@ export {
|
|
|
308
318
|
type TLDefaultVerticalAlignStyle,
|
|
309
319
|
} from './styles/TLVerticalAlignStyle'
|
|
310
320
|
export {
|
|
321
|
+
createCachedUserResolve,
|
|
311
322
|
type TLAssetContext,
|
|
312
323
|
type TLAssetStore,
|
|
313
324
|
type TLSerializedStore,
|
|
@@ -315,6 +326,7 @@ export {
|
|
|
315
326
|
type TLStoreProps,
|
|
316
327
|
type TLStoreSchema,
|
|
317
328
|
type TLStoreSnapshot,
|
|
329
|
+
type TLUserStore,
|
|
318
330
|
} from './TLStore'
|
|
319
331
|
export {
|
|
320
332
|
getDefaultTranslationLocale,
|