Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Undo/Redo for object.set and object.remove operations #658

Merged
merged 42 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9d45b52
Implement undo/redo for object set, remove
chacha912 Sep 26, 2023
e2da9e0
Add object-devtool example for testing object undo/redo
chacha912 Sep 26, 2023
c64da6e
Handle cases where the reverse operation cannot be executed
chacha912 Sep 26, 2023
a9f3e21
Merge branch 'main' of https://github.com/yorkie-team/yorkie-js-sdk i…
chacha912 Oct 12, 2023
f6abc10
Merge remote-tracking branch 'origin/main' into undo-redo-object
chacha912 Oct 13, 2023
eb1bd0e
Add whiteboard example to test object undo
chacha912 Oct 16, 2023
e6b198d
Add test to verify synchronization of nested object undo
chacha912 Oct 16, 2023
576cee1
Add rectangle deletion
chacha912 Oct 17, 2023
0c15239
Refactor object devtool
chacha912 Oct 17, 2023
4b8e829
Refactor opsource code
chacha912 Oct 17, 2023
5e0256c
Add some comments
chacha912 Oct 17, 2023
01a4d2e
Add sync button in whiteboard example
chacha912 Oct 17, 2023
45e3046
Add sync button in whiteboard example
chacha912 Oct 17, 2023
c47c7ed
Replace `executedAt` with `movedAt`
chacha912 Oct 25, 2023
05cf051
Fix bug caused by shared reference when adding elements
chacha912 Oct 25, 2023
80630d6
Merge branch 'undo-redo-object' of https://github.com/yorkie-team/yor…
chacha912 Oct 25, 2023
9b2dc2b
Cleanup test code
chacha912 Oct 27, 2023
633b429
Merge branch 'main' of https://github.com/yorkie-team/yorkie-js-sdk i…
chacha912 Oct 27, 2023
1b63499
Add test case for reverse operations referencing already garbage-coll…
chacha912 Oct 27, 2023
c910f2d
Handle reverse remove operation targeting elements deleted by other p…
chacha912 Nov 6, 2023
ee3c113
Modify to raise explicit error when `executedAt` of operation is absent
chacha912 Nov 6, 2023
24111e0
Specify OpSource
chacha912 Nov 6, 2023
d44fb4a
Modify to handle cases where operation is not applied only when eleme…
chacha912 Nov 6, 2023
13668c3
Handle error in cases where executedAt is missing
chacha912 Nov 6, 2023
9ecca35
Add test to verify reverse operation of object.set and remove
chacha912 Nov 6, 2023
08efd9c
Fix test to use disableGC option
chacha912 Nov 6, 2023
3bdee75
Update whiteboard example
hackerwins Nov 7, 2023
116fac2
Rename Element.getLastExecutedAt with Element.getPositionedAt
hackerwins Nov 7, 2023
4db1fc0
Update history stack log when presence is changed in whiteboard example
chacha912 Nov 7, 2023
7e3f118
Use event delegation
chacha912 Nov 7, 2023
df8ca4f
Rename Id to ID
chacha912 Nov 7, 2023
f47a223
Specify return type for the `toJSForTest()` instead of using any
chacha912 Nov 8, 2023
f61ac95
Add comments for event handling during undo/redo
chacha912 Nov 8, 2023
b2ff8ce
Make source non-optional in operation.execute()
chacha912 Nov 8, 2023
d83fdfb
Add comments
chacha912 Nov 8, 2023
030acb8
Modify shape movement to use presence
chacha912 Nov 8, 2023
77513bf
Add `get` and `getByID` methods to the CRDTContainer
chacha912 Nov 9, 2023
ba622ce
Unify the event triggering condition to opInfos
chacha912 Nov 10, 2023
cccd592
Clear redo when a new local operation is applied
chacha912 Nov 10, 2023
3fac1ca
Update to handle not exposing deleted elements in the JSON layer
chacha912 Nov 10, 2023
001bc70
Add comments
chacha912 Nov 10, 2023
79eca12
Add comments and revise the codes
hackerwins Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions public/devtool/object.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
.devtool-root-holder,
.devtool-ops-holder {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: scroll;
font-size: 14px;
font-weight: 400;
}

