feat(notes): show block locks and presence, conflict copies with accept/discard, undo/redo
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m21s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m21s
Phase 3 of the collaboration plan:
- Blocks held by someone else show a lock with the name and are read-only;
Enter in such a block adds a new block below. Delete, merge, indent and
drag are not started when a involved block (or one in its branch) is
locked; a short notice explains why (fixes FLAW-25).
- Presence: the editor header lists who else has the note open.
- Conflict copies are highlighted ("Konflikt – Version von …") with
Übernehmen (replaces the original or the title) and Verwerfen. Both check
the locks of the original and of the copy first.
- Undo/redo of own changes (Ctrl/Cmd+Z, Ctrl+Shift+Z, Ctrl+Y, beforeinput
history events): typing is grouped per block until a 1 s pause, up to 200
steps per tab; a step is skipped with a notice if someone else changed or
holds the block meanwhile. New clearConflict props patch on the server.
- New blocks get focus synchronously on mount (fixes FLAW-26).
- Fix: after reopening a note from local storage the engine polled every
30 s instead of 3 s until the next change, because the delay was decided
before the first response told it the note is shared.
- Tests: undo/conflict engine tests, lock/undo/conflict UI tests and
e2e/lock.e2e.mjs (two browsers); all three browser tests pass against a
freshly built image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08d9b2dc77
commit
aa397c18cd
20 files changed
+1060
-38
No files matched your search
@@ -29,6 +29,8 @@ type PropsPatch struct {
|
||||
Checked *bool `json:"checked,omitempty"`
|
||||
Collapsed *bool `json:"collapsed,omitempty"`
|
||||
Lang *string `json:"lang,omitempty"`
|
||||
// ClearConflict removes the conflict marking (after the user accepted or kept a conflict copy)
|
||||
ClearConflict *bool `json:"clearConflict,omitempty"`
|
||||
}
|
||||
|
||||
// Op is a change requested by a client. OpID is chosen by the client and makes retries idempotent.
|
||||
@@ -262,6 +264,9 @@ func (ap *applier) apply(op Op) Result {
|
||||
if p.Lang != nil {
|
||||
props.Lang = *p.Lang
|
||||
}
|
||||
if p.ClearConflict != nil && *p.ClearConflict {
|
||||
props.ConflictOf, props.ConflictAuthor = "", ""
|
||||
}
|
||||
}
|
||||
if typ != b.Type || props != b.Props {
|
||||
ap.emit(Effect{Kind: OpSetProps, BlockID: b.ID, Type: typ, Props: &props})
|
||||
|
||||
@@ -261,3 +261,14 @@ func TestNotifier(t *testing.T) {
|
||||
t.Fatal("Notify must wake waiters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearConflictMarking(t *testing.T) {
|
||||
d := docWith(t, [3]string{"a", "", "Start"})
|
||||
Apply(d, Op{OpID: "1", Kind: OpSetContent, BlockID: "a", BaseVersion: 1, Content: str("A")}, anna, Env{})
|
||||
_, res := Apply(d, Op{OpID: "2", Kind: OpSetContent, BlockID: "a", BaseVersion: 1, Content: str("B")}, ben, Env{})
|
||||
yes := true
|
||||
Apply(d, Op{OpID: "3", Kind: OpSetProps, BlockID: res.ConflictBlockID, Patch: &PropsPatch{ClearConflict: &yes}}, ben, Env{})
|
||||
if p := d.Blocks[res.ConflictBlockID].Props; p.ConflictOf != "" || p.ConflictAuthor != "" {
|
||||
t.Fatalf("conflict marking not cleared: %+v", p)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user