Skip to content

Commit

Permalink
Merge pull request #2410 from concord-consortium/188129121-history-bu…
Browse files Browse the repository at this point in the history
…g-fixes

Make history playback work for "X It!"
  • Loading branch information
scytacki committed Sep 20, 2024
2 parents f268607 + 5ad1167 commit 5355131
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 89 deletions.
59 changes: 59 additions & 0 deletions src/models/history/tree-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,54 @@ const action4 = {
undoable: true
};

const sharedModelChange = {
"action": "/content/sharedModelMap/sm1/sharedModel/setValue",
"created": expect.any(Number),
"id": expect.any(String),
"records": [
{
"action": "/content/sharedModelMap/sm1/sharedModel/setValue",
"inversePatches": [
{
"op": "replace",
"path": "/content/sharedModelMap/sm1/sharedModel/value",
"value": undefined,
},
],
"patches": [
{
"op": "replace",
"path": "/content/sharedModelMap/sm1/sharedModel/value",
"value": "shared value",
},
],
"tree": "test",
},
{
"action": "/handleSharedModelChanges",
"inversePatches": [
{
"op": "replace",
"path": "/content/tileMap/t1/content/text",
},
],
"patches": [
{
"op": "replace",
"path": "/content/tileMap/t1/content/text",
"value": "shared value-tile",
},
],
"tree": "test",
}
],
"state": "complete",
"tree": "test",
"undoable": true,
};



