fix: editing linear element (#9839)

This commit is contained in:
Marcel Mraz
2025-08-08 09:30:11 +02:00
committed by GitHub
parent df25de7e68
commit 9036812b6d
9 changed files with 264 additions and 308 deletions

View File

@@ -2,7 +2,6 @@ import {
arrayToMap, arrayToMap,
arrayToObject, arrayToObject,
assertNever, assertNever,
invariant,
isDevEnv, isDevEnv,
isShallowEqual, isShallowEqual,
isTestEnv, isTestEnv,
@@ -561,70 +560,50 @@ export class AppStateDelta implements DeltaContainer<AppState> {
): [AppState, boolean] { ): [AppState, boolean] {
try { try {
const { const {
selectedElementIds: removedSelectedElementIds = {}, selectedElementIds: deletedSelectedElementIds = {},
selectedGroupIds: removedSelectedGroupIds = {}, selectedGroupIds: deletedSelectedGroupIds = {},
} = this.delta.deleted; } = this.delta.deleted;
const { const {
selectedElementIds: addedSelectedElementIds = {}, selectedElementIds: insertedSelectedElementIds = {},
selectedGroupIds: addedSelectedGroupIds = {}, selectedGroupIds: insertedSelectedGroupIds = {},
selectedLinearElementId, selectedLinearElement: insertedSelectedLinearElement,
selectedLinearElementIsEditing,
...directlyApplicablePartial ...directlyApplicablePartial
} = this.delta.inserted; } = this.delta.inserted;
const mergedSelectedElementIds = Delta.mergeObjects( const mergedSelectedElementIds = Delta.mergeObjects(
appState.selectedElementIds, appState.selectedElementIds,
addedSelectedElementIds, insertedSelectedElementIds,
removedSelectedElementIds, deletedSelectedElementIds,
); );
const mergedSelectedGroupIds = Delta.mergeObjects( const mergedSelectedGroupIds = Delta.mergeObjects(
appState.selectedGroupIds, appState.selectedGroupIds,
addedSelectedGroupIds, insertedSelectedGroupIds,
removedSelectedGroupIds, deletedSelectedGroupIds,
); );
let selectedLinearElement = appState.selectedLinearElement; const selectedLinearElement =
insertedSelectedLinearElement &&
if (selectedLinearElementId === null) { nextElements.has(insertedSelectedLinearElement.elementId)
// Unselect linear element (visible change) ? new LinearElementEditor(
selectedLinearElement = null; nextElements.get(
} else if ( insertedSelectedLinearElement.elementId,
selectedLinearElementId && ) as NonDeleted<ExcalidrawLinearElement>,
nextElements.has(selectedLinearElementId) nextElements,
) { insertedSelectedLinearElement.isEditing,
selectedLinearElement = new LinearElementEditor( )
nextElements.get( : null;
selectedLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>,
nextElements,
selectedLinearElementIsEditing === true, // Can be unknown which is defaulted to false
);
}
if (
// Value being 'null' is equivaluent to unknown in this case because it only gets set
// to null when 'selectedLinearElementId' is set to null
selectedLinearElementIsEditing != null
) {
invariant(
selectedLinearElement,
`selectedLinearElement is null when selectedLinearElementIsEditing is set to ${selectedLinearElementIsEditing}`,
);
selectedLinearElement = {
...selectedLinearElement,
isEditing: selectedLinearElementIsEditing,
};
}
const nextAppState = { const nextAppState = {
...appState, ...appState,
...directlyApplicablePartial, ...directlyApplicablePartial,
selectedElementIds: mergedSelectedElementIds, selectedElementIds: mergedSelectedElementIds,
selectedGroupIds: mergedSelectedGroupIds, selectedGroupIds: mergedSelectedGroupIds,
selectedLinearElement, selectedLinearElement:
typeof insertedSelectedLinearElement !== "undefined"
? selectedLinearElement
: appState.selectedLinearElement,
}; };
const constainsVisibleChanges = this.filterInvisibleChanges( const constainsVisibleChanges = this.filterInvisibleChanges(
@@ -753,64 +732,53 @@ export class AppStateDelta implements DeltaContainer<AppState> {
} }
break; break;
case "selectedLinearElementId": { case "selectedLinearElement":
const appStateKey = AppStateDelta.convertToAppStateKey(key); const nextLinearElement = nextAppState[key];
const linearElement = nextAppState[appStateKey];
if (!linearElement) { if (!nextLinearElement) {
// previously there was a linear element (assuming visible), now there is none // previously there was a linear element (assuming visible), now there is none
visibleDifferenceFlag.value = true; visibleDifferenceFlag.value = true;
} else { } else {
const element = nextElements.get(linearElement.elementId); const element = nextElements.get(nextLinearElement.elementId);
if (element && !element.isDeleted) { if (element && !element.isDeleted) {
// previously there wasn't a linear element, now there is one which is visible // previously there wasn't a linear element, now there is one which is visible
visibleDifferenceFlag.value = true; visibleDifferenceFlag.value = true;
} else { } else {
// there was assigned a linear element now, but it's deleted // there was assigned a linear element now, but it's deleted
nextAppState[appStateKey] = null; nextAppState[key] = null;
} }
} }
break; break;
} case "lockedMultiSelections":
case "selectedLinearElementIsEditing": {
// Changes in editing state are always visible
const prevIsEditing =
prevAppState.selectedLinearElement?.isEditing ?? false;
const nextIsEditing =
nextAppState.selectedLinearElement?.isEditing ?? false;
if (prevIsEditing !== nextIsEditing) {
visibleDifferenceFlag.value = true;
}
break;
}
case "lockedMultiSelections": {
const prevLockedUnits = prevAppState[key] || {}; const prevLockedUnits = prevAppState[key] || {};
const nextLockedUnits = nextAppState[key] || {}; const nextLockedUnits = nextAppState[key] || {};
// TODO: this seems wrong, we are already doing this comparison generically above,
// hence instead we should check whether elements are actually visible,
// so that once these changes are applied they actually result in a visible change to the user
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) { if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
visibleDifferenceFlag.value = true; visibleDifferenceFlag.value = true;
} }
break; break;
} case "activeLockedId":
case "activeLockedId": {
const prevHitLockedId = prevAppState[key] || null; const prevHitLockedId = prevAppState[key] || null;
const nextHitLockedId = nextAppState[key] || null; const nextHitLockedId = nextAppState[key] || null;
// TODO: this seems wrong, we are already doing this comparison generically above,
// hence instead we should check whether elements are actually visible,
// so that once these changes are applied they actually result in a visible change to the user
if (prevHitLockedId !== nextHitLockedId) { if (prevHitLockedId !== nextHitLockedId) {
visibleDifferenceFlag.value = true; visibleDifferenceFlag.value = true;
} }
break; break;
} default:
default: {
assertNever( assertNever(
key, key,
`Unknown ObservedElementsAppState's key "${key}"`, `Unknown ObservedElementsAppState's key "${key}"`,
true, true,
); );
}
} }
} }
} }
@@ -818,15 +786,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
return visibleDifferenceFlag.value; return visibleDifferenceFlag.value;
} }
private static convertToAppStateKey(
key: keyof Pick<ObservedElementsAppState, "selectedLinearElementId">,
): keyof Pick<AppState, "selectedLinearElement"> {
switch (key) {
case "selectedLinearElementId":
return "selectedLinearElement";
}
}
private static filterSelectedElements( private static filterSelectedElements(
selectedElementIds: AppState["selectedElementIds"], selectedElementIds: AppState["selectedElementIds"],
elements: SceneElementsMap, elements: SceneElementsMap,
@@ -891,8 +850,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
editingGroupId, editingGroupId,
selectedGroupIds, selectedGroupIds,
selectedElementIds, selectedElementIds,
selectedLinearElementId, selectedLinearElement,
selectedLinearElementIsEditing,
croppingElementId, croppingElementId,
lockedMultiSelections, lockedMultiSelections,
activeLockedId, activeLockedId,

View File

@@ -978,8 +978,7 @@ const getDefaultObservedAppState = (): ObservedAppState => {
viewBackgroundColor: COLOR_PALETTE.white, viewBackgroundColor: COLOR_PALETTE.white,
selectedElementIds: {}, selectedElementIds: {},
selectedGroupIds: {}, selectedGroupIds: {},
selectedLinearElementId: null, selectedLinearElement: null,
selectedLinearElementIsEditing: null,
croppingElementId: null, croppingElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
@@ -998,14 +997,12 @@ export const getObservedAppState = (
croppingElementId: appState.croppingElementId, croppingElementId: appState.croppingElementId,
activeLockedId: appState.activeLockedId, activeLockedId: appState.activeLockedId,
lockedMultiSelections: appState.lockedMultiSelections, lockedMultiSelections: appState.lockedMultiSelections,
selectedLinearElementId: selectedLinearElement: appState.selectedLinearElement
(appState as AppState).selectedLinearElement?.elementId ?? ? {
(appState as ObservedAppState).selectedLinearElementId ?? elementId: appState.selectedLinearElement.elementId,
null, isEditing: !!appState.selectedLinearElement.isEditing,
selectedLinearElementIsEditing: }
(appState as AppState).selectedLinearElement?.isEditing ?? : null,
(appState as ObservedAppState).selectedLinearElementIsEditing ??
null,
}; };
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, { Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {

View File

@@ -7,7 +7,10 @@ describe("AppStateDelta", () => {
describe("ensure stable delta properties order", () => { describe("ensure stable delta properties order", () => {
it("should maintain stable order for root properties", () => { it("should maintain stable order for root properties", () => {
const name = "untitled scene"; const name = "untitled scene";
const selectedLinearElementId = "id1" as LinearElementEditor["elementId"]; const selectedLinearElement = {
elementId: "id1" as LinearElementEditor["elementId"],
isEditing: false,
};
const commonAppState = { const commonAppState = {
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
@@ -24,23 +27,23 @@ describe("AppStateDelta", () => {
const prevAppState1: ObservedAppState = { const prevAppState1: ObservedAppState = {
...commonAppState, ...commonAppState,
name: "", name: "",
selectedLinearElementId: null, selectedLinearElement: null,
}; };
const nextAppState1: ObservedAppState = { const nextAppState1: ObservedAppState = {
...commonAppState, ...commonAppState,
name, name,
selectedLinearElementId, selectedLinearElement,
}; };
const prevAppState2: ObservedAppState = { const prevAppState2: ObservedAppState = {
selectedLinearElementId: null, selectedLinearElement: null,
name: "", name: "",
...commonAppState, ...commonAppState,
}; };
const nextAppState2: ObservedAppState = { const nextAppState2: ObservedAppState = {
selectedLinearElementId, selectedLinearElement,
name, name,
...commonAppState, ...commonAppState,
}; };
@@ -58,9 +61,7 @@ describe("AppStateDelta", () => {
selectedGroupIds: {}, selectedGroupIds: {},
editingGroupId: null, editingGroupId: null,
croppingElementId: null, croppingElementId: null,
selectedLinearElementId: null, selectedLinearElement: null,
selectedLinearElementIsEditing: null,
editingLinearElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
}; };
@@ -106,9 +107,7 @@ describe("AppStateDelta", () => {
selectedElementIds: {}, selectedElementIds: {},
editingGroupId: null, editingGroupId: null,
croppingElementId: null, croppingElementId: null,
selectedLinearElementId: null, selectedLinearElement: null,
selectedLinearElementIsEditing: null,
editingLinearElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
}; };

View File

@@ -20,7 +20,7 @@ export type ReconciledExcalidrawElement = OrderedExcalidrawElement &
export type RemoteExcalidrawElement = OrderedExcalidrawElement & export type RemoteExcalidrawElement = OrderedExcalidrawElement &
MakeBrand<"RemoteExcalidrawElement">; MakeBrand<"RemoteExcalidrawElement">;
const shouldDiscardRemoteElement = ( export const shouldDiscardRemoteElement = (
localAppState: AppState, localAppState: AppState,
local: OrderedExcalidrawElement | undefined, local: OrderedExcalidrawElement | undefined,
remote: RemoteExcalidrawElement, remote: RemoteExcalidrawElement,
@@ -30,7 +30,7 @@ const shouldDiscardRemoteElement = (
// local element is being edited // local element is being edited
(local.id === localAppState.editingTextElement?.id || (local.id === localAppState.editingTextElement?.id ||
local.id === localAppState.resizingElement?.id || local.id === localAppState.resizingElement?.id ||
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array local.id === localAppState.newElement?.id ||
// local element is newer // local element is newer
local.version > remote.version || local.version > remote.version ||
// resolve conflicting edits deterministically by taking the one with // resolve conflicting edits deterministically by taking the one with

View File

@@ -291,6 +291,7 @@ const restoreElement = (
// if empty text, mark as deleted. We keep in array // if empty text, mark as deleted. We keep in array
// for data integrity purposes (collab etc.) // for data integrity purposes (collab etc.)
if (!text && !element.isDeleted) { if (!text && !element.isDeleted) {
// TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
element = { ...element, originalText: text, isDeleted: true }; element = { ...element, originalText: text, isDeleted: true };
element = bumpVersion(element); element = bumpVersion(element);
} }

View File

@@ -543,13 +543,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"selectedElementIds": { "selectedElementIds": {
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id4",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -1026,13 +1027,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"selectedElementIds": { "selectedElementIds": {
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id4",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -2414,13 +2416,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"selectedElementIds": { "selectedElementIds": {
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id4",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -7028,9 +7031,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
"searchMatches": null, "searchMatches": null,
"selectedElementIds": { "selectedElementIds": {},
"id0": true,
},
"selectedElementsAreBeingDragged": false, "selectedElementsAreBeingDragged": false,
"selectedGroupIds": {}, "selectedGroupIds": {},
"selectionElement": null, "selectionElement": null,
@@ -7131,74 +7132,70 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"elements": { "elements": {
"added": {}, "added": {},
"removed": { "removed": {},
"id0": { "updated": {},
"deleted": { },
"angle": 0, "id": "id13",
"backgroundColor": "transparent", },
"boundElements": null, {
"customData": undefined, "appState": AppStateDelta {
"elbowed": false, "delta": Delta {
"endArrowhead": "arrow", "deleted": {
"endBinding": null, "selectedLinearElement": {
"fillStyle": "solid", "elementId": "id0",
"frameId": null, "isEditing": false,
"groupIds": [],
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": [
10,
10,
],
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
10,
10,
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"startArrowhead": null,
"startBinding": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"version": 6,
"width": 10,
"x": 0,
"y": 0,
}, },
"inserted": { },
"isDeleted": true, "inserted": {
"version": 5, "selectedLinearElement": null,
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {},
},
"id": "id14",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedLinearElement": {
"elementId": "id0",
"isEditing": true,
},
},
"inserted": {
"selectedLinearElement": {
"elementId": "id0",
"isEditing": false,
}, },
}, },
}, },
},
"elements": {
"added": {},
"removed": {},
"updated": {}, "updated": {},
}, },
"id": "id2", "id": "id15",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElement": {
"selectedLinearElementIsEditing": null, "elementId": "id0",
"isEditing": true,
},
}, },
}, },
}, },
@@ -7207,43 +7204,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"removed": {}, "removed": {},
"updated": {}, "updated": {},
}, },
"id": "id4", "id": "id16",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedLinearElementIsEditing": true,
},
"inserted": {
"selectedLinearElementIsEditing": false,
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {},
},
"id": "id6",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedLinearElementIsEditing": true,
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {},
},
"id": "id10",
}, },
] ]
`; `;
@@ -10210,12 +10171,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -15770,13 +15732,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -16064,15 +16027,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -16688,15 +16652,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -17320,15 +17285,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -18017,15 +17983,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -18132,15 +18099,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
}, },
}, },
@@ -18744,15 +18712,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -18859,15 +18828,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id13",
"isEditing": false,
},
}, },
}, },
}, },
@@ -20656,12 +20626,13 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -20676,10 +20647,16 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementIsEditing": true, "selectedLinearElement": {
"elementId": "id0",
"isEditing": true,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementIsEditing": false, "selectedLinearElement": {
"elementId": "id0",
"isEditing": false,
},
}, },
}, },
}, },
@@ -20747,10 +20724,16 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementIsEditing": false, "selectedLinearElement": {
"elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementIsEditing": true, "selectedLinearElement": {
"elementId": "id0",
"isEditing": true,
},
}, },
}, },
}, },

