From 854d00c31b7105290396fe34294fc2a0331ea469 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:26:14 +0200 Subject: [PATCH] feat(editor): track element creation timestamps (#12060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(editor): element creation timestamp tracking Co-authored-by: Márk Tolmács --- AGENTS.md | 1 + dev-docs/docs/codebase/json-schema.mdx | 12 + .../__snapshots__/transform.test.ts.snap | 62 +++++ .../element/src/__tests__/transform.test.ts | 93 +++++++ packages/element/src/convertToShape.ts | 4 + packages/element/src/delta.ts | 5 +- packages/element/src/duplicate.ts | 1 + packages/element/src/mutateElement.ts | 5 +- packages/element/src/newElement.ts | 7 +- packages/element/src/transform.ts | 45 +++- packages/element/src/types.ts | 5 + packages/element/tests/duplicate.test.tsx | 25 +- packages/element/tests/newElement.test.ts | 52 ++++ packages/excalidraw/data/blob.ts | 11 +- packages/excalidraw/data/restore.ts | 1 + .../__snapshots__/contextmenu.test.tsx.snap | 52 ++++ .../__snapshots__/dragCreate.test.tsx.snap | 5 + .../tests/__snapshots__/export.test.tsx.snap | 2 +- .../tests/__snapshots__/history.test.tsx.snap | 249 ++++++++++++++++++ .../tests/__snapshots__/move.test.tsx.snap | 6 + .../multiPointCreate.test.tsx.snap | 2 + .../regressionTests.test.tsx.snap | 103 ++++++++ .../__snapshots__/selection.test.tsx.snap | 5 + packages/excalidraw/tests/created.test.tsx | 162 ++++++++++++ .../data/__snapshots__/restore.test.ts.snap | 10 + .../excalidraw/tests/data/restore.test.ts | 92 +++++++ packages/excalidraw/tests/drawShape.test.tsx | 8 +- .../tests/fixtures/elementFixture.ts | 1 + packages/excalidraw/tests/helpers/api.ts | 3 + packages/excalidraw/tests/lasso.test.tsx | 3 + .../scene/__snapshots__/export.test.ts.snap | 2 +- packages/excalidraw/types.ts | 3 +- packages/utils/src/export.ts | 10 +- packages/utils/tests/export.test.ts | 85 ++++++ 34 files changed, 1101 insertions(+), 31 deletions(-) create mode 100644 packages/element/tests/newElement.test.ts create mode 100644 packages/excalidraw/tests/created.test.tsx diff --git a/AGENTS.md b/AGENTS.md index 29096e3674..c1c96592b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,4 @@ # Guidelines - For new DOM/browser API usage, use `app.ownerDocument` and `app.ownerWindow` instead of globals; without `app`, derive them from the mounted node's `ownerDocument` and its `defaultView`. +- When overriding properties of an existing type, prefer `Merge` from `@excalidraw/common/utility-types` over `Omit & Overrides`. diff --git a/dev-docs/docs/codebase/json-schema.mdx b/dev-docs/docs/codebase/json-schema.mdx index e4302dc4e0..7eb22a015e 100644 --- a/dev-docs/docs/codebase/json-schema.mdx +++ b/dev-docs/docs/codebase/json-schema.mdx @@ -62,6 +62,18 @@ When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) } ``` +### Element creation time + +Every normalized element has a `created: number | null` field. A number is the client's wall-clock time in epoch milliseconds when this element instance was created. `null` means the creation time is unknown; restoring an older file does not substitute its load time or last-update time. + +| Operation | `created` | +| --- | --- | +| Construct a new element | Current time, unless explicitly supplied (including `null`) | +| Duplicate, paste, or insert from a library | New time for the new instance | +| Convert element skeletons with ID regeneration | New time for each resulting instance | +| Restore/open/import an existing scene | Preserve the carried value; missing values become `null` | +| Edit, change type while retaining the ID, capture, undo, or redo | Preserve the value | + ## Excalidraw clipboard format When copying selected excalidraw elements to clipboard, the JSON schema is similar to `.excalidraw` format, except it differs in attributes. diff --git a/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap b/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap index ad93aeadfb..a61cd0fc44 100644 --- a/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap +++ b/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap @@ -14,6 +14,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -51,6 +52,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -83,6 +85,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -144,6 +147,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -210,6 +214,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -249,6 +254,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t }, ], "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -296,6 +302,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t }, ], "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -341,6 +348,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -404,6 +412,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "boundElements": null, "containerId": "id50", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -449,6 +458,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -512,6 +522,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "backgroundColor": "transparent", "boundElements": null, "containerId": "id39", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -557,6 +568,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -594,6 +606,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -631,6 +644,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -694,6 +708,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "backgroundColor": "transparent", "boundElements": null, "containerId": "id43", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -741,6 +756,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when }, ], "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -788,6 +804,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when }, ], "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -828,6 +845,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -860,6 +878,7 @@ exports[`Test Transform > should transform linear elements 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -907,6 +926,7 @@ exports[`Test Transform > should transform linear elements 2`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "triangle", @@ -954,6 +974,7 @@ exports[`Test Transform > should transform linear elements 3`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "endArrowhead": null, "endBinding": null, @@ -1001,6 +1022,7 @@ exports[`Test Transform > should transform linear elements 4`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "endArrowhead": null, "endBinding": null, @@ -1048,6 +1070,7 @@ exports[`Test Transform > should transform regular shapes 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1080,6 +1103,7 @@ exports[`Test Transform > should transform regular shapes 2`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1112,6 +1136,7 @@ exports[`Test Transform > should transform regular shapes 3`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1144,6 +1169,7 @@ exports[`Test Transform > should transform regular shapes 4`] = ` "angle": 0, "backgroundColor": "#c0eb75", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1176,6 +1202,7 @@ exports[`Test Transform > should transform regular shapes 5`] = ` "angle": 0, "backgroundColor": "#ffc9c9", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1208,6 +1235,7 @@ exports[`Test Transform > should transform regular shapes 6`] = ` "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, @@ -1242,6 +1270,7 @@ exports[`Test Transform > should transform text element 1`] = ` "backgroundColor": "transparent", "boundElements": null, "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1284,6 +1313,7 @@ exports[`Test Transform > should transform text element 2`] = ` "backgroundColor": "transparent", "boundElements": null, "containerId": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1333,6 +1363,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1372,6 +1403,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1415,6 +1447,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1462,6 +1495,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "arrow", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1501,6 +1535,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -1569,6 +1604,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -1630,6 +1666,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "B", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1674,6 +1711,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "A", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1718,6 +1756,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "Alice", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1762,6 +1801,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "Bob", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1806,6 +1846,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "Bob_Alice", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1848,6 +1889,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": null, "containerId": "Bob_B", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -1893,6 +1935,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -1945,6 +1988,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -1997,6 +2041,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -2049,6 +2094,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -2098,6 +2144,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id25", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2140,6 +2187,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id26", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2182,6 +2230,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id27", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2225,6 +2274,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id28", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2271,6 +2321,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2308,6 +2359,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2345,6 +2397,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2382,6 +2435,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2419,6 +2473,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2456,6 +2511,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2490,6 +2546,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id13", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2532,6 +2589,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id14", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2575,6 +2633,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id15", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2620,6 +2679,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id16", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2663,6 +2723,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id17", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2707,6 +2768,7 @@ exports[`Test Transform > should transform to text containers when label provide "backgroundColor": "transparent", "boundElements": null, "containerId": "id18", + "created": 1, "customData": undefined, "fillStyle": "solid", "fontFamily": 5, diff --git a/packages/element/src/__tests__/transform.test.ts b/packages/element/src/__tests__/transform.test.ts index d47354c425..a0cb99dd2a 100644 --- a/packages/element/src/__tests__/transform.test.ts +++ b/packages/element/src/__tests__/transform.test.ts @@ -1,6 +1,15 @@ import { pointFrom } from "@excalidraw/math"; import { vi } from "vitest"; +import { getUpdatedTimestamp } from "@excalidraw/common"; + +import { + newElement, + newEmbeddableElement, + newFreeDrawElement, + newIframeElement, +} from "../newElement"; + import { convertToExcalidrawElements, type ExcalidrawElementSkeleton, @@ -965,4 +974,88 @@ describe("Test Transform", () => { }); }); }); + + describe("creation timestamps", () => { + const createElements = () => [ + { + ...newElement({ type: "rectangle", x: 0, y: 0 }), + type: "rectangle" as const, + }, + newFreeDrawElement({ + type: "freedraw", + x: 0, + y: 0, + simulatePressure: false, + }), + newIframeElement({ type: "iframe", x: 0, y: 0 }), + newEmbeddableElement({ type: "embeddable", x: 0, y: 0 }), + ]; + + it("assigns fresh timestamps when regenerating IDs without mutating the input", () => { + const elements = createElements().map((element) => ({ + ...element, + created: 123, + })); + const converted = convertToExcalidrawElements(elements); + + expect(converted).toHaveLength(elements.length); + converted.forEach((element, index) => { + expect(element.id).not.toBe(elements[index].id); + expect(element.created).toBe(getUpdatedTimestamp()); + expect(elements[index].created).toBe(123); + }); + }); + + it.each([123, 0, null])( + "preserves an explicit timestamp of %s when retaining IDs", + (created) => { + const elements = createElements().map((element) => ({ + ...element, + created, + })); + const converted = convertToExcalidrawElements(elements, opts); + + converted.forEach((element, index) => { + expect(element.id).toBe(elements[index].id); + expect(element.created).toBe(created); + }); + }, + ); + + it("defaults missing timestamps when retaining IDs, as every type goes through a constructor", () => { + // models skeletons persisted before `created` existed; the declared + // skeleton type is a complete element + const elements = createElements().map( + ({ created, ...element }) => element, + ) as unknown as ExcalidrawElementSkeleton[]; + const converted = convertToExcalidrawElements(elements, opts); + + converted.forEach((element, index) => { + expect(element.id).toBe(elements[index].id); + expect(element.created).toBe(getUpdatedTimestamp()); + expect(elements[index]).not.toHaveProperty("created"); + }); + }); + + it("assigns fresh timestamps to generated labels and binding endpoints", () => { + const elements: ExcalidrawElementSkeleton[] = [ + { + type: "arrow", + x: 100, + y: 100, + created: 123, + // fragments cannot carry a creation time; they are always new + label: { text: "label" }, + start: { type: "rectangle" }, + end: { type: "text", text: "end" }, + }, + ]; + const converted = convertToExcalidrawElements(elements); + + expect(converted).toHaveLength(4); + expect(converted.map((element) => element.created)).toEqual( + Array(4).fill(getUpdatedTimestamp()), + ); + }); + }); }); diff --git a/packages/element/src/convertToShape.ts b/packages/element/src/convertToShape.ts index f44a530d74..38ca1e83de 100644 --- a/packages/element/src/convertToShape.ts +++ b/packages/element/src/convertToShape.ts @@ -797,6 +797,10 @@ export const convertToShapeHandlePointerMoveFromPointerDown = ( prevPreview?.type === shapePreview.type ? prevPreview.id : shapePreview.id, + created: + prevPreview?.type === shapePreview.type + ? prevPreview.created + : shapePreview.created, seed: 1, opacity: ELEMENT_PENDING_DRAW_SHAPE_OPACITY, isDeleted: false as const, diff --git a/packages/element/src/delta.ts b/packages/element/src/delta.ts index 1f26da07c8..c1c0f737f4 100644 --- a/packages/element/src/delta.ts +++ b/packages/element/src/delta.ts @@ -1688,8 +1688,9 @@ export class ElementsDelta implements DeltaContainer { } if (!flags.containsVisibleDifference) { - // strip away fractional index, as even if it would be different, it doesn't have to result in visible change - const { index, ...rest } = directlyApplicablePartial; + // Creation metadata is not visible; a different fractional index does + // not necessarily change the visible order either. + const { index, created, ...rest } = directlyApplicablePartial; const containsVisibleDifference = ElementsDelta.checkForVisibleDifference( element, rest, diff --git a/packages/element/src/duplicate.ts b/packages/element/src/duplicate.ts index 7058aba035..affe3916ac 100644 --- a/packages/element/src/duplicate.ts +++ b/packages/element/src/duplicate.ts @@ -75,6 +75,7 @@ export const duplicateElement = ( copy.id = randomId(); copy.updated = getUpdatedTimestamp(); + copy.created = copy.updated; if (randomizeSeed) { copy.seed = randomInteger(); bumpVersion(copy); diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 62a1670fa5..3efc3bff16 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -21,9 +21,12 @@ import type { NonDeletedSceneElementsMap, } from "./types"; +// `created` is lifetime metadata: it is set once when an element instance is +// constructed (or duplicated) and never updated, so it is not part of an +// element update. An explicit repair must construct a new element object. export type ElementUpdate = Omit< Partial, - "id" | "updated" + "id" | "updated" | "created" >; /** diff --git a/packages/element/src/newElement.ts b/packages/element/src/newElement.ts index 76c1b8b323..2d5ec8e0ba 100644 --- a/packages/element/src/newElement.ts +++ b/packages/element/src/newElement.ts @@ -74,6 +74,7 @@ export type ElementConstructorOpts = MarkOptional< | "locked" | "opacity" | "customData" + | "created" >; const _newElementBase = ( @@ -123,6 +124,8 @@ const _newElementBase = ( }); } + const timestamp = getUpdatedTimestamp(); + // assign type to guard against excess properties const element: Merge< ExcalidrawGenericElement, @@ -151,7 +154,9 @@ const _newElementBase = ( versionNonce: rest.versionNonce ?? 0, isDeleted: false as false, boundElements, - updated: getUpdatedTimestamp(), + updated: timestamp, + // Preserve explicit null when reconstructing a legacy element with its id. + created: rest.created === undefined ? timestamp : rest.created, link, locked, customData: rest.customData, diff --git a/packages/element/src/transform.ts b/packages/element/src/transform.ts index 21287abaa9..8b0b46aac9 100644 --- a/packages/element/src/transform.ts +++ b/packages/element/src/transform.ts @@ -11,6 +11,7 @@ import { assertNever, cloneJSON, getFontString, + getUpdatedTimestamp, isDevEnv, toBrandedType, getLineHeight, @@ -22,7 +23,10 @@ import { bindBindingElement } from "./binding"; import { newArrowElement, newElement, + newEmbeddableElement, newFrameElement, + newFreeDrawElement, + newIframeElement, newImageElement, newLinearElement, newMagicFrameElement, @@ -68,6 +72,16 @@ import type { VerticalAlign, } from "./types"; +/** + * Options for elements generated from a skeleton fragment (bound labels and + * binding endpoints). Such elements are always constructed fresh, hence they + * never carry a creation time of their own. + */ +type FragmentConstructorOpts = MarkOptional< + Omit, + "x" | "y" +>; + export type ValidLinearElement = { type: "arrow" | "line"; x: number; @@ -78,7 +92,7 @@ export type ValidLinearElement = { fontFamily?: FontFamilyValues; textAlign?: TextAlign; verticalAlign?: VerticalAlign; - } & MarkOptional; + } & FragmentConstructorOpts; end?: | ( | ( @@ -118,9 +132,9 @@ export type ValidLinearElement = { text: string; } ) & - Partial) + Partial>) ) & - MarkOptional; + FragmentConstructorOpts; start?: | ( | ( @@ -160,9 +174,9 @@ export type ValidLinearElement = { text: string; } ) & - Partial) + Partial>) ) & - MarkOptional; + FragmentConstructorOpts; } & Partial; export type ValidContainer = @@ -175,7 +189,7 @@ export type ValidContainer = fontFamily?: FontFamilyValues; textAlign?: TextAlign; verticalAlign?: VerticalAlign; - } & MarkOptional; + } & FragmentConstructorOpts; } & ElementConstructorOpts; export type ExcalidrawElementSkeleton = @@ -223,7 +237,7 @@ const DEFAULT_DIMENSION = 100; const bindTextToContainer = ( container: ExcalidrawElement, - textProps: { text: string } & MarkOptional, + textProps: { text: string } & FragmentConstructorOpts, scene: Scene, ) => { const textElement: ExcalidrawTextElement = newTextElement({ @@ -531,11 +545,14 @@ export const convertToExcalidrawElements = ( const oldToNewElementIdMap = new Map(); // Create individual elements + // regenerated ids mean new instances, hence a fresh creation time as well + const created = getUpdatedTimestamp(); + for (const element of elements) { let excalidrawElement: ExcalidrawElement; const originalId = element.id; if (opts?.regenerateIds !== false) { - Object.assign(element, { id: randomId() }); + Object.assign(element, { id: randomId(), created }); } switch (element.type) { @@ -634,10 +651,16 @@ export const convertToExcalidrawElements = ( }); break; } - case "freedraw": - case "iframe": + case "freedraw": { + excalidrawElement = newFreeDrawElement({ ...element }); + break; + } + case "iframe": { + excalidrawElement = newIframeElement({ ...element }); + break; + } case "embeddable": { - excalidrawElement = element; + excalidrawElement = newEmbeddableElement({ ...element }); break; } diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index f91b2c6ea1..aff6f52708 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -76,6 +76,11 @@ type _ExcalidrawElementBase = Readonly<{ boundElements: readonly BoundElement[] | null; /** epoch (ms) timestamp of last element update */ updated: number; + /** Client wall-clock creation time in epoch milliseconds; null if unknown. + Preserved for this element's lifetime, including edits and undo/redo, + and excluded from `ElementUpdate` (mutateElement / newElementWith). + Duplicating an element starts a new lifetime. Not an ordering clock. */ + created: number | null; link: string | null; locked: boolean; customData?: Record; diff --git a/packages/element/tests/duplicate.test.tsx b/packages/element/tests/duplicate.test.tsx index b8c5bede27..abe8caa8b6 100644 --- a/packages/element/tests/duplicate.test.tsx +++ b/packages/element/tests/duplicate.test.tsx @@ -5,6 +5,7 @@ import { ORIG_ID, ROUNDNESS, isPrimitive, + getUpdatedTimestamp, } from "@excalidraw/common"; import { Excalidraw, mutateElement } from "@excalidraw/excalidraw"; @@ -24,7 +25,11 @@ import { import type { LocalPoint } from "@excalidraw/math"; -import { duplicateElement, duplicateElements } from "../src/duplicate"; +import { + deepCopyElement, + duplicateElement, + duplicateElements, +} from "../src/duplicate"; import type { ExcalidrawLinearElement } from "../src/types"; @@ -43,6 +48,24 @@ const assertCloneObjects = (source: any, clone: any) => { }; describe("duplicating single elements", () => { + it.each([123, 0, null])( + "preserves created=%s on a deep copy and resets it for a new instance", + (created) => { + const element = API.createElement({ type: "rectangle", created }); + const cloned = deepCopyElement(element); + const duplicate = duplicateElement(null, new Map(), element); + + expect(cloned).toEqual(element); + expect(duplicate).toMatchObject({ + created: getUpdatedTimestamp(), + version: element.version, + versionNonce: element.versionNonce, + }); + expect(duplicate.id).not.toBe(element.id); + expect(element.created).toBe(created); + }, + ); + it("clones arrow element", () => { const element = API.createElement({ type: "arrow", diff --git a/packages/element/tests/newElement.test.ts b/packages/element/tests/newElement.test.ts new file mode 100644 index 0000000000..89d32f40a5 --- /dev/null +++ b/packages/element/tests/newElement.test.ts @@ -0,0 +1,52 @@ +import * as common from "@excalidraw/common"; + +import { API } from "@excalidraw/excalidraw/tests/helpers/api"; + +import { newElement, newLinearElement } from "../src/newElement"; +import { mutateElement, newElementWith } from "../src/mutateElement"; + +describe("element creation time", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.each([ + "rectangle", + "diamond", + "ellipse", + "text", + "line", + "arrow", + "freedraw", + "image", + "frame", + "magicframe", + "embeddable", + "iframe", + ] as const)("initializes a new %s before any capture", (type) => { + vi.spyOn(common, "getUpdatedTimestamp").mockReturnValue(123); + const element = API.createElement({ type }); + expect(element.created).toBe(123); + + vi.mocked(common.getUpdatedTimestamp).mockReturnValue(456); + const edited = newElementWith(element, { x: 50 }); + mutateElement(edited, new Map(), { y: 50 }); + + expect(edited).toMatchObject({ created: 123, updated: 456 }); + }); + + it.each([123, 0, null])( + "preserves explicit created=%s when reconstructing the same element", + (created) => { + const original = newElement({ + type: "rectangle", + x: 0, + y: 0, + created, + }); + const converted = newLinearElement({ ...original, type: "line" }); + + expect(converted).toMatchObject({ id: original.id, created }); + }, + ); +}); diff --git a/packages/excalidraw/data/blob.ts b/packages/excalidraw/data/blob.ts index 05fb47b7bb..3234965960 100644 --- a/packages/excalidraw/data/blob.ts +++ b/packages/excalidraw/data/blob.ts @@ -158,20 +158,21 @@ export const loadSceneOrLibraryFromBlob = async ( throw error; } if (isValidExcalidrawData(data)) { + const elements = restoreElements(data.elements, localElements, { + repairBindings: true, + deleteInvisibleElements: true, + }); return { type: MIME_TYPES.excalidraw, data: { - elements: restoreElements(data.elements, localElements, { - repairBindings: true, - deleteInvisibleElements: true, - }), + elements, appState: restoreAppState( { theme: localAppState?.theme, fileHandle: fileHandle || blob.handle || null, ...cleanAppStateForExport(data.appState || {}), ...(localAppState - ? getScrollToContentState(data.elements || [], localAppState) + ? getScrollToContentState(elements, localAppState) : {}), }, localAppState, diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index d3d570fce9..b6932a4632 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -477,6 +477,7 @@ const restoreElementWithProperties = < ? element.boundElementIds.map((id) => ({ type: "arrow", id })) : element.boundElements ?? [], updated: element.updated ?? getUpdatedTimestamp(), + created: element.created ?? null, link: element.link ? normalizeLink(element.link) : null, locked: element.locked ?? false, }; diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index f81b29f599..05f0af708a 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -1008,6 +1008,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "angle": 0, "backgroundColor": "red", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1042,6 +1043,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "angle": 0, "backgroundColor": "red", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1209,6 +1211,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1265,6 +1268,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1427,6 +1431,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1459,6 +1464,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1515,6 +1521,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1569,6 +1576,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1764,6 +1772,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1796,6 +1805,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1852,6 +1862,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -1906,6 +1917,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2103,6 +2115,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2159,6 +2172,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2319,6 +2333,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2375,6 +2390,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2568,6 +2584,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2600,6 +2617,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2656,6 +2674,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2710,6 +2729,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2877,6 +2897,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2911,6 +2932,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -2969,6 +2991,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3023,6 +3046,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3252,6 +3276,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, @@ -3284,6 +3309,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, @@ -3340,6 +3366,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3394,6 +3421,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3776,6 +3804,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3808,6 +3837,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3864,6 +3894,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -3918,6 +3949,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4105,6 +4137,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4137,6 +4170,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4193,6 +4227,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4247,6 +4282,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4437,6 +4473,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4469,6 +4506,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4525,6 +4563,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -4579,6 +4618,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -5728,6 +5768,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -5760,6 +5801,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -5816,6 +5858,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -5870,6 +5913,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -6953,6 +6997,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -6987,6 +7032,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -7045,6 +7091,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -7099,6 +7146,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -9917,6 +9965,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -9949,6 +9998,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "red", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -9981,6 +10031,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "red", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -10043,6 +10094,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] un "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, diff --git a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap index a538500c25..a082842101 100644 --- a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -7,6 +7,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "elbowed": false, "endArrowhead": "arrow", @@ -58,6 +59,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -92,6 +94,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, @@ -124,6 +127,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "endArrowhead": null, "endBinding": null, @@ -173,6 +177,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "created": 1, "customData": undefined, "fillStyle": "solid", "frameId": null, diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index 663311b6e1..b56ff0ef37 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`export > export svg-embedded scene > svg-embdedded scene export output 1`] = ` -"eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTwY7TMFx1MDAxML3zXHUwMDE1UfaK2LRcYlx1MDAwZb0tsCv2sFxiUSRcdTAwMGWIg1x1MDAxYk+TUV3bsifblqpcdTAwMTKfwY1f5Fx1MDAxM1x1MDAxODshrtPdVorkNzN+b96Mjy+KoqSDhXJRlLCvhULpxK58XHUwMDE58EdwXHUwMDFljebQPJ696VxcXHUwMDFkM1tcIru4vlaGXHUwMDBiWuNp8bqqqr5cYlx1MDAxNGxBk+e073wuimP8clx1MDAwNGUovYlpXHUwMDEx+M9LsKeE7lx1MDAxOarG0yE77VBSy8js7VxitYBNSzkmdKMgK/TkzFx1MDAwNt5cdTAwMWJlXFxgvJpB+CfSlag3jTOdlmNcdTAwMGU5ob1cdTAwMTWOm0l5a1RqSYd4O/vBbpVcdTAwMTOOb4PE+Vx1MDAwNH+uikmbVoNcdTAwMGaGzUbUWFEjheZnVepcIii09zJ6+yNpcmJcdTAwMGL3wVxc3Sk1wqgl7KdgbHFgy1x1MDAwMlx1MDAxZUBmXG7S8C+wT0bXub3oP/DcKV6xXHUwMDE2ykMyNlx1MDAxMN6mnchIOytcdTAwMDVNeFx1MDAxNerNNI/3bPPE3XFv2NG/f37/Olx1MDAxYpHRtMSfQeC8ytA7sUVcdTAwMTU8fZNdcaOwXHR9llxu1mej5mZcdHm/xzBcdTAwMTmbojXfJ1CDuzTeOGxQXHUwMDBi9fVJeaIj81x1MDAwNXwvkFxcXHUwMDA3553Dx3GbX82TSiVWoD5cdTAwMWKP1E8k0MXYib9xXHUwMDExSmHtkthMjvZcdTAwMGaOl1x1MDAwNeXEh1x1MDAxZSOwyYNcYj1cdTAwMThcdLdarNTU5PJcdTAwMTFh9+7ybVxcreNv6GqYz0OnXGKXvFx1MDAwN3WQXHUwMDE5hn1cZvpOUVx1MDAxZj9cdTAwMWNcdTAwMTig0z+sMjD+In0=