.devtool-root-holder .object-content {
margin-left: 24px;
}

.devtool-root-holder .object-key-val,
.devtool-root-holder .object-val {
position: relative;
}

.devtool-root-holder .object-key-val:not(:last-of-type):before,
.devtool-root-holder .object-val:not(:last-of-type):before {
border: 1px dashed #ddd;
border-width: 0 0 0 1px;
content: '';
position: absolute;
bottom: -12px;
left: -18px;
top: 12px;
}

.devtool-root-holder .object-key-val:after,
.devtool-root-holder .object-val:after {
border-bottom: 1px dashed #ddd;
border-left: 1px dashed #ddd;
border-radius: 0 0 0 4px;
border-right: 0 dashed #ddd;
border-top: 0 dashed #ddd;
content: '';
height: 16px;
position: absolute;
top: 0;
width: 16px;
height: 32px;
top: -16px;
left: -18px;
}

.devtool-root-holder > .object-key-val:after,
.devtool-root-holder > .object-val:after {
content: none;
}

.devtool-root-holder .object-val > span,
.devtool-root-holder .object-key-val > span,
.devtool-root-holder .object-val label,
.devtool-root-holder .object-key-val label {
border: 1px solid #ddd;
border-radius: 4px;
display: inline-block;
font-size: 14px;
font-weight: 500;
letter-spacing: 0 !important;
line-height: 1.72;
margin-bottom: 16px;
padding: 6px 8px;
}

.devtool-root-holder label {
cursor: pointer;
}

.devtool-root-holder .object-key-val label:before {
content: '▾';
margin-right: 4px;
}

.devtool-root-holder input[type='checkbox']:checked + label:before {
content: '▸';
}

.devtool-root-holder input[type='checkbox']:checked ~ .object-content {
display: none;
}

.devtool-root-holder input[type='checkbox'] {
display: none;
}

.devtool-root-holder .timeticket,
.devtool-ops-holder .timeticket {
border-radius: 4px;
background: #f1f2f3;
font-size: 12px;
font-weight: 400;
padding: 2px 6px;
margin-left: 4px;
letter-spacing: 1px;
}