View File

@@ -6394,15 +6394,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id9": true, "id9": true,
}, },
"selectedLinearElementId": "id9", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id9",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id6": true, "id6": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -6470,13 +6471,19 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id12": true, "id12": true,
}, },
"selectedLinearElementId": "id12", "selectedLinearElement": {
"elementId": "id12",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id9": true, "id9": true,
}, },
"selectedLinearElementId": "id9", "selectedLinearElement": {
"elementId": "id9",
"isEditing": false,
},
}, },
}, },
}, },
@@ -6542,15 +6549,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id15": true, "id15": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id12": true, "id12": true,
}, },
"selectedLinearElementId": "id12", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id12",
"isEditing": false,
},
}, },
}, },
}, },
@@ -6677,12 +6685,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id15", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id15",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -6700,15 +6709,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id22": true, "id22": true,
}, },
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id15": true, "id15": true,
}, },
"selectedLinearElementId": "id15", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id15",
"isEditing": false,
},
}, },
}, },
}, },
@@ -6833,12 +6843,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id22", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id22",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -6873,12 +6884,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": "id22", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id22",
"isEditing": false,
},
}, },
}, },
}, },
@@ -8719,13 +8731,14 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -8946,13 +8959,14 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -9365,13 +9379,14 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -9770,13 +9785,14 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id0",
"isEditing": false,
},
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@@ -14494,12 +14510,13 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": null, "selectedLinearElement": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": "id6", "selectedLinearElement": {
"selectedLinearElementIsEditing": false, "elementId": "id6",
"isEditing": false,
},
}, },
}, },
}, },

