mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
feat: expose StoreAction
in relation to multiplayer history (#7898)
Improved Store API and improved handling of actions to eliminate potential concurrency issues
This commit is contained in:
parent
530617be90
commit
015b46ab23
@ -26,7 +26,8 @@ import {
|
|||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
TTDDialog,
|
TTDDialog,
|
||||||
TTDDialogTrigger,
|
TTDDialogTrigger,
|
||||||
} from "../packages/excalidraw/index";
|
StoreAction,
|
||||||
|
} from "../packages/excalidraw";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
@ -438,7 +439,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...data.scene,
|
...data.scene,
|
||||||
...restore(data.scene, null, null, { repairBindings: true }),
|
...restore(data.scene, null, null, { repairBindings: true }),
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -469,6 +470,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
setLangCode(langCode);
|
setLangCode(langCode);
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...localDataState,
|
...localDataState,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
LibraryIndexedDBAdapter.load().then((data) => {
|
LibraryIndexedDBAdapter.load().then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -604,6 +606,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (didChange) {
|
if (didChange) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,11 @@ import {
|
|||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import {
|
||||||
|
StoreAction,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
zoomToFitBounds,
|
zoomToFitBounds,
|
||||||
} from "../../packages/excalidraw/index";
|
} from "../../packages/excalidraw";
|
||||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
assertNever,
|
assertNever,
|
||||||
@ -356,6 +357,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
|
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -506,6 +508,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
// to database even if deleted before creating the room.
|
// to database even if deleted before creating the room.
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
||||||
@ -743,6 +746,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
) => {
|
) => {
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loadImageFiles();
|
this.loadImageFiles();
|
||||||
|
@ -19,6 +19,7 @@ import throttle from "lodash.throttle";
|
|||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
|
import { StoreAction } from "../../packages/excalidraw";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: TCollabClass;
|
collab: TCollabClass;
|
||||||
@ -127,6 +128,7 @@ class Portal {
|
|||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}),
|
}),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
}, FILE_UPLOAD_TIMEOUT);
|
}, FILE_UPLOAD_TIMEOUT);
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { StoreAction } from "../../packages/excalidraw";
|
||||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
import { compressData } from "../../packages/excalidraw/data/encode";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
@ -238,5 +239,6 @@ export const updateStaleImageStatuses = (params: {
|
|||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}),
|
}),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -269,7 +269,6 @@ export const loadScene = async (
|
|||||||
// in the scene database/localStorage, and instead fetch them async
|
// in the scene database/localStorage, and instead fetch them async
|
||||||
// from a different database
|
// from a different database
|
||||||
files: data.files,
|
files: data.files,
|
||||||
commitToStore: false,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
createRedoAction,
|
createRedoAction,
|
||||||
createUndoAction,
|
createUndoAction,
|
||||||
} from "../../packages/excalidraw/actions/actionHistory";
|
} from "../../packages/excalidraw/actions/actionHistory";
|
||||||
import { newElementWith } from "../../packages/excalidraw";
|
import { StoreAction, newElementWith } from "../../packages/excalidraw";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ describe("collaboration", () => {
|
|||||||
|
|
||||||
updateSceneData({
|
updateSceneData({
|
||||||
elements: syncInvalidIndices([rect1, rect2]),
|
elements: syncInvalidIndices([rect1, rect2]),
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSceneData({
|
updateSceneData({
|
||||||
@ -98,7 +98,7 @@ describe("collaboration", () => {
|
|||||||
rect1,
|
rect1,
|
||||||
newElementWith(h.elements[1], { isDeleted: true }),
|
newElementWith(h.elements[1], { isDeleted: true }),
|
||||||
]),
|
]),
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -145,6 +145,7 @@ describe("collaboration", () => {
|
|||||||
// simulate force deleting the element remotely
|
// simulate force deleting the element remotely
|
||||||
updateSceneData({
|
updateSceneData({
|
||||||
elements: syncInvalidIndices([rect1]),
|
elements: syncInvalidIndices([rect1]),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -182,7 +183,7 @@ describe("collaboration", () => {
|
|||||||
h.elements[0],
|
h.elements[0],
|
||||||
newElementWith(h.elements[1], { x: 100 }),
|
newElementWith(h.elements[1], { x: 100 }),
|
||||||
]),
|
]),
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -217,6 +218,7 @@ describe("collaboration", () => {
|
|||||||
// simulate force deleting the element remotely
|
// simulate force deleting the element remotely
|
||||||
updateSceneData({
|
updateSceneData({
|
||||||
elements: syncInvalidIndices([rect1]),
|
elements: syncInvalidIndices([rect1]),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// snapshot was correctly updated and marked the element as deleted
|
// snapshot was correctly updated and marked the element as deleted
|
||||||
|
@ -35,9 +35,13 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
- Renamed required `updatedScene` parameter from `commitToHistory` into `commitToStore` [#7348](https://github.com/excalidraw/excalidraw/pull/7348).
|
- `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898)
|
||||||
|
|
||||||
### Breaking Changes
|
| | Before `commitToHistory` | After `storeAction` | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| _Immediately undoable_ | `true` | `"capture"` | As before, use for all updates which should be recorded by the store & history. Should be used for the most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
|
||||||
|
| _Eventually undoable_ | `false` | `"none"` | Similar to before, use for all updates which should not be recorded immediately (likely exceptions which are part of some async multi-step process) or those not meant to be recorded at all (i.e. updates to `collaborators` object, parts of `AppState` which are not observed by the store & history - not `ObservedAppState`).<br/><br/>**IMPORTANT** It's likely you should switch to `"update"` in all the other cases. Otherwise, all such updates would end up being recorded with the next `"capture"` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
|
||||||
|
| _Never undoable_ | n/a | `"update"` | **NEW**: previously there was no equivalent for this value. Now, it's recommended to use `"update"` for all remote updates (from the other clients), scene initialization, or those updates, which should not be locally "undoable". These updates will _never_ make it to the local undo / redo stacks. |
|
||||||
|
|
||||||
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { KEYS } from "../keys";
|
|||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { isWindows } from "../constants";
|
import { isWindows } from "../constants";
|
||||||
import { SceneElementsMap } from "../element/types";
|
import { SceneElementsMap } from "../element/types";
|
||||||
import { IStore, StoreAction } from "../store";
|
import { Store, StoreAction } from "../store";
|
||||||
import { useEmitter } from "../hooks/useEmitter";
|
import { useEmitter } from "../hooks/useEmitter";
|
||||||
|
|
||||||
const writeData = (
|
const writeData = (
|
||||||
@ -40,7 +40,7 @@ const writeData = (
|
|||||||
return { storeAction: StoreAction.NONE };
|
return { storeAction: StoreAction.NONE };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActionCreator = (history: History, store: IStore) => Action;
|
type ActionCreator = (history: History, store: Store) => Action;
|
||||||
|
|
||||||
export const createUndoAction: ActionCreator = (history, store) => ({
|
export const createUndoAction: ActionCreator = (history, store) => ({
|
||||||
name: "undo",
|
name: "undo",
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
UIAppState,
|
UIAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import { MarkOptional } from "../utility-types";
|
||||||
import { StoreAction } from "../store";
|
import { StoreActionType } from "../store";
|
||||||
|
|
||||||
export type ActionSource =
|
export type ActionSource =
|
||||||
| "ui"
|
| "ui"
|
||||||
@ -26,7 +26,7 @@ export type ActionResult =
|
|||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> | null;
|
> | null;
|
||||||
files?: BinaryFiles | null;
|
files?: BinaryFiles | null;
|
||||||
storeAction: keyof typeof StoreAction;
|
storeAction: StoreActionType;
|
||||||
replaceFiles?: boolean;
|
replaceFiles?: boolean;
|
||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
@ -183,7 +183,6 @@ import {
|
|||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
Ordered,
|
Ordered,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@ -412,7 +411,7 @@ import { ElementCanvasButton } from "./MagicButton";
|
|||||||
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||||
import FollowMode from "./FollowMode/FollowMode";
|
import FollowMode from "./FollowMode/FollowMode";
|
||||||
import { IStore, Store, StoreAction } from "../store";
|
import { Store, StoreAction } from "../store";
|
||||||
import { AnimationFrameHandler } from "../animation-frame-handler";
|
import { AnimationFrameHandler } from "../animation-frame-handler";
|
||||||
import { AnimatedTrail } from "../animated-trail";
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
import { LaserTrails } from "../laser-trails";
|
import { LaserTrails } from "../laser-trails";
|
||||||
@ -543,7 +542,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
public libraryItemsFromStorage: LibraryItems | undefined;
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
||||||
public id: string;
|
public id: string;
|
||||||
private store: IStore;
|
private store: Store;
|
||||||
private history: History;
|
private history: History;
|
||||||
private excalidrawContainerValue: {
|
private excalidrawContainerValue: {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
@ -2123,7 +2122,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
}),
|
}),
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2810,7 +2809,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.store.capture(elementsMap, this.state);
|
this.store.commit(elementsMap, this.state);
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
// potential issues, this fixes a case where the tab isn't focused during
|
// potential issues, this fixes a case where the tab isn't focused during
|
||||||
@ -3683,51 +3682,39 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements?: SceneData["elements"];
|
elements?: SceneData["elements"];
|
||||||
appState?: Pick<AppState, K> | null;
|
appState?: Pick<AppState, K> | null;
|
||||||
collaborators?: SceneData["collaborators"];
|
collaborators?: SceneData["collaborators"];
|
||||||
commitToStore?: SceneData["commitToStore"];
|
/** @default StoreAction.CAPTURE */
|
||||||
|
storeAction?: SceneData["storeAction"];
|
||||||
}) => {
|
}) => {
|
||||||
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
|
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
|
||||||
|
|
||||||
if (sceneData.commitToStore) {
|
if (sceneData.storeAction && sceneData.storeAction !== StoreAction.NONE) {
|
||||||
this.store.shouldCaptureIncrement();
|
const prevCommittedAppState = this.store.snapshot.appState;
|
||||||
}
|
const prevCommittedElements = this.store.snapshot.elements;
|
||||||
|
|
||||||
if (sceneData.elements || sceneData.appState) {
|
const nextCommittedAppState = sceneData.appState
|
||||||
let nextCommittedAppState = this.state;
|
? Object.assign({}, prevCommittedAppState, sceneData.appState) // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState`
|
||||||
let nextCommittedElements: Map<string, OrderedExcalidrawElement>;
|
: prevCommittedAppState;
|
||||||
|
|
||||||
if (sceneData.appState) {
|
const nextCommittedElements = sceneData.elements
|
||||||
nextCommittedAppState = {
|
? this.store.filterUncomittedElements(
|
||||||
...this.state,
|
this.scene.getElementsMapIncludingDeleted(), // Only used to detect uncomitted local elements
|
||||||
...sceneData.appState, // Here we expect just partial appState
|
arrayToMap(nextElements), // We expect all (already reconciled) elements
|
||||||
};
|
)
|
||||||
}
|
: prevCommittedElements;
|
||||||
|
|
||||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
// WARN: store action always performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
|
||||||
|
// do NOT schedule store actions (execute after re-render), as it might cause unexpected concurrency issues if not handled well
|
||||||
if (sceneData.elements) {
|
if (sceneData.storeAction === StoreAction.CAPTURE) {
|
||||||
/**
|
this.store.captureIncrement(
|
||||||
* We need to schedule a snapshot update, as in case `commitToStore` is false (i.e. remote update),
|
nextCommittedElements,
|
||||||
* as it's essential for computing local changes after the async action is completed (i.e. not to include remote changes in the diff).
|
nextCommittedAppState,
|
||||||
*
|
);
|
||||||
* This is also a breaking change for all local `updateScene` calls without set `commitToStore` to true,
|
} else if (sceneData.storeAction === StoreAction.UPDATE) {
|
||||||
* as it makes such updates impossible to undo (previously they were undone coincidentally with the switch to the whole snapshot captured by the history).
|
this.store.updateSnapshot(
|
||||||
*
|
nextCommittedElements,
|
||||||
* WARN: be careful here as moving it elsewhere could break the history for remote client without noticing
|
nextCommittedAppState,
|
||||||
* - we need to find a way to test two concurrent client updates simultaneously, while having access to both stores & histories.
|
|
||||||
*/
|
|
||||||
this.store.shouldUpdateSnapshot();
|
|
||||||
|
|
||||||
// TODO#7348: deprecate once exchanging just store increments between clients
|
|
||||||
nextCommittedElements = this.store.ignoreUncomittedElements(
|
|
||||||
arrayToMap(prevElements),
|
|
||||||
arrayToMap(nextElements),
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
nextCommittedElements = arrayToMap(prevElements);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WARN: Performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
|
|
||||||
this.store.capture(nextCommittedElements, nextCommittedAppState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sceneData.appState) {
|
if (sceneData.appState) {
|
||||||
@ -5704,6 +5691,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -7993,6 +7981,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: {
|
appState: {
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
},
|
},
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -8165,6 +8154,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: this.scene
|
elements: this.scene
|
||||||
.getElementsIncludingDeleted()
|
.getElementsIncludingDeleted()
|
||||||
.filter((el) => el.id !== resizingElement.id),
|
.filter((el) => el.id !== resizingElement.id),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9228,13 +9218,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ret.type === MIME_TYPES.excalidraw) {
|
if (ret.type === MIME_TYPES.excalidraw) {
|
||||||
// Restore the fractional indices by mutating elements and update the
|
// restore the fractional indices by mutating elements
|
||||||
// store snapshot, otherwise we would end up with duplicate indices
|
|
||||||
syncInvalidIndices(elements.concat(ret.data.elements));
|
syncInvalidIndices(elements.concat(ret.data.elements));
|
||||||
this.store.snapshot = this.store.snapshot.clone(
|
|
||||||
arrayToMap(elements),
|
// update the store snapshot for old elements, otherwise we would end up with duplicated fractional indices on undo
|
||||||
this.state,
|
this.store.updateSnapshot(arrayToMap(elements), this.state);
|
||||||
);
|
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
...ret.data,
|
...ret.data,
|
||||||
|
@ -49,7 +49,7 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
|||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { IStore } from "../store";
|
import { Store } from "../store";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
@ -642,7 +642,7 @@ export class LinearElementEditor {
|
|||||||
static handlePointerDown(
|
static handlePointerDown(
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
store: IStore,
|
store: Store,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
|
@ -251,6 +251,8 @@ export {
|
|||||||
bumpVersion,
|
bumpVersion,
|
||||||
} from "./element/mutateElement";
|
} from "./element/mutateElement";
|
||||||
|
|
||||||
|
export { StoreAction } from "./store";
|
||||||
|
|
||||||
export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
|
export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { getDefaultAppState } from "./appState";
|
import { getDefaultAppState } from "./appState";
|
||||||
import { AppStateChange, ElementsChange } from "./change";
|
import { AppStateChange, ElementsChange } from "./change";
|
||||||
|
import { ENV } from "./constants";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
import { OrderedExcalidrawElement } from "./element/types";
|
import { OrderedExcalidrawElement } from "./element/types";
|
||||||
@ -7,8 +8,11 @@ import { Emitter } from "./emitter";
|
|||||||
import { AppState, ObservedAppState } from "./types";
|
import { AppState, ObservedAppState } from "./types";
|
||||||
import { isShallowEqual } from "./utils";
|
import { isShallowEqual } from "./utils";
|
||||||
|
|
||||||
|
// hidden non-enumerable property for runtime checks
|
||||||
|
const hiddenObservedAppStateProp = "__observedAppState";
|
||||||
|
|
||||||
export const getObservedAppState = (appState: AppState): ObservedAppState => {
|
export const getObservedAppState = (appState: AppState): ObservedAppState => {
|
||||||
return {
|
const observedAppState = {
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
@ -17,14 +21,40 @@ export const getObservedAppState = (appState: AppState): ObservedAppState => {
|
|||||||
editingLinearElementId: appState.editingLinearElement?.elementId || null,
|
editingLinearElementId: appState.editingLinearElement?.elementId || null,
|
||||||
selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
|
selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
||||||
|
value: true,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return observedAppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoreAction = {
|
const isObservedAppState = (
|
||||||
NONE: "NONE",
|
appState: AppState | ObservedAppState,
|
||||||
UPDATE: "UPDATE",
|
): appState is ObservedAppState =>
|
||||||
CAPTURE: "CAPTURE",
|
Object.hasOwn(appState, hiddenObservedAppStateProp);
|
||||||
|
|
||||||
|
export type StoreActionType = "capture" | "update" | "none";
|
||||||
|
|
||||||
|
export const StoreAction: {
|
||||||
|
[K in Uppercase<StoreActionType>]: StoreActionType;
|
||||||
|
} = {
|
||||||
|
CAPTURE: "capture",
|
||||||
|
UPDATE: "update",
|
||||||
|
NONE: "none",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent an increment to the Store.
|
||||||
|
*/
|
||||||
|
class StoreIncrementEvent {
|
||||||
|
constructor(
|
||||||
|
public readonly elementsChange: ElementsChange,
|
||||||
|
public readonly appStateChange: AppStateChange,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store which captures the observed changes and emits them as `StoreIncrementEvent` events.
|
* Store which captures the observed changes and emits them as `StoreIncrementEvent` events.
|
||||||
*
|
*
|
||||||
@ -41,18 +71,18 @@ export interface IStore {
|
|||||||
shouldUpdateSnapshot(): void;
|
shouldUpdateSnapshot(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use to schedule calculation of a store increment on a next component update.
|
* Use to schedule calculation of a store increment.
|
||||||
*/
|
*/
|
||||||
shouldCaptureIncrement(): void;
|
shouldCaptureIncrement(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture changes to the `elements` and `appState` by calculating changes (based on a snapshot) and emitting resulting changes as a store increment.
|
* Based on the scheduled operation, either only updates store snapshot or also calculates increment and emits the result as a `StoreIncrementEvent`.
|
||||||
*
|
*
|
||||||
* @emits StoreIncrementEvent
|
* @emits StoreIncrementEvent when increment is calculated.
|
||||||
*/
|
*/
|
||||||
capture(
|
commit(
|
||||||
elements: Map<string, OrderedExcalidrawElement>,
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
appState: AppState,
|
appState: AppState | ObservedAppState | undefined,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,33 +94,19 @@ export interface IStore {
|
|||||||
* Filters out yet uncomitted elements from `nextElements`, which are part of in-progress local async actions (ephemerals) and thus were not yet commited to the snapshot.
|
* Filters out yet uncomitted elements from `nextElements`, which are part of in-progress local async actions (ephemerals) and thus were not yet commited to the snapshot.
|
||||||
*
|
*
|
||||||
* This is necessary in updates in which we receive reconciled elements, already containing elements which were not yet captured by the local store (i.e. collab).
|
* This is necessary in updates in which we receive reconciled elements, already containing elements which were not yet captured by the local store (i.e. collab).
|
||||||
*
|
|
||||||
* Once we will be exchanging just store increments for all ephemerals, this could be deprecated.
|
|
||||||
*/
|
*/
|
||||||
ignoreUncomittedElements(
|
filterUncomittedElements(
|
||||||
prevElements: Map<string, OrderedExcalidrawElement>,
|
prevElements: Map<string, OrderedExcalidrawElement>,
|
||||||
nextElements: Map<string, OrderedExcalidrawElement>,
|
nextElements: Map<string, OrderedExcalidrawElement>,
|
||||||
): Map<string, OrderedExcalidrawElement>;
|
): Map<string, OrderedExcalidrawElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represent an increment to the Store.
|
|
||||||
*/
|
|
||||||
class StoreIncrementEvent {
|
|
||||||
constructor(
|
|
||||||
public readonly elementsChange: ElementsChange,
|
|
||||||
public readonly appStateChange: AppStateChange,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Store implements IStore {
|
export class Store implements IStore {
|
||||||
public readonly onStoreIncrementEmitter = new Emitter<
|
public readonly onStoreIncrementEmitter = new Emitter<
|
||||||
[StoreIncrementEvent]
|
[StoreIncrementEvent]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
private calculatingIncrement: boolean = false;
|
private scheduledActions: Set<StoreActionType> = new Set();
|
||||||
private updatingSnapshot: boolean = false;
|
|
||||||
|
|
||||||
private _snapshot = Snapshot.empty();
|
private _snapshot = Snapshot.empty();
|
||||||
|
|
||||||
public get snapshot() {
|
public get snapshot() {
|
||||||
@ -101,64 +117,81 @@ export class Store implements IStore {
|
|||||||
this._snapshot = snapshot;
|
this._snapshot = snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldUpdateSnapshot = () => {
|
// TODO: Suspicious that this is called so many places. Seems error-prone.
|
||||||
this.updatingSnapshot = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Suspicious that this is called so many places. Seems error-prone.
|
|
||||||
public shouldCaptureIncrement = () => {
|
public shouldCaptureIncrement = () => {
|
||||||
this.calculatingIncrement = true;
|
this.scheduleAction(StoreAction.CAPTURE);
|
||||||
};
|
};
|
||||||
|
|
||||||
public capture = (
|
public shouldUpdateSnapshot = () => {
|
||||||
elements: Map<string, OrderedExcalidrawElement>,
|
this.scheduleAction(StoreAction.UPDATE);
|
||||||
appState: AppState,
|
};
|
||||||
|
|
||||||
|
private scheduleAction = (action: StoreActionType) => {
|
||||||
|
this.scheduledActions.add(action);
|
||||||
|
this.satisfiesScheduledActionsInvariant();
|
||||||
|
};
|
||||||
|
|
||||||
|
public commit = (
|
||||||
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
|
appState: AppState | ObservedAppState | undefined,
|
||||||
): void => {
|
): void => {
|
||||||
// Quick exit for irrelevant changes
|
|
||||||
if (!this.calculatingIncrement && !this.updatingSnapshot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextSnapshot = this._snapshot.clone(elements, appState);
|
// Capture has precedence since it also performs update
|
||||||
|
if (this.scheduledActions.has(StoreAction.CAPTURE)) {
|
||||||
// Optimisation, don't continue if nothing has changed
|
this.captureIncrement(elements, appState);
|
||||||
if (this._snapshot !== nextSnapshot) {
|
} else if (this.scheduledActions.has(StoreAction.UPDATE)) {
|
||||||
// Calculate and record the changes based on the previous and next snapshot
|
this.updateSnapshot(elements, appState);
|
||||||
if (this.calculatingIncrement) {
|
|
||||||
const elementsChange = nextSnapshot.meta.didElementsChange
|
|
||||||
? ElementsChange.calculate(
|
|
||||||
this._snapshot.elements,
|
|
||||||
nextSnapshot.elements,
|
|
||||||
)
|
|
||||||
: ElementsChange.empty();
|
|
||||||
|
|
||||||
const appStateChange = nextSnapshot.meta.didAppStateChange
|
|
||||||
? AppStateChange.calculate(
|
|
||||||
this._snapshot.appState,
|
|
||||||
nextSnapshot.appState,
|
|
||||||
)
|
|
||||||
: AppStateChange.empty();
|
|
||||||
|
|
||||||
if (!elementsChange.isEmpty() || !appStateChange.isEmpty()) {
|
|
||||||
// Notify listeners with the increment
|
|
||||||
this.onStoreIncrementEmitter.trigger(
|
|
||||||
new StoreIncrementEvent(elementsChange, appStateChange),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the snapshot
|
|
||||||
this._snapshot = nextSnapshot;
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Reset props
|
this.satisfiesScheduledActionsInvariant();
|
||||||
this.updatingSnapshot = false;
|
// Defensively reset all scheduled actions, potentially cleans up other runtime garbage
|
||||||
this.calculatingIncrement = false;
|
this.scheduledActions = new Set();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public ignoreUncomittedElements = (
|
public captureIncrement = (
|
||||||
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
|
appState: AppState | ObservedAppState | undefined,
|
||||||
|
) => {
|
||||||
|
const prevSnapshot = this.snapshot;
|
||||||
|
const nextSnapshot = this.snapshot.maybeClone(elements, appState);
|
||||||
|
|
||||||
|
// Optimisation, don't continue if nothing has changed
|
||||||
|
if (prevSnapshot !== nextSnapshot) {
|
||||||
|
// Calculate and record the changes based on the previous and next snapshot
|
||||||
|
const elementsChange = nextSnapshot.meta.didElementsChange
|
||||||
|
? ElementsChange.calculate(prevSnapshot.elements, nextSnapshot.elements)
|
||||||
|
: ElementsChange.empty();
|
||||||
|
|
||||||
|
const appStateChange = nextSnapshot.meta.didAppStateChange
|
||||||
|
? AppStateChange.calculate(prevSnapshot.appState, nextSnapshot.appState)
|
||||||
|
: AppStateChange.empty();
|
||||||
|
|
||||||
|
if (!elementsChange.isEmpty() || !appStateChange.isEmpty()) {
|
||||||
|
// Notify listeners with the increment
|
||||||
|
this.onStoreIncrementEmitter.trigger(
|
||||||
|
new StoreIncrementEvent(elementsChange, appStateChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update snapshot
|
||||||
|
this.snapshot = nextSnapshot;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public updateSnapshot = (
|
||||||
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
|
appState: AppState | ObservedAppState | undefined,
|
||||||
|
) => {
|
||||||
|
const nextSnapshot = this.snapshot.maybeClone(elements, appState);
|
||||||
|
|
||||||
|
if (this.snapshot !== nextSnapshot) {
|
||||||
|
// Update snapshot
|
||||||
|
this.snapshot = nextSnapshot;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public filterUncomittedElements = (
|
||||||
prevElements: Map<string, OrderedExcalidrawElement>,
|
prevElements: Map<string, OrderedExcalidrawElement>,
|
||||||
nextElements: Map<string, OrderedExcalidrawElement>,
|
nextElements: Map<string, OrderedExcalidrawElement>,
|
||||||
) => {
|
) => {
|
||||||
@ -170,7 +203,7 @@ export class Store implements IStore {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementSnapshot = this._snapshot.elements.get(id);
|
const elementSnapshot = this.snapshot.elements.get(id);
|
||||||
|
|
||||||
// Checks for in progress async user action
|
// Checks for in progress async user action
|
||||||
if (!elementSnapshot) {
|
if (!elementSnapshot) {
|
||||||
@ -186,7 +219,19 @@ export class Store implements IStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public clear = (): void => {
|
public clear = (): void => {
|
||||||
this._snapshot = Snapshot.empty();
|
this.snapshot = Snapshot.empty();
|
||||||
|
this.scheduledActions = new Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
private satisfiesScheduledActionsInvariant = () => {
|
||||||
|
if (!(this.scheduledActions.size >= 0 && this.scheduledActions.size <= 3)) {
|
||||||
|
const message = `There can be at most three store actions scheduled at the same time, but there are "${this.scheduledActions.size}".`;
|
||||||
|
console.error(message, this.scheduledActions.values());
|
||||||
|
|
||||||
|
if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,29 +263,30 @@ export class Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Efficiently clone the existing snapshot.
|
* Efficiently clone the existing snapshot, only if we detected changes.
|
||||||
*
|
*
|
||||||
* @returns same instance if there are no changes detected, new instance otherwise.
|
* @returns same instance if there are no changes detected, new instance otherwise.
|
||||||
*/
|
*/
|
||||||
public clone(
|
public maybeClone(
|
||||||
elements: Map<string, OrderedExcalidrawElement>,
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
appState: AppState,
|
appState: AppState | ObservedAppState | undefined,
|
||||||
) {
|
) {
|
||||||
const didElementsChange = this.detectChangedElements(elements);
|
const nextElementsSnapshot = this.maybeCreateElementsSnapshot(elements);
|
||||||
|
const nextAppStateSnapshot = this.maybeCreateAppStateSnapshot(appState);
|
||||||
|
|
||||||
// Not watching over everything from app state, just the relevant props
|
let didElementsChange = false;
|
||||||
const nextAppStateSnapshot = getObservedAppState(appState);
|
let didAppStateChange = false;
|
||||||
const didAppStateChange = this.detectChangedAppState(nextAppStateSnapshot);
|
|
||||||
|
|
||||||
// Nothing has changed, so there is no point of continuing further
|
if (this.elements !== nextElementsSnapshot) {
|
||||||
if (!didElementsChange && !didAppStateChange) {
|
didElementsChange = true;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone only if there was really a change
|
if (this.appState !== nextAppStateSnapshot) {
|
||||||
let nextElementsSnapshot = this.elements;
|
didAppStateChange = true;
|
||||||
if (didElementsChange) {
|
}
|
||||||
nextElementsSnapshot = this.createElementsSnapshot(elements);
|
|
||||||
|
if (!didElementsChange && !didAppStateChange) {
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = new Snapshot(nextElementsSnapshot, nextAppStateSnapshot, {
|
const snapshot = new Snapshot(nextElementsSnapshot, nextAppStateSnapshot, {
|
||||||
@ -251,10 +297,55 @@ export class Snapshot {
|
|||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private maybeCreateAppStateSnapshot(
|
||||||
|
appState: AppState | ObservedAppState | undefined,
|
||||||
|
) {
|
||||||
|
if (!appState) {
|
||||||
|
return this.appState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not watching over everything from the app state, just the relevant props
|
||||||
|
const nextAppStateSnapshot = !isObservedAppState(appState)
|
||||||
|
? getObservedAppState(appState)
|
||||||
|
: appState;
|
||||||
|
|
||||||
|
const didAppStateChange = this.detectChangedAppState(nextAppStateSnapshot);
|
||||||
|
|
||||||
|
if (!didAppStateChange) {
|
||||||
|
return this.appState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextAppStateSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectChangedAppState(nextObservedAppState: ObservedAppState) {
|
||||||
|
return !isShallowEqual(this.appState, nextObservedAppState, {
|
||||||
|
selectedElementIds: isShallowEqual,
|
||||||
|
selectedGroupIds: isShallowEqual,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private maybeCreateElementsSnapshot(
|
||||||
|
elements: Map<string, OrderedExcalidrawElement> | undefined,
|
||||||
|
) {
|
||||||
|
if (!elements) {
|
||||||
|
return this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const didElementsChange = this.detectChangedElements(elements);
|
||||||
|
|
||||||
|
if (!didElementsChange) {
|
||||||
|
return this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementsSnapshot = this.createElementsSnapshot(elements);
|
||||||
|
return elementsSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if there any changed elements.
|
* Detect if there any changed elements.
|
||||||
*
|
*
|
||||||
* NOTE: we shouldn't use `sceneVersionNonce` instead, as we need to call this before the scene updates.
|
* NOTE: we shouldn't just use `sceneVersionNonce` instead, as we need to call this before the scene updates.
|
||||||
*/
|
*/
|
||||||
private detectChangedElements(
|
private detectChangedElements(
|
||||||
nextElements: Map<string, OrderedExcalidrawElement>,
|
nextElements: Map<string, OrderedExcalidrawElement>,
|
||||||
@ -286,13 +377,6 @@ export class Snapshot {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private detectChangedAppState(observedAppState: ObservedAppState) {
|
|
||||||
return !isShallowEqual(this.appState, observedAppState, {
|
|
||||||
selectedElementIds: isShallowEqual,
|
|
||||||
selectedGroupIds: isShallowEqual,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform structural clone, cloning only elements that changed.
|
* Perform structural clone, cloning only elements that changed.
|
||||||
*/
|
*/
|
||||||
|
@ -35,7 +35,7 @@ import { vi } from "vitest";
|
|||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
import { HistoryEntry } from "../history";
|
import { HistoryEntry } from "../history";
|
||||||
import { AppStateChange, ElementsChange } from "../change";
|
import { AppStateChange, ElementsChange } from "../change";
|
||||||
import { Snapshot } from "../store";
|
import { Snapshot, StoreAction } from "../store";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ describe("history", () => {
|
|||||||
|
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, rect2],
|
elements: [rect1, rect2],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
@ -188,7 +188,7 @@ describe("history", () => {
|
|||||||
|
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, rect2],
|
elements: [rect1, rect2],
|
||||||
commitToStore: true, // even though the flag is on, same elements are passed, nothing to commit
|
storeAction: StoreAction.CAPTURE, // even though the flag is on, same elements are passed, nothing to commit
|
||||||
});
|
});
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
@ -556,7 +556,7 @@ describe("history", () => {
|
|||||||
appState: {
|
appState: {
|
||||||
name: "New name",
|
name: "New name",
|
||||||
},
|
},
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
@ -567,7 +567,7 @@ describe("history", () => {
|
|||||||
appState: {
|
appState: {
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
},
|
},
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
@ -580,7 +580,7 @@ describe("history", () => {
|
|||||||
name: "New name",
|
name: "New name",
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
},
|
},
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
@ -1235,7 +1235,7 @@ describe("history", () => {
|
|||||||
|
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, text, rect2],
|
elements: [rect1, text, rect2],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// bind text1 to rect1
|
// bind text1 to rect1
|
||||||
@ -1638,6 +1638,7 @@ describe("history", () => {
|
|||||||
<Excalidraw
|
<Excalidraw
|
||||||
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||||
handleKeyboardGlobally={true}
|
handleKeyboardGlobally={true}
|
||||||
|
isCollaborating={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
excalidrawAPI = await excalidrawAPIPromise;
|
excalidrawAPI = await excalidrawAPIPromise;
|
||||||
@ -1663,6 +1664,7 @@ describe("history", () => {
|
|||||||
strokeColor: blue,
|
strokeColor: blue,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -1700,6 +1702,7 @@ describe("history", () => {
|
|||||||
strokeColor: yellow,
|
strokeColor: yellow,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -1747,6 +1750,7 @@ describe("history", () => {
|
|||||||
backgroundColor: yellow,
|
backgroundColor: yellow,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// At this point our entry gets updated from `red` -> `blue` into `red` -> `yellow`
|
// At this point our entry gets updated from `red` -> `blue` into `red` -> `yellow`
|
||||||
@ -1762,6 +1766,7 @@ describe("history", () => {
|
|||||||
backgroundColor: violet,
|
backgroundColor: violet,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// At this point our (inversed) entry gets updated from `red` -> `yellow` into `violet` -> `yellow`
|
// At this point our (inversed) entry gets updated from `red` -> `yellow` into `violet` -> `yellow`
|
||||||
@ -1790,6 +1795,7 @@ describe("history", () => {
|
|||||||
// Initialize scene
|
// Initialize scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, rect2],
|
elements: [rect1, rect2],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -1798,7 +1804,7 @@ describe("history", () => {
|
|||||||
newElementWith(h.elements[0], { groupIds: ["A"] }),
|
newElementWith(h.elements[0], { groupIds: ["A"] }),
|
||||||
newElementWith(h.elements[1], { groupIds: ["A"] }),
|
newElementWith(h.elements[1], { groupIds: ["A"] }),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rect3 = API.createElement({ type: "rectangle", groupIds: ["B"] });
|
const rect3 = API.createElement({ type: "rectangle", groupIds: ["B"] });
|
||||||
@ -1812,6 +1818,7 @@ describe("history", () => {
|
|||||||
rect3,
|
rect3,
|
||||||
rect4,
|
rect4,
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -1857,6 +1864,7 @@ describe("history", () => {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo(); // undo `actionFinalize`
|
Keyboard.undo(); // undo `actionFinalize`
|
||||||
@ -1951,6 +1959,7 @@ describe("history", () => {
|
|||||||
isDeleted: false, // undeletion might happen due to concurrency between clients
|
isDeleted: false, // undeletion might happen due to concurrency between clients
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getSelectedElements()).toEqual([]);
|
expect(API.getSelectedElements()).toEqual([]);
|
||||||
@ -2027,6 +2036,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
@ -2088,6 +2098,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2163,6 +2174,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2201,6 +2213,7 @@ describe("history", () => {
|
|||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
@ -2246,6 +2259,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, rect2],
|
elements: [rect1, rect2],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
@ -2255,6 +2269,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
@ -2275,6 +2290,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2299,6 +2315,7 @@ describe("history", () => {
|
|||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
@ -2309,6 +2326,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
@ -2354,6 +2372,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2374,6 +2393,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
h.elements[1],
|
h.elements[1],
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2416,6 +2436,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2458,6 +2479,7 @@ describe("history", () => {
|
|||||||
h.elements[0],
|
h.elements[0],
|
||||||
h.elements[1],
|
h.elements[1],
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
@ -2496,6 +2518,7 @@ describe("history", () => {
|
|||||||
h.elements[0],
|
h.elements[0],
|
||||||
h.elements[1],
|
h.elements[1],
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
@ -2546,6 +2569,7 @@ describe("history", () => {
|
|||||||
h.elements[0], // rect2
|
h.elements[0], // rect2
|
||||||
h.elements[1], // rect1
|
h.elements[1], // rect1
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2575,6 +2599,7 @@ describe("history", () => {
|
|||||||
h.elements[0], // rect3
|
h.elements[0], // rect3
|
||||||
h.elements[2], // rect1
|
h.elements[2], // rect1
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2604,6 +2629,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [...h.elements, rect],
|
elements: [...h.elements, rect],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
mouse.moveTo(60, 60);
|
mouse.moveTo(60, 60);
|
||||||
@ -2655,6 +2681,7 @@ describe("history", () => {
|
|||||||
// // Simulate remote update
|
// // Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [...h.elements, rect3],
|
elements: [...h.elements, rect3],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -2744,6 +2771,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [...h.elements, rect3],
|
elements: [...h.elements, rect3],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -2920,6 +2948,7 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container, text],
|
elements: [container, text],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -2932,7 +2961,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -2963,6 +2992,7 @@ describe("history", () => {
|
|||||||
x: h.elements[1].x + 10,
|
x: h.elements[1].x + 10,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3005,6 +3035,7 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container, text],
|
elements: [container, text],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -3017,7 +3048,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3051,6 +3082,7 @@ describe("history", () => {
|
|||||||
remoteText,
|
remoteText,
|
||||||
h.elements[1],
|
h.elements[1],
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3106,6 +3138,7 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container, text],
|
elements: [container, text],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -3118,7 +3151,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3155,6 +3188,7 @@ describe("history", () => {
|
|||||||
containerId: remoteContainer.id,
|
containerId: remoteContainer.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3212,7 +3246,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container],
|
elements: [container],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3223,6 +3257,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
newElementWith(text, { containerId: container.id }),
|
newElementWith(text, { containerId: container.id }),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3272,7 +3307,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [text],
|
elements: [text],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3283,6 +3318,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
newElementWith(text, { containerId: container.id }),
|
newElementWith(text, { containerId: container.id }),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3331,7 +3367,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container],
|
elements: [container],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3344,6 +3380,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3380,6 +3417,7 @@ describe("history", () => {
|
|||||||
// rebinding the container with a new text element!
|
// rebinding the container with a new text element!
|
||||||
remoteText,
|
remoteText,
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3436,7 +3474,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [text],
|
elements: [text],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3449,6 +3487,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3485,6 +3524,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3540,7 +3580,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container],
|
elements: [container],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3554,6 +3594,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3596,7 +3637,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [text],
|
elements: [text],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
@ -3610,6 +3651,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -3652,6 +3694,7 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [container],
|
elements: [container],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -3663,7 +3706,7 @@ describe("history", () => {
|
|||||||
angle: 90,
|
angle: 90,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3676,6 +3719,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
newElementWith(text, { containerId: container.id }),
|
newElementWith(text, { containerId: container.id }),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
@ -3768,6 +3812,7 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [text],
|
elements: [text],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -3779,7 +3824,7 @@ describe("history", () => {
|
|||||||
angle: 90,
|
angle: 90,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -3794,6 +3839,7 @@ describe("history", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(0);
|
expect(API.getUndoStack().length).toBe(0);
|
||||||
@ -3884,7 +3930,7 @@ describe("history", () => {
|
|||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect1, rect2],
|
elements: [rect1, rect2],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
@ -3962,6 +4008,7 @@ describe("history", () => {
|
|||||||
x: h.elements[1].x + 50,
|
x: h.elements[1].x + 50,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -4082,6 +4129,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
remoteContainer,
|
remoteContainer,
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -4166,6 +4214,7 @@ describe("history", () => {
|
|||||||
boundElements: [{ id: arrow.id, type: "arrow" }],
|
boundElements: [{ id: arrow.id, type: "arrow" }],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -4230,7 +4279,10 @@ describe("history", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({ elements: [arrow], commitToStore: true });
|
excalidrawAPI.updateScene({
|
||||||
|
elements: [arrow],
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
});
|
||||||
|
|
||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
@ -4246,6 +4298,7 @@ describe("history", () => {
|
|||||||
boundElements: [{ id: arrow.id, type: "arrow" }],
|
boundElements: [{ id: arrow.id, type: "arrow" }],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
runTwice(() => {
|
runTwice(() => {
|
||||||
@ -4357,6 +4410,7 @@ describe("history", () => {
|
|||||||
newElementWith(h.elements[1], { x: 500, y: -500 }),
|
newElementWith(h.elements[1], { x: 500, y: -500 }),
|
||||||
h.elements[2],
|
h.elements[2],
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
@ -4424,12 +4478,13 @@ describe("history", () => {
|
|||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [frame],
|
elements: [frame],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: [rect, h.elements[0]],
|
elements: [rect, h.elements[0]],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate local update
|
// Simulate local update
|
||||||
@ -4440,7 +4495,7 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
h.elements[1],
|
h.elements[1],
|
||||||
],
|
],
|
||||||
commitToStore: true,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
@ -4484,6 +4539,7 @@ describe("history", () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { Excalidraw } from "../../index";
|
import { Excalidraw, StoreAction } from "../../index";
|
||||||
import { ExcalidrawImperativeAPI } from "../../types";
|
import { ExcalidrawImperativeAPI } from "../../types";
|
||||||
import { resolvablePromise } from "../../utils";
|
import { resolvablePromise } from "../../utils";
|
||||||
import { render } from "../test-utils";
|
import { render } from "../test-utils";
|
||||||
@ -27,7 +27,10 @@ describe("event callbacks", () => {
|
|||||||
|
|
||||||
const origBackgroundColor = h.state.viewBackgroundColor;
|
const origBackgroundColor = h.state.viewBackgroundColor;
|
||||||
excalidrawAPI.onChange(onChange);
|
excalidrawAPI.onChange(onChange);
|
||||||
excalidrawAPI.updateScene({ appState: { viewBackgroundColor: "red" } });
|
excalidrawAPI.updateScene({
|
||||||
|
appState: { viewBackgroundColor: "red" },
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
});
|
||||||
expect(onChange).toHaveBeenCalledWith(
|
expect(onChange).toHaveBeenCalledWith(
|
||||||
// elements
|
// elements
|
||||||
[],
|
[],
|
||||||
|
@ -40,6 +40,7 @@ import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
|||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import { SnapLine } from "./snapping";
|
import { SnapLine } from "./snapping";
|
||||||
import { Merge, MaybePromise, ValueOf } from "./utility-types";
|
import { Merge, MaybePromise, ValueOf } from "./utility-types";
|
||||||
|
import { StoreActionType } from "./store";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
@ -507,7 +508,7 @@ export type SceneData = {
|
|||||||
elements?: ImportedDataState["elements"];
|
elements?: ImportedDataState["elements"];
|
||||||
appState?: ImportedDataState["appState"];
|
appState?: ImportedDataState["appState"];
|
||||||
collaborators?: Map<SocketId, Collaborator>;
|
collaborators?: Map<SocketId, Collaborator>;
|
||||||
commitToStore?: boolean;
|
storeAction?: StoreActionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum UserIdleState {
|
export enum UserIdleState {
|
||||||
|
Loading…
Reference in New Issue
Block a user