mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
rename flowy-core to flowy-folder that manages the workspace,app,view
This commit is contained in:
55
frontend/rust-lib/flowy-folder/Cargo.toml
Executable file
55
frontend/rust-lib/flowy-folder/Cargo.toml
Executable file
@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "flowy-folder"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
flowy-folder-data-model = { path = "../../../shared-lib/flowy-folder-data-model" }
|
||||
flowy-collaboration = { path = "../../../shared-lib/flowy-collaboration" }
|
||||
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
|
||||
lib-ot = { path = "../../../shared-lib/lib-ot" }
|
||||
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
||||
|
||||
flowy-document = { path = "../flowy-document" }
|
||||
flowy-database = { path = "../flowy-database" }
|
||||
flowy-error = { path = "../flowy-error", features = ["db", "backend"]}
|
||||
dart-notify = { path = "../dart-notify" }
|
||||
lib-dispatch = { path = "../lib-dispatch" }
|
||||
lib-sqlite = { path = "../lib-sqlite" }
|
||||
flowy-sync = { path = "../flowy-sync" }
|
||||
|
||||
parking_lot = "0.11"
|
||||
protobuf = {version = "2.18.0"}
|
||||
log = "0.4.14"
|
||||
diesel = {version = "1.4.8", features = ["sqlite"]}
|
||||
diesel_derives = {version = "1.4.1", features = ["sqlite"]}
|
||||
#diesel = { git = "https://github.com/diesel-rs/diesel.git", branch = "master", features = ["sqlite"] }
|
||||
#diesel_derives = { git = "https://github.com/diesel-rs/diesel.git", branch = "master",features = ["sqlite"] }
|
||||
futures-core = { version = "0.3", default-features = false }
|
||||
futures = "0.3.15"
|
||||
pin-project = "1.0.0"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
derive_more = {version = "0.99", features = ["display"]}
|
||||
bincode = { version = "1.3"}
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
bytes = { version = "1.0" }
|
||||
crossbeam = "0.8"
|
||||
crossbeam-utils = "0.8"
|
||||
chrono = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.5.1"
|
||||
serde_json = "1.0"
|
||||
flowy-folder = { path = "../flowy-folder", features = ["flowy_unit_test"]}
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
http_server = []
|
||||
flowy_unit_test = ["lib-ot/flowy_unit_test", "flowy-sync/flowy_unit_test"]
|
3
frontend/rust-lib/flowy-folder/Flowy.toml
Executable file
3
frontend/rust-lib/flowy-folder/Flowy.toml
Executable file
@ -0,0 +1,3 @@
|
||||
|
||||
proto_crates = ["src/entities", "src/event.rs", "src/dart_notification.rs"]
|
||||
event_files = ["src/event.rs"]
|
225
frontend/rust-lib/flowy-folder/src/controller.rs
Executable file
225
frontend/rust-lib/flowy-folder/src/controller.rs
Executable file
@ -0,0 +1,225 @@
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use flowy_collaboration::client_document::default::{initial_delta, initial_read_me};
|
||||
use flowy_folder_data_model::user_default;
|
||||
use flowy_sync::RevisionWebSocket;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use flowy_collaboration::{client_folder::FolderPad, entities::ws_data::ServerRevisionWSData};
|
||||
use flowy_document::FlowyDocumentManager;
|
||||
|
||||
use std::{collections::HashMap, convert::TryInto, fmt::Formatter, sync::Arc};
|
||||
use tokio::sync::RwLock as TokioRwLock;
|
||||
|
||||
use crate::{
|
||||
dart_notification::{send_dart_notification, FolderNotification},
|
||||
entities::workspace::RepeatedWorkspace,
|
||||
errors::FlowyResult,
|
||||
module::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
|
||||
services::{
|
||||
folder_editor::FolderEditor, persistence::FolderPersistence, set_current_workspace, AppController,
|
||||
TrashController, ViewController, WorkspaceController,
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref INIT_FOLDER_FLAG: TokioRwLock<HashMap<String, bool>> = TokioRwLock::new(HashMap::new());
|
||||
}
|
||||
|
||||
const FOLDER_ID: &str = "folder";
|
||||
const FOLDER_ID_SPLIT: &str = ":";
|
||||
#[derive(Clone)]
|
||||
pub struct FolderId(String);
|
||||
impl FolderId {
|
||||
pub fn new(user_id: &str) -> Self {
|
||||
Self(format!("{}{}{}", user_id, FOLDER_ID_SPLIT, FOLDER_ID))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FolderId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(FOLDER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FolderId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(FOLDER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for FolderId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FolderManager {
|
||||
pub user: Arc<dyn WorkspaceUser>,
|
||||
pub(crate) cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
pub(crate) persistence: Arc<FolderPersistence>,
|
||||
pub(crate) workspace_controller: Arc<WorkspaceController>,
|
||||
pub(crate) app_controller: Arc<AppController>,
|
||||
pub(crate) view_controller: Arc<ViewController>,
|
||||
pub(crate) trash_controller: Arc<TrashController>,
|
||||
web_socket: Arc<dyn RevisionWebSocket>,
|
||||
folder_editor: Arc<TokioRwLock<Option<Arc<FolderEditor>>>>,
|
||||
}
|
||||
|
||||
impl FolderManager {
|
||||
pub async fn new(
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
database: Arc<dyn WorkspaceDatabase>,
|
||||
document_manager: Arc<FlowyDocumentManager>,
|
||||
web_socket: Arc<dyn RevisionWebSocket>,
|
||||
) -> Self {
|
||||
if let Ok(user_id) = user.user_id() {
|
||||
// Reset the flag if the folder manager gets initialized, otherwise,
|
||||
// the folder_editor will not be initialized after flutter hot reload.
|
||||
INIT_FOLDER_FLAG.write().await.insert(user_id.to_owned(), false);
|
||||
}
|
||||
|
||||
let folder_editor = Arc::new(TokioRwLock::new(None));
|
||||
let persistence = Arc::new(FolderPersistence::new(database.clone(), folder_editor.clone()));
|
||||
|
||||
let trash_controller = Arc::new(TrashController::new(
|
||||
persistence.clone(),
|
||||
cloud_service.clone(),
|
||||
user.clone(),
|
||||
));
|
||||
|
||||
let view_controller = Arc::new(ViewController::new(
|
||||
user.clone(),
|
||||
persistence.clone(),
|
||||
cloud_service.clone(),
|
||||
trash_controller.clone(),
|
||||
document_manager,
|
||||
));
|
||||
|
||||
let app_controller = Arc::new(AppController::new(
|
||||
user.clone(),
|
||||
persistence.clone(),
|
||||
trash_controller.clone(),
|
||||
cloud_service.clone(),
|
||||
));
|
||||
|
||||
let workspace_controller = Arc::new(WorkspaceController::new(
|
||||
user.clone(),
|
||||
persistence.clone(),
|
||||
trash_controller.clone(),
|
||||
cloud_service.clone(),
|
||||
));
|
||||
|
||||
Self {
|
||||
user,
|
||||
cloud_service,
|
||||
persistence,
|
||||
workspace_controller,
|
||||
app_controller,
|
||||
view_controller,
|
||||
trash_controller,
|
||||
web_socket,
|
||||
folder_editor,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn network_state_changed(&self, new_type: NetworkType) {
|
||||
// match new_type {
|
||||
// NetworkType::UnknownNetworkType => {},
|
||||
// NetworkType::Wifi => {},
|
||||
// NetworkType::Cell => {},
|
||||
// NetworkType::Ethernet => {},
|
||||
// }
|
||||
// }
|
||||
|
||||
pub async fn did_receive_ws_data(&self, data: Bytes) {
|
||||
let result: Result<ServerRevisionWSData, protobuf::ProtobufError> = data.try_into();
|
||||
match result {
|
||||
Ok(data) => match self.folder_editor.read().await.clone() {
|
||||
None => {}
|
||||
Some(editor) => match editor.receive_ws_data(data).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("Folder receive data error: {:?}", e),
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Folder ws data parser failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn initialize(&self, user_id: &str, token: &str) -> FlowyResult<()> {
|
||||
let mut write_guard = INIT_FOLDER_FLAG.write().await;
|
||||
if let Some(is_init) = write_guard.get(user_id) {
|
||||
if *is_init {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tracing::debug!("Initialize folder editor");
|
||||
let folder_id = FolderId::new(user_id);
|
||||
let _ = self.persistence.initialize(user_id, &folder_id).await?;
|
||||
|
||||
let pool = self.persistence.db_pool()?;
|
||||
let folder_editor = FolderEditor::new(user_id, &folder_id, token, pool, self.web_socket.clone()).await?;
|
||||
*self.folder_editor.write().await = Some(Arc::new(folder_editor));
|
||||
|
||||
let _ = self.app_controller.initialize()?;
|
||||
let _ = self.view_controller.initialize()?;
|
||||
write_guard.insert(user_id.to_owned(), true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn initialize_with_new_user(&self, user_id: &str, token: &str) -> FlowyResult<()> {
|
||||
DefaultFolderBuilder::build(token, user_id, self.persistence.clone(), self.view_controller.clone()).await?;
|
||||
self.initialize(user_id, token).await
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
*self.folder_editor.write().await = None;
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultFolderBuilder();
|
||||
impl DefaultFolderBuilder {
|
||||
async fn build(
|
||||
token: &str,
|
||||
user_id: &str,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
view_controller: Arc<ViewController>,
|
||||
) -> FlowyResult<()> {
|
||||
log::debug!("Create user default workspace");
|
||||
let time = Utc::now();
|
||||
let workspace = user_default::create_default_workspace(time);
|
||||
set_current_workspace(&workspace.id);
|
||||
for app in workspace.apps.iter() {
|
||||
for (index, view) in app.belongings.iter().enumerate() {
|
||||
let view_data = if index == 0 {
|
||||
initial_read_me().to_json()
|
||||
} else {
|
||||
initial_delta().to_json()
|
||||
};
|
||||
view_controller.set_latest_view(view);
|
||||
let _ = view_controller
|
||||
.create_view_document_content(&view.id, view_data)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
let folder = FolderPad::new(vec![workspace.clone()], vec![])?;
|
||||
let folder_id = FolderId::new(user_id);
|
||||
let _ = persistence.save_folder(user_id, &folder_id, folder).await?;
|
||||
let repeated_workspace = RepeatedWorkspace { items: vec![workspace] };
|
||||
send_dart_notification(token, FolderNotification::UserCreateWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "flowy_unit_test")]
|
||||
impl FolderManager {
|
||||
pub async fn folder_editor(&self) -> Arc<FolderEditor> {
|
||||
self.folder_editor.read().await.clone().unwrap()
|
||||
}
|
||||
}
|
42
frontend/rust-lib/flowy-folder/src/dart_notification.rs
Executable file
42
frontend/rust-lib/flowy-folder/src/dart_notification.rs
Executable file
@ -0,0 +1,42 @@
|
||||
use dart_notify::DartNotifyBuilder;
|
||||
use flowy_derive::ProtoBuf_Enum;
|
||||
const OBSERVABLE_CATEGORY: &str = "Workspace";
|
||||
|
||||
#[derive(ProtoBuf_Enum, Debug)]
|
||||
pub(crate) enum FolderNotification {
|
||||
Unknown = 0,
|
||||
UserCreateWorkspace = 10,
|
||||
UserDeleteWorkspace = 11,
|
||||
WorkspaceUpdated = 12,
|
||||
WorkspaceListUpdated = 13,
|
||||
WorkspaceAppsChanged = 14,
|
||||
AppUpdated = 21,
|
||||
AppViewsChanged = 24,
|
||||
ViewUpdated = 31,
|
||||
ViewDeleted = 32,
|
||||
ViewRestored = 33,
|
||||
UserUnauthorized = 100,
|
||||
TrashUpdated = 1000,
|
||||
}
|
||||
|
||||
impl std::default::Default for FolderNotification {
|
||||
fn default() -> Self {
|
||||
FolderNotification::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<FolderNotification> for i32 {
|
||||
fn from(notification: FolderNotification) -> Self {
|
||||
notification as i32
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub(crate) fn send_dart_notification(id: &str, ty: FolderNotification) -> DartNotifyBuilder {
|
||||
DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub(crate) fn send_anonymous_dart_notification(ty: FolderNotification) -> DartNotifyBuilder {
|
||||
DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY)
|
||||
}
|
81
frontend/rust-lib/flowy-folder/src/event.rs
Executable file
81
frontend/rust-lib/flowy-folder/src/event.rs
Executable file
@ -0,0 +1,81 @@
|
||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||
use strum_macros::Display;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum FolderEvent {
|
||||
#[event(input = "CreateWorkspaceRequest", output = "Workspace")]
|
||||
CreateWorkspace = 0,
|
||||
|
||||
#[event(output = "CurrentWorkspaceSetting")]
|
||||
ReadCurWorkspace = 1,
|
||||
|
||||
#[event(input = "QueryWorkspaceRequest", output = "RepeatedWorkspace")]
|
||||
ReadWorkspaces = 2,
|
||||
|
||||
#[event(input = "QueryWorkspaceRequest")]
|
||||
DeleteWorkspace = 3,
|
||||
|
||||
#[event(input = "QueryWorkspaceRequest", output = "Workspace")]
|
||||
OpenWorkspace = 4,
|
||||
|
||||
#[event(input = "QueryWorkspaceRequest", output = "RepeatedApp")]
|
||||
ReadWorkspaceApps = 5,
|
||||
|
||||
#[event(input = "CreateAppRequest", output = "App")]
|
||||
CreateApp = 101,
|
||||
|
||||
#[event(input = "QueryAppRequest")]
|
||||
DeleteApp = 102,
|
||||
|
||||
#[event(input = "QueryAppRequest", output = "App")]
|
||||
ReadApp = 103,
|
||||
|
||||
#[event(input = "UpdateAppRequest")]
|
||||
UpdateApp = 104,
|
||||
|
||||
#[event(input = "CreateViewRequest", output = "View")]
|
||||
CreateView = 201,
|
||||
|
||||
#[event(input = "QueryViewRequest", output = "View")]
|
||||
ReadView = 202,
|
||||
|
||||
#[event(input = "UpdateViewRequest", output = "View")]
|
||||
UpdateView = 203,
|
||||
|
||||
#[event(input = "QueryViewRequest")]
|
||||
DeleteView = 204,
|
||||
|
||||
#[event(input = "QueryViewRequest")]
|
||||
DuplicateView = 205,
|
||||
|
||||
#[event()]
|
||||
CopyLink = 206,
|
||||
|
||||
#[event(input = "QueryViewRequest", output = "DocumentDelta")]
|
||||
OpenDocument = 207,
|
||||
|
||||
#[event(input = "QueryViewRequest")]
|
||||
CloseView = 208,
|
||||
|
||||
#[event(output = "RepeatedTrash")]
|
||||
ReadTrash = 300,
|
||||
|
||||
#[event(input = "TrashId")]
|
||||
PutbackTrash = 301,
|
||||
|
||||
#[event(input = "RepeatedTrashId")]
|
||||
DeleteTrash = 302,
|
||||
|
||||
#[event()]
|
||||
RestoreAllTrash = 303,
|
||||
|
||||
#[event()]
|
||||
DeleteAllTrash = 304,
|
||||
|
||||
#[event(input = "DocumentDelta", output = "DocumentDelta")]
|
||||
ApplyDocDelta = 400,
|
||||
|
||||
#[event(input = "ExportRequest", output = "ExportData")]
|
||||
ExportDocument = 500,
|
||||
}
|
26
frontend/rust-lib/flowy-folder/src/lib.rs
Executable file
26
frontend/rust-lib/flowy-folder/src/lib.rs
Executable file
@ -0,0 +1,26 @@
|
||||
pub use flowy_folder_data_model::entities;
|
||||
|
||||
pub mod event;
|
||||
pub mod module;
|
||||
pub mod services;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
#[macro_use]
|
||||
extern crate flowy_database;
|
||||
|
||||
pub mod controller;
|
||||
mod dart_notification;
|
||||
pub mod protobuf;
|
||||
mod util;
|
||||
|
||||
pub mod prelude {
|
||||
pub use flowy_folder_data_model::entities::{app::*, trash::*, view::*, workspace::*};
|
||||
|
||||
pub use crate::{errors::*, module::*};
|
||||
}
|
||||
|
||||
pub mod errors {
|
||||
pub use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
||||
}
|
49
frontend/rust-lib/flowy-folder/src/macros.rs
Executable file
49
frontend/rust-lib/flowy-folder/src/macros.rs
Executable file
@ -0,0 +1,49 @@
|
||||
// #[macro_export]
|
||||
// macro_rules! impl_save_func {
|
||||
// ($func_name:ident, $target:ident, $table_name:expr, $conn:ident) => {
|
||||
// fn $func_name(object: $target) -> Result<(), FlowyError> {
|
||||
// let _ = diesel::insert_into($table_name)
|
||||
// .values($target)
|
||||
// .execute(&*($conn))?;
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_def_and_def_mut {
|
||||
($target:ident, $item: ident) => {
|
||||
impl std::ops::Deref for $target {
|
||||
type Target = Vec<$item>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for $target {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.items
|
||||
}
|
||||
}
|
||||
|
||||
impl $target {
|
||||
#[allow(dead_code)]
|
||||
pub fn into_inner(&mut self) -> Vec<$item> {
|
||||
::std::mem::replace(&mut self.items, vec![])
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn push(&mut self, item: $item) {
|
||||
if self.items.contains(&item) {
|
||||
log::error!("add duplicate item: {:?}", item);
|
||||
return;
|
||||
}
|
||||
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
pub fn first_or_crash(&self) -> &$item {
|
||||
self.items.first().unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
119
frontend/rust-lib/flowy-folder/src/module.rs
Executable file
119
frontend/rust-lib/flowy-folder/src/module.rs
Executable file
@ -0,0 +1,119 @@
|
||||
use crate::{
|
||||
controller::FolderManager,
|
||||
entities::{
|
||||
app::{App, AppId, CreateAppParams, UpdateAppParams},
|
||||
trash::{RepeatedTrash, RepeatedTrashId},
|
||||
view::{CreateViewParams, RepeatedViewId, UpdateViewParams, View, ViewId},
|
||||
workspace::{CreateWorkspaceParams, RepeatedWorkspace, UpdateWorkspaceParams, Workspace, WorkspaceId},
|
||||
},
|
||||
errors::FlowyError,
|
||||
event::FolderEvent,
|
||||
services::{app::event_handler::*, trash::event_handler::*, view::event_handler::*, workspace::event_handler::*},
|
||||
};
|
||||
use flowy_database::DBConnection;
|
||||
|
||||
use lib_dispatch::prelude::*;
|
||||
use lib_infra::future::FutureResult;
|
||||
use lib_sqlite::ConnectionPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait WorkspaceDeps: WorkspaceUser + WorkspaceDatabase {}
|
||||
|
||||
pub trait WorkspaceUser: Send + Sync {
|
||||
fn user_id(&self) -> Result<String, FlowyError>;
|
||||
fn token(&self) -> Result<String, FlowyError>;
|
||||
}
|
||||
|
||||
pub trait WorkspaceDatabase: Send + Sync {
|
||||
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
||||
|
||||
fn db_connection(&self) -> Result<DBConnection, FlowyError> {
|
||||
let pool = self.db_pool()?;
|
||||
let conn = pool.get().map_err(|e| FlowyError::internal().context(e))?;
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(folder: Arc<FolderManager>) -> Module {
|
||||
let mut module = Module::new()
|
||||
.name("Flowy-Workspace")
|
||||
.data(folder.workspace_controller.clone())
|
||||
.data(folder.app_controller.clone())
|
||||
.data(folder.view_controller.clone())
|
||||
.data(folder.trash_controller.clone())
|
||||
.data(folder.clone());
|
||||
|
||||
module = module
|
||||
.event(FolderEvent::CreateWorkspace, create_workspace_handler)
|
||||
.event(FolderEvent::ReadCurWorkspace, read_cur_workspace_handler)
|
||||
.event(FolderEvent::ReadWorkspaces, read_workspaces_handler)
|
||||
.event(FolderEvent::OpenWorkspace, open_workspace_handler)
|
||||
.event(FolderEvent::ReadWorkspaceApps, read_workspace_apps_handler);
|
||||
|
||||
module = module
|
||||
.event(FolderEvent::CreateApp, create_app_handler)
|
||||
.event(FolderEvent::ReadApp, read_app_handler)
|
||||
.event(FolderEvent::UpdateApp, update_app_handler)
|
||||
.event(FolderEvent::DeleteApp, delete_app_handler);
|
||||
|
||||
module = module
|
||||
.event(FolderEvent::CreateView, create_view_handler)
|
||||
.event(FolderEvent::ReadView, read_view_handler)
|
||||
.event(FolderEvent::UpdateView, update_view_handler)
|
||||
.event(FolderEvent::DeleteView, delete_view_handler)
|
||||
.event(FolderEvent::DuplicateView, duplicate_view_handler)
|
||||
.event(FolderEvent::OpenDocument, open_document_handler)
|
||||
.event(FolderEvent::CloseView, close_view_handler)
|
||||
.event(FolderEvent::ApplyDocDelta, document_delta_handler);
|
||||
|
||||
module = module
|
||||
.event(FolderEvent::ReadTrash, read_trash_handler)
|
||||
.event(FolderEvent::PutbackTrash, putback_trash_handler)
|
||||
.event(FolderEvent::DeleteTrash, delete_trash_handler)
|
||||
.event(FolderEvent::RestoreAllTrash, restore_all_trash_handler)
|
||||
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler);
|
||||
|
||||
module = module.event(FolderEvent::ExportDocument, export_handler);
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
pub trait FolderCouldServiceV1: Send + Sync {
|
||||
fn init(&self);
|
||||
|
||||
// Workspace
|
||||
fn create_workspace(&self, token: &str, params: CreateWorkspaceParams) -> FutureResult<Workspace, FlowyError>;
|
||||
|
||||
fn read_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<RepeatedWorkspace, FlowyError>;
|
||||
|
||||
fn update_workspace(&self, token: &str, params: UpdateWorkspaceParams) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn delete_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<(), FlowyError>;
|
||||
|
||||
// View
|
||||
fn create_view(&self, token: &str, params: CreateViewParams) -> FutureResult<View, FlowyError>;
|
||||
|
||||
fn read_view(&self, token: &str, params: ViewId) -> FutureResult<Option<View>, FlowyError>;
|
||||
|
||||
fn delete_view(&self, token: &str, params: RepeatedViewId) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn update_view(&self, token: &str, params: UpdateViewParams) -> FutureResult<(), FlowyError>;
|
||||
|
||||
// App
|
||||
fn create_app(&self, token: &str, params: CreateAppParams) -> FutureResult<App, FlowyError>;
|
||||
|
||||
fn read_app(&self, token: &str, params: AppId) -> FutureResult<Option<App>, FlowyError>;
|
||||
|
||||
fn update_app(&self, token: &str, params: UpdateAppParams) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn delete_app(&self, token: &str, params: AppId) -> FutureResult<(), FlowyError>;
|
||||
|
||||
// Trash
|
||||
fn create_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn delete_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn read_trash(&self, token: &str) -> FutureResult<RepeatedTrash, FlowyError>;
|
||||
}
|
||||
|
||||
pub trait FolderCouldServiceV2: Send + Sync {}
|
4
frontend/rust-lib/flowy-folder/src/protobuf/mod.rs
Executable file
4
frontend/rust-lib/flowy-folder/src/protobuf/mod.rs
Executable file
@ -0,0 +1,4 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
// Auto-generated, do not edit
|
||||
mod model;
|
||||
pub use model::*;
|
158
frontend/rust-lib/flowy-folder/src/protobuf/model/dart_notification.rs
Executable file
158
frontend/rust-lib/flowy-folder/src/protobuf/model/dart_notification.rs
Executable file
@ -0,0 +1,158 @@
|
||||
// This file is generated by rust-protobuf 2.22.1. Do not edit
|
||||
// @generated
|
||||
|
||||
// https://github.com/rust-lang/rust-clippy/issues/702
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
#![allow(unused_attributes)]
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
|
||||
#![allow(box_pointers)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(trivial_casts)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_results)]
|
||||
//! Generated file from `dart_notification.proto`
|
||||
|
||||
/// Generated files are compatible only with the same version
|
||||
/// of protobuf runtime.
|
||||
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_22_1;
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
|
||||
pub enum FolderNotification {
|
||||
Unknown = 0,
|
||||
UserCreateWorkspace = 10,
|
||||
UserDeleteWorkspace = 11,
|
||||
WorkspaceUpdated = 12,
|
||||
WorkspaceListUpdated = 13,
|
||||
WorkspaceAppsChanged = 14,
|
||||
AppUpdated = 21,
|
||||
AppViewsChanged = 24,
|
||||
ViewUpdated = 31,
|
||||
ViewDeleted = 32,
|
||||
ViewRestored = 33,
|
||||
UserUnauthorized = 100,
|
||||
TrashUpdated = 1000,
|
||||
}
|
||||
|
||||
impl ::protobuf::ProtobufEnum for FolderNotification {
|
||||
fn value(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
|
||||
fn from_i32(value: i32) -> ::std::option::Option<FolderNotification> {
|
||||
match value {
|
||||
0 => ::std::option::Option::Some(FolderNotification::Unknown),
|
||||
10 => ::std::option::Option::Some(FolderNotification::UserCreateWorkspace),
|
||||
11 => ::std::option::Option::Some(FolderNotification::UserDeleteWorkspace),
|
||||
12 => ::std::option::Option::Some(FolderNotification::WorkspaceUpdated),
|
||||
13 => ::std::option::Option::Some(FolderNotification::WorkspaceListUpdated),
|
||||
14 => ::std::option::Option::Some(FolderNotification::WorkspaceAppsChanged),
|
||||
21 => ::std::option::Option::Some(FolderNotification::AppUpdated),
|
||||
24 => ::std::option::Option::Some(FolderNotification::AppViewsChanged),
|
||||
31 => ::std::option::Option::Some(FolderNotification::ViewUpdated),
|
||||
32 => ::std::option::Option::Some(FolderNotification::ViewDeleted),
|
||||
33 => ::std::option::Option::Some(FolderNotification::ViewRestored),
|
||||
100 => ::std::option::Option::Some(FolderNotification::UserUnauthorized),
|
||||
1000 => ::std::option::Option::Some(FolderNotification::TrashUpdated),
|
||||
_ => ::std::option::Option::None
|
||||
}
|
||||
}
|
||||
|
||||
fn values() -> &'static [Self] {
|
||||
static values: &'static [FolderNotification] = &[
|
||||
FolderNotification::Unknown,
|
||||
FolderNotification::UserCreateWorkspace,
|
||||
FolderNotification::UserDeleteWorkspace,
|
||||
FolderNotification::WorkspaceUpdated,
|
||||
FolderNotification::WorkspaceListUpdated,
|
||||
FolderNotification::WorkspaceAppsChanged,
|
||||
FolderNotification::AppUpdated,
|
||||
FolderNotification::AppViewsChanged,
|
||||
FolderNotification::ViewUpdated,
|
||||
FolderNotification::ViewDeleted,
|
||||
FolderNotification::ViewRestored,
|
||||
FolderNotification::UserUnauthorized,
|
||||
FolderNotification::TrashUpdated,
|
||||
];
|
||||
values
|
||||
}
|
||||
|
||||
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
|
||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
||||
descriptor.get(|| {
|
||||
::protobuf::reflect::EnumDescriptor::new_pb_name::<FolderNotification>("FolderNotification", file_descriptor_proto())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::marker::Copy for FolderNotification {
|
||||
}
|
||||
|
||||
impl ::std::default::Default for FolderNotification {
|
||||
fn default() -> Self {
|
||||
FolderNotification::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
impl ::protobuf::reflect::ProtobufValue for FolderNotification {
|
||||
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
|
||||
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
|
||||
}
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x17dart_notification.proto*\x9f\x02\n\x12FolderNotification\x12\x0b\n\
|
||||
\x07Unknown\x10\0\x12\x17\n\x13UserCreateWorkspace\x10\n\x12\x17\n\x13Us\
|
||||
erDeleteWorkspace\x10\x0b\x12\x14\n\x10WorkspaceUpdated\x10\x0c\x12\x18\
|
||||
\n\x14WorkspaceListUpdated\x10\r\x12\x18\n\x14WorkspaceAppsChanged\x10\
|
||||
\x0e\x12\x0e\n\nAppUpdated\x10\x15\x12\x13\n\x0fAppViewsChanged\x10\x18\
|
||||
\x12\x0f\n\x0bViewUpdated\x10\x1f\x12\x0f\n\x0bViewDeleted\x10\x20\x12\
|
||||
\x10\n\x0cViewRestored\x10!\x12\x14\n\x10UserUnauthorized\x10d\x12\x11\n\
|
||||
\x0cTrashUpdated\x10\xe8\x07J\xbf\x04\n\x06\x12\x04\0\0\x10\x01\n\x08\n\
|
||||
\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x10\x01\n\n\n\x03\
|
||||
\x05\0\x01\x12\x03\x02\x05\x17\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\
|
||||
\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x0b\n\x0c\n\x05\x05\0\
|
||||
\x02\0\x02\x12\x03\x03\x0e\x0f\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\
|
||||
\x1d\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x17\n\x0c\n\x05\x05\0\
|
||||
\x02\x01\x02\x12\x03\x04\x1a\x1c\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\
|
||||
\x04\x1d\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x17\n\x0c\n\x05\
|
||||
\x05\0\x02\x02\x02\x12\x03\x05\x1a\x1c\n\x0b\n\x04\x05\0\x02\x03\x12\x03\
|
||||
\x06\x04\x1a\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x14\n\x0c\n\
|
||||
\x05\x05\0\x02\x03\x02\x12\x03\x06\x17\x19\n\x0b\n\x04\x05\0\x02\x04\x12\
|
||||
\x03\x07\x04\x1e\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x18\n\x0c\
|
||||
\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x1b\x1d\n\x0b\n\x04\x05\0\x02\x05\
|
||||
\x12\x03\x08\x04\x1e\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x18\n\
|
||||
\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x1b\x1d\n\x0b\n\x04\x05\0\x02\
|
||||
\x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\x0e\n\
|
||||
\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x11\x13\n\x0b\n\x04\x05\0\x02\x07\
|
||||
\x12\x03\n\x04\x19\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\x13\n\x0c\
|
||||
\n\x05\x05\0\x02\x07\x02\x12\x03\n\x16\x18\n\x0b\n\x04\x05\0\x02\x08\x12\
|
||||
\x03\x0b\x04\x15\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\x0f\n\x0c\
|
||||
\n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x12\x14\n\x0b\n\x04\x05\0\x02\t\x12\
|
||||
\x03\x0c\x04\x15\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x0c\x04\x0f\n\x0c\n\
|
||||
\x05\x05\0\x02\t\x02\x12\x03\x0c\x12\x14\n\x0b\n\x04\x05\0\x02\n\x12\x03\
|
||||
\r\x04\x16\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\r\x04\x10\n\x0c\n\x05\x05\
|
||||
\0\x02\n\x02\x12\x03\r\x13\x15\n\x0b\n\x04\x05\0\x02\x0b\x12\x03\x0e\x04\
|
||||
\x1b\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\x0e\x04\x14\n\x0c\n\x05\x05\0\
|
||||
\x02\x0b\x02\x12\x03\x0e\x17\x1a\n\x0b\n\x04\x05\0\x02\x0c\x12\x03\x0f\
|
||||
\x04\x18\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\x03\x0f\x04\x10\n\x0c\n\x05\
|
||||
\x05\0\x02\x0c\x02\x12\x03\x0f\x13\x17b\x06proto3\
|
||||
";
|
||||
|
||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||
|
||||
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
|
||||
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
|
||||
}
|
||||
|
||||
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
|
||||
file_descriptor_proto_lazy.get(|| {
|
||||
parse_descriptor_proto()
|
||||
})
|
||||
}
|
224
frontend/rust-lib/flowy-folder/src/protobuf/model/event.rs
Executable file
224
frontend/rust-lib/flowy-folder/src/protobuf/model/event.rs
Executable file
@ -0,0 +1,224 @@
|
||||
// This file is generated by rust-protobuf 2.22.1. Do not edit
|
||||
// @generated
|
||||
|
||||
// https://github.com/rust-lang/rust-clippy/issues/702
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
#![allow(unused_attributes)]
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
|
||||
#![allow(box_pointers)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(trivial_casts)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_results)]
|
||||
//! Generated file from `event.proto`
|
||||
|
||||
/// Generated files are compatible only with the same version
|
||||
/// of protobuf runtime.
|
||||
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_22_1;
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
|
||||
pub enum FolderEvent {
|
||||
CreateWorkspace = 0,
|
||||
ReadCurWorkspace = 1,
|
||||
ReadWorkspaces = 2,
|
||||
DeleteWorkspace = 3,
|
||||
OpenWorkspace = 4,
|
||||
ReadWorkspaceApps = 5,
|
||||
CreateApp = 101,
|
||||
DeleteApp = 102,
|
||||
ReadApp = 103,
|
||||
UpdateApp = 104,
|
||||
CreateView = 201,
|
||||
ReadView = 202,
|
||||
UpdateView = 203,
|
||||
DeleteView = 204,
|
||||
DuplicateView = 205,
|
||||
CopyLink = 206,
|
||||
OpenDocument = 207,
|
||||
CloseView = 208,
|
||||
ReadTrash = 300,
|
||||
PutbackTrash = 301,
|
||||
DeleteTrash = 302,
|
||||
RestoreAllTrash = 303,
|
||||
DeleteAllTrash = 304,
|
||||
ApplyDocDelta = 400,
|
||||
ExportDocument = 500,
|
||||
}
|
||||
|
||||
impl ::protobuf::ProtobufEnum for FolderEvent {
|
||||
fn value(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
|
||||
fn from_i32(value: i32) -> ::std::option::Option<FolderEvent> {
|
||||
match value {
|
||||
0 => ::std::option::Option::Some(FolderEvent::CreateWorkspace),
|
||||
1 => ::std::option::Option::Some(FolderEvent::ReadCurWorkspace),
|
||||
2 => ::std::option::Option::Some(FolderEvent::ReadWorkspaces),
|
||||
3 => ::std::option::Option::Some(FolderEvent::DeleteWorkspace),
|
||||
4 => ::std::option::Option::Some(FolderEvent::OpenWorkspace),
|
||||
5 => ::std::option::Option::Some(FolderEvent::ReadWorkspaceApps),
|
||||
101 => ::std::option::Option::Some(FolderEvent::CreateApp),
|
||||
102 => ::std::option::Option::Some(FolderEvent::DeleteApp),
|
||||
103 => ::std::option::Option::Some(FolderEvent::ReadApp),
|
||||
104 => ::std::option::Option::Some(FolderEvent::UpdateApp),
|
||||
201 => ::std::option::Option::Some(FolderEvent::CreateView),
|
||||
202 => ::std::option::Option::Some(FolderEvent::ReadView),
|
||||
203 => ::std::option::Option::Some(FolderEvent::UpdateView),
|
||||
204 => ::std::option::Option::Some(FolderEvent::DeleteView),
|
||||
205 => ::std::option::Option::Some(FolderEvent::DuplicateView),
|
||||
206 => ::std::option::Option::Some(FolderEvent::CopyLink),
|
||||
207 => ::std::option::Option::Some(FolderEvent::OpenDocument),
|
||||
208 => ::std::option::Option::Some(FolderEvent::CloseView),
|
||||
300 => ::std::option::Option::Some(FolderEvent::ReadTrash),
|
||||
301 => ::std::option::Option::Some(FolderEvent::PutbackTrash),
|
||||
302 => ::std::option::Option::Some(FolderEvent::DeleteTrash),
|
||||
303 => ::std::option::Option::Some(FolderEvent::RestoreAllTrash),
|
||||
304 => ::std::option::Option::Some(FolderEvent::DeleteAllTrash),
|
||||
400 => ::std::option::Option::Some(FolderEvent::ApplyDocDelta),
|
||||
500 => ::std::option::Option::Some(FolderEvent::ExportDocument),
|
||||
_ => ::std::option::Option::None
|
||||
}
|
||||
}
|
||||
|
||||
fn values() -> &'static [Self] {
|
||||
static values: &'static [FolderEvent] = &[
|
||||
FolderEvent::CreateWorkspace,
|
||||
FolderEvent::ReadCurWorkspace,
|
||||
FolderEvent::ReadWorkspaces,
|
||||
FolderEvent::DeleteWorkspace,
|
||||
FolderEvent::OpenWorkspace,
|
||||
FolderEvent::ReadWorkspaceApps,
|
||||
FolderEvent::CreateApp,
|
||||
FolderEvent::DeleteApp,
|
||||
FolderEvent::ReadApp,
|
||||
FolderEvent::UpdateApp,
|
||||
FolderEvent::CreateView,
|
||||
FolderEvent::ReadView,
|
||||
FolderEvent::UpdateView,
|
||||
FolderEvent::DeleteView,
|
||||
FolderEvent::DuplicateView,
|
||||
FolderEvent::CopyLink,
|
||||
FolderEvent::OpenDocument,
|
||||
FolderEvent::CloseView,
|
||||
FolderEvent::ReadTrash,
|
||||
FolderEvent::PutbackTrash,
|
||||
FolderEvent::DeleteTrash,
|
||||
FolderEvent::RestoreAllTrash,
|
||||
FolderEvent::DeleteAllTrash,
|
||||
FolderEvent::ApplyDocDelta,
|
||||
FolderEvent::ExportDocument,
|
||||
];
|
||||
values
|
||||
}
|
||||
|
||||
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
|
||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
||||
descriptor.get(|| {
|
||||
::protobuf::reflect::EnumDescriptor::new_pb_name::<FolderEvent>("FolderEvent", file_descriptor_proto())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::marker::Copy for FolderEvent {
|
||||
}
|
||||
|
||||
impl ::std::default::Default for FolderEvent {
|
||||
fn default() -> Self {
|
||||
FolderEvent::CreateWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
impl ::protobuf::reflect::ProtobufValue for FolderEvent {
|
||||
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
|
||||
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
|
||||
}
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x0bevent.proto*\xd6\x03\n\x0bFolderEvent\x12\x13\n\x0fCreateWorkspace\
|
||||
\x10\0\x12\x14\n\x10ReadCurWorkspace\x10\x01\x12\x12\n\x0eReadWorkspaces\
|
||||
\x10\x02\x12\x13\n\x0fDeleteWorkspace\x10\x03\x12\x11\n\rOpenWorkspace\
|
||||
\x10\x04\x12\x15\n\x11ReadWorkspaceApps\x10\x05\x12\r\n\tCreateApp\x10e\
|
||||
\x12\r\n\tDeleteApp\x10f\x12\x0b\n\x07ReadApp\x10g\x12\r\n\tUpdateApp\
|
||||
\x10h\x12\x0f\n\nCreateView\x10\xc9\x01\x12\r\n\x08ReadView\x10\xca\x01\
|
||||
\x12\x0f\n\nUpdateView\x10\xcb\x01\x12\x0f\n\nDeleteView\x10\xcc\x01\x12\
|
||||
\x12\n\rDuplicateView\x10\xcd\x01\x12\r\n\x08CopyLink\x10\xce\x01\x12\
|
||||
\x11\n\x0cOpenDocument\x10\xcf\x01\x12\x0e\n\tCloseView\x10\xd0\x01\x12\
|
||||
\x0e\n\tReadTrash\x10\xac\x02\x12\x11\n\x0cPutbackTrash\x10\xad\x02\x12\
|
||||
\x10\n\x0bDeleteTrash\x10\xae\x02\x12\x14\n\x0fRestoreAllTrash\x10\xaf\
|
||||
\x02\x12\x13\n\x0eDeleteAllTrash\x10\xb0\x02\x12\x12\n\rApplyDocDelta\
|
||||
\x10\x90\x03\x12\x13\n\x0eExportDocument\x10\xf4\x03J\xab\x08\n\x06\x12\
|
||||
\x04\0\0\x1c\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\
|
||||
\x02\0\x1c\x01\n\n\n\x03\x05\0\x01\x12\x03\x02\x05\x10\n\x0b\n\x04\x05\0\
|
||||
\x02\0\x12\x03\x03\x04\x18\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\
|
||||
\x13\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x03\x16\x17\n\x0b\n\x04\x05\0\
|
||||
\x02\x01\x12\x03\x04\x04\x19\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\
|
||||
\x04\x14\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x04\x17\x18\n\x0b\n\x04\
|
||||
\x05\0\x02\x02\x12\x03\x05\x04\x17\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\
|
||||
\x05\x04\x12\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\x05\x15\x16\n\x0b\n\
|
||||
\x04\x05\0\x02\x03\x12\x03\x06\x04\x18\n\x0c\n\x05\x05\0\x02\x03\x01\x12\
|
||||
\x03\x06\x04\x13\n\x0c\n\x05\x05\0\x02\x03\x02\x12\x03\x06\x16\x17\n\x0b\
|
||||
\n\x04\x05\0\x02\x04\x12\x03\x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\
|
||||
\x12\x03\x07\x04\x11\n\x0c\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x14\x15\n\
|
||||
\x0b\n\x04\x05\0\x02\x05\x12\x03\x08\x04\x1a\n\x0c\n\x05\x05\0\x02\x05\
|
||||
\x01\x12\x03\x08\x04\x15\n\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x18\
|
||||
\x19\n\x0b\n\x04\x05\0\x02\x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\
|
||||
\x06\x01\x12\x03\t\x04\r\n\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x10\x13\
|
||||
\n\x0b\n\x04\x05\0\x02\x07\x12\x03\n\x04\x14\n\x0c\n\x05\x05\0\x02\x07\
|
||||
\x01\x12\x03\n\x04\r\n\x0c\n\x05\x05\0\x02\x07\x02\x12\x03\n\x10\x13\n\
|
||||
\x0b\n\x04\x05\0\x02\x08\x12\x03\x0b\x04\x12\n\x0c\n\x05\x05\0\x02\x08\
|
||||
\x01\x12\x03\x0b\x04\x0b\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x0e\
|
||||
\x11\n\x0b\n\x04\x05\0\x02\t\x12\x03\x0c\x04\x14\n\x0c\n\x05\x05\0\x02\t\
|
||||
\x01\x12\x03\x0c\x04\r\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x0c\x10\x13\n\
|
||||
\x0b\n\x04\x05\0\x02\n\x12\x03\r\x04\x15\n\x0c\n\x05\x05\0\x02\n\x01\x12\
|
||||
\x03\r\x04\x0e\n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\r\x11\x14\n\x0b\n\x04\
|
||||
\x05\0\x02\x0b\x12\x03\x0e\x04\x13\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\
|
||||
\x0e\x04\x0c\n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\x0e\x0f\x12\n\x0b\n\
|
||||
\x04\x05\0\x02\x0c\x12\x03\x0f\x04\x15\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\
|
||||
\x03\x0f\x04\x0e\n\x0c\n\x05\x05\0\x02\x0c\x02\x12\x03\x0f\x11\x14\n\x0b\
|
||||
\n\x04\x05\0\x02\r\x12\x03\x10\x04\x15\n\x0c\n\x05\x05\0\x02\r\x01\x12\
|
||||
\x03\x10\x04\x0e\n\x0c\n\x05\x05\0\x02\r\x02\x12\x03\x10\x11\x14\n\x0b\n\
|
||||
\x04\x05\0\x02\x0e\x12\x03\x11\x04\x18\n\x0c\n\x05\x05\0\x02\x0e\x01\x12\
|
||||
\x03\x11\x04\x11\n\x0c\n\x05\x05\0\x02\x0e\x02\x12\x03\x11\x14\x17\n\x0b\
|
||||
\n\x04\x05\0\x02\x0f\x12\x03\x12\x04\x13\n\x0c\n\x05\x05\0\x02\x0f\x01\
|
||||
\x12\x03\x12\x04\x0c\n\x0c\n\x05\x05\0\x02\x0f\x02\x12\x03\x12\x0f\x12\n\
|
||||
\x0b\n\x04\x05\0\x02\x10\x12\x03\x13\x04\x17\n\x0c\n\x05\x05\0\x02\x10\
|
||||
\x01\x12\x03\x13\x04\x10\n\x0c\n\x05\x05\0\x02\x10\x02\x12\x03\x13\x13\
|
||||
\x16\n\x0b\n\x04\x05\0\x02\x11\x12\x03\x14\x04\x14\n\x0c\n\x05\x05\0\x02\
|
||||
\x11\x01\x12\x03\x14\x04\r\n\x0c\n\x05\x05\0\x02\x11\x02\x12\x03\x14\x10\
|
||||
\x13\n\x0b\n\x04\x05\0\x02\x12\x12\x03\x15\x04\x14\n\x0c\n\x05\x05\0\x02\
|
||||
\x12\x01\x12\x03\x15\x04\r\n\x0c\n\x05\x05\0\x02\x12\x02\x12\x03\x15\x10\
|
||||
\x13\n\x0b\n\x04\x05\0\x02\x13\x12\x03\x16\x04\x17\n\x0c\n\x05\x05\0\x02\
|
||||
\x13\x01\x12\x03\x16\x04\x10\n\x0c\n\x05\x05\0\x02\x13\x02\x12\x03\x16\
|
||||
\x13\x16\n\x0b\n\x04\x05\0\x02\x14\x12\x03\x17\x04\x16\n\x0c\n\x05\x05\0\
|
||||
\x02\x14\x01\x12\x03\x17\x04\x0f\n\x0c\n\x05\x05\0\x02\x14\x02\x12\x03\
|
||||
\x17\x12\x15\n\x0b\n\x04\x05\0\x02\x15\x12\x03\x18\x04\x1a\n\x0c\n\x05\
|
||||
\x05\0\x02\x15\x01\x12\x03\x18\x04\x13\n\x0c\n\x05\x05\0\x02\x15\x02\x12\
|
||||
\x03\x18\x16\x19\n\x0b\n\x04\x05\0\x02\x16\x12\x03\x19\x04\x19\n\x0c\n\
|
||||
\x05\x05\0\x02\x16\x01\x12\x03\x19\x04\x12\n\x0c\n\x05\x05\0\x02\x16\x02\
|
||||
\x12\x03\x19\x15\x18\n\x0b\n\x04\x05\0\x02\x17\x12\x03\x1a\x04\x18\n\x0c\
|
||||
\n\x05\x05\0\x02\x17\x01\x12\x03\x1a\x04\x11\n\x0c\n\x05\x05\0\x02\x17\
|
||||
\x02\x12\x03\x1a\x14\x17\n\x0b\n\x04\x05\0\x02\x18\x12\x03\x1b\x04\x19\n\
|
||||
\x0c\n\x05\x05\0\x02\x18\x01\x12\x03\x1b\x04\x12\n\x0c\n\x05\x05\0\x02\
|
||||
\x18\x02\x12\x03\x1b\x15\x18b\x06proto3\
|
||||
";
|
||||
|
||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||
|
||||
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
|
||||
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
|
||||
}
|
||||
|
||||
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
|
||||
file_descriptor_proto_lazy.get(|| {
|
||||
parse_descriptor_proto()
|
||||
})
|
||||
}
|
8
frontend/rust-lib/flowy-folder/src/protobuf/model/mod.rs
Executable file
8
frontend/rust-lib/flowy-folder/src/protobuf/model/mod.rs
Executable file
@ -0,0 +1,8 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
// Auto-generated, do not edit
|
||||
|
||||
mod dart_notification;
|
||||
pub use dart_notification::*;
|
||||
|
||||
mod event;
|
||||
pub use event::*;
|
17
frontend/rust-lib/flowy-folder/src/protobuf/proto/dart_notification.proto
Executable file
17
frontend/rust-lib/flowy-folder/src/protobuf/proto/dart_notification.proto
Executable file
@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
enum FolderNotification {
|
||||
Unknown = 0;
|
||||
UserCreateWorkspace = 10;
|
||||
UserDeleteWorkspace = 11;
|
||||
WorkspaceUpdated = 12;
|
||||
WorkspaceListUpdated = 13;
|
||||
WorkspaceAppsChanged = 14;
|
||||
AppUpdated = 21;
|
||||
AppViewsChanged = 24;
|
||||
ViewUpdated = 31;
|
||||
ViewDeleted = 32;
|
||||
ViewRestored = 33;
|
||||
UserUnauthorized = 100;
|
||||
TrashUpdated = 1000;
|
||||
}
|
29
frontend/rust-lib/flowy-folder/src/protobuf/proto/event.proto
Executable file
29
frontend/rust-lib/flowy-folder/src/protobuf/proto/event.proto
Executable file
@ -0,0 +1,29 @@
|
||||
syntax = "proto3";
|
||||
|
||||
enum FolderEvent {
|
||||
CreateWorkspace = 0;
|
||||
ReadCurWorkspace = 1;
|
||||
ReadWorkspaces = 2;
|
||||
DeleteWorkspace = 3;
|
||||
OpenWorkspace = 4;
|
||||
ReadWorkspaceApps = 5;
|
||||
CreateApp = 101;
|
||||
DeleteApp = 102;
|
||||
ReadApp = 103;
|
||||
UpdateApp = 104;
|
||||
CreateView = 201;
|
||||
ReadView = 202;
|
||||
UpdateView = 203;
|
||||
DeleteView = 204;
|
||||
DuplicateView = 205;
|
||||
CopyLink = 206;
|
||||
OpenDocument = 207;
|
||||
CloseView = 208;
|
||||
ReadTrash = 300;
|
||||
PutbackTrash = 301;
|
||||
DeleteTrash = 302;
|
||||
RestoreAllTrash = 303;
|
||||
DeleteAllTrash = 304;
|
||||
ApplyDocDelta = 400;
|
||||
ExportDocument = 500;
|
||||
}
|
246
frontend/rust-lib/flowy-folder/src/services/app/controller.rs
Executable file
246
frontend/rust-lib/flowy-folder/src/services/app/controller.rs
Executable file
@ -0,0 +1,246 @@
|
||||
use crate::{
|
||||
dart_notification::*,
|
||||
entities::{
|
||||
app::{App, CreateAppParams, *},
|
||||
trash::TrashType,
|
||||
},
|
||||
errors::*,
|
||||
module::{FolderCouldServiceV1, WorkspaceUser},
|
||||
services::{
|
||||
persistence::{AppChangeset, FolderPersistence, FolderPersistenceTransaction},
|
||||
TrashController, TrashEvent,
|
||||
},
|
||||
};
|
||||
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
pub(crate) struct AppController {
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
trash_controller: Arc<TrashController>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
}
|
||||
|
||||
impl AppController {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
trash_can: Arc<TrashController>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user,
|
||||
persistence,
|
||||
trash_controller: trash_can,
|
||||
cloud_service,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&self) -> Result<(), FlowyError> {
|
||||
self.listen_trash_controller_event();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, params), fields(name = %params.name) err)]
|
||||
pub(crate) async fn create_app_from_params(&self, params: CreateAppParams) -> Result<App, FlowyError> {
|
||||
let app = self.create_app_on_server(params).await?;
|
||||
self.create_app_on_local(app).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_app_on_local(&self, app: App) -> Result<App, FlowyError> {
|
||||
let _ = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.create_app(app.clone())?;
|
||||
let _ = notify_apps_changed(&app.workspace_id, self.trash_controller.clone(), &transaction)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_app(&self, params: AppId) -> Result<App, FlowyError> {
|
||||
let app = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let app = transaction.read_app(¶ms.app_id)?;
|
||||
let trash_ids = self.trash_controller.read_trash_ids(&transaction)?;
|
||||
if trash_ids.contains(&app.id) {
|
||||
return Err(FlowyError::record_not_found());
|
||||
}
|
||||
Ok(app)
|
||||
})
|
||||
.await?;
|
||||
let _ = self.read_app_on_server(params)?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_app(&self, params: UpdateAppParams) -> Result<(), FlowyError> {
|
||||
let changeset = AppChangeset::new(params.clone());
|
||||
let app_id = changeset.id.clone();
|
||||
|
||||
let app = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.update_app(changeset)?;
|
||||
let app = transaction.read_app(&app_id)?;
|
||||
Ok(app)
|
||||
})
|
||||
.await?;
|
||||
send_dart_notification(&app_id, FolderNotification::AppUpdated)
|
||||
.payload(app)
|
||||
.send();
|
||||
let _ = self.update_app_on_server(params)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn read_local_apps(&self, ids: Vec<String>) -> Result<Vec<App>, FlowyError> {
|
||||
let apps = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut apps = vec![];
|
||||
for id in ids {
|
||||
apps.push(transaction.read_app(&id)?);
|
||||
}
|
||||
Ok(apps)
|
||||
})
|
||||
.await?;
|
||||
Ok(apps)
|
||||
}
|
||||
}
|
||||
|
||||
impl AppController {
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
async fn create_app_on_server(&self, params: CreateAppParams) -> Result<App, FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let app = self.cloud_service.create_app(&token, params).await?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
fn update_app_on_server(&self, params: UpdateAppParams) -> Result<(), FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
tokio::spawn(async move {
|
||||
match server.update_app(&token, params).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// TODO: retry?
|
||||
log::error!("Update app failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
fn read_app_on_server(&self, params: AppId) -> Result<(), FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
let persistence = self.persistence.clone();
|
||||
tokio::spawn(async move {
|
||||
match server.read_app(&token, params).await {
|
||||
Ok(Some(app)) => {
|
||||
match persistence
|
||||
.begin_transaction(|transaction| transaction.create_app(app.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
send_dart_notification(&app.id, FolderNotification::AppUpdated)
|
||||
.payload(app)
|
||||
.send();
|
||||
}
|
||||
Err(e) => log::error!("Save app failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => log::error!("Read app failed: {:?}", e),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn listen_trash_controller_event(&self) {
|
||||
let mut rx = self.trash_controller.subscribe();
|
||||
let persistence = self.persistence.clone();
|
||||
let trash_controller = self.trash_controller.clone();
|
||||
let _ = tokio::spawn(async move {
|
||||
loop {
|
||||
let mut stream = Box::pin(rx.recv().into_stream().filter_map(|result| async move {
|
||||
match result {
|
||||
Ok(event) => event.select(TrashType::App),
|
||||
Err(_e) => None,
|
||||
}
|
||||
}));
|
||||
if let Some(event) = stream.next().await {
|
||||
handle_trash_event(persistence.clone(), trash_controller.clone(), event).await
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(persistence, trash_controller))]
|
||||
async fn handle_trash_event(
|
||||
persistence: Arc<FolderPersistence>,
|
||||
trash_controller: Arc<TrashController>,
|
||||
event: TrashEvent,
|
||||
) {
|
||||
match event {
|
||||
TrashEvent::NewTrash(identifiers, ret) | TrashEvent::Putback(identifiers, ret) => {
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
for identifier in identifiers.items {
|
||||
let app = transaction.read_app(&identifier.id)?;
|
||||
let _ = notify_apps_changed(&app.workspace_id, trash_controller.clone(), &transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let _ = ret.send(result).await;
|
||||
}
|
||||
TrashEvent::Delete(identifiers, ret) => {
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut notify_ids = HashSet::new();
|
||||
for identifier in identifiers.items {
|
||||
let app = transaction.read_app(&identifier.id)?;
|
||||
let _ = transaction.delete_app(&identifier.id)?;
|
||||
notify_ids.insert(app.workspace_id);
|
||||
}
|
||||
|
||||
for notify_id in notify_ids {
|
||||
let _ = notify_apps_changed(¬ify_id, trash_controller.clone(), &transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let _ = ret.send(result).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(workspace_id, trash_controller, transaction), err)]
|
||||
fn notify_apps_changed<'a>(
|
||||
workspace_id: &str,
|
||||
trash_controller: Arc<TrashController>,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> FlowyResult<()> {
|
||||
let repeated_app = read_local_workspace_apps(workspace_id, trash_controller, transaction)?;
|
||||
send_dart_notification(workspace_id, FolderNotification::WorkspaceAppsChanged)
|
||||
.payload(repeated_app)
|
||||
.send();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_local_workspace_apps<'a>(
|
||||
workspace_id: &str,
|
||||
trash_controller: Arc<TrashController>,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> Result<RepeatedApp, FlowyError> {
|
||||
let mut apps = transaction.read_workspace_apps(workspace_id)?;
|
||||
let trash_ids = trash_controller.read_trash_ids(transaction)?;
|
||||
apps.retain(|app| !trash_ids.contains(&app.id));
|
||||
Ok(RepeatedApp { items: apps })
|
||||
}
|
60
frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs
Executable file
60
frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs
Executable file
@ -0,0 +1,60 @@
|
||||
use crate::{
|
||||
entities::{
|
||||
app::{App, AppId, CreateAppParams, CreateAppRequest, QueryAppRequest, UpdateAppParams, UpdateAppRequest},
|
||||
trash::Trash,
|
||||
},
|
||||
errors::FlowyError,
|
||||
services::{AppController, TrashController, ViewController},
|
||||
};
|
||||
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
pub(crate) async fn create_app_handler(
|
||||
data: Data<CreateAppRequest>,
|
||||
controller: Unit<Arc<AppController>>,
|
||||
) -> DataResult<App, FlowyError> {
|
||||
let params: CreateAppParams = data.into_inner().try_into()?;
|
||||
let detail = controller.create_app_from_params(params).await?;
|
||||
|
||||
data_result(detail)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_app_handler(
|
||||
data: Data<QueryAppRequest>,
|
||||
app_controller: Unit<Arc<AppController>>,
|
||||
trash_controller: Unit<Arc<TrashController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: AppId = data.into_inner().try_into()?;
|
||||
let trash = app_controller
|
||||
.read_local_apps(vec![params.app_id])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|app| app.into())
|
||||
.collect::<Vec<Trash>>();
|
||||
|
||||
let _ = trash_controller.add(trash).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, controller))]
|
||||
pub(crate) async fn update_app_handler(
|
||||
data: Data<UpdateAppRequest>,
|
||||
controller: Unit<Arc<AppController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: UpdateAppParams = data.into_inner().try_into()?;
|
||||
let _ = controller.update_app(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, app_controller, view_controller))]
|
||||
pub(crate) async fn read_app_handler(
|
||||
data: Data<QueryAppRequest>,
|
||||
app_controller: Unit<Arc<AppController>>,
|
||||
view_controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<App, FlowyError> {
|
||||
let params: AppId = data.into_inner().try_into()?;
|
||||
let mut app = app_controller.read_app(params.clone()).await?;
|
||||
app.belongings = view_controller.read_views_belong_to(¶ms.app_id).await?;
|
||||
|
||||
data_result(app)
|
||||
}
|
2
frontend/rust-lib/flowy-folder/src/services/app/mod.rs
Executable file
2
frontend/rust-lib/flowy-folder/src/services/app/mod.rs
Executable file
@ -0,0 +1,2 @@
|
||||
pub mod controller;
|
||||
pub mod event_handler;
|
152
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
Executable file
152
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
Executable file
@ -0,0 +1,152 @@
|
||||
use crate::services::web_socket::make_folder_ws_manager;
|
||||
use flowy_collaboration::{
|
||||
client_folder::{FolderChange, FolderPad},
|
||||
entities::{revision::Revision, ws_data::ServerRevisionWSData},
|
||||
};
|
||||
|
||||
use crate::controller::FolderId;
|
||||
use flowy_collaboration::util::make_delta_from_revisions;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_sync::{
|
||||
RevisionCache, RevisionCloudService, RevisionCompact, RevisionManager, RevisionObjectBuilder, RevisionWebSocket,
|
||||
RevisionWebSocketManager,
|
||||
};
|
||||
use lib_infra::future::FutureResult;
|
||||
use lib_ot::core::PlainAttributes;
|
||||
use lib_sqlite::ConnectionPool;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct FolderEditor {
|
||||
user_id: String,
|
||||
pub(crate) folder_id: FolderId,
|
||||
pub(crate) folder: Arc<RwLock<FolderPad>>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
ws_manager: Arc<RevisionWebSocketManager>,
|
||||
}
|
||||
|
||||
impl FolderEditor {
|
||||
pub async fn new(
|
||||
user_id: &str,
|
||||
folder_id: &FolderId,
|
||||
token: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
web_socket: Arc<dyn RevisionWebSocket>,
|
||||
) -> FlowyResult<Self> {
|
||||
let cache = Arc::new(RevisionCache::new(user_id, folder_id.as_ref(), pool));
|
||||
let mut rev_manager = RevisionManager::new(user_id, folder_id.as_ref(), cache);
|
||||
let cloud = Arc::new(FolderRevisionCloudServiceImpl {
|
||||
token: token.to_string(),
|
||||
});
|
||||
let folder = Arc::new(RwLock::new(
|
||||
rev_manager
|
||||
.load::<FolderPadBuilder, FolderRevisionCompact>(cloud)
|
||||
.await?,
|
||||
));
|
||||
let rev_manager = Arc::new(rev_manager);
|
||||
let ws_manager = make_folder_ws_manager(
|
||||
user_id,
|
||||
folder_id.as_ref(),
|
||||
rev_manager.clone(),
|
||||
web_socket,
|
||||
folder.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let user_id = user_id.to_owned();
|
||||
let folder_id = folder_id.to_owned();
|
||||
Ok(Self {
|
||||
user_id,
|
||||
folder_id,
|
||||
folder,
|
||||
rev_manager,
|
||||
ws_manager,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn receive_ws_data(&self, data: ServerRevisionWSData) -> FlowyResult<()> {
|
||||
let _ = self.ws_manager.ws_passthrough_tx.send(data).await.map_err(|e| {
|
||||
let err_msg = format!("{} passthrough error: {}", self.folder_id, e);
|
||||
FlowyError::internal().context(err_msg)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn apply_change(&self, change: FolderChange) -> FlowyResult<()> {
|
||||
let FolderChange { delta, md5 } = change;
|
||||
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
||||
let delta_data = delta.to_bytes();
|
||||
let revision = Revision::new(
|
||||
&self.rev_manager.object_id,
|
||||
base_rev_id,
|
||||
rev_id,
|
||||
delta_data,
|
||||
&self.user_id,
|
||||
md5,
|
||||
);
|
||||
let _ = futures::executor::block_on(async {
|
||||
self.rev_manager
|
||||
.add_local_revision::<FolderRevisionCompact>(&revision)
|
||||
.await
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn folder_json(&self) -> FlowyResult<String> {
|
||||
let json = self.folder.read().to_json()?;
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderPadBuilder();
|
||||
impl RevisionObjectBuilder for FolderPadBuilder {
|
||||
type Output = FolderPad;
|
||||
|
||||
fn build_with_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
|
||||
let pad = FolderPad::from_revisions(revisions)?;
|
||||
Ok(pad)
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderRevisionCloudServiceImpl {
|
||||
#[allow(dead_code)]
|
||||
token: String,
|
||||
// server: Arc<dyn FolderCouldServiceV2>,
|
||||
}
|
||||
|
||||
impl RevisionCloudService for FolderRevisionCloudServiceImpl {
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
|
||||
FutureResult::new(async move { Ok(vec![]) })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "flowy_unit_test")]
|
||||
impl FolderEditor {
|
||||
pub fn rev_manager(&self) -> Arc<RevisionManager> {
|
||||
self.rev_manager.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderRevisionCompact();
|
||||
impl RevisionCompact for FolderRevisionCompact {
|
||||
fn compact_revisions(user_id: &str, object_id: &str, mut revisions: Vec<Revision>) -> FlowyResult<Revision> {
|
||||
if revisions.is_empty() {
|
||||
return Err(FlowyError::internal().context("Can't compact the empty folder's revisions"));
|
||||
}
|
||||
|
||||
if revisions.len() == 1 {
|
||||
return Ok(revisions.pop().unwrap());
|
||||
}
|
||||
|
||||
let first_revision = revisions.first().unwrap();
|
||||
let last_revision = revisions.last().unwrap();
|
||||
|
||||
let (base_rev_id, rev_id) = first_revision.pair_rev_id();
|
||||
let md5 = last_revision.md5.clone();
|
||||
let delta = make_delta_from_revisions::<PlainAttributes>(revisions)?;
|
||||
let delta_data = delta.to_bytes();
|
||||
Ok(Revision::new(object_id, base_rev_id, rev_id, delta_data, user_id, md5))
|
||||
}
|
||||
}
|
14
frontend/rust-lib/flowy-folder/src/services/mod.rs
Executable file
14
frontend/rust-lib/flowy-folder/src/services/mod.rs
Executable file
@ -0,0 +1,14 @@
|
||||
pub(crate) use app::controller::*;
|
||||
pub(crate) use trash::controller::*;
|
||||
pub(crate) use view::controller::*;
|
||||
pub(crate) use workspace::controller::*;
|
||||
|
||||
pub(crate) mod app;
|
||||
pub mod folder_editor;
|
||||
pub(crate) mod persistence;
|
||||
pub(crate) mod trash;
|
||||
pub(crate) mod view;
|
||||
mod web_socket;
|
||||
pub(crate) mod workspace;
|
||||
|
||||
pub const FOLDER_SYNC_INTERVAL_IN_MILLIS: u64 = 5000;
|
78
frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs
Executable file
78
frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs
Executable file
@ -0,0 +1,78 @@
|
||||
use crate::{
|
||||
module::WorkspaceDatabase,
|
||||
services::persistence::{AppTableSql, TrashTableSql, ViewTableSql, WorkspaceTableSql},
|
||||
};
|
||||
use flowy_collaboration::{client_folder::FolderPad, entities::revision::md5};
|
||||
use flowy_database::kv::KV;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::{App, RepeatedApp},
|
||||
view::{RepeatedView, View},
|
||||
workspace::Workspace,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
|
||||
|
||||
pub(crate) struct FolderMigration {
|
||||
user_id: String,
|
||||
database: Arc<dyn WorkspaceDatabase>,
|
||||
}
|
||||
|
||||
impl FolderMigration {
|
||||
pub fn new(user_id: &str, database: Arc<dyn WorkspaceDatabase>) -> Self {
|
||||
Self {
|
||||
user_id: user_id.to_owned(),
|
||||
database,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_v1_migration(&self) -> FlowyResult<Option<FolderPad>> {
|
||||
let key = md5(format!("{}{}", self.user_id, V1_MIGRATION));
|
||||
if KV::get_bool(&key).unwrap_or(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
tracing::trace!("Run folder version 1 migrations");
|
||||
let pool = self.database.db_pool()?;
|
||||
let conn = &*pool.get()?;
|
||||
let workspaces = conn.immediate_transaction::<_, FlowyError, _>(|| {
|
||||
let mut workspaces = WorkspaceTableSql::read_workspaces(&self.user_id, None, conn)?
|
||||
.into_iter()
|
||||
.map(Workspace::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for workspace in workspaces.iter_mut() {
|
||||
let mut apps = AppTableSql::read_workspace_apps(&workspace.id, conn)?
|
||||
.into_iter()
|
||||
.map(App::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for app in apps.iter_mut() {
|
||||
let views = ViewTableSql::read_views(&app.id, conn)?
|
||||
.into_iter()
|
||||
.map(View::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
app.belongings = RepeatedView { items: views };
|
||||
}
|
||||
|
||||
workspace.apps = RepeatedApp { items: apps };
|
||||
}
|
||||
Ok(workspaces)
|
||||
})?;
|
||||
|
||||
if workspaces.is_empty() {
|
||||
KV::set_bool(&key, true);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let trash = conn.immediate_transaction::<_, FlowyError, _>(|| {
|
||||
let trash = TrashTableSql::read_all(conn)?.take_items();
|
||||
Ok(trash)
|
||||
})?;
|
||||
|
||||
let folder = FolderPad::new(workspaces, trash)?;
|
||||
KV::set_bool(&key, true);
|
||||
Ok(Some(folder))
|
||||
}
|
||||
}
|
127
frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs
Executable file
127
frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs
Executable file
@ -0,0 +1,127 @@
|
||||
mod migration;
|
||||
pub mod version_1;
|
||||
mod version_2;
|
||||
|
||||
use flowy_collaboration::{
|
||||
client_folder::FolderPad,
|
||||
entities::revision::{Revision, RevisionState},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*};
|
||||
|
||||
use crate::{
|
||||
controller::FolderId,
|
||||
module::WorkspaceDatabase,
|
||||
services::{folder_editor::FolderEditor, persistence::migration::FolderMigration},
|
||||
};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::App,
|
||||
trash::{RepeatedTrash, Trash},
|
||||
view::View,
|
||||
workspace::Workspace,
|
||||
};
|
||||
use flowy_sync::{mk_revision_disk_cache, RevisionRecord};
|
||||
use lib_sqlite::ConnectionPool;
|
||||
|
||||
pub trait FolderPersistenceTransaction {
|
||||
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()>;
|
||||
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>>;
|
||||
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()>;
|
||||
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()>;
|
||||
|
||||
fn create_app(&self, app: App) -> FlowyResult<()>;
|
||||
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()>;
|
||||
fn read_app(&self, app_id: &str) -> FlowyResult<App>;
|
||||
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>>;
|
||||
fn delete_app(&self, app_id: &str) -> FlowyResult<App>;
|
||||
|
||||
fn create_view(&self, view: View) -> FlowyResult<()>;
|
||||
fn read_view(&self, view_id: &str) -> FlowyResult<View>;
|
||||
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>>;
|
||||
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()>;
|
||||
fn delete_view(&self, view_id: &str) -> FlowyResult<()>;
|
||||
|
||||
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()>;
|
||||
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash>;
|
||||
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()>;
|
||||
}
|
||||
|
||||
pub struct FolderPersistence {
|
||||
database: Arc<dyn WorkspaceDatabase>,
|
||||
folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>,
|
||||
}
|
||||
|
||||
impl FolderPersistence {
|
||||
pub fn new(database: Arc<dyn WorkspaceDatabase>, folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>) -> Self {
|
||||
Self {
|
||||
database,
|
||||
folder_editor,
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.0.3",
|
||||
note = "please use `begin_transaction` instead, this interface will be removed in the future"
|
||||
)]
|
||||
#[allow(dead_code)]
|
||||
pub fn begin_transaction_v_1<F, O>(&self, f: F) -> FlowyResult<O>
|
||||
where
|
||||
F: for<'a> FnOnce(Box<dyn FolderPersistenceTransaction + 'a>) -> FlowyResult<O>,
|
||||
{
|
||||
//[[immediate_transaction]]
|
||||
// https://sqlite.org/lang_transaction.html
|
||||
// IMMEDIATE cause the database connection to start a new write immediately,
|
||||
// without waiting for a write statement. The BEGIN IMMEDIATE might fail
|
||||
// with SQLITE_BUSY if another write transaction is already active on another
|
||||
// database connection.
|
||||
//
|
||||
// EXCLUSIVE is similar to IMMEDIATE in that a write transaction is started
|
||||
// immediately. EXCLUSIVE and IMMEDIATE are the same in WAL mode, but in
|
||||
// other journaling modes, EXCLUSIVE prevents other database connections from
|
||||
// reading the database while the transaction is underway.
|
||||
let conn = self.database.db_connection()?;
|
||||
conn.immediate_transaction::<_, FlowyError, _>(|| f(Box::new(V1Transaction(&conn))))
|
||||
}
|
||||
|
||||
pub async fn begin_transaction<F, O>(&self, f: F) -> FlowyResult<O>
|
||||
where
|
||||
F: FnOnce(Arc<dyn FolderPersistenceTransaction>) -> FlowyResult<O>,
|
||||
{
|
||||
match self.folder_editor.read().await.clone() {
|
||||
None => Err(FlowyError::internal().context("FolderEditor should be initialized after user login in.")),
|
||||
Some(editor) => f(editor),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn db_pool(&self) -> FlowyResult<Arc<ConnectionPool>> {
|
||||
self.database.db_pool()
|
||||
}
|
||||
|
||||
pub async fn initialize(&self, user_id: &str, folder_id: &FolderId) -> FlowyResult<()> {
|
||||
let migrations = FolderMigration::new(user_id, self.database.clone());
|
||||
if let Some(migrated_folder) = migrations.run_v1_migration()? {
|
||||
tracing::trace!("Save migration folder");
|
||||
self.save_folder(user_id, folder_id, migrated_folder).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> {
|
||||
let pool = self.database.db_pool()?;
|
||||
let delta_data = folder.delta().to_bytes();
|
||||
let md5 = folder.md5();
|
||||
let revision = Revision::new(folder_id.as_ref(), 0, 0, delta_data, user_id, md5);
|
||||
let record = RevisionRecord {
|
||||
revision,
|
||||
state: RevisionState::Sync,
|
||||
write_to_disk: true,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
let disk_cache = mk_revision_disk_cache(user_id, pool);
|
||||
disk_cache.create_revision_records(vec![record], &conn)
|
||||
}
|
||||
}
|
199
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs
Executable file
199
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs
Executable file
@ -0,0 +1,199 @@
|
||||
use crate::entities::{
|
||||
app::{App, ColorStyle, UpdateAppParams},
|
||||
trash::{Trash, TrashType},
|
||||
view::RepeatedView,
|
||||
};
|
||||
use diesel::sql_types::Binary;
|
||||
use flowy_database::{
|
||||
prelude::*,
|
||||
schema::{app_table, app_table::dsl},
|
||||
SqliteConnection,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, __private::TryFrom};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{errors::FlowyError, services::persistence::version_1::workspace_sql::WorkspaceTable};
|
||||
|
||||
pub struct AppTableSql();
|
||||
impl AppTableSql {
|
||||
pub(crate) fn create_app(app: App, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
let app_table = AppTable::new(app);
|
||||
match diesel_record_count!(app_table, &app_table.id, conn) {
|
||||
0 => diesel_insert_table!(app_table, &app_table, conn),
|
||||
_ => {
|
||||
let changeset = AppChangeset::from_table(app_table);
|
||||
diesel_update_table!(app_table, changeset, conn)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn update_app(changeset: AppChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_update_table!(app_table, changeset, conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_app(app_id: &str, conn: &SqliteConnection) -> Result<AppTable, FlowyError> {
|
||||
let filter = dsl::app_table.filter(app_table::id.eq(app_id)).into_boxed();
|
||||
let app_table = filter.first::<AppTable>(conn)?;
|
||||
Ok(app_table)
|
||||
}
|
||||
|
||||
pub(crate) fn read_workspace_apps(
|
||||
workspace_id: &str,
|
||||
conn: &SqliteConnection,
|
||||
) -> Result<Vec<AppTable>, FlowyError> {
|
||||
let app_table = dsl::app_table
|
||||
.filter(app_table::workspace_id.eq(workspace_id))
|
||||
.order(app_table::create_time.asc())
|
||||
.load::<AppTable>(conn)?;
|
||||
|
||||
Ok(app_table)
|
||||
}
|
||||
|
||||
pub(crate) fn delete_app(app_id: &str, conn: &SqliteConnection) -> Result<AppTable, FlowyError> {
|
||||
let app_table = dsl::app_table
|
||||
.filter(app_table::id.eq(app_id))
|
||||
.first::<AppTable>(conn)?;
|
||||
diesel_delete_table!(app_table, app_id, conn);
|
||||
Ok(app_table)
|
||||
}
|
||||
|
||||
// pub(crate) fn read_views_belong_to_app(
|
||||
// &self,
|
||||
// app_id: &str,
|
||||
// ) -> Result<Vec<ViewTable>, FlowyError> {
|
||||
// let conn = self.database.db_connection()?;
|
||||
//
|
||||
// let views = conn.immediate_transaction::<_, FlowyError, _>(|| {
|
||||
// let app_table: AppTable = dsl::app_table
|
||||
// .filter(app_table::id.eq(app_id))
|
||||
// .first::<AppTable>(&*(conn))?;
|
||||
// let views =
|
||||
// ViewTable::belonging_to(&app_table).load::<ViewTable>(&*conn)?;
|
||||
// Ok(views)
|
||||
// })?;
|
||||
//
|
||||
// Ok(views)
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
|
||||
#[belongs_to(WorkspaceTable, foreign_key = "workspace_id")]
|
||||
#[table_name = "app_table"]
|
||||
pub(crate) struct AppTable {
|
||||
pub id: String,
|
||||
pub workspace_id: String, // equal to #[belongs_to(Workspace, foreign_key = "workspace_id")].
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub color_style: ColorStyleCol,
|
||||
pub last_view_id: Option<String>,
|
||||
pub modified_time: i64,
|
||||
pub create_time: i64,
|
||||
pub version: i64,
|
||||
pub is_trash: bool,
|
||||
}
|
||||
|
||||
impl AppTable {
|
||||
pub fn new(app: App) -> Self {
|
||||
Self {
|
||||
id: app.id,
|
||||
workspace_id: app.workspace_id,
|
||||
name: app.name,
|
||||
desc: app.desc,
|
||||
color_style: ColorStyleCol::default(),
|
||||
last_view_id: None,
|
||||
modified_time: app.modified_time,
|
||||
create_time: app.create_time,
|
||||
version: 0,
|
||||
is_trash: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<AppTable> for Trash {
|
||||
fn from(table: AppTable) -> Self {
|
||||
Trash {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
modified_time: table.modified_time,
|
||||
create_time: table.create_time,
|
||||
ty: TrashType::App,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default, FromSqlRow, AsExpression)]
|
||||
#[sql_type = "Binary"]
|
||||
pub(crate) struct ColorStyleCol {
|
||||
pub(crate) theme_color: String,
|
||||
}
|
||||
|
||||
impl std::convert::From<ColorStyle> for ColorStyleCol {
|
||||
fn from(s: ColorStyle) -> Self {
|
||||
Self {
|
||||
theme_color: s.theme_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryInto<Vec<u8>> for &ColorStyleCol {
|
||||
type Error = String;
|
||||
|
||||
fn try_into(self) -> Result<Vec<u8>, Self::Error> {
|
||||
bincode::serialize(self).map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<&[u8]> for ColorStyleCol {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
bincode::deserialize(value).map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl_sql_binary_expression!(ColorStyleCol);
|
||||
|
||||
#[derive(AsChangeset, Identifiable, Default, Debug)]
|
||||
#[table_name = "app_table"]
|
||||
pub struct AppChangeset {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub is_trash: Option<bool>,
|
||||
}
|
||||
|
||||
impl AppChangeset {
|
||||
pub(crate) fn new(params: UpdateAppParams) -> Self {
|
||||
AppChangeset {
|
||||
id: params.app_id,
|
||||
name: params.name,
|
||||
desc: params.desc,
|
||||
is_trash: params.is_trash,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_table(table: AppTable) -> Self {
|
||||
AppChangeset {
|
||||
id: table.id,
|
||||
name: Some(table.name),
|
||||
desc: Some(table.desc),
|
||||
is_trash: Some(table.is_trash),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::convert::From<AppTable> for App {
|
||||
fn from(table: AppTable) -> Self {
|
||||
App {
|
||||
id: table.id,
|
||||
workspace_id: table.workspace_id,
|
||||
name: table.name,
|
||||
desc: table.desc,
|
||||
belongings: RepeatedView::default(),
|
||||
version: table.version,
|
||||
modified_time: table.modified_time,
|
||||
create_time: table.create_time,
|
||||
}
|
||||
}
|
||||
}
|
5
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/mod.rs
Executable file
5
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/mod.rs
Executable file
@ -0,0 +1,5 @@
|
||||
pub mod app_sql;
|
||||
pub mod trash_sql;
|
||||
pub mod v1_impl;
|
||||
pub mod view_sql;
|
||||
pub mod workspace_sql;
|
146
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/trash_sql.rs
Executable file
146
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/trash_sql.rs
Executable file
@ -0,0 +1,146 @@
|
||||
use crate::{
|
||||
entities::trash::{RepeatedTrash, Trash, TrashType},
|
||||
errors::FlowyError,
|
||||
};
|
||||
use diesel::sql_types::Integer;
|
||||
use flowy_database::{
|
||||
prelude::*,
|
||||
schema::{trash_table, trash_table::dsl},
|
||||
SqliteConnection,
|
||||
};
|
||||
|
||||
pub struct TrashTableSql();
|
||||
impl TrashTableSql {
|
||||
pub(crate) fn create_trash(trashes: Vec<Trash>, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
for trash in trashes {
|
||||
let trash_table: TrashTable = trash.into();
|
||||
match diesel_record_count!(trash_table, &trash_table.id, conn) {
|
||||
0 => diesel_insert_table!(trash_table, &trash_table, conn),
|
||||
_ => {
|
||||
let changeset = TrashChangeset::from(trash_table);
|
||||
diesel_update_table!(trash_table, changeset, conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_all(conn: &SqliteConnection) -> Result<RepeatedTrash, FlowyError> {
|
||||
let trash_tables = dsl::trash_table.load::<TrashTable>(conn)?;
|
||||
let items = trash_tables.into_iter().map(|t| t.into()).collect::<Vec<Trash>>();
|
||||
Ok(RepeatedTrash { items })
|
||||
}
|
||||
|
||||
pub(crate) fn delete_all(conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
let _ = diesel::delete(dsl::trash_table).execute(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read(trash_id: &str, conn: &SqliteConnection) -> Result<TrashTable, FlowyError> {
|
||||
let trash_table = dsl::trash_table
|
||||
.filter(trash_table::id.eq(trash_id))
|
||||
.first::<TrashTable>(conn)?;
|
||||
Ok(trash_table)
|
||||
}
|
||||
|
||||
pub(crate) fn delete_trash(trash_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_delete_table!(trash_table, trash_id, conn);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
|
||||
#[table_name = "trash_table"]
|
||||
pub(crate) struct TrashTable {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub modified_time: i64,
|
||||
pub create_time: i64,
|
||||
pub ty: SqlTrashType,
|
||||
}
|
||||
impl std::convert::From<TrashTable> for Trash {
|
||||
fn from(table: TrashTable) -> Self {
|
||||
Trash {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
modified_time: table.modified_time,
|
||||
create_time: table.create_time,
|
||||
ty: table.ty.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Trash> for TrashTable {
|
||||
fn from(trash: Trash) -> Self {
|
||||
TrashTable {
|
||||
id: trash.id,
|
||||
name: trash.name,
|
||||
desc: "".to_owned(),
|
||||
modified_time: trash.modified_time,
|
||||
create_time: trash.create_time,
|
||||
ty: trash.ty.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
|
||||
#[table_name = "trash_table"]
|
||||
pub(crate) struct TrashChangeset {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub modified_time: i64,
|
||||
}
|
||||
|
||||
impl std::convert::From<TrashTable> for TrashChangeset {
|
||||
fn from(trash: TrashTable) -> Self {
|
||||
TrashChangeset {
|
||||
id: trash.id,
|
||||
name: Some(trash.name),
|
||||
modified_time: trash.modified_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
|
||||
#[repr(i32)]
|
||||
#[sql_type = "Integer"]
|
||||
pub(crate) enum SqlTrashType {
|
||||
Unknown = 0,
|
||||
View = 1,
|
||||
App = 2,
|
||||
}
|
||||
|
||||
impl std::convert::From<i32> for SqlTrashType {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => SqlTrashType::Unknown,
|
||||
1 => SqlTrashType::View,
|
||||
2 => SqlTrashType::App,
|
||||
_o => SqlTrashType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_sql_integer_expression!(SqlTrashType);
|
||||
|
||||
impl std::convert::From<SqlTrashType> for TrashType {
|
||||
fn from(ty: SqlTrashType) -> Self {
|
||||
match ty {
|
||||
SqlTrashType::Unknown => TrashType::Unknown,
|
||||
SqlTrashType::View => TrashType::View,
|
||||
SqlTrashType::App => TrashType::App,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<TrashType> for SqlTrashType {
|
||||
fn from(ty: TrashType) -> Self {
|
||||
match ty {
|
||||
TrashType::Unknown => SqlTrashType::Unknown,
|
||||
TrashType::View => SqlTrashType::View,
|
||||
TrashType::App => SqlTrashType::App,
|
||||
}
|
||||
}
|
||||
}
|
194
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/v1_impl.rs
Executable file
194
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/v1_impl.rs
Executable file
@ -0,0 +1,194 @@
|
||||
use crate::services::persistence::{
|
||||
version_1::{
|
||||
app_sql::{AppChangeset, AppTableSql},
|
||||
view_sql::{ViewChangeset, ViewTableSql},
|
||||
workspace_sql::{WorkspaceChangeset, WorkspaceTableSql},
|
||||
},
|
||||
FolderPersistenceTransaction, TrashTableSql,
|
||||
};
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::App,
|
||||
trash::{RepeatedTrash, Trash},
|
||||
view::View,
|
||||
workspace::Workspace,
|
||||
};
|
||||
use lib_sqlite::DBConnection;
|
||||
|
||||
pub struct V1Transaction<'a>(pub &'a DBConnection);
|
||||
|
||||
impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
|
||||
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
|
||||
let _ = WorkspaceTableSql::create_workspace(user_id, workspace, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
|
||||
let tables = WorkspaceTableSql::read_workspaces(user_id, workspace_id, &*self.0)?;
|
||||
let workspaces = tables.into_iter().map(Workspace::from).collect::<Vec<_>>();
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
|
||||
WorkspaceTableSql::update_workspace(changeset, &*self.0)
|
||||
}
|
||||
|
||||
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
|
||||
WorkspaceTableSql::delete_workspace(workspace_id, &*self.0)
|
||||
}
|
||||
|
||||
fn create_app(&self, app: App) -> FlowyResult<()> {
|
||||
let _ = AppTableSql::create_app(app, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
|
||||
let _ = AppTableSql::update_app(changeset, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
let table = AppTableSql::read_app(app_id, &*self.0)?;
|
||||
Ok(App::from(table))
|
||||
}
|
||||
|
||||
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
|
||||
let tables = AppTableSql::read_workspace_apps(workspace_id, &*self.0)?;
|
||||
let apps = tables.into_iter().map(App::from).collect::<Vec<_>>();
|
||||
Ok(apps)
|
||||
}
|
||||
|
||||
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
let table = AppTableSql::delete_app(app_id, &*self.0)?;
|
||||
Ok(App::from(table))
|
||||
}
|
||||
|
||||
fn create_view(&self, view: View) -> FlowyResult<()> {
|
||||
let _ = ViewTableSql::create_view(view, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
|
||||
let table = ViewTableSql::read_view(view_id, &*self.0)?;
|
||||
Ok(View::from(table))
|
||||
}
|
||||
|
||||
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
|
||||
let tables = ViewTableSql::read_views(belong_to_id, &*self.0)?;
|
||||
let views = tables.into_iter().map(View::from).collect::<Vec<_>>();
|
||||
Ok(views)
|
||||
}
|
||||
|
||||
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
|
||||
let _ = ViewTableSql::update_view(changeset, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
let _ = ViewTableSql::delete_view(view_id, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
|
||||
let _ = TrashTableSql::create_trash(trashes, &*self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
|
||||
match trash_id {
|
||||
None => TrashTableSql::read_all(&*self.0),
|
||||
Some(trash_id) => {
|
||||
let table = TrashTableSql::read(&trash_id, &*self.0)?;
|
||||
Ok(RepeatedTrash {
|
||||
items: vec![Trash::from(table)],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
|
||||
match trash_ids {
|
||||
None => TrashTableSql::delete_all(&*self.0),
|
||||
Some(trash_ids) => {
|
||||
for trash_id in &trash_ids {
|
||||
let _ = TrashTableSql::delete_trash(trash_id, &*self.0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.reddit.com/r/rust/comments/droxdg/why_arent_traits_impld_for_boxdyn_trait/
|
||||
impl<T> FolderPersistenceTransaction for Box<T>
|
||||
where
|
||||
T: FolderPersistenceTransaction + ?Sized,
|
||||
{
|
||||
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
|
||||
(**self).create_workspace(user_id, workspace)
|
||||
}
|
||||
|
||||
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
|
||||
(**self).read_workspaces(user_id, workspace_id)
|
||||
}
|
||||
|
||||
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
|
||||
(**self).update_workspace(changeset)
|
||||
}
|
||||
|
||||
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
|
||||
(**self).delete_workspace(workspace_id)
|
||||
}
|
||||
|
||||
fn create_app(&self, app: App) -> FlowyResult<()> {
|
||||
(**self).create_app(app)
|
||||
}
|
||||
|
||||
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
|
||||
(**self).update_app(changeset)
|
||||
}
|
||||
|
||||
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
(**self).read_app(app_id)
|
||||
}
|
||||
|
||||
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
|
||||
(**self).read_workspace_apps(workspace_id)
|
||||
}
|
||||
|
||||
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
(**self).delete_app(app_id)
|
||||
}
|
||||
|
||||
fn create_view(&self, view: View) -> FlowyResult<()> {
|
||||
(**self).create_view(view)
|
||||
}
|
||||
|
||||
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
|
||||
(**self).read_view(view_id)
|
||||
}
|
||||
|
||||
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
|
||||
(**self).read_views(belong_to_id)
|
||||
}
|
||||
|
||||
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
|
||||
(**self).update_view(changeset)
|
||||
}
|
||||
|
||||
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
(**self).delete_view(view_id)
|
||||
}
|
||||
|
||||
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
|
||||
(**self).create_trash(trashes)
|
||||
}
|
||||
|
||||
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
|
||||
(**self).read_trash(trash_id)
|
||||
}
|
||||
|
||||
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
|
||||
(**self).delete_trash(trash_ids)
|
||||
}
|
||||
}
|
245
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs
Executable file
245
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs
Executable file
@ -0,0 +1,245 @@
|
||||
use crate::{
|
||||
entities::{
|
||||
trash::{Trash, TrashType},
|
||||
view::{RepeatedView, UpdateViewParams, View, ViewType},
|
||||
},
|
||||
errors::FlowyError,
|
||||
services::persistence::version_1::app_sql::AppTable,
|
||||
};
|
||||
use diesel::sql_types::Integer;
|
||||
use flowy_database::{
|
||||
prelude::*,
|
||||
schema::{view_table, view_table::dsl},
|
||||
SqliteConnection,
|
||||
};
|
||||
use lib_infra::timestamp;
|
||||
|
||||
pub struct ViewTableSql();
|
||||
impl ViewTableSql {
|
||||
pub(crate) fn create_view(view: View, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
let view_table = ViewTable::new(view);
|
||||
match diesel_record_count!(view_table, &view_table.id, conn) {
|
||||
0 => diesel_insert_table!(view_table, &view_table, conn),
|
||||
_ => {
|
||||
let changeset = ViewChangeset::from_table(view_table);
|
||||
diesel_update_table!(view_table, changeset, conn)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_view(view_id: &str, conn: &SqliteConnection) -> Result<ViewTable, FlowyError> {
|
||||
// https://docs.diesel.rs/diesel/query_builder/struct.UpdateStatement.html
|
||||
// let mut filter =
|
||||
// dsl::view_table.filter(view_table::id.eq(view_id)).into_boxed();
|
||||
// if let Some(is_trash) = is_trash {
|
||||
// filter = filter.filter(view_table::is_trash.eq(is_trash));
|
||||
// }
|
||||
// let repeated_view = filter.first::<ViewTable>(conn)?;
|
||||
let view_table = dsl::view_table
|
||||
.filter(view_table::id.eq(view_id))
|
||||
.first::<ViewTable>(conn)?;
|
||||
|
||||
Ok(view_table)
|
||||
}
|
||||
|
||||
// belong_to_id will be the app_id or view_id.
|
||||
pub(crate) fn read_views(belong_to_id: &str, conn: &SqliteConnection) -> Result<Vec<ViewTable>, FlowyError> {
|
||||
let view_tables = dsl::view_table
|
||||
.filter(view_table::belong_to_id.eq(belong_to_id))
|
||||
.order(view_table::create_time.asc())
|
||||
.into_boxed()
|
||||
.load::<ViewTable>(conn)?;
|
||||
|
||||
Ok(view_tables)
|
||||
}
|
||||
|
||||
pub(crate) fn update_view(changeset: ViewChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_update_table!(view_table, changeset, conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn delete_view(view_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_delete_table!(view_table, view_id, conn);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn read_views(
|
||||
// belong_to_id: &str,
|
||||
// is_trash: Option<bool>,
|
||||
// conn: &SqliteConnection,
|
||||
// ) -> Result<RepeatedView, FlowyError> {
|
||||
// let views = dsl::view_table
|
||||
// .inner_join(trash_table::dsl::trash_table.on(trash_id.ne(view_table::
|
||||
// id))) .filter(view_table::belong_to_id.eq(belong_to_id))
|
||||
// .select((
|
||||
// view_table::id,
|
||||
// view_table::belong_to_id,
|
||||
// view_table::name,
|
||||
// view_table::desc,
|
||||
// view_table::modified_time,
|
||||
// view_table::create_time,
|
||||
// view_table::thumbnail,
|
||||
// view_table::view_type,
|
||||
// view_table::version,
|
||||
// ))
|
||||
// .load(conn)?
|
||||
// .into_iter()
|
||||
// .map(
|
||||
// |(id, belong_to_id, name, desc, create_time, modified_time,
|
||||
// thumbnail, view_type, version)| { ViewTable {
|
||||
// id,
|
||||
// belong_to_id,
|
||||
// name,
|
||||
// desc,
|
||||
// modified_time,
|
||||
// create_time,
|
||||
// thumbnail,
|
||||
// view_type,
|
||||
// version,
|
||||
// is_trash: false,
|
||||
// }
|
||||
// .into()
|
||||
// },
|
||||
// )
|
||||
// .collect::<Vec<View>>();
|
||||
//
|
||||
// Ok(RepeatedView { items: views })
|
||||
// }
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
|
||||
#[belongs_to(AppTable, foreign_key = "belong_to_id")]
|
||||
#[table_name = "view_table"]
|
||||
pub(crate) struct ViewTable {
|
||||
pub id: String,
|
||||
pub belong_to_id: String,
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub modified_time: i64,
|
||||
pub create_time: i64,
|
||||
pub thumbnail: String,
|
||||
pub view_type: ViewTableType,
|
||||
pub version: i64,
|
||||
pub is_trash: bool,
|
||||
}
|
||||
|
||||
impl ViewTable {
|
||||
pub fn new(view: View) -> Self {
|
||||
let view_type = match view.view_type {
|
||||
ViewType::Blank => ViewTableType::Docs,
|
||||
ViewType::Doc => ViewTableType::Docs,
|
||||
};
|
||||
|
||||
ViewTable {
|
||||
id: view.id,
|
||||
belong_to_id: view.belong_to_id,
|
||||
name: view.name,
|
||||
desc: view.desc,
|
||||
modified_time: view.modified_time,
|
||||
create_time: view.create_time,
|
||||
// TODO: thumbnail
|
||||
thumbnail: "".to_owned(),
|
||||
view_type,
|
||||
version: 0,
|
||||
is_trash: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<ViewTable> for View {
|
||||
fn from(table: ViewTable) -> Self {
|
||||
let view_type = match table.view_type {
|
||||
ViewTableType::Docs => ViewType::Doc,
|
||||
};
|
||||
|
||||
View {
|
||||
id: table.id,
|
||||
belong_to_id: table.belong_to_id,
|
||||
name: table.name,
|
||||
desc: table.desc,
|
||||
view_type,
|
||||
belongings: RepeatedView::default(),
|
||||
modified_time: table.modified_time,
|
||||
version: table.version,
|
||||
create_time: table.create_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<ViewTable> for Trash {
|
||||
fn from(table: ViewTable) -> Self {
|
||||
Trash {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
modified_time: table.modified_time,
|
||||
create_time: table.create_time,
|
||||
ty: TrashType::View,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
|
||||
#[table_name = "view_table"]
|
||||
pub struct ViewChangeset {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub modified_time: i64,
|
||||
}
|
||||
|
||||
impl ViewChangeset {
|
||||
pub(crate) fn new(params: UpdateViewParams) -> Self {
|
||||
ViewChangeset {
|
||||
id: params.view_id,
|
||||
name: params.name,
|
||||
desc: params.desc,
|
||||
thumbnail: params.thumbnail,
|
||||
modified_time: timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_table(table: ViewTable) -> Self {
|
||||
ViewChangeset {
|
||||
id: table.id,
|
||||
name: Some(table.name),
|
||||
desc: Some(table.desc),
|
||||
thumbnail: Some(table.thumbnail),
|
||||
modified_time: table.modified_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
|
||||
#[repr(i32)]
|
||||
#[sql_type = "Integer"]
|
||||
pub enum ViewTableType {
|
||||
Docs = 0,
|
||||
}
|
||||
|
||||
impl std::default::Default for ViewTableType {
|
||||
fn default() -> Self {
|
||||
ViewTableType::Docs
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<i32> for ViewTableType {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => ViewTableType::Docs,
|
||||
o => {
|
||||
log::error!("Unsupported view type {}, fallback to ViewType::Docs", o);
|
||||
ViewTableType::Docs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewTableType {
|
||||
pub fn value(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl_sql_integer_expression!(ViewTableType);
|
@ -0,0 +1,127 @@
|
||||
use crate::{
|
||||
entities::{
|
||||
app::RepeatedApp,
|
||||
workspace::{UpdateWorkspaceParams, Workspace},
|
||||
},
|
||||
errors::FlowyError,
|
||||
};
|
||||
use diesel::SqliteConnection;
|
||||
use flowy_database::{
|
||||
prelude::*,
|
||||
schema::{workspace_table, workspace_table::dsl},
|
||||
};
|
||||
pub(crate) struct WorkspaceTableSql();
|
||||
impl WorkspaceTableSql {
|
||||
pub(crate) fn create_workspace(
|
||||
user_id: &str,
|
||||
workspace: Workspace,
|
||||
conn: &SqliteConnection,
|
||||
) -> Result<(), FlowyError> {
|
||||
let table = WorkspaceTable::new(workspace, user_id);
|
||||
match diesel_record_count!(workspace_table, &table.id, conn) {
|
||||
0 => diesel_insert_table!(workspace_table, &table, conn),
|
||||
_ => {
|
||||
let changeset = WorkspaceChangeset::from_table(table);
|
||||
diesel_update_table!(workspace_table, changeset, conn);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_workspaces(
|
||||
user_id: &str,
|
||||
workspace_id: Option<String>,
|
||||
conn: &SqliteConnection,
|
||||
) -> Result<Vec<WorkspaceTable>, FlowyError> {
|
||||
let mut filter = dsl::workspace_table
|
||||
.filter(workspace_table::user_id.eq(user_id))
|
||||
.order(workspace_table::create_time.asc())
|
||||
.into_boxed();
|
||||
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
filter = filter.filter(workspace_table::id.eq(workspace_id));
|
||||
};
|
||||
|
||||
let workspaces = filter.load::<WorkspaceTable>(conn)?;
|
||||
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn update_workspace(changeset: WorkspaceChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_update_table!(workspace_table, changeset, conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn delete_workspace(workspace_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
|
||||
diesel_delete_table!(workspace_table, workspace_id, conn);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable)]
|
||||
#[table_name = "workspace_table"]
|
||||
pub struct WorkspaceTable {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub modified_time: i64,
|
||||
pub create_time: i64,
|
||||
pub user_id: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
impl WorkspaceTable {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(workspace: Workspace, user_id: &str) -> Self {
|
||||
WorkspaceTable {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
desc: workspace.desc,
|
||||
modified_time: workspace.modified_time,
|
||||
create_time: workspace.create_time,
|
||||
user_id: user_id.to_owned(),
|
||||
version: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<WorkspaceTable> for Workspace {
|
||||
fn from(table: WorkspaceTable) -> Self {
|
||||
Workspace {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
desc: table.desc,
|
||||
apps: RepeatedApp::default(),
|
||||
modified_time: table.modified_time,
|
||||
create_time: table.create_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
|
||||
#[table_name = "workspace_table"]
|
||||
pub struct WorkspaceChangeset {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
impl WorkspaceChangeset {
|
||||
pub fn new(params: UpdateWorkspaceParams) -> Self {
|
||||
WorkspaceChangeset {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
desc: params.desc,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_table(table: WorkspaceTable) -> Self {
|
||||
WorkspaceChangeset {
|
||||
id: table.id,
|
||||
name: Some(table.name),
|
||||
desc: Some(table.desc),
|
||||
}
|
||||
}
|
||||
}
|
1
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/mod.rs
Executable file
1
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/mod.rs
Executable file
@ -0,0 +1 @@
|
||||
pub mod v2_impl;
|
212
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs
Executable file
212
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs
Executable file
@ -0,0 +1,212 @@
|
||||
use crate::services::{
|
||||
folder_editor::FolderEditor,
|
||||
persistence::{AppChangeset, FolderPersistenceTransaction, ViewChangeset, WorkspaceChangeset},
|
||||
};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::App,
|
||||
trash::{RepeatedTrash, Trash},
|
||||
view::View,
|
||||
workspace::Workspace,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
impl FolderPersistenceTransaction for FolderEditor {
|
||||
fn create_workspace(&self, _user_id: &str, workspace: Workspace) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().create_workspace(workspace)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_workspaces(&self, _user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
|
||||
let workspaces = self.folder.read().read_workspaces(workspace_id)?;
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
|
||||
if let Some(change) = self
|
||||
.folder
|
||||
.write()
|
||||
.update_workspace(&changeset.id, changeset.name, changeset.desc)?
|
||||
{
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().delete_workspace(workspace_id)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_app(&self, app: App) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().create_app(app)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
|
||||
if let Some(change) = self
|
||||
.folder
|
||||
.write()
|
||||
.update_app(&changeset.id, changeset.name, changeset.desc)?
|
||||
{
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
let app = self.folder.read().read_app(app_id)?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
|
||||
let workspaces = self.folder.read().read_workspaces(Some(workspace_id.to_owned()))?;
|
||||
match workspaces.first() {
|
||||
None => {
|
||||
Err(FlowyError::record_not_found().context(format!("can't find workspace with id {}", workspace_id)))
|
||||
}
|
||||
Some(workspace) => Ok(workspace.apps.clone().take_items()),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
let app = self.folder.read().read_app(app_id)?;
|
||||
if let Some(change) = self.folder.write().delete_app(app_id)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn create_view(&self, view: View) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().create_view(view)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
|
||||
let view = self.folder.read().read_view(view_id)?;
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
|
||||
let views = self.folder.read().read_views(belong_to_id)?;
|
||||
Ok(views)
|
||||
}
|
||||
|
||||
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
|
||||
if let Some(change) =
|
||||
self.folder
|
||||
.write()
|
||||
.update_view(&changeset.id, changeset.name, changeset.desc, changeset.modified_time)?
|
||||
{
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().delete_view(view_id)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().create_trash(trashes)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
|
||||
let trash = self.folder.read().read_trash(trash_id)?;
|
||||
Ok(RepeatedTrash { items: trash })
|
||||
}
|
||||
|
||||
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
|
||||
if let Some(change) = self.folder.write().delete_trash(trash_ids)? {
|
||||
let _ = self.apply_change(change)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FolderPersistenceTransaction for Arc<T>
|
||||
where
|
||||
T: FolderPersistenceTransaction + ?Sized,
|
||||
{
|
||||
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
|
||||
(**self).create_workspace(user_id, workspace)
|
||||
}
|
||||
|
||||
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
|
||||
(**self).read_workspaces(user_id, workspace_id)
|
||||
}
|
||||
|
||||
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
|
||||
(**self).update_workspace(changeset)
|
||||
}
|
||||
|
||||
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
|
||||
(**self).delete_workspace(workspace_id)
|
||||
}
|
||||
|
||||
fn create_app(&self, app: App) -> FlowyResult<()> {
|
||||
(**self).create_app(app)
|
||||
}
|
||||
|
||||
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
|
||||
(**self).update_app(changeset)
|
||||
}
|
||||
|
||||
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
(**self).read_app(app_id)
|
||||
}
|
||||
|
||||
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
|
||||
(**self).read_workspace_apps(workspace_id)
|
||||
}
|
||||
|
||||
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
|
||||
(**self).delete_app(app_id)
|
||||
}
|
||||
|
||||
fn create_view(&self, view: View) -> FlowyResult<()> {
|
||||
(**self).create_view(view)
|
||||
}
|
||||
|
||||
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
|
||||
(**self).read_view(view_id)
|
||||
}
|
||||
|
||||
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
|
||||
(**self).read_views(belong_to_id)
|
||||
}
|
||||
|
||||
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
|
||||
(**self).update_view(changeset)
|
||||
}
|
||||
|
||||
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
(**self).delete_view(view_id)
|
||||
}
|
||||
|
||||
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
|
||||
(**self).create_trash(trashes)
|
||||
}
|
||||
|
||||
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
|
||||
(**self).read_trash(trash_id)
|
||||
}
|
||||
|
||||
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
|
||||
(**self).delete_trash(trash_ids)
|
||||
}
|
||||
}
|
331
frontend/rust-lib/flowy-folder/src/services/trash/controller.rs
Executable file
331
frontend/rust-lib/flowy-folder/src/services/trash/controller.rs
Executable file
@ -0,0 +1,331 @@
|
||||
use crate::{
|
||||
dart_notification::{send_anonymous_dart_notification, FolderNotification},
|
||||
entities::trash::{RepeatedTrash, RepeatedTrashId, Trash, TrashId, TrashType},
|
||||
errors::{FlowyError, FlowyResult},
|
||||
module::{FolderCouldServiceV1, WorkspaceUser},
|
||||
services::persistence::{FolderPersistence, FolderPersistenceTransaction},
|
||||
};
|
||||
|
||||
use std::{fmt::Formatter, sync::Arc};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
pub struct TrashController {
|
||||
persistence: Arc<FolderPersistence>,
|
||||
notify: broadcast::Sender<TrashEvent>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
}
|
||||
|
||||
impl TrashController {
|
||||
pub fn new(
|
||||
persistence: Arc<FolderPersistence>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
) -> Self {
|
||||
let (tx, _) = broadcast::channel(10);
|
||||
Self {
|
||||
persistence,
|
||||
notify: tx,
|
||||
cloud_service,
|
||||
user,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), fields(putback) err)]
|
||||
pub async fn putback(&self, trash_id: &str) -> FlowyResult<()> {
|
||||
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
|
||||
let trash = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut repeated_trash = transaction.read_trash(Some(trash_id.to_owned()))?;
|
||||
let _ = transaction.delete_trash(Some(vec![trash_id.to_owned()]))?;
|
||||
notify_trash_changed(transaction.read_trash(None)?);
|
||||
|
||||
if repeated_trash.is_empty() {
|
||||
return Err(FlowyError::internal().context("Try to put back trash is not exists"));
|
||||
}
|
||||
Ok(repeated_trash.pop().unwrap())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let identifier = TrashId {
|
||||
id: trash.id,
|
||||
ty: trash.ty,
|
||||
};
|
||||
|
||||
let _ = self.delete_trash_on_server(RepeatedTrashId {
|
||||
items: vec![identifier.clone()],
|
||||
delete_all: false,
|
||||
})?;
|
||||
|
||||
tracing::Span::current().record("putback", &format!("{:?}", &identifier).as_str());
|
||||
let _ = self.notify.send(TrashEvent::Putback(vec![identifier].into(), tx));
|
||||
let _ = rx.recv().await.unwrap()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self) err)]
|
||||
pub async fn restore_all_trash(&self) -> FlowyResult<()> {
|
||||
let repeated_trash = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let trash = transaction.read_trash(None);
|
||||
let _ = transaction.delete_trash(None);
|
||||
trash
|
||||
})
|
||||
.await?;
|
||||
|
||||
let identifiers: RepeatedTrashId = repeated_trash.items.clone().into();
|
||||
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
|
||||
let _ = self.notify.send(TrashEvent::Putback(identifiers, tx));
|
||||
let _ = rx.recv().await;
|
||||
|
||||
notify_trash_changed(RepeatedTrash { items: vec![] });
|
||||
let _ = self.delete_all_trash_on_server().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn delete_all_trash(&self) -> FlowyResult<()> {
|
||||
let repeated_trash = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| transaction.read_trash(None))
|
||||
.await?;
|
||||
let trash_identifiers: RepeatedTrashId = repeated_trash.items.clone().into();
|
||||
let _ = self.delete_with_identifiers(trash_identifiers.clone()).await?;
|
||||
|
||||
notify_trash_changed(RepeatedTrash { items: vec![] });
|
||||
let _ = self.delete_all_trash_on_server().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn delete(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> {
|
||||
let _ = self.delete_with_identifiers(trash_identifiers.clone()).await?;
|
||||
let repeated_trash = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| transaction.read_trash(None))
|
||||
.await?;
|
||||
notify_trash_changed(repeated_trash);
|
||||
let _ = self.delete_trash_on_server(trash_identifiers)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), fields(delete_trash_ids), err)]
|
||||
pub async fn delete_with_identifiers(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> {
|
||||
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
|
||||
tracing::Span::current().record("delete_trash_ids", &format!("{}", trash_identifiers).as_str());
|
||||
let _ = self.notify.send(TrashEvent::Delete(trash_identifiers.clone(), tx));
|
||||
|
||||
match rx.recv().await {
|
||||
None => {}
|
||||
Some(result) => match result {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("{}", e),
|
||||
},
|
||||
}
|
||||
let _ = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let ids = trash_identifiers
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.id)
|
||||
.collect::<Vec<_>>();
|
||||
transaction.delete_trash(Some(ids))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// [[ transaction ]]
|
||||
// https://www.tutlane.com/tutorial/sqlite/sqlite-transactions-begin-commit-rollback
|
||||
// We can use these commands only when we are performing INSERT, UPDATE, and
|
||||
// DELETE operations. It’s not possible for us to use these commands to
|
||||
// CREATE and DROP tables operations because those are auto-commit in the
|
||||
// database.
|
||||
#[tracing::instrument(name = "add_trash", level = "debug", skip(self, trash), fields(trash_ids), err)]
|
||||
pub async fn add<T: Into<Trash>>(&self, trash: Vec<T>) -> Result<(), FlowyError> {
|
||||
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
|
||||
let repeated_trash = trash.into_iter().map(|t| t.into()).collect::<Vec<Trash>>();
|
||||
let identifiers = repeated_trash.iter().map(|t| t.into()).collect::<Vec<TrashId>>();
|
||||
|
||||
tracing::Span::current().record(
|
||||
"trash_ids",
|
||||
&format!(
|
||||
"{:?}",
|
||||
identifiers
|
||||
.iter()
|
||||
.map(|identifier| format!("{:?}:{}", identifier.ty, identifier.id))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.create_trash(repeated_trash.clone())?;
|
||||
let _ = self.create_trash_on_server(repeated_trash);
|
||||
notify_trash_changed(transaction.read_trash(None)?);
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
let _ = self.notify.send(TrashEvent::NewTrash(identifiers.into(), tx));
|
||||
let _ = rx.recv().await.unwrap()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<TrashEvent> {
|
||||
self.notify.subscribe()
|
||||
}
|
||||
|
||||
pub async fn read_trash(&self) -> Result<RepeatedTrash, FlowyError> {
|
||||
let repeated_trash = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| transaction.read_trash(None))
|
||||
.await?;
|
||||
let _ = self.read_trash_on_server()?;
|
||||
Ok(repeated_trash)
|
||||
}
|
||||
|
||||
pub fn read_trash_ids<'a>(
|
||||
&self,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> Result<Vec<String>, FlowyError> {
|
||||
let ids = transaction
|
||||
.read_trash(None)?
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|item| item.id)
|
||||
.collect::<Vec<String>>();
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
|
||||
impl TrashController {
|
||||
#[tracing::instrument(level = "trace", skip(self, trash), err)]
|
||||
fn create_trash_on_server<T: Into<RepeatedTrashId>>(&self, trash: T) -> FlowyResult<()> {
|
||||
let token = self.user.token()?;
|
||||
let trash_identifiers = trash.into();
|
||||
let server = self.cloud_service.clone();
|
||||
// TODO: retry?
|
||||
let _ = tokio::spawn(async move {
|
||||
match server.create_trash(&token, trash_identifiers).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Create trash failed: {:?}", e),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, trash), err)]
|
||||
fn delete_trash_on_server<T: Into<RepeatedTrashId>>(&self, trash: T) -> FlowyResult<()> {
|
||||
let token = self.user.token()?;
|
||||
let trash_identifiers = trash.into();
|
||||
let server = self.cloud_service.clone();
|
||||
let _ = tokio::spawn(async move {
|
||||
match server.delete_trash(&token, trash_identifiers).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Delete trash failed: {:?}", e),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
fn read_trash_on_server(&self) -> FlowyResult<()> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
let persistence = self.persistence.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match server.read_trash(&token).await {
|
||||
Ok(repeated_trash) => {
|
||||
tracing::debug!("Remote trash count: {}", repeated_trash.items.len());
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.create_trash(repeated_trash.items.clone())?;
|
||||
transaction.read_trash(None)
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(repeated_trash) => {
|
||||
notify_trash_changed(repeated_trash);
|
||||
}
|
||||
Err(e) => log::error!("Save trash failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("Read trash failed: {:?}", e),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
async fn delete_all_trash_on_server(&self) -> FlowyResult<()> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
server.delete_trash(&token, RepeatedTrashId::all()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(repeated_trash), fields(n_trash))]
|
||||
fn notify_trash_changed(repeated_trash: RepeatedTrash) {
|
||||
tracing::Span::current().record("n_trash", &repeated_trash.len());
|
||||
send_anonymous_dart_notification(FolderNotification::TrashUpdated)
|
||||
.payload(repeated_trash)
|
||||
.send();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TrashEvent {
|
||||
NewTrash(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
|
||||
Putback(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
|
||||
Delete(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TrashEvent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TrashEvent::NewTrash(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
|
||||
TrashEvent::Putback(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
|
||||
TrashEvent::Delete(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TrashEvent {
|
||||
pub fn select(self, s: TrashType) -> Option<TrashEvent> {
|
||||
match self {
|
||||
TrashEvent::Putback(mut identifiers, sender) => {
|
||||
identifiers.items.retain(|item| item.ty == s);
|
||||
if identifiers.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TrashEvent::Putback(identifiers, sender))
|
||||
}
|
||||
}
|
||||
TrashEvent::Delete(mut identifiers, sender) => {
|
||||
identifiers.items.retain(|item| item.ty == s);
|
||||
if identifiers.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TrashEvent::Delete(identifiers, sender))
|
||||
}
|
||||
}
|
||||
TrashEvent::NewTrash(mut identifiers, sender) => {
|
||||
identifiers.items.retain(|item| item.ty == s);
|
||||
if identifiers.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TrashEvent::NewTrash(identifiers, sender))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs
Executable file
45
frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs
Executable file
@ -0,0 +1,45 @@
|
||||
use crate::{
|
||||
entities::trash::{RepeatedTrash, RepeatedTrashId, TrashId},
|
||||
errors::FlowyError,
|
||||
services::TrashController,
|
||||
};
|
||||
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tracing::instrument(skip(controller), err)]
|
||||
pub(crate) async fn read_trash_handler(
|
||||
controller: Unit<Arc<TrashController>>,
|
||||
) -> DataResult<RepeatedTrash, FlowyError> {
|
||||
let repeated_trash = controller.read_trash().await?;
|
||||
data_result(repeated_trash)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(identifier, controller), err)]
|
||||
pub(crate) async fn putback_trash_handler(
|
||||
identifier: Data<TrashId>,
|
||||
controller: Unit<Arc<TrashController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let _ = controller.putback(&identifier.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(identifiers, controller), err)]
|
||||
pub(crate) async fn delete_trash_handler(
|
||||
identifiers: Data<RepeatedTrashId>,
|
||||
controller: Unit<Arc<TrashController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let _ = controller.delete(identifiers.into_inner()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(controller), err)]
|
||||
pub(crate) async fn restore_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
|
||||
let _ = controller.restore_all_trash().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(controller), err)]
|
||||
pub(crate) async fn delete_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
|
||||
let _ = controller.delete_all_trash().await?;
|
||||
Ok(())
|
||||
}
|
2
frontend/rust-lib/flowy-folder/src/services/trash/mod.rs
Executable file
2
frontend/rust-lib/flowy-folder/src/services/trash/mod.rs
Executable file
@ -0,0 +1,2 @@
|
||||
pub mod controller;
|
||||
pub mod event_handler;
|
434
frontend/rust-lib/flowy-folder/src/services/view/controller.rs
Executable file
434
frontend/rust-lib/flowy-folder/src/services/view/controller.rs
Executable file
@ -0,0 +1,434 @@
|
||||
use bytes::Bytes;
|
||||
use flowy_collaboration::entities::{
|
||||
document_info::{DocumentDelta, DocumentId},
|
||||
revision::{RepeatedRevision, Revision},
|
||||
};
|
||||
|
||||
use flowy_collaboration::client_document::default::initial_delta_string;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
dart_notification::{send_dart_notification, FolderNotification},
|
||||
entities::{
|
||||
trash::{RepeatedTrashId, TrashType},
|
||||
view::{CreateViewParams, RepeatedView, UpdateViewParams, View, ViewId},
|
||||
},
|
||||
errors::{FlowyError, FlowyResult},
|
||||
module::{FolderCouldServiceV1, WorkspaceUser},
|
||||
services::{
|
||||
persistence::{FolderPersistence, FolderPersistenceTransaction, ViewChangeset},
|
||||
TrashController, TrashEvent,
|
||||
},
|
||||
};
|
||||
use flowy_database::kv::KV;
|
||||
use flowy_document::FlowyDocumentManager;
|
||||
use flowy_folder_data_model::entities::share::{ExportData, ExportParams};
|
||||
use lib_infra::uuid_string;
|
||||
|
||||
const LATEST_VIEW_ID: &str = "latest_view_id";
|
||||
|
||||
pub(crate) struct ViewController {
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
trash_controller: Arc<TrashController>,
|
||||
document_manager: Arc<FlowyDocumentManager>,
|
||||
}
|
||||
|
||||
impl ViewController {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
trash_can: Arc<TrashController>,
|
||||
document_manager: Arc<FlowyDocumentManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user,
|
||||
cloud_service,
|
||||
persistence,
|
||||
trash_controller: trash_can,
|
||||
document_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn initialize(&self) -> Result<(), FlowyError> {
|
||||
let _ = self.document_manager.init()?;
|
||||
self.listen_trash_can_event();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, params), fields(name = %params.name), err)]
|
||||
pub(crate) async fn create_view_from_params(&self, params: CreateViewParams) -> Result<View, FlowyError> {
|
||||
let view_data = if params.view_data.is_empty() {
|
||||
initial_delta_string()
|
||||
} else {
|
||||
params.view_data.clone()
|
||||
};
|
||||
|
||||
let delta_data = Bytes::from(view_data);
|
||||
let user_id = self.user.user_id()?;
|
||||
let repeated_revision: RepeatedRevision =
|
||||
Revision::initial_revision(&user_id, ¶ms.view_id, delta_data).into();
|
||||
let _ = self
|
||||
.document_manager
|
||||
.save_document(¶ms.view_id, repeated_revision)
|
||||
.await?;
|
||||
let view = self.create_view_on_server(params).await?;
|
||||
let _ = self.create_view_on_local(view.clone()).await?;
|
||||
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, view_id, view_data), err)]
|
||||
pub(crate) async fn create_view_document_content(
|
||||
&self,
|
||||
view_id: &str,
|
||||
view_data: String,
|
||||
) -> Result<(), FlowyError> {
|
||||
if view_data.is_empty() {
|
||||
return Err(FlowyError::internal().context("The content of the view should not be empty"));
|
||||
}
|
||||
|
||||
let delta_data = Bytes::from(view_data);
|
||||
let user_id = self.user.user_id()?;
|
||||
let repeated_revision: RepeatedRevision = Revision::initial_revision(&user_id, view_id, delta_data).into();
|
||||
let _ = self.document_manager.save_document(view_id, repeated_revision).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_view_on_local(&self, view: View) -> Result<(), FlowyError> {
|
||||
let trash_controller = self.trash_controller.clone();
|
||||
self.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let belong_to_id = view.belong_to_id.clone();
|
||||
let _ = transaction.create_view(view)?;
|
||||
let _ = notify_views_changed(&belong_to_id, trash_controller, &transaction)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, params), fields(view_id = %params.view_id), err)]
|
||||
pub(crate) async fn read_view(&self, params: ViewId) -> Result<View, FlowyError> {
|
||||
let view = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let view = transaction.read_view(¶ms.view_id)?;
|
||||
let trash_ids = self.trash_controller.read_trash_ids(&transaction)?;
|
||||
if trash_ids.contains(&view.id) {
|
||||
return Err(FlowyError::record_not_found());
|
||||
}
|
||||
Ok(view)
|
||||
})
|
||||
.await?;
|
||||
let _ = self.read_view_on_server(params);
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_local_views(&self, ids: Vec<String>) -> Result<Vec<View>, FlowyError> {
|
||||
self.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut views = vec![];
|
||||
for view_id in ids {
|
||||
views.push(transaction.read_view(&view_id)?);
|
||||
}
|
||||
Ok(views)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub(crate) async fn open_document(&self, doc_id: &str) -> Result<DocumentDelta, FlowyError> {
|
||||
let editor = self.document_manager.open_document(doc_id).await?;
|
||||
KV::set_str(LATEST_VIEW_ID, doc_id.to_owned());
|
||||
let document_json = editor.document_json().await?;
|
||||
Ok(DocumentDelta {
|
||||
doc_id: doc_id.to_string(),
|
||||
delta_json: document_json,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub(crate) async fn close_view(&self, doc_id: &str) -> Result<(), FlowyError> {
|
||||
let _ = self.document_manager.close_document(doc_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self,params), fields(doc_id = %params.doc_id), err)]
|
||||
pub(crate) async fn delete_view(&self, params: DocumentId) -> Result<(), FlowyError> {
|
||||
if let Some(view_id) = KV::get_str(LATEST_VIEW_ID) {
|
||||
if view_id == params.doc_id {
|
||||
let _ = KV::remove(LATEST_VIEW_ID);
|
||||
}
|
||||
}
|
||||
let _ = self.document_manager.close_document(¶ms.doc_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub(crate) async fn duplicate_view(&self, doc_id: &str) -> Result<(), FlowyError> {
|
||||
let view = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| transaction.read_view(doc_id))
|
||||
.await?;
|
||||
|
||||
let editor = self.document_manager.open_document(doc_id).await?;
|
||||
let document_json = editor.document_json().await?;
|
||||
let duplicate_params = CreateViewParams {
|
||||
belong_to_id: view.belong_to_id.clone(),
|
||||
name: format!("{} (copy)", &view.name),
|
||||
desc: view.desc.clone(),
|
||||
thumbnail: "".to_owned(),
|
||||
view_type: view.view_type.clone(),
|
||||
view_data: document_json,
|
||||
view_id: uuid_string(),
|
||||
};
|
||||
|
||||
let _ = self.create_view_from_params(duplicate_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, params), err)]
|
||||
pub(crate) async fn export_doc(&self, params: ExportParams) -> Result<ExportData, FlowyError> {
|
||||
let editor = self.document_manager.open_document(¶ms.doc_id).await?;
|
||||
let delta_json = editor.document_json().await?;
|
||||
Ok(ExportData {
|
||||
data: delta_json,
|
||||
export_type: params.export_type,
|
||||
})
|
||||
}
|
||||
|
||||
// belong_to_id will be the app_id or view_id.
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, FlowyError> {
|
||||
self.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
read_belonging_views_on_local(belong_to_id, self.trash_controller.clone(), &transaction)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, params), err)]
|
||||
pub(crate) async fn update_view(&self, params: UpdateViewParams) -> Result<View, FlowyError> {
|
||||
let changeset = ViewChangeset::new(params.clone());
|
||||
let view_id = changeset.id.clone();
|
||||
let view = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.update_view(changeset)?;
|
||||
let view = transaction.read_view(&view_id)?;
|
||||
send_dart_notification(&view_id, FolderNotification::ViewUpdated)
|
||||
.payload(view.clone())
|
||||
.send();
|
||||
let _ = notify_views_changed(&view.belong_to_id, self.trash_controller.clone(), &transaction)?;
|
||||
Ok(view)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let _ = self.update_view_on_server(params);
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
pub(crate) async fn receive_document_delta(&self, params: DocumentDelta) -> Result<DocumentDelta, FlowyError> {
|
||||
let doc = self.document_manager.receive_local_delta(params).await?;
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
pub(crate) async fn latest_visit_view(&self) -> FlowyResult<Option<View>> {
|
||||
match KV::get_str(LATEST_VIEW_ID) {
|
||||
None => Ok(None),
|
||||
Some(view_id) => {
|
||||
let view = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| transaction.read_view(&view_id))
|
||||
.await?;
|
||||
Ok(Some(view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_latest_view(&self, view: &View) {
|
||||
KV::set_str(LATEST_VIEW_ID, view.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewController {
|
||||
#[tracing::instrument(skip(self), err)]
|
||||
async fn create_view_on_server(&self, params: CreateViewParams) -> Result<View, FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let view = self.cloud_service.create_view(&token, params).await?;
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), err)]
|
||||
fn update_view_on_server(&self, params: UpdateViewParams) -> Result<(), FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
tokio::spawn(async move {
|
||||
match server.update_view(&token, params).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// TODO: retry?
|
||||
log::error!("Update view failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), err)]
|
||||
fn read_view_on_server(&self, params: ViewId) -> Result<(), FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let server = self.cloud_service.clone();
|
||||
let persistence = self.persistence.clone();
|
||||
// TODO: Retry with RetryAction?
|
||||
tokio::spawn(async move {
|
||||
match server.read_view(&token, params).await {
|
||||
Ok(Some(view)) => {
|
||||
match persistence
|
||||
.begin_transaction(|transaction| transaction.create_view(view.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
send_dart_notification(&view.id, FolderNotification::ViewUpdated)
|
||||
.payload(view.clone())
|
||||
.send();
|
||||
}
|
||||
Err(e) => log::error!("Save view failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => log::error!("Read view failed: {:?}", e),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn listen_trash_can_event(&self) {
|
||||
let mut rx = self.trash_controller.subscribe();
|
||||
let persistence = self.persistence.clone();
|
||||
let document_manager = self.document_manager.clone();
|
||||
let trash_controller = self.trash_controller.clone();
|
||||
let _ = tokio::spawn(async move {
|
||||
loop {
|
||||
let mut stream = Box::pin(rx.recv().into_stream().filter_map(|result| async move {
|
||||
match result {
|
||||
Ok(event) => event.select(TrashType::View),
|
||||
Err(_e) => None,
|
||||
}
|
||||
}));
|
||||
|
||||
if let Some(event) = stream.next().await {
|
||||
handle_trash_event(
|
||||
persistence.clone(),
|
||||
document_manager.clone(),
|
||||
trash_controller.clone(),
|
||||
event,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(persistence, document_manager, trash_can))]
|
||||
async fn handle_trash_event(
|
||||
persistence: Arc<FolderPersistence>,
|
||||
document_manager: Arc<FlowyDocumentManager>,
|
||||
trash_can: Arc<TrashController>,
|
||||
event: TrashEvent,
|
||||
) {
|
||||
match event {
|
||||
TrashEvent::NewTrash(identifiers, ret) => {
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let views = read_local_views_with_transaction(identifiers, &transaction)?;
|
||||
for view in views {
|
||||
let _ = notify_views_changed(&view.belong_to_id, trash_can.clone(), &transaction)?;
|
||||
notify_dart(view, FolderNotification::ViewDeleted);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let _ = ret.send(result).await;
|
||||
}
|
||||
TrashEvent::Putback(identifiers, ret) => {
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let views = read_local_views_with_transaction(identifiers, &transaction)?;
|
||||
for view in views {
|
||||
let _ = notify_views_changed(&view.belong_to_id, trash_can.clone(), &transaction)?;
|
||||
notify_dart(view, FolderNotification::ViewRestored);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let _ = ret.send(result).await;
|
||||
}
|
||||
TrashEvent::Delete(identifiers, ret) => {
|
||||
let result = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut notify_ids = HashSet::new();
|
||||
for identifier in identifiers.items {
|
||||
let view = transaction.read_view(&identifier.id)?;
|
||||
let _ = transaction.delete_view(&identifier.id)?;
|
||||
let _ = document_manager.delete(&identifier.id)?;
|
||||
notify_ids.insert(view.belong_to_id);
|
||||
}
|
||||
|
||||
for notify_id in notify_ids {
|
||||
let _ = notify_views_changed(¬ify_id, trash_can.clone(), &transaction)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let _ = ret.send(result).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_local_views_with_transaction<'a>(
|
||||
identifiers: RepeatedTrashId,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> Result<Vec<View>, FlowyError> {
|
||||
let mut views = vec![];
|
||||
for identifier in identifiers.items {
|
||||
let view = transaction.read_view(&identifier.id)?;
|
||||
views.push(view);
|
||||
}
|
||||
Ok(views)
|
||||
}
|
||||
|
||||
fn notify_dart(view: View, notification: FolderNotification) {
|
||||
send_dart_notification(&view.id, notification).payload(view).send();
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(belong_to_id, trash_controller, transaction), fields(view_count), err)]
|
||||
fn notify_views_changed<'a>(
|
||||
belong_to_id: &str,
|
||||
trash_controller: Arc<TrashController>,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> FlowyResult<()> {
|
||||
let repeated_view = read_belonging_views_on_local(belong_to_id, trash_controller.clone(), transaction)?;
|
||||
tracing::Span::current().record("view_count", &format!("{}", repeated_view.len()).as_str());
|
||||
send_dart_notification(belong_to_id, FolderNotification::AppViewsChanged)
|
||||
.payload(repeated_view)
|
||||
.send();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_belonging_views_on_local<'a>(
|
||||
belong_to_id: &str,
|
||||
trash_controller: Arc<TrashController>,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> FlowyResult<RepeatedView> {
|
||||
let mut views = transaction.read_views(belong_to_id)?;
|
||||
let trash_ids = trash_controller.read_trash_ids(transaction)?;
|
||||
views.retain(|view_table| !trash_ids.contains(&view_table.id));
|
||||
|
||||
Ok(RepeatedView { items: views })
|
||||
}
|
115
frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs
Executable file
115
frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs
Executable file
@ -0,0 +1,115 @@
|
||||
use crate::{
|
||||
entities::{
|
||||
trash::Trash,
|
||||
view::{
|
||||
CreateViewParams, CreateViewRequest, QueryViewRequest, RepeatedViewId, UpdateViewParams, UpdateViewRequest,
|
||||
View, ViewId,
|
||||
},
|
||||
},
|
||||
errors::FlowyError,
|
||||
services::{TrashController, ViewController},
|
||||
};
|
||||
use flowy_collaboration::entities::document_info::DocumentDelta;
|
||||
use flowy_folder_data_model::entities::share::{ExportData, ExportParams, ExportRequest};
|
||||
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
pub(crate) async fn create_view_handler(
|
||||
data: Data<CreateViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<View, FlowyError> {
|
||||
let params: CreateViewParams = data.into_inner().try_into()?;
|
||||
let view = controller.create_view_from_params(params).await?;
|
||||
data_result(view)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_view_handler(
|
||||
data: Data<QueryViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<View, FlowyError> {
|
||||
let params: ViewId = data.into_inner().try_into()?;
|
||||
let mut view = controller.read_view(params.clone()).await?;
|
||||
// For the moment, app and view can contains lots of views. Reading the view
|
||||
// belongings using the view_id.
|
||||
view.belongings = controller.read_views_belong_to(¶ms.view_id).await?;
|
||||
|
||||
data_result(view)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, controller), err)]
|
||||
pub(crate) async fn update_view_handler(
|
||||
data: Data<UpdateViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: UpdateViewParams = data.into_inner().try_into()?;
|
||||
let _ = controller.update_view(params).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn document_delta_handler(
|
||||
data: Data<DocumentDelta>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<DocumentDelta, FlowyError> {
|
||||
let doc = controller.receive_document_delta(data.into_inner()).await?;
|
||||
data_result(doc)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_view_handler(
|
||||
data: Data<QueryViewRequest>,
|
||||
view_controller: Unit<Arc<ViewController>>,
|
||||
trash_controller: Unit<Arc<TrashController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: RepeatedViewId = data.into_inner().try_into()?;
|
||||
for view_id in ¶ms.items {
|
||||
let _ = view_controller.delete_view(view_id.into()).await;
|
||||
}
|
||||
|
||||
let trash = view_controller
|
||||
.read_local_views(params.items)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|view| view.into())
|
||||
.collect::<Vec<Trash>>();
|
||||
|
||||
let _ = trash_controller.add(trash).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn open_document_handler(
|
||||
data: Data<QueryViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<DocumentDelta, FlowyError> {
|
||||
let params: ViewId = data.into_inner().try_into()?;
|
||||
let doc = controller.open_document(¶ms.view_id).await?;
|
||||
data_result(doc)
|
||||
}
|
||||
|
||||
pub(crate) async fn close_view_handler(
|
||||
data: Data<QueryViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: ViewId = data.into_inner().try_into()?;
|
||||
let _ = controller.close_view(¶ms.view_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, controller), err)]
|
||||
pub(crate) async fn duplicate_view_handler(
|
||||
data: Data<QueryViewRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: ViewId = data.into_inner().try_into()?;
|
||||
let _ = controller.duplicate_view(¶ms.view_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, controller), err)]
|
||||
pub(crate) async fn export_handler(
|
||||
data: Data<ExportRequest>,
|
||||
controller: Unit<Arc<ViewController>>,
|
||||
) -> DataResult<ExportData, FlowyError> {
|
||||
let params: ExportParams = data.into_inner().try_into()?;
|
||||
let data = controller.export_doc(params).await?;
|
||||
data_result(data)
|
||||
}
|
2
frontend/rust-lib/flowy-folder/src/services/view/mod.rs
Executable file
2
frontend/rust-lib/flowy-folder/src/services/view/mod.rs
Executable file
@ -0,0 +1,2 @@
|
||||
pub mod controller;
|
||||
pub mod event_handler;
|
128
frontend/rust-lib/flowy-folder/src/services/web_socket.rs
Executable file
128
frontend/rust-lib/flowy-folder/src/services/web_socket.rs
Executable file
@ -0,0 +1,128 @@
|
||||
use crate::services::FOLDER_SYNC_INTERVAL_IN_MILLIS;
|
||||
use bytes::Bytes;
|
||||
use flowy_collaboration::{
|
||||
client_folder::FolderPad,
|
||||
entities::{
|
||||
revision::RevisionRange,
|
||||
ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSDataType},
|
||||
},
|
||||
};
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_sync::*;
|
||||
use lib_infra::future::{BoxResultFuture, FutureResult};
|
||||
use lib_ot::core::{Delta, OperationTransformable, PlainAttributes, PlainDelta};
|
||||
use parking_lot::RwLock;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
pub(crate) async fn make_folder_ws_manager(
|
||||
user_id: &str,
|
||||
folder_id: &str,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
web_socket: Arc<dyn RevisionWebSocket>,
|
||||
folder_pad: Arc<RwLock<FolderPad>>,
|
||||
) -> Arc<RevisionWebSocketManager> {
|
||||
let composite_sink_provider = Arc::new(CompositeWSSinkDataProvider::new(folder_id, rev_manager.clone()));
|
||||
let resolve_target = Arc::new(FolderRevisionResolveTarget { folder_pad });
|
||||
let resolver = RevisionConflictResolver::<PlainAttributes>::new(
|
||||
user_id,
|
||||
resolve_target,
|
||||
Arc::new(composite_sink_provider.clone()),
|
||||
rev_manager,
|
||||
);
|
||||
|
||||
let ws_stream_consumer = Arc::new(FolderWSStreamConsumerAdapter {
|
||||
resolver: Arc::new(resolver),
|
||||
});
|
||||
|
||||
let sink_provider = Arc::new(FolderWSSinkDataProviderAdapter(composite_sink_provider));
|
||||
let ping_duration = Duration::from_millis(FOLDER_SYNC_INTERVAL_IN_MILLIS);
|
||||
Arc::new(RevisionWebSocketManager::new(
|
||||
"Folder",
|
||||
folder_id,
|
||||
web_socket,
|
||||
sink_provider,
|
||||
ws_stream_consumer,
|
||||
ping_duration,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) struct FolderWSSinkDataProviderAdapter(Arc<CompositeWSSinkDataProvider>);
|
||||
impl RevisionWSSinkDataProvider for FolderWSSinkDataProviderAdapter {
|
||||
fn next(&self) -> FutureResult<Option<ClientRevisionWSData>, FlowyError> {
|
||||
let sink_provider = self.0.clone();
|
||||
FutureResult::new(async move { sink_provider.next().await })
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderRevisionResolveTarget {
|
||||
folder_pad: Arc<RwLock<FolderPad>>,
|
||||
}
|
||||
|
||||
impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
|
||||
fn compose_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
|
||||
let folder_pad = self.folder_pad.clone();
|
||||
Box::pin(async move {
|
||||
let md5 = folder_pad.write().compose_remote_delta(delta)?;
|
||||
Ok(md5)
|
||||
})
|
||||
}
|
||||
|
||||
fn transform_delta(
|
||||
&self,
|
||||
delta: Delta<PlainAttributes>,
|
||||
) -> BoxResultFuture<TransformDeltas<PlainAttributes>, FlowyError> {
|
||||
let folder_pad = self.folder_pad.clone();
|
||||
Box::pin(async move {
|
||||
let read_guard = folder_pad.read();
|
||||
let mut server_prime: Option<PlainDelta> = None;
|
||||
let client_prime: PlainDelta;
|
||||
if read_guard.is_empty() {
|
||||
// Do nothing
|
||||
client_prime = delta;
|
||||
} else {
|
||||
let (s_prime, c_prime) = read_guard.delta().transform(&delta)?;
|
||||
client_prime = c_prime;
|
||||
server_prime = Some(s_prime);
|
||||
}
|
||||
drop(read_guard);
|
||||
Ok(TransformDeltas {
|
||||
client_prime,
|
||||
server_prime,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
|
||||
let folder_pad = self.folder_pad.clone();
|
||||
Box::pin(async move {
|
||||
let md5 = folder_pad.write().reset_folder(delta)?;
|
||||
Ok(md5)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderWSStreamConsumerAdapter {
|
||||
resolver: Arc<RevisionConflictResolver<PlainAttributes>>,
|
||||
}
|
||||
|
||||
impl RevisionWSSteamConsumer for FolderWSStreamConsumerAdapter {
|
||||
fn receive_push_revision(&self, bytes: Bytes) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.resolver.clone();
|
||||
Box::pin(async move { resolver.receive_bytes(bytes).await })
|
||||
}
|
||||
|
||||
fn receive_ack(&self, id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.resolver.clone();
|
||||
Box::pin(async move { resolver.ack_revision(id, ty).await })
|
||||
}
|
||||
|
||||
fn receive_new_user_connect(&self, _new_user: NewDocumentUser) -> BoxResultFuture<(), FlowyError> {
|
||||
// Do nothing by now, just a placeholder for future extension.
|
||||
Box::pin(async move { Ok(()) })
|
||||
}
|
||||
|
||||
fn pull_revisions_in_range(&self, range: RevisionRange) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.resolver.clone();
|
||||
Box::pin(async move { resolver.send_revisions(range).await })
|
||||
}
|
||||
}
|
207
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
Executable file
207
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
Executable file
@ -0,0 +1,207 @@
|
||||
use crate::{
|
||||
dart_notification::*,
|
||||
errors::*,
|
||||
module::{FolderCouldServiceV1, WorkspaceUser},
|
||||
services::{
|
||||
persistence::{FolderPersistence, FolderPersistenceTransaction, WorkspaceChangeset},
|
||||
read_local_workspace_apps, TrashController,
|
||||
},
|
||||
};
|
||||
use flowy_database::kv::KV;
|
||||
use flowy_folder_data_model::entities::{app::RepeatedApp, workspace::*};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct WorkspaceController {
|
||||
pub user: Arc<dyn WorkspaceUser>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
pub(crate) trash_controller: Arc<TrashController>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
}
|
||||
|
||||
impl WorkspaceController {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
persistence: Arc<FolderPersistence>,
|
||||
trash_can: Arc<TrashController>,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user,
|
||||
persistence,
|
||||
trash_controller: trash_can,
|
||||
cloud_service,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create_workspace_from_params(
|
||||
&self,
|
||||
params: CreateWorkspaceParams,
|
||||
) -> Result<Workspace, FlowyError> {
|
||||
let workspace = self.create_workspace_on_server(params.clone()).await?;
|
||||
let user_id = self.user.user_id()?;
|
||||
let token = self.user.token()?;
|
||||
let workspaces = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.create_workspace(&user_id, workspace.clone())?;
|
||||
transaction.read_workspaces(&user_id, None)
|
||||
})
|
||||
.await?;
|
||||
let repeated_workspace = RepeatedWorkspace { items: workspaces };
|
||||
send_dart_notification(&token, FolderNotification::UserCreateWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
set_current_workspace(&workspace.id);
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn update_workspace(&self, params: UpdateWorkspaceParams) -> Result<(), FlowyError> {
|
||||
let changeset = WorkspaceChangeset::new(params.clone());
|
||||
let workspace_id = changeset.id.clone();
|
||||
let workspace = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.update_workspace(changeset)?;
|
||||
let user_id = self.user.user_id()?;
|
||||
self.read_local_workspace(workspace_id.clone(), &user_id, &transaction)
|
||||
})
|
||||
.await?;
|
||||
|
||||
send_dart_notification(&workspace_id, FolderNotification::WorkspaceUpdated)
|
||||
.payload(workspace)
|
||||
.send();
|
||||
let _ = self.update_workspace_on_server(params)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let token = self.user.token()?;
|
||||
let repeated_workspace = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let _ = transaction.delete_workspace(workspace_id)?;
|
||||
self.read_local_workspaces(None, &user_id, &transaction)
|
||||
})
|
||||
.await?;
|
||||
send_dart_notification(&token, FolderNotification::UserDeleteWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
let _ = self.delete_workspace_on_server(workspace_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn open_workspace(&self, params: WorkspaceId) -> Result<Workspace, FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
if let Some(workspace_id) = params.workspace_id {
|
||||
let workspace = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| self.read_local_workspace(workspace_id, &user_id, &transaction))
|
||||
.await?;
|
||||
set_current_workspace(&workspace.id);
|
||||
Ok(workspace)
|
||||
} else {
|
||||
Err(FlowyError::workspace_id().context("Opened workspace id should not be empty"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_current_workspace_apps(&self) -> Result<RepeatedApp, FlowyError> {
|
||||
let workspace_id = get_current_workspace()?;
|
||||
let repeated_app = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
read_local_workspace_apps(&workspace_id, self.trash_controller.clone(), &transaction)
|
||||
})
|
||||
.await?;
|
||||
// TODO: read from server
|
||||
Ok(repeated_app)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, transaction), err)]
|
||||
pub(crate) fn read_local_workspaces<'a>(
|
||||
&self,
|
||||
workspace_id: Option<String>,
|
||||
user_id: &str,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> Result<RepeatedWorkspace, FlowyError> {
|
||||
let workspace_id = workspace_id.to_owned();
|
||||
let workspaces = transaction.read_workspaces(user_id, workspace_id)?;
|
||||
Ok(RepeatedWorkspace { items: workspaces })
|
||||
}
|
||||
|
||||
pub(crate) fn read_local_workspace<'a>(
|
||||
&self,
|
||||
workspace_id: String,
|
||||
user_id: &str,
|
||||
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
|
||||
) -> Result<Workspace, FlowyError> {
|
||||
let mut workspaces = transaction.read_workspaces(user_id, Some(workspace_id.clone()))?;
|
||||
if workspaces.is_empty() {
|
||||
return Err(FlowyError::record_not_found().context(format!("{} workspace not found", workspace_id)));
|
||||
}
|
||||
debug_assert_eq!(workspaces.len(), 1);
|
||||
let workspace = workspaces.drain(..1).collect::<Vec<Workspace>>().pop().unwrap();
|
||||
Ok(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceController {
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
async fn create_workspace_on_server(&self, params: CreateWorkspaceParams) -> Result<Workspace, FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let workspace = self.cloud_service.create_workspace(&token, params).await?;
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
fn update_workspace_on_server(&self, params: UpdateWorkspaceParams) -> Result<(), FlowyError> {
|
||||
let (token, server) = (self.user.token()?, self.cloud_service.clone());
|
||||
tokio::spawn(async move {
|
||||
match server.update_workspace(&token, params).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// TODO: retry?
|
||||
log::error!("Update workspace failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
fn delete_workspace_on_server(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||
let params = WorkspaceId {
|
||||
workspace_id: Some(workspace_id.to_string()),
|
||||
};
|
||||
let (token, server) = (self.user.token()?, self.cloud_service.clone());
|
||||
tokio::spawn(async move {
|
||||
match server.delete_workspace(&token, params).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// TODO: retry?
|
||||
log::error!("Delete workspace failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
|
||||
|
||||
pub fn set_current_workspace(workspace_id: &str) {
|
||||
KV::set_str(CURRENT_WORKSPACE_ID, workspace_id.to_owned());
|
||||
}
|
||||
|
||||
pub fn get_current_workspace() -> Result<String, FlowyError> {
|
||||
match KV::get_str(CURRENT_WORKSPACE_ID) {
|
||||
None => {
|
||||
Err(FlowyError::record_not_found()
|
||||
.context("Current workspace not found or should call open workspace first"))
|
||||
}
|
||||
Some(workspace_id) => Ok(workspace_id),
|
||||
}
|
||||
}
|
143
frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs
Executable file
143
frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs
Executable file
@ -0,0 +1,143 @@
|
||||
use crate::{
|
||||
controller::FolderManager,
|
||||
dart_notification::{send_dart_notification, FolderNotification},
|
||||
errors::FlowyError,
|
||||
services::{get_current_workspace, read_local_workspace_apps, WorkspaceController},
|
||||
};
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::RepeatedApp,
|
||||
view::View,
|
||||
workspace::{CurrentWorkspaceSetting, QueryWorkspaceRequest, RepeatedWorkspace, WorkspaceId, *},
|
||||
};
|
||||
|
||||
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
#[tracing::instrument(skip(data, controller), err)]
|
||||
pub(crate) async fn create_workspace_handler(
|
||||
data: Data<CreateWorkspaceRequest>,
|
||||
controller: Unit<Arc<WorkspaceController>>,
|
||||
) -> DataResult<Workspace, FlowyError> {
|
||||
let controller = controller.get_ref().clone();
|
||||
let params: CreateWorkspaceParams = data.into_inner().try_into()?;
|
||||
let detail = controller.create_workspace_from_params(params).await?;
|
||||
data_result(detail)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(controller), err)]
|
||||
pub(crate) async fn read_workspace_apps_handler(
|
||||
controller: Unit<Arc<WorkspaceController>>,
|
||||
) -> DataResult<RepeatedApp, FlowyError> {
|
||||
let repeated_app = controller.read_current_workspace_apps().await?;
|
||||
data_result(repeated_app)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, controller), err)]
|
||||
pub(crate) async fn open_workspace_handler(
|
||||
data: Data<QueryWorkspaceRequest>,
|
||||
controller: Unit<Arc<WorkspaceController>>,
|
||||
) -> DataResult<Workspace, FlowyError> {
|
||||
let params: WorkspaceId = data.into_inner().try_into()?;
|
||||
let workspaces = controller.open_workspace(params).await?;
|
||||
data_result(workspaces)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(data, folder), err)]
|
||||
pub(crate) async fn read_workspaces_handler(
|
||||
data: Data<QueryWorkspaceRequest>,
|
||||
folder: Unit<Arc<FolderManager>>,
|
||||
) -> DataResult<RepeatedWorkspace, FlowyError> {
|
||||
let params: WorkspaceId = data.into_inner().try_into()?;
|
||||
let user_id = folder.user.user_id()?;
|
||||
let workspace_controller = folder.workspace_controller.clone();
|
||||
|
||||
let trash_controller = folder.trash_controller.clone();
|
||||
let workspaces = folder
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
let mut workspaces =
|
||||
workspace_controller.read_local_workspaces(params.workspace_id.clone(), &user_id, &transaction)?;
|
||||
for workspace in workspaces.iter_mut() {
|
||||
let apps =
|
||||
read_local_workspace_apps(&workspace.id, trash_controller.clone(), &transaction)?.into_inner();
|
||||
workspace.apps.items = apps;
|
||||
}
|
||||
Ok(workspaces)
|
||||
})
|
||||
.await?;
|
||||
let _ = read_workspaces_on_server(folder, user_id, params);
|
||||
data_result(workspaces)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(folder), err)]
|
||||
pub async fn read_cur_workspace_handler(
|
||||
folder: Unit<Arc<FolderManager>>,
|
||||
) -> DataResult<CurrentWorkspaceSetting, FlowyError> {
|
||||
let workspace_id = get_current_workspace()?;
|
||||
let user_id = folder.user.user_id()?;
|
||||
let params = WorkspaceId {
|
||||
workspace_id: Some(workspace_id.clone()),
|
||||
};
|
||||
|
||||
let workspace = folder
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
folder
|
||||
.workspace_controller
|
||||
.read_local_workspace(workspace_id, &user_id, &transaction)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let latest_view: Option<View> = folder.view_controller.latest_visit_view().await.unwrap_or(None);
|
||||
let setting = CurrentWorkspaceSetting { workspace, latest_view };
|
||||
let _ = read_workspaces_on_server(folder, user_id, params);
|
||||
data_result(setting)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(folder_manager), err)]
|
||||
fn read_workspaces_on_server(
|
||||
folder_manager: Unit<Arc<FolderManager>>,
|
||||
user_id: String,
|
||||
params: WorkspaceId,
|
||||
) -> Result<(), FlowyError> {
|
||||
let (token, server) = (folder_manager.user.token()?, folder_manager.cloud_service.clone());
|
||||
let persistence = folder_manager.persistence.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let workspaces = server.read_workspace(&token, params).await?;
|
||||
let _ = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
tracing::debug!("Save {} workspace", workspaces.len());
|
||||
for workspace in &workspaces.items {
|
||||
let m_workspace = workspace.clone();
|
||||
let apps = m_workspace.apps.clone().into_inner();
|
||||
let _ = transaction.create_workspace(&user_id, m_workspace)?;
|
||||
tracing::debug!("Save {} apps", apps.len());
|
||||
for app in apps {
|
||||
let views = app.belongings.clone().into_inner();
|
||||
match transaction.create_app(app) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("create app failed: {:?}", e),
|
||||
}
|
||||
|
||||
tracing::debug!("Save {} views", views.len());
|
||||
for view in views {
|
||||
match transaction.create_view(view) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("create view failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
send_dart_notification(&token, FolderNotification::WorkspaceListUpdated)
|
||||
.payload(workspaces)
|
||||
.send();
|
||||
Result::<(), FlowyError>::Ok(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
2
frontend/rust-lib/flowy-folder/src/services/workspace/mod.rs
Executable file
2
frontend/rust-lib/flowy-folder/src/services/workspace/mod.rs
Executable file
@ -0,0 +1,2 @@
|
||||
pub mod controller;
|
||||
pub mod event_handler;
|
75
frontend/rust-lib/flowy-folder/src/util.rs
Executable file
75
frontend/rust-lib/flowy-folder/src/util.rs
Executable file
@ -0,0 +1,75 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
use crate::module::{FolderCouldServiceV1, WorkspaceUser};
|
||||
use lib_infra::retry::Action;
|
||||
use pin_project::pin_project;
|
||||
use std::{
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
pub(crate) type Builder<Fut> = Box<dyn Fn(String, Arc<dyn FolderCouldServiceV1>) -> Fut + Send + Sync>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RetryAction<Fut, T, E> {
|
||||
token: String,
|
||||
cloud_service: Arc<dyn FolderCouldServiceV1>,
|
||||
user: Arc<dyn WorkspaceUser>,
|
||||
builder: Builder<Fut>,
|
||||
phantom: PhantomData<(T, E)>,
|
||||
}
|
||||
|
||||
impl<Fut, T, E> RetryAction<Fut, T, E> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new<F>(cloud_service: Arc<dyn FolderCouldServiceV1>, user: Arc<dyn WorkspaceUser>, builder: F) -> Self
|
||||
where
|
||||
Fut: Future<Output = Result<T, E>> + Send + Sync + 'static,
|
||||
F: Fn(String, Arc<dyn FolderCouldServiceV1>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
let token = user.token().unwrap_or_else(|_| "".to_owned());
|
||||
Self {
|
||||
token,
|
||||
cloud_service,
|
||||
user,
|
||||
builder: Box::new(builder),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, T, E> Action for RetryAction<Fut, T, E>
|
||||
where
|
||||
Fut: Future<Output = Result<T, E>> + Send + Sync + 'static,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Item, Self::Error>> + Send + Sync>>;
|
||||
type Item = T;
|
||||
type Error = E;
|
||||
|
||||
fn run(&mut self) -> Self::Future {
|
||||
let fut = (self.builder)(self.token.clone(), self.cloud_service.clone());
|
||||
Box::pin(RetryActionFut { fut: Box::pin(fut) })
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
struct RetryActionFut<T, E> {
|
||||
#[pin]
|
||||
fut: Pin<Box<dyn Future<Output = Result<T, E>> + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl<T, E> Future for RetryActionFut<T, E>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
type Output = Result<T, E>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let mut this = self.project();
|
||||
this.fut.as_mut().poll(cx)
|
||||
}
|
||||
}
|
365
frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs
Executable file
365
frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs
Executable file
@ -0,0 +1,365 @@
|
||||
use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
|
||||
use flowy_collaboration::{client_document::default::initial_delta_string, entities::revision::RevisionState};
|
||||
use flowy_folder::entities::workspace::CreateWorkspaceRequest;
|
||||
use flowy_test::{event_builder::*, FlowySDKTest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_read_all() {
|
||||
let mut test = FolderTest::new().await;
|
||||
test.run_scripts(vec![ReadAllWorkspaces]).await;
|
||||
// The first workspace will be the default workspace
|
||||
// The second workspace will be created by FolderTest
|
||||
assert_eq!(test.all_workspace.len(), 2);
|
||||
|
||||
let new_name = "My new workspace".to_owned();
|
||||
test.run_scripts(vec![
|
||||
CreateWorkspace {
|
||||
name: new_name.clone(),
|
||||
desc: "Daily routines".to_owned(),
|
||||
},
|
||||
ReadAllWorkspaces,
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.all_workspace.len(), 3);
|
||||
assert_eq!(test.all_workspace[2].name, new_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_create() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let name = "My new workspace".to_owned();
|
||||
let desc = "Daily routines".to_owned();
|
||||
test.run_scripts(vec![CreateWorkspace {
|
||||
name: name.clone(),
|
||||
desc: desc.clone(),
|
||||
}])
|
||||
.await;
|
||||
|
||||
let workspace = test.workspace.clone();
|
||||
assert_eq!(workspace.name, name);
|
||||
assert_eq!(workspace.desc, desc);
|
||||
|
||||
test.run_scripts(vec![
|
||||
ReadWorkspace(Some(workspace.id.clone())),
|
||||
AssertWorkspace(workspace),
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_read() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let workspace = test.workspace.clone();
|
||||
let json = serde_json::to_string(&workspace).unwrap();
|
||||
|
||||
test.run_scripts(vec![
|
||||
ReadWorkspace(Some(workspace.id.clone())),
|
||||
AssertWorkspaceJson(json),
|
||||
AssertWorkspace(workspace),
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_create_with_apps() {
|
||||
let mut test = FolderTest::new().await;
|
||||
test.run_scripts(vec![CreateApp {
|
||||
name: "App".to_string(),
|
||||
desc: "App description".to_string(),
|
||||
}])
|
||||
.await;
|
||||
|
||||
let app = test.app.clone();
|
||||
let json = serde_json::to_string(&app).unwrap();
|
||||
test.run_scripts(vec![ReadApp(app.id), AssertAppJson(json)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_create_with_invalid_name() {
|
||||
for (name, code) in invalid_workspace_name_test_case() {
|
||||
let sdk = FlowySDKTest::default();
|
||||
let request = CreateWorkspaceRequest {
|
||||
name,
|
||||
desc: "".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
FolderEventBuilder::new(sdk)
|
||||
.event(flowy_folder::event::FolderEvent::CreateWorkspace)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.error()
|
||||
.code,
|
||||
code.value()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn app_delete() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
test.run_scripts(vec![DeleteApp, ReadApp(app.id)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_delete_then_restore() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
test.run_scripts(vec![
|
||||
DeleteApp,
|
||||
RestoreAppFromTrash,
|
||||
ReadApp(app.id.clone()),
|
||||
AssertApp(app),
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_read() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
test.run_scripts(vec![ReadApp(app.id.clone()), AssertApp(app)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_update() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
let new_name = "😁 hell world".to_owned();
|
||||
assert_ne!(app.name, new_name);
|
||||
|
||||
test.run_scripts(vec![
|
||||
UpdateApp {
|
||||
name: Some(new_name.clone()),
|
||||
desc: None,
|
||||
},
|
||||
ReadApp(app.id),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.app.name, new_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_create_with_view() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let mut app = test.app.clone();
|
||||
test.run_scripts(vec![
|
||||
CreateView {
|
||||
name: "View A".to_owned(),
|
||||
desc: "View A description".to_owned(),
|
||||
},
|
||||
CreateView {
|
||||
name: "View B".to_owned(),
|
||||
desc: "View B description".to_owned(),
|
||||
},
|
||||
ReadApp(app.id),
|
||||
])
|
||||
.await;
|
||||
|
||||
app = test.app.clone();
|
||||
assert_eq!(app.belongings.len(), 3);
|
||||
assert_eq!(app.belongings[1].name, "View A");
|
||||
assert_eq!(app.belongings[2].name, "View B")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_update() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.view.clone();
|
||||
let new_name = "😁 123".to_owned();
|
||||
assert_ne!(view.name, new_name);
|
||||
|
||||
test.run_scripts(vec![
|
||||
UpdateView {
|
||||
name: Some(new_name.clone()),
|
||||
desc: None,
|
||||
},
|
||||
ReadView(view.id),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.view.name, new_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn open_document_view() {
|
||||
let mut test = FolderTest::new().await;
|
||||
assert_eq!(test.document_info, None);
|
||||
|
||||
test.run_scripts(vec![OpenDocument]).await;
|
||||
let document_info = test.document_info.unwrap();
|
||||
assert_eq!(document_info.text, initial_delta_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn view_delete() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.view.clone();
|
||||
test.run_scripts(vec![DeleteView, ReadView(view.id)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_delete_then_restore() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.view.clone();
|
||||
test.run_scripts(vec![
|
||||
DeleteView,
|
||||
RestoreViewFromTrash,
|
||||
ReadView(view.id.clone()),
|
||||
AssertView(view),
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_delete_all() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
test.run_scripts(vec![
|
||||
CreateView {
|
||||
name: "View A".to_owned(),
|
||||
desc: "View A description".to_owned(),
|
||||
},
|
||||
CreateView {
|
||||
name: "View B".to_owned(),
|
||||
desc: "View B description".to_owned(),
|
||||
},
|
||||
ReadApp(app.id.clone()),
|
||||
])
|
||||
.await;
|
||||
|
||||
assert_eq!(test.app.belongings.len(), 3);
|
||||
let view_ids = test
|
||||
.app
|
||||
.belongings
|
||||
.iter()
|
||||
.map(|view| view.id.clone())
|
||||
.collect::<Vec<String>>();
|
||||
test.run_scripts(vec![DeleteViews(view_ids), ReadApp(app.id), ReadTrash])
|
||||
.await;
|
||||
|
||||
assert_eq!(test.app.belongings.len(), 0);
|
||||
assert_eq!(test.trash.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_delete_all_permanent() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app = test.app.clone();
|
||||
test.run_scripts(vec![
|
||||
CreateView {
|
||||
name: "View A".to_owned(),
|
||||
desc: "View A description".to_owned(),
|
||||
},
|
||||
ReadApp(app.id.clone()),
|
||||
])
|
||||
.await;
|
||||
|
||||
let view_ids = test
|
||||
.app
|
||||
.belongings
|
||||
.iter()
|
||||
.map(|view| view.id.clone())
|
||||
.collect::<Vec<String>>();
|
||||
test.run_scripts(vec![DeleteViews(view_ids), ReadApp(app.id), DeleteAllTrash, ReadTrash])
|
||||
.await;
|
||||
|
||||
assert_eq!(test.app.belongings.len(), 0);
|
||||
assert_eq!(test.trash.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_sync_revision_state() {
|
||||
let mut test = FolderTest::new().await;
|
||||
test.run_scripts(vec![
|
||||
AssertRevisionState {
|
||||
rev_id: 1,
|
||||
state: RevisionState::Sync,
|
||||
},
|
||||
AssertNextSyncRevId(Some(1)),
|
||||
AssertRevisionState {
|
||||
rev_id: 1,
|
||||
state: RevisionState::Ack,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_sync_revision_seq() {
|
||||
let mut test = FolderTest::new().await;
|
||||
test.run_scripts(vec![
|
||||
AssertRevisionState {
|
||||
rev_id: 1,
|
||||
state: RevisionState::Sync,
|
||||
},
|
||||
AssertRevisionState {
|
||||
rev_id: 2,
|
||||
state: RevisionState::Sync,
|
||||
},
|
||||
AssertNextSyncRevId(Some(1)),
|
||||
AssertNextSyncRevId(Some(2)),
|
||||
AssertRevisionState {
|
||||
rev_id: 1,
|
||||
state: RevisionState::Ack,
|
||||
},
|
||||
AssertRevisionState {
|
||||
rev_id: 2,
|
||||
state: RevisionState::Ack,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_sync_revision_with_new_app() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let app_name = "AppFlowy contributors".to_owned();
|
||||
let app_desc = "Welcome to be a AppFlowy contributor".to_owned();
|
||||
|
||||
test.run_scripts(vec![
|
||||
AssertNextSyncRevId(Some(1)),
|
||||
AssertNextSyncRevId(Some(2)),
|
||||
CreateApp {
|
||||
name: app_name.clone(),
|
||||
desc: app_desc.clone(),
|
||||
},
|
||||
AssertCurrentRevId(3),
|
||||
AssertNextSyncRevId(Some(3)),
|
||||
AssertNextSyncRevId(None),
|
||||
])
|
||||
.await;
|
||||
|
||||
let app = test.app.clone();
|
||||
assert_eq!(app.name, app_name);
|
||||
assert_eq!(app.desc, app_desc);
|
||||
test.run_scripts(vec![ReadApp(app.id.clone()), AssertApp(app)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_sync_revision_with_new_view() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view_name = "AppFlowy features".to_owned();
|
||||
let view_desc = "😁".to_owned();
|
||||
|
||||
test.run_scripts(vec![
|
||||
AssertNextSyncRevId(Some(1)),
|
||||
AssertNextSyncRevId(Some(2)),
|
||||
CreateView {
|
||||
name: view_name.clone(),
|
||||
desc: view_desc.clone(),
|
||||
},
|
||||
AssertCurrentRevId(3),
|
||||
AssertNextSyncRevId(Some(3)),
|
||||
AssertNextSyncRevId(None),
|
||||
])
|
||||
.await;
|
||||
|
||||
let view = test.view.clone();
|
||||
assert_eq!(view.name, view_name);
|
||||
assert_eq!(view.desc, view_desc);
|
||||
test.run_scripts(vec![ReadView(view.id.clone()), AssertView(view)])
|
||||
.await;
|
||||
}
|
209
frontend/rust-lib/flowy-folder/tests/workspace/helper.rs
Executable file
209
frontend/rust-lib/flowy-folder/tests/workspace/helper.rs
Executable file
@ -0,0 +1,209 @@
|
||||
use flowy_collaboration::entities::document_info::DocumentInfo;
|
||||
use flowy_folder::event::FolderEvent::*;
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::{App, AppId, CreateAppRequest, QueryAppRequest, UpdateAppRequest},
|
||||
trash::{RepeatedTrash, TrashId, TrashType},
|
||||
view::{CreateViewRequest, QueryViewRequest, UpdateViewRequest, View, ViewType},
|
||||
workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, RepeatedWorkspace, Workspace},
|
||||
};
|
||||
use flowy_test::{event_builder::*, FlowySDKTest};
|
||||
|
||||
pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
|
||||
let request = CreateWorkspaceRequest {
|
||||
name: name.to_owned(),
|
||||
desc: desc.to_owned(),
|
||||
};
|
||||
|
||||
let workspace = FolderEventBuilder::new(sdk.clone())
|
||||
.event(CreateWorkspace)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<Workspace>();
|
||||
workspace
|
||||
}
|
||||
|
||||
pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) -> Vec<Workspace> {
|
||||
let request = QueryWorkspaceRequest { workspace_id };
|
||||
let repeated_workspace = FolderEventBuilder::new(sdk.clone())
|
||||
.event(ReadWorkspaces)
|
||||
.request(request.clone())
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<RepeatedWorkspace>();
|
||||
|
||||
let workspaces;
|
||||
if let Some(workspace_id) = &request.workspace_id {
|
||||
workspaces = repeated_workspace
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.filter(|workspace| &workspace.id == workspace_id)
|
||||
.collect::<Vec<Workspace>>();
|
||||
debug_assert_eq!(workspaces.len(), 1);
|
||||
} else {
|
||||
workspaces = repeated_workspace.items;
|
||||
}
|
||||
|
||||
workspaces
|
||||
}
|
||||
|
||||
pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> App {
|
||||
let create_app_request = CreateAppRequest {
|
||||
workspace_id: workspace_id.to_owned(),
|
||||
name: name.to_string(),
|
||||
desc: desc.to_string(),
|
||||
color_style: Default::default(),
|
||||
};
|
||||
|
||||
let app = FolderEventBuilder::new(sdk.clone())
|
||||
.event(CreateApp)
|
||||
.request(create_app_request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<App>();
|
||||
app
|
||||
}
|
||||
|
||||
pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App {
|
||||
let request = QueryAppRequest {
|
||||
app_ids: vec![app_id.to_owned()],
|
||||
};
|
||||
|
||||
let app = FolderEventBuilder::new(sdk.clone())
|
||||
.event(ReadApp)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<App>();
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option<String>, desc: Option<String>) {
|
||||
let request = UpdateAppRequest {
|
||||
app_id: app_id.to_string(),
|
||||
name,
|
||||
desc,
|
||||
color_style: None,
|
||||
is_trash: None,
|
||||
};
|
||||
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(UpdateApp)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
|
||||
let request = AppId {
|
||||
app_id: app_id.to_string(),
|
||||
};
|
||||
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(DeleteApp)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, view_type: ViewType) -> View {
|
||||
let request = CreateViewRequest {
|
||||
belong_to_id: app_id.to_string(),
|
||||
name: name.to_string(),
|
||||
desc: desc.to_string(),
|
||||
thumbnail: None,
|
||||
view_type,
|
||||
};
|
||||
let view = FolderEventBuilder::new(sdk.clone())
|
||||
.event(CreateView)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<View>();
|
||||
view
|
||||
}
|
||||
|
||||
pub async fn read_view(sdk: &FlowySDKTest, view_ids: Vec<String>) -> View {
|
||||
let request = QueryViewRequest { view_ids };
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(ReadView)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<View>()
|
||||
}
|
||||
|
||||
pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option<String>, desc: Option<String>) {
|
||||
let request = UpdateViewRequest {
|
||||
view_id: view_id.to_string(),
|
||||
name,
|
||||
desc,
|
||||
thumbnail: None,
|
||||
};
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(UpdateView)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec<String>) {
|
||||
let request = QueryViewRequest { view_ids };
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(DeleteView)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn open_document(sdk: &FlowySDKTest, view_id: &str) -> DocumentInfo {
|
||||
let request = QueryViewRequest {
|
||||
view_ids: vec![view_id.to_owned()],
|
||||
};
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(OpenDocument)
|
||||
.request(request)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<DocumentInfo>()
|
||||
}
|
||||
|
||||
pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(ReadTrash)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<RepeatedTrash>()
|
||||
}
|
||||
|
||||
pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) {
|
||||
let id = TrashId {
|
||||
id: app_id.to_owned(),
|
||||
ty: TrashType::App,
|
||||
};
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(PutbackTrash)
|
||||
.request(id)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) {
|
||||
let id = TrashId {
|
||||
id: view_id.to_owned(),
|
||||
ty: TrashType::View,
|
||||
};
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(PutbackTrash)
|
||||
.request(id)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn delete_all_trash(sdk: &FlowySDKTest) {
|
||||
FolderEventBuilder::new(sdk.clone())
|
||||
.event(DeleteAllTrash)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
3
frontend/rust-lib/flowy-folder/tests/workspace/main.rs
Executable file
3
frontend/rust-lib/flowy-folder/tests/workspace/main.rs
Executable file
@ -0,0 +1,3 @@
|
||||
mod folder_test;
|
||||
mod helper;
|
||||
mod script;
|
219
frontend/rust-lib/flowy-folder/tests/workspace/script.rs
Executable file
219
frontend/rust-lib/flowy-folder/tests/workspace/script.rs
Executable file
@ -0,0 +1,219 @@
|
||||
use crate::helper::*;
|
||||
use flowy_collaboration::entities::{document_info::DocumentInfo, revision::RevisionState};
|
||||
use flowy_folder::{errors::ErrorCode, services::folder_editor::FolderEditor};
|
||||
use flowy_folder_data_model::entities::{
|
||||
app::{App, RepeatedApp},
|
||||
trash::Trash,
|
||||
view::{RepeatedView, View, ViewType},
|
||||
workspace::Workspace,
|
||||
};
|
||||
use flowy_sync::REVISION_WRITE_INTERVAL_IN_MILLIS;
|
||||
use flowy_test::FlowySDKTest;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub enum FolderScript {
|
||||
// Workspace
|
||||
ReadAllWorkspaces,
|
||||
CreateWorkspace { name: String, desc: String },
|
||||
AssertWorkspaceJson(String),
|
||||
AssertWorkspace(Workspace),
|
||||
ReadWorkspace(Option<String>),
|
||||
|
||||
// App
|
||||
CreateApp { name: String, desc: String },
|
||||
AssertAppJson(String),
|
||||
AssertApp(App),
|
||||
ReadApp(String),
|
||||
UpdateApp { name: Option<String>, desc: Option<String> },
|
||||
DeleteApp,
|
||||
|
||||
// View
|
||||
CreateView { name: String, desc: String },
|
||||
AssertView(View),
|
||||
ReadView(String),
|
||||
UpdateView { name: Option<String>, desc: Option<String> },
|
||||
DeleteView,
|
||||
DeleteViews(Vec<String>),
|
||||
|
||||
// Trash
|
||||
RestoreAppFromTrash,
|
||||
RestoreViewFromTrash,
|
||||
ReadTrash,
|
||||
DeleteAllTrash,
|
||||
|
||||
// Document
|
||||
OpenDocument,
|
||||
|
||||
// Sync
|
||||
AssertCurrentRevId(i64),
|
||||
AssertNextSyncRevId(Option<i64>),
|
||||
AssertRevisionState { rev_id: i64, state: RevisionState },
|
||||
}
|
||||
|
||||
pub struct FolderTest {
|
||||
pub sdk: FlowySDKTest,
|
||||
pub all_workspace: Vec<Workspace>,
|
||||
pub workspace: Workspace,
|
||||
pub app: App,
|
||||
pub view: View,
|
||||
pub trash: Vec<Trash>,
|
||||
pub document_info: Option<DocumentInfo>,
|
||||
// pub folder_editor:
|
||||
}
|
||||
|
||||
impl FolderTest {
|
||||
pub async fn new() -> Self {
|
||||
let sdk = FlowySDKTest::default();
|
||||
let _ = sdk.init_user().await;
|
||||
let mut workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await;
|
||||
let mut app = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await;
|
||||
let view = create_view(&sdk, &app.id, "Folder View", "Folder test view", ViewType::Doc).await;
|
||||
app.belongings = RepeatedView {
|
||||
items: vec![view.clone()],
|
||||
};
|
||||
|
||||
workspace.apps = RepeatedApp {
|
||||
items: vec![app.clone()],
|
||||
};
|
||||
Self {
|
||||
sdk,
|
||||
all_workspace: vec![],
|
||||
workspace,
|
||||
app,
|
||||
view,
|
||||
trash: vec![],
|
||||
document_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_scripts(&mut self, scripts: Vec<FolderScript>) {
|
||||
for script in scripts {
|
||||
self.run_script(script).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_script(&mut self, script: FolderScript) {
|
||||
let sdk = &self.sdk;
|
||||
let folder_editor: Arc<FolderEditor> = sdk.folder_manager.folder_editor().await;
|
||||
let rev_manager = folder_editor.rev_manager();
|
||||
let cache = rev_manager.revision_cache().await;
|
||||
|
||||
match script {
|
||||
FolderScript::ReadAllWorkspaces => {
|
||||
let all_workspace = read_workspace(sdk, None).await;
|
||||
self.all_workspace = all_workspace;
|
||||
}
|
||||
FolderScript::CreateWorkspace { name, desc } => {
|
||||
let workspace = create_workspace(sdk, &name, &desc).await;
|
||||
self.workspace = workspace;
|
||||
}
|
||||
FolderScript::AssertWorkspaceJson(expected_json) => {
|
||||
let workspace = read_workspace(sdk, Some(self.workspace.id.clone()))
|
||||
.await
|
||||
.pop()
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&workspace).unwrap();
|
||||
assert_eq!(json, expected_json);
|
||||
}
|
||||
FolderScript::AssertWorkspace(workspace) => {
|
||||
assert_eq!(self.workspace, workspace);
|
||||
}
|
||||
FolderScript::ReadWorkspace(workspace_id) => {
|
||||
let workspace = read_workspace(sdk, workspace_id).await.pop().unwrap();
|
||||
self.workspace = workspace;
|
||||
}
|
||||
FolderScript::CreateApp { name, desc } => {
|
||||
let app = create_app(sdk, &self.workspace.id, &name, &desc).await;
|
||||
self.app = app;
|
||||
}
|
||||
FolderScript::AssertAppJson(expected_json) => {
|
||||
let json = serde_json::to_string(&self.app).unwrap();
|
||||
assert_eq!(json, expected_json);
|
||||
}
|
||||
FolderScript::AssertApp(app) => {
|
||||
assert_eq!(self.app, app);
|
||||
}
|
||||
FolderScript::ReadApp(app_id) => {
|
||||
let app = read_app(sdk, &app_id).await;
|
||||
self.app = app;
|
||||
}
|
||||
FolderScript::UpdateApp { name, desc } => {
|
||||
update_app(sdk, &self.app.id, name, desc).await;
|
||||
}
|
||||
FolderScript::DeleteApp => {
|
||||
delete_app(sdk, &self.app.id).await;
|
||||
}
|
||||
|
||||
FolderScript::CreateView { name, desc } => {
|
||||
let view = create_view(sdk, &self.app.id, &name, &desc, ViewType::Doc).await;
|
||||
self.view = view;
|
||||
}
|
||||
FolderScript::AssertView(view) => {
|
||||
assert_eq!(self.view, view);
|
||||
}
|
||||
FolderScript::ReadView(view_id) => {
|
||||
let view = read_view(sdk, vec![view_id]).await;
|
||||
self.view = view;
|
||||
}
|
||||
FolderScript::UpdateView { name, desc } => {
|
||||
update_view(sdk, &self.view.id, name, desc).await;
|
||||
}
|
||||
FolderScript::DeleteView => {
|
||||
delete_view(sdk, vec![self.view.id.clone()]).await;
|
||||
}
|
||||
FolderScript::DeleteViews(view_ids) => {
|
||||
delete_view(sdk, view_ids).await;
|
||||
}
|
||||
FolderScript::RestoreAppFromTrash => {
|
||||
restore_app_from_trash(sdk, &self.app.id).await;
|
||||
}
|
||||
FolderScript::RestoreViewFromTrash => {
|
||||
restore_view_from_trash(sdk, &self.view.id).await;
|
||||
}
|
||||
FolderScript::ReadTrash => {
|
||||
let trash = read_trash(sdk).await;
|
||||
self.trash = trash.into_inner();
|
||||
}
|
||||
FolderScript::DeleteAllTrash => {
|
||||
delete_all_trash(sdk).await;
|
||||
self.trash = vec![];
|
||||
}
|
||||
FolderScript::OpenDocument => {
|
||||
let document_info = open_document(sdk, &self.view.id).await;
|
||||
self.document_info = Some(document_info);
|
||||
}
|
||||
FolderScript::AssertRevisionState { rev_id, state } => {
|
||||
let record = cache.get(rev_id).await.unwrap();
|
||||
assert_eq!(record.state, state);
|
||||
if let RevisionState::Ack = state {
|
||||
// There is a defer action that writes the revisions to disk, so we wait here.
|
||||
// Make sure everything is written.
|
||||
sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await;
|
||||
}
|
||||
}
|
||||
FolderScript::AssertCurrentRevId(rev_id) => {
|
||||
assert_eq!(rev_manager.rev_id(), rev_id, "Current rev_id is not match");
|
||||
}
|
||||
FolderScript::AssertNextSyncRevId(rev_id) => {
|
||||
let next_revision = rev_manager.next_sync_revision().await.unwrap();
|
||||
if rev_id.is_none() {
|
||||
assert!(next_revision.is_none(), "Next revision should be None");
|
||||
return;
|
||||
}
|
||||
let next_revision = next_revision
|
||||
.unwrap_or_else(|| panic!("Expected Next revision is {}, but receive None", rev_id.unwrap()));
|
||||
let mut receiver = rev_manager.revision_ack_receiver();
|
||||
let _ = receiver.recv().await;
|
||||
assert_eq!(next_revision.rev_id, rev_id.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
|
||||
vec![
|
||||
("".to_owned(), ErrorCode::WorkspaceNameInvalid),
|
||||
("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user