mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Support ui update when receive doc changes (#2270)
* fix: add method * fix: update text block and doc title * fix: support ui update when receive doc changes * fix: modify the subscribe change * chore: add test for document manager * chore: add test for document manager * chore: add insert and update test for document manager * fix: load document data * fix: add update page block test * fix: try fix again * fix: node can not rerender when the node data change * fix: it should cover all content when the text delta updated * fix: add insert and delete operation in left menu * fix: put the UI Actions in async thunks * fix: remove log * fix: split text block * fix: review code --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
0068c7e731
commit
973cd9194d
128
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
128
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -48,6 +48,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alloc-no-stdlib"
|
name = "alloc-no-stdlib"
|
||||||
version = "2.0.4"
|
version = "2.0.4"
|
||||||
@ -169,7 +178,7 @@ dependencies = [
|
|||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -363,9 +372,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.12.0"
|
version = "3.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
|
checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytecheck"
|
name = "bytecheck"
|
||||||
@ -442,7 +451,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -500,11 +509,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-expr"
|
name = "cfg-expr"
|
||||||
version = "0.14.0"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a35b255461940a32985c627ce82900867c61db1659764d3675ea81963f72a4c6"
|
checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -804,9 +814,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
|
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -1154,9 +1164,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
|
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
@ -1539,6 +1549,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2012,7 +2023,7 @@ dependencies = [
|
|||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2029,7 +2040,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"pango-sys",
|
"pango-sys",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2041,21 +2052,21 @@ dependencies = [
|
|||||||
"gdk-sys",
|
"gdk-sys",
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
"x11",
|
"x11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generator"
|
name = "generator"
|
||||||
version = "0.7.3"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33a20a288a94683f5f4da0adecdbe095c94a77c295e514cc6484e9394dd8376e"
|
checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows 0.44.0",
|
"windows 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2134,7 +2145,7 @@ dependencies = [
|
|||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2180,7 +2191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
|
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2195,7 +2206,7 @@ version = "0.4.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 0.7.20",
|
||||||
"bstr",
|
"bstr",
|
||||||
"fnv",
|
"fnv",
|
||||||
"log",
|
"log",
|
||||||
@ -2221,7 +2232,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2262,7 +2273,7 @@ dependencies = [
|
|||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"pango-sys",
|
"pango-sys",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2281,9 +2292,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
|
checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -2840,9 +2851,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.141"
|
version = "0.2.142"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
|
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
@ -2924,9 +2935,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.3.1"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
|
checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@ -2959,7 +2970,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber 0.3.16",
|
"tracing-subscriber 0.3.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3330,9 +3341,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.50"
|
version = "0.10.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1"
|
checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@ -3362,9 +3373,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.85"
|
version = "0.9.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0"
|
checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@ -3410,7 +3421,7 @@ dependencies = [
|
|||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4064,13 +4075,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.3"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
|
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.0.1",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4079,7 +4090,7 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex-syntax",
|
"regex-syntax 0.6.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4088,6 +4099,12 @@ version = "0.6.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rend"
|
name = "rend"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -4209,9 +4226,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.22"
|
version = "0.1.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
@ -4230,9 +4247,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.37.11"
|
version = "0.37.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
|
checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
@ -4768,11 +4785,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.0.4"
|
version = "6.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f"
|
checksum = "d0fe581ad25d11420b873cf9aedaca0419c2b411487b134d4d21065f3d092055"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-expr 0.14.0",
|
"cfg-expr 0.15.1",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml 0.7.3",
|
"toml 0.7.3",
|
||||||
@ -4836,6 +4853,12 @@ dependencies = [
|
|||||||
"xattr",
|
"xattr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "1.2.4"
|
version = "1.2.4"
|
||||||
@ -5395,9 +5418,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.16"
|
version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
|
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers 0.1.0",
|
"matchers 0.1.0",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
@ -5799,7 +5822,7 @@ dependencies = [
|
|||||||
"pango-sys",
|
"pango-sys",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"soup2-sys",
|
"soup2-sys",
|
||||||
"system-deps 6.0.4",
|
"system-deps 6.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5896,15 +5919,6 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.39.0",
|
"windows_x86_64_msvc 0.39.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.44.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.42.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
|
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||||
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useBlockMenu(nodeId: string, open: boolean) {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [style, setStyle] = useState({ top: '0px', left: '0px' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set selection when open
|
||||||
|
dispatch(documentActions.setSelectionById(nodeId));
|
||||||
|
// get node rect
|
||||||
|
const rect = document.querySelector(`[data-block-id="${nodeId}"]`)?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
// set menu position
|
||||||
|
setStyle({
|
||||||
|
top: rect.top + 'px',
|
||||||
|
left: rect.left + 'px',
|
||||||
|
});
|
||||||
|
}, [open, nodeId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
style,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
import { insertAfterNodeThunk, deleteNodeThunk } from '@/appflowy_app/stores/reducers/document/async_actions';
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
export enum ActionType {
|
||||||
|
InsertAfter = 'insertAfter',
|
||||||
|
Remove = 'remove',
|
||||||
|
}
|
||||||
|
export function useActions(id: string, type: ActionType) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const controller = useContext(DocumentControllerContext);
|
||||||
|
|
||||||
|
const insertAfter = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(insertAfterNodeThunk({ id, controller }));
|
||||||
|
}, [id, controller, dispatch]);
|
||||||
|
|
||||||
|
const remove = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(deleteNodeThunk({ id, controller }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
if (type === ActionType.InsertAfter) {
|
||||||
|
return insertAfter;
|
||||||
|
}
|
||||||
|
if (type === ActionType.Remove) {
|
||||||
|
return remove;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { ActionType, useActions } from './MenuItem.hooks';
|
||||||
|
|
||||||
|
const icon: Record<ActionType, React.ReactNode> = {
|
||||||
|
[ActionType.InsertAfter]: <AddIcon />,
|
||||||
|
[ActionType.Remove]: <DeleteIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
function MenuItem({ id, type, onClick }: { id: string; type: ActionType; onClick?: () => void }) {
|
||||||
|
const action = useActions(id, type);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={type}
|
||||||
|
className='w-[100%]'
|
||||||
|
variant={'text'}
|
||||||
|
color={'inherit'}
|
||||||
|
startIcon={icon[type]}
|
||||||
|
onClick={() => {
|
||||||
|
void action?.();
|
||||||
|
onClick?.();
|
||||||
|
}}
|
||||||
|
style={{ justifyContent: 'flex-start' }}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuItem;
|
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useBlockMenu } from './BlockMenu.hooks';
|
||||||
|
import MenuItem from './MenuItem';
|
||||||
|
import { ActionType } from '$app/components/document/BlockMenu/MenuItem.hooks';
|
||||||
|
|
||||||
|
function BlockMenu({ open, onClose, nodeId }: { open: boolean; onClose: () => void; nodeId: string }) {
|
||||||
|
const { ref, style } = useBlockMenu(nodeId, open);
|
||||||
|
|
||||||
|
return open ? (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className='appflowy-block-menu-overlay z-1 fixed inset-0 overflow-hidden'
|
||||||
|
onScrollCapture={(e) => {
|
||||||
|
// prevent scrolling of the document when menu is open
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// prevent menu from taking focus away from editor
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='z-99 absolute flex w-[200px] translate-x-[-100%] translate-y-[32px] transform flex-col items-start justify-items-start rounded bg-white p-4 shadow'
|
||||||
|
style={style}
|
||||||
|
onClick={(e) => {
|
||||||
|
// prevent menu close when clicking on menu
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem id={nodeId} type={ActionType.InsertAfter} />
|
||||||
|
<MenuItem id={nodeId} type={ActionType.Remove} onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(BlockMenu);
|
@ -1,16 +1,14 @@
|
|||||||
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
|
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
|
||||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import { debounce } from '@/appflowy_app/utils/tool';
|
import { debounce } from '@/appflowy_app/utils/tool';
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
export function useBlockSideTools({ container }: { container: HTMLDivElement }) {
|
export function useBlockSideTools({ container }: { container: HTMLDivElement }) {
|
||||||
const [nodeId, setHoverNodeId] = useState<string>('');
|
const [nodeId, setHoverNodeId] = useState<string>('');
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const nodes = useAppSelector((state) => state.document.nodes);
|
const nodes = useAppSelector((state) => state.document.nodes);
|
||||||
const { insertAfter } = useController();
|
const nodesRef = useRef(nodes);
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
@ -20,7 +18,7 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
setHoverNodeId('');
|
setHoverNodeId('');
|
||||||
} else {
|
} else {
|
||||||
if ([BlockType.ColumnBlock].includes(nodes[id].type)) {
|
if ([BlockType.ColumnBlock].includes(nodesRef.current[id].type)) {
|
||||||
setHoverNodeId('');
|
setHoverNodeId('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -34,13 +32,13 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
const el = ref.current;
|
const el = ref.current;
|
||||||
if (!el || !nodeId) return;
|
if (!el || !nodeId) return;
|
||||||
|
|
||||||
const node = nodes[nodeId];
|
const node = nodesRef.current[nodeId];
|
||||||
if (!node) {
|
if (!node) {
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
el.style.zIndex = '-1';
|
el.style.pointerEvents = 'none';
|
||||||
} else {
|
} else {
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
el.style.zIndex = '1';
|
el.style.pointerEvents = 'auto';
|
||||||
el.style.top = '1px';
|
el.style.top = '1px';
|
||||||
if (node?.type === BlockType.HeadingBlock) {
|
if (node?.type === BlockType.HeadingBlock) {
|
||||||
const nodeData = node.data as HeadingBlockData;
|
const nodeData = node.data as HeadingBlockData;
|
||||||
@ -53,12 +51,14 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [nodeId, nodes]);
|
}, [nodeId]);
|
||||||
|
|
||||||
const handleAddClick = useCallback(() => {
|
const handleToggleMenu = useCallback((isOpen: boolean) => {
|
||||||
if (!nodeId) return;
|
setMenuOpen(isOpen);
|
||||||
insertAfter(nodes[nodeId]);
|
if (!isOpen) {
|
||||||
}, [nodeId, nodes]);
|
setHoverNodeId('');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
container.addEventListener('mousemove', debounceMove);
|
container.addEventListener('mousemove', debounceMove);
|
||||||
@ -67,25 +67,15 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
};
|
};
|
||||||
}, [debounceMove]);
|
}, [debounceMove]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
nodesRef.current = nodes;
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodeId,
|
nodeId,
|
||||||
ref,
|
ref,
|
||||||
handleAddClick,
|
handleToggleMenu,
|
||||||
};
|
menuOpen,
|
||||||
}
|
|
||||||
|
|
||||||
function useController() {
|
|
||||||
const controller = useContext(DocumentControllerContext);
|
|
||||||
|
|
||||||
const insertAfter = useCallback((node: Node) => {
|
|
||||||
const parentId = node.parent;
|
|
||||||
if (!parentId || !controller) return;
|
|
||||||
|
|
||||||
//
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
insertAfter,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,40 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useBlockSideTools } from './BlockSideTools.hooks';
|
import { useBlockSideTools } from './BlockSideTools.hooks';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import ExpandCircleDownSharpIcon from '@mui/icons-material/ExpandCircleDownSharp';
|
||||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||||
import Portal from '../BlockPortal';
|
import Portal from '../BlockPortal';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
|
import BlockMenu from '../BlockMenu';
|
||||||
|
|
||||||
const sx = { height: 24, width: 24 };
|
const sx = { height: 24, width: 24 };
|
||||||
|
|
||||||
export default function BlockSideTools(props: { container: HTMLDivElement }) {
|
export default function BlockSideTools(props: { container: HTMLDivElement }) {
|
||||||
const { nodeId, ref, handleAddClick } = useBlockSideTools(props);
|
const { nodeId, ref, menuOpen, handleToggleMenu } = useBlockSideTools(props);
|
||||||
|
|
||||||
if (!nodeId) return null;
|
if (!nodeId) return null;
|
||||||
return (
|
return (
|
||||||
<Portal blockId={nodeId}>
|
<>
|
||||||
<div
|
<Portal blockId={nodeId}>
|
||||||
ref={ref}
|
<div
|
||||||
style={{
|
ref={ref}
|
||||||
opacity: 0,
|
style={{
|
||||||
}}
|
opacity: 0,
|
||||||
className='z-1 absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500'
|
}}
|
||||||
onMouseDown={(e) => {
|
className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500'
|
||||||
// prevent toolbar from taking focus away from editor
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
// prevent toolbar from taking focus away from editor
|
||||||
}}
|
e.preventDefault();
|
||||||
>
|
}}
|
||||||
<IconButton onClick={() => handleAddClick()} sx={sx}>
|
>
|
||||||
<AddIcon />
|
<IconButton onClick={() => handleToggleMenu(true)} sx={sx}>
|
||||||
</IconButton>
|
<ExpandCircleDownSharpIcon />
|
||||||
<IconButton sx={sx}>
|
</IconButton>
|
||||||
<DragIndicatorIcon />
|
<IconButton sx={sx}>
|
||||||
</IconButton>
|
<DragIndicatorRoundedIcon />
|
||||||
</div>
|
</IconButton>
|
||||||
</Portal>
|
</div>
|
||||||
|
</Portal>
|
||||||
|
<BlockMenu open={menuOpen} onClose={() => handleToggleMenu(false)} nodeId={nodeId} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export default function DocumentTitle({ id }: { id: string }) {
|
|||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
return (
|
return (
|
||||||
<NodeContext.Provider value={node}>
|
<NodeContext.Provider value={node}>
|
||||||
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
|
<div data-block-id={node.id} className='doc-title relative mb-2 px-2 pt-[50px] text-4xl font-bold'>
|
||||||
<TextBlock placeholder='Untitled' childIds={[]} node={node} />
|
<TextBlock placeholder='Untitled' childIds={[]} node={node} />
|
||||||
</div>
|
</div>
|
||||||
</NodeContext.Provider>
|
</NodeContext.Provider>
|
||||||
|
@ -9,28 +9,26 @@ import { NodeContext } from '../_shared/SubscribeNode.hooks';
|
|||||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { node, childIds, isSelected, ref } = useNode(id);
|
const { node, childIds, isSelected, ref } = useNode(id);
|
||||||
|
|
||||||
console.log('=====', id);
|
const renderBlock = useCallback(() => {
|
||||||
const renderBlock = useCallback((_props: { node: Node; childIds?: string[] }) => {
|
switch (node.type) {
|
||||||
switch (_props.node.type) {
|
|
||||||
case 'text': {
|
case 'text': {
|
||||||
return <TextBlock node={node} childIds={childIds} />;
|
return <TextBlock node={node} childIds={childIds} />;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [node, childIds]);
|
||||||
|
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeContext.Provider value={node}>
|
<NodeContext.Provider value={node}>
|
||||||
<div {...props} ref={ref} data-block-id={node.id} className={`relative my-[2px] px-[2px] ${props.className}`}>
|
<div {...props} ref={ref} data-block-id={node.id} className={`relative px-2 ${props.className}`}>
|
||||||
{renderBlock({
|
{renderBlock()}
|
||||||
node,
|
|
||||||
childIds,
|
|
||||||
})}
|
|
||||||
<div className='block-overlay' />
|
<div className='block-overlay' />
|
||||||
{isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}
|
{isSelected ? (
|
||||||
|
<div className='pointer-events-none absolute inset-0 z-[-1] m-[1px] rounded-[4px] bg-[#E0F8FF]' />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</NodeContext.Provider>
|
</NodeContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
|
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
import { Descendant, Range } from 'slate';
|
import { Descendant, Range, Editor, Element, Text, Location } from 'slate';
|
||||||
import { TextDelta } from '$app/interfaces/document';
|
import { TextDelta } from '$app/interfaces/document';
|
||||||
import { useTextInput } from '../_shared/TextInput.hooks';
|
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||||
|
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||||
|
import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||||
|
import {
|
||||||
|
backspaceNodeThunk,
|
||||||
|
indentNodeThunk,
|
||||||
|
splitNodeThunk,
|
||||||
|
} from '@/appflowy_app/stores/reducers/document/async_actions';
|
||||||
|
import { TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
|
|
||||||
export function useTextBlock(delta: TextDelta[]) {
|
export function useTextBlock(id: string, delta: TextDelta[]) {
|
||||||
const { editor } = useTextInput(delta);
|
const { editor, onSelectionChange } = useTextInput(id, delta);
|
||||||
const [value, setValue] = useState<Descendant[]>([]);
|
const [value, setValue] = useState<Descendant[]>([]);
|
||||||
|
const { onTab, onBackSpace, onEnter } = useActions(id);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: Descendant[]) => {
|
(e: Descendant[]) => {
|
||||||
setValue(e);
|
setValue(e);
|
||||||
|
editor.operations.forEach((op) => {
|
||||||
|
if (op.type === 'set_selection') {
|
||||||
|
onSelectionChange(op.newProperties as TextSelection);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
@ -18,19 +31,40 @@ export function useTextBlock(delta: TextDelta[]) {
|
|||||||
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
|
if (!editor.selection) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const retainRange = getRetainRangeBy(editor);
|
||||||
|
const retain = getDelta(editor, retainRange);
|
||||||
|
const insertRange = getInsertRangeBy(editor);
|
||||||
|
const insert = getDelta(editor, insertRange);
|
||||||
|
void (async () => {
|
||||||
|
await onEnter(retain, insert);
|
||||||
|
})();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'Backspace': {
|
case 'Backspace': {
|
||||||
if (!editor.selection) return;
|
if (!editor.selection) return;
|
||||||
|
|
||||||
const { anchor } = editor.selection;
|
const { anchor } = editor.selection;
|
||||||
const isCollapsed = Range.isCollapsed(editor.selection);
|
const isCollapsed = Range.isCollapsed(editor.selection);
|
||||||
if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
void (async () => {
|
||||||
|
await onBackSpace();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Tab': {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
void (async () => {
|
||||||
|
await onTab();
|
||||||
|
})();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
triggerHotkey(event, editor);
|
triggerHotkey(event, editor);
|
||||||
@ -53,3 +87,65 @@ export function useTextBlock(delta: TextDelta[]) {
|
|||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useActions(id: string) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const controller = useContext(DocumentControllerContext);
|
||||||
|
|
||||||
|
const onTab = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(
|
||||||
|
indentNodeThunk({
|
||||||
|
id,
|
||||||
|
controller,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [id, controller]);
|
||||||
|
|
||||||
|
const onBackSpace = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(backspaceNodeThunk({ id, controller }));
|
||||||
|
}, [controller, id]);
|
||||||
|
|
||||||
|
const onEnter = useCallback(
|
||||||
|
async (retain: TextDelta[], insert: TextDelta[]) => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(splitNodeThunk({ id, retain, insert, controller }));
|
||||||
|
},
|
||||||
|
[controller, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onTab,
|
||||||
|
onBackSpace,
|
||||||
|
onEnter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDelta(editor: Editor, at: Location): TextDelta[] {
|
||||||
|
const baseElement = Editor.fragment(editor, at)[0] as Element;
|
||||||
|
return baseElement.children.map((item) => {
|
||||||
|
const { text, ...attributes } = item as Text;
|
||||||
|
return {
|
||||||
|
insert: text,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRetainRangeBy(editor: Editor) {
|
||||||
|
const start = Editor.start(editor, editor.selection!);
|
||||||
|
return {
|
||||||
|
anchor: { path: [0, 0], offset: 0 },
|
||||||
|
focus: start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInsertRangeBy(editor: Editor) {
|
||||||
|
const end = Editor.end(editor, editor.selection!);
|
||||||
|
const fragment = (editor.children[0] as Element).children;
|
||||||
|
return {
|
||||||
|
anchor: end,
|
||||||
|
focus: { path: [0, fragment.length - 1], offset: (fragment[fragment.length - 1] as Text).text.length },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -17,19 +17,21 @@ function TextBlock({
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const delta = useMemo(() => node.data.delta || [], [node.data.delta]);
|
const delta = useMemo(() => node.data.delta || [], [node.data.delta]);
|
||||||
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(delta);
|
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.id, delta);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props} className={`py-[2px] ${props.className}`}>
|
<>
|
||||||
<Slate editor={editor} onChange={onChange} value={value}>
|
<div {...props} className={`py-[2px] ${props.className}`}>
|
||||||
<HoveringToolbar id={node.id} />
|
<Slate editor={editor} onChange={onChange} value={value}>
|
||||||
<Editable
|
<HoveringToolbar id={node.id} />
|
||||||
onKeyDownCapture={onKeyDownCapture}
|
<Editable
|
||||||
onDOMBeforeInput={onDOMBeforeInput}
|
onKeyDownCapture={onKeyDownCapture}
|
||||||
renderLeaf={(leafProps) => <Leaf {...leafProps} />}
|
onDOMBeforeInput={onDOMBeforeInput}
|
||||||
placeholder={placeholder || 'Please enter some text...'}
|
renderLeaf={(leafProps) => <Leaf {...leafProps} />}
|
||||||
/>
|
placeholder={placeholder || 'Please enter some text...'}
|
||||||
</Slate>
|
/>
|
||||||
|
</Slate>
|
||||||
|
</div>
|
||||||
{childIds && childIds.length > 0 ? (
|
{childIds && childIds.length > 0 ? (
|
||||||
<div className='pl-[1.5em]'>
|
<div className='pl-[1.5em]'>
|
||||||
{childIds.map((item) => (
|
{childIds.map((item) => (
|
||||||
@ -37,7 +39,7 @@ function TextBlock({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,12 @@ export default function VirtualizedList({
|
|||||||
{virtualItems.map((virtualRow) => {
|
{virtualItems.map((virtualRow) => {
|
||||||
const id = childIds[virtualRow.index];
|
const id = childIds[virtualRow.index];
|
||||||
return (
|
return (
|
||||||
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
<div
|
||||||
|
className='float-left w-[100%]'
|
||||||
|
key={id}
|
||||||
|
data-index={virtualRow.index}
|
||||||
|
ref={virtualize.measureElement}
|
||||||
|
>
|
||||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||||
{renderNode(id)}
|
{renderNode(id)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,10 +16,10 @@ export function useHoveringToolbar(id: string) {
|
|||||||
|
|
||||||
if (!position) {
|
if (!position) {
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
el.style.zIndex = '-1';
|
el.style.pointerEvents = 'none';
|
||||||
} else {
|
} else {
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
el.style.zIndex = '1';
|
el.style.pointerEvents = 'auto';
|
||||||
el.style.top = position.top;
|
el.style.top = position.top;
|
||||||
el.style.left = position.left;
|
el.style.left = position.left;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ const HoveringToolbar = ({ id }: { id: string }) => {
|
|||||||
style={{
|
style={{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
}}
|
}}
|
||||||
className='z-1 absolute mt-[-6px] inline-flex h-[32px] items-stretch overflow-hidden rounded-[8px] bg-[#333] p-2 leading-tight shadow-lg transition-opacity duration-700'
|
className='absolute mt-[-6px] inline-flex h-[32px] items-stretch overflow-hidden rounded-[8px] bg-[#333] p-2 leading-tight shadow-lg transition-opacity duration-700'
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
// prevent toolbar from taking focus away from editor
|
// prevent toolbar from taking focus away from editor
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1,27 +1,53 @@
|
|||||||
import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
|
import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
|
||||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
import { TextDelta, BlockActionType } from '$app/interfaces/document';
|
import { TextDelta } from '$app/interfaces/document';
|
||||||
import { debounce } from '@/appflowy_app/utils/tool';
|
import { debounce } from '@/appflowy_app/utils/tool';
|
||||||
import { createEditor } from 'slate';
|
import { NodeContext } from './SubscribeNode.hooks';
|
||||||
import { withReact } from 'slate-react';
|
import { BlockActionTypePB } from '@/services/backend/models/flowy-document2';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
|
import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
|
|
||||||
|
import { createEditor, Transforms } from 'slate';
|
||||||
|
import { withReact, ReactEditor } from 'slate-react';
|
||||||
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||||
import { NodeContext } from './SubscribeNode.hooks';
|
|
||||||
import { BlockActionTypePB } from '@/services/backend/models/flowy-document2';
|
|
||||||
|
|
||||||
export function useTextInput(delta: TextDelta[]) {
|
export function useTextInput(id: string, delta: TextDelta[]) {
|
||||||
const { sendDelta } = useTransact();
|
const { sendDelta } = useTransact();
|
||||||
const { editor } = useBindYjs(delta, sendDelta);
|
const { editor, yText } = useBindYjs(delta, sendDelta);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) return;
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
Transforms.select(editor, currentSelection);
|
||||||
|
}, [currentSelection, editor]);
|
||||||
|
|
||||||
|
const onSelectionChange = useCallback(
|
||||||
|
(selection?: TextSelection) => {
|
||||||
|
dispatch(
|
||||||
|
documentActions.setTextSelection({
|
||||||
|
blockId: id,
|
||||||
|
selection,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editor,
|
editor,
|
||||||
|
yText,
|
||||||
|
onSelectionChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useController() {
|
function useController() {
|
||||||
const docController = useContext(DocumentControllerContext);
|
const docController = useContext(DocumentControllerContext);
|
||||||
const node = useContext(NodeContext);
|
const node = useContext(NodeContext);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
async (delta: TextDelta[]) => {
|
async (delta: TextDelta[]) => {
|
||||||
@ -43,6 +69,14 @@ function useController() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
dispatch(
|
||||||
|
documentActions.setBlockMap({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
delta,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[docController, node]
|
[docController, node]
|
||||||
);
|
);
|
||||||
@ -105,12 +139,14 @@ function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const yText = yTextRef.current;
|
const yText = yTextRef.current;
|
||||||
if (!yText) return;
|
if (!yText) return;
|
||||||
|
|
||||||
const textEventHandler = (event: Y.YTextEvent) => {
|
const textEventHandler = (event: Y.YTextEvent) => {
|
||||||
const textDelta = event.target.toDelta();
|
const textDelta = event.target.toDelta();
|
||||||
update(textDelta);
|
update(textDelta);
|
||||||
};
|
};
|
||||||
yText.applyDelta(delta);
|
if (JSON.stringify(yText.toDelta()) !== JSON.stringify(delta)) {
|
||||||
|
yText.delete(0, yText.length);
|
||||||
|
yText.applyDelta(delta);
|
||||||
|
}
|
||||||
yText.observe(textEventHandler);
|
yText.observe(textEventHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -118,5 +154,5 @@ function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
|||||||
};
|
};
|
||||||
}, [delta]);
|
}, [delta]);
|
||||||
|
|
||||||
return { editor };
|
return { editor, yText: yTextRef.current };
|
||||||
}
|
}
|
||||||
|
@ -118,14 +118,16 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
layoutType: ViewLayoutPB.Document,
|
layoutType: ViewLayoutPB.Document,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await new DocumentController(newView.id).create();
|
const c = new DocumentController(newView.id);
|
||||||
|
await c.create();
|
||||||
|
await c.dispose();
|
||||||
appDispatch(
|
appDispatch(
|
||||||
pagesActions.addPage({
|
pagesActions.addPage({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
pageType: ViewLayoutPB.Document,
|
pageType: ViewLayoutPB.Document,
|
||||||
title: newView.name,
|
title: newView.name,
|
||||||
id: newView.id,
|
id: newView.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
setShowPages(true);
|
setShowPages(true);
|
||||||
@ -134,7 +136,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddNewBoardPage = async () => {
|
const onAddNewBoardPage = async () => {
|
||||||
|
@ -47,5 +47,13 @@ export enum BlockActionType {
|
|||||||
Insert = 0,
|
Insert = 0,
|
||||||
Update = 1,
|
Update = 1,
|
||||||
Delete = 2,
|
Delete = 2,
|
||||||
Move = 3
|
Move = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeltaItem {
|
||||||
|
action: 'inserted' | 'removed' | 'updated';
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
value?: NestedBlock | string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
|
import { DocumentData, BlockType, DeltaItem } from '@/appflowy_app/interfaces/document';
|
||||||
import { createContext } from 'react';
|
import { createContext, Dispatch } from 'react';
|
||||||
import { DocumentBackendService } from './document_bd_svc';
|
import { DocumentBackendService } from './document_bd_svc';
|
||||||
import { FlowyError, BlockActionPB } from '@/services/backend';
|
import { FlowyError, BlockActionPB, DocEventPB, DeltaTypePB, BlockActionTypePB } from '@/services/backend';
|
||||||
import { DocumentObserver } from './document_observer';
|
import { DocumentObserver } from './document_observer';
|
||||||
|
import { documentActions, Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
|
import { Log } from '@/appflowy_app/utils/log';
|
||||||
|
|
||||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||||
|
|
||||||
@ -10,7 +12,7 @@ export class DocumentController {
|
|||||||
private readonly backendService: DocumentBackendService;
|
private readonly backendService: DocumentBackendService;
|
||||||
private readonly observer: DocumentObserver;
|
private readonly observer: DocumentObserver;
|
||||||
|
|
||||||
constructor(public readonly viewId: string) {
|
constructor(public readonly viewId: string, private dispatch?: Dispatch<any>) {
|
||||||
this.backendService = new DocumentBackendService(viewId);
|
this.backendService = new DocumentBackendService(viewId);
|
||||||
this.observer = new DocumentObserver(viewId);
|
this.observer = new DocumentObserver(viewId);
|
||||||
}
|
}
|
||||||
@ -66,11 +68,122 @@ export class DocumentController {
|
|||||||
await this.backendService.applyActions(actions);
|
await this.backendService.applyActions(actions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getInsertAction = (node: Node, prevId: string | null) => {
|
||||||
|
return {
|
||||||
|
action: BlockActionTypePB.Insert,
|
||||||
|
payload: this.getActionPayloadByNode(node, prevId),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getUpdateAction = (node: Node) => {
|
||||||
|
return {
|
||||||
|
action: BlockActionTypePB.Update,
|
||||||
|
payload: this.getActionPayloadByNode(node, ''),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getMoveAction = (node: Node, parentId: string, prevId: string | null) => {
|
||||||
|
return {
|
||||||
|
action: BlockActionTypePB.Move,
|
||||||
|
payload: this.getActionPayloadByNode(
|
||||||
|
{
|
||||||
|
...node,
|
||||||
|
parent: parentId,
|
||||||
|
},
|
||||||
|
prevId
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeleteAction = (node: Node) => {
|
||||||
|
return {
|
||||||
|
action: BlockActionTypePB.Delete,
|
||||||
|
payload: this.getActionPayloadByNode(node, ''),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
dispose = async () => {
|
dispose = async () => {
|
||||||
await this.backendService.close();
|
await this.backendService.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getActionPayloadByNode = (node: Node, prevId: string | null) => {
|
||||||
|
return {
|
||||||
|
block: this.getBlockByNode(node),
|
||||||
|
parent_id: node.parent || '',
|
||||||
|
prev_id: prevId || '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private getBlockByNode = (node: Node) => {
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
parent_id: node.parent || '',
|
||||||
|
children_id: node.children,
|
||||||
|
data: JSON.stringify(node.data),
|
||||||
|
ty: node.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
private updated = (payload: Uint8Array) => {
|
private updated = (payload: Uint8Array) => {
|
||||||
console.log('didReceiveUpdate', payload);
|
const dispatch = this.dispatch;
|
||||||
|
if (!dispatch) return;
|
||||||
|
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
||||||
|
console.log('updated', events, is_remote);
|
||||||
|
if (!is_remote) return;
|
||||||
|
events.forEach((event) => {
|
||||||
|
event.event.forEach((_payload) => {
|
||||||
|
const { path, id, value, command } = _payload;
|
||||||
|
let valueJson;
|
||||||
|
try {
|
||||||
|
valueJson = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
console.error('json parse error', value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!valueJson) return;
|
||||||
|
|
||||||
|
if (command === DeltaTypePB.Inserted || command === DeltaTypePB.Updated) {
|
||||||
|
// set map key and value ( block map or children map)
|
||||||
|
if (path[0] === 'blocks') {
|
||||||
|
const block = blockChangeValue2Node(valueJson);
|
||||||
|
dispatch(documentActions.setBlockMap(block));
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
documentActions.setChildrenMap({
|
||||||
|
id,
|
||||||
|
childIds: valueJson,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// remove map key ( block map or children map)
|
||||||
|
if (path[0] === 'blocks') {
|
||||||
|
dispatch(documentActions.removeBlockMapKey(id));
|
||||||
|
} else {
|
||||||
|
dispatch(documentActions.removeChildrenMapKey(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blockChangeValue2Node(value: { id: string; ty: string; parent: string; children: string; data: string }): Node {
|
||||||
|
const block = {
|
||||||
|
id: value.id,
|
||||||
|
type: value.ty as BlockType,
|
||||||
|
parent: value.parent,
|
||||||
|
children: value.children,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
if ('data' in value && typeof value.data === 'string') {
|
||||||
|
try {
|
||||||
|
Object.assign(block, {
|
||||||
|
data: JSON.parse(value.data),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
Log.error('valueJson data parse error', block.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { BlockType } from '@/appflowy_app/interfaces/document';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
import { outdentNodeThunk } from './outdent';
|
||||||
|
|
||||||
|
const composeParentThunk = createAsyncThunk(
|
||||||
|
'document/composeParent',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
if (!node.parent) return;
|
||||||
|
const parent = state.nodes[node.parent];
|
||||||
|
// merge delta
|
||||||
|
const newParent = {
|
||||||
|
...parent,
|
||||||
|
data: {
|
||||||
|
...parent.data,
|
||||||
|
delta: [...parent.data.delta, ...node.data.delta],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await controller.applyActions([controller.getDeleteAction(node), controller.getUpdateAction(newParent)]);
|
||||||
|
|
||||||
|
dispatch(documentActions.setBlockMap(newParent));
|
||||||
|
dispatch(documentActions.removeBlockMapKey(node.id));
|
||||||
|
dispatch(documentActions.removeChildrenMapKey(node.children));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const composePrevNodeThunk = createAsyncThunk(
|
||||||
|
'document/composePrevNode',
|
||||||
|
async (payload: { prevNodeId: string; id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, prevNodeId, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
const prevNode = state.nodes[prevNodeId];
|
||||||
|
// find prev line
|
||||||
|
let prevLineId = prevNode.id;
|
||||||
|
while (prevLineId) {
|
||||||
|
const prevLineChildren = state.children[state.nodes[prevLineId].children];
|
||||||
|
if (prevLineChildren.length === 0) break;
|
||||||
|
prevLineId = prevLineChildren[prevLineChildren.length - 1];
|
||||||
|
}
|
||||||
|
const prevLine = state.nodes[prevLineId];
|
||||||
|
// merge delta
|
||||||
|
const newPrevLine = {
|
||||||
|
...prevLine,
|
||||||
|
data: {
|
||||||
|
...prevLine.data,
|
||||||
|
delta: [...prevLine.data.delta, ...node.data.delta],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await controller.applyActions([controller.getDeleteAction(node), controller.getUpdateAction(newPrevLine)]);
|
||||||
|
|
||||||
|
dispatch(documentActions.setBlockMap(newPrevLine));
|
||||||
|
dispatch(documentActions.removeBlockMapKey(node.id));
|
||||||
|
dispatch(documentActions.removeChildrenMapKey(node.children));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const backspaceNodeThunk = createAsyncThunk(
|
||||||
|
'document/backspaceNode',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
if (!node.parent) return;
|
||||||
|
const parent = state.nodes[node.parent];
|
||||||
|
const ancestorId = parent.parent;
|
||||||
|
const children = state.children[parent.children];
|
||||||
|
const index = children.indexOf(id);
|
||||||
|
const prevNodeId = children[index - 1];
|
||||||
|
const nextNodeId = children[index + 1];
|
||||||
|
// transform to text block
|
||||||
|
if (node.type !== BlockType.TextBlock) {
|
||||||
|
// todo: transform to text block
|
||||||
|
}
|
||||||
|
// compose to previous line when it has next sibling or no ancestor
|
||||||
|
if (nextNodeId || !ancestorId) {
|
||||||
|
// compose to parent when it has no previous sibling
|
||||||
|
if (!prevNodeId) {
|
||||||
|
await dispatch(composeParentThunk({ id, controller }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dispatch(composePrevNodeThunk({ prevNodeId, id, controller }));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// outdent when it has no next sibling
|
||||||
|
await dispatch(outdentNodeThunk({ id, controller }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,32 @@
|
|||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
|
||||||
|
export const deleteNodeThunk = createAsyncThunk(
|
||||||
|
'document/deleteNode',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = getState() as { document: DocumentState };
|
||||||
|
const node = state.document.nodes[id];
|
||||||
|
if (!node) return;
|
||||||
|
await controller.applyActions([controller.getDeleteAction(node)]);
|
||||||
|
|
||||||
|
const deleteNode = (deleteId: string) => {
|
||||||
|
const deleteItem = state.document.nodes[deleteId];
|
||||||
|
const children = state.document.children[deleteItem.children];
|
||||||
|
// delete children
|
||||||
|
if (children.length > 0) {
|
||||||
|
children.forEach((childId) => {
|
||||||
|
deleteNode(childId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispatch(documentActions.removeBlockMapKey(deleteItem.id));
|
||||||
|
dispatch(documentActions.removeChildrenMapKey(deleteItem.children));
|
||||||
|
};
|
||||||
|
deleteNode(node.id);
|
||||||
|
|
||||||
|
if (!node.parent) return;
|
||||||
|
dispatch(documentActions.deleteChild({ id: node.parent, childId: node.id }));
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,37 @@
|
|||||||
|
import { BlockType } from '@/appflowy_app/interfaces/document';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
|
||||||
|
export const indentNodeThunk = createAsyncThunk(
|
||||||
|
'document/indentNode',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
if (!node.parent) return;
|
||||||
|
// get parent
|
||||||
|
const parent = state.nodes[node.parent];
|
||||||
|
// get prev node
|
||||||
|
const children = state.children[parent.children];
|
||||||
|
const index = children.indexOf(id);
|
||||||
|
if (index === 0) return;
|
||||||
|
const newParentId = children[index - 1];
|
||||||
|
const prevNode = state.nodes[newParentId];
|
||||||
|
// check if prev node is allowed to have children
|
||||||
|
if (prevNode.type !== BlockType.TextBlock) return;
|
||||||
|
// check if prev node has children and get last child for new prev node
|
||||||
|
const prevNodeChildren = state.children[prevNode.children];
|
||||||
|
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
||||||
|
|
||||||
|
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
||||||
|
dispatch(
|
||||||
|
documentActions.moveNode({
|
||||||
|
id,
|
||||||
|
newParentId,
|
||||||
|
newPrevId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,6 @@
|
|||||||
|
export * from './delete';
|
||||||
|
export * from './indent';
|
||||||
|
export * from './insert';
|
||||||
|
export * from './backspace';
|
||||||
|
export * from './outdent';
|
||||||
|
export * from './split';
|
@ -0,0 +1,41 @@
|
|||||||
|
import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
import { generateId } from '@/appflowy_app/utils/block';
|
||||||
|
export const insertAfterNodeThunk = createAsyncThunk(
|
||||||
|
'document/insertAfterNode',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = getState() as { document: DocumentState };
|
||||||
|
const node = state.document.nodes[payload.id];
|
||||||
|
if (!node) return;
|
||||||
|
const parentId = node.parent;
|
||||||
|
if (!parentId) return;
|
||||||
|
// create new node
|
||||||
|
const newNode: NestedBlock = {
|
||||||
|
id: generateId(),
|
||||||
|
parent: parentId,
|
||||||
|
type: BlockType.TextBlock,
|
||||||
|
data: {},
|
||||||
|
children: generateId(),
|
||||||
|
};
|
||||||
|
await controller.applyActions([controller.getInsertAction(newNode, node.id)]);
|
||||||
|
dispatch(documentActions.setBlockMap(newNode));
|
||||||
|
dispatch(
|
||||||
|
documentActions.setChildrenMap({
|
||||||
|
id: newNode.children,
|
||||||
|
childIds: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// insert new node to parent
|
||||||
|
dispatch(
|
||||||
|
documentActions.insertChild({
|
||||||
|
id: parentId,
|
||||||
|
childId: newNode.id,
|
||||||
|
prevId: node.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,26 @@
|
|||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
|
||||||
|
export const outdentNodeThunk = createAsyncThunk(
|
||||||
|
'document/outdentNode',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
const newPrevId = node.parent;
|
||||||
|
if (!newPrevId) return;
|
||||||
|
const parent = state.nodes[newPrevId];
|
||||||
|
const newParentId = parent.parent;
|
||||||
|
if (!newParentId) return;
|
||||||
|
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
||||||
|
dispatch(
|
||||||
|
documentActions.moveNode({
|
||||||
|
id: node.id,
|
||||||
|
newParentId,
|
||||||
|
newPrevId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,54 @@
|
|||||||
|
import { BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||||
|
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { generateId } from '@/appflowy_app/utils/block';
|
||||||
|
import { documentActions, DocumentState } from '../slice';
|
||||||
|
|
||||||
|
export const splitNodeThunk = createAsyncThunk(
|
||||||
|
'document/splitNode',
|
||||||
|
async (
|
||||||
|
payload: { id: string; retain: TextDelta[]; insert: TextDelta[]; controller: DocumentController },
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { id, controller, retain, insert } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
if (!node.parent) return;
|
||||||
|
const children = state.children[node.children];
|
||||||
|
const prevId = children.length > 0 ? null : node.id;
|
||||||
|
const parent = children.length > 0 ? node : state.nodes[node.parent];
|
||||||
|
const newNode = {
|
||||||
|
id: generateId(),
|
||||||
|
parent: parent.id,
|
||||||
|
type: BlockType.TextBlock,
|
||||||
|
data: {
|
||||||
|
delta: insert,
|
||||||
|
},
|
||||||
|
children: generateId(),
|
||||||
|
};
|
||||||
|
const retainNode = {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
delta: retain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await controller.applyActions([controller.getInsertAction(newNode, prevId), controller.getUpdateAction(retainNode)]);
|
||||||
|
dispatch(documentActions.setBlockMap(newNode));
|
||||||
|
dispatch(documentActions.setBlockMap(retainNode));
|
||||||
|
dispatch(
|
||||||
|
documentActions.setChildrenMap({
|
||||||
|
id: newNode.children,
|
||||||
|
childIds: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
documentActions.insertChild({
|
||||||
|
id: parent.id,
|
||||||
|
childId: newNode.id,
|
||||||
|
prevId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -55,7 +55,7 @@ export class RegionGrid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeBlock(blockId: string) {
|
removeBlock(blockId: string) {
|
||||||
for (const rows of this.regions) {
|
for (const rows of this.regions.filter(r => r)) {
|
||||||
for (const region of rows) {
|
for (const region of rows) {
|
||||||
if (!region) return;
|
if (!region) return;
|
||||||
const blockIndex = region.blocks.findIndex(b => b.id === blockId);
|
const blockIndex = region.blocks.findIndex(b => b.id === blockId);
|
||||||
|
@ -1,31 +1,51 @@
|
|||||||
import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
|
import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { DocumentController } from '../../effects/document/document_controller';
|
||||||
import { RegionGrid } from './region_grid';
|
import { RegionGrid } from './region_grid';
|
||||||
|
|
||||||
export type Node = NestedBlock;
|
export type Node = NestedBlock;
|
||||||
|
|
||||||
export interface NodeState {
|
export interface SelectionPoint {
|
||||||
|
path: [number, number];
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextSelection {
|
||||||
|
anchor: SelectionPoint;
|
||||||
|
focus: SelectionPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentState {
|
||||||
|
// map of block id to block
|
||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
|
// map of block id to children block ids
|
||||||
children: Record<string, string[]>;
|
children: Record<string, string[]>;
|
||||||
|
// selected block ids
|
||||||
selections: string[];
|
selections: string[];
|
||||||
|
// map of block id to text selection
|
||||||
|
textSelections: Record<string, TextSelection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regionGrid = new RegionGrid(50);
|
const regionGrid = new RegionGrid(50);
|
||||||
|
|
||||||
const initialState: NodeState = {
|
const initialState: DocumentState = {
|
||||||
nodes: {},
|
nodes: {},
|
||||||
children: {},
|
children: {},
|
||||||
selections: [],
|
selections: [],
|
||||||
|
textSelections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const documentSlice = createSlice({
|
export const documentSlice = createSlice({
|
||||||
name: 'document',
|
name: 'document',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
// initialize the document
|
||||||
clear: () => {
|
clear: () => {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// set document data
|
||||||
create: (
|
create: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@ -38,10 +58,18 @@ export const documentSlice = createSlice({
|
|||||||
state.children = children;
|
state.children = children;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// update block selections
|
||||||
updateSelections: (state, action: PayloadAction<string[]>) => {
|
updateSelections: (state, action: PayloadAction<string[]>) => {
|
||||||
state.selections = action.payload;
|
state.selections = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// set block selected
|
||||||
|
setSelectionById: (state, action: PayloadAction<string>) => {
|
||||||
|
const id = action.payload;
|
||||||
|
state.selections = [id];
|
||||||
|
},
|
||||||
|
|
||||||
|
// set block selected by selection rect
|
||||||
setSelectionByRect: (
|
setSelectionByRect: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@ -56,6 +84,7 @@ export const documentSlice = createSlice({
|
|||||||
state.selections = blocks.map((block) => block.id);
|
state.selections = blocks.map((block) => block.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// update block position
|
||||||
updateNodePosition: (
|
updateNodePosition: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@ -76,50 +105,85 @@ export const documentSlice = createSlice({
|
|||||||
regionGrid.updateBlock(id, position);
|
regionGrid.updateBlock(id, position);
|
||||||
},
|
},
|
||||||
|
|
||||||
addNode: (state, action: PayloadAction<Node>) => {
|
// update text selections
|
||||||
state.nodes[action.payload.id] = action.payload;
|
setTextSelection: (
|
||||||
},
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
addChild: (state, action: PayloadAction<{ parentId: string; childId: string; prevId: string }>) => {
|
blockId: string;
|
||||||
const { parentId, childId, prevId } = action.payload;
|
selection?: TextSelection;
|
||||||
const parentChildrenId = state.nodes[parentId].children;
|
}>
|
||||||
const children = state.children[parentChildrenId];
|
) => {
|
||||||
const prevIndex = children.indexOf(prevId);
|
const { blockId, selection } = action.payload;
|
||||||
if (prevIndex === -1) {
|
if (!selection) {
|
||||||
children.push(childId);
|
delete state.textSelections[blockId];
|
||||||
} else {
|
} else {
|
||||||
children.splice(prevIndex + 1, 0, childId);
|
state.textSelections = {
|
||||||
|
[blockId]: selection,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => {
|
// update block
|
||||||
|
setBlockMap: (state, action: PayloadAction<Node>) => {
|
||||||
|
state.nodes[action.payload.id] = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
// remove block
|
||||||
|
removeBlockMapKey(state, action: PayloadAction<string>) {
|
||||||
|
if (!state.nodes[action.payload]) return;
|
||||||
|
const { id } = state.nodes[action.payload];
|
||||||
|
regionGrid.removeBlock(id);
|
||||||
|
delete state.nodes[id];
|
||||||
|
},
|
||||||
|
|
||||||
|
// set block's relationship with its children
|
||||||
|
setChildrenMap: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => {
|
||||||
const { id, childIds } = action.payload;
|
const { id, childIds } = action.payload;
|
||||||
state.children[id] = childIds;
|
state.children[id] = childIds;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNode: (state, action: PayloadAction<{ id: string; data: any }>) => {
|
// remove block's relationship with its children
|
||||||
state.nodes[action.payload.id] = {
|
removeChildrenMapKey(state, action: PayloadAction<string>) {
|
||||||
...state.nodes[action.payload.id],
|
if (state.children[action.payload]) {
|
||||||
...action.payload,
|
delete state.children[action.payload];
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNode: (state, action: PayloadAction<string>) => {
|
// set block's relationship with its parent
|
||||||
const { children, data, parent } = state.nodes[action.payload];
|
insertChild: (state, action: PayloadAction<{ id: string; childId: string; prevId: string | null }>) => {
|
||||||
// remove from parent
|
const { id, childId, prevId } = action.payload;
|
||||||
if (parent) {
|
const parent = state.nodes[id];
|
||||||
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
|
const children = state.children[parent.children];
|
||||||
if (index > -1) {
|
const index = prevId ? children.indexOf(prevId) + 1 : 0;
|
||||||
state.children[state.nodes[parent].children].splice(index, 1);
|
children.splice(index, 0, childId);
|
||||||
}
|
},
|
||||||
}
|
|
||||||
// remove children
|
|
||||||
if (children) {
|
|
||||||
delete state.children[children];
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove node
|
// remove block's relationship with its parent
|
||||||
delete state.nodes[action.payload];
|
deleteChild: (state, action: PayloadAction<{ id: string; childId: string }>) => {
|
||||||
|
const { id, childId } = action.payload;
|
||||||
|
const parent = state.nodes[id];
|
||||||
|
const children = state.children[parent.children];
|
||||||
|
const index = children.indexOf(childId);
|
||||||
|
children.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
// move block to another parent
|
||||||
|
moveNode: (state, action: PayloadAction<{ id: string; newParentId: string; newPrevId: string | null }>) => {
|
||||||
|
const { id, newParentId, newPrevId } = action.payload;
|
||||||
|
const newParent = state.nodes[newParentId];
|
||||||
|
const oldParentId = state.nodes[id].parent;
|
||||||
|
if (!oldParentId) return;
|
||||||
|
const oldParent = state.nodes[oldParentId];
|
||||||
|
|
||||||
|
state.nodes[id] = {
|
||||||
|
...state.nodes[id],
|
||||||
|
parent: newParentId,
|
||||||
|
};
|
||||||
|
const index = state.children[oldParent.children].indexOf(id);
|
||||||
|
state.children[oldParent.children].splice(index, 1);
|
||||||
|
|
||||||
|
const newIndex = newPrevId ? state.children[newParent.children].indexOf(newPrevId) + 1 : 0;
|
||||||
|
state.children[newParent.children].splice(newIndex, 0, id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
5
frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
Normal file
5
frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
export function generateId() {
|
||||||
|
return nanoid(10);
|
||||||
|
}
|
@ -1,39 +1,41 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
|
||||||
DocumentEventGetDocument,
|
|
||||||
DocumentVersionPB,
|
|
||||||
OpenDocumentPayloadPB,
|
|
||||||
} from '../../services/backend/events/flowy-document';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { DocumentData } from '../interfaces/document';
|
import { DocumentData } from '../interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { useAppDispatch } from '../stores/store';
|
||||||
|
import { Log } from '../utils/log';
|
||||||
|
|
||||||
export const useDocument = () => {
|
export const useDocument = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [ documentId, setDocumentId ] = useState<string>();
|
const [documentId, setDocumentId] = useState<string>();
|
||||||
const [ documentData, setDocumentData ] = useState<DocumentData>();
|
const [documentData, setDocumentData] = useState<DocumentData>();
|
||||||
const [ controller, setController ] = useState<DocumentController | null>(null);
|
const [controller, setController] = useState<DocumentController | null>(null);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let documentController: DocumentController | null = null;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!params?.id) return;
|
if (!params?.id) return;
|
||||||
const c = new DocumentController(params.id);
|
Log.debug('open document', params.id);
|
||||||
setController(c);
|
documentController = new DocumentController(params.id, dispatch);
|
||||||
|
setController(documentController);
|
||||||
try {
|
try {
|
||||||
const res = await c.open();
|
const res = await documentController.open();
|
||||||
console.log(res)
|
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
setDocumentData(res);
|
setDocumentData(res);
|
||||||
setDocumentId(params.id);
|
setDocumentId(params.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
Log.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
console.log('==== leave ====', params?.id)
|
void (async () => {
|
||||||
}
|
if (documentController) {
|
||||||
|
await documentController.dispose();
|
||||||
|
}
|
||||||
|
Log.debug('close document', params.id);
|
||||||
|
})();
|
||||||
|
};
|
||||||
}, [params.id]);
|
}, [params.id]);
|
||||||
return { documentId, documentData, controller };
|
return { documentId, documentData, controller };
|
||||||
};
|
};
|
||||||
|
22
frontend/rust-lib/Cargo.lock
generated
22
frontend/rust-lib/Cargo.lock
generated
@ -1427,7 +1427,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-subscriber 0.3.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2710,6 +2713,16 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@ -2830,6 +2843,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -4434,12 +4453,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
|
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers 0.1.0",
|
"matchers 0.1.0",
|
||||||
|
"nu-ansi-term",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -92,6 +92,8 @@ fn create_log_filter(level: String, with_crates: Vec<String>) -> String {
|
|||||||
filters.push(format!("flowy_folder={}", level));
|
filters.push(format!("flowy_folder={}", level));
|
||||||
filters.push(format!("flowy_folder2={}", level));
|
filters.push(format!("flowy_folder2={}", level));
|
||||||
filters.push(format!("collab_folder={}", level));
|
filters.push(format!("collab_folder={}", level));
|
||||||
|
filters.push(format!("collab_persistence={}", level));
|
||||||
|
filters.push(format!("collab={}", level));
|
||||||
filters.push(format!("flowy_user={}", level));
|
filters.push(format!("flowy_user={}", level));
|
||||||
filters.push(format!("flowy_document={}", level));
|
filters.push(format!("flowy_document={}", level));
|
||||||
filters.push(format!("flowy_document2={}", level));
|
filters.push(format!("flowy_document2={}", level));
|
||||||
|
@ -25,10 +25,16 @@ strum_macros = "0.21"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = {version = "1.0"}
|
serde_json = {version = "1.0"}
|
||||||
tracing = { version = "0.1", features = ["log"] }
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
|
tokio = { version = "1.26", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.4.0"
|
||||||
|
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
flowy-codegen = { path = "../flowy-codegen"}
|
flowy-codegen = { path = "../flowy-codegen"}
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
|
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
|
||||||
ts = ["flowy-codegen/ts", "flowy-notification/ts"]
|
ts = ["flowy-codegen/ts", "flowy-notification/ts"]
|
||||||
|
|
||||||
|
@ -25,6 +25,12 @@ impl Document {
|
|||||||
.map_err(|_| FlowyError::from(ErrorCode::DocumentDataInvalid))?;
|
.map_err(|_| FlowyError::from(ErrorCode::DocumentDataInvalid))?;
|
||||||
Ok(Self(Arc::new(Mutex::new(inner))))
|
Ok(Self(Arc::new(Mutex::new(inner))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_with_data(collab: Collab, data: DocumentData) -> FlowyResult<Self> {
|
||||||
|
let inner = InnerDocument::create_with_data(collab, data)
|
||||||
|
.map_err(|_| FlowyError::from(ErrorCode::DocumentDataInvalid))?;
|
||||||
|
Ok(Self(Arc::new(Mutex::new(inner))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Sync for Document {}
|
unsafe impl Sync for Document {}
|
||||||
|
@ -23,7 +23,7 @@ pub struct CloseDocumentPayloadPBV2 {
|
|||||||
// Support customize initial data
|
// Support customize initial data
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct ApplyActionPayloadPBV2 {
|
pub struct ApplyActionPayloadPBV2 {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
@ -44,7 +44,7 @@ pub struct DocumentDataPB2 {
|
|||||||
pub meta: MetaPB,
|
pub meta: MetaPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct BlockPB {
|
pub struct BlockPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -75,7 +75,7 @@ pub struct ChildrenPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct BlockActionPB {
|
pub struct BlockActionPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub action: BlockActionTypePB,
|
pub action: BlockActionTypePB,
|
||||||
@ -84,7 +84,7 @@ pub struct BlockActionPB {
|
|||||||
pub payload: BlockActionPayloadPB,
|
pub payload: BlockActionPayloadPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct BlockActionPayloadPB {
|
pub struct BlockActionPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub block: BlockPB,
|
pub block: BlockPB,
|
||||||
@ -96,7 +96,7 @@ pub struct BlockActionPayloadPB {
|
|||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum)]
|
#[derive(ProtoBuf_Enum, Debug)]
|
||||||
pub enum BlockActionTypePB {
|
pub enum BlockActionTypePB {
|
||||||
Insert = 0,
|
Insert = 0,
|
||||||
Update = 1,
|
Update = 1,
|
||||||
@ -110,6 +110,18 @@ impl Default for BlockActionTypePB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf_Enum)]
|
||||||
|
pub enum DeltaTypePB {
|
||||||
|
Inserted = 0,
|
||||||
|
Updated = 1,
|
||||||
|
Removed = 2,
|
||||||
|
}
|
||||||
|
impl Default for DeltaTypePB {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Inserted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct DocEventPB {
|
pub struct DocEventPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
@ -122,8 +134,20 @@ pub struct DocEventPB {
|
|||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct BlockEventPB {
|
pub struct BlockEventPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub path: Vec<String>,
|
pub event: Vec<BlockEventPayloadPB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf)]
|
||||||
|
pub struct BlockEventPayloadPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub command: DeltaTypePB,
|
||||||
|
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub delta: String,
|
pub path: Vec<String>,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,15 @@ use crate::{
|
|||||||
document::DocumentDataWrapper,
|
document::DocumentDataWrapper,
|
||||||
entities::{
|
entities::{
|
||||||
ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
||||||
BlockPB, CloseDocumentPayloadPBV2, CreateDocumentPayloadPBV2, DocumentDataPB2,
|
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPBV2, CreateDocumentPayloadPBV2, DeltaTypePB,
|
||||||
OpenDocumentPayloadPBV2,
|
DocEventPB, DocumentDataPB2, OpenDocumentPayloadPBV2,
|
||||||
},
|
},
|
||||||
manager::DocumentManager,
|
manager::DocumentManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
use collab_document::blocks::{
|
use collab_document::blocks::{
|
||||||
json_str_to_hashmap, Block, BlockAction, BlockActionPayload, BlockActionType, BlockEvent,
|
json_str_to_hashmap, Block, BlockAction, BlockActionPayload, BlockActionType, BlockEvent,
|
||||||
|
BlockEventPayload, DeltaType,
|
||||||
};
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||||
@ -109,15 +110,39 @@ impl From<BlockPB> for Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<BlockEvent> for BlockEventPB {
|
impl From<BlockEvent> for BlockEventPB {
|
||||||
fn from(_block_event: BlockEvent) -> Self {
|
fn from(payload: BlockEvent) -> Self {
|
||||||
// let delta = serde_json::to_value(&block_event.delta).unwrap();
|
|
||||||
// Self {
|
|
||||||
// path: block_event.path.into(),
|
|
||||||
// delta: delta.to_string(),
|
|
||||||
// }
|
|
||||||
Self {
|
Self {
|
||||||
path: vec![],
|
event: payload.iter().map(|e| e.to_owned().into()).collect(),
|
||||||
delta: "".to_string(),
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BlockEventPayload> for BlockEventPayloadPB {
|
||||||
|
fn from(payload: BlockEventPayload) -> Self {
|
||||||
|
Self {
|
||||||
|
command: payload.command.into(),
|
||||||
|
path: payload.path,
|
||||||
|
id: payload.id,
|
||||||
|
value: payload.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeltaType> for DeltaTypePB {
|
||||||
|
fn from(action: DeltaType) -> Self {
|
||||||
|
match action {
|
||||||
|
DeltaType::Inserted => Self::Inserted,
|
||||||
|
DeltaType::Updated => Self::Updated,
|
||||||
|
DeltaType::Removed => Self::Removed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocEventPB {
|
||||||
|
pub(crate) fn get_from(events: &Vec<BlockEvent>, is_remote: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
events: events.iter().map(|e| e.to_owned().into()).collect(),
|
||||||
|
is_remote,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
pub mod document;
|
||||||
pub mod entities;
|
pub mod entities;
|
||||||
pub mod event_map;
|
pub mod event_map;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod protobuf;
|
pub mod protobuf;
|
||||||
|
|
||||||
mod document;
|
|
||||||
mod event_handler;
|
mod event_handler;
|
||||||
mod notification;
|
mod notification;
|
||||||
|
@ -7,7 +7,7 @@ use std::{collections::HashMap, sync::Arc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
document::{Document, DocumentDataWrapper},
|
document::{Document, DocumentDataWrapper},
|
||||||
entities::{BlockEventPB, DocEventPB},
|
entities::DocEventPB,
|
||||||
notification::{send_notification, DocumentNotification},
|
notification::{send_notification, DocumentNotification},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,25 +37,13 @@ impl DocumentManager {
|
|||||||
&self,
|
&self,
|
||||||
doc_id: String,
|
doc_id: String,
|
||||||
data: DocumentDataWrapper,
|
data: DocumentDataWrapper,
|
||||||
) -> FlowyResult<Arc<Document>> {
|
|
||||||
self.get_document(doc_id, Some(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_document(
|
|
||||||
&self,
|
|
||||||
doc_id: String,
|
|
||||||
data: Option<DocumentDataWrapper>,
|
|
||||||
) -> FlowyResult<Arc<Document>> {
|
) -> FlowyResult<Arc<Document>> {
|
||||||
let collab = self.get_collab_for_doc_id(&doc_id)?;
|
let collab = self.get_collab_for_doc_id(&doc_id)?;
|
||||||
let document = Arc::new(Document::new(collab)?);
|
let document = Arc::new(Document::create_with_data(collab, data.0)?);
|
||||||
self.documents.write().insert(doc_id, document.clone());
|
self
|
||||||
if data.is_some() {
|
.documents
|
||||||
// Here use unwrap() is safe, because we have checked data.is_some() before.
|
.write()
|
||||||
// document
|
.insert(doc_id.clone(), document.clone());
|
||||||
// .lock()
|
|
||||||
// .create_with_data(data.unwrap().0)
|
|
||||||
// .map_err(|err| FlowyError::internal().context(err))?;
|
|
||||||
}
|
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,25 +51,23 @@ impl DocumentManager {
|
|||||||
if let Some(doc) = self.documents.read().get(&doc_id) {
|
if let Some(doc) = self.documents.read().get(&doc_id) {
|
||||||
return Ok(doc.clone());
|
return Ok(doc.clone());
|
||||||
}
|
}
|
||||||
|
tracing::debug!("open_document: {:?}", &doc_id);
|
||||||
|
let collab = self.get_collab_for_doc_id(&doc_id)?;
|
||||||
|
let document = Arc::new(Document::new(collab)?);
|
||||||
|
|
||||||
let document = self.get_document(doc_id.clone(), None)?;
|
|
||||||
let clone_doc_id = doc_id.clone();
|
let clone_doc_id = doc_id.clone();
|
||||||
let _document_data = document
|
document
|
||||||
.lock()
|
.lock()
|
||||||
.open(move |events, is_remote| {
|
.open(move |events, is_remote| {
|
||||||
println!("events: {:?}", events);
|
|
||||||
println!("is_remote: {:?}", is_remote);
|
|
||||||
send_notification(&clone_doc_id, DocumentNotification::DidReceiveUpdate)
|
send_notification(&clone_doc_id, DocumentNotification::DidReceiveUpdate)
|
||||||
.payload(DocEventPB {
|
.payload(DocEventPB::get_from(events, is_remote))
|
||||||
events: events
|
|
||||||
.iter()
|
|
||||||
.map(|event| event.to_owned().into())
|
|
||||||
.collect::<Vec<BlockEventPB>>(),
|
|
||||||
is_remote: is_remote.to_owned(),
|
|
||||||
})
|
|
||||||
.send();
|
.send();
|
||||||
})
|
})
|
||||||
.map_err(|err| FlowyError::internal().context(err))?;
|
.map_err(|err| FlowyError::internal().context(err))?;
|
||||||
|
self
|
||||||
|
.documents
|
||||||
|
.write()
|
||||||
|
.insert(doc_id.clone(), document.clone());
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,210 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc, vec};
|
||||||
|
|
||||||
|
use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
|
||||||
|
use flowy_document2::{document::DocumentDataWrapper, manager::DocumentManager};
|
||||||
|
use nanoid::nanoid;
|
||||||
|
use serde_json::{json, to_value, Value};
|
||||||
|
|
||||||
|
use super::util::FakeUser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restore_document() {
|
||||||
|
let user = FakeUser::new();
|
||||||
|
let manager = DocumentManager::new(Arc::new(user));
|
||||||
|
|
||||||
|
// create a document
|
||||||
|
let doc_id: String = nanoid!(10);
|
||||||
|
let data = DocumentDataWrapper::default();
|
||||||
|
let document_a = manager
|
||||||
|
.create_document(doc_id.clone(), data.clone())
|
||||||
|
.unwrap();
|
||||||
|
let data_a = document_a.lock().get_document().unwrap();
|
||||||
|
assert_eq!(data_a, data.0);
|
||||||
|
|
||||||
|
// open a document
|
||||||
|
let data_b = manager
|
||||||
|
.open_document(doc_id.clone())
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.get_document()
|
||||||
|
.unwrap();
|
||||||
|
// close a document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
assert_eq!(data_b, data.0);
|
||||||
|
|
||||||
|
// restore
|
||||||
|
_ = manager.create_document(doc_id.clone(), data.clone());
|
||||||
|
// open a document
|
||||||
|
let data_b = manager
|
||||||
|
.open_document(doc_id.clone())
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.get_document()
|
||||||
|
.unwrap();
|
||||||
|
// close a document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
|
||||||
|
assert_eq!(data_b, data.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_apply_insert_action() {
|
||||||
|
let user = FakeUser::new();
|
||||||
|
let manager = DocumentManager::new(Arc::new(user));
|
||||||
|
|
||||||
|
let doc_id: String = nanoid!(10);
|
||||||
|
let data = DocumentDataWrapper::default();
|
||||||
|
|
||||||
|
// create a document
|
||||||
|
_ = manager.create_document(doc_id.clone(), data.clone());
|
||||||
|
|
||||||
|
// open a document
|
||||||
|
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||||
|
let page_block = document.lock().get_block(&data.0.page_id).unwrap();
|
||||||
|
|
||||||
|
// insert a text block
|
||||||
|
let text_block = Block {
|
||||||
|
id: nanoid!(10),
|
||||||
|
ty: "text".to_string(),
|
||||||
|
parent: page_block.id.clone(),
|
||||||
|
children: nanoid!(10),
|
||||||
|
external_id: None,
|
||||||
|
external_type: None,
|
||||||
|
data: HashMap::new(),
|
||||||
|
};
|
||||||
|
let insert_text_action = BlockAction {
|
||||||
|
action: BlockActionType::Insert,
|
||||||
|
payload: BlockActionPayload {
|
||||||
|
block: text_block.clone(),
|
||||||
|
parent_id: None,
|
||||||
|
prev_id: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
document.lock().apply_action(vec![insert_text_action]);
|
||||||
|
let data_a = document.lock().get_document().unwrap();
|
||||||
|
// close the original document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
|
||||||
|
// re-open the document
|
||||||
|
let data_b = manager
|
||||||
|
.open_document(doc_id.clone())
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.get_document()
|
||||||
|
.unwrap();
|
||||||
|
// close a document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
|
||||||
|
assert_eq!(data_b, data_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_apply_update_page_action() {
|
||||||
|
let user = FakeUser::new();
|
||||||
|
let manager = DocumentManager::new(Arc::new(user));
|
||||||
|
|
||||||
|
let doc_id: String = nanoid!(10);
|
||||||
|
let data = DocumentDataWrapper::default();
|
||||||
|
|
||||||
|
// create a document
|
||||||
|
_ = manager.create_document(doc_id.clone(), data.clone());
|
||||||
|
|
||||||
|
// open a document
|
||||||
|
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||||
|
let page_block = document.lock().get_block(&data.0.page_id).unwrap();
|
||||||
|
|
||||||
|
let mut page_block_clone = page_block.clone();
|
||||||
|
page_block_clone.data = HashMap::new();
|
||||||
|
page_block_clone.data.insert(
|
||||||
|
"delta".to_string(),
|
||||||
|
to_value(json!([{"insert": "Hello World!"}])).unwrap(),
|
||||||
|
);
|
||||||
|
let action = BlockAction {
|
||||||
|
action: BlockActionType::Update,
|
||||||
|
payload: BlockActionPayload {
|
||||||
|
block: page_block_clone,
|
||||||
|
parent_id: None,
|
||||||
|
prev_id: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let actions = vec![action];
|
||||||
|
tracing::trace!("{:?}", &actions);
|
||||||
|
document.lock().apply_action(actions);
|
||||||
|
let page_block_old = document.lock().get_block(&data.0.page_id).unwrap();
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
|
||||||
|
// re-open the document
|
||||||
|
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||||
|
let page_block_new = document.lock().get_block(&data.0.page_id).unwrap();
|
||||||
|
assert_eq!(page_block_old, page_block_new);
|
||||||
|
assert!(page_block_new.data.contains_key("delta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_apply_update_action() {
|
||||||
|
let user = FakeUser::new();
|
||||||
|
let manager = DocumentManager::new(Arc::new(user));
|
||||||
|
|
||||||
|
let doc_id: String = nanoid!(10);
|
||||||
|
let data = DocumentDataWrapper::default();
|
||||||
|
|
||||||
|
// create a document
|
||||||
|
_ = manager.create_document(doc_id.clone(), data.clone());
|
||||||
|
|
||||||
|
// open a document
|
||||||
|
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||||
|
let page_block = document.lock().get_block(&data.0.page_id).unwrap();
|
||||||
|
|
||||||
|
// insert a text block
|
||||||
|
let text_block_id = nanoid!(10);
|
||||||
|
let text_block = Block {
|
||||||
|
id: text_block_id.clone(),
|
||||||
|
ty: "text".to_string(),
|
||||||
|
parent: page_block.id.clone(),
|
||||||
|
children: nanoid!(10),
|
||||||
|
external_id: None,
|
||||||
|
external_type: None,
|
||||||
|
data: HashMap::new(),
|
||||||
|
};
|
||||||
|
let insert_text_action = BlockAction {
|
||||||
|
action: BlockActionType::Insert,
|
||||||
|
payload: BlockActionPayload {
|
||||||
|
block: text_block.clone(),
|
||||||
|
parent_id: None,
|
||||||
|
prev_id: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
document.lock().apply_action(vec![insert_text_action]);
|
||||||
|
|
||||||
|
// update the text block
|
||||||
|
let existing_text_block = document.lock().get_block(&text_block_id).unwrap();
|
||||||
|
let mut updated_text_block_data = HashMap::new();
|
||||||
|
updated_text_block_data.insert("delta".to_string(), Value::String("delta".to_string()));
|
||||||
|
let updated_text_block = Block {
|
||||||
|
id: existing_text_block.id,
|
||||||
|
ty: existing_text_block.ty,
|
||||||
|
parent: existing_text_block.parent,
|
||||||
|
children: existing_text_block.children,
|
||||||
|
external_id: None,
|
||||||
|
external_type: None,
|
||||||
|
data: updated_text_block_data.clone(),
|
||||||
|
};
|
||||||
|
let update_text_action = BlockAction {
|
||||||
|
action: BlockActionType::Update,
|
||||||
|
payload: BlockActionPayload {
|
||||||
|
block: updated_text_block.clone(),
|
||||||
|
parent_id: None,
|
||||||
|
prev_id: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
document.lock().apply_action(vec![update_text_action]);
|
||||||
|
// close the original document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
|
||||||
|
// re-open the document
|
||||||
|
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||||
|
let block = document.lock().get_block(&text_block_id).unwrap();
|
||||||
|
assert_eq!(block.data, updated_text_block_data);
|
||||||
|
// close a document
|
||||||
|
_ = manager.close_document(doc_id.clone());
|
||||||
|
}
|
2
frontend/rust-lib/flowy-document2/tests/document/mod.rs
Normal file
2
frontend/rust-lib/flowy-document2/tests/document/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod document_test;
|
||||||
|
mod util;
|
47
frontend/rust-lib/flowy-document2/tests/document/util.rs
Normal file
47
frontend/rust-lib/flowy-document2/tests/document/util.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use collab_persistence::CollabKV;
|
||||||
|
use flowy_document2::manager::DocumentUser;
|
||||||
|
use parking_lot::Once;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
|
pub struct FakeUser {
|
||||||
|
kv: Arc<CollabKV>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeUser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { kv: db() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentUser for FakeUser {
|
||||||
|
fn user_id(&self) -> Result<i64, flowy_error::FlowyError> {
|
||||||
|
Ok(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token(&self) -> Result<String, flowy_error::FlowyError> {
|
||||||
|
Ok("1".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kv_db(&self) -> Result<std::sync::Arc<CollabKV>, flowy_error::FlowyError> {
|
||||||
|
Ok(self.kv.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db() -> Arc<CollabKV> {
|
||||||
|
static START: Once = Once::new();
|
||||||
|
START.call_once(|| {
|
||||||
|
std::env::set_var("RUST_LOG", "collab_persistence=trace");
|
||||||
|
let subscriber = Subscriber::builder()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.with_ansi(true)
|
||||||
|
.finish();
|
||||||
|
subscriber.try_init().unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let tempdir = TempDir::new().unwrap();
|
||||||
|
let path = tempdir.into_path();
|
||||||
|
Arc::new(CollabKV::open(path).unwrap())
|
||||||
|
}
|
1
frontend/rust-lib/flowy-document2/tests/main.rs
Normal file
1
frontend/rust-lib/flowy-document2/tests/main.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
mod document;
|
@ -132,7 +132,6 @@ impl Folder2Manager {
|
|||||||
/// Called when the current user logout
|
/// Called when the current user logout
|
||||||
///
|
///
|
||||||
pub async fn clear(&self, _user_id: i64) {
|
pub async fn clear(&self, _user_id: i64) {
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult<Workspace> {
|
pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult<Workspace> {
|
||||||
|
Loading…
Reference in New Issue
Block a user