.devtool-ops-holder .change {
display: flex;
margin-bottom: 3px;
border-top: 1px solid #ddd;
word-break: break-all;
}
.devtool-ops-holder label {
position: relative;
overflow: hidden;
padding-left: 24px;
cursor: pointer;
line-height: 1.6;
}
.devtool-ops-holder input[type='checkbox']:checked + label {
height: 22px;
}
.devtool-ops-holder input[type='checkbox'] {
display: none;
}
.devtool-ops-holder .count {
position: absolute;
left: 0px;
display: flex;
justify-content: center;
width: 20px;
height: 20px;
font-size: 13px;
}
.devtool-ops-holder .op {
display: block;
}
.devtool-ops-holder .op:first-child {
display: inline-block;
}
.devtool-ops-holder .op .type {
padding: 0 4px;
border-radius: 4px;
background: #e6e6fa;
}
.devtool-ops-holder .op .type.set {
background: #cff7cf;
}
.devtool-ops-holder .op .type.remove {
background: #f9c0c8;
}
.devtool-ops-holder .op .type.add {
background: #add8e6;
}
121 changes: 121 additions & 0 deletions public/devtool/object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const objectDevtool = (
doc,
{ rootHolder, opsHolder, undoOpsHolder, redoOpsHolder },
) => {
const displayRootObject = () => {
const rootObj = doc.getRoot().toJSForTest();
rootHolder.innerHTML = `
<div class="devtool-root-holder">
${renderContainer(rootObj)}
</div>
`;
};

const renderContainer = ({ key, value, id }) => {
const valueHTML = Object.values(value)
.map((v) => {
return v.type === 'YORKIE_OBJECT' || v.type === 'YORKIE_ARRAY'
? renderContainer(v)
: renderValue(v);
})
.join('');
if (key === undefined) key = 'root';
return `
<div class="object-key-val">
${renderKey({ key, id })}
<div class="object-content">
${valueHTML}
</div>
</div>
`;
};

const renderKey = ({ key, id }) => {
return `
<input type="checkbox" id="${id}" />
<label for="${id}">${key}
<span class="timeticket">${id}</span>
</label>
`;
};

const renderValue = ({ key, value, id }) => {
return `
<div class="object-val">
<span>${key} : ${JSON.stringify(value)}
<span class="timeticket">${id}</span>
</span>
</div>
`;
};

const displayOps = () => {
opsHolder.innerHTML = `
<div class="devtool-ops-holder">
${renderOpsHolder(doc.getOpsForTest(), 'op')}
</div>
`;
};

const displayUndoOps = () => {
undoOpsHolder.innerHTML = `
<div class="devtool-ops-holder">
${renderOpsHolder(doc.getUndoStackForTest(), 'undo')}
</div>
`;
};

const displayRedoOps = () => {
redoOpsHolder.innerHTML = `
<div class="devtool-ops-holder">
${renderOpsHolder(doc.getRedoStackForTest(), 'redo')}
</div>
`;
};

const renderOpsHolder = (changes, idPrefix) => {
return changes
.map((ops, i) => {
const opsStr = ops
.map((op) => {
if (op.type === 'presence') {
return `<span class="op"><span class="type presence">presence</span>${JSON.stringify(
op.value,
)}</span>`;
}
const opType = op.toTestString().split('.')[1];
try {
const id = op.getExecutedAt()?.toTestString();
return `
<span class="op">
<span class="type ${opType.toLowerCase()}">${opType}</span>
${`<span class="timeticket">${id}</span>`}${op.toTestString()}
</span>`;
} catch (e) {
// operation in the undo/redo stack does not yet have "executedAt" set.
return `
<span class="op">
<span class="type ${opType.toLowerCase()}">${opType}</span>
${op.toTestString()}
</span>`;
}
})
.join('\n');
return `
<div class="change">
<input type="checkbox" id="${idPrefix}-${i}" />
<label for="${idPrefix}-${i}">
<span class="count">${ops.length}</span>
<span class="ops">${opsStr}</span>
</label>
</div>
`;
})
.reverse()
.join('');
};

return { displayRootObject, displayOps, displayUndoOps, displayRedoOps };
};

export default objectDevtool;
119 changes: 119 additions & 0 deletions public/whiteboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
.whiteboard-example {
display: flex;
flex-direction: column;
height: 100vh;
}

.dev-log-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 350px);
}
.dev-log {
display: flex;
flex: 1;
overflow: hidden;
}
.dev-log .log-holders {
padding: 10px;
}
.dev-log .log-holders,
.dev-log .log-holder-wrap,
.dev-log .log-holder {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.log-holder-wrap h2 {
margin: 10px 0;
}
.log-holder-wrap .stack-count {
font-weight: normal;
font-size: 14px;
}
.dev-log-wrap .network {
margin-top: 10px;
}

.canvas {
position: relative;
width: 100%;
height: 350px;
}
.canvas .toolbar {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
}
.toolbar button {
margin: 2px;
padding: 4px 6px;
color: #666;
}
.canvas .shapes {
position: absolute;
width: 100%;
height: 100%;
background: #eee;
overflow: hidden;
}
.canvas .shape {
position: absolute;
width: 50px;
height: 50px;
border-style: solid;
border-width: 2px;
}
.selection-tools {
display: none;
position: absolute;
z-index: 1;
top: 4px;
right: 4px;
background: #fff;
padding: 6px;
border-radius: 4px;
justify-content: center;
gap: 4px;
}
.selection-tools .color-picker {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 120px;
}
.selection-tools .color {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
margin: 4px;
border: 1px solid #ddd;
}
.selection-tools .color:nth-child(1) {
background: orangered;
}
.selection-tools .color:nth-child(2) {
background: gold;
}
.selection-tools .color:nth-child(3) {
background: limegreen;
}
.selection-tools .color:nth-child(4) {
background: dodgerblue;
}
.selection-tools .color:nth-child(5) {
background: darkviolet;
}
.selection-tools .color:nth-child(6) {
background: darkorange;
}
.selection-tools .color:nth-child(7) {
background: dimgray;
}
.selection-tools .color:nth-child(8) {
background: white;
}
Loading
Loading