diff --git a/Cargo.lock b/Cargo.lock index 41bd37609..d5a8f416c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "client-api", "client-api-test", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", @@ -2085,7 +2086,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cf5c871191e5fbbaa58af5ab7cb037f792e28297#cf5c871191e5fbbaa58af5ab7cb037f792e28297" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" dependencies = [ "anyhow", "arc-swap", @@ -2107,10 +2108,39 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-database" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "collab", + "collab-entity", + "dashmap 5.5.3", + "futures", + "getrandom 0.2.15", + "js-sys", + "lazy_static", + "nanoid", + "rayon", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cf5c871191e5fbbaa58af5ab7cb037f792e28297#cf5c871191e5fbbaa58af5ab7cb037f792e28297" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" dependencies = [ "anyhow", "arc-swap", @@ -2130,7 +2160,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cf5c871191e5fbbaa58af5ab7cb037f792e28297#cf5c871191e5fbbaa58af5ab7cb037f792e28297" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" dependencies = [ "anyhow", "bytes", @@ -2149,7 +2179,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cf5c871191e5fbbaa58af5ab7cb037f792e28297#cf5c871191e5fbbaa58af5ab7cb037f792e28297" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" dependencies = [ "anyhow", "arc-swap", @@ -2233,7 +2263,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cf5c871191e5fbbaa58af5ab7cb037f792e28297#cf5c871191e5fbbaa58af5ab7cb037f792e28297" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af8ffdab672e4eccfc426525dcf8daa42cbe7087#af8ffdab672e4eccfc426525dcf8daa42cbe7087" dependencies = [ "anyhow", "collab", @@ -3480,6 +3510,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -5054,7 +5090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck", + "heck 0.5.0", "itertools 0.12.1", "log", "multimap", @@ -6516,7 +6552,7 @@ checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -6713,6 +6749,25 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.72", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8036,6 +8091,7 @@ dependencies = [ "async-trait", "bytes", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", diff --git a/Cargo.toml b/Cargo.toml index e0bad1470..7fc4ff90e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,7 @@ collab-document = { workspace = true } collab-entity = { workspace = true } collab-folder = { workspace = true } collab-user = { workspace = true } +collab-database = { workspace = true } collab-rt-protocol.workspace = true #Local crate @@ -269,6 +270,7 @@ collab = { version = "0.2.0" } collab-entity = { version = "0.2.0" } collab-folder = { version = "0.2.0" } collab-document = { version = "0.2.0" } +collab-database = { version = "0.2.0" } collab-user = { version = "0.2.0" } [profile.release] @@ -283,11 +285,12 @@ debug = true [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cf5c871191e5fbbaa58af5ab7cb037f792e28297" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cf5c871191e5fbbaa58af5ab7cb037f792e28297" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cf5c871191e5fbbaa58af5ab7cb037f792e28297" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cf5c871191e5fbbaa58af5ab7cb037f792e28297" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cf5c871191e5fbbaa58af5ab7cb037f792e28297" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af8ffdab672e4eccfc426525dcf8daa42cbe7087" } [features] history = [] diff --git a/libs/workspace-template/Cargo.toml b/libs/workspace-template/Cargo.toml index 435183ba7..cb00bde1d 100644 --- a/libs/workspace-template/Cargo.toml +++ b/libs/workspace-template/Cargo.toml @@ -12,6 +12,7 @@ bytes.workspace = true collab = { workspace = true } collab-folder = { workspace = true } collab-document = { workspace = true } +collab-database = { workspace = true } collab-entity = { workspace = true } async-trait = "0.1.77" anyhow.workspace = true @@ -22,5 +23,9 @@ serde_json.workspace = true nanoid = "0.4.0" serde = { version = "1.0.195", features = ["derive"] } +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } +serde_json = "1.0" + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/libs/workspace-template/assets/default_space.json b/libs/workspace-template/assets/default_space.json new file mode 100644 index 000000000..f0ab27555 --- /dev/null +++ b/libs/workspace-template/assets/default_space.json @@ -0,0 +1,4 @@ +{ + "type": "page", + "children": [{ "type": "paragraph", "data": { "delta": [] } }] +} diff --git a/libs/workspace-template/assets/desktop_guide.json b/libs/workspace-template/assets/desktop_guide.json new file mode 100644 index 000000000..d741c6d9c --- /dev/null +++ b/libs/workspace-template/assets/desktop_guide.json @@ -0,0 +1,279 @@ +{ + "type": "page", + "children": [ + { + "type": "heading", + "data": { + "level": 1, + "delta": [{ "insert": "AppFlowy Desktop on macOS, Windows, and Linux" }] + } + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "Download", + "attributes": { "href": "https://appflowy.io/download" } + }, + { "insert": " " } + ] + } + }, + { + "type": "heading", + "data": { "level": 3, "delta": [{ "insert": "Basics" }] } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [{ "insert": "Click anywhere and just start typing." }] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "insert": "Highlight", + "attributes": { "bg_color": "0x4dffeb3b" } + }, + { "insert": " any text, and use the editing menu to " }, + { "insert": "style", "attributes": { "italic": true } }, + { "insert": " " }, + { "insert": "your", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "writing", "attributes": { "underline": true } }, + { "insert": " " }, + { "insert": "however", "attributes": { "code": true } }, + { "insert": " you " }, + { "insert": "like.", "attributes": { "strikethrough": true } } + ] + } + }, + { + "type": "todo_list", + "children": [ + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Type " }, + { "insert": "/", "attributes": { "code": true } }, + { "insert": " followed by " }, + { "insert": "/bullet", "attributes": { "code": true } }, + { "insert": " or " }, + { "insert": "/num", "attributes": { "code": true } }, + { + "insert": " to create a list. ", + "attributes": { "code": false } + } + ] + } + } + ], + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "insert": "/", + "attributes": { "code": true, "font_color": "0xff00b5ff" } + }, + { "insert": " a menu will pop up. Select " }, + { + "insert": "different types", + "attributes": { "bg_color": "0x4d9c27b0" } + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": true, + "delta": [ + { "insert": "Click " }, + { "insert": "+ New Page ", "attributes": { "code": true } }, + { "insert": "button at the top of your sidebar to" }, + { + "insert": " quickly", + "attributes": { "font_color": "0xff9c27b0" } + }, + { "insert": " add a new" }, + { "insert": " page", "attributes": { "font_color": "0xff795548" } }, + { "insert": "." } + ] + } + }, + { + "type": "todo_list", + "children": [ + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Document" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Grid" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Kanban Board" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Calendar" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "AI Chat" }] } + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "insert": "or through import", + "attributes": { "code": false } + } + ] + } + } + ], + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "insert": "+", "attributes": { "code": true } }, + { + "insert": " next to any page title or space name in the sidebar to add a new page/subpage:" + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { + "type": "heading", + "data": { + "level": 3, + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "insert": "guide", + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + } + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "insert": "reference", + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + } + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "insert": "/code", "attributes": { "code": true } }, + { + "insert": " to insert a code block", + "attributes": { "code": false } + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "align": "center", + "level": 3, + "delta": [{ "insert": "Spaces" }] + } + }, + { + "type": "paragraph", + "data": { + "align": "center", + "delta": [ + { "insert": "Create multiple spaces to better organize your work" } + ] + } + }, + { + "type": "multi_image", + "data": { + "layout": 0, + "images": [ + { + "type": 1, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/desktop_guide_1.jpg?raw=true" + }, + { + "type": 1, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/desktop_guide_2.jpg?raw=true" + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] } + }, + { + "type": "quote", + "data": { + "delta": [ + { "insert": "Click " }, + { "insert": "?", "attributes": { "code": true } }, + { "insert": " at the bottom right for help and support." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } } + ] +} diff --git a/libs/workspace-template/assets/getting_started.json b/libs/workspace-template/assets/getting_started.json new file mode 100644 index 000000000..eea0a87c0 --- /dev/null +++ b/libs/workspace-template/assets/getting_started.json @@ -0,0 +1,151 @@ +{ + "type": "page", + "children": [ + { + "type": "heading", + "data": { "level": 2, "delta": [{ "insert": "Welcome to AppFlowy" }] } + }, + { + "type": "heading", + "data": { + "level": 4, + "delta": [ + { + "insert": "$", + "attributes": { + "mention": { + "page_id": "", + "type": "page" + }, + "bold": true + } + } + ] + } + }, + { + "type": "toggle_list", + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { "insert": " " }, + { + "insert": "link", + "attributes": { "href": "https://appflowy.io/download" } + } + ] + } + } + ], + "data": { + "collapsed": true, + "delta": [{ "insert": "Download for macOS, Windows, and Linux" }] + } + }, + { + "type": "heading", + "data": { + "level": 4, + "delta": [ + { + "insert": "$", + "attributes": { + "mention": { + "type": "page", + "page_id": "" + }, + "bold": true + } + } + ] + } + }, + { + "type": "heading", + "data": { + "level": 4, + "delta": [ + { + "insert": "$", + "attributes": { + "bold": true, + "mention": { + "page_id": "", + "type": "page" + } + } + }, + { "insert": "quick start", "attributes": { "code": true } } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "callout", + "data": { + "bgColor": "0x0", + "icon": "", + "delta": [ + { "insert": "Ask AI", "attributes": { "bold": true } }, + { + "insert": " powered by advanced AI models: chat, search, write, and much more ✨" + } + ] + } + }, + { "type": "divider" }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "❤️Love AppFlowy and open source? Follow our latest product updates:" + } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "insert": "Twitter", + "attributes": { "href": "https://twitter.com/appflowy" } + }, + { "insert": ": @appflowy" } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "insert": "Reddit", + "attributes": { "href": "https://www.reddit.com/r/AppFlowy/" } + }, + { "insert": ": r/appflowy" } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "insert": "Github", + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + } + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } } + ] +} diff --git a/libs/workspace-template/assets/inbox.json b/libs/workspace-template/assets/inbox.json new file mode 100644 index 000000000..99ccaae78 --- /dev/null +++ b/libs/workspace-template/assets/inbox.json @@ -0,0 +1,280 @@ +{ + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "inline_view_id": "a8739c21-ed9e-4e05-ad6c-29c467b4129e", + "views": [ + { + "id": "efc0db35-29db-4dc4-b48a-da46282809e5", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "name": "Grid", + "layout": 0, + "layout_settings": {}, + "filters": [], + "group_settings": [], + "sorts": [], + "row_orders": [ + { "id": "ea5da1fc-cb8f-4fd0-bef1-fb5627a9cc71", "height": 60 }, + { "id": "3e7fdb49-6ed2-4e99-b4dd-b90c8290771f", "height": 60 }, + { "id": "380ee34e-8be7-43ff-ac37-27a2f705c6e9", "height": 60 }, + { "id": "fffc8261-d736-4408-acaa-ead7164b5bc0", "height": 60 }, + { "id": "207f0ae1-61ee-4b3c-9745-dcbb1530a619", "height": 60 } + ], + "field_orders": [ + { "id": "phVRgL" }, + { "id": "SqwRg1" }, + { "id": "wdX8DG" }, + { "id": "KinVda" }, + { "id": "3AE6iK" } + ], + "field_settings": { + "wdX8DG": { "wrap": true, "visibility": 0, "width": 218 }, + "KinVda": { "visibility": 0 }, + "3AE6iK": { "visibility": 0 } + }, + "created_at": 0, + "modified_at": 1723799193 + }, + { + "id": "a8739c21-ed9e-4e05-ad6c-29c467b4129e", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "name": "Untitled", + "layout": 0, + "layout_settings": { + "1": { "collapse_hidden_groups": true, "hide_ungrouped_column": true } + }, + "filters": [], + "group_settings": [ + { + "groups": [ + { "id": "SqwRg1", "visible": true }, + { "visible": true, "id": "CEZD" }, + { "id": "TznH", "visible": true }, + { "visible": true, "id": "__n6" } + ], + "ty": 3, + "id": "g:gJ9y65", + "content": "", + "field_id": "SqwRg1" + } + ], + "sorts": [], + "row_orders": [ + { "id": "ea5da1fc-cb8f-4fd0-bef1-fb5627a9cc71", "height": 60 }, + { "id": "3e7fdb49-6ed2-4e99-b4dd-b90c8290771f", "height": 60 }, + { "id": "380ee34e-8be7-43ff-ac37-27a2f705c6e9", "height": 60 }, + { "id": "fffc8261-d736-4408-acaa-ead7164b5bc0", "height": 60 }, + { "id": "207f0ae1-61ee-4b3c-9745-dcbb1530a619", "height": 60 } + ], + "field_orders": [ + { "id": "phVRgL" }, + { "id": "SqwRg1" }, + { "id": "wdX8DG" }, + { "id": "KinVda" }, + { "id": "3AE6iK" } + ], + "field_settings": { + "3AE6iK": { "visibility": 2, "width": 150, "wrap": true }, + "SqwRg1": { "visibility": 1, "width": 150, "wrap": true }, + "wdX8DG": { "width": 150, "wrap": true, "visibility": 2 }, + "phVRgL": { "width": 150, "visibility": 0, "wrap": true }, + "KinVda": { "visibility": 1 } + }, + "created_at": 1723792464, + "modified_at": 1723799193 + } + ], + "fields": [ + { + "id": "phVRgL", + "name": "Description", + "field_type": 0, + "type_options": { "0": { "data": "" } }, + "is_primary": true + }, + { + "id": "SqwRg1", + "name": "Status", + "field_type": 3, + "type_options": { + "3": { + "content": "{\"options\":[{\"id\":\"CEZD\",\"name\":\"To Do\",\"color\":\"Purple\"},{\"id\":\"TznH\",\"name\":\"Doing\",\"color\":\"Orange\"},{\"id\":\"__n6\",\"name\":\"✅ Done\",\"color\":\"Yellow\"}],\"disable_color\":false}" + } + }, + "is_primary": false + }, + { + "id": "wdX8DG", + "name": "Multiselect", + "field_type": 4, + "type_options": { + "4": { + "content": "{\"options\":[{\"id\":\"4PDn\",\"name\":\"get things done\",\"color\":\"Purple\"},{\"id\":\"Bpyg\",\"name\":\"self-host\",\"color\":\"Blue\"},{\"id\":\"GOQj\",\"name\":\"open source\",\"color\":\"Aqua\"},{\"id\":\"BD-T\",\"name\":\"looks great\",\"color\":\"Green\"},{\"id\":\"6UxM\",\"name\":\"fast\",\"color\":\"Lime\"},{\"id\":\"g2Uq\",\"name\":\"Claude 3\",\"color\":\"Yellow\"},{\"id\":\"Tt-J\",\"name\":\"GPT-4o\",\"color\":\"Orange\"},{\"id\":\"5QDY\",\"name\":\"Q&A\",\"color\":\"LightPink\"},{\"id\":\"XYUx\",\"name\":\"news\",\"color\":\"Pink\"},{\"id\":\"hoZx\",\"name\":\"social\",\"color\":\"Purple\"}],\"disable_color\":false}" + }, + "0": { + "data": "", + "content": "{\"options\":[],\"disable_color\":false}" + } + }, + "is_primary": false + }, + { + "id": "KinVda", + "name": "Tasks", + "field_type": 7, + "type_options": { "7": {}, "0": { "data": "" } }, + "is_primary": false + }, + { + "id": "3AE6iK", + "name": "Last modified", + "field_type": 8, + "type_options": { + "0": { + "time_format": 1, + "include_time": true, + "field_type": 8, + "data": "", + "date_format": 3 + }, + "8": { + "field_type": 8, + "date_format": 3, + "include_time": true, + "time_format": 1 + } + }, + "is_primary": false + } + ], + "rows": [ + { + "id": "ea5da1fc-cb8f-4fd0-bef1-fb5627a9cc71", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "cells": { + "phVRgL": { + "field_type": 0, + "last_modified": 1723792576, + "created_at": 1723792501, + "data": "Follow us on Twitter @appflowy" + }, + "SqwRg1": { "field_type": 3, "data": "CEZD" }, + "wdX8DG": { + "field_type": 4, + "last_modified": 1723792957, + "data": "hoZx,XYUx", + "created_at": 1723792951 + } + }, + "height": 60, + "visibility": true, + "created_at": 1723792464, + "modified_at": 1723792957 + }, + { + "id": "3e7fdb49-6ed2-4e99-b4dd-b90c8290771f", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "cells": { + "phVRgL": { + "field_type": 0, + "data": "Try out AI Chat 💬", + "last_modified": 1723792781, + "created_at": 1723792584 + }, + "KinVda": { + "field_type": 7, + "data": "{\"options\":[{\"id\":\"HJlI\",\"name\":\"Create an AI chat \",\"color\":\"Purple\"},{\"id\":\"4Ik2\",\"name\":\"Ask a question about your pages\",\"color\":\"Purple\"}],\"selected_option_ids\":[\"HJlI\"]}", + "created_at": 1723793102, + "last_modified": 1723793133 + }, + "wdX8DG": { + "field_type": 4, + "created_at": 1723792994, + "data": "5QDY,Tt-J,g2Uq", + "last_modified": 1723793006 + }, + "SqwRg1": { "field_type": 3, "data": "CEZD" }, + "1li9Eo": { + "last_modified": 1723793216, + "field_type": 12, + "data": "Essayez le Chatbot Intelligence Artificielle 💬,À faire,Q&A,GPT-4o,Claude 3,Créer un chatbot ,Demander une question sur vos pages", + "created_at": 1723793216 + } + }, + "height": 60, + "visibility": true, + "created_at": 1723792464, + "modified_at": 1723793216 + }, + { + "id": "380ee34e-8be7-43ff-ac37-27a2f705c6e9", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "cells": { + "wdX8DG": { + "data": "BD-T,6UxM", + "created_at": 1723793015, + "field_type": 4, + "last_modified": 1723793037 + }, + "SqwRg1": { "data": "CEZD", "field_type": 3 }, + "phVRgL": { + "field_type": 0, + "created_at": 1723792611, + "last_modified": 1723792623, + "data": "Install AppFlowy Mobile" + } + }, + "height": 60, + "visibility": true, + "created_at": 1723792464, + "modified_at": 1723793037 + }, + { + "id": "fffc8261-d736-4408-acaa-ead7164b5bc0", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "cells": { + "phVRgL": { + "data": "Love AppFlowy and open source", + "last_modified": 1723792862, + "field_type": 0, + "created_at": 1723792815 + }, + "wdX8DG": { + "created_at": 1723793051, + "field_type": 4, + "last_modified": 1723793058, + "data": "GOQj,Bpyg" + }, + "SqwRg1": { "data": "TznH", "field_type": 3 } + }, + "height": 60, + "visibility": true, + "created_at": 1723792698, + "modified_at": 1723793058 + }, + { + "id": "207f0ae1-61ee-4b3c-9745-dcbb1530a619", + "database_id": "4e1b59a3-0767-43bc-86f2-8078dd6c0c49", + "cells": { + "wdX8DG": { + "field_type": 4, + "last_modified": 1723793067, + "data": "4PDn", + "created_at": 1723793067 + }, + "SqwRg1": { + "last_modified": 1723792881, + "data": "__n6", + "created_at": 1723792881, + "field_type": 3 + }, + "phVRgL": { + "field_type": 0, + "data": "Create an AppFlowy Cloud account" + } + }, + "height": 60, + "visibility": true, + "created_at": 1723792765, + "modified_at": 1723793067 + } + ] +} diff --git a/libs/workspace-template/assets/mobile_guide.json b/libs/workspace-template/assets/mobile_guide.json new file mode 100644 index 000000000..70b2de3e6 --- /dev/null +++ b/libs/workspace-template/assets/mobile_guide.json @@ -0,0 +1,220 @@ +{ + "type": "page", + "children": [ + { + "type": "heading", + "data": { "level": 3, "delta": [{ "insert": "Adding content" }] } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "insert": "+", "attributes": { "code": true } }, + { "insert": " ", "attributes": { "code": false } }, + { "insert": "in the toolbar to add a block" } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Type " }, + { "insert": "-[]", "attributes": { "code": true } }, + { "insert": " to insert a to-do list" } + ] + } + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { "insert": "-", "attributes": { "code": true } }, + { "insert": " or " }, + { "insert": "*", "attributes": { "code": true } }, + { "insert": " for bulleted list" } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "1.", "attributes": { "code": true } }, + { "insert": " for numbered list" } + ] + } + }, + { + "type": "toggle_list", + "children": [ + { + "type": "paragraph", + "data": { "delta": [{ "insert": "Expand or collapse" }] } + } + ], + "data": { + "collapsed": true, + "delta": [ + { "insert": ">", "attributes": { "code": true } }, + { "insert": " for toggle list" } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { "level": 3, "delta": [{ "insert": "Styling" }] } + }, + { + "type": "paragraph", + "data": { + "delta": [ + { "insert": "Select text to " }, + { "insert": "style", "attributes": { "bg_color": "0xff6455a2" } }, + { "insert": " using the " }, + { + "insert": "toolbar", + "attributes": { "italic": true, "bold": true, "underline": true } + }, + { "insert": " menu" } + ] + } + }, + { + "type": "paragraph", + "data": { + "delta": [ + { "insert": "More styling can be found in " }, + { "insert": "Aa", "attributes": { "code": true } } + ] + } + }, + { + "type": "quote", + "data": { + "delta": [ + { + "insert": "AppFlowy allows you to style your content in a beautiful and simple way" + } + ] + } + }, + { + "type": "image", + "data": { + "width": 369.44921875, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/mobile_guide_1.png?raw=true", + "align": "left" + } + }, + { "type": "divider" }, + { + "type": "heading", + "data": { "level": 3, "delta": [{ "insert": "Database views" }] } + }, + { + "type": "callout", + "data": { + "icon": "📌", + "delta": [ + { + "insert": "Visualize your content in different ways — Kanban, Grid, and Calendar views" + } + ] + } + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "Click on a database record (row/card) to open the Card view" + } + ] + } + }, + { + "type": "multi_image", + "data": { + "layout": 0, + "images": [ + { + "type": 1, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/mobile_guide_2.png?raw=true" + }, + { + "type": 1, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/mobile_guide_3.png?raw=true" + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "In a Grid view, tap on a property name to edit the property.", + "attributes": { "bold": false } + } + ] + } + }, + { + "type": "image", + "data": { + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/mobile_guide_4.png?raw=true", + "width": 342, + "align": "left" + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { "level": 3, "delta": [{ "insert": "Manage pages" }] } + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "Swipe left on a page to show the actions: add a subpage and more actions" + } + ] + } + }, + { + "type": "image", + "data": { + "align": "left", + "width": 342, + "url": "https://github.com/AppFlowy-IO/AppFlowy/blob/main/doc/readme/mobile_guide_5.png?raw=true" + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "paragraph", + "data": { "delta": [{ "insert": "More actions include:" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Add to Favorites " }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Rename" }] } + }, + { + "type": "bulleted_list", + "data": { "delta": [{ "insert": "Duplicate " }] } + }, + { "type": "bulleted_list", "data": { "delta": [{ "insert": "Delete" }] } } + ] +} diff --git a/libs/workspace-template/assets/read_me.json b/libs/workspace-template/assets/read_me.json deleted file mode 100644 index 22924bc42..000000000 --- a/libs/workspace-template/assets/read_me.json +++ /dev/null @@ -1,223 +0,0 @@ -{ - "type": "page", - "data": { - "delta": [ - {"insert": ""} - ] - }, - "children": [ - { - "type": "heading", - "data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 } - }, - { - "type": "heading", - "data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 } - }, - { - "type": "todo_list", - "data": { - "delta": [{ "insert": "Click anywhere and just start typing." }], - "checked": false - } - }, - { - "type": "todo_list", - "data": { - "checked": false, - "delta": [ - { - "attributes": { "bg_color": "0x4dffeb3b" }, - "insert": "Highlight " - }, - { "insert": "any text, and use the editing menu to " }, - { "attributes": { "italic": true }, "insert": "style" }, - { "insert": " " }, - { "attributes": { "bold": true }, "insert": "your" }, - { "insert": " " }, - { "attributes": { "underline": true }, "insert": "writing" }, - { "insert": " " }, - { "attributes": { "code": true }, "insert": "however" }, - { "insert": " you " }, - { "attributes": { "strikethrough": true }, "insert": "like." } - ] - } - }, - { - "type": "todo_list", - "data": { - "checked": false, - "delta": [ - { "insert": "As soon as you type " }, - { - "attributes": { "code": true, "font_color": "0xff00b5ff" }, - "insert": "/" - }, - { "insert": " a menu will pop up. Select " }, - { - "attributes": { "bg_color": "0x4d9c27b0" }, - "insert": "different types" - }, - { "insert": " of content blocks you can add." } - ] - } - }, - { - "type": "todo_list", - "data": { - "delta": [ - { "insert": "Type " }, - { "attributes": { "code": true }, "insert": "/" }, - { "insert": " followed by " }, - { "attributes": { "code": true }, "insert": "/bullet" }, - { "insert": " or " }, - { "attributes": { "code": true }, "insert": "/num" }, - { "attributes": { "code": false }, "insert": " to create a list." } - ], - "checked": false - } - }, - { - "type": "todo_list", - "data": { - "delta": [ - { "insert": "Click " }, - { "attributes": { "code": true }, "insert": "+ New Page " }, - { - "insert": "button at the bottom of your sidebar to add a new page." - } - ], - "checked": true - } - }, - { - "type": "todo_list", - "data": { - "checked": false, - "delta": [ - { "insert": "Click " }, - { "attributes": { "code": true }, "insert": "+" }, - { "insert": " next to any page title in the sidebar to " }, - { - "attributes": { "font_color": "0xff8427e0" }, - "insert": "quickly" - }, - { "insert": " add a new subpage, " }, - { "attributes": { "code": true }, "insert": "Document" }, - { "attributes": { "code": false }, "insert": ", " }, - { "attributes": { "code": true }, "insert": "Grid" }, - { "attributes": { "code": false }, "insert": ", or " }, - { "attributes": { "code": true }, "insert": "Kanban Board" }, - { "attributes": { "code": false }, "insert": "." } - ] - } - }, - { "type": "paragraph", "data": { "delta": [] } }, - { "type": "divider" }, - { "type": "paragraph", "data": { "delta": [] } }, - { - "type": "heading", - "data": { - "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], - "level": 2 - } - }, - { - "type": "numbered_list", - "data": { - "delta": [ - { "insert": "Keyboard shortcuts " }, - { - "attributes": { - "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" - }, - "insert": "guide" - } - ] - } - }, - { - "type": "numbered_list", - "data": { - "delta": [ - { "insert": "Markdown " }, - { - "attributes": { - "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" - }, - "insert": "reference" - } - ] - } - }, - { - "type": "numbered_list", - "data": { - "delta": [ - { "insert": "Type " }, - { "attributes": { "code": true }, "insert": "/code" }, - { - "attributes": { "code": false }, - "insert": " to insert a code block" - } - ] - } - }, - { - "type": "code", - "data": { - "language": "rust", - "delta": [ - { - "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" - } - ] - } - }, - { "type": "paragraph", "data": { "delta": [] } }, - { - "type": "heading", - "data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] } - }, - { - "type": "quote", - "data": { - "delta": [ - { "insert": "Click " }, - { "attributes": { "code": true }, "insert": "?" }, - { "insert": " at the bottom right for help and support." } - ] - } - }, - { "type": "paragraph", "data": { "delta": [] } }, - { - "type": "callout", - "data": { - "delta": [ - { "insert": "\nLike AppFlowy? Follow us:\n" }, - { - "attributes": { - "href": "https://github.com/AppFlowy-IO/AppFlowy" - }, - "insert": "GitHub" - }, - { "insert": "\n" }, - { - "attributes": { "href": "https://twitter.com/appflowy" }, - "insert": "Twitter" - }, - { "insert": ": @appflowy\n" }, - { - "attributes": { "href": "https://blog-appflowy.ghost.io/" }, - "insert": "Newsletter" - }, - { "insert": "\n" } - ], - "icon": "🥰" - } - }, - { "type": "paragraph", "data": { "delta": [] } }, - { "type": "paragraph", "data": { "delta": [] } }, - { "type": "paragraph", "data": { "delta": [] } } - ] -} diff --git a/libs/workspace-template/assets/to-dos.json b/libs/workspace-template/assets/to-dos.json new file mode 100644 index 000000000..1e10ad867 --- /dev/null +++ b/libs/workspace-template/assets/to-dos.json @@ -0,0 +1,280 @@ +{ + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "inline_view_id": "010326bb-a6de-42c1-8037-7a0f825e029d", + "views": [ + { + "id": "010326bb-a6de-42c1-8037-7a0f825e029d", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "name": "Untitled", + "layout": 1, + "layout_settings": { + "1": { "collapse_hidden_groups": true, "hide_ungrouped_column": true } + }, + "filters": [], + "group_settings": [ + { + "field_id": "SqwRg1", + "id": "g:gJ9y65", + "content": "", + "ty": 3, + "groups": [ + { "visible": true, "id": "SqwRg1" }, + { "visible": true, "id": "CEZD" }, + { "visible": true, "id": "TznH" }, + { "id": "__n6", "visible": true } + ] + } + ], + "sorts": [], + "row_orders": [ + { "id": "0003a3cc-afd1-49ca-a75c-f360dd82ffe3", "height": 60 }, + { "id": "de94c0c4-6434-46ce-b0f8-7d3c0463e698", "height": 60 }, + { "id": "7ae3af92-efe0-4abb-b861-30f488d164c0", "height": 60 }, + { "id": "a8ae4baf-0eed-42c5-a2b7-1683c5a79da6", "height": 60 }, + { "id": "69ba4b59-d541-4131-a010-7a03f73ff088", "height": 60 } + ], + "field_orders": [ + { "id": "phVRgL" }, + { "id": "SqwRg1" }, + { "id": "wdX8DG" }, + { "id": "KinVda" }, + { "id": "3AE6iK" } + ], + "field_settings": { + "SqwRg1": { "wrap": true, "width": 150, "visibility": 1 }, + "wdX8DG": { "visibility": 2, "wrap": true, "width": 150 }, + "phVRgL": { "visibility": 0, "wrap": true, "width": 150 }, + "KinVda": { "visibility": 1 }, + "3AE6iK": { "visibility": 2, "width": 150, "wrap": true } + }, + "created_at": 1724830348, + "modified_at": 1724830348 + }, + { + "id": "0efd6c11-d38e-451c-9842-bc2322ad543f", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "name": "Grid", + "layout": 0, + "layout_settings": {}, + "filters": [], + "group_settings": [], + "sorts": [], + "row_orders": [ + { "id": "0003a3cc-afd1-49ca-a75c-f360dd82ffe3", "height": 60 }, + { "id": "de94c0c4-6434-46ce-b0f8-7d3c0463e698", "height": 60 }, + { "id": "7ae3af92-efe0-4abb-b861-30f488d164c0", "height": 60 }, + { "id": "a8ae4baf-0eed-42c5-a2b7-1683c5a79da6", "height": 60 }, + { "id": "69ba4b59-d541-4131-a010-7a03f73ff088", "height": 60 } + ], + "field_orders": [ + { "id": "phVRgL" }, + { "id": "SqwRg1" }, + { "id": "wdX8DG" }, + { "id": "KinVda" }, + { "id": "3AE6iK" } + ], + "field_settings": { + "KinVda": { "visibility": 0 }, + "wdX8DG": { "visibility": 0, "wrap": true, "width": 218 }, + "3AE6iK": { "visibility": 0 } + }, + "created_at": 1724830348, + "modified_at": 1724830348 + } + ], + "fields": [ + { + "id": "phVRgL", + "name": "Description", + "field_type": 0, + "type_options": { "0": { "data": "" } }, + "is_primary": true + }, + { + "id": "SqwRg1", + "name": "Status", + "field_type": 3, + "type_options": { + "3": { + "content": "{\"options\":[{\"id\":\"CEZD\",\"name\":\"To Do\",\"color\":\"Purple\"},{\"id\":\"TznH\",\"name\":\"Doing\",\"color\":\"Orange\"},{\"id\":\"__n6\",\"name\":\"✅ Done\",\"color\":\"Yellow\"}],\"disable_color\":false}" + } + }, + "is_primary": false + }, + { + "id": "wdX8DG", + "name": "Multiselect", + "field_type": 4, + "type_options": { + "0": { + "content": "{\"options\":[],\"disable_color\":false}", + "data": "" + }, + "4": { + "content": "{\"options\":[{\"id\":\"4PDn\",\"name\":\"get things done\",\"color\":\"Purple\"},{\"id\":\"Bpyg\",\"name\":\"self-host\",\"color\":\"Blue\"},{\"id\":\"GOQj\",\"name\":\"open source\",\"color\":\"Aqua\"},{\"id\":\"BD-T\",\"name\":\"looks great\",\"color\":\"Green\"},{\"id\":\"6UxM\",\"name\":\"fast\",\"color\":\"Lime\"},{\"id\":\"g2Uq\",\"name\":\"Claude 3\",\"color\":\"Yellow\"},{\"id\":\"Tt-J\",\"name\":\"GPT-4o\",\"color\":\"Orange\"},{\"id\":\"5QDY\",\"name\":\"Q&A\",\"color\":\"LightPink\"},{\"id\":\"XYUx\",\"name\":\"news\",\"color\":\"Pink\"},{\"id\":\"hoZx\",\"name\":\"social\",\"color\":\"Purple\"}],\"disable_color\":false}" + } + }, + "is_primary": false + }, + { + "id": "KinVda", + "name": "Tasks", + "field_type": 7, + "type_options": { "0": { "data": "" }, "7": {} }, + "is_primary": false + }, + { + "id": "3AE6iK", + "name": "Last modified", + "field_type": 8, + "type_options": { + "8": { + "time_format": 1, + "include_time": true, + "date_format": 3, + "field_type": 8 + }, + "0": { + "date_format": 3, + "include_time": true, + "field_type": 8, + "time_format": 1, + "data": "" + } + }, + "is_primary": false + } + ], + "rows": [ + { + "id": "0003a3cc-afd1-49ca-a75c-f360dd82ffe3", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "cells": { + "wdX8DG": { + "created_at": 1723792951, + "field_type": 4, + "last_modified": 1723792957, + "data": "hoZx,XYUx" + }, + "SqwRg1": { "field_type": 3, "data": "CEZD" }, + "phVRgL": { + "last_modified": 1723792576, + "field_type": 0, + "created_at": 1723792501, + "data": "Follow us on Twitter @appflowy" + } + }, + "height": 60, + "visibility": true, + "created_at": 1724830348, + "modified_at": 1724830348 + }, + { + "id": "de94c0c4-6434-46ce-b0f8-7d3c0463e698", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "cells": { + "wdX8DG": { + "data": "5QDY,Tt-J,g2Uq", + "last_modified": 1723793006, + "field_type": 4, + "created_at": 1723792994 + }, + "1li9Eo": { + "data": "Essayez le Chatbot Intelligence Artificielle 💬,À faire,Q&A,GPT-4o,Claude 3,Créer un chatbot ,Demander une question sur vos pages", + "created_at": 1723793216, + "field_type": 12, + "last_modified": 1723793216 + }, + "KinVda": { + "data": "{\"options\":[{\"id\":\"HJlI\",\"name\":\"Create an AI chat \",\"color\":\"Purple\"},{\"id\":\"4Ik2\",\"name\":\"Ask a question about your pages\",\"color\":\"Purple\"}],\"selected_option_ids\":[\"HJlI\"]}", + "created_at": 1723793102, + "field_type": 7, + "last_modified": 1723793133 + }, + "phVRgL": { + "created_at": 1723792584, + "data": "Try out AI Chat 💬", + "field_type": 0, + "last_modified": 1723792781 + }, + "SqwRg1": { "data": "CEZD", "field_type": 3 } + }, + "height": 60, + "visibility": true, + "created_at": 1724830348, + "modified_at": 1724830348 + }, + { + "id": "7ae3af92-efe0-4abb-b861-30f488d164c0", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "cells": { + "wdX8DG": { + "field_type": 4, + "created_at": 1723793015, + "last_modified": 1723793037, + "data": "BD-T,6UxM" + }, + "phVRgL": { + "last_modified": 1723792623, + "created_at": 1723792611, + "field_type": 0, + "data": "Install AppFlowy Mobile" + }, + "SqwRg1": { "field_type": 3, "data": "CEZD" } + }, + "height": 60, + "visibility": true, + "created_at": 1724830348, + "modified_at": 1724830348 + }, + { + "id": "a8ae4baf-0eed-42c5-a2b7-1683c5a79da6", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "cells": { + "phVRgL": { + "last_modified": 1723792862, + "data": "Love AppFlowy and open source", + "field_type": 0, + "created_at": 1723792815 + }, + "wdX8DG": { + "data": "GOQj,Bpyg", + "last_modified": 1723793058, + "field_type": 4, + "created_at": 1723793051 + }, + "SqwRg1": { "field_type": 3, "data": "TznH" } + }, + "height": 60, + "visibility": true, + "created_at": 1724830348, + "modified_at": 1724830348 + }, + { + "id": "69ba4b59-d541-4131-a010-7a03f73ff088", + "database_id": "57eb746e-9913-4bd3-a3f2-eb7aa40e21da", + "cells": { + "wdX8DG": { + "data": "4PDn", + "last_modified": 1723793067, + "field_type": 4, + "created_at": 1723793067 + }, + "phVRgL": { + "field_type": 0, + "data": "Create an AppFlowy Cloud account" + }, + "SqwRg1": { + "created_at": 1723792881, + "field_type": 3, + "last_modified": 1723792881, + "data": "__n6" + } + }, + "height": 60, + "visibility": true, + "created_at": 1724830348, + "modified_at": 1724830348 + } + ] +} diff --git a/libs/workspace-template/src/database/database_collab.rs b/libs/workspace-template/src/database/database_collab.rs new file mode 100644 index 000000000..083be6353 --- /dev/null +++ b/libs/workspace-template/src/database/database_collab.rs @@ -0,0 +1,53 @@ +use anyhow::Error; +use async_trait::async_trait; +use collab::preclude::Collab; +use collab_database::database::{Database, DatabaseContext}; +use collab_database::entity::{CreateDatabaseParams, EncodedDatabase}; +use collab_database::error::DatabaseError; +use collab_database::workspace_database::{ + DatabaseCollabPersistenceService, DatabaseCollabService, +}; +use collab_entity::CollabType; +use collab_folder::CollabOrigin; +use std::sync::Arc; +use std::vec; + +struct TemplateDatabaseCollabServiceImpl; + +#[async_trait] +impl DatabaseCollabService for TemplateDatabaseCollabServiceImpl { + async fn build_collab( + &self, + object_id: &str, + _object_type: CollabType, + _is_new: bool, + ) -> Result { + Ok(Collab::new_with_origin( + CollabOrigin::Empty, + object_id, + vec![], + false, + )) + } + + fn persistence(&self) -> Option> { + None + } +} + +pub async fn create_database_collab( + _object_id: String, + params: CreateDatabaseParams, +) -> Result { + let collab_service = Arc::new(TemplateDatabaseCollabServiceImpl); + let context = DatabaseContext { + collab_service, + notifier: Default::default(), + is_new: true, + }; + Database::create_with_view(params, context) + .await? + .encode_database_collabs() + .await + .map_err(|e| anyhow::anyhow!("Failed to encode database collabs: {:?}", e)) +} diff --git a/libs/workspace-template/src/database/mod.rs b/libs/workspace-template/src/database/mod.rs new file mode 100644 index 000000000..cca18fe14 --- /dev/null +++ b/libs/workspace-template/src/database/mod.rs @@ -0,0 +1 @@ +pub mod database_collab; diff --git a/libs/workspace-template/src/document/get_started.rs b/libs/workspace-template/src/document/get_started.rs deleted file mode 100644 index 4e409eb7b..000000000 --- a/libs/workspace-template/src/document/get_started.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::sync::Arc; - -use anyhow::Error; -use async_trait::async_trait; -use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; -use collab_document::blocks::DocumentData; -use collab_document::document::Document; -use collab_entity::CollabType; -use collab_folder::ViewLayout; -use tokio::sync::RwLock; - -use crate::document::parser::JsonToDocumentParser; -use crate::hierarchy_builder::WorkspaceViewBuilder; -use crate::{TemplateData, WorkspaceTemplate}; - -/// This template generates a document containing a 'read me' guide. -/// It ensures that at least one view is created for the document. -pub struct GetStartedDocumentTemplate; - -#[async_trait] -impl WorkspaceTemplate for GetStartedDocumentTemplate { - fn layout(&self) -> ViewLayout { - ViewLayout::Document - } - - async fn create(&self, object_id: String) -> anyhow::Result { - let data = tokio::task::spawn_blocking(|| { - let document_data = get_started_document_data().unwrap(); - let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); - let document = Document::open_with(collab, Some(document_data))?; - let data = document.encode_collab()?; - Ok::<_, anyhow::Error>(TemplateData { - object_id, - object_type: CollabType::Document, - object_data: data, - }) - }) - .await??; - Ok(data) - } - - async fn create_workspace_view( - &self, - _uid: i64, - workspace_view_builder: Arc>, - ) -> anyhow::Result { - let view_id = workspace_view_builder - .write() - .await - .with_view_builder(|view_builder| async { - view_builder - .with_name("Getting started") - .with_icon("⭐️") - .build() - }) - .await; - - self.create(view_id).await - } -} - -pub enum DocumentTemplateContent { - Json(String), - Data(DocumentData), -} - -/// Create a document with the given content -pub struct DocumentTemplate(DocumentData); - -impl DocumentTemplate { - pub fn from_json(json: &str) -> Result { - let data = JsonToDocumentParser::json_str_to_document(json)?; - Ok(Self(data)) - } - - pub fn from_data(data: DocumentData) -> Self { - Self(data) - } -} - -#[async_trait] -impl WorkspaceTemplate for DocumentTemplate { - fn layout(&self) -> ViewLayout { - ViewLayout::Document - } - - async fn create(&self, object_id: String) -> anyhow::Result { - let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); - let document = Document::open_with(collab, Some(self.0.clone()))?; - let data = document.encode_collab()?; - Ok(TemplateData { - object_id, - object_type: CollabType::Document, - object_data: data, - }) - } - - async fn create_workspace_view( - &self, - _uid: i64, - workspace_view_builder: Arc>, - ) -> anyhow::Result { - let view_id = workspace_view_builder - .write() - .await - .with_view_builder(|view_builder| async { - view_builder - .with_name("Getting started") - .with_icon("⭐️") - .build() - }) - .await; - self.create(view_id).await - } -} - -pub fn get_started_document_data() -> Result { - let json_str = include_str!("../../assets/read_me.json"); - JsonToDocumentParser::json_str_to_document(json_str) -} - -pub fn get_initial_document_data() -> Result { - let json_str = include_str!("../../assets/initial_document.json"); - JsonToDocumentParser::json_str_to_document(json_str) -} diff --git a/libs/workspace-template/src/document/getting_started.rs b/libs/workspace-template/src/document/getting_started.rs new file mode 100644 index 000000000..0140e9def --- /dev/null +++ b/libs/workspace-template/src/document/getting_started.rs @@ -0,0 +1,447 @@ +use std::collections::HashMap; + +use std::sync::Arc; + +use anyhow::Error; +use async_trait::async_trait; +use collab::core::origin::CollabOrigin; +use collab::preclude::Collab; +use collab_database::database::{timestamp, DatabaseData}; +use collab_database::entity::CreateDatabaseParams; +use collab_document::blocks::DocumentData; +use collab_document::document::Document; +use collab_entity::CollabType; +use collab_folder::ViewLayout; +use serde_json::Value; +use tokio::sync::RwLock; + +use crate::database::database_collab::create_database_collab; +use crate::document::parser::JsonToDocumentParser; +use crate::hierarchy_builder::{ViewBuilder, WorkspaceViewBuilder}; +use crate::{gen_view_id, TemplateData, TemplateObjectId, WorkspaceTemplate}; + +// Template Folder Structure: +// |-- General (space) +// |-- Getting started (document) +// |-- Desktop guide (document) +// |-- Mobile guide (document) +// |-- To-Dos (board) +// |-- Shared (space) +// |-- ... (empty) +// Note: Update the folder structure above if you changed the code below +pub struct GettingStartedTemplate; + +impl GettingStartedTemplate { + /// Create a document template data from the given JSON string + pub async fn create_document_from_json( + &self, + object_id: String, + json_str: &str, + ) -> anyhow::Result { + // 1. read the getting started document from the assets + let document_data = JsonToDocumentParser::json_str_to_document(json_str)?; + + // 2. create a new document with the getting started data + let data = tokio::task::spawn_blocking(move || { + let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); + let document = Document::open_with(collab, Some(document_data))?; + let encoded_collab = document.encode_collab()?; + + Ok::<_, anyhow::Error>(TemplateData { + template_id: TemplateObjectId::Document(object_id), + collab_type: CollabType::Document, + encoded_collab, + }) + }) + .await??; + + Ok(data) + } + + /// Create a series of database templates from the given JSON String + /// + /// Notes: The output contains DatabaseCollab, DatabaseRowCollab + pub async fn create_database_from_params( + &self, + object_id: String, + create_database_params: CreateDatabaseParams, + ) -> anyhow::Result> { + let object_id = object_id.clone(); + let database_id = create_database_params.database_id.clone(); + + let encoded_database = tokio::task::spawn_blocking({ + let object_id = object_id.clone(); + let create_database_params = create_database_params.clone(); + move || create_database_collab(object_id, create_database_params) + }) + .await? + .await?; + + let encoded_database_collab = encoded_database + .encoded_database_collab + .encoded_collab + .clone(); + + // 1. create the new database collab + let database_template_data = TemplateData { + template_id: TemplateObjectId::Database { + object_id: object_id.clone(), + database_id: database_id.clone(), + }, + collab_type: CollabType::Database, + encoded_collab: encoded_database_collab, + }; + + // 2. create the new database row collabs + let database_row_template_data = + encoded_database + .encoded_row_collabs + .iter() + .map(|encoded_row_collab| { + let object_id = encoded_row_collab.object_id.clone(); + let data = encoded_row_collab.encoded_collab.clone(); + TemplateData { + template_id: TemplateObjectId::DatabaseRow(object_id.clone()), + collab_type: CollabType::DatabaseRow, + encoded_collab: data, + } + }); + + let mut template_data = vec![database_template_data]; + template_data.extend(database_row_template_data); + + Ok(template_data) + } + + async fn create_document_and_database_data( + &self, + general_view_uuid: String, + shared_view_uuid: String, + getting_started_view_uuid: String, + desktop_guide_view_uuid: String, + mobile_guide_view_uuid: String, + todos_view_uuid: String, + ) -> anyhow::Result<( + TemplateData, + TemplateData, + TemplateData, + TemplateData, + TemplateData, + Vec, + )> { + let default_space_json = include_str!("../../assets/default_space.json"); + let general_data = self + .create_document_from_json(general_view_uuid.clone(), default_space_json) + .await?; + + let shared_data = self + .create_document_from_json(shared_view_uuid.clone(), default_space_json) + .await?; + + let getting_started_json = include_str!("../../assets/getting_started.json"); + let mut getting_started_json: Value = serde_json::from_str(getting_started_json).unwrap(); + let mut replacements = HashMap::new(); + replacements.insert( + "desktop_guide_id".to_string(), + desktop_guide_view_uuid.clone(), + ); + replacements.insert( + "mobile_guide_id".to_string(), + mobile_guide_view_uuid.clone(), + ); + replacements.insert("todos_id".to_string(), todos_view_uuid.clone()); + replace_json_placeholders(&mut getting_started_json, &replacements); + let getting_started_data = self + .create_document_from_json( + getting_started_view_uuid.clone(), + &getting_started_json.to_string(), + ) + .await?; + + let desktop_guide_json = include_str!("../../assets/desktop_guide.json"); + let desktop_guide_data = self + .create_document_from_json(desktop_guide_view_uuid.clone(), desktop_guide_json) + .await?; + + let mobile_guide_json = include_str!("../../assets/mobile_guide.json"); + let mobile_guide_data = self + .create_document_from_json(mobile_guide_view_uuid.clone(), mobile_guide_json) + .await?; + + let todos_json = include_str!("../../assets/to-dos.json"); + let database_data = serde_json::from_str::(todos_json)?; + let create_database_params = + CreateDatabaseParams::from_database_data(database_data, Some(todos_view_uuid.clone())); + let todos_data = self + .create_database_from_params(todos_view_uuid.clone(), create_database_params.clone()) + .await?; + + Ok(( + general_data, + shared_data, + getting_started_data, + desktop_guide_data, + mobile_guide_data, + todos_data, + )) + } + + async fn build_getting_started_view( + &self, + view_builder: ViewBuilder, + getting_started_view_uuid: String, + desktop_guide_view_uuid: String, + mobile_guide_view_uuid: String, + ) -> ViewBuilder { + // getting started view + let mut view_builder = view_builder + .with_name("Getting started") + .with_icon("⭐️") + .with_extra(r#"{"font_layout":"normal","line_height_layout":"normal","cover":{"type":"gradient","value":"appflowy_them_color_gradient4"},"font":null}"#) + .with_view_id(getting_started_view_uuid); + + view_builder = view_builder + .with_child_view_builder({ + |child_view_builder| async { + // desktop guide view + let desktop_guide_view_uuid = desktop_guide_view_uuid.clone(); + child_view_builder + .with_name("Desktop guide") + .with_icon("📎") + .with_view_id(desktop_guide_view_uuid) + .build() + } + }) + .await; + + view_builder = view_builder + .with_child_view_builder({ + |child_view_builder| async { + // mobile guide view + let mobile_guide_view_uuid = mobile_guide_view_uuid.clone(); + child_view_builder + .with_name("Mobile guide") + .with_view_id(mobile_guide_view_uuid) + .build() + } + }) + .await; + + view_builder + } +} + +#[async_trait] +impl WorkspaceTemplate for GettingStartedTemplate { + fn layout(&self) -> ViewLayout { + ViewLayout::Document + } + + async fn create(&self, _object_id: String) -> anyhow::Result> { + unreachable!("This function is not supposed to be called.") + } + + async fn create_workspace_view( + &self, + _uid: i64, + workspace_view_builder: Arc>, + ) -> anyhow::Result> { + let general_view_uuid = gen_view_id().to_string(); + let shared_view_uuid = gen_view_id().to_string(); + let getting_started_view_uuid = gen_view_id().to_string(); + let desktop_guide_view_uuid = gen_view_id().to_string(); + let mobile_guide_view_uuid = gen_view_id().to_string(); + let todos_view_uuid = gen_view_id().to_string(); + + let ( + general_data, + shared_data, + getting_started_data, + desktop_guide_data, + mobile_guide_data, + todos_data, + ) = self + .create_document_and_database_data( + general_view_uuid.clone(), + shared_view_uuid.clone(), + getting_started_view_uuid.clone(), + desktop_guide_view_uuid.clone(), + mobile_guide_view_uuid.clone(), + todos_view_uuid.clone(), + ) + .await?; + + let mut builder = workspace_view_builder.write().await; + + // Create general space with 2 built-in views: Getting started, To-Dos + // The Getting started view is a document view, and the To-Dos view is a board view + // The Getting started view contains 2 sub views: Desktop guide, Mobile guide + builder + .with_view_builder(|view_builder| async { + let created_at = timestamp(); + let mut view_builder = view_builder + .with_view_id(general_view_uuid.clone()) + .with_name("General") + .with_extra(&format!( + "{{\"is_space\":true,\"space_icon\":\"interface_essential/home-3\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":{}}}", + created_at + )); + + view_builder = view_builder.with_child_view_builder( + |child_view_builder| async { + let getting_started_view_uuid = getting_started_view_uuid.clone(); + let desktop_guide_view_uuid = desktop_guide_view_uuid.clone(); + let mobile_guide_view_uuid = mobile_guide_view_uuid.clone(); + let child_view_builder = self.build_getting_started_view(child_view_builder, getting_started_view_uuid, desktop_guide_view_uuid, mobile_guide_view_uuid).await; + child_view_builder.build() + } + ).await; + + view_builder = view_builder.with_child_view_builder( + |child_view_builder| async { + let child_view_builder = child_view_builder + .with_layout(ViewLayout::Board) + .with_view_id(todos_view_uuid.clone()) + .with_name("To-Dos") + .with_icon("✅"); + child_view_builder.build() + } + ).await; + + view_builder.build() + }) + .await; + + // Create shared space without any built-in views + builder + .with_view_builder(|view_builder| async { + let created_at = timestamp(); + let view_builder = view_builder + .with_view_id(shared_view_uuid.clone()) + .with_name("Shared") + .with_extra(&format!( + "{{\"is_space\":true,\"space_icon\":\"interface_essential/star-2\",\"space_icon_color\":\"0xFFFFBA00\",\"space_permission\":0,\"space_created_at\":{}}}", + created_at + )); + + view_builder.build() + }) + .await; + + let mut template_data = vec![ + general_data, + shared_data, + getting_started_data, + desktop_guide_data, + mobile_guide_data, + ]; + template_data.extend(todos_data); + Ok(template_data) + } +} + +pub enum DocumentTemplateContent { + Json(String), + Data(DocumentData), +} + +/// Create a document with the given content +pub struct DocumentTemplate(DocumentData); + +impl DocumentTemplate { + pub fn from_json(json: &str) -> Result { + let data = JsonToDocumentParser::json_str_to_document(json)?; + Ok(Self(data)) + } + + pub fn from_data(data: DocumentData) -> Self { + Self(data) + } +} + +#[async_trait] +impl WorkspaceTemplate for DocumentTemplate { + fn layout(&self) -> ViewLayout { + ViewLayout::Document + } + + async fn create(&self, object_id: String) -> anyhow::Result> { + let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); + let document = Document::open_with(collab, Some(self.0.clone()))?; + let data = document.encode_collab()?; + Ok(vec![TemplateData { + template_id: TemplateObjectId::Document(object_id), + collab_type: CollabType::Document, + encoded_collab: data, + }]) + } + + async fn create_workspace_view( + &self, + _uid: i64, + workspace_view_builder: Arc>, + ) -> anyhow::Result> { + let view_id = gen_view_id().to_string(); + + let mut builder = workspace_view_builder.write().await; + builder + .with_view_builder(|view_builder| async { + view_builder + .with_name("Getting started") + .with_icon("⭐️") + .with_view_id(view_id.clone()) + .build() + }) + .await; + + self.create(view_id).await + } +} + +pub fn getting_started_document_data() -> Result { + let json_str = include_str!("../../assets/getting_started.json"); + JsonToDocumentParser::json_str_to_document(json_str) +} + +pub fn desktop_guide_document_data() -> Result { + let json_str = include_str!("../../assets/desktop_guide.json"); + JsonToDocumentParser::json_str_to_document(json_str) +} + +pub fn mobile_guide_document_data() -> Result { + let json_str = include_str!("../../assets/mobile_guide.json"); + JsonToDocumentParser::json_str_to_document(json_str) +} + +pub fn get_initial_document_data() -> Result { + let json_str = include_str!("../../assets/initial_document.json"); + JsonToDocumentParser::json_str_to_document(json_str) +} + +/// Replace the placeholders in the JSON value with the given replacements. +/// +/// The placeholders are in the format of "", for example "". +/// The value of the placeholder will be replaced with the value of the key in the replacements map. +pub fn replace_json_placeholders(value: &mut Value, replacements: &HashMap) { + match value { + Value::String(s) => { + if s.starts_with('<') && s.ends_with('>') { + let key = s.trim_start_matches('<').trim_end_matches('>'); + if let Some(replacement) = replacements.get(key) { + *s = replacement.to_string(); + } + } + }, + Value::Array(arr) => { + for item in arr { + replace_json_placeholders(item, replacements); + } + }, + Value::Object(obj) => { + for (_, v) in obj { + replace_json_placeholders(v, replacements); + } + }, + _ => {}, + } +} diff --git a/libs/workspace-template/src/document/mod.rs b/libs/workspace-template/src/document/mod.rs index effd01a98..3b374e813 100644 --- a/libs/workspace-template/src/document/mod.rs +++ b/libs/workspace-template/src/document/mod.rs @@ -1,2 +1,2 @@ -pub mod get_started; +pub mod getting_started; mod parser; diff --git a/libs/workspace-template/src/hierarchy_builder.rs b/libs/workspace-template/src/hierarchy_builder.rs index 54717340c..78bda25cc 100644 --- a/libs/workspace-template/src/hierarchy_builder.rs +++ b/libs/workspace-template/src/hierarchy_builder.rs @@ -21,16 +21,15 @@ impl WorkspaceViewBuilder { } } - pub async fn with_view_builder(&mut self, view_builder: F) -> String + pub async fn with_view_builder(&mut self, view_builder: F) -> &mut Self where F: Fn(ViewBuilder) -> O, O: Future, { let builder = ViewBuilder::new(self.uid, self.workspace_id.clone()); let view = view_builder(builder).await; - let view_id = view.parent_view.id.clone(); self.views.push(view); - view_id + self } pub fn build(&mut self) -> Vec { @@ -50,6 +49,7 @@ pub struct ViewBuilder { child_views: Vec, is_favorite: bool, icon: Option, + extra: Option, } impl ViewBuilder { @@ -64,6 +64,7 @@ impl ViewBuilder { child_views: vec![], is_favorite: false, icon: None, + extra: None, } } @@ -71,6 +72,11 @@ impl ViewBuilder { &self.view_id } + pub fn with_view_id(mut self, view_id: T) -> Self { + self.view_id = view_id.to_string(); + self + } + pub fn with_layout(mut self, layout: ViewLayout) -> Self { self.layout = layout; self @@ -94,6 +100,11 @@ impl ViewBuilder { self } + pub fn with_extra(mut self, extra: &str) -> Self { + self.extra = Some(extra.to_string()); + self + } + /// Create a child view for the current view. /// The view created by this builder will be the next level view of the current view. pub async fn with_child_view_builder(mut self, child_view_builder: F) -> Self @@ -128,7 +139,7 @@ impl ViewBuilder { .collect(), ), last_edited_by: Some(self.uid), - extra: None, + extra: self.extra, }; ParentChildViews { parent_view: view, diff --git a/libs/workspace-template/src/lib.rs b/libs/workspace-template/src/lib.rs index 2a9e15a62..d6eba8578 100644 --- a/libs/workspace-template/src/lib.rs +++ b/libs/workspace-template/src/lib.rs @@ -14,26 +14,42 @@ use tokio::sync::RwLock; use crate::hierarchy_builder::{FlattedViews, WorkspaceViewBuilder}; +pub mod database; pub mod document; + mod hierarchy_builder; +#[cfg(test)] +mod tests; #[async_trait] pub trait WorkspaceTemplate { fn layout(&self) -> ViewLayout; - async fn create(&self, object_id: String) -> Result; + async fn create(&self, object_id: String) -> Result>; async fn create_workspace_view( &self, uid: i64, workspace_view_builder: Arc>, - ) -> Result; + ) -> Result>; +} + +#[derive(Clone, Debug)] +pub enum TemplateObjectId { + Folder(String), + Document(String), + DatabaseRow(String), + Database { + object_id: String, + // It's used to reference the database id from the object_id + database_id: String, + }, } pub struct TemplateData { - pub object_id: String, - pub object_type: CollabType, - pub object_data: EncodedCollab, + pub template_id: TemplateObjectId, + pub collab_type: CollabType, + pub encoded_collab: EncodedCollab, } pub type WorkspaceTemplateHandlers = HashMap>; @@ -79,13 +95,14 @@ impl WorkspaceTemplateBuilder { self.workspace_id.clone(), self.uid, ))); - let mut templates = vec![]; + + let mut templates: Vec = vec![]; for handler in self.handlers.values() { if let Ok(template) = handler .create_workspace_view(self.uid, workspace_view_builder.clone()) .await { - templates.push(template); + templates.extend(template); } } @@ -126,9 +143,9 @@ impl WorkspaceTemplateBuilder { let folder = Folder::open_with(uid, collab, None, Some(folder_data)); let data = folder.encode_collab()?; Ok::<_, anyhow::Error>(TemplateData { - object_id: workspace_id, - object_type: CollabType::Folder, - object_data: data, + template_id: TemplateObjectId::Folder(workspace_id), + collab_type: CollabType::Folder, + encoded_collab: data, }) }) .await??; diff --git a/libs/workspace-template/src/tests/getting_started_tests.rs b/libs/workspace-template/src/tests/getting_started_tests.rs new file mode 100644 index 000000000..5d8456581 --- /dev/null +++ b/libs/workspace-template/src/tests/getting_started_tests.rs @@ -0,0 +1,157 @@ +use crate::document::getting_started::*; +use crate::TemplateData; +use crate::TemplateObjectId; +use crate::{hierarchy_builder::WorkspaceViewBuilder, WorkspaceTemplate}; +use collab::preclude::uuid_v4; +use collab_database::database::DatabaseData; +use collab_database::entity::CreateDatabaseParams; +use collab_document::document_data::generate_id; +use collab_entity::CollabType; +use serde_json::json; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[cfg(test)] +mod tests { + use super::*; + use collab_database::database::gen_database_view_id; + + #[tokio::test] + async fn create_document_from_desktop_guide_json_test() { + let json_str = include_str!("../../assets/desktop_guide.json"); + test_document_json(json_str).await; + } + + #[tokio::test] + async fn create_document_from_mobile_guide_json_test() { + let json_str = include_str!("../../assets/mobile_guide.json"); + test_document_json(json_str).await; + } + + #[tokio::test] + async fn create_document_from_getting_started_json_test() { + let json_str = include_str!("../../assets/getting_started.json"); + test_document_json(json_str).await; + } + + #[tokio::test] + async fn create_database_from_todos_json_test() { + let json_str = include_str!("../../assets/to-dos.json"); + let template_data = test_database_json(json_str).await; + // one database and 5 rows + assert_eq!(template_data.len(), 6); + } + + async fn test_document_json(json_str: &str) { + let template = GettingStartedTemplate; + let object_id = uuid_v4().to_string(); + let result = template + .create_document_from_json(object_id.clone(), json_str) + .await; + let template_data = result.unwrap(); + + match template_data.template_id { + TemplateObjectId::Document(oid) => { + assert_eq!(oid, object_id); + }, + _ => { + panic!("Template data is not a document"); + }, + } + assert_eq!(template_data.collab_type, CollabType::Document); + assert!(!template_data.encoded_collab.doc_state.is_empty()); + } + + async fn test_database_json(json_str: &str) -> Vec { + let template = GettingStartedTemplate; + let object_id = gen_database_view_id().to_string(); + let database_data = serde_json::from_str::(json_str).unwrap(); + let create_database_params = + CreateDatabaseParams::from_database_data(database_data, Some(object_id.clone())); + let result = template + .create_database_from_params(object_id.clone(), create_database_params) + .await; + let template_data = result.unwrap(); + + for (i, data) in template_data.iter().enumerate() { + if i == 0 { + // The first item is the database + assert_eq!(data.collab_type, CollabType::Database); + } else { + // The rest are database rows + assert_eq!(data.collab_type, CollabType::DatabaseRow); + } + + assert!(!data.encoded_collab.doc_state.is_empty()); + } + + template_data + } + + #[tokio::test] + async fn create_workspace_view_with_getting_started_template_test() { + let template = GettingStartedTemplate; + let workspace_view_builder = Arc::new(RwLock::new(WorkspaceViewBuilder::new(generate_id(), 1))); + + let result = template + .create_workspace_view(1, workspace_view_builder.clone()) + .await + .unwrap(); + + // 2 spaces + 3 documents + 1 database + 5 database rows + assert_eq!(result.len(), 11); + + let mut builder = workspace_view_builder.write().await; + let views = builder.build(); + + // check the number of spaces + assert_eq!(views.len(), 2); + + let general_space = &views[0]; + let shared_space = &views[1]; + + // General + assert_eq!(general_space.parent_view.name, "General"); + // generate space contains 1 document and 1 database at the first level + assert_eq!(general_space.child_views.len(), 2); + // the first document contains 2 children + assert_eq!(general_space.child_views[0].child_views.len(), 2); + // the first database contains 0 children + assert_eq!(general_space.child_views[1].child_views.len(), 0); + + // Shared + assert_eq!(shared_space.parent_view.name, "Shared"); + // shared space is empty by default + assert!(shared_space.child_views.is_empty()); + } + + #[test] + fn replace_json_placeholders_test() { + let mut json_value = json!({ + "id": "", + "children": ["", ""], + "attributes": { + "key": "" + } + }); + + let mut replacements = HashMap::new(); + replacements.insert("desktop_guide_view_id".to_string(), "1".to_string()); + replacements.insert("referenced_view_id_1".to_string(), "2".to_string()); + replacements.insert("referenced_view_id_2".to_string(), "3".to_string()); + replacements.insert("value".to_string(), "appflowy".to_string()); + + replace_json_placeholders(&mut json_value, &replacements); + + let expected = json!({ + "id": "1", + "children": ["2", "3"], + "attributes": { + "key": "appflowy" + } + }); + + assert_eq!(json_value, expected); + } +} diff --git a/libs/workspace-template/src/tests/mod.rs b/libs/workspace-template/src/tests/mod.rs new file mode 100644 index 000000000..5f650b18a --- /dev/null +++ b/libs/workspace-template/src/tests/mod.rs @@ -0,0 +1 @@ +mod getting_started_tests; diff --git a/services/appflowy-collaborate/tests/indexer_test.rs b/services/appflowy-collaborate/tests/indexer_test.rs index b0597b9d1..e8e876098 100644 --- a/services/appflowy-collaborate/tests/indexer_test.rs +++ b/services/appflowy-collaborate/tests/indexer_test.rs @@ -1,13 +1,13 @@ use appflowy_collaborate::indexer::DocumentDataExt; -use workspace_template::document::get_started::{ - get_initial_document_data, get_started_document_data, +use workspace_template::document::getting_started::{ + get_initial_document_data, getting_started_document_data, }; #[test] fn document_plain_text() { - let doc = get_started_document_data().unwrap(); + let doc = getting_started_document_data().unwrap(); let text = doc.to_plain_text(); - let expected = "Welcome to AppFlowy! Here are the basics Click anywhere and just start typing. Highlight any text, and use the editing menu to style your writing however you like. As soon as you type / a menu will pop up. Select different types of content blocks you can add. Type / followed by /bullet or /num to create a list. Click + New Page button at the bottom of your sidebar to add a new page. Click + next to any page title in the sidebar to quickly add a new subpage, Document , Grid , or Kanban Board . Keyboard shortcuts, markdown, and code block Keyboard shortcuts guide Markdown reference Type /code to insert a code block // This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n} Have a question❓ Click ? at the bottom right for help and support. Like AppFlowy? Follow us: GitHub Twitter : @appflowy Newsletter "; + let expected = "Welcome to AppFlowy $ Download for macOS, Windows, and Linux link $ $ quick start Ask AI powered by advanced AI models: chat, search, write, and much more ✨ ❤\u{fe0f}Love AppFlowy and open source? Follow our latest product updates: Twitter : @appflowy Reddit : r/appflowy Github "; assert_eq!(&text, expected); } diff --git a/src/biz/user/user_init.rs b/src/biz/user/user_init.rs index 7666a1bfb..879f389ee 100644 --- a/src/biz/user/user_init.rs +++ b/src/biz/user/user_init.rs @@ -3,17 +3,17 @@ use std::sync::Arc; use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use collab::core::origin::CollabOrigin; -use collab::preclude::{ArrayRef, Collab, Map}; -use collab_entity::define::WORKSPACE_DATABASES; +use collab::preclude::Collab; +use collab_database::workspace_database::WorkspaceDatabaseBody; use collab_entity::CollabType; use collab_user::core::UserAwareness; use database::collab::CollabStorage; use database::pg_row::AFWorkspaceRow; use database_entity::dto::CollabParams; use sqlx::Transaction; -use tracing::{debug, error, instrument, trace}; +use tracing::{error, instrument, trace}; use uuid::Uuid; -use workspace_template::{WorkspaceTemplate, WorkspaceTemplateBuilder}; +use workspace_template::{TemplateObjectId, WorkspaceTemplate, WorkspaceTemplateBuilder}; /// This function generates templates for a workspace and stores them in the database. /// Each template is stored as an individual collaborative object. @@ -35,6 +35,50 @@ where .build() .await?; + let mut database_records = vec![]; + for template in templates { + let template_id = template.template_id; + let (view_id, object_id) = match &template_id { + TemplateObjectId::Document(oid) => (oid.to_string(), oid.to_string()), + TemplateObjectId::Folder(oid) => (oid.to_string(), oid.to_string()), + TemplateObjectId::DatabaseRow(oid) => (oid.to_string(), oid.to_string()), + TemplateObjectId::Database { + object_id, + database_id, + } => (object_id.clone(), database_id.clone()), + }; + let object_type = template.collab_type.clone(); + let encoded_collab_v1 = template + .encoded_collab + .encode_to_bytes() + .map_err(|err| AppError::Internal(anyhow::Error::from(err)))?; + + collab_storage + .insert_new_collab_with_transaction( + &workspace_id, + &uid, + CollabParams { + object_id: object_id.clone(), + encoded_collab_v1, + collab_type: object_type.clone(), + embeddings: None, + }, + txn, + ) + .await?; + + // push the database record + if object_type == CollabType::Database { + if let TemplateObjectId::Database { + object_id: _, + database_id, + } = &template_id + { + database_records.push((view_id, database_id.clone())); + } + } + } + // Create a workspace database object for given user // The database_storage_id is auto-generated when the workspace is created. So, it should be available if let Some(database_storage_id) = row.database_storage_id.as_ref() { @@ -45,6 +89,7 @@ where &workspace_database_object_id, collab_storage, txn, + database_records, ) .await?; @@ -63,28 +108,6 @@ where ))); } - debug!("create {} templates for user:{}", templates.len(), uid); - for template in templates { - let object_id = template.object_id; - let encoded_collab_v1 = template - .object_data - .encode_to_bytes() - .map_err(|err| AppError::Internal(anyhow::Error::from(err)))?; - - collab_storage - .insert_new_collab_with_transaction( - &workspace_id, - &uid, - CollabParams { - object_id, - encoded_collab_v1, - collab_type: template.object_type, - embeddings: None, - }, - txn, - ) - .await?; - } Ok(()) } @@ -130,14 +153,16 @@ async fn create_workspace_database_collab( object_id: &str, storage: &Arc, txn: &mut Transaction<'_, sqlx::Postgres>, + initial_database_records: Vec<(String, String)>, ) -> Result<(), AppError> { let collab_type = CollabType::WorkspaceDatabase; let mut collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); { + let workspace_database_body = WorkspaceDatabaseBody::new(&mut collab); let mut txn = collab.context.transact_mut(); - let _ = collab - .data - .get_or_init::<_, ArrayRef>(&mut txn, WORKSPACE_DATABASES); + for (object_id, database_id) in initial_database_records { + workspace_database_body.add_database(&mut txn, &database_id, vec![object_id]); + } }; let encode_collab = collab diff --git a/src/biz/user/user_verify.rs b/src/biz/user/user_verify.rs index e1f79ec9b..714f57013 100644 --- a/src/biz/user/user_verify.rs +++ b/src/biz/user/user_verify.rs @@ -9,7 +9,7 @@ use app_error::AppError; use database::user::{create_user, is_user_exist}; use database::workspace::select_workspace; use database_entity::dto::AFRole; -use workspace_template::document::get_started::GetStartedDocumentTemplate; +use workspace_template::document::getting_started::GettingStartedTemplate; use crate::biz::user::user_init::initialize_workspace_for_user; use crate::state::AppState; @@ -49,7 +49,7 @@ pub async fn verify_token(access_token: &str, state: &AppState) -> Result Document { + let params = QueryCollabParams { + workspace_id, + inner: QueryCollab { + object_id: document_id.to_string(), + collab_type: CollabType::Document, + }, + }; + let resp = test_client.get_collab(params).await.unwrap(); + Document::open_with_options( + CollabOrigin::Empty, + DataSource::DocStateV1(resp.encode_collab.doc_state.to_vec()), + document_id, + vec![], + ) + .unwrap() +} + +// |-- General (space) +// |-- Getting started (document) +// |-- Desktop guide (document) +// |-- Mobile guide (document) +// |-- To-Dos (board) +// |-- Shared (space) +// |-- ... (empty) +#[tokio::test] +async fn get_user_default_workspace_test() { + let email = generate_unique_email(); + let password = "Hello!123#"; + let c = localhost_client(); + c.sign_up(&email, password).await.unwrap(); + let mut test_client = TestClient::new_user().await; + let folder = test_client.get_user_folder().await; + + let workspace_id = test_client.workspace_id().await; + let views = folder.get_views_belong_to(&workspace_id); + + // 2 spaces + assert_eq!(views.len(), 2); + + // the first view is the general space + let general_space = views[0].clone(); + assert_eq!(general_space.name, "General"); + assert!(general_space.icon.is_none()); + assert!(general_space.extra.is_some()); + let extra = general_space.extra.as_ref().unwrap(); + let general_space_extra = json_str_to_hashmap(extra).unwrap(); + assert_eq!( + general_space_extra.get("is_space"), + Some(&serde_json::json!(true)) + ); + + // it contains 1 document and 1 board + let general_space_views = folder.get_views_belong_to(&general_space.id); + assert_eq!(general_space_views.len(), 2); + { + // the first view is the getting started document, and contains 2 sub views + let getting_started_view = general_space_views[0].clone(); + assert_eq!(getting_started_view.name, "Getting started"); + assert_eq!(getting_started_view.layout, ViewLayout::Document); + assert_eq!( + getting_started_view.icon, + Some(ViewIcon { + ty: IconType::Emoji, + value: "⭐️".to_string() + }) + ); + + let getting_started_document = get_document_collab_from_remote( + &mut test_client, + workspace_id.clone(), + &getting_started_view.id, + ) + .await; + let document_data = getting_started_document.get_document_data().unwrap(); + assert_eq!(document_data.blocks.len(), 15); + + let getting_started_sub_views = folder.get_views_belong_to(&getting_started_view.id); + assert_eq!(getting_started_sub_views.len(), 2); + + let desktop_guide_view = getting_started_sub_views[0].clone(); + assert_eq!(desktop_guide_view.name, "Desktop guide"); + assert_eq!(desktop_guide_view.layout, ViewLayout::Document); + assert_eq!( + desktop_guide_view.icon, + Some(ViewIcon { + ty: IconType::Emoji, + value: "📎".to_string() + }) + ); + let desktop_guide_document = get_document_collab_from_remote( + &mut test_client, + workspace_id.clone(), + &desktop_guide_view.id, + ) + .await; + let desktop_guide_document_data = desktop_guide_document.get_document_data().unwrap(); + assert_eq!(desktop_guide_document_data.blocks.len(), 39); + + let mobile_guide_view = getting_started_sub_views[1].clone(); + assert_eq!(mobile_guide_view.name, "Mobile guide"); + assert_eq!(mobile_guide_view.layout, ViewLayout::Document); + assert_eq!(mobile_guide_view.icon, None); + let mobile_guide_document = get_document_collab_from_remote( + &mut test_client, + workspace_id.clone(), + &mobile_guide_view.id, + ) + .await; + let mobile_guide_document_data = mobile_guide_document.get_document_data().unwrap(); + assert_eq!(mobile_guide_document_data.blocks.len(), 34); + } + + // the second view is the to-dos board, and contains 0 sub views + { + let to_dos_view = general_space_views[1].clone(); + assert_eq!(to_dos_view.name, "To-Dos"); + assert_eq!(to_dos_view.layout, ViewLayout::Board); + assert_eq!( + to_dos_view.icon, + Some(ViewIcon { + ty: IconType::Emoji, + value: "✅".to_string() + }) + ); + + let to_dos_sub_views = folder.get_views_belong_to(&to_dos_view.id); + assert_eq!(to_dos_sub_views.len(), 0); + } + + // shared space is empty + let shared_space = views[1].clone(); + assert_eq!(shared_space.name, "Shared"); + assert!(shared_space.icon.is_none()); + assert!(shared_space.extra.is_some()); + let extra = shared_space.extra.as_ref().unwrap(); + let shared_space_extra = json_str_to_hashmap(extra).unwrap(); + assert_eq!( + shared_space_extra.get("is_space"), + Some(&serde_json::json!(true)) + ); + let shared_space_views = folder.get_views_belong_to(&shared_space.id); + assert_eq!(shared_space_views.len(), 0); +} diff --git a/tests/workspace/mod.rs b/tests/workspace/mod.rs index 657b87c5e..0d3552f28 100644 --- a/tests/workspace/mod.rs +++ b/tests/workspace/mod.rs @@ -1,3 +1,4 @@ +mod default_user_workspace; mod edit_workspace; mod invitation_crud; mod member_crud; diff --git a/tests/workspace/template.rs b/tests/workspace/template.rs index 81f63f012..73e8cdb0f 100644 --- a/tests/workspace/template.rs +++ b/tests/workspace/template.rs @@ -6,11 +6,6 @@ use client_api::entity::{ PublishCollabMetadata, TemplateCategoryType, UpdateTemplateCategoryParams, UpdateTemplateParams, }; use client_api_test::*; -use collab::core::collab::DataSource; -use collab::core::origin::CollabOrigin; -use collab_document::document::Document; -use collab_entity::CollabType; -use database_entity::dto::{QueryCollab, QueryCollabParams}; use uuid::Uuid; async fn get_first_workspace_string(c: &client_api::Client) -> String { @@ -23,49 +18,6 @@ async fn get_first_workspace_string(c: &client_api::Client) -> String { .to_string() } -#[tokio::test] -async fn get_user_default_workspace_test() { - let email = generate_unique_email(); - let password = "Hello!123#"; - let c = localhost_client(); - c.sign_up(&email, password).await.unwrap(); - let mut test_client = TestClient::new_user().await; - let folder = test_client.get_user_folder().await; - - let workspace_id = test_client.workspace_id().await; - let views = folder.get_views_belong_to(&workspace_id); - assert_eq!(views.len(), 1); - assert_eq!(views[0].name, "Getting started"); - - let document_id = views[0].id.clone(); - let document = - get_document_collab_from_remote(&mut test_client, workspace_id, &document_id).await; - let document_data = document.get_document_data().unwrap(); - assert_eq!(document_data.blocks.len(), 25); -} - -async fn get_document_collab_from_remote( - test_client: &mut TestClient, - workspace_id: String, - document_id: &str, -) -> Document { - let params = QueryCollabParams { - workspace_id, - inner: QueryCollab { - object_id: document_id.to_string(), - collab_type: CollabType::Document, - }, - }; - let resp = test_client.get_collab(params).await.unwrap(); - Document::open_with_options( - CollabOrigin::Empty, - DataSource::DocStateV1(resp.encode_collab.doc_state.to_vec()), - document_id, - vec![], - ) - .unwrap() -} - #[tokio::test] async fn test_template_category_crud() { let (authorized_client, _) = generate_unique_registered_user_client().await;