View File

@@ -3043,15 +3043,14 @@ describe("history", () => {
}); });
Keyboard.undo(); Keyboard.undo();
expect(API.getUndoStack().length).toBe(3); expect(API.getUndoStack().length).toBe(0);
expect(API.getRedoStack().length).toBe(1); expect(API.getRedoStack().length).toBe(4);
expect(h.state.selectedLinearElement).not.toBeNull(); expect(h.state.selectedLinearElement).toBeNull();
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
Keyboard.redo(); Keyboard.redo();
expect(API.getUndoStack().length).toBe(4); expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0); expect(API.getRedoStack().length).toBe(0);
expect(h.state.selectedLinearElement).not.toBeNull(); expect(h.state.selectedLinearElement).toBeNull();
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
}); });

View File

@@ -248,8 +248,10 @@ export type ObservedElementsAppState = {
editingGroupId: AppState["editingGroupId"]; editingGroupId: AppState["editingGroupId"];
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
selectedGroupIds: AppState["selectedGroupIds"]; selectedGroupIds: AppState["selectedGroupIds"];
selectedLinearElementId: LinearElementEditor["elementId"] | null; selectedLinearElement: {
selectedLinearElementIsEditing: boolean | null; elementId: LinearElementEditor["elementId"];
isEditing: boolean;
} | null;
croppingElementId: AppState["croppingElementId"]; croppingElementId: AppState["croppingElementId"];
lockedMultiSelections: AppState["lockedMultiSelections"]; lockedMultiSelections: AppState["lockedMultiSelections"];
activeLockedId: AppState["activeLockedId"]; activeLockedId: AppState["activeLockedId"];