/**
* Remove the Jest `expect.any(Number)` on created, and provide a real id.
* @param entry
Expand Down Expand Up @@ -295,6 +343,17 @@ it("can replay the history entries", async () => {
expect(getSnapshot(manager.document.history)).toEqual(history);
});

it("records tile model changes in response to shared model changes", async () => {
const {tileContent, manager, sharedModel} = setupDocument();
sharedModel.setValue("shared value");
await expectEntryToBeComplete(manager, 1);
expect(tileContent.text).toBe("shared value-tile");
expect(tileContent.updateCount).toBe(1);

const changeDocument = manager.document as Instance<typeof CDocument>;
expect(getSnapshot(changeDocument.history)).toEqual([sharedModelChange]);
});

// TODO: it would nicer to use a custom Jest matcher here so we can
// provide a better error message when it fails
async function expectEntryToBeComplete(manager: Instance<typeof TreeManager>, length: number) {
Expand Down
14 changes: 9 additions & 5 deletions src/models/history/tree-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,13 +543,17 @@ export const TreeManager = types
const startingIndex = direction === 1 ? self.numHistoryEventsApplied : self.numHistoryEventsApplied - 1;
const endingIndex = direction === 1 ? newHistoryPosition : newHistoryPosition - 1;
for (let i=startingIndex; i !== endingIndex; i=i+direction) {
const entry = self.document.history.at(i);
for (const treeEntry of (entry?.records || [])) {
const patches = treePatches[treeEntry.tree];
const historyEntry = self.document.history.at(i);
let records = historyEntry ? [ ...historyEntry.records] : [];
if (direction === -1) {
records = records.reverse();
}
for (const entryRecord of records) {
const patches = treePatches[entryRecord.tree];
if (newHistoryPosition > self.numHistoryEventsApplied) {
patches?.push(...treeEntry.getPatches(HistoryOperation.Redo));
patches?.push(...entryRecord.getPatches(HistoryOperation.Redo));
} else {
patches?.push(...treeEntry.getPatches(HistoryOperation.Undo));
patches?.push(...entryRecord.getPatches(HistoryOperation.Undo));
}
}
}
Expand Down
59 changes: 33 additions & 26 deletions src/models/history/tree-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export class TreeMonitor {
// TODO: If there are multiple shared model changes, we might want
// to send them all to the tree at the same time, that way it can
// inform the tiles of all changes at the same time.
const exchangesToProcess: { id: string, path: string }[] = [];
for (const [sharedModelPath, numModifications] of Object.entries(sharedModelModifications)) {
if (numModifications > 0) {
// If a shared model has been deleted, we can't run these callbacks without errors,
Expand Down Expand Up @@ -251,32 +252,7 @@ export class TreeMonitor {
await this.manager.startExchange(historyEntryId, sharedModelChangesExchangeId,
"recordAction.sharedModelChanges");

// Recursion: handleSharedModelChanges is an action on the
// tree, and we are currently in a middleware that is
// monitoring actions on that tree. At this point in the
// middleware we are finishing a different action. Calling
// handleSharedModelChanges starts a new top level action:
// an action with no parent actions. This is what we want so
// we can record any changes made to the tree as part of the
// undo entry. I don't know if calling an action from a
// middleware is an officially supported or tested approach.
// It is working now. If it stops working we could delay the
// call to handleSharedModelChanges with a setTimeout.
//
// It is recursive because we will end up back in this
// recordAction function. Because we are awaiting
// handleSharedModelChanges that second recursive
// recordAction will get kicked off before this call to
// handleSharedModelChanges returns. The tree's
// implementation of handleSharedModelChanges should not
// modify the shared model itself or we could get into an
// infinite loop.
//
// Within this recursive call to recordAction,
// addTreePatchRecord will be called. This is how the
// startExchange above is closed out.
await this.tree.handleSharedModelChanges(historyEntryId, sharedModelChangesExchangeId,
call, sharedModelPath, initialSharedModelMap);
exchangesToProcess.push({ id: sharedModelChangesExchangeId, path: sharedModelPath });
}
}

Expand All @@ -301,5 +277,36 @@ export class TreeMonitor {
inversePatches,
};
this.manager.addTreePatchRecord(historyEntryId, exchangeId, record);

for (const exchange of exchangesToProcess) {
// Now that the patches have been recorded, we can process the shared model changes
// Recursion: handleSharedModelChanges is an action on the
// tree, and we are currently in a middleware that is
// monitoring actions on that tree. At this point in the
// middleware we are finishing a different action. Calling
// handleSharedModelChanges starts a new top level action:
// an action with no parent actions. This is what we want so
// we can record any changes made to the tree as part of the
// undo entry. I don't know if calling an action from a
// middleware is an officially supported or tested approach.
// It is working now. If it stops working we could delay the
// call to handleSharedModelChanges with a setTimeout.
//
// It is recursive because we will end up back in this
// recordAction function. Because we are awaiting
// handleSharedModelChanges that second recursive
// recordAction will get kicked off before this call to
// handleSharedModelChanges returns. The tree's
// implementation of handleSharedModelChanges should not
// modify the shared model itself or we could get into an
// infinite loop.
//
// Within this recursive call to recordAction,
// addTreePatchRecord will be called. This is how the
// startExchange above is closed out.
await this.tree.handleSharedModelChanges(historyEntryId, exchange.id,
call, exchange.path, initialSharedModelMap);
}

}
}
56 changes: 28 additions & 28 deletions src/models/history/undo-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,21 +610,21 @@ const initialSharedModelUpdateEntry = {
created: expect.any(Number),
id: expect.any(String),
records: [
{ action: "/handleSharedModelChanges",
{ action: "/content/sharedModelMap/sm1/sharedModel/setValue",
inversePatches: [
{ op: "replace", path: "/content/tileMap/t1/content/text", value: undefined}
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: undefined}
],
patches: [
{ op: "replace", path: "/content/tileMap/t1/content/text", value: "something-tile"}
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: "something"}
],
tree: "test"
},
{ action: "/content/sharedModelMap/sm1/sharedModel/setValue",
{ action: "/handleSharedModelChanges",
inversePatches: [
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: undefined}
{ op: "replace", path: "/content/tileMap/t1/content/text", value: undefined}
],
patches: [
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: "something"}
{ op: "replace", path: "/content/tileMap/t1/content/text", value: "something-tile"}
],
tree: "test"
}
Expand Down Expand Up @@ -710,19 +710,19 @@ const redoSharedModelEntry = {
records: [
{ action: "/applyPatchesFromManager",
inversePatches: [
{ op: "replace", path: "/content/tileMap/t1/content/text", value: undefined}
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: undefined}
],
patches: [
{ op: "replace", path: "/content/tileMap/t1/content/text", value: "something-tile"}
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: "something"}
],
tree: "test"
},
{ action: "/applyPatchesFromManager",
inversePatches: [
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: undefined}
{ op: "replace", path: "/content/tileMap/t1/content/text", value: undefined}
],
patches: [
{ op: "replace", path: "/content/sharedModelMap/sm1/sharedModel/value", value: "something"}
{ op: "replace", path: "/content/tileMap/t1/content/text", value: "something-tile"}
],
tree: "test"
}
Expand Down Expand Up @@ -821,24 +821,6 @@ it("can track the addition of a new shared model", async () => {
created: expect.any(Number),
id: expect.any(String),
records: [
{
tree: "test",
action: "/handleSharedModelChanges",
patches: [
{
op: "replace",
path: "/content/tileMap/t1/content/text",
value: "new model-tile"
}
],
inversePatches: [
{
op: "replace",
path: "/content/tileMap/t1/content/text",
value: undefined
}
]
},
{
action: "/content/addSharedModel",
inversePatches: [
Expand All @@ -859,6 +841,24 @@ it("can track the addition of a new shared model", async () => {
}
],
tree: "test"
},
{
tree: "test",
action: "/handleSharedModelChanges",
patches: [
{
op: "replace",
path: "/content/tileMap/t1/content/text",
value: "new model-tile"
}
],
inversePatches: [
{
op: "replace",
path: "/content/tileMap/t1/content/text",
value: undefined
}
]
}
],
state: "complete",
Expand Down
15 changes: 9 additions & 6 deletions src/models/history/undo-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,20 @@ export const UndoStore = types
const uniqueTreeIds = [...new Set(treeIds)];

// first disable shared model syncing in each tree
// Order of calls does not matter for this operation.
const startPromises = uniqueTreeIds.map(treeId => {
const startExchangeId = nanoid();
manager.startExchange(historyEntryId, startExchangeId, "UndoStore.applyPatchesToTrees.start");

return manager.trees[treeId].startApplyingPatchesFromManager(historyEntryId, startExchangeId);
});
yield Promise.all(startPromises);

// apply the patches to all trees
const applyPromises = treePatchRecords.map(treePatchRecord => {
// apply the patches to all trees, in reverse order if we are undoing changes.
const undoRecords = [ ...treePatchRecords ];
if (opType === HistoryOperation.Undo) {
undoRecords.reverse();
}
for (const treePatchRecord of undoRecords) {
// console.log(`send tile entry to ${opType} to the tree`, getSnapshot(treeEntry));

// If there are multiple trees, and a patch is applied to shared model
Expand All @@ -91,10 +95,9 @@ export const UndoStore = types
manager.startExchange(historyEntryId, applyExchangeId, "UndoStore.applyPatchesToTrees.apply");

const tree = manager.trees[treePatchRecord.tree];
return tree.applyPatchesFromManager(historyEntryId, applyExchangeId,
yield tree.applyPatchesFromManager(historyEntryId, applyExchangeId,
treePatchRecord.getPatches(opType));
});
yield Promise.all(applyPromises);
}

// finish the patch application
//
Expand Down
12 changes: 9 additions & 3 deletions src/plugins/data-card/components/case-attribute.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { isAlive } from "mobx-state-tree";
import escapeStringRegexp from "escape-string-regexp";
import { useCombobox } from "downshift";
import { uniq } from "lodash";
import { VisuallyHidden } from "@chakra-ui/react";
import classNames from "classnames";
import { gImageMap } from "../../../models/image-map";
import { ITileModel } from "../../../models/tiles/tile-model";
import { DataCardContentModelType } from "../data-card-content";
Expand All @@ -25,7 +27,6 @@ import NumberTypeIcon from "../assets/id-type-number.svg";
import ExpandDownIcon from "../assets/expand-more-icon.svg";

import './single-card-data-area.scss';
import classNames from "classnames";

const typeIcons = {
"date": <DateTypeIcon />,
Expand Down Expand Up @@ -57,13 +58,16 @@ export const CaseAttribute: React.FC<IProps> = observer(props => {
model, caseId, attrKey, currEditAttrId, currEditFacet,
setCurrEditFacet, setCurrEditAttrId, readOnly
} = props;
if (!isAlive(model)) {
console.log("rendering unalive model", model);
}
const content = model.content as DataCardContentModelType;
const dataSet = content.dataSet;
const cell = { attributeId: attrKey, caseId: caseId ?? "" };
const isLinked = useIsLinked();

const getName = useCallback(() => {
return content.dataSet.attrFromID(attrKey).name;
return content.dataSet.attrFromID(attrKey)?.name;
}, [attrKey, content.dataSet]);

const getValue = useCallback(() => {
Expand Down Expand Up @@ -92,7 +96,9 @@ export const CaseAttribute: React.FC<IProps> = observer(props => {
}, [editingValue, valueCandidate.length]);

useEffect(() => {
const nameLines = measureTextLines(getName(), kFieldWidthFactor);
const name = getName();
if (!name) return;
const nameLines = measureTextLines(name, kFieldWidthFactor);
const nameCandidateLines = measureTextLines(nameCandidate, kFieldWidthFactor);
const nameLinesNeeded = Math.max(nameLines, nameCandidateLines);
const valueLines = measureTextLines(valueStr, kFieldWidthFactor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const DeleteAttrButton = () => {
const isEditingValue = !!context?.currEditAttrId && context?.currEditFacet === "value";

const handleClick = () => {
const thisCaseId = content?.dataSet.caseIDFromIndex(content.caseIndex);
const thisCaseId = content?.dataSet.caseIDFromIndex(content.caseIndexNumber);
if (thisCaseId && context){
content?.setAttValue(thisCaseId, context.currEditAttrId, "");
}
Expand Down
Loading

0 comments on commit 5355131

Please sign in to comment.