From b21ee5d2de69c0c1894cb147441fc99e32b86b76 Mon Sep 17 00:00:00 2001
From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com>
Date: Sat, 18 Mar 2023 06:45:12 +0800
Subject: [PATCH 01/31] feat: migration database (#2009)
* feat: migration database
* ci: fix tauri ci
* feat: migrate database view
* ci: fix ci
---
frontend/appflowy_tauri/package.json | 1 +
.../rust-lib/flowy-codegen/src/flowy_toml.rs | 8 +-
.../flowy-codegen/src/protobuf_file/mod.rs | 9 ++
.../src/deps_resolve/folder_deps.rs | 14 +-
.../flowy-core/src/deps_resolve/grid_deps.rs | 15 +-
frontend/rust-lib/flowy-core/src/lib.rs | 34 +++-
.../rust-lib/flowy-database/src/manager.rs | 56 +++----
.../src/services/database/database_editor.rs | 1 +
.../services/database_view/editor_manager.rs | 20 +--
.../date_type_option/date_type_option.rs | 5 +-
.../src/services/persistence/database_ref.rs | 4 +-
...uct_migration.rs => database_migration.rs} | 36 +++--
.../migration/database_ref_indexing.rs | 47 ------
.../migration/database_view_migration.rs | 147 ++++++++++++++++++
.../src/services/persistence/migration/mod.rs | 80 ++++++----
.../flowy-document/src/services/migration.rs | 5 +-
frontend/rust-lib/flowy-folder/src/manager.rs | 18 ++-
17 files changed, 337 insertions(+), 163 deletions(-)
rename frontend/rust-lib/flowy-database/src/services/persistence/migration/{database_rev_struct_migration.rs => database_migration.rs} (62%)
delete mode 100644 frontend/rust-lib/flowy-database/src/services/persistence/migration/database_ref_indexing.rs
create mode 100644 frontend/rust-lib/flowy-database/src/services/persistence/migration/database_view_migration.rs
diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json
index 8ad6f3a410..e0fa62458c 100644
--- a/frontend/appflowy_tauri/package.json
+++ b/frontend/appflowy_tauri/package.json
@@ -27,6 +27,7 @@
"is-hotkey": "^0.2.0",
"jest": "^29.5.0",
"nanoid": "^4.0.0",
+ "protoc-gen-ts": "^0.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
diff --git a/frontend/rust-lib/flowy-codegen/src/flowy_toml.rs b/frontend/rust-lib/flowy-codegen/src/flowy_toml.rs
index 558374f44c..da6f468e0e 100644
--- a/frontend/rust-lib/flowy-codegen/src/flowy_toml.rs
+++ b/frontend/rust-lib/flowy-codegen/src/flowy_toml.rs
@@ -20,11 +20,15 @@ pub struct FlowyConfig {
}
fn default_proto_output() -> String {
- "resources/proto".to_owned()
+ let mut path = PathBuf::from("resources");
+ path.push("proto");
+ path.to_str().unwrap().to_owned()
}
fn default_protobuf_crate() -> String {
- "src/protobuf".to_owned()
+ let mut path = PathBuf::from("src");
+ path.push("protobuf");
+ path.to_str().unwrap().to_owned()
}
impl FlowyConfig {
diff --git a/frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs
index de0209cf49..d01dea5ed7 100644
--- a/frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs
+++ b/frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs
@@ -127,6 +127,15 @@ fn generate_ts_protobuf_files(
}
let protoc_bin_path = protoc_bin_path.to_str().unwrap().to_owned();
paths.iter().for_each(|path| {
+ // if let Err(err) = Command::new(protoc_bin_path.clone())
+ // .arg(format!("--ts_out={}", output.to_str().unwrap()))
+ // .arg(format!("--proto_path={}", proto_file_output_path))
+ // .arg(path)
+ // .spawn()
+ // {
+ // panic!("Generate ts pb file failed: {}, {:?}", path, err);
+ // }
+
let result = cmd_lib::run_cmd! {
${protoc_bin_path} --ts_out=${output} --proto_path=${proto_file_output_path} ${path}
};
diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs
index b7f1fd7987..9bea5efd32 100644
--- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs
+++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs
@@ -60,12 +60,12 @@ impl FolderDepsResolver {
.await,
);
- if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
- match folder_manager.initialize(&user_id, &token).await {
- Ok(_) => {},
- Err(e) => tracing::error!("Initialize folder manager failed: {}", e),
- }
- }
+ // if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
+ // match folder_manager.initialize(&user_id, &token).await {
+ // Ok(_) => {},
+ // Err(e) => tracing::error!("Initialize folder manager failed: {}", e),
+ // }
+ // }
let receiver = Arc::new(FolderWSMessageReceiverImpl(folder_manager.clone()));
ws_conn.add_ws_message_receiver(receiver).unwrap();
@@ -339,7 +339,7 @@ impl ViewDataProcessor for DatabaseViewDataProcessor {
}
}
-fn layout_type_from_view_layout(layout: ViewLayoutTypePB) -> LayoutTypePB {
+pub fn layout_type_from_view_layout(layout: ViewLayoutTypePB) -> LayoutTypePB {
match layout {
ViewLayoutTypePB::Grid => LayoutTypePB::Grid,
ViewLayoutTypePB::Board => LayoutTypePB::Board,
diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs
index a728cc13f1..63fdf4bcad 100644
--- a/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs
+++ b/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs
@@ -25,21 +25,12 @@ impl DatabaseDepsResolver {
) -> Arc {
let user = Arc::new(GridUserImpl(user_session.clone()));
let rev_web_socket = Arc::new(GridRevisionWebSocket(ws_conn));
- let database_manager = Arc::new(DatabaseManager::new(
- user.clone(),
+ Arc::new(DatabaseManager::new(
+ user,
rev_web_socket,
task_scheduler,
Arc::new(DatabaseDBConnectionImpl(user_session)),
- ));
-
- if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
- match database_manager.initialize(&user_id, &token).await {
- Ok(_) => {},
- Err(e) => tracing::error!("Initialize grid manager failed: {}", e),
- }
- }
-
- database_manager
+ ))
}
}
diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs
index 0ecdc807a6..c37efabad7 100644
--- a/frontend/rust-lib/flowy-core/src/lib.rs
+++ b/frontend/rust-lib/flowy-core/src/lib.rs
@@ -6,7 +6,7 @@ use flowy_database::manager::DatabaseManager;
use flowy_document::entities::DocumentVersionPB;
use flowy_document::{DocumentConfig, DocumentManager};
use flowy_error::FlowyResult;
-use flowy_folder::entities::ViewDataFormatPB;
+use flowy_folder::entities::{ViewDataFormatPB, ViewLayoutTypePB};
use flowy_folder::{errors::FlowyError, manager::FolderManager};
pub use flowy_net::get_client_server_configuration;
use flowy_net::local_server::LocalServer;
@@ -17,6 +17,7 @@ use flowy_user::services::{UserSession, UserSessionConfig};
use lib_dispatch::prelude::*;
use lib_dispatch::runtime::tokio_default_runtime;
+use flowy_database::entities::LayoutTypePB;
use lib_infra::future::{to_fut, Fut};
use module::make_plugins;
pub use module::*;
@@ -310,7 +311,36 @@ impl UserStatusListener {
async fn did_sign_in(&self, token: &str, user_id: &str) -> FlowyResult<()> {
self.folder_manager.initialize(user_id, token).await?;
self.document_manager.initialize(user_id).await?;
- self.database_manager.initialize(user_id, token).await?;
+
+ let cloned_folder_manager = self.folder_manager.clone();
+ let get_views_fn = to_fut(async move {
+ cloned_folder_manager
+ .get_current_workspace()
+ .await
+ .map(|workspace| {
+ workspace
+ .apps
+ .items
+ .into_iter()
+ .flat_map(|app| app.belongings.items)
+ .flat_map(|view| match view.layout {
+ ViewLayoutTypePB::Grid | ViewLayoutTypePB::Board | ViewLayoutTypePB::Calendar => {
+ Some((
+ view.id,
+ view.name,
+ layout_type_from_view_layout(view.layout),
+ ))
+ },
+ _ => None,
+ })
+ .collect::>()
+ })
+ .unwrap_or_default()
+ });
+ self
+ .database_manager
+ .initialize(user_id, token, get_views_fn)
+ .await?;
self
.ws_conn
.start(token.to_owned(), user_id.to_owned())
diff --git a/frontend/rust-lib/flowy-database/src/manager.rs b/frontend/rust-lib/flowy-database/src/manager.rs
index 650dc3ec64..4bedef024d 100644
--- a/frontend/rust-lib/flowy-database/src/manager.rs
+++ b/frontend/rust-lib/flowy-database/src/manager.rs
@@ -7,9 +7,7 @@ use crate::services::database_view::{
make_database_view_rev_manager, make_database_view_revision_pad, DatabaseViewEditor,
};
use crate::services::persistence::block_index::BlockRowIndexer;
-use crate::services::persistence::database_ref::{
- DatabaseInfo, DatabaseRefIndexer, DatabaseViewRef,
-};
+use crate::services::persistence::database_ref::{DatabaseInfo, DatabaseRefs, DatabaseViewRef};
use crate::services::persistence::kv::DatabaseKVPersistence;
use crate::services::persistence::migration::DatabaseMigration;
use crate::services::persistence::rev_sqlite::{
@@ -31,6 +29,7 @@ use flowy_revision::{
use flowy_sqlite::ConnectionPool;
use flowy_task::TaskDispatcher;
+use lib_infra::future::Fut;
use revision_model::Revision;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -45,7 +44,7 @@ pub struct DatabaseManager {
editors_by_database_id: RwLock>>,
database_user: Arc,
block_indexer: Arc,
- database_ref_indexer: Arc,
+ database_refs: Arc,
#[allow(dead_code)]
kv_persistence: Arc,
task_scheduler: Arc>,
@@ -63,30 +62,30 @@ impl DatabaseManager {
let editors_by_database_id = RwLock::new(HashMap::new());
let kv_persistence = Arc::new(DatabaseKVPersistence::new(database_db.clone()));
let block_indexer = Arc::new(BlockRowIndexer::new(database_db.clone()));
- let database_ref_indexer = Arc::new(DatabaseRefIndexer::new(database_db.clone()));
- let migration = DatabaseMigration::new(
- database_user.clone(),
- database_db,
- database_ref_indexer.clone(),
- );
+ let database_refs = Arc::new(DatabaseRefs::new(database_db));
+ let migration = DatabaseMigration::new(database_user.clone(), database_refs.clone());
Self {
editors_by_database_id,
database_user,
kv_persistence,
block_indexer,
- database_ref_indexer,
+ database_refs,
task_scheduler,
migration,
}
}
- pub async fn initialize_with_new_user(&self, user_id: &str, _token: &str) -> FlowyResult<()> {
- self.migration.run(user_id).await?;
+ pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> {
Ok(())
}
- pub async fn initialize(&self, user_id: &str, _token: &str) -> FlowyResult<()> {
- self.migration.run(user_id).await?;
+ pub async fn initialize(
+ &self,
+ user_id: &str,
+ _token: &str,
+ get_views_fn: Fut>,
+ ) -> FlowyResult<()> {
+ self.migration.run(user_id, get_views_fn).await?;
Ok(())
}
@@ -100,7 +99,7 @@ impl DatabaseManager {
) -> FlowyResult<()> {
let db_pool = self.database_user.db_pool()?;
let _ = self
- .database_ref_indexer
+ .database_refs
.bind(database_id, view_id.as_ref(), true, name);
let rev_manager = self.make_database_rev_manager(database_id, db_pool)?;
rev_manager.reset_object(revisions).await?;
@@ -115,7 +114,9 @@ impl DatabaseManager {
revisions: Vec,
) -> FlowyResult<()> {
let view_id = view_id.as_ref();
- let rev_manager = make_database_view_rev_manager(&self.database_user, view_id).await?;
+ let user_id = self.database_user.user_id()?;
+ let pool = self.database_user.db_pool()?;
+ let rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
rev_manager.reset_object(revisions).await?;
Ok(())
}
@@ -131,12 +132,13 @@ impl DatabaseManager {
Ok(())
}
+ #[tracing::instrument(level = "trace", skip_all, err)]
pub async fn open_database_view>(
&self,
view_id: T,
) -> FlowyResult> {
let view_id = view_id.as_ref();
- let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
+ let database_info = self.database_refs.get_database_with_view(view_id)?;
self
.get_or_create_database_editor(&database_info.database_id, view_id)
.await
@@ -145,7 +147,7 @@ impl DatabaseManager {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
- let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
+ let database_info = self.database_refs.get_database_with_view(view_id)?;
tracing::Span::current().record("database_id", &database_info.database_id);
// Create a temporary reference database_editor in case of holding the write lock
@@ -174,7 +176,7 @@ impl DatabaseManager {
// #[tracing::instrument(level = "debug", skip(self), err)]
pub async fn get_database_editor(&self, view_id: &str) -> FlowyResult> {
- let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
+ let database_info = self.database_refs.get_database_with_view(view_id)?;
let database_editor = self
.editors_by_database_id
.read()
@@ -191,16 +193,14 @@ impl DatabaseManager {
}
pub async fn get_databases(&self) -> FlowyResult> {
- self.database_ref_indexer.get_all_databases()
+ self.database_refs.get_all_databases()
}
pub async fn get_database_ref_views(
&self,
database_id: &str,
) -> FlowyResult> {
- self
- .database_ref_indexer
- .get_ref_views_with_database(database_id)
+ self.database_refs.get_ref_views_with_database(database_id)
}
async fn get_or_create_database_editor(
@@ -282,7 +282,7 @@ impl DatabaseManager {
database_pad,
rev_manager,
self.block_indexer.clone(),
- self.database_ref_indexer.clone(),
+ self.database_refs.clone(),
self.task_scheduler.clone(),
)
.await?;
@@ -359,7 +359,7 @@ pub async fn link_existing_database(
.await?;
let _ = database_manager
- .database_ref_indexer
+ .database_refs
.bind(database_id, view_id, false, &name);
Ok(())
}
@@ -429,13 +429,13 @@ pub async fn create_new_database(
Ok(())
}
-impl DatabaseRefIndexerQuery for DatabaseRefIndexer {
+impl DatabaseRefIndexerQuery for DatabaseRefs {
fn get_ref_views(&self, database_id: &str) -> FlowyResult> {
self.get_ref_views_with_database(database_id)
}
}
-impl DatabaseRefIndexerQuery for Arc {
+impl DatabaseRefIndexerQuery for Arc {
fn get_ref_views(&self, database_id: &str) -> FlowyResult> {
(**self).get_ref_views(database_id)
}
diff --git a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs
index fa19ced144..217b5f6d22 100644
--- a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs
+++ b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs
@@ -695,6 +695,7 @@ impl DatabaseEditor {
Ok(())
}
+ #[tracing::instrument(level = "trace", skip(self), err)]
pub async fn get_database(&self, view_id: &str) -> FlowyResult {
let pad = self.database_pad.read().await;
let fields = pad
diff --git a/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs b/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs
index 23feaf3d5c..cff486f1c5 100644
--- a/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs
+++ b/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs
@@ -313,7 +313,9 @@ impl DatabaseViews {
}
async fn make_view_editor(&self, view_id: &str) -> FlowyResult {
- let rev_manager = make_database_view_rev_manager(&self.user, view_id).await?;
+ let user_id = self.user.user_id()?;
+ let pool = self.user.db_pool()?;
+ let rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
let user_id = self.user.user_id()?;
let token = self.user.token()?;
let view_id = view_id.to_owned();
@@ -338,7 +340,9 @@ pub async fn make_database_view_revision_pad(
DatabaseViewRevisionPad,
RevisionManager>,
)> {
- let mut rev_manager = make_database_view_rev_manager(&user, view_id).await?;
+ let user_id = user.user_id()?;
+ let pool = user.db_pool()?;
+ let mut rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
let view_rev_pad = rev_manager
.initialize::(None)
.await?;
@@ -346,16 +350,14 @@ pub async fn make_database_view_revision_pad(
}
pub async fn make_database_view_rev_manager(
- user: &Arc,
+ user_id: &str,
+ pool: Arc,
view_id: &str,
) -> FlowyResult>> {
- let user_id = user.user_id()?;
-
// Create revision persistence
- let pool = user.db_pool()?;
- let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(&user_id, pool.clone());
+ let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(user_id, pool.clone());
let configuration = RevisionPersistenceConfiguration::new(2, false);
- let rev_persistence = RevisionPersistence::new(&user_id, view_id, disk_cache, configuration);
+ let rev_persistence = RevisionPersistence::new(user_id, view_id, disk_cache, configuration);
// Create snapshot persistence
const DATABASE_VIEW_SP_PREFIX: &str = "grid_view";
@@ -365,7 +367,7 @@ pub async fn make_database_view_rev_manager(
let rev_compress = DatabaseViewRevisionMergeable();
Ok(RevisionManager::new(
- &user_id,
+ user_id,
view_id,
rev_persistence,
rev_compress,
diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs
index 431ee508cd..a42b5fd100 100644
--- a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs
+++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs
@@ -75,7 +75,7 @@ impl DateTypeOptionPB {
let time = if include_time {
let fmt = self.time_format.format_str();
- format!("{}", naive.format_with_items(StrftimeItems::new(&fmt)))
+ format!("{}", naive.format_with_items(StrftimeItems::new(fmt)))
} else {
"".to_string()
};
@@ -95,8 +95,7 @@ impl DateTypeOptionPB {
) -> FlowyResult {
if let Some(time_str) = time_str.as_ref() {
if !time_str.is_empty() {
- let naive_time =
- chrono::NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
+ let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str());
match naive_time {
Ok(naive_time) => {
diff --git a/frontend/rust-lib/flowy-database/src/services/persistence/database_ref.rs b/frontend/rust-lib/flowy-database/src/services/persistence/database_ref.rs
index 6ba13b8e1a..607946c600 100644
--- a/frontend/rust-lib/flowy-database/src/services/persistence/database_ref.rs
+++ b/frontend/rust-lib/flowy-database/src/services/persistence/database_ref.rs
@@ -7,11 +7,11 @@ use flowy_sqlite::{
};
use std::sync::Arc;
-pub struct DatabaseRefIndexer {
+pub struct DatabaseRefs {
database: Arc,
}
-impl DatabaseRefIndexer {
+impl DatabaseRefs {
pub fn new(database: Arc) -> Self {
Self { database }
}
diff --git a/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_rev_struct_migration.rs b/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_migration.rs
similarity index 62%
rename from frontend/rust-lib/flowy-database/src/services/persistence/migration/database_rev_struct_migration.rs
rename to frontend/rust-lib/flowy-database/src/services/persistence/migration/database_migration.rs
index c467901160..2c351e7327 100644
--- a/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_rev_struct_migration.rs
+++ b/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_migration.rs
@@ -1,6 +1,7 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_variables)]
+use crate::services::persistence::migration::MigratedDatabase;
use crate::services::persistence::rev_sqlite::SQLiteDatabaseRevisionPersistence;
use bytes::Bytes;
use database_model::DatabaseRevision;
@@ -15,31 +16,36 @@ use lib_infra::util::md5;
use revision_model::Revision;
use std::sync::Arc;
-const V1_MIGRATION: &str = "GRID_V1_MIGRATION";
+const V1_MIGRATION: &str = "DATABASE_V1_MIGRATION";
+pub fn is_database_rev_migrated(user_id: &str) -> bool {
+ let key = migration_flag_key(&user_id, V1_MIGRATION);
+ KV::get_bool(&key)
+}
-pub async fn migration_database_rev_struct(
+pub(crate) async fn migration_database_rev_struct(
user_id: &str,
- database_id: &str,
+ databases: &Vec,
pool: Arc,
) -> FlowyResult<()> {
- let key = migration_flag_key(&user_id, V1_MIGRATION, database_id);
- if KV::get_bool(&key) {
+ if is_database_rev_migrated(user_id) || databases.is_empty() {
return Ok(());
}
- let object = DatabaseRevisionResettable {
- database_id: database_id.to_owned(),
- };
- let disk_cache = SQLiteDatabaseRevisionPersistence::new(&user_id, pool);
- let reset = RevisionStructReset::new(&user_id, object, Arc::new(disk_cache));
- reset.run().await?;
-
- tracing::trace!("Run database:{} v1 migration", database_id);
+ tracing::debug!("Migrate databases");
+ for database in databases {
+ let object = DatabaseRevisionResettable {
+ database_id: database.view_id.clone(),
+ };
+ let disk_cache = SQLiteDatabaseRevisionPersistence::new(&user_id, pool.clone());
+ let reset = RevisionStructReset::new(&user_id, object, Arc::new(disk_cache));
+ reset.run().await?;
+ }
+ let key = migration_flag_key(&user_id, V1_MIGRATION);
KV::set_bool(&key, true);
Ok(())
}
-fn migration_flag_key(user_id: &str, version: &str, grid_id: &str) -> String {
- md5(format!("{}{}{}", user_id, version, grid_id,))
+fn migration_flag_key(user_id: &str, version: &str) -> String {
+ md5(format!("{}{}", user_id, version,))
}
struct DatabaseRevisionResettable {
diff --git a/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_ref_indexing.rs b/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_ref_indexing.rs
deleted file mode 100644
index c80d50a755..0000000000
--- a/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_ref_indexing.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use crate::manager::DatabaseUser;
-use crate::services::database_view::make_database_view_revision_pad;
-use crate::services::persistence::database_ref::DatabaseRefIndexer;
-
-use flowy_error::{FlowyError, FlowyResult};
-use flowy_sqlite::kv::KV;
-
-use flowy_sqlite::{
- prelude::*,
- schema::{grid_view_rev_table, grid_view_rev_table::dsl},
-};
-use lib_infra::util::md5;
-
-use std::sync::Arc;
-
-const DATABASE_REF_INDEXING: &str = "database_ref_indexing";
-
-pub async fn indexing_database_view_refs(
- user_id: &str,
- user: Arc,
- database_ref_indexer: Arc,
-) -> FlowyResult<()> {
- let key = md5(format!("{}{}", user_id, DATABASE_REF_INDEXING));
- if KV::get_bool(&key) {
- return Ok(());
- }
- tracing::trace!("Indexing database view refs");
- let pool = user.db_pool()?;
- let view_ids = dsl::grid_view_rev_table
- .select(grid_view_rev_table::object_id)
- .distinct()
- .load::(&*pool.get().map_err(|e| FlowyError::internal().context(e))?)?;
-
- for view_id in view_ids {
- if let Ok((pad, _)) = make_database_view_revision_pad(&view_id, user.clone()).await {
- tracing::trace!(
- "Indexing database:{} with view:{}",
- pad.database_id,
- pad.view_id
- );
- let _ = database_ref_indexer.bind(&pad.database_id, &pad.view_id, true, &pad.name);
- }
- }
-
- KV::set_bool(&key, true);
- Ok(())
-}
diff --git a/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_view_migration.rs b/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_view_migration.rs
new file mode 100644
index 0000000000..5479a34649
--- /dev/null
+++ b/frontend/rust-lib/flowy-database/src/services/persistence/migration/database_view_migration.rs
@@ -0,0 +1,147 @@
+use crate::services::database_view::make_database_view_rev_manager;
+use crate::services::persistence::database_ref::DatabaseRefs;
+use flowy_error::FlowyResult;
+use flowy_sqlite::kv::KV;
+
+use crate::services::persistence::migration::MigratedDatabase;
+use crate::services::persistence::rev_sqlite::SQLiteDatabaseViewRevisionPersistence;
+use bytes::Bytes;
+use database_model::DatabaseViewRevision;
+use flowy_client_sync::client_database::{
+ make_database_view_operations, make_database_view_rev_json_str, DatabaseViewOperationsBuilder,
+ DatabaseViewRevisionPad,
+};
+use flowy_revision::reset::{RevisionResettable, RevisionStructReset};
+use flowy_sqlite::{
+ prelude::*,
+ schema::{grid_view_rev_table, grid_view_rev_table::dsl},
+};
+use lib_infra::util::md5;
+use revision_model::Revision;
+use std::sync::Arc;
+
+const DATABASE_VIEW_MIGRATE: &str = "database_view_migrate";
+
+pub fn is_database_view_migrated(user_id: &str) -> bool {
+ let key = md5(format!("{}{}", user_id, DATABASE_VIEW_MIGRATE));
+ KV::get_bool(&key)
+}
+
+pub(crate) async fn migrate_database_view(
+ user_id: &str,
+ database_refs: Arc,
+ migrated_databases: &Vec,
+ pool: Arc,
+) -> FlowyResult<()> {
+ if is_database_view_migrated(user_id) || migrated_databases.is_empty() {
+ return Ok(());
+ }
+
+ let mut database_with_view = vec![];
+
+ let database_without_view = {
+ let conn = pool.get()?;
+ let databases = migrated_databases
+ .iter()
+ .filter(|database| {
+ let predicate = grid_view_rev_table::object_id.eq(&database.view_id);
+ let exist = diesel::dsl::exists(dsl::grid_view_rev_table.filter(predicate));
+ match select(exist).get_result::(&*conn) {
+ Ok(is_exist) => {
+ if is_exist {
+ database_with_view.push((**database).clone())
+ }
+ !is_exist
+ },
+ Err(_) => true,
+ }
+ })
+ .collect::>();
+ drop(conn);
+ databases
+ };
+
+ // Create database view if it's not exist.
+ for database in database_without_view {
+ tracing::debug!("[Migration]: create database view: {}", database.view_id);
+ let database_id = database.view_id.clone();
+ let database_view_id = database.view_id.clone();
+ //
+ let database_view_rev = DatabaseViewRevision::new(
+ database_id,
+ database_view_id.clone(),
+ true,
+ database.name.clone(),
+ database.layout.clone(),
+ );
+ let database_view_ops = make_database_view_operations(&database_view_rev);
+ let database_view_bytes = database_view_ops.json_bytes();
+ let revision = Revision::initial_revision(&database_view_id, database_view_bytes);
+ let rev_manager =
+ make_database_view_rev_manager(user_id, pool.clone(), &database_view_id).await?;
+ rev_manager.reset_object(vec![revision]).await?;
+ }
+
+ // Reset existing database view
+ for database in database_with_view {
+ let object = DatabaseViewRevisionResettable {
+ database_view_id: database.view_id.clone(),
+ };
+ let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(user_id, pool.clone());
+ let reset = RevisionStructReset::new(user_id, object, Arc::new(disk_cache));
+ reset.run().await?;
+ }
+
+ tracing::debug!("[Migration]: Add database view refs");
+ for database in migrated_databases {
+ // Bind the database with database view id. For historical reasons,
+ // the default database_id is empty, so the view_id will be used
+ // as the database_id.
+ let database_id = database.view_id.clone();
+ let database_view_id = database.view_id.clone();
+ tracing::debug!(
+ "Bind database:{} with view:{}",
+ database_id,
+ database_view_id
+ );
+ let _ = database_refs.bind(&database_id, &database_view_id, true, &database.name);
+ }
+
+ let key = md5(format!("{}{}", user_id, DATABASE_VIEW_MIGRATE));
+ KV::set_bool(&key, true);
+ Ok(())
+}
+
+struct DatabaseViewRevisionResettable {
+ database_view_id: String,
+}
+
+impl RevisionResettable for DatabaseViewRevisionResettable {
+ fn target_id(&self) -> &str {
+ &self.database_view_id
+ }
+
+ fn reset_data(&self, revisions: Vec) -> FlowyResult {
+ let pad = DatabaseViewRevisionPad::from_revisions(revisions)?;
+ let json = pad.json_str()?;
+ let bytes = DatabaseViewOperationsBuilder::new()
+ .insert(&json)
+ .build()
+ .json_bytes();
+ Ok(bytes)
+ }
+
+ fn default_target_rev_str(&self) -> FlowyResult {
+ let database_view_rev = DatabaseViewRevision::default();
+ let json = make_database_view_rev_json_str(&database_view_rev)?;
+ Ok(json)
+ }
+
+ fn read_record(&self) -> Option {
+ KV::get_str(self.target_id())
+ }
+
+ fn set_record(&self, record: String) {
+ KV::set_str(self.target_id(), record);
+ }
+}
diff --git a/frontend/rust-lib/flowy-database/src/services/persistence/migration/mod.rs b/frontend/rust-lib/flowy-database/src/services/persistence/migration/mod.rs
index 613224c5b9..7be1d5f27b 100644
--- a/frontend/rust-lib/flowy-database/src/services/persistence/migration/mod.rs
+++ b/frontend/rust-lib/flowy-database/src/services/persistence/migration/mod.rs
@@ -1,49 +1,69 @@
-mod database_ref_indexing;
-mod database_rev_struct_migration;
-
+mod database_migration;
+mod database_view_migration;
+use crate::entities::LayoutTypePB;
use crate::manager::DatabaseUser;
-use crate::services::persistence::database_ref::DatabaseRefIndexer;
-use crate::services::persistence::migration::database_ref_indexing::indexing_database_view_refs;
-use crate::services::persistence::migration::database_rev_struct_migration::migration_database_rev_struct;
-use crate::services::persistence::DatabaseDBConnection;
+use crate::services::persistence::database_ref::DatabaseRefs;
+use crate::services::persistence::migration::database_migration::{
+ is_database_rev_migrated, migration_database_rev_struct,
+};
+use crate::services::persistence::migration::database_view_migration::{
+ is_database_view_migrated, migrate_database_view,
+};
+use database_model::LayoutRevision;
use flowy_error::FlowyResult;
+use lib_infra::future::Fut;
use std::sync::Arc;
pub(crate) struct DatabaseMigration {
#[allow(dead_code)]
user: Arc,
- database: Arc,
- database_ref_indexer: Arc,
+ database_refs: Arc,
}
impl DatabaseMigration {
- pub fn new(
- user: Arc,
- database: Arc,
- database_ref_indexer: Arc,
- ) -> Self {
+ pub fn new(user: Arc, database_refs: Arc) -> Self {
Self {
user,
- database,
- database_ref_indexer,
+ database_refs,
}
}
- pub async fn run(&self, user_id: &str) -> FlowyResult<()> {
- let _ = indexing_database_view_refs(
- user_id,
- self.user.clone(),
- self.database_ref_indexer.clone(),
- )
- .await;
- Ok(())
- }
+ pub async fn run(
+ &self,
+ user_id: &str,
+ get_views_fn: Fut>,
+ ) -> FlowyResult<()> {
+ let pool = self.user.db_pool()?;
+
+ if !is_database_view_migrated(user_id) || !is_database_rev_migrated(user_id) {
+ let migrated_databases = get_views_fn
+ .await
+ .into_iter()
+ .map(|(view_id, name, layout)| MigratedDatabase {
+ view_id,
+ name,
+ layout: layout.into(),
+ })
+ .collect::>();
+
+ migration_database_rev_struct(user_id, &migrated_databases, pool.clone()).await?;
+
+ let _ = migrate_database_view(
+ user_id,
+ self.database_refs.clone(),
+ &migrated_databases,
+ pool.clone(),
+ )
+ .await;
+ }
- #[allow(dead_code)]
- pub async fn database_rev_struct_migration(&self, grid_id: &str) -> FlowyResult<()> {
- let user_id = self.user.user_id()?;
- let pool = self.database.get_db_pool()?;
- migration_database_rev_struct(&user_id, grid_id, pool).await?;
Ok(())
}
}
+
+#[derive(Debug, Clone)]
+pub(crate) struct MigratedDatabase {
+ pub(crate) view_id: String,
+ pub(crate) name: String,
+ pub(crate) layout: LayoutRevision,
+}
diff --git a/frontend/rust-lib/flowy-document/src/services/migration.rs b/frontend/rust-lib/flowy-document/src/services/migration.rs
index 2570ecbabf..2ea9bea1dc 100644
--- a/frontend/rust-lib/flowy-document/src/services/migration.rs
+++ b/frontend/rust-lib/flowy-document/src/services/migration.rs
@@ -32,10 +32,7 @@ impl DocumentMigration {
let conn = &*pool.get()?;
let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool);
let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?;
- tracing::debug!(
- "[Document Migration]: try migrate {} documents",
- documents.len()
- );
+ tracing::debug!("[Migration]: try migrate {} documents", documents.len());
for revisions in documents {
if revisions.is_empty() {
continue;
diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs
index 76e5a6277a..088688ee26 100644
--- a/frontend/rust-lib/flowy-folder/src/manager.rs
+++ b/frontend/rust-lib/flowy-folder/src/manager.rs
@@ -1,5 +1,5 @@
use crate::entities::view::ViewDataFormatPB;
-use crate::entities::{ViewLayoutTypePB, ViewPB};
+use crate::entities::{ViewLayoutTypePB, ViewPB, WorkspacePB};
use crate::services::folder_editor::FolderRevisionMergeable;
use crate::{
entities::workspace::RepeatedWorkspacePB,
@@ -21,10 +21,10 @@ use folder_model::user_default;
use lazy_static::lazy_static;
use lib_infra::future::FutureResult;
-use crate::services::clear_current_workspace;
use crate::services::persistence::rev_sqlite::{
SQLiteFolderRevisionPersistence, SQLiteFolderRevisionSnapshotPersistence,
};
+use crate::services::{clear_current_workspace, get_current_workspace};
use flowy_client_sync::client_folder::FolderPad;
use std::convert::TryFrom;
use std::{collections::HashMap, fmt::Formatter, sync::Arc};
@@ -209,6 +209,20 @@ impl FolderManager {
Ok(())
}
+ pub async fn get_current_workspace(&self) -> FlowyResult {
+ let user_id = self.user.user_id()?;
+ let workspace_id = get_current_workspace(&user_id)?;
+ let workspace = self
+ .persistence
+ .begin_transaction(|transaction| {
+ self
+ .workspace_controller
+ .read_workspace(workspace_id, &user_id, &transaction)
+ })
+ .await?;
+ Ok(workspace)
+ }
+
pub async fn initialize_with_new_user(
&self,
user_id: &str,
From 81e50b8dd56dc7dd61e519beee3533e2bbb20db8 Mon Sep 17 00:00:00 2001
From: Sudhanva-Nadiger <93595710+Sudhanva-Nadiger@users.noreply.github.com>
Date: Mon, 20 Mar 2023 16:14:41 +0530
Subject: [PATCH 02/31] feat: add icon to dropdown button in language view
(#2031)
- drop down button gives the default icon
- set auto focus true in dropdown button
- add padding to dropdown button
- change edgeIsets.only(left,right) to edgeInsets.symmetric(horizontal)
---
.../widgets/settings_language_view.dart | 47 ++++++++++---------
1 file changed, 24 insertions(+), 23 deletions(-)
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
index ffb16a2f41..20cb1eaafa 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
@@ -57,35 +57,36 @@ class _LanguageSelectorDropdownState extends State {
onEnter: (event) => {hoverEnterLanguage()},
onExit: (event) => {hoverExitLanguage()},
child: Container(
- margin: const EdgeInsets.only(left: 8, right: 8),
+ margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: currHoverColor,
),
child: DropdownButtonHideUnderline(
- child: DropdownButton(
- value: context.locale,
- onChanged: (val) {
- setState(() {
- context
- .read()
- .setLocale(context, val!);
- });
- },
- icon: const Visibility(
- visible: false,
- child: (Icon(Icons.arrow_downward)),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: DropdownButton(
+ value: context.locale,
+ onChanged: (val) {
+ setState(() {
+ context
+ .read()
+ .setLocale(context, val!);
+ });
+ },
+ autofocus: true,
+ borderRadius: BorderRadius.circular(8),
+ items:
+ EasyLocalization.of(context)!.supportedLocales.map((locale) {
+ return DropdownMenuItem(
+ value: locale,
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: FlowyText.medium(languageFromLocale(locale)),
+ ),
+ );
+ }).toList(),
),
- borderRadius: BorderRadius.circular(8),
- items: EasyLocalization.of(context)!.supportedLocales.map((locale) {
- return DropdownMenuItem(
- value: locale,
- child: Padding(
- padding: const EdgeInsets.all(12.0),
- child: FlowyText.medium(languageFromLocale(locale)),
- ),
- );
- }).toList(),
),
),
),
From 893aae002e3834652d73b8627e57c8111dc137e3 Mon Sep 17 00:00:00 2001
From: Samiksha Garg <79906086+Samiksha-Garg@users.noreply.github.com>
Date: Mon, 20 Mar 2023 18:41:47 +0530
Subject: [PATCH 03/31] fix: scrolling selection-menu when it goes out of
bound, fixes #2019 (#2035)
* fix: scrolling selection-menu when it goes out of bound
* refactor: added comma as per style guidelines
---
.../selection_menu_service.dart | 32 +++++++++++--------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
index 7e0ce3eeea..b4f7bb5c67 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
@@ -91,20 +91,24 @@ class SelectionMenu implements SelectionMenuService {
top: showBelow ? _offset.dy : null,
bottom: showBelow ? null : _offset.dy,
left: offset.dx,
- child: SelectionMenuWidget(
- items: [
- ..._defaultSelectionMenuItems,
- ...editorState.selectionMenuItems,
- ],
- maxItemInRow: 5,
- editorState: editorState,
- menuService: this,
- onExit: () {
- dismiss();
- },
- onSelectionUpdate: () {
- _selectionUpdateByInner = true;
- },
+ right: 0,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SelectionMenuWidget(
+ items: [
+ ..._defaultSelectionMenuItems,
+ ...editorState.selectionMenuItems,
+ ],
+ maxItemInRow: 5,
+ editorState: editorState,
+ menuService: this,
+ onExit: () {
+ dismiss();
+ },
+ onSelectionUpdate: () {
+ _selectionUpdateByInner = true;
+ },
+ ),
),
);
});
From 77d787a9293391b4898131abbfb9e9960506da4e Mon Sep 17 00:00:00 2001
From: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
Date: Mon, 20 Mar 2023 21:16:37 +0800
Subject: [PATCH 04/31] feat: calendar UI improvements (#1941)
* chore: enable calendar
* feat: set font of the day event widget
* feat: support add/remove event
* chore: initial settings popover
* chore: calendar bloc can update layout settings
* fix: events overflow in day cell
* feat: calendar layout settings UI
* feat: layout calendar by another date field
* chore: i18n
* chore: hide the show weekend option
* chore: add popover mutex
* fix: clear existing events before adding new ones
---------
Co-authored-by: nathan
---
.../assets/translations/en.json | 9 +-
.../application/database_controller.dart | 52 ++-
.../database_view/application/defines.dart | 9 +-
.../layout/calendar_setting_listener.dart | 49 +++
.../application/row/row_cache.dart | 10 +-
.../application/row/row_list.dart | 22 +-
.../application/view/view_cache.dart | 79 +++-
.../calendar/application/calendar_bloc.dart | 171 ++++++--
.../application/calendar_setting_bloc.dart | 55 +++
.../database_view/calendar/calendar.dart | 2 +-
.../calendar/presentation/calendar_day.dart | 267 ++++++++++++
.../calendar/presentation/calendar_page.dart | 286 +++---------
.../toolbar/calendar_layout_setting.dart | 410 ++++++++++++++++++
.../toolbar/calendar_setting.dart | 112 +++++
.../toolbar/calendar_toolbar.dart | 74 +++-
.../grid/application/grid_bloc.dart | 2 +-
.../widgets/card/card_cell_builder.dart | 3 +
.../widgets/card/cells/card_cell.dart | 17 +-
.../card/cells/select_option_card_cell.dart | 5 +-
.../widgets/card/cells/text_card_cell.dart | 25 +-
.../test/bloc_test/board_test/util.dart | 2 +-
.../test/bloc_test/grid_test/util.dart | 2 +-
.../flowy-database/src/notification.rs | 2 -
23 files changed, 1337 insertions(+), 328 deletions(-)
create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart
create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart
create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart
create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart
diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json
index f19519d68c..f1aade4e48 100644
--- a/frontend/appflowy_flutter/assets/translations/en.json
+++ b/frontend/appflowy_flutter/assets/translations/en.json
@@ -216,7 +216,8 @@
"addFilter": "Add Filter",
"deleteFilter": "Delete filter",
"filterBy": "Filter by...",
- "typeAValue": "Type a value..."
+ "typeAValue": "Type a value...",
+ "layout": "Layout"
},
"textFilter": {
"contains": "Contains",
@@ -393,6 +394,12 @@
"jumpToday": "Jump to Today",
"previousMonth": "Previous Month",
"nextMonth": "Next Month"
+ },
+ "settings": {
+ "showWeekNumbers": "Show week numbers",
+ "showWeekends": "Show weekends",
+ "firstDayOfWeek": "First day of week",
+ "layoutDateField": "Layout calendar by"
}
}
}
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
index dac75a6e52..6cdbc964e4 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
@@ -18,11 +19,6 @@ import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
import 'group/group_listener.dart';
-typedef OnRowsChanged = void Function(
- List rowInfos,
- RowsChangedReason,
-);
-
typedef OnGroupByField = void Function(List);
typedef OnUpdateGroup = void Function(List);
typedef OnDeleteGroup = void Function(List);
@@ -52,16 +48,29 @@ class LayoutCallbacks {
});
}
+class CalendarLayoutCallbacks {
+ final void Function(LayoutSettingPB) onCalendarLayoutChanged;
+
+ CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
+}
+
class DatabaseCallbacks {
OnDatabaseChanged? onDatabaseChanged;
- OnRowsChanged? onRowsChanged;
OnFieldsChanged? onFieldsChanged;
OnFiltersChanged? onFiltersChanged;
+ OnRowsChanged? onRowsChanged;
+ OnRowsDeleted? onRowsDeleted;
+ OnRowsUpdated? onRowsUpdated;
+ OnRowsCreated? onRowsCreated;
+
DatabaseCallbacks({
this.onDatabaseChanged,
this.onRowsChanged,
this.onFieldsChanged,
this.onFiltersChanged,
+ this.onRowsUpdated,
+ this.onRowsDeleted,
+ this.onRowsCreated,
});
}
@@ -76,21 +85,23 @@ class DatabaseController {
DatabaseCallbacks? _databaseCallbacks;
GroupCallbacks? _groupCallbacks;
LayoutCallbacks? _layoutCallbacks;
+ CalendarLayoutCallbacks? _calendarLayoutCallbacks;
// Getters
- List get rowInfos => _viewCache.rowInfos;
RowCache get rowCache => _viewCache.rowCache;
// Listener
final DatabaseGroupListener groupListener;
final DatabaseLayoutListener layoutListener;
+ final DatabaseCalendarLayoutListener calendarLayoutListener;
DatabaseController({required ViewPB view, required this.layoutType})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
groupListener = DatabaseGroupListener(view.id),
- layoutListener = DatabaseLayoutListener(view.id) {
+ layoutListener = DatabaseLayoutListener(view.id),
+ calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
_viewCache = DatabaseViewCache(
viewId: viewId,
fieldController: fieldController,
@@ -99,16 +110,21 @@ class DatabaseController {
_listenOnFieldsChanged();
_listenOnGroupChanged();
_listenOnLayoutChanged();
+ if (layoutType == LayoutTypePB.Calendar) {
+ _listenOnCalendarLayoutChanged();
+ }
}
void addListener({
DatabaseCallbacks? onDatabaseChanged,
LayoutCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
+ CalendarLayoutCallbacks? onCalendarLayoutChanged,
}) {
_layoutCallbacks = onLayoutChanged;
_databaseCallbacks = onDatabaseChanged;
_groupCallbacks = onGroupChanged;
+ _calendarLayoutCallbacks = onCalendarLayoutChanged;
}
Future> open() async {
@@ -218,9 +234,17 @@ class DatabaseController {
}
void _listenOnRowsChanged() {
- _viewCache.addListener(onRowsChanged: (reason) {
- _databaseCallbacks?.onRowsChanged?.call(rowInfos, reason);
+ final callbacks =
+ DatabaseViewCallbacks(onRowsChanged: (rows, rowByRowId, reason) {
+ _databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason);
+ }, onRowsDeleted: (ids) {
+ _databaseCallbacks?.onRowsDeleted?.call(ids);
+ }, onRowsUpdated: (ids) {
+ _databaseCallbacks?.onRowsUpdated?.call(ids);
+ }, onRowsCreated: (ids) {
+ _databaseCallbacks?.onRowsCreated?.call(ids);
});
+ _viewCache.addListener(callbacks);
}
void _listenOnFieldsChanged() {
@@ -266,6 +290,14 @@ class DatabaseController {
}, (r) => Log.error(r));
});
}
+
+ void _listenOnCalendarLayoutChanged() {
+ calendarLayoutListener.start(onCalendarLayoutChanged: (result) {
+ result.fold((l) {
+ _calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
+ }, (r) => Log.error(r));
+ });
+ }
}
class RowDataBuilder {
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
index d724028e63..e45a428db7 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
@@ -10,9 +10,14 @@ import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView);
typedef OnFiltersChanged = void Function(List);
typedef OnDatabaseChanged = void Function(DatabasePB);
+
+typedef OnRowsCreated = void Function(List ids);
+typedef OnRowsUpdated = void Function(List ids);
+typedef OnRowsDeleted = void Function(List ids);
typedef OnRowsChanged = void Function(
- List,
- RowsChangedReason,
+ UnmodifiableListView rows,
+ UnmodifiableMapView rowByRowId,
+ RowsChangedReason reason,
);
typedef OnError = void Function(FlowyError);
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart
new file mode 100644
index 0000000000..9ec8c1f656
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart
@@ -0,0 +1,49 @@
+import 'dart:typed_data';
+
+import 'package:appflowy/core/grid_notification.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
+import 'package:dartz/dartz.dart';
+
+typedef NewLayoutFieldValue = Either;
+
+class DatabaseCalendarLayoutListener {
+ final String viewId;
+ PublishNotifier? _newLayoutFieldNotifier =
+ PublishNotifier();
+ DatabaseNotificationListener? _listener;
+ DatabaseCalendarLayoutListener(this.viewId);
+
+ void start(
+ {required void Function(NewLayoutFieldValue) onCalendarLayoutChanged}) {
+ _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged);
+ _listener = DatabaseNotificationListener(
+ objectId: viewId,
+ handler: _handler,
+ );
+ }
+
+ void _handler(
+ DatabaseNotification ty,
+ Either result,
+ ) {
+ switch (ty) {
+ case DatabaseNotification.DidSetNewLayoutField:
+ result.fold(
+ (payload) => _newLayoutFieldNotifier?.value =
+ left(LayoutSettingPB.fromBuffer(payload)),
+ (error) => _newLayoutFieldNotifier?.value = right(error),
+ );
+ break;
+ default:
+ break;
+ }
+ }
+
+ Future stop() async {
+ await _listener?.stop();
+ _newLayoutFieldNotifier?.dispose();
+ _newLayoutFieldNotifier = null;
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
index 7f0265ccc7..608cc7b907 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
@@ -37,11 +37,15 @@ class RowCache {
final RowCacheDelegate _delegate;
final RowChangesetNotifier _rowChangeReasonNotifier;
- UnmodifiableListView get visibleRows {
+ UnmodifiableListView get rowInfos {
var visibleRows = [..._rowList.rows];
return UnmodifiableListView(visibleRows);
}
+ UnmodifiableMapView get rowByRowId {
+ return UnmodifiableMapView(_rowList.rowInfoByRowId);
+ }
+
CellCache get cellCache => _cellCache;
RowCache({
@@ -61,6 +65,10 @@ class RowCache {
});
}
+ RowInfo? getRow(String rowId) {
+ return _rowList.get(rowId);
+ }
+
void setInitialRows(List rows) {
for (final row in rows) {
final rowInfo = buildGridRow(row);
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart
index bd163cce58..f6d4114495 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart
@@ -9,14 +9,14 @@ class RowList {
List get rows => List.from(_rowInfos);
/// Use Map for faster access the raw row data.
- final HashMap _rowInfoByRowId = HashMap();
+ final HashMap rowInfoByRowId = HashMap();
RowInfo? get(String rowId) {
- return _rowInfoByRowId[rowId];
+ return rowInfoByRowId[rowId];
}
int? indexOfRow(String rowId) {
- final rowInfo = _rowInfoByRowId[rowId];
+ final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
return _rowInfos.indexOf(rowInfo);
}
@@ -33,7 +33,7 @@ class RowList {
} else {
_rowInfos.add(rowInfo);
}
- _rowInfoByRowId[rowId] = rowInfo;
+ rowInfoByRowId[rowId] = rowInfo;
}
InsertedIndex? insert(int index, RowInfo rowInfo) {
@@ -47,21 +47,21 @@ class RowList {
if (oldRowInfo != null) {
_rowInfos.insert(insertedIndex, rowInfo);
_rowInfos.remove(oldRowInfo);
- _rowInfoByRowId[rowId] = rowInfo;
+ rowInfoByRowId[rowId] = rowInfo;
return null;
} else {
_rowInfos.insert(insertedIndex, rowInfo);
- _rowInfoByRowId[rowId] = rowInfo;
+ rowInfoByRowId[rowId] = rowInfo;
return InsertedIndex(index: insertedIndex, rowId: rowId);
}
}
DeletedIndex? remove(String rowId) {
- final rowInfo = _rowInfoByRowId[rowId];
+ final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
final index = _rowInfos.indexOf(rowInfo);
if (index != -1) {
- _rowInfoByRowId.remove(rowInfo.rowPB.id);
+ rowInfoByRowId.remove(rowInfo.rowPB.id);
_rowInfos.remove(rowInfo);
}
return DeletedIndex(index: index, rowInfo: rowInfo);
@@ -105,7 +105,7 @@ class RowList {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
- _rowInfoByRowId.remove(rowInfo.rowPB.id);
+ rowInfoByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
}
});
@@ -136,7 +136,7 @@ class RowList {
_rowInfos.clear();
for (final rowId in rowIds) {
- final rowInfo = _rowInfoByRowId[rowId];
+ final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
_rowInfos.add(rowInfo);
}
@@ -155,6 +155,6 @@ class RowList {
}
bool contains(String rowId) {
- return _rowInfoByRowId[rowId] != null;
+ return rowInfoByRowId[rowId] != null;
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
index 6f58c3fbde..002bc40ec0 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
@@ -1,22 +1,50 @@
import 'dart:async';
+import 'dart:collection';
import 'package:appflowy_backend/log.dart';
+import '../defines.dart';
import '../field/field_controller.dart';
import '../row/row_cache.dart';
import 'view_listener.dart';
+class DatabaseViewCallbacks {
+ /// Will get called when number of rows were changed that includes
+ /// update/delete/insert rows. The [onRowsChanged] will return all
+ /// the rows of the current database
+ final OnRowsChanged? onRowsChanged;
+
+ // Will get called when creating new rows
+ final OnRowsCreated? onRowsCreated;
+
+ /// Will get called when number of rows were updated
+ final OnRowsUpdated? onRowsUpdated;
+
+ /// Will get called when number of rows were deleted
+ final OnRowsDeleted? onRowsDeleted;
+
+ const DatabaseViewCallbacks({
+ this.onRowsChanged,
+ this.onRowsCreated,
+ this.onRowsUpdated,
+ this.onRowsDeleted,
+ });
+}
+
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
class DatabaseViewCache {
final String viewId;
late RowCache _rowCache;
- final DatabaseViewListener _gridViewListener;
+ final DatabaseViewListener _databaseViewListener;
+ DatabaseViewCallbacks? _callbacks;
- List get rowInfos => _rowCache.visibleRows;
+ UnmodifiableListView get rowInfos => _rowCache.rowInfos;
RowCache get rowCache => _rowCache;
+ RowInfo? getRow(String rowId) => _rowCache.getRow(rowId);
+
DatabaseViewCache({
required this.viewId,
required FieldController fieldController,
- }) : _gridViewListener = DatabaseViewListener(viewId: viewId) {
+ }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {
final delegate = RowDelegatesImpl(fieldController);
_rowCache = RowCache(
viewId: viewId,
@@ -24,10 +52,28 @@ class DatabaseViewCache {
cacheDelegate: delegate,
);
- _gridViewListener.start(
+ _databaseViewListener.start(
onRowsChanged: (result) {
result.fold(
- (changeset) => _rowCache.applyRowsChanged(changeset),
+ (changeset) {
+ // Update the cache
+ _rowCache.applyRowsChanged(changeset);
+
+ if (changeset.deletedRows.isNotEmpty) {
+ _callbacks?.onRowsDeleted?.call(changeset.deletedRows);
+ }
+
+ if (changeset.updatedRows.isNotEmpty) {
+ _callbacks?.onRowsUpdated
+ ?.call(changeset.updatedRows.map((e) => e.row.id).toList());
+ }
+
+ if (changeset.insertedRows.isNotEmpty) {
+ _callbacks?.onRowsCreated?.call(changeset.insertedRows
+ .map((insertedRow) => insertedRow.row.id)
+ .toList());
+ }
+ },
(err) => Log.error(err),
);
},
@@ -50,23 +96,22 @@ class DatabaseViewCache {
);
},
);
+
+ _rowCache.onRowsChanged(
+ (reason) => _callbacks?.onRowsChanged?.call(
+ rowInfos,
+ _rowCache.rowByRowId,
+ reason,
+ ),
+ );
}
Future dispose() async {
- await _gridViewListener.stop();
+ await _databaseViewListener.stop();
await _rowCache.dispose();
}
- void addListener({
- required void Function(RowsChangedReason) onRowsChanged,
- bool Function()? listenWhen,
- }) {
- _rowCache.onRowsChanged((reason) {
- if (listenWhen != null && listenWhen() == false) {
- return;
- }
-
- onRowsChanged(reason);
- });
+ void addListener(DatabaseViewCallbacks callbacks) {
+ _callbacks = callbacks;
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
index bf0e1ab8f6..f0cfb43b48 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
@@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart';
class CalendarBloc extends Bloc {
final DatabaseController _databaseController;
+ Map fieldInfoByFieldId = {};
// Getters
String get viewId => _databaseController.viewId;
+ FieldController get fieldController => _databaseController.fieldController;
CellCache get cellCache => _databaseController.rowCache.cellCache;
RowCache get rowCache => _databaseController.rowCache;
@@ -28,7 +30,7 @@ class CalendarBloc extends Bloc {
view: view,
layoutType: LayoutTypePB.Calendar,
),
- super(CalendarState.initial(view.id)) {
+ super(CalendarState.initial()) {
on(
(event, emit) async {
await event.when(
@@ -44,16 +46,49 @@ class CalendarBloc extends Bloc {
emit(state.copyWith(database: Some(database)));
},
didLoadAllEvents: (events) {
- emit(state.copyWith(events: events));
+ emit(state.copyWith(initialEvents: events, allEvents: events));
+ },
+ didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) {
+ _loadAllEvents();
+ emit(state.copyWith(settings: Some(layoutSettings)));
},
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
- didReceiveEvent: (CalendarEventData newEvent) {
- emit(state.copyWith(events: [...state.events, newEvent]));
+ updateCalendarLayoutSetting:
+ (CalendarLayoutSettingsPB layoutSetting) async {
+ await _updateCalendarLayoutSetting(layoutSetting);
},
- didUpdateFieldInfos: (Map fieldInfoByFieldId) {
- emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
+ didUpdateEvent: (CalendarEventData eventData) {
+ var allEvents = [...state.allEvents];
+ final index = allEvents.indexWhere(
+ (element) => element.event!.cellId == eventData.event!.cellId,
+ );
+ if (index != -1) {
+ allEvents[index] = eventData;
+ }
+ emit(state.copyWith(
+ allEvents: allEvents,
+ updateEvent: eventData,
+ ));
+ },
+ didReceiveNewEvent: (CalendarEventData event) {
+ emit(state.copyWith(
+ allEvents: [...state.allEvents, event],
+ newEvent: event,
+ ));
+ },
+ didDeleteEvents: (List deletedRowIds) {
+ var events = [...state.allEvents];
+ events.retainWhere(
+ (element) => !deletedRowIds.contains(element.event!.cellId.rowId),
+ );
+ emit(
+ state.copyWith(
+ allEvents: events,
+ deleteEventIds: deletedRowIds,
+ ),
+ );
},
);
},
@@ -97,7 +132,7 @@ class CalendarBloc extends Bloc {
}
Future _createEvent(DateTime date, String title) async {
- state.settings.fold(
+ return state.settings.fold(
() => null,
(settings) async {
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
@@ -110,8 +145,8 @@ class CalendarBloc extends Bloc {
},
);
- result.fold(
- (newRow) => _loadEvent(newRow.id),
+ return result.fold(
+ (newRow) {},
(err) => Log.error(err),
);
}
@@ -119,17 +154,23 @@ class CalendarBloc extends Bloc {
);
}
- Future _loadEvent(String rowId) async {
+ Future _updateCalendarLayoutSetting(
+ CalendarLayoutSettingsPB layoutSetting) async {
+ return _databaseController.updateCalenderLayoutSetting(layoutSetting);
+ }
+
+ Future?> _loadEvent(String rowId) async {
final payload = RowIdPB(viewId: viewId, rowId: rowId);
- DatabaseEventGetCalendarEvent(payload).send().then((result) {
- result.fold(
+ return DatabaseEventGetCalendarEvent(payload).send().then((result) {
+ return result.fold(
(eventPB) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
- if (calendarEvent != null) {
- add(CalendarEvent.didReceiveEvent(calendarEvent));
- }
+ return calendarEvent;
+ },
+ (r) {
+ Log.error(r);
+ return null;
},
- (r) => Log.error(r),
);
});
}
@@ -140,7 +181,7 @@ class CalendarBloc extends Bloc {
result.fold(
(events) {
if (!isClosed) {
- final calendarEvents = >[];
+ final calendarEvents = >[];
for (final eventPB in events.items) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
@@ -156,9 +197,9 @@ class CalendarBloc extends Bloc {
});
}
- CalendarEventData? _calendarEventDataFromEventPB(
+ CalendarEventData? _calendarEventDataFromEventPB(
CalendarEventPB eventPB) {
- final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
+ final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
if (fieldInfo != null) {
final cellId = CellIdentifier(
viewId: viewId,
@@ -166,7 +207,7 @@ class CalendarBloc extends Bloc {
fieldInfo: fieldInfo,
);
- final eventData = CalendarCardData(
+ final eventData = CalendarDayEvent(
event: eventPB,
cellId: cellId,
);
@@ -192,10 +233,31 @@ class CalendarBloc extends Bloc {
},
onFieldsChanged: (fieldInfos) {
if (isClosed) return;
- final fieldInfoByFieldId = {
+ fieldInfoByFieldId = {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
};
- add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId));
+ },
+ onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
+ onRowsCreated: ((ids) async {
+ for (final id in ids) {
+ final event = await _loadEvent(id);
+ if (event != null && !isClosed) {
+ add(CalendarEvent.didReceiveNewEvent(event));
+ }
+ }
+ }),
+ onRowsDeleted: (ids) {
+ if (isClosed) return;
+ add(CalendarEvent.didDeleteEvents(ids));
+ },
+ onRowsUpdated: (ids) async {
+ if (isClosed) return;
+ for (final id in ids) {
+ final event = await _loadEvent(id);
+ if (event != null) {
+ add(CalendarEvent.didUpdateEvent(event));
+ }
+ }
},
);
@@ -204,9 +266,13 @@ class CalendarBloc extends Bloc {
onLoadLayout: _didReceiveLayoutSetting,
);
+ final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
+ onCalendarLayoutChanged: _didReceiveNewLayoutField);
+
_databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
+ onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
);
}
@@ -216,44 +282,75 @@ class CalendarBloc extends Bloc {
add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
}
}
+
+ void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) {
+ if (layoutSetting.hasCalendar()) {
+ if (isClosed) return;
+ add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
+ }
+ }
}
-typedef Events = List>;
+typedef Events = List>;
@freezed
class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.initial() = _InitialCalendar;
+
+ // Called after loading the calendar layout setting from the backend
const factory CalendarEvent.didReceiveCalendarSettings(
CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
+
+ // Called after loading all the current evnets
const factory CalendarEvent.didLoadAllEvents(Events events) =
_ReceiveCalendarEvents;
- const factory CalendarEvent.didReceiveEvent(
- CalendarEventData event) = _ReceiveEvent;
- const factory CalendarEvent.didUpdateFieldInfos(
- Map fieldInfoByFieldId) = _DidUpdateFieldInfos;
+
+ // Called when specific event was updated
+ const factory CalendarEvent.didUpdateEvent(
+ CalendarEventData event) = _DidUpdateEvent;
+
+ // Called after creating a new event
+ const factory CalendarEvent.didReceiveNewEvent(
+ CalendarEventData event) = _DidReceiveNewEvent;
+
+ // Called when deleting events
+ const factory CalendarEvent.didDeleteEvents(List rowIds) =
+ _DidDeleteEvents;
+
+ // Called when creating a new event
const factory CalendarEvent.createEvent(DateTime date, String title) =
_CreateEvent;
+
+ // Called when updating the calendar's layout settings
+ const factory CalendarEvent.updateCalendarLayoutSetting(
+ CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting;
+
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
_ReceiveDatabaseUpdate;
+
+ const factory CalendarEvent.didReceiveNewLayoutField(
+ CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField;
}
@freezed
class CalendarState with _$CalendarState {
const factory CalendarState({
- required String databaseId,
required Option database,
- required Events events,
- required Map fieldInfoByFieldId,
+ required Events allEvents,
+ required Events initialEvents,
+ CalendarEventData? newEvent,
+ required List deleteEventIds,
+ CalendarEventData? updateEvent,
required Option settings,
required DatabaseLoadingState loadingState,
required Option noneOrError,
}) = _CalendarState;
- factory CalendarState.initial(String databaseId) => CalendarState(
+ factory CalendarState.initial() => CalendarState(
database: none(),
- databaseId: databaseId,
- fieldInfoByFieldId: {},
- events: [],
+ allEvents: [],
+ initialEvents: [],
+ deleteEventIds: [],
settings: none(),
noneOrError: none(),
loadingState: const _Loading(),
@@ -277,8 +374,10 @@ class CalendarEditingRow {
});
}
-class CalendarCardData {
+class CalendarDayEvent {
final CalendarEventPB event;
final CellIdentifier cellId;
- CalendarCardData({required this.cellId, required this.event});
+
+ String get eventId => cellId.rowId;
+ CalendarDayEvent({required this.cellId, required this.event});
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart
new file mode 100644
index 0000000000..1ebc97d90a
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart
@@ -0,0 +1,55 @@
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
+import 'package:bloc/bloc.dart';
+import 'package:dartz/dartz.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'calendar_setting_bloc.freezed.dart';
+
+typedef DayOfWeek = int;
+
+class CalendarSettingBloc
+ extends Bloc {
+ CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings})
+ : super(CalendarSettingState.initial(layoutSettings)) {
+ on((event, emit) {
+ event.when(
+ performAction: (action) {
+ emit(state.copyWith(selectedAction: Some(action)));
+ },
+ updateLayoutSetting: (setting) {
+ emit(state.copyWith(layoutSetting: Some(setting)));
+ },
+ );
+ });
+ }
+
+ @override
+ Future close() async => super.close();
+}
+
+@freezed
+class CalendarSettingState with _$CalendarSettingState {
+ const factory CalendarSettingState({
+ required Option selectedAction,
+ required Option layoutSetting,
+ }) = _CalendarSettingState;
+
+ factory CalendarSettingState.initial(
+ CalendarLayoutSettingsPB? layoutSettings) =>
+ CalendarSettingState(
+ selectedAction: none(),
+ layoutSetting: layoutSettings == null ? none() : Some(layoutSettings),
+ );
+}
+
+@freezed
+class CalendarSettingEvent with _$CalendarSettingEvent {
+ const factory CalendarSettingEvent.performAction(
+ CalendarSettingAction action) = _PerformAction;
+ const factory CalendarSettingEvent.updateLayoutSetting(
+ CalendarLayoutSettingsPB setting) = _UpdateLayoutSetting;
+}
+
+enum CalendarSettingAction {
+ layout,
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
index a7ce510565..1ddfb7a64f 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
@@ -34,7 +34,7 @@ class CalendarPluginBuilder extends PluginBuilder {
class CalendarPluginConfig implements PluginConfig {
@override
- bool get creatable => false;
+ bool get creatable => true;
}
class CalendarPlugin extends Plugin {
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
new file mode 100644
index 0000000000..3a71c2c4ff
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
@@ -0,0 +1,267 @@
+import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../../grid/presentation/layout/sizes.dart';
+import '../application/calendar_bloc.dart';
+
+class CalendarDayCard extends StatelessWidget {
+ final String viewId;
+ final bool isToday;
+ final bool isInMonth;
+ final DateTime date;
+ final RowCache _rowCache;
+ final CardCellBuilder _cellBuilder;
+ final List events;
+ final void Function(DateTime) onCreateEvent;
+
+ CalendarDayCard({
+ required this.viewId,
+ required this.isToday,
+ required this.isInMonth,
+ required this.date,
+ required this.onCreateEvent,
+ required RowCache rowCache,
+ required this.events,
+ Key? key,
+ }) : _rowCache = rowCache,
+ _cellBuilder = CardCellBuilder(rowCache.cellCache),
+ super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ Color backgroundColor = Theme.of(context).colorScheme.surface;
+ if (!isInMonth) {
+ backgroundColor = AFThemeExtension.of(context).lightGreyHover;
+ }
+
+ return ChangeNotifierProvider(
+ create: (_) => _CardEnterNotifier(),
+ builder: ((context, child) {
+ final children = events.map((event) {
+ return _DayEventCell(
+ event: event,
+ viewId: viewId,
+ onClick: () => _showRowDetailPage(event, context),
+ child: _cellBuilder.buildCell(
+ cellId: event.cellId,
+ styles: {FieldType.RichText: TextCardCellStyle(10)},
+ ),
+ );
+ }).toList();
+
+ final child = Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _Header(
+ date: date,
+ isInMonth: isInMonth,
+ isToday: isToday,
+ onCreate: () => onCreateEvent(date),
+ ),
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ Flexible(
+ child: ListView.separated(
+ itemBuilder: (BuildContext context, int index) {
+ return children[index];
+ },
+ itemCount: children.length,
+ separatorBuilder: (BuildContext context, int index) =>
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ ),
+ ),
+ ],
+ ));
+
+ return Container(
+ color: backgroundColor,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.click,
+ onEnter: (p) => notifyEnter(context, true),
+ onExit: (p) => notifyEnter(context, false),
+ child: child,
+ ),
+ );
+ }),
+ );
+ }
+
+ void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
+ final dataController = RowController(
+ rowId: event.cellId.rowId,
+ viewId: viewId,
+ rowCache: _rowCache,
+ );
+
+ FlowyOverlay.show(
+ context: context,
+ builder: (BuildContext context) {
+ return RowDetailPage(
+ cellBuilder: GridCellBuilder(
+ cellCache: _rowCache.cellCache,
+ ),
+ dataController: dataController,
+ );
+ },
+ );
+ }
+
+ notifyEnter(BuildContext context, bool isEnter) {
+ Provider.of<_CardEnterNotifier>(
+ context,
+ listen: false,
+ ).onEnter = isEnter;
+ }
+}
+
+class _DayEventCell extends StatelessWidget {
+ final String viewId;
+ final CalendarDayEvent event;
+ final VoidCallback onClick;
+ final Widget child;
+ const _DayEventCell({
+ required this.viewId,
+ required this.event,
+ required this.onClick,
+ required this.child,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return FlowyHover(
+ child: GestureDetector(
+ onTap: onClick,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: child,
+ ),
+ ),
+ );
+ }
+}
+
+class _Header extends StatelessWidget {
+ final bool isToday;
+ final bool isInMonth;
+ final DateTime date;
+ final VoidCallback onCreate;
+ const _Header({
+ required this.isToday,
+ required this.isInMonth,
+ required this.date,
+ required this.onCreate,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer<_CardEnterNotifier>(
+ builder: (context, notifier, _) {
+ final badge = _DayBadge(
+ isToday: isToday,
+ isInMonth: isInMonth,
+ date: date,
+ );
+ return Row(
+ children: [
+ if (notifier.onEnter) _NewEventButton(onClick: onCreate),
+ const Spacer(),
+ badge,
+ ],
+ );
+ },
+ );
+ }
+}
+
+class _NewEventButton extends StatelessWidget {
+ final VoidCallback onClick;
+ const _NewEventButton({
+ required this.onClick,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return FlowyIconButton(
+ onPressed: onClick,
+ iconPadding: EdgeInsets.zero,
+ icon: svgWidget(
+ "home/add",
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ width: 22,
+ );
+ }
+}
+
+class _DayBadge extends StatelessWidget {
+ final bool isToday;
+ final bool isInMonth;
+ final DateTime date;
+ const _DayBadge({
+ required this.isToday,
+ required this.isInMonth,
+ required this.date,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ Color dayTextColor = Theme.of(context).colorScheme.onSurface;
+ String dayString = date.day == 1
+ ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
+ : date.day.toString();
+
+ if (isToday) {
+ dayTextColor = Theme.of(context).colorScheme.onPrimary;
+ }
+ if (!isInMonth) {
+ dayTextColor = Theme.of(context).disabledColor;
+ }
+
+ Widget day = Container(
+ decoration: BoxDecoration(
+ color: isToday ? Theme.of(context).colorScheme.primary : null,
+ borderRadius: Corners.s6Border,
+ ),
+ padding: GridSize.typeOptionContentInsets,
+ child: FlowyText.medium(
+ dayString,
+ color: dayTextColor,
+ ),
+ );
+
+ return day;
+ }
+}
+
+class _CardEnterNotifier extends ChangeNotifier {
+ bool _onEnter = false;
+
+ _CardEnterNotifier();
+
+ set onEnter(bool value) {
+ if (_onEnter != value) {
+ _onEnter = value;
+ notifyListeners();
+ }
+ }
+
+ bool get onEnter => _onEnter;
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
index 0eac7870a3..988654a8ec 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
@@ -1,22 +1,15 @@
import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
-import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:provider/provider.dart';
-import '../../grid/presentation/layout/sizes.dart';
-import '../../widgets/row/cell_builder.dart';
-import '../../widgets/row/row_detail.dart';
+import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart';
@@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget {
}
class _CalendarPageState extends State {
- final _eventController = EventController();
+ final _eventController = EventController();
GlobalKey? _calendarState;
late CalendarBloc _calendarBloc;
@@ -58,21 +51,55 @@ class _CalendarPageState extends State {
value: _calendarBloc,
)
],
- child: BlocListener(
- listenWhen: (previous, current) => previous.events != current.events,
- listener: (context, state) {
- if (state.events.isNotEmpty) {
- _eventController.removeWhere((element) => true);
- _eventController.addAll(state.events);
- }
- },
+ child: MultiBlocListener(
+ listeners: [
+ BlocListener(
+ listenWhen: (p, c) => p.initialEvents != c.initialEvents,
+ listener: (context, state) {
+ _eventController.removeWhere((_) => true);
+ _eventController.addAll(state.initialEvents);
+ },
+ ),
+ BlocListener(
+ listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds,
+ listener: (context, state) {
+ _eventController.removeWhere(
+ (element) =>
+ state.deleteEventIds.contains(element.event!.eventId),
+ );
+ },
+ ),
+ BlocListener(
+ listenWhen: (p, c) => p.updateEvent != c.updateEvent,
+ listener: (context, state) {
+ if (state.updateEvent != null) {
+ _eventController.removeWhere((element) =>
+ state.updateEvent!.event!.eventId ==
+ element.event!.eventId);
+ _eventController.add(state.updateEvent!);
+ }
+ },
+ ),
+ BlocListener(
+ listenWhen: (p, c) => p.newEvent != c.newEvent,
+ listener: (context, state) {
+ if (state.newEvent != null) {
+ _eventController.add(state.newEvent!);
+ }
+ },
+ ),
+ ],
child: BlocBuilder(
builder: (context, state) {
return Column(
children: [
// const _ToolbarBlocAdaptor(),
- _toolbar(),
- _buildCalendar(_eventController),
+ const CalendarToolbar(),
+ _buildCalendar(
+ _eventController,
+ state.settings
+ .foldLeft(0, (previous, a) => a.firstDayOfWeek),
+ ),
],
);
},
@@ -82,16 +109,13 @@ class _CalendarPageState extends State {
);
}
- Widget _toolbar() {
- return const CalendarToolbar();
- }
-
- Widget _buildCalendar(EventController eventController) {
+ Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
return Expanded(
child: MonthView(
key: _calendarState,
controller: _eventController,
- cellAspectRatio: 1.75,
+ cellAspectRatio: .9,
+ startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
@@ -154,47 +178,19 @@ class _CalendarPageState extends State {
Widget _calendarDayBuilder(
DateTime date,
- List> calenderEvents,
+ List> calenderEvents,
isToday,
isInMonth,
) {
- final builder = CardCellBuilder(_calendarBloc.cellCache);
- final cells = calenderEvents.map((value) => value.event!).map((event) {
- final child = builder.buildCell(cellId: event.cellId);
+ final events = calenderEvents.map((value) => value.event!).toList();
- return FlowyHover(
- child: GestureDetector(
- onTap: () {
- final dataController = RowController(
- rowId: event.cellId.rowId,
- viewId: widget.view.id,
- rowCache: _calendarBloc.rowCache,
- );
-
- FlowyOverlay.show(
- context: context,
- builder: (BuildContext context) {
- return RowDetailPage(
- cellBuilder:
- GridCellBuilder(cellCache: _calendarBloc.cellCache),
- dataController: dataController,
- );
- },
- );
- },
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: child,
- ),
- ),
- );
- }).toList();
-
- return _CalendarCard(
+ return CalendarDayCard(
+ viewId: widget.view.id,
isToday: isToday,
isInMonth: isInMonth,
+ events: events,
date: date,
- children: cells,
+ rowCache: _calendarBloc.rowCache,
onCreateEvent: (date) {
_calendarBloc.add(
CalendarEvent.createEvent(
@@ -205,175 +201,9 @@ class _CalendarPageState extends State {
},
);
}
-}
-class _CalendarCard extends StatelessWidget {
- final bool isToday;
- final bool isInMonth;
- final DateTime date;
- final List children;
- final void Function(DateTime) onCreateEvent;
-
- const _CalendarCard({
- required this.isToday,
- required this.isInMonth,
- required this.date,
- required this.children,
- required this.onCreateEvent,
- Key? key,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- Color backgroundColor = Theme.of(context).colorScheme.surface;
- if (!isInMonth) {
- backgroundColor = AFThemeExtension.of(context).lightGreyHover;
- }
-
- return ChangeNotifierProvider(
- create: (_) => _CardEnterNotifier(),
- builder: ((context, child) {
- return Container(
- color: backgroundColor,
- child: MouseRegion(
- cursor: SystemMouseCursors.click,
- onEnter: (p) => notifyEnter(context, true),
- onExit: (p) => notifyEnter(context, false),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- children: [
- _Header(
- date: date,
- isInMonth: isInMonth,
- isToday: isToday,
- onCreate: () => onCreateEvent(date),
- ),
- ...children
- ],
- ),
- ),
- ),
- );
- }),
- );
- }
-
- notifyEnter(BuildContext context, bool isEnter) {
- Provider.of<_CardEnterNotifier>(
- context,
- listen: false,
- ).onEnter = isEnter;
+ WeekDays _weekdayFromInt(int dayOfWeek) {
+ // MonthView places the first day of week on the second column for some reason.
+ return WeekDays.values[(dayOfWeek + 1) % 7];
}
}
-
-class _Header extends StatelessWidget {
- final bool isToday;
- final bool isInMonth;
- final DateTime date;
- final VoidCallback onCreate;
- const _Header({
- required this.isToday,
- required this.isInMonth,
- required this.date,
- required this.onCreate,
- Key? key,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return Consumer<_CardEnterNotifier>(
- builder: (context, notifier, _) {
- final badge = _DayBadge(
- isToday: isToday,
- isInMonth: isInMonth,
- date: date,
- );
- return Row(
- children: [
- if (notifier.onEnter) _NewEventButton(onClick: onCreate),
- const Spacer(),
- badge,
- ],
- );
- },
- );
- }
-}
-
-class _NewEventButton extends StatelessWidget {
- final VoidCallback onClick;
- const _NewEventButton({
- required this.onClick,
- Key? key,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return FlowyIconButton(
- onPressed: onClick,
- iconPadding: EdgeInsets.zero,
- icon: svgWidget(
- "home/add",
- color: Theme.of(context).colorScheme.onSurface,
- ),
- width: 22,
- );
- }
-}
-
-class _DayBadge extends StatelessWidget {
- final bool isToday;
- final bool isInMonth;
- final DateTime date;
- const _DayBadge({
- required this.isToday,
- required this.isInMonth,
- required this.date,
- Key? key,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- Color dayTextColor = Theme.of(context).colorScheme.onSurface;
- String dayString = date.day == 1
- ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
- : date.day.toString();
-
- if (isToday) {
- dayTextColor = Theme.of(context).colorScheme.onPrimary;
- }
- if (!isInMonth) {
- dayTextColor = Theme.of(context).disabledColor;
- }
-
- Widget day = Container(
- decoration: BoxDecoration(
- color: isToday ? Theme.of(context).colorScheme.primary : null,
- borderRadius: Corners.s6Border,
- ),
- padding: GridSize.typeOptionContentInsets,
- child: FlowyText.medium(
- dayString,
- color: dayTextColor,
- ),
- );
-
- return day;
- }
-}
-
-class _CardEnterNotifier extends ChangeNotifier {
- bool _onEnter = false;
-
- _CardEnterNotifier();
-
- set onEnter(bool value) {
- if (_onEnter != value) {
- _onEnter = value;
- notifyListeners();
- }
- }
-
- bool get onEnter => _onEnter;
-}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart
new file mode 100644
index 0000000000..ff69888813
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart
@@ -0,0 +1,410 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'
+ hide DateFormat;
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:protobuf/protobuf.dart';
+
+import 'calendar_setting.dart';
+
+class CalendarLayoutSetting extends StatefulWidget {
+ final CalendarSettingContext settingContext;
+ final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
+
+ const CalendarLayoutSetting({
+ required this.onUpdated,
+ required this.settingContext,
+ super.key,
+ });
+
+ @override
+ State createState() => _CalendarLayoutSettingState();
+}
+
+class _CalendarLayoutSettingState extends State {
+ late final PopoverMutex popoverMutex;
+
+ @override
+ void initState() {
+ popoverMutex = PopoverMutex();
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ final CalendarLayoutSettingsPB? settings = state.layoutSetting
+ .foldLeft(null, (previous, settings) => settings);
+
+ if (settings == null) {
+ return const CircularProgressIndicator();
+ }
+ final availableSettings = _availableCalendarSettings(settings);
+
+ final items = availableSettings.map((setting) {
+ switch (setting) {
+ case CalendarLayoutSettingAction.showWeekNumber:
+ return ShowWeekNumber(
+ showWeekNumbers: settings.showWeekNumbers,
+ onUpdated: (showWeekNumbers) {
+ _updateLayoutSettings(
+ context,
+ showWeekNumbers: showWeekNumbers,
+ onUpdated: widget.onUpdated,
+ );
+ },
+ );
+ case CalendarLayoutSettingAction.showWeekends:
+ return ShowWeekends(
+ showWeekends: settings.showWeekends,
+ onUpdated: (showWeekends) {
+ _updateLayoutSettings(
+ context,
+ showWeekends: showWeekends,
+ onUpdated: widget.onUpdated,
+ );
+ },
+ );
+ case CalendarLayoutSettingAction.firstDayOfWeek:
+ return FirstDayOfWeek(
+ firstDayOfWeek: settings.firstDayOfWeek,
+ popoverMutex: popoverMutex,
+ onUpdated: (firstDayOfWeek) {
+ _updateLayoutSettings(
+ context,
+ onUpdated: widget.onUpdated,
+ firstDayOfWeek: firstDayOfWeek,
+ );
+ },
+ );
+ case CalendarLayoutSettingAction.layoutField:
+ return LayoutDateField(
+ fieldController: widget.settingContext.fieldController,
+ viewId: widget.settingContext.viewId,
+ fieldId: settings.layoutFieldId,
+ popoverMutex: popoverMutex,
+ onUpdated: (fieldId) {
+ _updateLayoutSettings(context,
+ onUpdated: widget.onUpdated, layoutFieldId: fieldId);
+ },
+ );
+ default:
+ return ShowWeekends(
+ showWeekends: settings.showWeekends,
+ onUpdated: (showWeekends) {
+ _updateLayoutSettings(context,
+ onUpdated: widget.onUpdated, showWeekends: showWeekends);
+ },
+ );
+ }
+ }).toList();
+
+ return SizedBox(
+ width: 200,
+ child: ListView.separated(
+ shrinkWrap: true,
+ controller: ScrollController(),
+ itemCount: items.length,
+ separatorBuilder: (context, index) =>
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ physics: StyledScrollPhysics(),
+ itemBuilder: (BuildContext context, int index) => items[index],
+ padding: const EdgeInsets.all(6.0),
+ ),
+ );
+ },
+ );
+ }
+
+ List _availableCalendarSettings(
+ CalendarLayoutSettingsPB layoutSettings) {
+ List settings = [
+ CalendarLayoutSettingAction.layoutField,
+ // CalendarLayoutSettingAction.layoutType,
+ // CalendarLayoutSettingAction.showWeekNumber,
+ ];
+
+ switch (layoutSettings.layoutTy) {
+ case CalendarLayoutPB.DayLayout:
+ // settings.add(CalendarLayoutSettingAction.showTimeLine);
+ break;
+ case CalendarLayoutPB.MonthLayout:
+ settings.addAll([
+ // CalendarLayoutSettingAction.showWeekends,
+ // if (layoutSettings.showWeekends)
+ CalendarLayoutSettingAction.firstDayOfWeek,
+ ]);
+ break;
+ case CalendarLayoutPB.WeekLayout:
+ settings.addAll([
+ // CalendarLayoutSettingAction.showWeekends,
+ // if (layoutSettings.showWeekends)
+ CalendarLayoutSettingAction.firstDayOfWeek,
+ // CalendarLayoutSettingAction.showTimeLine,
+ ]);
+ break;
+ }
+
+ return settings;
+ }
+
+ void _updateLayoutSettings(
+ BuildContext context, {
+ required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated,
+ bool? showWeekends,
+ bool? showWeekNumbers,
+ int? firstDayOfWeek,
+ String? layoutFieldId,
+ }) {
+ CalendarLayoutSettingsPB setting = context
+ .read()
+ .state
+ .layoutSetting
+ .foldLeft(null, (previous, settings) => settings)!;
+ setting.freeze();
+ setting = setting.rebuild((setting) {
+ if (showWeekends != null) {
+ setting.showWeekends = !showWeekends;
+ }
+ if (showWeekNumbers != null) {
+ setting.showWeekNumbers = !showWeekNumbers;
+ }
+ if (firstDayOfWeek != null) {
+ setting.firstDayOfWeek = firstDayOfWeek;
+ }
+ if (layoutFieldId != null) {
+ setting.layoutFieldId = layoutFieldId;
+ }
+ });
+ context
+ .read()
+ .add(CalendarSettingEvent.updateLayoutSetting(setting));
+ onUpdated(setting);
+ }
+}
+
+class LayoutDateField extends StatelessWidget {
+ final String fieldId;
+ final String viewId;
+ final FieldController fieldController;
+ final PopoverMutex popoverMutex;
+ final Function(String fieldId) onUpdated;
+
+ const LayoutDateField({
+ required this.fieldId,
+ required this.fieldController,
+ required this.viewId,
+ required this.popoverMutex,
+ required this.onUpdated,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return AppFlowyPopover(
+ direction: PopoverDirection.leftWithTopAligned,
+ constraints: BoxConstraints.loose(const Size(300, 400)),
+ mutex: popoverMutex,
+ popupBuilder: (context) {
+ return BlocProvider(
+ create: (context) => getIt(
+ param1: viewId, param2: fieldController)
+ ..add(const DatabasePropertyEvent.initial()),
+ child: BlocBuilder(
+ builder: (context, state) {
+ final items = state.fieldContexts
+ .where((field) => field.fieldType == FieldType.DateTime)
+ .map(
+ (fieldInfo) {
+ return SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: FlowyButton(
+ text: FlowyText.medium(fieldInfo.name),
+ onTap: () {
+ onUpdated(fieldInfo.id);
+ popoverMutex.close();
+ },
+ leftIcon: svgWidget('grid/field/date'),
+ rightIcon: fieldInfo.id == fieldId
+ ? svgWidget('grid/checkmark')
+ : null,
+ ),
+ );
+ },
+ ).toList();
+
+ return SizedBox(
+ width: 200,
+ child: ListView.separated(
+ shrinkWrap: true,
+ itemBuilder: (context, index) => items[index],
+ separatorBuilder: (context, index) =>
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ itemCount: items.length,
+ ),
+ );
+ },
+ ),
+ );
+ },
+ child: SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: FlowyButton(
+ margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+ text: FlowyText.medium(
+ LocaleKeys.calendar_settings_layoutDateField.tr()),
+ ),
+ ),
+ );
+ }
+}
+
+class ShowWeekNumber extends StatelessWidget {
+ final bool showWeekNumbers;
+ final Function(bool showWeekNumbers) onUpdated;
+
+ const ShowWeekNumber({
+ required this.showWeekNumbers,
+ required this.onUpdated,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return _toggleItem(
+ onToggle: (showWeekNumbers) {
+ onUpdated(!showWeekNumbers);
+ },
+ value: showWeekNumbers,
+ text: LocaleKeys.calendar_settings_showWeekNumbers.tr(),
+ );
+ }
+}
+
+class ShowWeekends extends StatelessWidget {
+ final bool showWeekends;
+ final Function(bool showWeekends) onUpdated;
+ const ShowWeekends({
+ super.key,
+ required this.showWeekends,
+ required this.onUpdated,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return _toggleItem(
+ onToggle: (showWeekends) {
+ onUpdated(!showWeekends);
+ },
+ value: showWeekends,
+ text: LocaleKeys.calendar_settings_showWeekends.tr(),
+ );
+ }
+}
+
+class FirstDayOfWeek extends StatelessWidget {
+ final int firstDayOfWeek;
+ final PopoverMutex popoverMutex;
+ final Function(int firstDayOfWeek) onUpdated;
+ const FirstDayOfWeek({
+ super.key,
+ required this.firstDayOfWeek,
+ required this.onUpdated,
+ required this.popoverMutex,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return AppFlowyPopover(
+ direction: PopoverDirection.leftWithTopAligned,
+ constraints: BoxConstraints.loose(const Size(300, 400)),
+ mutex: popoverMutex,
+ popupBuilder: (context) {
+ final symbols =
+ DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
+ // starts from sunday
+ final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
+ final index = (entry.key - 1) % 7;
+ final string = entry.value;
+ return SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: FlowyButton(
+ text: FlowyText.medium(string),
+ onTap: () {
+ onUpdated(index);
+ popoverMutex.close();
+ },
+ rightIcon:
+ firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
+ ),
+ );
+ }).toList();
+
+ return SizedBox(
+ width: 100,
+ child: ListView.separated(
+ shrinkWrap: true,
+ itemBuilder: (context, index) => items[index],
+ separatorBuilder: (context, index) =>
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ itemCount: 2,
+ ),
+ );
+ },
+ child: SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: FlowyButton(
+ margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+ text: FlowyText.medium(
+ LocaleKeys.calendar_settings_firstDayOfWeek.tr()),
+ ),
+ ),
+ );
+ }
+}
+
+Widget _toggleItem({
+ required String text,
+ required bool value,
+ required void Function(bool) onToggle,
+}) {
+ return SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+ child: Row(
+ children: [
+ FlowyText.medium(text),
+ const Spacer(),
+ Toggle(
+ value: value,
+ onChanged: (value) => onToggle(!value),
+ style: ToggleStyle.big,
+ padding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+enum CalendarLayoutSettingAction {
+ layoutField,
+ layoutType,
+ showWeekends,
+ firstDayOfWeek,
+ showWeekNumber,
+ showTimeLine,
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart
new file mode 100644
index 0000000000..d9777ebba8
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart
@@ -0,0 +1,112 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+import 'calendar_layout_setting.dart';
+
+/// The highest-level widget shown in the popover triggered by clicking the
+/// "Settings" button. By default, shows [AllCalendarSettings] but upon
+/// selecting a category, replaces contents with contents of the submenu.
+class CalendarSetting extends StatelessWidget {
+ final CalendarSettingContext settingContext;
+ final CalendarLayoutSettingsPB? layoutSettings;
+ final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
+
+ const CalendarSetting({
+ required this.onUpdated,
+ required this.layoutSettings,
+ required this.settingContext,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings),
+ child: BlocBuilder(
+ builder: (context, state) {
+ final CalendarSettingAction? action =
+ state.selectedAction.foldLeft(null, (previous, action) => action);
+ switch (action) {
+ case CalendarSettingAction.layout:
+ return CalendarLayoutSetting(
+ onUpdated: onUpdated,
+ settingContext: settingContext,
+ );
+ default:
+ return const AllCalendarSettings().padding(all: 6.0);
+ }
+ },
+ ),
+ );
+ }
+}
+
+/// Shows all of the available categories of settings that can be set here.
+/// For now, this only includes the Layout category.
+class AllCalendarSettings extends StatelessWidget {
+ const AllCalendarSettings({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final items = CalendarSettingAction.values
+ .map((e) => _settingItem(context, e))
+ .toList();
+
+ return SizedBox(
+ width: 140,
+ child: ListView.separated(
+ shrinkWrap: true,
+ controller: ScrollController(),
+ itemCount: items.length,
+ separatorBuilder: (context, index) =>
+ VSpace(GridSize.typeOptionSeparatorHeight),
+ physics: StyledScrollPhysics(),
+ itemBuilder: (BuildContext context, int index) => items[index],
+ ),
+ );
+ }
+
+ Widget _settingItem(BuildContext context, CalendarSettingAction action) {
+ return SizedBox(
+ height: GridSize.popoverItemHeight,
+ child: FlowyButton(
+ text: FlowyText.medium(action.title()),
+ onTap: () {
+ context
+ .read()
+ .add(CalendarSettingEvent.performAction(action));
+ },
+ ),
+ );
+ }
+}
+
+extension _SettingExtension on CalendarSettingAction {
+ String title() {
+ switch (this) {
+ case CalendarSettingAction.layout:
+ return LocaleKeys.grid_settings_layout.tr();
+ }
+ }
+}
+
+class CalendarSettingContext {
+ final String viewId;
+ final FieldController fieldController;
+
+ CalendarSettingContext({
+ required this.viewId,
+ required this.fieldController,
+ });
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
index 4b1399763b..1f704b5e83 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
@@ -1,5 +1,14 @@
-import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../application/calendar_bloc.dart';
+import 'calendar_setting.dart';
class CalendarToolbar extends StatelessWidget {
const CalendarToolbar({super.key});
@@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget {
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
- children: const [
- FlowyTextButton(
- "Settings",
- fillColor: Colors.transparent,
- padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- ),
+ children: [
+ _SettingButton(),
],
),
);
}
}
+
+class _SettingButton extends StatefulWidget {
+ @override
+ State createState() => _SettingButtonState();
+}
+
+class _SettingButtonState extends State<_SettingButton> {
+ late PopoverController popoverController;
+
+ @override
+ void initState() {
+ popoverController = PopoverController();
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AppFlowyPopover(
+ controller: popoverController,
+ direction: PopoverDirection.bottomWithRightAligned,
+ triggerActions: PopoverTriggerFlags.none,
+ constraints: BoxConstraints.loose(const Size(300, 400)),
+ margin: EdgeInsets.zero,
+ child: FlowyTextButton(
+ LocaleKeys.settings_title.tr(),
+ fillColor: Colors.transparent,
+ hoverColor: AFThemeExtension.of(context).lightGreyHover,
+ padding: GridSize.typeOptionContentInsets,
+ onPressed: () => popoverController.show(),
+ ),
+ popupBuilder: (BuildContext popoverContext) {
+ final bloc = context.watch();
+ final settingContext = CalendarSettingContext(
+ viewId: bloc.viewId,
+ fieldController: bloc.fieldController,
+ );
+ return CalendarSetting(
+ settingContext: settingContext,
+ layoutSettings: bloc.state.settings.fold(
+ () => null,
+ (settings) => settings,
+ ),
+ onUpdated: (layoutSettings) {
+ if (layoutSettings == null) {
+ return;
+ }
+ context
+ .read()
+ .add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings));
+ },
+ );
+ }, // use blocbuilder
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
index c0cb7eb245..7d73ab9b4a 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
@@ -72,7 +72,7 @@ class GridBloc extends Bloc {
add(GridEvent.didReceiveGridUpdate(database));
}
},
- onRowsChanged: (rowInfos, reason) {
+ onRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
index 4bc655a305..ddf2dee63a 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
@@ -23,6 +23,7 @@ class CardCellBuilder {
required CellIdentifier cellId,
EditableCardNotifier? cellNotifier,
CardConfiguration? cardConfiguration,
+ Map? styles,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellId: cellId,
@@ -30,6 +31,7 @@ class CardCellBuilder {
);
final key = cellId.key();
+ final style = styles?[cellId.fieldType];
switch (cellId.fieldType) {
case FieldType.Checkbox:
return CheckboxCardCell(
@@ -70,6 +72,7 @@ class CardCellBuilder {
return TextCardCell(
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
+ style: isStyleOrNull(style),
key: key,
);
case FieldType.URL:
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
index 6bc9ee9eac..e5942c8ab3 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
@@ -24,10 +24,21 @@ class CardConfiguration {
}
}
-abstract class CardCell extends StatefulWidget {
- final T? cardData;
+abstract class CardCellStyle {}
- const CardCell({super.key, this.cardData});
+S? isStyleOrNull(CardCellStyle? style) {
+ if (style is S) {
+ return style as S;
+ } else {
+ return null;
+ }
+}
+
+abstract class CardCell extends StatefulWidget {
+ final T? cardData;
+ final S? style;
+
+ const CardCell({super.key, this.cardData, this.style});
}
class EditableCardNotifier {
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
index 6eba4acef9..2251af0cf1 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
@@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/select_option_card_cell_bloc.dart';
import 'card_cell.dart';
-class SelectOptionCardCell extends CardCell with EditableCell {
+class SelectOptionCardCellStyle extends CardCellStyle {}
+
+class SelectOptionCardCell extends CardCell
+ with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook, T>? renderHook;
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
index 8ffc834247..4eb9d9137e 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
@@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
-class TextCardCell extends CardCell with EditableCell {
+class TextCardCellStyle extends CardCellStyle {
+ final double fontSize;
+
+ TextCardCellStyle(this.fontSize);
+}
+
+class TextCardCell extends CardCell
+ with EditableCell {
@override
final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder;
@@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell {
const TextCardCell({
required this.cellControllerBuilder,
this.editableNotifier,
+ TextCardCellStyle? style,
Key? key,
- }) : super(key: key);
+ }) : super(key: key, style: style);
@override
State createState() => _TextCardCellState();
@@ -129,6 +136,14 @@ class _TextCardCellState extends State {
super.dispose();
}
+ double _fontSize() {
+ if (widget.style != null) {
+ return widget.style!.fontSize;
+ } else {
+ return 14;
+ }
+ }
+
Widget _buildText(TextCardCellState state) {
return Padding(
padding: EdgeInsets.symmetric(
@@ -136,7 +151,7 @@ class _TextCardCellState extends State {
),
child: FlowyText.medium(
state.content,
- fontSize: 14,
+ fontSize: _fontSize(),
maxLines: null, // Enable multiple lines
),
);
@@ -150,7 +165,7 @@ class _TextCardCellState extends State {
onChanged: (value) => focusChanged(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
- style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
+ style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()),
decoration: InputDecoration(
// Magic number 4 makes the textField take up the same space as FlowyText
contentPadding: EdgeInsets.symmetric(
diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
index 88638912db..668338bfb5 100644
--- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
+++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
@@ -73,7 +73,7 @@ class BoardTestContext {
BoardTestContext(this.gridView, this._boardDataController);
List get rowInfos {
- return _boardDataController.rowInfos;
+ return _boardDataController.rowCache.rowInfos;
}
List get fieldContexts => fieldController.fieldInfos;
diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
index c68dffb976..af85cc8bf2 100644
--- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
+++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
@@ -26,7 +26,7 @@ class GridTestContext {
GridTestContext(this.gridView, this.gridController);
List get rowInfos {
- return gridController.rowInfos;
+ return gridController.rowCache.rowInfos;
}
List get fieldContexts => fieldController.fieldInfos;
diff --git a/frontend/rust-lib/flowy-database/src/notification.rs b/frontend/rust-lib/flowy-database/src/notification.rs
index c0dd347411..0a693d4ebe 100644
--- a/frontend/rust-lib/flowy-database/src/notification.rs
+++ b/frontend/rust-lib/flowy-database/src/notification.rs
@@ -35,8 +35,6 @@ pub enum DatabaseNotification {
DidUpdateLayoutSettings = 80,
// Trigger when the layout field of the database is changed
DidSetNewLayoutField = 81,
-
- DidArrangeCalendarWithNewField = 82,
}
impl std::default::Default for DatabaseNotification {
From 47984676212af39303b5b303451e01aa82c000ad Mon Sep 17 00:00:00 2001
From: Sara Tavares <29093946+stavares843@users.noreply.github.com>
Date: Mon, 20 Mar 2023 13:29:17 +0000
Subject: [PATCH 05/31] chore(typos): fix typos (#1961)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
---
.../integration_test/switch_folder_test.dart | 4 ++--
.../integration_test/util/settings.dart | 20 +++++++++----------
.../application/field/field_controller.dart | 6 +++---
.../setting/setting_controller.dart | 4 ++--
.../widgets/filter/choicechip/checkbox.dart | 4 ++--
.../select_option/select_option.dart | 4 ++--
.../widgets/filter/choicechip/text.dart | 4 ++--
.../lib/plugins/document/document_page.dart | 8 ++++----
.../lib/plugins/document/editor_styles.dart | 4 ++--
.../more/cubit/document_appearance_cubit.dart | 6 +++---
.../plugins/base/link_to_page_widget.dart | 6 +++---
.../plugins/cover/cover_node_widget.dart | 2 +-
.../plugins/openai/widgets/loading.dart | 2 +-
.../appflowy_flutter/lib/startup/startup.dart | 2 +-
.../workspace/application/app/app_bloc.dart | 2 +-
.../home/menu/app/section/section.dart | 4 ++--
.../documentation/customizing.md | 2 +-
.../example/lib/home_page.dart | 8 ++++----
.../example/lib/plugin/editor_theme.dart | 2 +-
.../appflowy_editor/lib/src/editor_state.dart | 4 ++--
.../selection_menu_service.dart | 2 +-
.../lib/src/render/style/plugin_styles.dart | 4 ++--
.../src/render/toolbar/toolbar_widget.dart | 6 +++---
.../lib/src/service/editor_service.dart | 2 +-
.../markdown_syntax_to_styled_text.dart | 6 +++---
.../built_in_shortcut_events.dart | 4 ++--
.../lib/src/service/toolbar_service.dart | 2 +-
.../test/command/command_extension_test.dart | 2 +-
.../test/extensions/node_extension_test.dart | 2 +-
.../test/infra/test_editor.dart | 2 +-
.../render/rich_text/checkbox_text_test.dart | 2 +-
.../exit_editing_mode_handler_test.dart | 2 +-
.../lib/src/emoji_picker/emoji_menu_item.dart | 6 +++---
.../type_option/type_option_controller.ts | 10 +++++-----
.../flowy-document/src/editor/READ_ME.json | 2 +-
.../io.appflowy.AppFlowy.metainfo.xml | 2 +-
frontend/scripts/makefile/desktop.toml | 4 ++--
.../lib-ot/src/text_delta/attributes.rs | 2 +-
38 files changed, 80 insertions(+), 80 deletions(-)
diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
index ad372e32a8..6dcd41b081 100644
--- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
+++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
@@ -86,7 +86,7 @@ void main() {
await tester.tapGoButton();
await tester.expectToSeeWelcomePage();
- // swith to user B
+ // switch to user B
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
@@ -120,7 +120,7 @@ void main() {
expect(find.textContaining(userA), findsOneWidget);
}
- // swith to the userB again
+ // switch to the userB again
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.files);
diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/util/settings.dart
index 76da12966c..4b01c2d791 100644
--- a/frontend/appflowy_flutter/integration_test/util/settings.dart
+++ b/frontend/appflowy_flutter/integration_test/util/settings.dart
@@ -39,7 +39,7 @@ extension AppFlowySettings on WidgetTester {
return;
}
- /// Open the page taht insides the settings page
+ /// Open the page that insides the settings page
Future openSettingsPage(SettingsPage page) async {
final button = find.text(page.name, findRichText: true);
expect(button, findsOneWidget);
@@ -49,25 +49,25 @@ extension AppFlowySettings on WidgetTester {
/// Restore the AppFlowy data storage location
Future restoreLocation() async {
- final buton =
+ final button =
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
- expect(buton, findsOneWidget);
- await tapButton(buton);
+ expect(button, findsOneWidget);
+ await tapButton(button);
return;
}
Future tapOpenFolderButton() async {
- final buton = find.text(LocaleKeys.settings_files_open.tr());
- expect(buton, findsOneWidget);
- await tapButton(buton);
+ final button = find.text(LocaleKeys.settings_files_open.tr());
+ expect(button, findsOneWidget);
+ await tapButton(button);
return;
}
Future tapCustomLocationButton() async {
- final buton =
+ final button =
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
- expect(buton, findsOneWidget);
- await tapButton(buton);
+ expect(button, findsOneWidget);
+ await tapButton(button);
return;
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart
index 6904835b24..231582621e 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart
@@ -162,7 +162,7 @@ class FieldController {
//Listen on setting changes
_listenOnSettingChanges();
- //Listen on the fitler changes
+ //Listen on the filter changes
_listenOnFilterChanges();
//Listen on the sort changes
@@ -177,7 +177,7 @@ class FieldController {
}
void _listenOnFilterChanges() {
- //Listen on the fitler changes
+ //Listen on the filter changes
deleteFilterFromChangeset(
List filters,
@@ -230,7 +230,7 @@ class FieldController {
.removeWhere((key, value) => value.id == updatedFilter.filterId);
}
- // Insert the filter if there is a fitler and its field info is
+ // Insert the filter if there is a filter and its field info is
// not null
if (updatedFilter.hasFilter()) {
final fieldInfo = _findFieldInfo(
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart
index cb63463149..a2b687332c 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart
@@ -27,7 +27,7 @@ class SettingController {
);
});
- // Listen on the seting changes
+ // Listen on the setting changes
_listener.start(onSettingUpdated: (result) {
result.fold(
(newSetting) => updateSetting(newSetting),
@@ -36,7 +36,7 @@ class SettingController {
});
}
- void startListeing({
+ void startListening({
required OnSettingUpdated onSettingUpdated,
required OnError onError,
}) {
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart
index f9cd9d104d..1b0ae521ef 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart
@@ -87,7 +87,7 @@ class _CheckboxFilterEditorState extends State {
child: BlocBuilder(
builder: (context, state) {
final List children = [
- _buildFilterPannel(context, state),
+ _buildFilterPanel(context, state),
];
return Padding(
@@ -99,7 +99,7 @@ class _CheckboxFilterEditorState extends State {
);
}
- Widget _buildFilterPannel(
+ Widget _buildFilterPanel(
BuildContext context, CheckboxFilterEditorState state) {
return SizedBox(
height: 20,
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
index b05dfdaf63..3ffc07b6c9 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
@@ -96,7 +96,7 @@ class _SelectOptionFilterEditorState extends State {
SelectOptionFilterEditorState>(
builder: (context, state) {
List slivers = [
- SliverToBoxAdapter(child: _buildFilterPannel(context, state)),
+ SliverToBoxAdapter(child: _buildFilterPanel(context, state)),
];
if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty &&
@@ -131,7 +131,7 @@ class _SelectOptionFilterEditorState extends State {
);
}
- Widget _buildFilterPannel(
+ Widget _buildFilterPanel(
BuildContext context, SelectOptionFilterEditorState state) {
return SizedBox(
height: 20,
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart
index 94ccd5164e..0eaebbc4cb 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart
@@ -94,7 +94,7 @@ class _TextFilterEditorState extends State {
child: BlocBuilder(
builder: (context, state) {
final List children = [
- _buildFilterPannel(context, state),
+ _buildFilterPanel(context, state),
];
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
@@ -112,7 +112,7 @@ class _TextFilterEditorState extends State {
);
}
- Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) {
+ Widget _buildFilterPanel(BuildContext context, TextFilterEditorState state) {
return SizedBox(
height: 20,
child: Row(
diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
index 7f54bf4d27..73bcd377bc 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
@@ -128,11 +128,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
- final autoFocusParamters = _autoFocusParamters();
+ final autoFocusParameters = _autoFocusParameters();
final editor = AppFlowyEditor(
editorState: editorState,
- autoFocus: autoFocusParamters.value1,
- focusedSelection: autoFocusParamters.value2,
+ autoFocus: autoFocusParameters.value1,
+ focusedSelection: autoFocusParameters.value2,
customBuilders: {
// Divider
kDividerType: DividerWidgetBuilder(),
@@ -234,7 +234,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
}
}
- dartz.Tuple2 _autoFocusParamters() {
+ dartz.Tuple2 _autoFocusParameters() {
if (editorState.document.isEmpty) {
return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
index d4e467ab27..16a286e7e3 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
@@ -82,8 +82,8 @@ Iterable> customPluginTheme(BuildContext context) {
},
);
final pluginTheme = Theme.of(context).brightness == Brightness.dark
- ? darkPlguinStyleExtension
- : lightPlguinStyleExtension;
+ ? darkPluginStyleExtension
+ : lightPluginStyleExtension;
return pluginTheme.toList()
..removeWhere((element) =>
element is HeadingPluginStyle || element is NumberListPluginStyle)
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart
index fcab0eacd0..80a52a9f3e 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart
@@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
-const String _kDocumentAppearenceFontSize = 'kDocumentAppearenceFontSize';
+const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize';
class DocumentAppearance {
const DocumentAppearance({
@@ -24,7 +24,7 @@ class DocumentAppearanceCubit extends Cubit {
void fetch() async {
final prefs = await SharedPreferences.getInstance();
- final fontSize = prefs.getDouble(_kDocumentAppearenceFontSize) ?? 14.0;
+ final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 14.0;
emit(state.copyWith(
fontSize: fontSize,
));
@@ -32,7 +32,7 @@ class DocumentAppearanceCubit extends Cubit {
void syncFontSize(double fontSize) async {
final prefs = await SharedPreferences.getInstance();
- prefs.setDouble(_kDocumentAppearenceFontSize, fontSize);
+ prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
emit(state.copyWith(
fontSize: fontSize,
));
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart
index ebce76461e..73cfbeecb4 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart
@@ -20,7 +20,7 @@ void showLinkToPageMenu(
BuildContext context,
ViewLayoutTypePB pageType,
) {
- final aligment = menuService.alignment;
+ final alignment = menuService.alignment;
final offset = menuService.offset;
menuService.dismiss();
@@ -41,8 +41,8 @@ void showLinkToPageMenu(
_linkToPageMenu?.remove();
_linkToPageMenu = OverlayEntry(builder: (context) {
return Positioned(
- top: aligment == Alignment.bottomLeft ? offset.dy : null,
- bottom: aligment == Alignment.topLeft ? offset.dy : null,
+ top: alignment == Alignment.bottomLeft ? offset.dy : null,
+ bottom: alignment == Alignment.topLeft ? offset.dy : null,
left: offset.dx,
child: Material(
color: Colors.transparent,
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart
index 67b083a774..808962e019 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart
@@ -463,7 +463,7 @@ class _CoverImageState extends State<_CoverImage> {
coverImage = const SizedBox();
break;
}
-//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr
+//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
return SizedBox(
height: height,
child: OverflowBox(
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart
index e34fe1e8d6..a2c5ce97e4 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart
@@ -17,7 +17,7 @@ class Loading {
return const SimpleDialog(
elevation: 0.0,
backgroundColor:
- Colors.transparent, // can change this to your prefered color
+ Colors.transparent, // can change this to your preferred color
children: [
Center(
child: CircularProgressIndicator(),
diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart
index ebe027939d..de2d612250 100644
--- a/frontend/appflowy_flutter/lib/startup/startup.dart
+++ b/frontend/appflowy_flutter/lib/startup/startup.dart
@@ -27,7 +27,7 @@ import 'tasks/prelude.dart';
// βββββΆβAppWidgetTaskββββββββββΆβApplicationWidget βββββββΆβ SplashScreen β
// βββββββββββββββ ββββββββββββββββββββ βββββββββββββββββ
//
-// 3.build MeterialApp
+// 3.build MaterialApp
final getIt = GetIt.instance;
abstract class EntryPoint {
diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart
index 2515750718..df9d203974 100644
--- a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart
+++ b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart
@@ -141,7 +141,7 @@ class AppEvent with _$AppEvent {
PluginBuilder pluginBuilder, {
String? desc,
- /// The initial data should be the JSON of the doucment
+ /// The initial data should be the JSON of the document
/// For example: {"document":{"type":"editor","children":[]}}
String? initialData,
Map? ext,
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
index e91d046ded..b16d9b0e57 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
@@ -33,14 +33,14 @@ class ViewSection extends StatelessWidget {
},
child: BlocBuilder(
builder: (context, state) {
- return _reorderableColum(context, state);
+ return _reorderableColumn(context, state);
},
),
),
);
}
- ReorderableColumn _reorderableColum(
+ ReorderableColumn _reorderableColumn(
BuildContext context, ViewSectionState state) {
final children = state.views.map((view) {
return ViewSectionItem(
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md
index 30926b3d50..89a6eecf9c 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md
@@ -365,7 +365,7 @@ ThemeData customizeEditorTheme(BuildContext context) {
return Theme.of(context).copyWith(extensions: [
editorStyle,
- ...darkPlguinStyleExtension,
+ ...darkPluginStyleExtension,
quote,
]);
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart
index 89a1c393af..293d7a6445 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart
@@ -48,7 +48,7 @@ class _HomePageState extends State {
ThemeData _themeData = ThemeData.light().copyWith(
extensions: [
...lightEditorStyleExtension,
- ...lightPlguinStyleExtension,
+ ...lightPluginStyleExtension,
],
);
@@ -151,7 +151,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
// Theme Demo
_buildSeparator(context, 'Theme Demo'),
- _buildListTile(context, 'Bulit In Dark Mode', () {
+ _buildListTile(context, 'Built In Dark Mode', () {
_jsonString = Future.value(
jsonEncode(_editorState.document.toJson()).toString(),
);
@@ -159,7 +159,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
_themeData = ThemeData.dark().copyWith(
extensions: [
...darkEditorStyleExtension,
- ...darkPlguinStyleExtension,
+ ...darkPluginStyleExtension,
],
);
});
@@ -372,7 +372,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
return Theme.of(context).copyWith(extensions: [
editorStyle,
- ...darkPlguinStyleExtension,
+ ...darkPluginStyleExtension,
quote,
]);
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart
index 65a8f868fa..be84ae38f9 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart
@@ -34,7 +34,7 @@ ThemeData customizeEditorTheme(BuildContext context) {
return Theme.of(context).copyWith(extensions: [
editorStyle,
- ...darkPlguinStyleExtension,
+ ...darkPluginStyleExtension,
quote,
]);
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart
index 44e4e3d5f5..4b214adad6 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart
@@ -77,7 +77,7 @@ class EditorState {
// TODO: only for testing.
bool disableSealTimer = false;
- bool disbaleRules = false;
+ bool disableRules = false;
bool editable = true;
@@ -209,7 +209,7 @@ class EditorState {
void _applyRules(int ruleCount) {
// Set a maximum count to prevent a dead loop.
- if (ruleCount >= 5 || disbaleRules) {
+ if (ruleCount >= 5 || disableRules) {
return;
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
index b4f7bb5c67..2d5af7bae1 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
@@ -69,7 +69,7 @@ class SelectionMenu implements SelectionMenuService {
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorHeight = editorState.renderBox!.size.height;
- // show below defualt
+ // show below default
var showBelow = true;
_alignment = Alignment.bottomLeft;
final bottomRight = selectionRects.first.bottomRight;
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart
index 44fabea573..19f70b2a92 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart
@@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
-Iterable> get lightPlguinStyleExtension => [
+Iterable> get lightPluginStyleExtension => [
HeadingPluginStyle.light,
CheckboxPluginStyle.light,
NumberListPluginStyle.light,
QuotedTextPluginStyle.light,
];
-Iterable> get darkPlguinStyleExtension => [
+Iterable> get darkPluginStyleExtension => [
HeadingPluginStyle.dark,
CheckboxPluginStyle.dark,
NumberListPluginStyle.dark,
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
index 93be8b0240..fc9df7346f 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
@@ -16,14 +16,14 @@ class ToolbarWidget extends StatefulWidget {
required this.layerLink,
required this.offset,
required this.items,
- this.aligment = Alignment.topLeft,
+ this.alignment = Alignment.topLeft,
}) : super(key: key);
final EditorState editorState;
final LayerLink layerLink;
final Offset offset;
final List items;
- final Alignment aligment;
+ final Alignment alignment;
@override
State createState() => _ToolbarWidgetState();
@@ -41,7 +41,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin {
link: widget.layerLink,
showWhenUnlinked: true,
offset: widget.offset,
- followerAnchor: widget.aligment,
+ followerAnchor: widget.alignment,
child: _buildToolbar(context),
),
);
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart
index 9fcfcfd108..86adfdecf1 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart
@@ -39,7 +39,7 @@ class AppFlowyEditor extends StatefulWidget {
this.themeData = themeData ??
ThemeData.light().copyWith(extensions: [
...lightEditorStyleExtension,
- ...lightPlguinStyleExtension,
+ ...lightPluginStyleExtension,
]);
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
index b38d838fed..1f798f4114 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
@@ -310,7 +310,7 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
return KeyEventResult.handled;
};
-ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) {
+ShortcutEventHandler doubleAsteriskToBoldHandler = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType();
@@ -366,8 +366,8 @@ ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) {
return KeyEventResult.handled;
};
-//Implement in the same way as doubleAsteriskToBoldHanlder
-ShortcutEventHandler doubleUnderscoreToBoldHanlder = (editorState, event) {
+//Implement in the same way as doubleAsteriskToBoldHandler
+ShortcutEventHandler doubleUnderscoreToBoldHandler = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType();
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
index d6338b6fe3..b4566acf92 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
@@ -310,12 +310,12 @@ List builtInShortcutEvents = [
ShortcutEvent(
key: 'Double asterisk to bold',
command: 'shift+digit 8',
- handler: doubleAsteriskToBoldHanlder,
+ handler: doubleAsteriskToBoldHandler,
),
ShortcutEvent(
key: 'Double underscore to bold',
command: 'shift+underscore',
- handler: doubleUnderscoreToBoldHanlder,
+ handler: doubleUnderscoreToBoldHandler,
),
// https://github.com/flutter/flutter/issues/104944
// Workaround: Using space editing on the web platform often results in errors,
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart
index 9fd8ca3648..2d63ac8158 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart
@@ -66,7 +66,7 @@ class _FlowyToolbarState extends State
layerLink: layerLink,
offset: offset,
items: items,
- aligment: alignment,
+ alignment: alignment,
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
index 1c7325987b..3fde48495f 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
@@ -4,7 +4,7 @@ import '../infra/test_editor.dart';
void main() {
group('command_extension.dart', () {
- testWidgets('insert a new checkbox after an exsiting checkbox',
+ testWidgets('insert a new checkbox after an existing checkbox',
(tester) async {
final editor = tester.editor
..insertTextNode(
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart
index 3c8b3b0cc0..9aa3db6bb2 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart
@@ -45,7 +45,7 @@ void main() {
expect(result, false);
});
- testWidgets('insert a new checkbox after an exsiting checkbox',
+ testWidgets('insert a new checkbox after an existing checkbox',
(tester) async {
const text = 'Welcome to Appflowy π';
final editor = tester.editor
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart
index 55672fba47..69019f889c 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart
@@ -160,7 +160,7 @@ class EditorWidgetTester {
),
)
..disableSealTimer = true
- ..disbaleRules = true;
+ ..disableRules = true;
}
bool runAction(int actionIndex, Node node) {
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
index eceb429894..576dabf7c4 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
@@ -71,7 +71,7 @@ void main() async {
// https://github.com/AppFlowy-IO/AppFlowy/issues/1763
// // [Bug] Mouse unable to click a certain area #1763
- testWidgets('insert a new checkbox after an exsiting checkbox',
+ testWidgets('insert a new checkbox after an existing checkbox',
(tester) async {
// Before
//
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart
index 6ee51e4da6..119bc652ae 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart
@@ -27,7 +27,7 @@ void main() async {
Selection.single(path: [1], startOffset: 0, endOffset: text.length),
);
- // mutliple selection
+ // multiple selection
await _testSelection(
editor,
Selection(
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart
index 34720d16f4..5f7e604a3e 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart
@@ -24,15 +24,15 @@ void _showEmojiSelectionMenu(
SelectionMenuService menuService,
BuildContext context,
) {
- final aligment = menuService.alignment;
+ final alignment = menuService.alignment;
final offset = menuService.offset;
menuService.dismiss();
_emojiSelectionMenu?.remove();
_emojiSelectionMenu = OverlayEntry(builder: (context) {
return Positioned(
- top: aligment == Alignment.bottomLeft ? offset.dy : null,
- bottom: aligment == Alignment.topLeft ? offset.dy : null,
+ top: alignment == Alignment.bottomLeft ? offset.dy : null,
+ bottom: alignment == Alignment.topLeft ? offset.dy : null,
left: offset.dx,
child: Material(
child: EmojiSelectionMenu(
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
index 1961cae413..c66f3b63ae 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
@@ -45,7 +45,7 @@ export class TypeOptionController {
if (this.initialFieldInfo.some) {
return this.initialFieldInfo.val;
} else {
- throw Error('Unexpect empty type option data. Should call initialize first');
+ throw Error('Unexpected empty type option data. Should call initialize first');
}
}
return new FieldInfo(this.typeOptionData.val.field);
@@ -69,7 +69,7 @@ export class TypeOptionController {
void this.fieldBackendSvc?.updateField({ name: name });
this.fieldNotifier.notify(this.typeOptionData.val.field);
} else {
- throw Error('Unexpect empty type option data. Should call initialize first');
+ throw Error('Unexpected empty type option data. Should call initialize first');
}
};
@@ -82,20 +82,20 @@ export class TypeOptionController {
}
});
} else {
- throw Error('Unexpect empty type option data. Should call initialize first');
+ throw Error('Unexpected empty type option data. Should call initialize first');
}
};
deleteField = async () => {
if (this.fieldBackendSvc === undefined) {
- Log.error('Unexpect empty field backend service');
+ Log.error('Unexpected empty field backend service');
}
return this.fieldBackendSvc?.deleteField();
};
duplicateField = async () => {
if (this.fieldBackendSvc === undefined) {
- Log.error('Unexpect empty field backend service');
+ Log.error('Unexpected empty field backend service');
}
return this.fieldBackendSvc?.duplicateField();
};
diff --git a/frontend/rust-lib/flowy-document/src/editor/READ_ME.json b/frontend/rust-lib/flowy-document/src/editor/READ_ME.json
index b47f43ee16..ee118fd296 100644
--- a/frontend/rust-lib/flowy-document/src/editor/READ_ME.json
+++ b/frontend/rust-lib/flowy-document/src/editor/READ_ME.json
@@ -241,7 +241,7 @@
"insert": "bold text",
"attributes": {
"bold": true,
- "defaultFormating": true
+ "defaultFormatting": true
}
}
]
diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml
index 4178c3d2a5..c9a58b68fa 100644
--- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml
+++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml
@@ -25,7 +25,7 @@
## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It's built on a community-driven toolbox, including templates, plugins, themes, and more.
- ## Truely native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web.
+ ## Truly native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web.
diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml
index 0d94d2d549..a7b964d261 100644
--- a/frontend/scripts/makefile/desktop.toml
+++ b/frontend/scripts/makefile/desktop.toml
@@ -201,7 +201,7 @@ run_task = { name = [
[tasks.compile_test_backend]
mac_alias = "compile_test_backend_default"
-windows_alias = "compile_test_backend_widnows"
+windows_alias = "compile_test_backend_windows"
linux_alias = "compile_test_backend_default"
[tasks.compile_test_backend_default]
@@ -217,7 +217,7 @@ script = [
]
script_runner = "@shell"
-[tasks.compile_test_backend_widnows]
+[tasks.compile_test_backend_windows]
private = true
script = [
"""
diff --git a/shared-lib/lib-ot/src/text_delta/attributes.rs b/shared-lib/lib-ot/src/text_delta/attributes.rs
index ceb20a8ad7..c12359a5f3 100644
--- a/shared-lib/lib-ot/src/text_delta/attributes.rs
+++ b/shared-lib/lib-ot/src/text_delta/attributes.rs
@@ -139,7 +139,7 @@ lazy_static! {
BuildInTextAttributeKey::Background,
BuildInTextAttributeKey::InlineCode,
]);
- static ref INGORE_KEYS: HashSet = HashSet::from_iter(vec![
+ static ref IGNORE_KEYS: HashSet = HashSet::from_iter(vec![
BuildInTextAttributeKey::Width,
BuildInTextAttributeKey::Height,
]);
From 5672df3b0e717191abc43b527c1ce43b4dcb6fc9 Mon Sep 17 00:00:00 2001
From: Destiny Saturday <84413505+DestinedCodes@users.noreply.github.com>
Date: Mon, 20 Mar 2023 22:01:44 +0100
Subject: [PATCH 06/31] fix: sort hint text typo (#2044)
---
.../grid/presentation/widgets/sort/create_sort_list.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart
index be25a01ec8..22f3403a4d 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart
@@ -122,7 +122,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
color: Theme.of(context).colorScheme.background,
height: fixHeight,
child: FlowyTextField(
- hintText: LocaleKeys.grid_settings_filterBy.tr(),
+ hintText: LocaleKeys.grid_settings_sortBy.tr(),
onChanged: (text) {
context
.read()
From b7867bf17746f7f86c1720d1c622b0d32afa6d18 Mon Sep 17 00:00:00 2001
From: Alex Wallen
Date: Mon, 20 Mar 2023 15:29:17 -1000
Subject: [PATCH 07/31] Create a new board from the slash menu (#2018)
* feat: create a new board.
* feat: switch slash menu keywords
* fix: remove unused imports
* chore: export SelectionMenuItem from appflowy_editor for integration test
* feat: add integration test for slash commands
* fix: test in new file was unable to start
* feat: add translations
---
.../assets/translations/en.json | 5 +-
.../integration_test/switch_folder_test.dart | 71 +++++++++++++++++++
.../lib/plugins/document/document_page.dart | 13 ++--
.../plugins/board/board_menu_item.dart | 3 +-
.../plugins/board/board_view_menu_item.dart | 61 ++++++++++++++++
.../appflowy_editor/lib/appflowy_editor.dart | 1 +
6 files changed, 146 insertions(+), 8 deletions(-)
create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart
diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json
index f1aade4e48..d01c2453e0 100644
--- a/frontend/appflowy_flutter/assets/translations/en.json
+++ b/frontend/appflowy_flutter/assets/translations/en.json
@@ -334,7 +334,8 @@
},
"slashMenu": {
"board": {
- "selectABoardToLinkTo": "Select a Board to link to"
+ "selectABoardToLinkTo": "Select a Board to link to",
+ "createANewBoard": "Create a new Board"
},
"grid": {
"selectAGridToLinkTo": "Select a Grid to link to"
@@ -402,4 +403,4 @@
"layoutDateField": "Layout calendar by"
}
}
-}
\ No newline at end of file
+}
diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
index 6dcd41b081..92ddaf3472 100644
--- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
+++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
@@ -1,5 +1,10 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
import 'package:appflowy/user/presentation/folder/folder_widget.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -157,5 +162,71 @@ void main() {
await TestFolder.currentLocation(),
);
});
+
+ testWidgets('/board shortcut creates a new board', (tester) async {
+ const folderName = 'appflowy';
+ await TestFolder.cleanTestLocation(folderName);
+ await TestFolder.setTestLocation(folderName);
+
+ await tester.initializeAppFlowy();
+
+ // tap open button
+ await mockGetDirectoryPath(folderName);
+ await tester.tapOpenFolderButton();
+
+ await tester.wait(1000);
+ await tester.expectToSeeWelcomePage();
+
+ final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ // Necessary for being able to enterText when not in debug mode
+ binding.testTextInput.register();
+
+ // Needs tab to obtain focus for the app flowy editor.
+ // by default the tap appears at the center of the widget.
+ final Finder editor = find.byType(AppFlowyEditor);
+ await tester.tap(editor);
+ await tester.pumpAndSettle();
+
+ // tester.sendText() cannot be used since the editor
+ // does not contain any EditableText widgets.
+ // to interact with the app during an integration test,
+ // simulate physical keyboard events.
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.slash);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyB);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyO);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
+ await tester.pumpAndSettle();
+
+ // Checks whether the options in the selection menu
+ // for /board exist.
+ expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
+
+ // Finalizes the slash command that creates the board.
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+
+ // Checks whether new board is referenced and properly on the page.
+ expect(find.byType(BuiltInPageWidget), findsOneWidget);
+
+ // Checks whether the new board is in the side bar.
+ final sidebarLabel = LocaleKeys.newPageText.tr();
+ expect(find.text(sidebarLabel), findsOneWidget);
+ });
});
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
index 73bcd377bc..42e3884023 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
@@ -1,4 +1,7 @@
-import 'package:appflowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
@@ -7,19 +10,17 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/widget/error_page.dart';
-import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../startup/startup.dart';
import 'application/doc_bloc.dart';
import 'editor_styles.dart';
import 'presentation/banner.dart';
+import 'presentation/plugins/board/board_menu_item.dart';
class DocumentPage extends StatefulWidget {
final VoidCallback onDeleted;
@@ -172,6 +173,8 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
emojiMenuItem,
// Board
boardMenuItem,
+ // Create Board
+ boardViewMenuItem(documentBloc),
// Grid
gridMenuItem,
// Callout
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart
index cccb671e7c..c81efbb279 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart
@@ -17,7 +17,8 @@ SelectionMenuItem boardMenuItem = SelectionMenuItem(
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
- keywords: ['board', 'kanban'],
+ // TODO(a-wallen): Translate keywords
+ keywords: ['referenced board', 'referenced kanban'],
handler: (editorState, menuService, context) {
showLinkToPageMenu(
editorState,
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart
new file mode 100644
index 0000000000..b25fe52679
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart
@@ -0,0 +1,61 @@
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/application/prelude.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:appflowy/workspace/application/app/app_service.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
+ SelectionMenuItem(
+ name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
+ icon: (editorState, onSelected) {
+ return svgWidget(
+ 'editor/board',
+ size: const Size.square(18.0),
+ color: onSelected
+ ? editorState.editorStyle.selectionMenuItemSelectedIconColor
+ : editorState.editorStyle.selectionMenuItemIconColor,
+ );
+ },
+ // TODO(a-wallen): Translate keywords.
+ keywords: ['board', 'kanban'],
+ handler: (editorState, menuService, context) async {
+ if (!documentBloc.view.hasAppId()) {
+ return;
+ }
+
+ final appId = documentBloc.view.appId;
+ final service = AppBackendService();
+
+ final result = (await service.createView(
+ appId: appId,
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ layoutType: ViewLayoutTypePB.Board,
+ ))
+ .getLeftOrNull();
+
+ // If the result is null, then something went wrong here.
+ if (result == null) {
+ return;
+ }
+
+ final app =
+ (await service.readApp(appId: result.appId)).getLeftOrNull();
+ // We should show an error dialog.
+ if (app == null) {
+ return;
+ }
+
+ final view =
+ (await service.getView(result.appId, result.id)).getLeftOrNull();
+ // As this.
+ if (view == null) {
+ return;
+ }
+
+ editorState.insertPage(app, view);
+ },
+ );
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart
index bea819e232..44595fcdff 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart
@@ -31,6 +31,7 @@ export 'src/extensions/attributes_extension.dart';
export 'src/render/rich_text/default_selectable.dart';
export 'src/render/rich_text/flowy_rich_text.dart';
export 'src/render/selection_menu/selection_menu_widget.dart';
+export 'src/render/selection_menu/selection_menu_item_widget.dart';
export 'src/l10n/l10n.dart';
export 'src/render/style/plugin_styles.dart';
export 'src/render/style/editor_style.dart';
From dc09d67d7cd462441e7efc1544b0ffd849481a10 Mon Sep 17 00:00:00 2001
From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com>
Date: Tue, 21 Mar 2023 17:22:47 +0800
Subject: [PATCH 08/31] ci: fix tauri CI (#2040)
* ci: print log
* ci: sudo install
* ci: enable ubuntu
* ci: try to fix
* ci: force run tauri ci
* ci: update node version
* ci: specific build protoc-gen-ts ts version
---
frontend/scripts/makefile/protobuf.toml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/scripts/makefile/protobuf.toml b/frontend/scripts/makefile/protobuf.toml
index 463a1bd5f2..094ee82429 100644
--- a/frontend/scripts/makefile/protobuf.toml
+++ b/frontend/scripts/makefile/protobuf.toml
@@ -1,19 +1,19 @@
[tasks.install_tauri_protobuf.windows]
script = """
-npm install -g protoc-gen-ts
+npm install -g protoc-gen-ts typescript@4.9.5
"""
script_runner = "@shell"
[tasks.install_tauri_protobuf.mac]
script = """
-npm install -g protoc-gen-ts
+sudo npm install -g protoc-gen-ts typescript@4.9.5
"""
script_runner = "@shell"
[tasks.install_tauri_protobuf.linux]
script = """
-sudo npm install -g protoc-gen-ts
+sudo npm install -g protoc-gen-ts typescript@4.9.5
"""
script_runner = "@shell"
From 5c099297a42d7e0155fbd3a7f49fa1bb137b156c Mon Sep 17 00:00:00 2001
From: squidrye
Date: Tue, 21 Mar 2023 16:26:04 +0530
Subject: [PATCH 09/31] fix: slash menu responds to multiple key events
---
.../selection_menu/selection_menu_widget.dart | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
index 3a88cfcd4a..8af25c7325 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
@@ -324,7 +324,7 @@ class _SelectionMenuWidgetState extends State {
_deleteLastCharacters();
return KeyEventResult.handled;
} else if (event.character != null &&
- !arrowKeys.contains(event.logicalKey)) {
+ !arrowKeys.contains(event.logicalKey) && event.logicalKey != LogicalKeyboardKey.tab) {
keyword += event.character!;
_insertText(event.character!);
return KeyEventResult.handled;
@@ -339,7 +339,18 @@ class _SelectionMenuWidgetState extends State {
newSelectedIndex -= 1;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
newSelectedIndex += 1;
+ }else if (event.logicalKey == LogicalKeyboardKey.tab) {
+ newSelectedIndex += widget.maxItemInRow;
+ var currRow = (newSelectedIndex) % widget.maxItemInRow;
+ if (newSelectedIndex >= _showingItems.length) {
+ if (currRow + 1 >= widget.maxItemInRow) {
+ newSelectedIndex = 0;
+ } else {
+ newSelectedIndex = (currRow + 1);
+ }
+ }
}
+
if (newSelectedIndex != _selectedIndex) {
setState(() {
_selectedIndex = newSelectedIndex.clamp(0, _showingItems.length - 1);
From 10b6b9e5adc7c6c2c3b78f9824315c7a60375552 Mon Sep 17 00:00:00 2001
From: squidrye
Date: Tue, 21 Mar 2023 17:38:22 +0530
Subject: [PATCH 10/31] refactor: suggested changes
---
.../src/render/selection_menu/selection_menu_widget.dart | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
index 8af25c7325..a212692e20 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
@@ -339,15 +339,11 @@ class _SelectionMenuWidgetState extends State {
newSelectedIndex -= 1;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
newSelectedIndex += 1;
- }else if (event.logicalKey == LogicalKeyboardKey.tab) {
+ } else if (event.logicalKey == LogicalKeyboardKey.tab) {
newSelectedIndex += widget.maxItemInRow;
var currRow = (newSelectedIndex) % widget.maxItemInRow;
if (newSelectedIndex >= _showingItems.length) {
- if (currRow + 1 >= widget.maxItemInRow) {
- newSelectedIndex = 0;
- } else {
- newSelectedIndex = (currRow + 1);
- }
+ newSelectedIndex = (currRow + 1) % widget.maxItemInRow;
}
}
From f49e2571facf01059d8e510793e72e4775ce4fe7 Mon Sep 17 00:00:00 2001
From: squidrye
Date: Tue, 21 Mar 2023 19:40:48 +0530
Subject: [PATCH 11/31] test: added tests for key events in selection menu
widget
---
.../selection_menu_widget_test.dart | 81 +++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
index 6b392cdebb..45bbea120a 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
@@ -118,6 +118,79 @@ void main() async {
findsNothing,
);
});
+
+ group('tab and arrow keys move selection in desired direction', () {
+
+ testWidgets('left and right keys move selection in desired direction',
+ (tester) async {
+ final editor = await _prepare(tester);
+
+ var initialSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
+
+ var newSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(newSelection.item), 5);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.arrowLeft);
+
+ var finalSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0);
+ });
+
+ testWidgets('up and down keys move selection in desired direction',
+ (tester) async {
+ final editor = await _prepare(tester);
+
+ var initialSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
+
+ var newSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(newSelection.item), 1);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.arrowUp);
+
+ var finalSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 0);
+ });
+
+ testWidgets('arrow keys and tab move same selection',
+ (tester) async {
+ final editor = await _prepare(tester);
+
+ var initialSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
+
+ var newSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(newSelection.item), 1);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+ var finalSelection = getSelectedMenuItem(tester);
+ expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 6);
+ });
+
+ testWidgets('tab moves selection to next row Item on reaching end of current row',
+ (tester) async {
+ final editor = await _prepare(tester);
+
+ final initialSelection = getSelectedMenuItem(tester);
+
+ expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0);
+
+ await editor.pressLogicKey(LogicalKeyboardKey.tab);
+ await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+ final finalSelection = getSelectedMenuItem(tester);
+
+ expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 1);
+ });
+ });
});
}
@@ -178,3 +251,11 @@ Future _testDefaultSelectionMenuItems(
expect(node?.attributes.check, false);
}
}
+
+SelectionMenuItemWidget getSelectedMenuItem(WidgetTester tester) {
+ return tester
+ .state(find.byWidgetPredicate(
+ (widget) => widget is SelectionMenuItemWidget && widget.isSelected,
+ ))
+ .widget as SelectionMenuItemWidget;
+}
\ No newline at end of file
From c6ffc0057cc08c59215e29420b348f8301832249 Mon Sep 17 00:00:00 2001
From: Alex Wallen
Date: Tue, 21 Mar 2023 15:55:20 -1000
Subject: [PATCH 12/31] feat: create a new grid from the document slash menu
(#2051)
* feat: new grid from slash menu
* feat: add translation strings
* feat: add integration test
* fix: analyzer errors
---
.../assets/translations/en.json | 3 +-
.../integration_test/switch_folder_test.dart | 64 +++++++++++++++++++
.../lib/plugins/document/document_page.dart | 3 +
.../plugins/grid/grid_menu_item.dart | 2 +-
.../plugins/grid/grid_view_menu_item.dart | 60 +++++++++++++++++
5 files changed, 130 insertions(+), 2 deletions(-)
create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart
diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json
index d01c2453e0..927e968b4e 100644
--- a/frontend/appflowy_flutter/assets/translations/en.json
+++ b/frontend/appflowy_flutter/assets/translations/en.json
@@ -338,7 +338,8 @@
"createANewBoard": "Create a new Board"
},
"grid": {
- "selectAGridToLinkTo": "Select a Grid to link to"
+ "selectAGridToLinkTo": "Select a Grid to link to",
+ "createANewGrid": "Create a new Grid"
}
},
"plugins": {
diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
index 92ddaf3472..555a5e41b1 100644
--- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
+++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart
@@ -228,5 +228,69 @@ void main() {
final sidebarLabel = LocaleKeys.newPageText.tr();
expect(find.text(sidebarLabel), findsOneWidget);
});
+
+ testWidgets('/grid shortcut creates a new grid', (tester) async {
+ const folderName = 'appflowy';
+ await TestFolder.cleanTestLocation(folderName);
+ await TestFolder.setTestLocation(folderName);
+
+ await tester.initializeAppFlowy();
+
+ // tap open button
+ await mockGetDirectoryPath(folderName);
+ await tester.tapOpenFolderButton();
+
+ await tester.wait(1000);
+ await tester.expectToSeeWelcomePage();
+
+ final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ // Necessary for being able to enterText when not in debug mode
+ binding.testTextInput.register();
+
+ // Needs tab to obtain focus for the app flowy editor.
+ // by default the tap appears at the center of the widget.
+ final Finder editor = find.byType(AppFlowyEditor);
+ await tester.tap(editor);
+ await tester.pumpAndSettle();
+
+ // tester.sendText() cannot be used since the editor
+ // does not contain any EditableText widgets.
+ // to interact with the app during an integration test,
+ // simulate physical keyboard events.
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.slash);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyG);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyI);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
+ await tester.pumpAndSettle();
+ await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
+ await tester.pumpAndSettle();
+
+ // Checks whether the options in the selection menu
+ // for /grid exist.
+ expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
+
+ // Finalizes the slash command that creates the board.
+ await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+ await tester.pumpAndSettle();
+
+ // Checks whether new board is referenced and properly on the page.
+ expect(find.byType(BuiltInPageWidget), findsOneWidget);
+
+ // Checks whether the new board is in the side bar.
+ final sidebarLabel = LocaleKeys.newPageText.tr();
+ expect(find.text(sidebarLabel), findsOneWidget);
+ });
});
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
index 42e3884023..8c87000722 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart
@@ -20,6 +20,7 @@ import '../../startup/startup.dart';
import 'application/doc_bloc.dart';
import 'editor_styles.dart';
import 'presentation/banner.dart';
+import 'presentation/plugins/grid/grid_view_menu_item.dart';
import 'presentation/plugins/board/board_menu_item.dart';
class DocumentPage extends StatefulWidget {
@@ -177,6 +178,8 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
boardViewMenuItem(documentBloc),
// Grid
gridMenuItem,
+ // Create Grid
+ gridViewMenuItem(documentBloc),
// Callout
calloutMenuItem,
// AI
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart
index 737c17f4f5..b2775543e9 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart
@@ -17,7 +17,7 @@ SelectionMenuItem gridMenuItem = SelectionMenuItem(
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
- keywords: ['grid'],
+ keywords: ['referenced grid'],
handler: (editorState, menuService, context) {
showLinkToPageMenu(
editorState,
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart
new file mode 100644
index 0000000000..2dc030d547
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart
@@ -0,0 +1,60 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/application/doc_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:appflowy/workspace/application/app/app_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
+ SelectionMenuItem(
+ name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
+ icon: (editorState, onSelected) {
+ return svgWidget(
+ 'editor/grid',
+ size: const Size.square(18.0),
+ color: onSelected
+ ? editorState.editorStyle.selectionMenuItemSelectedIconColor
+ : editorState.editorStyle.selectionMenuItemIconColor,
+ );
+ },
+ keywords: ['grid'],
+ handler: (editorState, menuService, context) async {
+ if (!documentBloc.view.hasAppId()) {
+ return;
+ }
+
+ final appId = documentBloc.view.appId;
+ final service = AppBackendService();
+
+ final result = (await service.createView(
+ appId: appId,
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ layoutType: ViewLayoutTypePB.Grid,
+ ))
+ .getLeftOrNull();
+
+ // If the result is null, then something went wrong here.
+ if (result == null) {
+ return;
+ }
+
+ final app =
+ (await service.readApp(appId: result.appId)).getLeftOrNull();
+ // We should show an error dialog.
+ if (app == null) {
+ return;
+ }
+
+ final view =
+ (await service.getView(result.appId, result.id)).getLeftOrNull();
+ // As this.
+ if (view == null) {
+ return;
+ }
+
+ editorState.insertPage(app, view);
+ },
+ );
From 8471bc299d7fda83e9e0a0024724b1a19d1aea7e Mon Sep 17 00:00:00 2001
From: qinluhe <108015703+qinluhe@users.noreply.github.com>
Date: Wed, 22 Mar 2023 10:46:01 +0800
Subject: [PATCH 13/31] feat: block list virtualized scroll (#2023)
* feat: block list virtualized scroll
* feat: block selection
* refactor: block editor
* fix: block selection scroll
* fix: ts error
---
frontend/appflowy_tauri/package.json | 3 +
frontend/appflowy_tauri/pnpm-lock.yaml | 735 +++++++++++-------
.../src/appflowy_app/block_editor/block.ts | 28 -
.../block_editor/blocks/text_block/index.ts | 71 ++
.../blocks/text_block/text_selection.ts | 35 +
.../appflowy_app/block_editor/core/block.ts | 107 +++
.../block_editor/core/block_chain.ts | 225 ++++++
.../block_editor/core/op_adapter.ts | 16 +
.../block_editor/core/operation.ts | 153 ++++
.../appflowy_app/block_editor/core/sync.ts | 48 ++
.../src/appflowy_app/block_editor/index.ts | 70 +-
.../src/appflowy_app/block_editor/rect.ts | 66 --
.../src/appflowy_app/block_editor/tree.ts | 140 ----
.../block_editor/view/block_position.ts | 73 ++
.../block_editor/view/region_grid.ts | 81 ++
.../appflowy_app/block_editor/view/tree.ts | 165 ++++
.../block_editor/view/tree_node.ts | 59 ++
.../HoveringToolbar/FormatButton.tsx | 29 +-
.../components/HoveringToolbar/FormatIcon.tsx | 20 +
.../components/HoveringToolbar/components.tsx | 5 -
.../components/HoveringToolbar/index.hooks.ts | 36 +
.../components/HoveringToolbar/index.tsx | 29 +-
.../BlockComponent/BlockComponet.hooks.ts | 36 +
.../components/block/BlockComponent/index.tsx | 91 +++
.../block/BlockList/BlockComponent.tsx | 39 -
.../block/BlockList/BlockList.hooks.tsx | 92 +++
.../block/BlockList/BlockListTitle.tsx | 18 +
.../block/BlockList/ListFallbackComponent.tsx | 31 +
.../components/block/BlockList/index.tsx | 85 +-
.../BlockSelection/BlockSelection.hooks.tsx | 137 ++++
.../components/block/BlockSelection/index.tsx | 18 +
.../components/block/CodeBlock/index.tsx | 6 +-
.../components/block/ColumnBlock/index.tsx | 15 +-
.../components/block/HeadingBlock/index.tsx | 14 +-
.../block/ListBlock/BulletedListBlock.tsx | 8 +-
.../block/ListBlock/ColumnListBlock.tsx | 6 +-
.../block/ListBlock/NumberedListBlock.tsx | 23 +-
.../components/block/ListBlock/index.tsx | 14 +-
.../components/block/PageBlock/index.tsx | 6 +-
.../components/block/TextBlock/index.hooks.ts | 98 +++
.../components/block/TextBlock/index.tsx | 90 +--
.../src/appflowy_app/constants/toolbar.ts | 14 +
.../src/appflowy_app/interfaces/index.ts | 68 +-
.../src/appflowy_app/utils/block.ts | 25 +
.../src/appflowy_app/utils/block_context.ts | 8 -
.../src/appflowy_app/utils/block_selection.ts | 36 +
.../src/appflowy_app/utils/editor/format.ts | 25 -
.../src/appflowy_app/utils/editor/hotkey.ts | 22 -
.../src/appflowy_app/utils/editor/toolbar.ts | 28 -
.../src/appflowy_app/utils/slate/context.ts | 6 +
.../src/appflowy_app/utils/slate/toolbar.ts | 16 +-
.../src/appflowy_app/utils/tool.ts | 26 +
.../appflowy_app/views/DocumentPage.hooks.ts | 597 +++++++++++++-
.../src/appflowy_app/views/DocumentPage.tsx | 23 +-
54 files changed, 2973 insertions(+), 942 deletions(-)
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json
index e0fa62458c..8215381041 100644
--- a/frontend/appflowy_tauri/package.json
+++ b/frontend/appflowy_tauri/package.json
@@ -20,7 +20,9 @@
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@reduxjs/toolkit": "^1.9.2",
+ "@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0",
+ "events": "^3.3.0",
"google-protobuf": "^3.21.2",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
@@ -40,6 +42,7 @@
"slate": "^0.91.4",
"slate-react": "^0.91.9",
"ts-results": "^3.3.0",
+ "ulid": "^2.3.0",
"utf8": "^3.0.0"
},
"devDependencies": {
diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml
index a6c00ac119..4402ceca71 100644
--- a/frontend/appflowy_tauri/pnpm-lock.yaml
+++ b/frontend/appflowy_tauri/pnpm-lock.yaml
@@ -6,12 +6,11 @@ specifiers:
'@mui/icons-material': ^5.11.11
'@mui/material': ^5.11.12
'@reduxjs/toolkit': ^1.9.2
+ '@tanstack/react-virtual': 3.0.0-beta.54
'@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.2
'@types/google-protobuf': ^3.15.6
'@types/is-hotkey': ^0.1.7
- '@types/jest': ^29.4.0
- '@types/mocha': ^10.0.1
'@types/node': ^18.7.10
'@types/react': ^18.0.15
'@types/react-dom': ^18.0.6
@@ -22,11 +21,12 @@ specifiers:
autoprefixer: ^10.4.13
eslint: ^8.34.0
eslint-plugin-react: ^7.32.2
+ events: ^3.3.0
google-protobuf: ^3.21.2
i18next: ^22.4.10
i18next-browser-languagedetector: ^7.0.1
is-hotkey: ^0.2.0
- jest: ^29.4.3
+ jest: ^29.5.0
nanoid: ^4.0.0
postcss: ^8.4.21
prettier: 2.8.4
@@ -43,9 +43,9 @@ specifiers:
slate: ^0.91.4
slate-react: ^0.91.9
tailwindcss: ^3.2.7
- ts-jest: ^29.0.5
ts-results: ^3.3.0
typescript: ^4.6.4
+ ulid: ^2.3.0
utf8: ^3.0.0
vite: ^4.0.0
@@ -55,12 +55,14 @@ dependencies:
'@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
'@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli
'@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq
+ '@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
'@tauri-apps/api': 1.2.0
+ events: 3.3.0
google-protobuf: 3.21.2
i18next: 22.4.10
i18next-browser-languagedetector: 7.0.1
is-hotkey: 0.2.0
- jest: 29.4.3_@types+node@18.14.6
+ jest: 29.5.0_@types+node@18.14.6
nanoid: 4.0.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@@ -74,14 +76,13 @@ dependencies:
slate: 0.91.4
slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u
ts-results: 3.3.0
+ ulid: 2.3.0
utf8: 3.0.0
devDependencies:
'@tauri-apps/cli': 1.2.3
'@types/google-protobuf': 3.15.6
'@types/is-hotkey': 0.1.7
- '@types/jest': 29.4.0
- '@types/mocha': 10.0.1
'@types/node': 18.14.6
'@types/react': 18.0.28
'@types/react-dom': 18.0.11
@@ -96,7 +97,6 @@ devDependencies:
prettier: 2.8.4
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
tailwindcss: 3.2.7_postcss@8.4.21
- ts-jest: 29.0.5_orzzknleilowtsz34rkaotjvzm
typescript: 4.9.5
vite: 4.1.4_@types+node@18.14.6
@@ -261,6 +261,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
@@ -269,6 +270,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.21.0:
resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
@@ -277,6 +279,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.21.0:
resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
@@ -285,6 +288,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
@@ -293,6 +297,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.21.0:
resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==}
@@ -302,6 +307,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.21.0:
resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
@@ -310,6 +316,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
@@ -318,6 +325,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.21.0:
resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
@@ -326,6 +334,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
@@ -334,6 +343,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
@@ -342,6 +352,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.21.0:
resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
@@ -350,6 +361,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.21.0:
resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
@@ -359,6 +371,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.21.0:
resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==}
@@ -368,6 +381,7 @@ packages:
dependencies:
'@babel/core': 7.21.0
'@babel/helper-plugin-utils': 7.20.2
+ dev: false
/@babel/plugin-transform-react-jsx-self/7.21.0_@babel+core@7.21.0:
resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==}
@@ -431,6 +445,7 @@ packages:
/@bcoe/v8-coverage/0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+ dev: false
/@emotion/babel-plugin/11.10.6:
resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==}
@@ -797,24 +812,27 @@ packages:
get-package-type: 0.1.0
js-yaml: 3.14.1
resolve-from: 5.0.0
+ dev: false
/@istanbuljs/schema/0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
+ dev: false
- /@jest/console/29.4.3:
- resolution: {integrity: sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==}
+ /@jest/console/29.5.0:
+ resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
chalk: 4.1.2
- jest-message-util: 29.4.3
- jest-util: 29.4.3
+ jest-message-util: 29.5.0
+ jest-util: 29.5.0
slash: 3.0.0
+ dev: false
- /@jest/core/29.4.3:
- resolution: {integrity: sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==}
+ /@jest/core/29.5.0:
+ resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
@@ -822,86 +840,92 @@ packages:
node-notifier:
optional: true
dependencies:
- '@jest/console': 29.4.3
- '@jest/reporters': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/transform': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/console': 29.5.0
+ '@jest/reporters': 29.5.0
+ '@jest/test-result': 29.5.0
+ '@jest/transform': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.10
- jest-changed-files: 29.4.3
- jest-config: 29.4.3_@types+node@18.14.6
- jest-haste-map: 29.4.3
- jest-message-util: 29.4.3
+ jest-changed-files: 29.5.0
+ jest-config: 29.5.0_@types+node@18.14.6
+ jest-haste-map: 29.5.0
+ jest-message-util: 29.5.0
jest-regex-util: 29.4.3
- jest-resolve: 29.4.3
- jest-resolve-dependencies: 29.4.3
- jest-runner: 29.4.3
- jest-runtime: 29.4.3
- jest-snapshot: 29.4.3
- jest-util: 29.4.3
- jest-validate: 29.4.3
- jest-watcher: 29.4.3
+ jest-resolve: 29.5.0
+ jest-resolve-dependencies: 29.5.0
+ jest-runner: 29.5.0
+ jest-runtime: 29.5.0
+ jest-snapshot: 29.5.0
+ jest-util: 29.5.0
+ jest-validate: 29.5.0
+ jest-watcher: 29.5.0
micromatch: 4.0.5
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
slash: 3.0.0
strip-ansi: 6.0.1
transitivePeerDependencies:
- supports-color
- ts-node
+ dev: false
- /@jest/environment/29.4.3:
- resolution: {integrity: sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==}
+ /@jest/environment/29.5.0:
+ resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/fake-timers': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/fake-timers': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
- jest-mock: 29.4.3
+ jest-mock: 29.5.0
+ dev: false
- /@jest/expect-utils/29.4.3:
- resolution: {integrity: sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==}
+ /@jest/expect-utils/29.5.0:
+ resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
jest-get-type: 29.4.3
+ dev: false
- /@jest/expect/29.4.3:
- resolution: {integrity: sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==}
+ /@jest/expect/29.5.0:
+ resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- expect: 29.4.3
- jest-snapshot: 29.4.3
+ expect: 29.5.0
+ jest-snapshot: 29.5.0
transitivePeerDependencies:
- supports-color
+ dev: false
- /@jest/fake-timers/29.4.3:
- resolution: {integrity: sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==}
+ /@jest/fake-timers/29.5.0:
+ resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@sinonjs/fake-timers': 10.0.2
'@types/node': 18.14.6
- jest-message-util: 29.4.3
- jest-mock: 29.4.3
- jest-util: 29.4.3
+ jest-message-util: 29.5.0
+ jest-mock: 29.5.0
+ jest-util: 29.5.0
+ dev: false
- /@jest/globals/29.4.3:
- resolution: {integrity: sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==}
+ /@jest/globals/29.5.0:
+ resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/environment': 29.4.3
- '@jest/expect': 29.4.3
- '@jest/types': 29.4.3
- jest-mock: 29.4.3
+ '@jest/environment': 29.5.0
+ '@jest/expect': 29.5.0
+ '@jest/types': 29.5.0
+ jest-mock: 29.5.0
transitivePeerDependencies:
- supports-color
+ dev: false
- /@jest/reporters/29.4.3:
- resolution: {integrity: sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==}
+ /@jest/reporters/29.5.0:
+ resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
@@ -910,10 +934,10 @@ packages:
optional: true
dependencies:
'@bcoe/v8-coverage': 0.2.3
- '@jest/console': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/transform': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/console': 29.5.0
+ '@jest/test-result': 29.5.0
+ '@jest/transform': 29.5.0
+ '@jest/types': 29.5.0
'@jridgewell/trace-mapping': 0.3.17
'@types/node': 18.14.6
chalk: 4.1.2
@@ -926,21 +950,23 @@ packages:
istanbul-lib-report: 3.0.0
istanbul-lib-source-maps: 4.0.1
istanbul-reports: 3.1.5
- jest-message-util: 29.4.3
- jest-util: 29.4.3
- jest-worker: 29.4.3
+ jest-message-util: 29.5.0
+ jest-util: 29.5.0
+ jest-worker: 29.5.0
slash: 3.0.0
string-length: 4.0.2
strip-ansi: 6.0.1
v8-to-istanbul: 9.1.0
transitivePeerDependencies:
- supports-color
+ dev: false
/@jest/schemas/29.4.3:
resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@sinclair/typebox': 0.25.24
+ dev: false
/@jest/source-map/29.4.3:
resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==}
@@ -949,49 +975,53 @@ packages:
'@jridgewell/trace-mapping': 0.3.17
callsites: 3.1.0
graceful-fs: 4.2.10
+ dev: false
- /@jest/test-result/29.4.3:
- resolution: {integrity: sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==}
+ /@jest/test-result/29.5.0:
+ resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/console': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/console': 29.5.0
+ '@jest/types': 29.5.0
'@types/istanbul-lib-coverage': 2.0.4
collect-v8-coverage: 1.0.1
+ dev: false
- /@jest/test-sequencer/29.4.3:
- resolution: {integrity: sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==}
+ /@jest/test-sequencer/29.5.0:
+ resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/test-result': 29.4.3
+ '@jest/test-result': 29.5.0
graceful-fs: 4.2.10
- jest-haste-map: 29.4.3
+ jest-haste-map: 29.5.0
slash: 3.0.0
+ dev: false
- /@jest/transform/29.4.3:
- resolution: {integrity: sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==}
+ /@jest/transform/29.5.0:
+ resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@babel/core': 7.21.0
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@jridgewell/trace-mapping': 0.3.17
babel-plugin-istanbul: 6.1.1
chalk: 4.1.2
convert-source-map: 2.0.0
fast-json-stable-stringify: 2.1.0
graceful-fs: 4.2.10
- jest-haste-map: 29.4.3
+ jest-haste-map: 29.5.0
jest-regex-util: 29.4.3
- jest-util: 29.4.3
+ jest-util: 29.5.0
micromatch: 4.0.5
pirates: 4.0.5
slash: 3.0.0
write-file-atomic: 4.0.2
transitivePeerDependencies:
- supports-color
+ dev: false
- /@jest/types/29.4.3:
- resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==}
+ /@jest/types/29.5.0:
+ resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/schemas': 29.4.3
@@ -1000,6 +1030,7 @@ packages:
'@types/node': 18.14.6
'@types/yargs': 17.0.22
chalk: 4.1.2
+ dev: false
/@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
@@ -1263,16 +1294,32 @@ packages:
/@sinclair/typebox/0.25.24:
resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==}
+ dev: false
/@sinonjs/commons/2.0.0:
resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==}
dependencies:
type-detect: 4.0.8
+ dev: false
/@sinonjs/fake-timers/10.0.2:
resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==}
dependencies:
'@sinonjs/commons': 2.0.0
+ dev: false
+
+ /@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0:
+ resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@tanstack/virtual-core': 3.0.0-beta.54
+ react: 18.2.0
+ dev: false
+
+ /@tanstack/virtual-core/3.0.0-beta.54:
+ resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==}
+ dev: false
/@tauri-apps/api/1.2.0:
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
@@ -1384,22 +1431,26 @@ packages:
'@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.1
'@types/babel__traverse': 7.18.3
+ dev: false
/@types/babel__generator/7.6.4:
resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==}
dependencies:
'@babel/types': 7.21.2
+ dev: false
/@types/babel__template/7.4.1:
resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==}
dependencies:
'@babel/parser': 7.21.2
'@babel/types': 7.21.2
+ dev: false
/@types/babel__traverse/7.18.3:
resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==}
dependencies:
'@babel/types': 7.21.2
+ dev: false
/@types/google-protobuf/3.15.6:
resolution: {integrity: sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==}
@@ -1409,6 +1460,7 @@ packages:
resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
dependencies:
'@types/node': 18.14.6
+ dev: false
/@types/hoist-non-react-statics/3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
@@ -1422,23 +1474,19 @@ packages:
/@types/istanbul-lib-coverage/2.0.4:
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
+ dev: false
/@types/istanbul-lib-report/3.0.0:
resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==}
dependencies:
'@types/istanbul-lib-coverage': 2.0.4
+ dev: false
/@types/istanbul-reports/3.0.1:
resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==}
dependencies:
'@types/istanbul-lib-report': 3.0.0
-
- /@types/jest/29.4.0:
- resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==}
- dependencies:
- expect: 29.4.3
- pretty-format: 29.4.3
- dev: true
+ dev: false
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
@@ -1448,10 +1496,6 @@ packages:
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
dev: false
- /@types/mocha/10.0.1:
- resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==}
- dev: true
-
/@types/node/18.14.6:
resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==}
@@ -1461,6 +1505,7 @@ packages:
/@types/prettier/2.7.2:
resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==}
+ dev: false
/@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
@@ -1498,6 +1543,7 @@ packages:
/@types/stack-utils/2.0.1:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
+ dev: false
/@types/use-sync-external-store/0.0.3:
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
@@ -1509,11 +1555,13 @@ packages:
/@types/yargs-parser/21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
+ dev: false
/@types/yargs/17.0.22:
resolution: {integrity: sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==}
dependencies:
'@types/yargs-parser': 21.0.0
+ dev: false
/@typescript-eslint/eslint-plugin/5.54.0_6mj2wypvdnknez7kws2nfdgupi:
resolution: {integrity: sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw==}
@@ -1708,6 +1756,7 @@ packages:
engines: {node: '>=8'}
dependencies:
type-fest: 0.21.3
+ dev: false
/ansi-regex/5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -1728,6 +1777,7 @@ packages:
/ansi-styles/5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
+ dev: false
/anymatch/3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
@@ -1744,6 +1794,7 @@ packages:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
+ dev: false
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1806,22 +1857,23 @@ packages:
engines: {node: '>= 0.4'}
dev: true
- /babel-jest/29.4.3_@babel+core@7.21.0:
- resolution: {integrity: sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==}
+ /babel-jest/29.5.0_@babel+core@7.21.0:
+ resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@babel/core': ^7.8.0
dependencies:
'@babel/core': 7.21.0
- '@jest/transform': 29.4.3
+ '@jest/transform': 29.5.0
'@types/babel__core': 7.20.0
babel-plugin-istanbul: 6.1.1
- babel-preset-jest: 29.4.3_@babel+core@7.21.0
+ babel-preset-jest: 29.5.0_@babel+core@7.21.0
chalk: 4.1.2
graceful-fs: 4.2.10
slash: 3.0.0
transitivePeerDependencies:
- supports-color
+ dev: false
/babel-plugin-istanbul/6.1.1:
resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
@@ -1834,15 +1886,17 @@ packages:
test-exclude: 6.0.0
transitivePeerDependencies:
- supports-color
+ dev: false
- /babel-plugin-jest-hoist/29.4.3:
- resolution: {integrity: sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==}
+ /babel-plugin-jest-hoist/29.5.0:
+ resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@babel/template': 7.20.7
'@babel/types': 7.21.2
'@types/babel__core': 7.20.0
'@types/babel__traverse': 7.18.3
+ dev: false
/babel-plugin-macros/3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
@@ -1871,16 +1925,18 @@ packages:
'@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.21.0
'@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.21.0
'@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.21.0
+ dev: false
- /babel-preset-jest/29.4.3_@babel+core@7.21.0:
- resolution: {integrity: sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==}
+ /babel-preset-jest/29.5.0_@babel+core@7.21.0:
+ resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.21.0
- babel-plugin-jest-hoist: 29.4.3
+ babel-plugin-jest-hoist: 29.5.0
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0
+ dev: false
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1912,20 +1968,15 @@ packages:
node-releases: 2.0.10
update-browserslist-db: 1.0.10_browserslist@4.21.5
- /bs-logger/0.2.6:
- resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
- engines: {node: '>= 6'}
- dependencies:
- fast-json-stable-stringify: 2.1.0
- dev: true
-
/bser/2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
dependencies:
node-int64: 0.4.0
+ dev: false
/buffer-from/1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ dev: false
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
@@ -1946,10 +1997,12 @@ packages:
/camelcase/5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
+ dev: false
/camelcase/6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
+ dev: false
/caniuse-lite/1.0.30001460:
resolution: {integrity: sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==}
@@ -1972,6 +2025,7 @@ packages:
/char-regex/1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
+ dev: false
/chokidar/3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
@@ -1991,9 +2045,11 @@ packages:
/ci-info/3.8.0:
resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==}
engines: {node: '>=8'}
+ dev: false
/cjs-module-lexer/1.2.2:
resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==}
+ dev: false
/cliui/8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
@@ -2002,6 +2058,7 @@ packages:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
+ dev: false
/clsx/1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
@@ -2011,9 +2068,11 @@ packages:
/co/4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+ dev: false
/collect-v8-coverage/1.0.1:
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
+ dev: false
/color-convert/1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -2044,6 +2103,7 @@ packages:
/convert-source-map/2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ dev: false
/cosmiconfig/7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
@@ -2086,6 +2146,7 @@ packages:
/dedent/0.7.0:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
+ dev: false
/deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -2094,6 +2155,7 @@ packages:
/deepmerge/4.3.0:
resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==}
engines: {node: '>=0.10.0'}
+ dev: false
/define-properties/1.2.0:
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
@@ -2110,6 +2172,7 @@ packages:
/detect-newline/3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
+ dev: false
/detective/5.2.1:
resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==}
@@ -2128,6 +2191,7 @@ packages:
/diff-sequences/29.4.3:
resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dev: false
/dir-glob/3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
@@ -2172,14 +2236,17 @@ packages:
/emittery/0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
engines: {node: '>=12'}
+ dev: false
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: false
/error-ex/1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
+ dev: false
/es-abstract/1.21.1:
resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==}
@@ -2285,6 +2352,7 @@ packages:
/escape-string-regexp/2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
+ dev: false
/escape-string-regexp/4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
@@ -2412,6 +2480,7 @@ packages:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
+ dev: false
/esquery/1.5.0:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
@@ -2442,6 +2511,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /events/3.3.0:
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+ engines: {node: '>=0.8.x'}
+ dev: false
+
/execa/5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -2455,20 +2529,23 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
+ dev: false
/exit/0.1.2:
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
engines: {node: '>= 0.8.0'}
+ dev: false
- /expect/29.4.3:
- resolution: {integrity: sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==}
+ /expect/29.5.0:
+ resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/expect-utils': 29.4.3
+ '@jest/expect-utils': 29.5.0
jest-get-type: 29.4.3
- jest-matcher-utils: 29.4.3
- jest-message-util: 29.4.3
- jest-util: 29.4.3
+ jest-matcher-utils: 29.5.0
+ jest-message-util: 29.5.0
+ jest-util: 29.5.0
+ dev: false
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2502,6 +2579,7 @@ packages:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
dependencies:
bser: 2.1.1
+ dev: false
/file-entry-cache/6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
@@ -2526,6 +2604,7 @@ packages:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
+ dev: false
/find-up/5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
@@ -2591,6 +2670,7 @@ packages:
/get-caller-file/2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
+ dev: false
/get-intrinsic/1.2.0:
resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==}
@@ -2603,10 +2683,12 @@ packages:
/get-package-type/0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
+ dev: false
/get-stream/6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
+ dev: false
/get-symbol-description/1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
@@ -2682,6 +2764,7 @@ packages:
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
+ dev: false
/grapheme-splitter/1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
@@ -2736,6 +2819,7 @@ packages:
/html-escaper/2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ dev: false
/html-parse-stringify/3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -2746,6 +2830,7 @@ packages:
/human-signals/2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
+ dev: false
/i18next-browser-languagedetector/7.0.1:
resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==}
@@ -2782,6 +2867,7 @@ packages:
dependencies:
pkg-dir: 4.2.0
resolve-cwd: 3.0.0
+ dev: false
/imurmurhash/0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
@@ -2815,6 +2901,7 @@ packages:
/is-arrayish/0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+ dev: false
/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
@@ -2862,10 +2949,12 @@ packages:
/is-fullwidth-code-point/3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
+ dev: false
/is-generator-fn/2.1.0:
resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
engines: {node: '>=6'}
+ dev: false
/is-glob/4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
@@ -2925,6 +3014,7 @@ packages:
/is-stream/2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
+ dev: false
/is-string/1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
@@ -2963,6 +3053,7 @@ packages:
/istanbul-lib-coverage/3.2.0:
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
engines: {node: '>=8'}
+ dev: false
/istanbul-lib-instrument/5.2.1:
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
@@ -2975,6 +3066,7 @@ packages:
semver: 6.3.0
transitivePeerDependencies:
- supports-color
+ dev: false
/istanbul-lib-report/3.0.0:
resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==}
@@ -2983,6 +3075,7 @@ packages:
istanbul-lib-coverage: 3.2.0
make-dir: 3.1.0
supports-color: 7.2.0
+ dev: false
/istanbul-lib-source-maps/4.0.1:
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
@@ -2993,6 +3086,7 @@ packages:
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
+ dev: false
/istanbul-reports/3.1.5:
resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==}
@@ -3000,42 +3094,46 @@ packages:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.0
+ dev: false
- /jest-changed-files/29.4.3:
- resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==}
+ /jest-changed-files/29.5.0:
+ resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
execa: 5.1.1
p-limit: 3.1.0
+ dev: false
- /jest-circus/29.4.3:
- resolution: {integrity: sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==}
+ /jest-circus/29.5.0:
+ resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/environment': 29.4.3
- '@jest/expect': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/environment': 29.5.0
+ '@jest/expect': 29.5.0
+ '@jest/test-result': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
chalk: 4.1.2
co: 4.6.0
dedent: 0.7.0
is-generator-fn: 2.1.0
- jest-each: 29.4.3
- jest-matcher-utils: 29.4.3
- jest-message-util: 29.4.3
- jest-runtime: 29.4.3
- jest-snapshot: 29.4.3
- jest-util: 29.4.3
+ jest-each: 29.5.0
+ jest-matcher-utils: 29.5.0
+ jest-message-util: 29.5.0
+ jest-runtime: 29.5.0
+ jest-snapshot: 29.5.0
+ jest-util: 29.5.0
p-limit: 3.1.0
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
+ pure-rand: 6.0.1
slash: 3.0.0
stack-utils: 2.0.6
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-cli/29.4.3_@types+node@18.14.6:
- resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==}
+ /jest-cli/29.5.0_@types+node@18.14.6:
+ resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
@@ -3044,25 +3142,26 @@ packages:
node-notifier:
optional: true
dependencies:
- '@jest/core': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/core': 29.5.0
+ '@jest/test-result': 29.5.0
+ '@jest/types': 29.5.0
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.10
import-local: 3.1.0
- jest-config: 29.4.3_@types+node@18.14.6
- jest-util: 29.4.3
- jest-validate: 29.4.3
+ jest-config: 29.5.0_@types+node@18.14.6
+ jest-util: 29.5.0
+ jest-validate: 29.5.0
prompts: 2.4.2
yargs: 17.7.1
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
+ dev: false
- /jest-config/29.4.3_@types+node@18.14.6:
- resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==}
+ /jest-config/29.5.0_@types+node@18.14.6:
+ resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@types/node': '*'
@@ -3074,128 +3173,139 @@ packages:
optional: true
dependencies:
'@babel/core': 7.21.0
- '@jest/test-sequencer': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/test-sequencer': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
- babel-jest: 29.4.3_@babel+core@7.21.0
+ babel-jest: 29.5.0_@babel+core@7.21.0
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.0
glob: 7.2.3
graceful-fs: 4.2.10
- jest-circus: 29.4.3
- jest-environment-node: 29.4.3
+ jest-circus: 29.5.0
+ jest-environment-node: 29.5.0
jest-get-type: 29.4.3
jest-regex-util: 29.4.3
- jest-resolve: 29.4.3
- jest-runner: 29.4.3
- jest-util: 29.4.3
- jest-validate: 29.4.3
+ jest-resolve: 29.5.0
+ jest-runner: 29.5.0
+ jest-util: 29.5.0
+ jest-validate: 29.5.0
micromatch: 4.0.5
parse-json: 5.2.0
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
slash: 3.0.0
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-diff/29.4.3:
- resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==}
+ /jest-diff/29.5.0:
+ resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
chalk: 4.1.2
diff-sequences: 29.4.3
jest-get-type: 29.4.3
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
+ dev: false
/jest-docblock/29.4.3:
resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
detect-newline: 3.1.0
+ dev: false
- /jest-each/29.4.3:
- resolution: {integrity: sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==}
+ /jest-each/29.5.0:
+ resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
chalk: 4.1.2
jest-get-type: 29.4.3
- jest-util: 29.4.3
- pretty-format: 29.4.3
+ jest-util: 29.5.0
+ pretty-format: 29.5.0
+ dev: false
- /jest-environment-node/29.4.3:
- resolution: {integrity: sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==}
+ /jest-environment-node/29.5.0:
+ resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/environment': 29.4.3
- '@jest/fake-timers': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/environment': 29.5.0
+ '@jest/fake-timers': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
- jest-mock: 29.4.3
- jest-util: 29.4.3
+ jest-mock: 29.5.0
+ jest-util: 29.5.0
+ dev: false
/jest-get-type/29.4.3:
resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dev: false
- /jest-haste-map/29.4.3:
- resolution: {integrity: sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==}
+ /jest-haste-map/29.5.0:
+ resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@types/graceful-fs': 4.1.6
'@types/node': 18.14.6
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.10
jest-regex-util: 29.4.3
- jest-util: 29.4.3
- jest-worker: 29.4.3
+ jest-util: 29.5.0
+ jest-worker: 29.5.0
micromatch: 4.0.5
walker: 1.0.8
optionalDependencies:
fsevents: 2.3.2
+ dev: false
- /jest-leak-detector/29.4.3:
- resolution: {integrity: sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==}
+ /jest-leak-detector/29.5.0:
+ resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
jest-get-type: 29.4.3
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
+ dev: false
- /jest-matcher-utils/29.4.3:
- resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==}
+ /jest-matcher-utils/29.5.0:
+ resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
chalk: 4.1.2
- jest-diff: 29.4.3
+ jest-diff: 29.5.0
jest-get-type: 29.4.3
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
+ dev: false
- /jest-message-util/29.4.3:
- resolution: {integrity: sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==}
+ /jest-message-util/29.5.0:
+ resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@babel/code-frame': 7.18.6
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@types/stack-utils': 2.0.1
chalk: 4.1.2
graceful-fs: 4.2.10
micromatch: 4.0.5
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
slash: 3.0.0
stack-utils: 2.0.6
+ dev: false
- /jest-mock/29.4.3:
- resolution: {integrity: sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==}
+ /jest-mock/29.5.0:
+ resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
- jest-util: 29.4.3
+ jest-util: 29.5.0
+ dev: false
- /jest-pnp-resolver/1.2.3_jest-resolve@29.4.3:
+ /jest-pnp-resolver/1.2.3_jest-resolve@29.5.0:
resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==}
engines: {node: '>=6'}
peerDependencies:
@@ -3204,94 +3314,100 @@ packages:
jest-resolve:
optional: true
dependencies:
- jest-resolve: 29.4.3
+ jest-resolve: 29.5.0
+ dev: false
/jest-regex-util/29.4.3:
resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dev: false
- /jest-resolve-dependencies/29.4.3:
- resolution: {integrity: sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==}
+ /jest-resolve-dependencies/29.5.0:
+ resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
jest-regex-util: 29.4.3
- jest-snapshot: 29.4.3
+ jest-snapshot: 29.5.0
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-resolve/29.4.3:
- resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==}
+ /jest-resolve/29.5.0:
+ resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
chalk: 4.1.2
graceful-fs: 4.2.10
- jest-haste-map: 29.4.3
- jest-pnp-resolver: 1.2.3_jest-resolve@29.4.3
- jest-util: 29.4.3
- jest-validate: 29.4.3
+ jest-haste-map: 29.5.0
+ jest-pnp-resolver: 1.2.3_jest-resolve@29.5.0
+ jest-util: 29.5.0
+ jest-validate: 29.5.0
resolve: 1.22.1
resolve.exports: 2.0.0
slash: 3.0.0
+ dev: false
- /jest-runner/29.4.3:
- resolution: {integrity: sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==}
+ /jest-runner/29.5.0:
+ resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/console': 29.4.3
- '@jest/environment': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/transform': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/console': 29.5.0
+ '@jest/environment': 29.5.0
+ '@jest/test-result': 29.5.0
+ '@jest/transform': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.10
jest-docblock: 29.4.3
- jest-environment-node: 29.4.3
- jest-haste-map: 29.4.3
- jest-leak-detector: 29.4.3
- jest-message-util: 29.4.3
- jest-resolve: 29.4.3
- jest-runtime: 29.4.3
- jest-util: 29.4.3
- jest-watcher: 29.4.3
- jest-worker: 29.4.3
+ jest-environment-node: 29.5.0
+ jest-haste-map: 29.5.0
+ jest-leak-detector: 29.5.0
+ jest-message-util: 29.5.0
+ jest-resolve: 29.5.0
+ jest-runtime: 29.5.0
+ jest-util: 29.5.0
+ jest-watcher: 29.5.0
+ jest-worker: 29.5.0
p-limit: 3.1.0
source-map-support: 0.5.13
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-runtime/29.4.3:
- resolution: {integrity: sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==}
+ /jest-runtime/29.5.0:
+ resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/environment': 29.4.3
- '@jest/fake-timers': 29.4.3
- '@jest/globals': 29.4.3
+ '@jest/environment': 29.5.0
+ '@jest/fake-timers': 29.5.0
+ '@jest/globals': 29.5.0
'@jest/source-map': 29.4.3
- '@jest/test-result': 29.4.3
- '@jest/transform': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/test-result': 29.5.0
+ '@jest/transform': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
chalk: 4.1.2
cjs-module-lexer: 1.2.2
collect-v8-coverage: 1.0.1
glob: 7.2.3
graceful-fs: 4.2.10
- jest-haste-map: 29.4.3
- jest-message-util: 29.4.3
- jest-mock: 29.4.3
+ jest-haste-map: 29.5.0
+ jest-message-util: 29.5.0
+ jest-mock: 29.5.0
jest-regex-util: 29.4.3
- jest-resolve: 29.4.3
- jest-snapshot: 29.4.3
- jest-util: 29.4.3
+ jest-resolve: 29.5.0
+ jest-snapshot: 29.5.0
+ jest-util: 29.5.0
slash: 3.0.0
strip-bom: 4.0.0
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-snapshot/29.4.3:
- resolution: {integrity: sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==}
+ /jest-snapshot/29.5.0:
+ resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@babel/core': 7.21.0
@@ -3300,73 +3416,77 @@ packages:
'@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.21.0
'@babel/traverse': 7.21.2
'@babel/types': 7.21.2
- '@jest/expect-utils': 29.4.3
- '@jest/transform': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/expect-utils': 29.5.0
+ '@jest/transform': 29.5.0
+ '@jest/types': 29.5.0
'@types/babel__traverse': 7.18.3
'@types/prettier': 2.7.2
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0
chalk: 4.1.2
- expect: 29.4.3
+ expect: 29.5.0
graceful-fs: 4.2.10
- jest-diff: 29.4.3
+ jest-diff: 29.5.0
jest-get-type: 29.4.3
- jest-haste-map: 29.4.3
- jest-matcher-utils: 29.4.3
- jest-message-util: 29.4.3
- jest-util: 29.4.3
+ jest-matcher-utils: 29.5.0
+ jest-message-util: 29.5.0
+ jest-util: 29.5.0
natural-compare: 1.4.0
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
semver: 7.3.8
transitivePeerDependencies:
- supports-color
+ dev: false
- /jest-util/29.4.3:
- resolution: {integrity: sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==}
+ /jest-util/29.5.0:
+ resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
chalk: 4.1.2
ci-info: 3.8.0
graceful-fs: 4.2.10
picomatch: 2.3.1
+ dev: false
- /jest-validate/29.4.3:
- resolution: {integrity: sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==}
+ /jest-validate/29.5.0:
+ resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/types': 29.4.3
+ '@jest/types': 29.5.0
camelcase: 6.3.0
chalk: 4.1.2
jest-get-type: 29.4.3
leven: 3.1.0
- pretty-format: 29.4.3
+ pretty-format: 29.5.0
+ dev: false
- /jest-watcher/29.4.3:
- resolution: {integrity: sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==}
+ /jest-watcher/29.5.0:
+ resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@jest/test-result': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/test-result': 29.5.0
+ '@jest/types': 29.5.0
'@types/node': 18.14.6
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
- jest-util: 29.4.3
+ jest-util: 29.5.0
string-length: 4.0.2
+ dev: false
- /jest-worker/29.4.3:
- resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==}
+ /jest-worker/29.5.0:
+ resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@types/node': 18.14.6
- jest-util: 29.4.3
+ jest-util: 29.5.0
merge-stream: 2.0.0
supports-color: 8.1.1
+ dev: false
- /jest/29.4.3_@types+node@18.14.6:
- resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==}
+ /jest/29.5.0_@types+node@18.14.6:
+ resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
@@ -3375,14 +3495,15 @@ packages:
node-notifier:
optional: true
dependencies:
- '@jest/core': 29.4.3
- '@jest/types': 29.4.3
+ '@jest/core': 29.5.0
+ '@jest/types': 29.5.0
import-local: 3.1.0
- jest-cli: 29.4.3_@types+node@18.14.6
+ jest-cli: 29.5.0_@types+node@18.14.6
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
+ dev: false
/js-sdsl/4.3.0:
resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
@@ -3397,6 +3518,7 @@ packages:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
+ dev: false
/js-yaml/4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
@@ -3412,6 +3534,7 @@ packages:
/json-parse-even-better-errors/2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+ dev: false
/json-schema-traverse/0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -3437,10 +3560,12 @@ packages:
/kleur/3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
+ dev: false
/leven/3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
+ dev: false
/levn/0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
@@ -3457,12 +3582,14 @@ packages:
/lines-and-columns/1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ dev: false
/locate-path/5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
dependencies:
p-locate: 4.1.0
+ dev: false
/locate-path/6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
@@ -3471,10 +3598,6 @@ packages:
p-locate: 5.0.0
dev: true
- /lodash.memoize/4.1.2:
- resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
- dev: true
-
/lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
@@ -3512,18 +3635,17 @@ packages:
engines: {node: '>=8'}
dependencies:
semver: 6.3.0
-
- /make-error/1.3.6:
- resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
- dev: true
+ dev: false
/makeerror/1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
dependencies:
tmpl: 1.0.5
+ dev: false
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: false
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
@@ -3540,6 +3662,7 @@ packages:
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
+ dev: false
/minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -3574,6 +3697,7 @@ packages:
/node-int64/0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+ dev: false
/node-releases/2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
@@ -3592,6 +3716,7 @@ packages:
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
+ dev: false
/object-assign/4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -3665,6 +3790,7 @@ packages:
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
+ dev: false
/optionator/0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
@@ -3683,6 +3809,7 @@ packages:
engines: {node: '>=6'}
dependencies:
p-try: 2.2.0
+ dev: false
/p-limit/3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
@@ -3695,6 +3822,7 @@ packages:
engines: {node: '>=8'}
dependencies:
p-limit: 2.3.0
+ dev: false
/p-locate/5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
@@ -3706,6 +3834,7 @@ packages:
/p-try/2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
+ dev: false
/parent-module/1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@@ -3721,6 +3850,7 @@ packages:
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
+ dev: false
/path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
@@ -3756,12 +3886,14 @@ packages:
/pirates/4.0.5:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
engines: {node: '>= 6'}
+ dev: false
/pkg-dir/4.2.0:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
dependencies:
find-up: 4.1.0
+ dev: false
/postcss-import/14.1.0_postcss@8.4.21:
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
@@ -3899,13 +4031,14 @@ packages:
hasBin: true
dev: true
- /pretty-format/29.4.3:
- resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==}
+ /pretty-format/29.5.0:
+ resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/schemas': 29.4.3
ansi-styles: 5.2.0
react-is: 18.2.0
+ dev: false
/prompts/2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
@@ -3913,6 +4046,7 @@ packages:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
+ dev: false
/prop-types/15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -3926,6 +4060,10 @@ packages:
engines: {node: '>=6'}
dev: true
+ /pure-rand/6.0.1:
+ resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==}
+ dev: false
+
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@@ -3980,6 +4118,7 @@ packages:
/react-is/18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+ dev: false
/react-redux/8.0.5_ctrls2ti7t7iutxbwkm5ipogyy:
resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==}
@@ -4122,6 +4261,7 @@ packages:
/require-directory/2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
+ dev: false
/reselect/4.1.7:
resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==}
@@ -4132,6 +4272,7 @@ packages:
engines: {node: '>=8'}
dependencies:
resolve-from: 5.0.0
+ dev: false
/resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
@@ -4140,10 +4281,12 @@ packages:
/resolve-from/5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
+ dev: false
/resolve.exports/2.0.0:
resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==}
engines: {node: '>=10'}
+ dev: false
/resolve/1.22.1:
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
@@ -4245,9 +4388,11 @@ packages:
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: false
/sisteransi/1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+ dev: false
/slash/3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
@@ -4292,6 +4437,7 @@ packages:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
+ dev: false
/source-map/0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
@@ -4301,15 +4447,18 @@ packages:
/source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ dev: false
/sprintf-js/1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+ dev: false
/stack-utils/2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
dependencies:
escape-string-regexp: 2.0.0
+ dev: false
/string-length/4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
@@ -4317,6 +4466,7 @@ packages:
dependencies:
char-regex: 1.0.2
strip-ansi: 6.0.1
+ dev: false
/string-width/4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
@@ -4325,6 +4475,7 @@ packages:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
+ dev: false
/string.prototype.matchall/4.0.8:
resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==}
@@ -4364,10 +4515,12 @@ packages:
/strip-bom/4.0.0:
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
engines: {node: '>=8'}
+ dev: false
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
+ dev: false
/strip-json-comments/3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
@@ -4394,6 +4547,7 @@ packages:
engines: {node: '>=10'}
dependencies:
has-flag: 4.0.0
+ dev: false
/supports-preserve-symlinks-flag/1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
@@ -4440,6 +4594,7 @@ packages:
'@istanbuljs/schema': 0.1.3
glob: 7.2.3
minimatch: 3.1.2
+ dev: false
/text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -4455,6 +4610,7 @@ packages:
/tmpl/1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
+ dev: false
/to-fast-properties/2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
@@ -4466,39 +4622,6 @@ packages:
dependencies:
is-number: 7.0.0
- /ts-jest/29.0.5_orzzknleilowtsz34rkaotjvzm:
- resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
- hasBin: true
- peerDependencies:
- '@babel/core': '>=7.0.0-beta.0 <8'
- '@jest/types': ^29.0.0
- babel-jest: ^29.0.0
- esbuild: '*'
- jest: ^29.0.0
- typescript: '>=4.3'
- peerDependenciesMeta:
- '@babel/core':
- optional: true
- '@jest/types':
- optional: true
- babel-jest:
- optional: true
- esbuild:
- optional: true
- dependencies:
- bs-logger: 0.2.6
- fast-json-stable-stringify: 2.1.0
- jest: 29.4.3_@types+node@18.14.6
- jest-util: 29.4.3
- json5: 2.2.3
- lodash.memoize: 4.1.2
- make-error: 1.3.6
- semver: 7.3.8
- typescript: 4.9.5
- yargs-parser: 21.1.1
- dev: true
-
/ts-results/3.3.0:
resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==}
dev: false
@@ -4531,6 +4654,7 @@ packages:
/type-detect/4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
+ dev: false
/type-fest/0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
@@ -4540,6 +4664,7 @@ packages:
/type-fest/0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
+ dev: false
/typed-array-length/1.0.4:
resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
@@ -4555,6 +4680,11 @@ packages:
hasBin: true
dev: true
+ /ulid/2.3.0:
+ resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
+ hasBin: true
+ dev: false
+
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
@@ -4603,6 +4733,7 @@ packages:
'@jridgewell/trace-mapping': 0.3.17
'@types/istanbul-lib-coverage': 2.0.4
convert-source-map: 1.9.0
+ dev: false
/vite/4.1.4_@types+node@18.14.6:
resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==}
@@ -4647,6 +4778,7 @@ packages:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
dependencies:
makeerror: 1.0.12
+ dev: false
/which-boxed-primitive/1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
@@ -4689,6 +4821,7 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
+ dev: false
/wrappy/1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -4699,6 +4832,7 @@ packages:
dependencies:
imurmurhash: 0.1.4
signal-exit: 3.0.7
+ dev: false
/xtend/4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
@@ -4708,6 +4842,7 @@ packages:
/y18n/5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
+ dev: false
/yallist/3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -4722,6 +4857,7 @@ packages:
/yargs-parser/21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
+ dev: false
/yargs/17.7.1:
resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==}
@@ -4734,6 +4870,7 @@ packages:
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
+ dev: false
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts
deleted file mode 100644
index 8fb8610b77..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { BlockInterface, BlockType } from '$app/interfaces/index';
-
-
-export class BlockDataManager {
- private head: BlockInterface | null = null;
- constructor(id: string, private map: Record> | null) {
- if (!map) return;
- this.head = map[id];
- }
-
- setBlocksMap = (id: string, map: Record>) => {
- this.map = map;
- this.head = map[id];
- }
-
- /**
- * get block data
- * @param blockId string
- * @returns Block
- */
- getBlock = (blockId: string) => {
- return this.map?.[blockId] || null;
- }
-
- destroy() {
- this.map = null;
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
new file mode 100644
index 0000000000..de42c3c373
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
@@ -0,0 +1,71 @@
+import { BaseEditor, BaseSelection, Descendant } from "slate";
+import { TreeNode } from '$app/block_editor/view/tree_node';
+import { Operation } from "$app/block_editor/core/operation";
+import { TextBlockSelectionManager } from './text_selection';
+
+export class TextBlockManager {
+ public selectionManager: TextBlockSelectionManager;
+ constructor(private operation: Operation) {
+ this.selectionManager = new TextBlockSelectionManager();
+ }
+
+ setSelection(node: TreeNode, selection: BaseSelection) {
+ // console.log(node.id, selection);
+ this.selectionManager.setSelection(node.id, selection)
+ }
+
+ update(node: TreeNode, path: string[], data: Descendant[]) {
+ this.operation.updateNode(node.id, path, data);
+ }
+
+ splitNode(node: TreeNode, editor: BaseEditor) {
+ const focus = editor.selection?.focus;
+ const path = focus?.path || [0, editor.children.length - 1];
+ const offset = focus?.offset || 0;
+ const parentIndex = path[0];
+ const index = path[1];
+ const editorNode = editor.children[parentIndex];
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const children: { [key: string]: boolean | string; text: string }[] = editorNode.children;
+ const retainItems = children.filter((_: any, i: number) => i < index);
+ const splitItem: { [key: string]: boolean | string } = children[index];
+ const text = splitItem.text.toString();
+ const prevText = text.substring(0, offset);
+ const afterText = text.substring(offset);
+ retainItems.push({
+ ...splitItem,
+ text: prevText
+ });
+
+ const removeItems = children.filter((_: any, i: number) => i > index);
+
+ const data = {
+ type: node.type,
+ data: {
+ ...node.data,
+ content: [
+ {
+ ...splitItem,
+ text: afterText
+ },
+ ...removeItems
+ ]
+ }
+ };
+
+ const newBlock = this.operation.splitNode(node.id, {
+ path: ['data', 'content'],
+ value: retainItems,
+ }, data);
+ newBlock && this.selectionManager.focusStart(newBlock.id);
+ }
+
+ destroy() {
+ this.selectionManager.destroy();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.operation = null;
+ }
+
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
new file mode 100644
index 0000000000..b25d7f6268
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
@@ -0,0 +1,35 @@
+export class TextBlockSelectionManager {
+ private focusId = '';
+ private selection?: any;
+
+ getFocusSelection() {
+ return {
+ focusId: this.focusId,
+ selection: this.selection
+ }
+ }
+
+ focusStart(blockId: string) {
+ this.focusId = blockId;
+ this.setSelection(blockId, {
+ focus: {
+ path: [0, 0],
+ offset: 0,
+ },
+ anchor: {
+ path: [0, 0],
+ offset: 0,
+ },
+ })
+ }
+
+ setSelection(blockId: string, selection: any) {
+ this.focusId = blockId;
+ this.selection = selection;
+ }
+
+ destroy() {
+ this.focusId = '';
+ this.selection = undefined;
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
new file mode 100644
index 0000000000..c550213daa
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
@@ -0,0 +1,107 @@
+import { BlockType, BlockData } from '$app/interfaces/index';
+import { generateBlockId } from '$app/utils/block';
+
+/**
+ * Represents a single block of content in a document.
+ */
+export class Block {
+ id: string;
+ type: T;
+ data: BlockData;
+ parent: Block | null = null; // Pointer to the parent block
+ prev: Block | null = null; // Pointer to the previous sibling block
+ next: Block | null = null; // Pointer to the next sibling block
+ firstChild: Block | null = null; // Pointer to the first child block
+
+ constructor(id: string, type: T, data: BlockData) {
+ this.id = id;
+ this.type = type;
+ this.data = data;
+ }
+
+ /**
+ * Adds a new child block to the beginning of the current block's children list.
+ *
+ * @param {Object} content - The content of the new block, including its type and data.
+ * @param {string} content.type - The type of the new block.
+ * @param {Object} content.data - The data associated with the new block.
+ * @returns {Block} The newly created child block.
+ */
+ prependChild(content: { type: T, data: BlockData }): Block | null {
+ const id = generateBlockId();
+ const newBlock = new Block(id, content.type, content.data);
+ newBlock.reposition(this, null);
+ return newBlock;
+ }
+
+ /**
+ * Add a new sibling block after this block.
+ *
+ * @param content The type and data for the new sibling block.
+ * @returns The newly created sibling block.
+ */
+ addSibling(content: { type: T, data: BlockData }): Block | null {
+ const id = generateBlockId();
+ const newBlock = new Block(id, content.type, content.data);
+ newBlock.reposition(this.parent, this);
+ return newBlock;
+ }
+
+ /**
+ * Remove this block and its descendants from the tree.
+ *
+ */
+ remove() {
+ this.detach();
+ let child = this.firstChild;
+ while (child) {
+ const next = child.next;
+ child.remove();
+ child = next;
+ }
+ }
+
+ reposition(newParent: Block | null, newPrev: Block | null) {
+ // Update the block's parent and siblings
+ this.parent = newParent;
+ this.prev = newPrev;
+ this.next = null;
+
+ if (newParent) {
+ const prev = newPrev;
+ if (!prev) {
+ const next = newParent.firstChild;
+ newParent.firstChild = this;
+ if (next) {
+ this.next = next;
+ next.prev = this;
+ }
+
+ } else {
+ // Update the next and prev pointers of the newPrev and next blocks
+ if (prev.next !== this) {
+ const next = prev.next;
+ if (next) {
+ next.prev = this
+ this.next = next;
+ }
+ prev.next = this;
+ }
+ }
+
+ }
+ }
+
+ // detach the block from its current position in the tree
+ detach() {
+ if (this.prev) {
+ this.prev.next = this.next;
+ } else if (this.parent) {
+ this.parent.firstChild = this.next;
+ }
+ if (this.next) {
+ this.next.prev = this.prev;
+ }
+ }
+
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
new file mode 100644
index 0000000000..877f3592df
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
@@ -0,0 +1,225 @@
+import { BlockData, BlockInterface, BlockType } from '$app/interfaces/index';
+import { set } from '../../utils/tool';
+import { Block } from './block';
+export interface BlockChangeProps {
+ block?: Block,
+ startBlock?: Block,
+ endBlock?: Block,
+ oldParentId?: string,
+ oldPrevId?: string
+}
+export class BlockChain {
+ private map: Map> = new Map();
+ public head: Block | null = null;
+
+ constructor(private onBlockChange: (command: string, data: BlockChangeProps) => void) {
+
+ }
+ /**
+ * generate blocks from doc data
+ * @param id doc id
+ * @param map doc data
+ */
+ rebuild = (id: string, map: Record>) => {
+ this.map.clear();
+ this.head = this.createBlock(id, map[id].type, map[id].data);
+
+ const callback = (block: Block) => {
+ const firstChildId = map[block.id].firstChild;
+ const nextId = map[block.id].next;
+ if (!block.firstChild && firstChildId) {
+ block.firstChild = this.createBlock(firstChildId, map[firstChildId].type, map[firstChildId].data);
+ block.firstChild.parent = block;
+ block.firstChild.prev = null;
+ }
+ if (!block.next && nextId) {
+ block.next = this.createBlock(nextId, map[nextId].type, map[nextId].data);
+ block.next.parent = block.parent;
+ block.next.prev = block;
+ }
+ }
+ this.traverse(callback);
+ }
+
+ /**
+ * Traversing the block list from front to back
+ * @param callback It will be call when the block visited
+ * @param block block item, it will be equal head node when the block item is undefined
+ */
+ traverse(callback: (_block: Block) => void, block?: Block) {
+ let currentBlock: Block | null = block || this.head;
+ while (currentBlock) {
+ callback(currentBlock);
+ if (currentBlock.firstChild) {
+ this.traverse(callback, currentBlock.firstChild);
+ }
+ currentBlock = currentBlock.next;
+ }
+ }
+
+ /**
+ * get block data
+ * @param blockId string
+ * @returns Block
+ */
+ getBlock = (blockId: string) => {
+ return this.map.get(blockId) || null;
+ }
+
+ destroy() {
+ this.map.clear();
+ this.head = null;
+ this.onBlockChange = () => null;
+ }
+
+ /**
+ * Adds a new child block to the beginning of the current block's children list.
+ *
+ * @param {string} parentId
+ * @param {Object} content - The content of the new block, including its type and data.
+ * @param {string} content.type - The type of the new block.
+ * @param {Object} content.data - The data associated with the new block.
+ * @returns {Block} The newly created child block.
+ */
+ prependChild(blockId: string, content: { type: BlockType, data: BlockData }): Block | null {
+ const parent = this.getBlock(blockId);
+ if (!parent) return null;
+ const newBlock = parent.prependChild(content);
+
+ if (newBlock) {
+ this.map.set(newBlock?.id, newBlock);
+ this.onBlockChange('insert', { block: newBlock });
+ }
+
+ return newBlock;
+ }
+
+ /**
+ * Add a new sibling block after this block.
+ * @param {string} blockId
+ * @param content The type and data for the new sibling block.
+ * @returns The newly created sibling block.
+ */
+ addSibling(blockId: string, content: { type: BlockType, data: BlockData }): Block | null {
+ const block = this.getBlock(blockId);
+ if (!block) return null;
+ const newBlock = block.addSibling(content);
+ if (newBlock) {
+ this.map.set(newBlock?.id, newBlock);
+ this.onBlockChange('insert', { block: newBlock });
+ }
+ return newBlock;
+ }
+
+ /**
+ * Remove this block and its descendants from the tree.
+ * @param {string} blockId
+ */
+ remove(blockId: string) {
+ const block = this.getBlock(blockId);
+ if (!block) return;
+ block.remove();
+ this.map.delete(block.id);
+ this.onBlockChange('delete', { block });
+ return block;
+ }
+
+ /**
+ * Move this block to a new position in the tree.
+ * @param {string} blockId
+ * @param newParentId The new parent block of this block. If null, the block becomes a top-level block.
+ * @param newPrevId The new previous sibling block of this block. If null, the block becomes the first child of the new parent.
+ * @returns This block after it has been moved.
+ */
+ move(blockId: string, newParentId: string, newPrevId: string): Block | null {
+ const block = this.getBlock(blockId);
+ if (!block) return null;
+ const oldParentId = block.parent?.id;
+ const oldPrevId = block.prev?.id;
+ block.detach();
+ const newParent = this.getBlock(newParentId);
+ const newPrev = this.getBlock(newPrevId);
+ block.reposition(newParent, newPrev);
+ this.onBlockChange('move', {
+ block,
+ oldParentId,
+ oldPrevId
+ });
+ return block;
+ }
+
+ updateBlock(id: string, data: { path: string[], value: any }) {
+ const block = this.getBlock(id);
+ if (!block) return null;
+
+ set(block, data.path, data.value);
+ this.onBlockChange('update', {
+ block
+ });
+ return block;
+ }
+
+
+ moveBulk(startBlockId: string, endBlockId: string, newParentId: string, newPrevId: string): [Block, Block] | null {
+ const startBlock = this.getBlock(startBlockId);
+ const endBlock = this.getBlock(endBlockId);
+ if (!startBlock || !endBlock) return null;
+
+ if (startBlockId === endBlockId) {
+ const block = this.move(startBlockId, newParentId, '');
+ if (!block) return null;
+ return [block, block];
+ }
+
+ const oldParent = startBlock.parent;
+ const prev = startBlock.prev;
+ const newParent = this.getBlock(newParentId);
+ if (!oldParent || !newParent) return null;
+
+ if (oldParent.firstChild === startBlock) {
+ oldParent.firstChild = endBlock.next;
+ } else if (prev) {
+ prev.next = endBlock.next;
+ }
+ startBlock.prev = null;
+ endBlock.next = null;
+
+ startBlock.parent = newParent;
+ endBlock.parent = newParent;
+ const newPrev = this.getBlock(newPrevId);
+ if (!newPrev) {
+ const firstChild = newParent.firstChild;
+ newParent.firstChild = startBlock;
+ if (firstChild) {
+ endBlock.next = firstChild;
+ firstChild.prev = endBlock;
+ }
+ } else {
+ const next = newPrev.next;
+ newPrev.next = startBlock;
+ endBlock.next = next;
+ if (next) {
+ next.prev = endBlock;
+ }
+ }
+
+ this.onBlockChange('move', {
+ startBlock,
+ endBlock,
+ oldParentId: oldParent.id,
+ oldPrevId: prev?.id
+ });
+
+ return [
+ startBlock,
+ endBlock
+ ];
+ }
+
+
+ private createBlock(id: string, type: BlockType, data: BlockData) {
+ const block = new Block(id, type, data);
+ this.map.set(id, block);
+ return block;
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
new file mode 100644
index 0000000000..0c5c0b3190
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
@@ -0,0 +1,16 @@
+import { BackendOp, LocalOp } from "$app/interfaces";
+
+export class OpAdapter {
+
+ toBackendOp(localOp: LocalOp): BackendOp {
+ const backendOp: BackendOp = { ...localOp };
+ // switch localOp type and generate backendOp
+ return backendOp;
+ }
+
+ toLocalOp(backendOp: BackendOp): LocalOp {
+ const localOp: LocalOp = { ...backendOp };
+ // switch backendOp type and generate localOp
+ return localOp;
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
new file mode 100644
index 0000000000..38f3a3fb76
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
@@ -0,0 +1,153 @@
+import { BlockChain } from './block_chain';
+import { BlockInterface, BlockType, InsertOpData, LocalOp, UpdateOpData, moveOpData, moveRangeOpData, removeOpData, BlockData } from '$app/interfaces';
+import { BlockEditorSync } from './sync';
+import { Block } from './block';
+
+export class Operation {
+ private sync: BlockEditorSync;
+ constructor(private blockChain: BlockChain) {
+ this.sync = new BlockEditorSync();
+ }
+
+
+ splitNode(
+ retainId: string,
+ retainData: { path: string[], value: any },
+ newBlockData: {
+ type: BlockType;
+ data: BlockData
+ }) {
+ const ops: {
+ type: LocalOp['type'];
+ data: LocalOp['data'];
+ }[] = [];
+ const newBlock = this.blockChain.addSibling(retainId, newBlockData);
+ const parentId = newBlock?.parent?.id;
+ const retainBlock = this.blockChain.getBlock(retainId);
+ if (!newBlock || !parentId || !retainBlock) return null;
+
+ const insertOp = this.getInsertNodeOp({
+ id: newBlock.id,
+ next: newBlock.next?.id || null,
+ firstChild: newBlock.firstChild?.id || null,
+ data: newBlock.data,
+ type: newBlock.type,
+ }, parentId, retainId);
+
+ const updateOp = this.getUpdateNodeOp(retainId, retainData.path, retainData.value);
+ this.blockChain.updateBlock(retainId, retainData);
+
+ ops.push(insertOp, updateOp);
+ const startBlock = retainBlock.firstChild;
+ if (startBlock) {
+ const startBlockId = startBlock.id;
+ let next: Block | null = startBlock.next;
+ let endBlockId = startBlockId;
+ while (next) {
+ endBlockId = next.id;
+ next = next.next;
+ }
+
+ const moveOp = this.getMoveRangeOp([startBlockId, endBlockId], newBlock.id);
+ this.blockChain.moveBulk(startBlockId, endBlockId, newBlock.id, '');
+ ops.push(moveOp);
+ }
+
+ this.sync.sendOps(ops);
+
+ return newBlock;
+ }
+
+ updateNode(blockId: string, path: string[], value: T) {
+ const op = this.getUpdateNodeOp(blockId, path, value);
+ this.blockChain.updateBlock(blockId, {
+ path,
+ value
+ });
+ this.sync.sendOps([op]);
+ }
+ private getUpdateNodeOp(blockId: string, path: string[], value: T): {
+ type: 'update',
+ data: UpdateOpData
+ } {
+ return {
+ type: 'update',
+ data: {
+ blockId,
+ path: path,
+ value
+ }
+ };
+ }
+
+ private getInsertNodeOp(block: T, parentId: string, prevId?: string): {
+ type: 'insert';
+ data: InsertOpData
+ } {
+ return {
+ type: 'insert',
+ data: {
+ block,
+ parentId,
+ prevId
+ }
+ }
+ }
+
+ private getMoveRangeOp(range: [string, string], newParentId: string, newPrevId?: string): {
+ type: 'move_range',
+ data: moveRangeOpData
+ } {
+ return {
+ type: 'move_range',
+ data: {
+ range,
+ newParentId,
+ newPrevId,
+ }
+ }
+ }
+
+ private getMoveOp(blockId: string, newParentId: string, newPrevId?: string): {
+ type: 'move',
+ data: moveOpData
+ } {
+ return {
+ type: 'move',
+ data: {
+ blockId,
+ newParentId,
+ newPrevId
+ }
+ }
+ }
+
+ private getRemoveOp(blockId: string): {
+ type: 'remove'
+ data: removeOpData
+ } {
+ return {
+ type: 'remove',
+ data: {
+ blockId
+ }
+ }
+ }
+
+ applyOperation(op: LocalOp) {
+ switch (op.type) {
+ case 'insert':
+
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ destroy() {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.blockChain = null;
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
new file mode 100644
index 0000000000..24070c0cd5
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
@@ -0,0 +1,48 @@
+import { BackendOp, LocalOp } from '$app/interfaces';
+import { OpAdapter } from './op_adapter';
+
+/**
+ * BlockEditorSync is a class that synchronizes changes made to a block chain with a server.
+ * It allows for adding, removing, and moving blocks in the chain, and sends pending operations to the server.
+ */
+export class BlockEditorSync {
+ private version = 0;
+ private opAdapter: OpAdapter;
+ private pendingOps: BackendOp[] = [];
+ private appliedOps: LocalOp[] = [];
+
+ constructor() {
+ this.opAdapter = new OpAdapter();
+ }
+
+ private applyOp(op: BackendOp): void {
+ const localOp = this.opAdapter.toLocalOp(op);
+ this.appliedOps.push(localOp);
+ }
+
+ private receiveOps(ops: BackendOp[]): void {
+ // Apply the incoming operations to the local document
+ ops.sort((a, b) => a.version - b.version);
+ for (const op of ops) {
+ this.applyOp(op);
+ }
+ }
+
+ private resolveConflict(): void {
+ // Implement conflict resolution logic here
+ }
+
+ public sendOps(ops: {
+ type: LocalOp["type"];
+ data: LocalOp["data"]
+ }[]) {
+ const backendOps = ops.map(op => this.opAdapter.toBackendOp({
+ ...op,
+ version: this.version
+ }));
+ this.pendingOps.push(...backendOps);
+ // Send the pending operations to the server
+ console.log('==== sync pending ops ====', [...this.pendingOps]);
+ }
+
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
index 01f49f656b..658b284906 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
@@ -1,48 +1,60 @@
+// Import dependencies
import { BlockInterface } from '../interfaces';
-import { BlockDataManager } from './block';
-import { TreeManager } from './tree';
+import { BlockChain, BlockChangeProps } from './core/block_chain';
+import { RenderTree } from './view/tree';
+import { Operation } from './core/operation';
/**
- * BlockEditor is a document data manager that operates on and renders data through managing blockData and RenderTreeManager.
- * The render tree will be re-render and update react component when block makes changes to the data.
- * RectManager updates the cache of node rect when the react component update is completed.
+ * The BlockEditor class manages a block chain and a render tree for a document editor.
+ * The block chain stores the content blocks of the document in sequence, while the
+ * render tree displays the document as a hierarchical tree structure.
*/
export class BlockEditor {
- // blockData manages document block data, including operations such as add, delete, update, and move.
- public blockData: BlockDataManager;
- // RenderTreeManager manages data rendering, including the construction and updating of the render tree.
- public renderTree: TreeManager;
+ // Public properties
+ public blockChain: BlockChain; // (local data) the block chain used to store the document
+ public renderTree: RenderTree; // the render tree used to display the document
+ public operation: Operation;
+ /**
+ * Constructs a new BlockEditor object.
+ * @param id - the ID of the document
+ * @param data - the initial data for the document
+ */
+ constructor(private id: string, data: Record) {
+ // Create the block chain and render tree
+ this.blockChain = new BlockChain(this.blockChange);
+ this.operation = new Operation(this.blockChain);
+ this.changeDoc(id, data);
- constructor(private id: string, data: Record) {
- this.blockData = new BlockDataManager(id, data);
- this.renderTree = new TreeManager(this.blockData.getBlock);
+ this.renderTree = new RenderTree(this.blockChain);
}
/**
- * update id and map when the doc is change
- * @param id
- * @param data
+ * Updates the document ID and block chain when the document changes.
+ * @param id - the new ID of the document
+ * @param data - the updated data for the document
*/
changeDoc = (id: string, data: Record) => {
- console.log('==== change document ====', id, data)
+ console.log('==== change document ====', id, data);
+
+ // Update the document ID and rebuild the block chain
this.id = id;
- this.blockData.setBlocksMap(id, data);
+ this.blockChain.rebuild(id, data);
}
+
+ /**
+ * Destroys the block chain and render tree.
+ */
destroy = () => {
+ // Destroy the block chain and render tree
+ this.blockChain.destroy();
this.renderTree.destroy();
- this.blockData.destroy();
+ this.operation.destroy();
}
-
+
+ private blockChange = (command: string, data: BlockChangeProps) => {
+ this.renderTree.onBlockChange(command, data);
+ }
+
}
-let blockEditorInstance: BlockEditor | null;
-
-export function getBlockEditor() {
- return blockEditorInstance;
-}
-
-export function createBlockEditor(id: string, data: Record) {
- blockEditorInstance = new BlockEditor(id, data);
- return blockEditorInstance;
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts
deleted file mode 100644
index 5398ab4a6f..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { TreeNodeInterface } from "../interfaces";
-
-
-export function calculateBlockRect(blockId: string) {
- const el = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
- return el?.getBoundingClientRect();
-}
-
-export class RectManager {
- map: Map;
-
- orderList: Set;
-
- private updatedQueue: Set;
-
- constructor(private getTreeNode: (nodeId: string) => TreeNodeInterface | null) {
- this.map = new Map();
- this.orderList = new Set();
- this.updatedQueue = new Set();
- }
-
- build() {
- console.log('====update all blocks position====')
- this.orderList.forEach(id => this.updateNodeRect(id));
- }
-
- getNodeRect = (nodeId: string) => {
- return this.map.get(nodeId) || null;
- }
-
- update() {
- // In order to avoid excessive calculation frequency
- // calculate and update the block position information in the queue every frame
- requestAnimationFrame(() => {
- // there is nothing to do if the updated queue is empty
- if (this.updatedQueue.size === 0) return;
- console.log(`==== update ${this.updatedQueue.size} blocks rect cache ====`)
- this.updatedQueue.forEach((id: string) => {
- const rect = calculateBlockRect(id);
- this.map.set(id, rect);
- this.updatedQueue.delete(id);
- });
- });
- }
-
- updateNodeRect = (nodeId: string) => {
- if (this.updatedQueue.has(nodeId)) return;
- let node: TreeNodeInterface | null = this.getTreeNode(nodeId);
-
- // When one of the blocks is updated
- // the positions of all its parent and child blocks need to be updated
- while(node) {
- node.parent?.children.forEach(child => this.updatedQueue.add(child.id));
- node = node.parent;
- }
-
- this.update();
- }
-
- destroy() {
- this.map.clear();
- this.orderList.clear();
- this.updatedQueue.clear();
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts
deleted file mode 100644
index bc545139fc..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { RectManager } from "./rect";
-import { BlockInterface, BlockData, BlockType, TreeNodeInterface } from '../interfaces/index';
-
-export class TreeManager {
-
- // RenderTreeManager holds RectManager, which manages the position information of each node in the render tree.
- private rect: RectManager;
-
- root: TreeNode | null = null;
-
- map: Map = new Map();
-
- constructor(private getBlock: (blockId: string) => BlockInterface | null) {
- this.rect = new RectManager(this.getTreeNode);
- }
-
- /**
- * Get render node data by nodeId
- * @param nodeId string
- * @returns TreeNode
- */
- getTreeNode = (nodeId: string): TreeNodeInterface | null => {
- return this.map.get(nodeId) || null;
- }
-
- /**
- * build tree node for rendering
- * @param rootId
- * @returns
- */
- build(rootId: string): TreeNode | null {
- const head = this.getBlock(rootId);
-
- if (!head) return null;
-
- this.root = new TreeNode(head);
-
- let node = this.root;
-
- // loop line
- while (node) {
- this.map.set(node.id, node);
- this.rect.orderList.add(node.id);
-
- const block = this.getBlock(node.id)!;
- const next = block.next ? this.getBlock(block.next) : null;
- const firstChild = block.firstChild ? this.getBlock(block.firstChild) : null;
-
- // find next line
- if (firstChild) {
- // the next line is node's first child
- const child = new TreeNode(firstChild);
- node.addChild(child);
- node = child;
- } else if (next) {
- // the next line is node's sibling
- const sibling = new TreeNode(next);
- node.parent?.addChild(sibling);
- node = sibling;
- } else {
- // the next line is parent's sibling
- let isFind = false;
- while(node.parent) {
- const parentId = node.parent.id;
- const parent = this.getBlock(parentId)!;
- const parentNext = parent.next ? this.getBlock(parent.next) : null;
- if (parentNext) {
- const parentSibling = new TreeNode(parentNext);
- node.parent?.parent?.addChild(parentSibling);
- node = parentSibling;
- isFind = true;
- break;
- } else {
- node = node.parent;
- }
- }
-
- if (!isFind) {
- // Exit if next line not found
- break;
- }
-
- }
- }
-
- return this.root;
- }
-
- /**
- * update dom rects cache
- */
- updateRects = () => {
- this.rect.build();
- }
-
- /**
- * get block rect cache
- * @param id string
- * @returns DOMRect
- */
- getNodeRect = (nodeId: string) => {
- return this.rect.getNodeRect(nodeId);
- }
-
- /**
- * update block rect cache
- * @param id string
- */
- updateNodeRect = (nodeId: string) => {
- this.rect.updateNodeRect(nodeId);
- }
-
- destroy() {
- this.rect?.destroy();
- }
-}
-
-
-class TreeNode implements TreeNodeInterface {
- id: string;
- type: BlockType;
- parent: TreeNode | null = null;
- children: TreeNode[] = [];
- data: BlockData;
-
- constructor({
- id,
- type,
- data
- }: BlockInterface) {
- this.id = id;
- this.data = data;
- this.type = type;
- }
-
- addChild(node: TreeNode) {
- node.parent = this;
- this.children.push(node);
- }
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
new file mode 100644
index 0000000000..a2841d8a3b
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
@@ -0,0 +1,73 @@
+import { RegionGrid, BlockPosition } from './region_grid';
+export class BlockPositionManager {
+ private regionGrid: RegionGrid;
+ private viewportBlocks: Set = new Set();
+ private blockPositions: Map = new Map();
+ private observer: IntersectionObserver;
+ private container: HTMLDivElement | null = null;
+
+ constructor(container: HTMLDivElement) {
+ this.container = container;
+ this.regionGrid = new RegionGrid(container.offsetHeight);
+ this.observer = new IntersectionObserver((entries) => {
+ for (const entry of entries) {
+ const blockId = entry.target.getAttribute('data-block-id');
+ if (!blockId) return;
+ if (entry.isIntersecting) {
+ this.updateBlockPosition(blockId);
+ this.viewportBlocks.add(blockId);
+ } else {
+ this.viewportBlocks.delete(blockId);
+ }
+ }
+ }, { root: container });
+ }
+
+ observeBlock(node: HTMLDivElement) {
+ this.observer.observe(node);
+ return {
+ unobserve: () => this.observer.unobserve(node),
+ }
+ }
+
+ getBlockPosition(blockId: string) {
+ if (!this.blockPositions.has(blockId)) {
+ this.updateBlockPosition(blockId);
+ }
+ return this.blockPositions.get(blockId);
+ }
+
+ updateBlockPosition(blockId: string) {
+ if (!this.container) return;
+ const node = document.querySelector(`[data-block-id=${blockId}]`) as HTMLDivElement;
+ if (!node) return;
+ const rect = node.getBoundingClientRect();
+ const position = {
+ id: blockId,
+ x: rect.x,
+ y: rect.y + this.container.scrollTop,
+ height: rect.height,
+ width: rect.width
+ };
+ const prevPosition = this.blockPositions.get(blockId);
+ if (prevPosition && prevPosition.x === position.x &&
+ prevPosition.y === position.y &&
+ prevPosition.height === position.height &&
+ prevPosition.width === position.width) {
+ return;
+ }
+ this.blockPositions.set(blockId, position);
+ this.regionGrid.removeBlock(blockId);
+ this.regionGrid.addBlock(position);
+ }
+
+ getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] {
+ return this.regionGrid.getIntersectBlocks(startX, startY, endX, endY);
+ }
+
+ destroy() {
+ this.container = null;
+ this.observer.disconnect();
+ }
+
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts
new file mode 100644
index 0000000000..5f06f253ad
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts
@@ -0,0 +1,81 @@
+export interface BlockPosition {
+ id: string;
+ x: number;
+ y: number;
+ height: number;
+ width: number;
+}
+interface BlockRegion {
+ regionX: number;
+ regionY: number;
+ blocks: BlockPosition[];
+}
+
+export class RegionGrid {
+ private regions: BlockRegion[][];
+ private regionSize: number;
+
+ constructor(regionSize: number) {
+ this.regionSize = regionSize;
+ this.regions = [];
+ }
+
+ addBlock(blockPosition: BlockPosition) {
+ const regionX = Math.floor(blockPosition.x / this.regionSize);
+ const regionY = Math.floor(blockPosition.y / this.regionSize);
+
+ let region = this.regions[regionY]?.[regionX];
+ if (!region) {
+ region = {
+ regionX,
+ regionY,
+ blocks: []
+ };
+ if (!this.regions[regionY]) {
+ this.regions[regionY] = [];
+ }
+ this.regions[regionY][regionX] = region;
+ }
+
+ region.blocks.push(blockPosition);
+ }
+
+ removeBlock(blockId: string) {
+ for (const rows of this.regions) {
+ for (const region of rows) {
+ if (!region) return;
+ const blockIndex = region.blocks.findIndex(b => b.id === blockId);
+ if (blockIndex !== -1) {
+ region.blocks.splice(blockIndex, 1);
+ return;
+ }
+ }
+ }
+ }
+
+
+ getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] {
+ const selectedBlocks: BlockPosition[] = [];
+
+ const startRegionX = Math.floor(startX / this.regionSize);
+ const startRegionY = Math.floor(startY / this.regionSize);
+ const endRegionX = Math.floor(endX / this.regionSize);
+ const endRegionY = Math.floor(endY / this.regionSize);
+
+ for (let y = startRegionY; y <= endRegionY; y++) {
+ for (let x = startRegionX; x <= endRegionX; x++) {
+ const region = this.regions[y]?.[x];
+ if (region) {
+ for (const block of region.blocks) {
+ if (block.x + block.width - 1 >= startX && block.x <= endX &&
+ block.y + block.height - 1 >= startY && block.y <= endY) {
+ selectedBlocks.push(block);
+ }
+ }
+ }
+ }
+ }
+
+ return selectedBlocks;
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
new file mode 100644
index 0000000000..4eb136ff09
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
@@ -0,0 +1,165 @@
+import { BlockChain, BlockChangeProps } from '../core/block_chain';
+import { Block } from '../core/block';
+import { TreeNode } from "./tree_node";
+import { BlockPositionManager } from './block_position';
+import { filterSelections } from '@/appflowy_app/utils/block_selection';
+
+export class RenderTree {
+ public blockPositionManager?: BlockPositionManager;
+
+ private map: Map = new Map();
+ private root: TreeNode | null = null;
+ private selections: Set = new Set();
+ constructor(private blockChain: BlockChain) {
+ }
+
+
+ createPositionManager(container: HTMLDivElement) {
+ this.blockPositionManager = new BlockPositionManager(container);
+ }
+
+ observeBlock(node: HTMLDivElement) {
+ return this.blockPositionManager?.observeBlock(node);
+ }
+
+ getBlockPosition(nodeId: string) {
+ return this.blockPositionManager?.getBlockPosition(nodeId) || null;
+ }
+ /**
+ * Get the TreeNode data by nodeId
+ * @param nodeId string
+ * @returns TreeNode|null
+ */
+ getTreeNode = (nodeId: string): TreeNode | null => {
+ // Return the TreeNode instance from the map or null if it does not exist
+ return this.map.get(nodeId) || null;
+ }
+
+ private createNode(block: Block): TreeNode {
+ if (this.map.has(block.id)) {
+ return this.map.get(block.id)!;
+ }
+ const node = new TreeNode(block);
+ this.map.set(block.id, node);
+ return node;
+ }
+
+
+ buildDeep(rootId: string): TreeNode | null {
+ this.map.clear();
+ // Define a callback function for the blockChain.traverse() method
+ const callback = (block: Block) => {
+ // Check if the TreeNode instance already exists in the map
+ const node = this.createNode(block);
+
+ // Add the TreeNode instance to the map
+ this.map.set(block.id, node);
+
+ // Add the first child of the block as a child of the current TreeNode instance
+ const firstChild = block.firstChild;
+ if (firstChild) {
+ const child = this.createNode(firstChild);
+ node.addChild(child);
+ this.map.set(child.id, child);
+ }
+
+ // Add the next block as a sibling of the current TreeNode instance
+ const next = block.next;
+ if (next) {
+ const nextNode = this.createNode(next);
+ node.parent?.addChild(nextNode);
+ this.map.set(next.id, nextNode);
+ }
+ }
+
+ // Traverse the blockChain using the callback function
+ this.blockChain.traverse(callback);
+
+ // Get the root node from the map and return it
+ const root = this.map.get(rootId)!;
+ this.root = root;
+ return root || null;
+ }
+
+
+ forceUpdate(nodeId: string, shouldUpdateChildren = false) {
+ const block = this.blockChain.getBlock(nodeId);
+ if (!block) return null;
+ const node = this.createNode(block);
+ if (!node) return null;
+
+ if (shouldUpdateChildren) {
+ const children: TreeNode[] = [];
+ let childBlock = block.firstChild;
+
+ while(childBlock) {
+ const child = this.createNode(childBlock);
+ child.update(childBlock, child.children);
+ children.push(child);
+ childBlock = childBlock.next;
+ }
+
+ node.update(block, children);
+ node?.reRender();
+ node?.children.forEach(child => {
+ child.reRender();
+ })
+ } else {
+ node.update(block, node.children);
+ node?.reRender();
+ }
+ }
+
+ onBlockChange(command: string, data: BlockChangeProps) {
+ const { block, startBlock, endBlock, oldParentId = '', oldPrevId = '' } = data;
+ switch (command) {
+ case 'insert':
+ if (block?.parent) this.forceUpdate(block.parent.id, true);
+ break;
+ case 'update':
+ this.forceUpdate(block!.id);
+ break;
+ case 'move':
+ if (oldParentId) this.forceUpdate(oldParentId, true);
+ if (block?.parent) this.forceUpdate(block.parent.id, true);
+ if (startBlock?.parent) this.forceUpdate(startBlock.parent.id, true);
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ updateSelections(selections: string[]) {
+ const newSelections = filterSelections(selections, this.map);
+
+ let isDiff = false;
+ if (newSelections.length !== this.selections.size) {
+ isDiff = true;
+ }
+
+ const selectedBlocksSet = new Set(newSelections);
+ if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) {
+ isDiff = true;
+ }
+
+ if (isDiff) {
+ const shouldUpdateIds = new Set([...this.selections, ...newSelections]);
+ this.selections = selectedBlocksSet;
+ shouldUpdateIds.forEach((id) => this.forceUpdate(id));
+ }
+ }
+
+ isSelected(nodeId: string) {
+ return this.selections.has(nodeId);
+ }
+
+ /**
+ * Destroy the RenderTreeRectManager instance
+ */
+ destroy() {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.blockChain = null;
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
new file mode 100644
index 0000000000..9ed78bd4b4
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
@@ -0,0 +1,59 @@
+import { BlockData, BlockType } from '$app/interfaces/index';
+import { Block } from '../core/block';
+
+/**
+ * Represents a node in a tree structure of blocks.
+ */
+export class TreeNode {
+ id: string;
+ type: BlockType;
+ parent: TreeNode | null = null;
+ children: TreeNode[] = [];
+ data: BlockData;
+
+ private forceUpdate?: () => void;
+
+ /**
+ * Create a new TreeNode instance.
+ * @param block - The block data used to create the node.
+ */
+ constructor(private _block: Block) {
+ this.id = _block.id;
+ this.data = _block.data;
+ this.type = _block.type;
+ }
+
+ registerUpdate(forceUpdate: () => void) {
+ this.forceUpdate = forceUpdate;
+ }
+
+ unregisterUpdate() {
+ this.forceUpdate = undefined;
+ }
+
+ reRender() {
+ this.forceUpdate?.();
+ }
+
+ update(block: Block, children: TreeNode[]) {
+ this.data = block.data;
+ this.children = [];
+ children.forEach(child => {
+ this.addChild(child);
+ })
+ }
+
+ /**
+ * Add a child node to the current node.
+ * @param node - The child node to add.
+ */
+ addChild(node: TreeNode) {
+ node.parent = this;
+ this.children.push(node);
+ }
+
+ get block() {
+ return this._block;
+ }
+
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx
index cbe27de694..1409680f24 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx
@@ -1,31 +1,12 @@
-import { useSlate } from 'slate-react';
import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
-import { useMemo } from 'react';
-import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
-import { command, iconSize } from '$app/constants/toolbar';
-const FormatButton = ({ format, icon }: { format: string; icon: string }) => {
- const editor = useSlate();
-
- const renderComponent = useMemo(() => {
- switch (icon) {
- case 'bold':
- return ;
- case 'underlined':
- return ;
- case 'italic':
- return ;
- case 'code':
- return ;
- case 'strikethrough':
- return ;
- default:
- break;
- }
- }, [icon]);
+import { command } from '$app/constants/toolbar';
+import FormatIcon from './FormatIcon';
+import { BaseEditor } from 'slate';
+const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => {
return (
{
sx={{ color: isFormatActive(editor, format) ? '#00BCF0' : 'white' }}
onClick={() => toggleFormat(editor, format)}
>
- {renderComponent}
+
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx
new file mode 100644
index 0000000000..371ec6585c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
+import { iconSize } from '$app/constants/toolbar';
+
+export default function FormatIcon({ icon }: { icon: string }) {
+ switch (icon) {
+ case 'bold':
+ return ;
+ case 'underlined':
+ return ;
+ case 'italic':
+ return ;
+ case 'code':
+ return ;
+ case 'strikethrough':
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx
deleted file mode 100644
index 0a18c3f5e9..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import ReactDOM from 'react-dom';
-export const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
- const root = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
- return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
-};
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
new file mode 100644
index 0000000000..8319291046
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
@@ -0,0 +1,36 @@
+import { useEffect, useRef } from 'react';
+import { useFocused, useSlate } from 'slate-react';
+import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
+import { TreeNode } from '$app/block_editor/view/tree_node';
+
+export function useHoveringToolbar({node}: {
+ node: TreeNode
+}) {
+ const editor = useSlate();
+ const inFocus = useFocused();
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const nodeRect = document.querySelector(`[data-block-id=${node.id}]`)?.getBoundingClientRect();
+
+ if (!nodeRect) return;
+ const position = calcToolbarPosition(editor, el, nodeRect);
+
+ if (!position) {
+ el.style.opacity = '0';
+ el.style.zIndex = '-1';
+ } else {
+ el.style.opacity = '1';
+ el.style.zIndex = '1';
+ el.style.top = position.top;
+ el.style.left = position.left;
+ }
+ });
+ return {
+ ref,
+ inFocus,
+ editor
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
index 7b8454800b..dcd502905f 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
@@ -1,29 +1,10 @@
-import { useEffect, useRef } from 'react';
-import { useFocused, useSlate } from 'slate-react';
import FormatButton from './FormatButton';
import Portal from './Portal';
-import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
-
-const HoveringToolbar = ({ blockId }: { blockId: string }) => {
- const editor = useSlate();
- const inFocus = useFocused();
- const ref = useRef(null);
-
- useEffect(() => {
- const el = ref.current;
- if (!el) return;
-
- const position = calcToolbarPosition(editor, el, blockId);
-
- if (!position) {
- el.style.opacity = '0';
- } else {
- el.style.opacity = '1';
- el.style.top = position.top;
- el.style.left = position.left;
- }
- });
+import { TreeNode } from '$app/block_editor/view/tree_node';
+import { useHoveringToolbar } from './index.hooks';
+const HoveringToolbar = ({ blockId, node }: { blockId: string; node: TreeNode }) => {
+ const { inFocus, ref, editor } = useHoveringToolbar({ node });
if (!inFocus) return null;
return (
@@ -40,7 +21,7 @@ const HoveringToolbar = ({ blockId }: { blockId: string }) => {
}}
>
{['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => (
-
+
))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
new file mode 100644
index 0000000000..20e31a1793
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
@@ -0,0 +1,36 @@
+import { useEffect, useState, useRef, useContext } from 'react';
+
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { BlockContext } from '$app/utils/block';
+
+export function useBlockComponent({
+ node
+}: {
+ node: TreeNode
+}) {
+ const { blockEditor } = useContext(BlockContext);
+
+ const [version, forceUpdate] = useState(0);
+ const myRef = useRef(null);
+
+ const isSelected = blockEditor?.renderTree.isSelected(node.id);
+
+ useEffect(() => {
+ if (!myRef.current) {
+ return;
+ }
+ const observe = blockEditor?.renderTree.observeBlock(myRef.current);
+ node.registerUpdate(() => forceUpdate((prev) => prev + 1));
+
+ return () => {
+ node.unregisterUpdate();
+ observe?.unobserve();
+ };
+ }, []);
+ return {
+ version,
+ myRef,
+ isSelected,
+ className: `relative my-[1px] px-1`
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
new file mode 100644
index 0000000000..9c8ee223dd
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
@@ -0,0 +1,91 @@
+import React, { forwardRef } from 'react';
+import { BlockCommonProps, BlockType } from '$app/interfaces';
+import PageBlock from '../PageBlock';
+import TextBlock from '../TextBlock';
+import HeadingBlock from '../HeadingBlock';
+import ListBlock from '../ListBlock';
+import CodeBlock from '../CodeBlock';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { withErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryFallbackComponent } from '../BlockList/BlockList.hooks';
+import { useBlockComponent } from './BlockComponet.hooks';
+
+const BlockComponent = forwardRef(
+ (
+ {
+ node,
+ renderChild,
+ ...props
+ }: { node: TreeNode; renderChild?: (_node: TreeNode) => React.ReactNode } & React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLDivElement
+ >,
+ ref: React.ForwardedRef
+ ) => {
+ const { myRef, className, version, isSelected } = useBlockComponent({
+ node,
+ });
+
+ const renderComponent = () => {
+ let BlockComponentClass: (_: BlockCommonProps) => JSX.Element | null;
+ switch (node.type) {
+ case BlockType.PageBlock:
+ BlockComponentClass = PageBlock;
+ break;
+ case BlockType.TextBlock:
+ BlockComponentClass = TextBlock;
+ break;
+ case BlockType.HeadingBlock:
+ BlockComponentClass = HeadingBlock;
+ break;
+ case BlockType.ListBlock:
+ BlockComponentClass = ListBlock;
+ break;
+ case BlockType.CodeBlock:
+ BlockComponentClass = CodeBlock;
+ break;
+ default:
+ break;
+ }
+
+ const blockProps: BlockCommonProps = {
+ version,
+ node,
+ };
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ if (BlockComponentClass) {
+ return ;
+ }
+ return null;
+ };
+
+ return (
+ {
+ myRef.current = el;
+ if (typeof ref === 'function') {
+ ref(el);
+ } else if (ref) {
+ ref.current = el;
+ }
+ }}
+ {...props}
+ data-block-id={node.id}
+ data-block-selected={isSelected}
+ className={props.className ? `${props.className} ${className}` : className}
+ >
+ {renderComponent()}
+ {renderChild ? node.children.map(renderChild) : null}
+
+ {isSelected ?
: null}
+
+ );
+ }
+);
+
+const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, {
+ FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+export default React.memo(ComponentWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx
deleted file mode 100644
index 051081ebaf..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import { BlockType, TreeNodeInterface } from '$app/interfaces';
-import PageBlock from '../PageBlock';
-import TextBlock from '../TextBlock';
-import HeadingBlock from '../HeadingBlock';
-import ListBlock from '../ListBlock';
-import CodeBlock from '../CodeBlock';
-
-function BlockComponent({
- node,
- ...props
-}: { node: TreeNodeInterface } & React.DetailedHTMLProps, HTMLDivElement>) {
- const renderComponent = () => {
- switch (node.type) {
- case BlockType.PageBlock:
- return ;
- case BlockType.TextBlock:
- return ;
- case BlockType.HeadingBlock:
- return ;
- case BlockType.ListBlock:
- return ;
- case BlockType.CodeBlock:
- return ;
- default:
- return null;
- }
- };
-
- return (
-
- {renderComponent()}
- {props.children}
-
-
- );
-}
-
-export default React.memo(BlockComponent);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
new file mode 100644
index 0000000000..0d673a47e8
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
@@ -0,0 +1,92 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { BlockEditor } from '@/appflowy_app/block_editor';
+import { TreeNode } from '$app/block_editor/view/tree_node';
+import { Alert } from '@mui/material';
+import { FallbackProps } from 'react-error-boundary';
+import { TextBlockManager } from '@/appflowy_app/block_editor/blocks/text_block';
+import { TextBlockContext } from '@/appflowy_app/utils/slate/context';
+import { useVirtualizer } from '@tanstack/react-virtual';
+export interface BlockListProps {
+ blockId: string;
+ blockEditor: BlockEditor;
+}
+
+const defaultSize = 45;
+
+export function useBlockList({ blockId, blockEditor }: BlockListProps) {
+ const [root, setRoot] = useState(null);
+
+ const parentRef = useRef(null);
+
+ const rowVirtualizer = useVirtualizer({
+ count: root?.children.length || 0,
+ getScrollElement: () => parentRef.current,
+ overscan: 5,
+ estimateSize: () => {
+ return defaultSize;
+ },
+ });
+
+ const [version, forceUpdate] = useState(0);
+
+ const buildDeepTree = useCallback(() => {
+ const treeNode = blockEditor.renderTree.buildDeep(blockId);
+ setRoot(treeNode);
+ }, [blockEditor]);
+
+ useEffect(() => {
+ if (!parentRef.current) return;
+ blockEditor.renderTree.createPositionManager(parentRef.current);
+ buildDeepTree();
+
+ return () => {
+ blockEditor.destroy();
+ };
+ }, [blockId, blockEditor]);
+
+ useEffect(() => {
+ root?.registerUpdate(() => forceUpdate((prev) => prev + 1));
+ return () => {
+ root?.unregisterUpdate();
+ };
+ }, [root]);
+
+ return {
+ root,
+ rowVirtualizer,
+ parentRef,
+ blockEditor,
+ };
+}
+
+export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
+ return (
+
+ Something went wrong:
+ {error.message}
+ Try again
+
+ );
+}
+
+export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) {
+ return (props: BlockListProps) => {
+ const textBlockManager = new TextBlockManager(props.blockEditor.operation);
+
+ useEffect(() => {
+ return () => {
+ textBlockManager.destroy();
+ };
+ }, []);
+
+ return (
+
+
+
+ );
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
new file mode 100644
index 0000000000..f74ae72283
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
@@ -0,0 +1,18 @@
+import TextBlock from '../TextBlock';
+import { TreeNode } from '$app/block_editor/view/tree_node';
+
+export default function BlockListTitle({ node }: { node: TreeNode | null }) {
+ if (!node) return null;
+ return (
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
new file mode 100644
index 0000000000..6078180374
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+import Typography, { TypographyProps } from '@mui/material/Typography';
+import Skeleton from '@mui/material/Skeleton';
+import Grid from '@mui/material/Grid';
+
+const variants = ['h1', 'h3', 'body1', 'caption'] as readonly TypographyProps['variant'][];
+
+export default function ListFallbackComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {variants.map((variant) => (
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
index 7badc069b1..9a8709ea64 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
@@ -1,43 +1,58 @@
-import BlockComponent from './BlockComponent';
-import React, { useEffect } from 'react';
-import { debounce } from '@/appflowy_app/utils/tool';
-import { getBlockEditor } from '../../../block_editor';
+import React from 'react';
+import { BlockListProps, useBlockList, withTextBlockManager } from './BlockList.hooks';
+import { withErrorBoundary } from 'react-error-boundary';
+import ListFallbackComponent from './ListFallbackComponent';
+import BlockListTitle from './BlockListTitle';
+import BlockComponent from '../BlockComponent';
+import BlockSelection from '../BlockSelection';
-const RESIZE_DELAY = 200;
-
-function BlockList({ blockId }: { blockId: string }) {
- const blockEditor = getBlockEditor();
- if (!blockEditor) return null;
-
- const root = blockEditor.renderTree.build(blockId);
- console.log('==== build tree ====', root);
-
- useEffect(() => {
- // update rect cache when did mount
- blockEditor.renderTree.updateRects();
-
- const resize = debounce(() => {
- // update rect cache when window resized
- blockEditor.renderTree.updateRects();
- }, RESIZE_DELAY);
-
- window.addEventListener('resize', resize);
-
- return () => {
- window.removeEventListener('resize', resize);
- };
- }, []);
+function BlockList(props: BlockListProps) {
+ const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props);
+ const virtualItems = rowVirtualizer.getVirtualItems();
return (
-
-
{root?.data.title}
-
- {root && root.children.length > 0
- ? root.children.map((node) =>
)
- : null}
+
+
+
+ {root && virtualItems.length ? (
+
+ {virtualItems.map((virtualRow) => {
+ const id = root.children[virtualRow.index].id;
+ return (
+
+ {virtualRow.index === 0 ? : null}
+
+
+ );
+ })}
+
+ ) : null}
+
+ {parentRef.current ?
: null}
);
}
-export default React.memo(BlockList);
+const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), {
+ FallbackComponent: ListFallbackComponent,
+});
+
+export default React.memo(ListWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx
new file mode 100644
index 0000000000..00bc05f2d1
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx
@@ -0,0 +1,137 @@
+import { BlockEditor } from '@/appflowy_app/block_editor';
+import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+
+export function useBlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) {
+ const blockPositionManager = blockEditor.renderTree.blockPositionManager;
+
+ const [isDragging, setDragging] = useState(false);
+ const pointRef = useRef
([]);
+ const startScrollTopRef = useRef(0);
+
+ const [rect, setRect] = useState<{
+ startX: number;
+ startY: number;
+ endX: number;
+ endY: number;
+ } | null>(null);
+
+ const style = useMemo(() => {
+ if (!rect) return;
+ const { startX, endX, startY, endY } = rect;
+ const x = Math.min(startX, endX);
+ const y = Math.min(startY, endY);
+ const width = Math.abs(endX - startX);
+ const height = Math.abs(endY - startY);
+ return {
+ left: x - container.scrollLeft + 'px',
+ top: y - container.scrollTop + 'px',
+ width: width + 'px',
+ height: height + 'px',
+ };
+ }, [rect]);
+
+ const isPointInBlock = useCallback((target: HTMLElement | null) => {
+ let node = target;
+ while (node) {
+ if (node.getAttribute('data-block-id')) {
+ return true;
+ }
+ node = node.parentElement;
+ }
+ return false;
+ }, []);
+
+ const handleDragStart = useCallback((e: MouseEvent) => {
+ if (isPointInBlock(e.target as HTMLElement)) {
+ return;
+ }
+ e.preventDefault();
+ setDragging(true);
+
+ const startX = e.clientX + container.scrollLeft;
+ const startY = e.clientY + container.scrollTop;
+ pointRef.current = [startX, startY];
+ startScrollTopRef.current = container.scrollTop;
+ setRect({
+ startX,
+ startY,
+ endX: startX,
+ endY: startY,
+ });
+ }, []);
+
+ const calcIntersectBlocks = useCallback(
+ (clientX: number, clientY: number) => {
+ if (!isDragging || !blockPositionManager) return;
+ const [startX, startY] = pointRef.current;
+ const endX = clientX + container.scrollLeft;
+ const endY = clientY + container.scrollTop;
+
+ setRect({
+ startX,
+ startY,
+ endX,
+ endY,
+ });
+ const selectedBlocks = blockPositionManager.getIntersectBlocks(
+ Math.min(startX, endX),
+ Math.min(startY, endY),
+ Math.max(startX, endX),
+ Math.max(startY, endY)
+ );
+ const ids = selectedBlocks.map((item) => item.id);
+ blockEditor.renderTree.updateSelections(ids);
+ },
+ [isDragging]
+ );
+
+ const handleDraging = useCallback(
+ (e: MouseEvent) => {
+ if (!isDragging || !blockPositionManager) return;
+ e.preventDefault();
+ calcIntersectBlocks(e.clientX, e.clientY);
+
+ const { top, bottom } = container.getBoundingClientRect();
+ if (e.clientY >= bottom) {
+ const delta = e.clientY - bottom;
+ container.scrollBy(0, delta);
+ } else if (e.clientY <= top) {
+ const delta = e.clientY - top;
+ container.scrollBy(0, delta);
+ }
+ },
+ [isDragging]
+ );
+
+ const handleDragEnd = useCallback(
+ (e: MouseEvent) => {
+ if (isPointInBlock(e.target as HTMLElement) && !isDragging) {
+ blockEditor.renderTree.updateSelections([]);
+ return;
+ }
+ if (!isDragging) return;
+ e.preventDefault();
+ calcIntersectBlocks(e.clientX, e.clientY);
+ setDragging(false);
+ setRect(null);
+ },
+ [isDragging]
+ );
+
+ useEffect(() => {
+ window.addEventListener('mousedown', handleDragStart);
+ window.addEventListener('mousemove', handleDraging);
+ window.addEventListener('mouseup', handleDragEnd);
+
+ return () => {
+ window.removeEventListener('mousedown', handleDragStart);
+ window.removeEventListener('mousemove', handleDraging);
+ window.removeEventListener('mouseup', handleDragEnd);
+ };
+ }, [handleDragStart, handleDragEnd, handleDraging]);
+
+ return {
+ isDragging,
+ style,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
new file mode 100644
index 0000000000..4ef554d489
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
@@ -0,0 +1,18 @@
+import { useBlockSelection } from './BlockSelection.hooks';
+import { BlockEditor } from '$app/block_editor';
+import React from 'react';
+
+function BlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) {
+ const { isDragging, style } = useBlockSelection({
+ container,
+ blockEditor,
+ });
+
+ return (
+
+ {isDragging ?
: null}
+
+ );
+}
+
+export default React.memo(BlockSelection);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
index ab04d15820..eb34844d2c 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
-import { TreeNodeInterface } from '$app/interfaces';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { BlockCommonProps } from '@/appflowy_app/interfaces';
-export default function CodeBlock({ node }: { node: TreeNodeInterface }) {
+export default function CodeBlock({ node }: BlockCommonProps) {
return {node.data.text}
;
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx
index 66c2076eed..8a6298bb2b 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx
@@ -1,13 +1,14 @@
import React from 'react';
-import { TreeNodeInterface } from '$app/interfaces/index';
-import BlockComponent from '../BlockList/BlockComponent';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+
+import BlockComponent from '../BlockComponent';
export default function ColumnBlock({
node,
resizerWidth,
index,
}: {
- node: TreeNodeInterface;
+ node: TreeNode;
resizerWidth: number;
index: number;
}) {
@@ -16,6 +17,7 @@ export default function ColumnBlock({
);
};
+
return (
<>
{index === 0 ? (
@@ -41,11 +43,8 @@ export default function ColumnBlock({
width: `calc((100% - ${resizerWidth}px) * ${node.data.ratio})`,
}}
node={node}
- >
- {node.children?.map((item) => (
-
- ))}
-
+ renderChild={(item) => }
+ />
>
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
index a3f47386fb..f0a1bd3323 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
@@ -1,21 +1,17 @@
-import React from 'react';
import TextBlock from '../TextBlock';
-import { TreeNodeInterface } from '$app/interfaces/index';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { BlockCommonProps } from '@/appflowy_app/interfaces';
const fontSize: Record = {
1: 'mt-8 text-3xl',
2: 'mt-6 text-2xl',
3: 'mt-4 text-xl',
};
-export default function HeadingBlock({ node }: { node: TreeNodeInterface }) {
+
+export default function HeadingBlock({ node, version }: BlockCommonProps) {
return (
-
+
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx
index 8a69d1e3aa..38f5b743ea 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx
@@ -1,13 +1,13 @@
import { Circle } from '@mui/icons-material';
-import BlockComponent from '../BlockList/BlockComponent';
-import { TreeNodeInterface } from '$app/interfaces/index';
+import BlockComponent from '../BlockComponent';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) {
+export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) {
return (
-
+
{title}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
index 1c2b745229..ce0a1254d3 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
@@ -1,8 +1,8 @@
-import { TreeNodeInterface } from '@/appflowy_app/interfaces';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
import React, { useMemo } from 'react';
-import ColumnBlock from '../ColumnBlock/index';
+import ColumnBlock from '../ColumnBlock';
-export default function ColumnListBlock({ node }: { node: TreeNodeInterface }) {
+export default function ColumnListBlock({ node }: { node: TreeNode }) {
const resizerWidth = useMemo(() => {
return 46 * (node.children?.length || 0);
}, [node.children?.length]);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
index 96857b663d..6bc63d41ef 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
@@ -1,16 +1,21 @@
-import { TreeNodeInterface } from '@/appflowy_app/interfaces';
-import React, { useMemo } from 'react';
-import BlockComponent from '../BlockList/BlockComponent';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import BlockComponent from '../BlockComponent';
+import { BlockType } from '@/appflowy_app/interfaces';
+import { Block } from '@/appflowy_app/block_editor/core/block';
-export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) {
- const index = useMemo(() => {
- const i = node.parent?.children?.findIndex((item) => item.id === node.id) || 0;
- return i + 1;
- }, [node]);
+export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) {
+ let prev = node.block.prev;
+ let index = 1;
+ while (prev && prev.type === BlockType.ListBlock && (prev as Block
).data.type === 'numbered') {
+ index++;
+ prev = prev.prev;
+ }
return (
-
{`${index} .`}
+
{`${index} .`}
{title}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx
index b235828ff4..87c31795ce 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx
@@ -3,22 +3,18 @@ import TextBlock from '../TextBlock';
import NumberedListBlock from './NumberedListBlock';
import BulletedListBlock from './BulletedListBlock';
import ColumnListBlock from './ColumnListBlock';
-import { TreeNodeInterface } from '$app/interfaces/index';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { BlockCommonProps } from '@/appflowy_app/interfaces';
-export default function ListBlock({ node }: { node: TreeNodeInterface }) {
+export default function ListBlock({ node, version }: BlockCommonProps
) {
const title = useMemo(() => {
if (node.data.type === 'column') return <>>;
return (
-
+
);
- }, [node]);
+ }, [node, version]);
if (node.data.type === 'numbered') {
return ;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
index f4a5326916..a79e036dbe 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
-import { TreeNodeInterface } from '$app/interfaces';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { BlockCommonProps } from '@/appflowy_app/interfaces';
-export default function PageBlock({ node }: { node: TreeNodeInterface }) {
+export default function PageBlock({ node }: BlockCommonProps) {
return {node.data.title}
;
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
new file mode 100644
index 0000000000..a776ae8be4
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
@@ -0,0 +1,98 @@
+import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node";
+import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
+import { useCallback, useContext, useLayoutEffect, useState } from "react";
+import { Transforms, createEditor, Descendant } from 'slate';
+import { ReactEditor, withReact } from 'slate-react';
+import { TextBlockContext } from '$app/utils/slate/context';
+
+export function useTextBlock({
+ node,
+}: {
+ node: TreeNode;
+}) {
+ const [editor] = useState(() => withReact(createEditor()));
+
+ const { textBlockManager } = useContext(TextBlockContext);
+
+ const value = [
+ {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ type: 'paragraph',
+ children: node.data.content,
+ },
+ ];
+
+
+ const onChange = useCallback(
+ (e: Descendant[]) => {
+ if (!editor.operations || editor.operations.length === 0) return;
+ if (editor.operations[0].type !== 'set_selection') {
+ console.log('====text block ==== ', editor.operations)
+ const children = 'children' in e[0] ? e[0].children : [];
+ textBlockManager?.update(node, ['data', 'content'], children);
+ } else {
+ const newProperties = editor.operations[0].newProperties;
+ textBlockManager?.setSelection(node, editor.selection);
+ }
+ },
+ [node.id, editor],
+ );
+
+
+ const onKeyDownCapture = (event: React.KeyboardEvent) => {
+ switch (event.key) {
+ case 'Enter': {
+ event.stopPropagation();
+ event.preventDefault();
+ textBlockManager?.splitNode(node, editor);
+
+ return;
+ }
+ }
+
+ triggerHotkey(event, editor);
+ }
+
+
+
+ const { focusId, selection } = textBlockManager!.selectionManager.getFocusSelection();
+
+ editor.children = value;
+ Transforms.collapse(editor);
+
+ useLayoutEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (focusId === node.id && selection) {
+ ReactEditor.focus(editor);
+ Transforms.select(editor, selection);
+ // Use setTimeout to delay setting the selection
+ // until Slate has fully loaded and rendered all components and contents,
+ // to ensure that the operation succeeds.
+ timer = setTimeout(() => {
+ Transforms.select(editor, selection);
+ }, 100);
+ }
+
+ return () => timer && clearTimeout(timer)
+ }, [editor]);
+
+ const onDOMBeforeInput = useCallback((e: InputEvent) => {
+ // COMPAT: in Apple, `compositionend` is dispatched after the
+ // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
+ // Here, prevent the beforeInput event and wait for the compositionend event to take effect
+ if (e.inputType === 'insertFromComposition') {
+ e.preventDefault();
+ }
+
+ }, []);
+
+
+ return {
+ editor,
+ value,
+ onChange,
+ onKeyDownCapture,
+ onDOMBeforeInput,
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
index dd3dbce5de..906e9a4060 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
@@ -1,77 +1,37 @@
-import React, { useContext, useMemo, useState } from 'react';
-import { TreeNodeInterface } from '$app/interfaces';
-import BlockComponent from '../BlockList/BlockComponent';
-
-import { createEditor } from 'slate';
-import { Slate, Editable, withReact } from 'slate-react';
+import BlockComponent from '../BlockComponent';
+import { Slate, Editable } from 'slate-react';
import Leaf from './Leaf';
import HoveringToolbar from '$app/components/HoveringToolbar';
-import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
-import { BlockContext } from '$app/utils/block_context';
-import { debounce } from '@/appflowy_app/utils/tool';
-import { getBlockEditor } from '@/appflowy_app/block_editor/index';
+import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import { useTextBlock } from './index.hooks';
+import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces';
+import { toolbarDefaultProps } from '@/appflowy_app/constants/toolbar';
-const INPUT_CHANGE_CACHE_DELAY = 300;
-
-export default function TextBlock({ node }: { node: TreeNodeInterface }) {
- const blockEditor = getBlockEditor();
- if (!blockEditor) return null;
-
- const [editor] = useState(() => withReact(createEditor()));
-
- const { id } = useContext(BlockContext);
-
- const debounceUpdateBlockCache = useMemo(
- () => debounce(blockEditor.renderTree.updateNodeRect, INPUT_CHANGE_CACHE_DELAY),
- [id, node.id]
- );
+export default function TextBlock({
+ node,
+ needRenderChildren = true,
+ toolbarProps,
+ ...props
+}: {
+ needRenderChildren?: boolean;
+ toolbarProps?: TextBlockToolbarProps;
+} & BlockCommonProps &
+ React.HTMLAttributes) {
+ const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node });
+ const { showGroups } = toolbarProps || toolbarDefaultProps;
return (
-
-
{
- if (editor.operations[0].type !== 'set_selection') {
- console.log('=== text op ===', e, editor.operations);
- // Temporary code, in the future, it is necessary to monitor the OP changes of the document to determine whether the location cache of the block needs to be updated
- debounceUpdateBlockCache(node.id);
- }
- }}
- value={[
- {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- type: 'paragraph',
- children: node.data.content,
- },
- ]}
- >
-
+
+
+ {showGroups.length > 0 && }
{
- switch (event.key) {
- case 'Enter': {
- event.stopPropagation();
- event.preventDefault();
- return;
- }
- }
-
- triggerHotkey(event, editor);
- }}
- onDOMBeforeInput={(e) => {
- // COMPAT: in Apple, `compositionend` is dispatched after the
- // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
- // Here, prevent the beforeInput event and wait for the compositionend event to take effect
- if (e.inputType === 'insertFromComposition') {
- e.preventDefault();
- }
- }}
- renderLeaf={(props) => }
+ onKeyDownCapture={onKeyDownCapture}
+ onDOMBeforeInput={onDOMBeforeInput}
+ renderLeaf={(leafProps) => }
placeholder='Enter some text...'
/>
- {node.children && node.children.length > 0 ? (
+ {needRenderChildren && node.children.length > 0 ? (
{node.children.map((item) => (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
index 07cd1d8be9..a0efb98d60 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
@@ -1,3 +1,4 @@
+import { TextBlockToolbarGroup } from "../interfaces";
export const iconSize = { width: 18, height: 18 };
@@ -22,4 +23,17 @@ export const command: Record
= {
title: 'Strike through',
key: 'β + Shift + S or β + Shift + X',
},
+};
+
+export const toolbarDefaultProps = {
+ showGroups: [
+ TextBlockToolbarGroup.ASK_AI,
+ TextBlockToolbarGroup.BLOCK_SELECT,
+ TextBlockToolbarGroup.ADD_LINK,
+ TextBlockToolbarGroup.COMMENT,
+ TextBlockToolbarGroup.TEXT_FORMAT,
+ TextBlockToolbarGroup.TEXT_COLOR,
+ TextBlockToolbarGroup.MENTION,
+ TextBlockToolbarGroup.MORE,
+ ],
};
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
index 4d91df74a6..e6d0760f64 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
@@ -16,9 +16,7 @@ export enum BlockType {
}
-
-
-export type BlockData = T extends BlockType.TextBlock ? TextBlockData :
+export type BlockData = T extends BlockType.TextBlock ? TextBlockData :
T extends BlockType.PageBlock ? PageBlockData :
T extends BlockType.HeadingBlock ? HeadingBlockData :
T extends BlockType.ListBlock ? ListBlockData :
@@ -34,7 +32,7 @@ export interface BlockInterface {
}
-interface TextBlockData {
+export interface TextBlockData {
content: Descendant[];
}
@@ -54,11 +52,61 @@ interface ColumnBlockData {
ratio: string;
}
+// eslint-disable-next-line no-shadow
+export enum TextBlockToolbarGroup {
+ ASK_AI,
+ BLOCK_SELECT,
+ ADD_LINK,
+ COMMENT,
+ TEXT_FORMAT,
+ TEXT_COLOR,
+ MENTION,
+ MORE
+}
+export interface TextBlockToolbarProps {
+ showGroups: TextBlockToolbarGroup[]
+}
-export interface TreeNodeInterface {
- id: string;
- type: BlockType;
- parent: TreeNodeInterface | null;
- children: TreeNodeInterface[];
- data: BlockData;
+
+export interface BlockCommonProps {
+ version: number;
+ node: T;
+}
+
+export interface BackendOp {
+ type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
+ version: number;
+ data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
+}
+export interface LocalOp {
+ type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
+ version: number;
+ data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
+}
+
+export interface UpdateOpData {
+ blockId: string;
+ value: BlockData;
+ path: string[];
+}
+export interface InsertOpData {
+ block: BlockInterface;
+ parentId: string;
+ prevId?: string
+}
+
+export interface moveRangeOpData {
+ range: [string, string];
+ newParentId: string;
+ newPrevId?: string
+}
+
+export interface moveOpData {
+ blockId: string;
+ newParentId: string;
+ newPrevId?: string
+}
+
+export interface removeOpData {
+ blockId: string
}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
new file mode 100644
index 0000000000..c40e840036
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
@@ -0,0 +1,25 @@
+
+import { createContext } from 'react';
+import { ulid } from "ulid";
+import { BlockEditor } from '../block_editor/index';
+
+export const BlockContext = createContext<{
+ id?: string;
+ blockEditor?: BlockEditor;
+}>({});
+
+
+export function generateBlockId() {
+ const blockId = ulid()
+ return `block-id-${blockId}`;
+}
+
+const AVERAGE_BLOCK_HEIGHT = 30;
+export function calculateViewportBlockMaxCount() {
+ const viewportHeight = window.innerHeight;
+ const viewportBlockCount = Math.ceil(viewportHeight / AVERAGE_BLOCK_HEIGHT);
+
+ return viewportBlockCount;
+}
+
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts
deleted file mode 100644
index 71b99fd8a2..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-
-import { createContext } from 'react';
-
-export const BlockContext = createContext<{
- id?: string;
-}>({});
-
-
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
new file mode 100644
index 0000000000..8bc67522ce
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
@@ -0,0 +1,36 @@
+import { BlockData, BlockType } from "../interfaces";
+
+
+export function filterSelections(ids: string[], nodeMap: Map): string[] {
+ const selected = new Set(ids);
+ const newSelected = new Set();
+ ids.forEach(selectedId => {
+ const node = nodeMap.get(selectedId);
+ if (!node) return;
+ if (node.type === BlockType.ListBlock && node.data.type === 'column') {
+ return;
+ }
+ if (node.children.length === 0) {
+ newSelected.add(selectedId);
+ return;
+ }
+ const hasChildSelected = node.children.some(i => selected.has(i.id));
+ if (!hasChildSelected) {
+ newSelected.add(selectedId);
+ return;
+ }
+ const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id));
+ if (hasChildSelected && hasSiblingSelected) {
+ newSelected.add(selectedId);
+ return;
+ }
+ });
+
+ return Array.from(newSelected);
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts
deleted file mode 100644
index fd36928b76..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import {
- Editor,
- Transforms,
- Text,
- Node
-} from 'slate';
-
-export function toggleFormat(editor: Editor, format: string) {
- const isActive = isFormatActive(editor, format)
- Transforms.setNodes(
- editor,
- { [format]: isActive ? null : true },
- { match: Text.isText, split: true }
- )
-}
-
-export const isFormatActive = (editor: Editor, format: string) => {
- const [match] = Editor.nodes(editor, {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- match: (n: Node) => n[format] === true,
- mode: 'all',
- })
- return !!match
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts
deleted file mode 100644
index fad418086d..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import isHotkey from 'is-hotkey';
-import { toggleFormat } from './format';
-import { Editor } from 'slate';
-
-const HOTKEYS: Record = {
- 'mod+b': 'bold',
- 'mod+i': 'italic',
- 'mod+u': 'underline',
- 'mod+e': 'code',
- 'mod+shift+X': 'strikethrough',
- 'mod+shift+S': 'strikethrough',
-};
-
-export function triggerHotkey(event: React.KeyboardEvent, editor: Editor) {
- for (const hotkey in HOTKEYS) {
- if (isHotkey(hotkey, event)) {
- event.preventDefault()
- const format = HOTKEYS[hotkey]
- toggleFormat(editor, format)
- }
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts
deleted file mode 100644
index 80131a4d69..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Editor, Range } from 'slate';
-export function calcToolbarPosition(editor: Editor, el: HTMLDivElement, blockRect: DOMRect) {
- const { selection } = editor;
-
- if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
- return;
- }
-
- const domSelection = window.getSelection();
- let domRange;
- if (domSelection?.rangeCount === 0) {
- domRange = document.createRange();
- domRange.setStart(el, domSelection?.anchorOffset);
- domRange.setEnd(el, domSelection?.anchorOffset);
- } else {
- domRange = domSelection?.getRangeAt(0);
- }
-
- const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
-
- const top = `${-el.offsetHeight - 5}px`;
- const left = `${rect.left - blockRect.left - el.offsetWidth / 2 + rect.width / 2}px`;
- return {
- top,
- left,
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
new file mode 100644
index 0000000000..387b74ff50
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
@@ -0,0 +1,6 @@
+import { createContext } from "react";
+import { TextBlockManager } from '../../block_editor/blocks/text_block';
+
+export const TextBlockContext = createContext<{
+ textBlockManager?: TextBlockManager
+}>({});
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts
index ff0572d278..52681474d5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts
@@ -1,21 +1,11 @@
-import { getBlockEditor } from '@/appflowy_app/block_editor';
import { Editor, Range } from 'slate';
-export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockId: string) {
+export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockRect: DOMRect) {
const { selection } = editor;
- const scrollContainer = document.querySelector('.doc-scroller-container');
- if (!scrollContainer) return;
-
if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
return;
}
- const blockEditor = getBlockEditor();
- const blockRect = blockEditor?.renderTree.getNodeRect(blockId);
- const blockDom = document.querySelector(`[data-block-id=${blockId}]`);
-
- if (!blockDom || !blockRect) return;
-
const domSelection = window.getSelection();
let domRange;
if (domSelection?.rangeCount === 0) {
@@ -26,8 +16,8 @@ export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement,
const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
- const top = `${-toolbarDom.offsetHeight - 5 + (rect.top + scrollContainer.scrollTop - blockRect.top)}px`;
- const left = `${rect.left - blockRect.left - toolbarDom.offsetWidth / 2 + rect.width / 2}px`;
+ const top = `${-toolbarDom.offsetHeight - 5 + (rect.top - blockRect.y)}px`;
+ const left = `${rect.left - blockRect.x - toolbarDom.offsetWidth / 2 + rect.width / 2}px`;
return {
top,
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
index a893f2eb0f..88036d82d5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
@@ -8,3 +8,29 @@ export function debounce(fn: (...args: any[]) => void, delay: number) {
}, delay)
}
}
+
+export function get(obj: any, path: string[], defaultValue?: any) {
+ let value = obj;
+ for (const prop of path) {
+ value = value[prop];
+ if (value === undefined) {
+ return defaultValue !== undefined ? defaultValue : undefined;
+ }
+ }
+ return value;
+}
+
+export function set(obj: any, path: string[], value: any): void {
+ let current = obj;
+ for (let i = 0; i < path.length; i++) {
+ const prop = path[i];
+ if (i === path.length - 1) {
+ current[prop] = value;
+ } else {
+ if (!current[prop]) {
+ current[prop] = {};
+ }
+ current = current[prop];
+ }
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
index 6c4bda8e97..1dfe73fd85 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import {
DocumentEventGetDocument,
DocumentVersionPB,
@@ -6,14 +6,14 @@ import {
} from '../../services/backend/events/flowy-document';
import { BlockInterface, BlockType } from '../interfaces';
import { useParams } from 'react-router-dom';
-import { getBlockEditor, createBlockEditor } from '../block_editor';
+import { BlockEditor } from '../block_editor';
const loadBlockData = async (id: string): Promise> => {
return {
[id]: {
id: id,
type: BlockType.PageBlock,
- data: { title: 'Document Title' },
+ data: { content: [{ text: 'Document Title' }] },
next: null,
firstChild: "L1-1",
},
@@ -202,26 +202,580 @@ const loadBlockData = async (id: string): Promise
next: null,
firstChild: null,
},
+ "L1-8": {
+ id: "L1-8",
+ type: BlockType.HeadingBlock,
+ data: { level: 1, content: [{ text: 'Heading 1' }] },
+ next: "L1-9",
+ firstChild: null,
+ },
+ "L1-9": {
+ id: "L1-9",
+ type: BlockType.HeadingBlock,
+ data: { level: 2, content: [{ text: 'Heading 2' }] },
+ next: "L1-10",
+ firstChild: null,
+ },
+ "L1-10": {
+ id: "L1-10",
+ type: BlockType.HeadingBlock,
+ data: { level: 3, content: [{ text: 'Heading 3' }] },
+ next: "L1-11",
+ firstChild: null,
+ },
+ "L1-11": {
+ id: "L1-11",
+ type: BlockType.TextBlock,
+ data: { content: [
+ {
+ text:
+ 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
+ },
+ { text: 'bold', bold: true },
+ { text: ', ' },
+ { text: 'italic', italic: true },
+ { text: ', or anything else you might want to do!' },
+ ] },
+ next: "L1-12",
+ firstChild: null,
+ },
+ "L1-12": {
+ id: "L1-12",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ { text: 'select any piece of text and the menu will appear', bold: true },
+ { text: '.' },
+ ] },
+ next: "L2-1",
+ firstChild: "L1-12-1",
+ },
+ "L1-12-1": {
+ id: "L1-12-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L1-12-2",
+ firstChild: null,
+ },
+ "L1-12-2": {
+ id: "L1-12-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L2-1": {
+ id: "L2-1",
+ type: BlockType.HeadingBlock,
+ data: { level: 1, content: [{ text: 'Heading 1' }] },
+ next: "L2-2",
+ firstChild: null,
+ },
+ "L2-2": {
+ id: "L2-2",
+ type: BlockType.HeadingBlock,
+ data: { level: 2, content: [{ text: 'Heading 2' }] },
+ next: "L2-3",
+ firstChild: null,
+ },
+ "L2-3": {
+ id: "L2-3",
+ type: BlockType.HeadingBlock,
+ data: { level: 3, content: [{ text: 'Heading 3' }] },
+ next: "L2-4",
+ firstChild: null,
+ },
+ "L2-4": {
+ id: "L2-4",
+ type: BlockType.TextBlock,
+ data: { content: [
+ {
+ text:
+ 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
+ },
+ { text: 'bold', bold: true },
+ { text: ', ' },
+ { text: 'italic', italic: true },
+ { text: ', or anything else you might want to do!' },
+ ] },
+ next: "L2-5",
+ firstChild: null,
+ },
+ "L2-5": {
+ id: "L2-5",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ { text: 'select any piece of text and the menu will appear', bold: true },
+ { text: '.' },
+ ] },
+ next: "L2-6",
+ firstChild: "L2-5-1",
+ },
+ "L2-5-1": {
+ id: "L2-5-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L2-5-2",
+ firstChild: null,
+ },
+ "L2-5-2": {
+ id: "L2-5-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L2-6": {
+ id: "L2-6",
+ type: BlockType.ListBlock,
+ data: { type: 'bulleted', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+ { text: 'bold', bold: true },
+ {
+ text:
+ ', or add a semantically rendered block quote in the middle of the page, like this:',
+ },
+ ] },
+ next: "L2-7",
+ firstChild: "L2-6-1",
+ },
+ "L2-6-1": {
+ id: "L2-6-1",
+ type: BlockType.ListBlock,
+ data: { type: 'numbered', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+
+ ] },
+
+ next: "L2-6-2",
+ firstChild: null,
+ },
+ "L2-6-2": {
+ id: "L2-6-2",
+ type: BlockType.ListBlock,
+ data: { type: 'numbered', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+
+ ] },
+
+ next: "L2-6-3",
+ firstChild: null,
+ },
+
+ "L2-6-3": {
+ id: "L2-6-3",
+ type: BlockType.TextBlock,
+ data: { content: [{ text: 'A wise quote.' }] },
+ next: null,
+ firstChild: null,
+ },
+
+ "L2-7": {
+ id: "L2-7",
+ type: BlockType.ListBlock,
+ data: { type: 'column' },
+
+ next: "L2-8",
+ firstChild: "L2-7-1",
+ },
+ "L2-7-1": {
+ id: "L2-7-1",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: "L2-7-2",
+ firstChild: "L2-7-1-1",
+ },
+ "L2-7-1-1": {
+ id: "L2-7-1-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L2-7-2": {
+ id: "L2-7-2",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: "L2-7-3",
+ firstChild: "L2-7-2-1",
+ },
+ "L2-7-2-1": {
+ id: "L2-7-2-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L2-7-2-2",
+ firstChild: null,
+ },
+ "L2-7-2-2": {
+ id: "L2-7-2-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L2-7-3": {
+ id: "L2-7-3",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: null,
+ firstChild: "L2-7-3-1",
+ },
+ "L2-7-3-1": {
+ id: "L2-7-3-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L2-8": {
+ id: "L2-8",
+ type: BlockType.HeadingBlock,
+ data: { level: 1, content: [{ text: 'Heading 1' }] },
+ next: "L2-9",
+ firstChild: null,
+ },
+ "L2-9": {
+ id: "L2-9",
+ type: BlockType.HeadingBlock,
+ data: { level: 2, content: [{ text: 'Heading 2' }] },
+ next: "L2-10",
+ firstChild: null,
+ },
+ "L2-10": {
+ id: "L2-10",
+ type: BlockType.HeadingBlock,
+ data: { level: 3, content: [{ text: 'Heading 3' }] },
+ next: "L2-11",
+ firstChild: null,
+ },
+ "L2-11": {
+ id: "L2-11",
+ type: BlockType.TextBlock,
+ data: { content: [
+ {
+ text:
+ 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
+ },
+ { text: 'bold', bold: true },
+ { text: ', ' },
+ { text: 'italic', italic: true },
+ { text: ', or anything else you might want to do!' },
+ ] },
+ next: "L2-12",
+ firstChild: null,
+ },
+ "L2-12": {
+ id: "L2-12",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ { text: 'select any piece of text and the menu will appear', bold: true },
+ { text: '.' },
+ ] },
+ next: "L3-1",
+ firstChild: "L2-12-1",
+ },
+ "L2-12-1": {
+ id: "L2-12-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L2-12-2",
+ firstChild: null,
+ },
+ "L2-12-2": {
+ id: "L2-12-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },"L3-1": {
+ id: "L3-1",
+ type: BlockType.HeadingBlock,
+ data: { level: 1, content: [{ text: 'Heading 1' }] },
+ next: "L3-2",
+ firstChild: null,
+ },
+ "L3-2": {
+ id: "L3-2",
+ type: BlockType.HeadingBlock,
+ data: { level: 2, content: [{ text: 'Heading 2' }] },
+ next: "L3-3",
+ firstChild: null,
+ },
+ "L3-3": {
+ id: "L3-3",
+ type: BlockType.HeadingBlock,
+ data: { level: 3, content: [{ text: 'Heading 3' }] },
+ next: "L3-4",
+ firstChild: null,
+ },
+ "L3-4": {
+ id: "L3-4",
+ type: BlockType.TextBlock,
+ data: { content: [
+ {
+ text:
+ 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
+ },
+ { text: 'bold', bold: true },
+ { text: ', ' },
+ { text: 'italic', italic: true },
+ { text: ', or anything else you might want to do!' },
+ ] },
+ next: "L3-5",
+ firstChild: null,
+ },
+ "L3-5": {
+ id: "L3-5",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ { text: 'select any piece of text and the menu will appear', bold: true },
+ { text: '.' },
+ ] },
+ next: "L3-6",
+ firstChild: "L3-5-1",
+ },
+ "L3-5-1": {
+ id: "L3-5-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L3-5-2",
+ firstChild: null,
+ },
+ "L3-5-2": {
+ id: "L3-5-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L3-6": {
+ id: "L3-6",
+ type: BlockType.ListBlock,
+ data: { type: 'bulleted', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+ { text: 'bold', bold: true },
+ {
+ text:
+ ', or add a semantically rendered block quote in the middle of the page, like this:',
+ },
+ ] },
+ next: "L3-7",
+ firstChild: "L3-6-1",
+ },
+ "L3-6-1": {
+ id: "L3-6-1",
+ type: BlockType.ListBlock,
+ data: { type: 'numbered', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+
+ ] },
+
+ next: "L3-6-2",
+ firstChild: null,
+ },
+ "L3-6-2": {
+ id: "L3-6-2",
+ type: BlockType.ListBlock,
+ data: { type: 'numbered', content: [
+ {
+ text:
+ "Since it's rich text, you can do things like turn a selection of text ",
+ },
+
+ ] },
+
+ next: "L3-6-3",
+ firstChild: null,
+ },
+
+ "L3-6-3": {
+ id: "L3-6-3",
+ type: BlockType.TextBlock,
+ data: { content: [{ text: 'A wise quote.' }] },
+ next: null,
+ firstChild: null,
+ },
+
+ "L3-7": {
+ id: "L3-7",
+ type: BlockType.ListBlock,
+ data: { type: 'column' },
+
+ next: "L3-8",
+ firstChild: "L3-7-1",
+ },
+ "L3-7-1": {
+ id: "L3-7-1",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: "L3-7-2",
+ firstChild: "L3-7-1-1",
+ },
+ "L3-7-1-1": {
+ id: "L3-7-1-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L3-7-2": {
+ id: "L3-7-2",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: "L3-7-3",
+ firstChild: "L3-7-2-1",
+ },
+ "L3-7-2-1": {
+ id: "L3-7-2-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L3-7-2-2",
+ firstChild: null,
+ },
+ "L3-7-2-2": {
+ id: "L3-7-2-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L3-7-3": {
+ id: "L3-7-3",
+ type: BlockType.ColumnBlock,
+ data: { ratio: '0.33' },
+ next: null,
+ firstChild: "L3-7-3-1",
+ },
+ "L3-7-3-1": {
+ id: "L3-7-3-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
+ "L3-8": {
+ id: "L3-8",
+ type: BlockType.HeadingBlock,
+ data: { level: 1, content: [{ text: 'Heading 1' }] },
+ next: "L3-9",
+ firstChild: null,
+ },
+ "L3-9": {
+ id: "L3-9",
+ type: BlockType.HeadingBlock,
+ data: { level: 2, content: [{ text: 'Heading 2' }] },
+ next: "L3-10",
+ firstChild: null,
+ },
+ "L3-10": {
+ id: "L3-10",
+ type: BlockType.HeadingBlock,
+ data: { level: 3, content: [{ text: 'Heading 3' }] },
+ next: "L3-11",
+ firstChild: null,
+ },
+ "L3-11": {
+ id: "L3-11",
+ type: BlockType.TextBlock,
+ data: { content: [
+ {
+ text:
+ 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
+ },
+ { text: 'bold', bold: true },
+ { text: ', ' },
+ { text: 'italic', italic: true },
+ { text: ', or anything else you might want to do!' },
+ ] },
+ next: "L3-12",
+ firstChild: null,
+ },
+ "L3-12": {
+ id: "L3-12",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ { text: 'select any piece of text and the menu will appear', bold: true },
+ { text: '.' },
+ ] },
+ next: null,
+ firstChild: "L3-12-1",
+ },
+ "L3-12-1": {
+ id: "L3-12-1",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: "L3-12-2",
+ firstChild: null,
+ },
+ "L3-12-2": {
+ id: "L3-12-2",
+ type: BlockType.TextBlock,
+ data: { content: [
+ { text: 'Try it out yourself! Just ' },
+ ] },
+ next: null,
+ firstChild: null,
+ },
}
}
export const useDocument = () => {
const params = useParams();
const [blockId, setBlockId] = useState();
- const loadDocument = async (id: string): Promise => {
- const getDocumentResult = await DocumentEventGetDocument(
- OpenDocumentPayloadPB.fromObject({
- document_id: id,
- version: DocumentVersionPB.V1,
- })
- );
+ const blockEditorRef = useRef(null)
- if (getDocumentResult.ok) {
- const pb = getDocumentResult.val;
- return JSON.parse(pb.content);
- } else {
- throw new Error('get document error');
- }
- };
useEffect(() => {
void (async () => {
@@ -229,11 +783,10 @@ export const useDocument = () => {
const data = await loadBlockData(params.id);
console.log('==== enter ====', params?.id, data);
- const blockEditor = getBlockEditor();
- if (blockEditor) {
- blockEditor.changeDoc(params?.id, data);
+ if (!blockEditorRef.current) {
+ blockEditorRef.current = new BlockEditor(params?.id, data);
} else {
- createBlockEditor(params?.id, data);
+ blockEditorRef.current.changeDoc(params?.id, data);
}
setBlockId(params.id)
@@ -242,5 +795,5 @@ export const useDocument = () => {
console.log('==== leave ====', params?.id)
}
}, [params.id]);
- return { blockId };
+ return { blockId, blockEditor: blockEditorRef.current };
};
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
index 5bf71870da..8ab2e71b07 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
@@ -1,6 +1,6 @@
import { useDocument } from './DocumentPage.hooks';
import BlockList from '../components/block/BlockList';
-import { BlockContext } from '../utils/block_context';
+import { BlockContext } from '../utils/block';
import { createTheme, ThemeProvider } from '@mui/material';
const theme = createTheme({
@@ -9,20 +9,19 @@ const theme = createTheme({
},
});
export const DocumentPage = () => {
- const { blockId } = useDocument();
+ const { blockId, blockEditor } = useDocument();
- if (!blockId) return
;
+ if (!blockId || !blockEditor) return
;
return (
-
-
-
-
-
+
+
+
);
};
From 92878d7e89982e4a733430194325b86f60c49176 Mon Sep 17 00:00:00 2001
From: Muhammad Rizwan <47111784+rizwan3395@users.noreply.github.com>
Date: Wed, 22 Mar 2023 07:46:46 +0500
Subject: [PATCH 14/31] Cover plugin widget breakdown 1928 (#2007)
* feat: added emoji and network image support
* fix: code cleanup and improvements
* fix: blank preview on invalid image save
* fix: flutter analyzer warnings
* fix: code refactor and bug fixes
* chore: removed unused imports
* chore: formate code
* chore: widget tree breakdown
* chore: added the deleted code
---------
Co-authored-by: ahmeduzair890
Co-authored-by: Lucas.Xu
---
.../plugins/cover/cover_image_picker.dart | 335 ++++++++++--------
1 file changed, 193 insertions(+), 142 deletions(-)
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart
index 32f402cb83..5e6d59be81 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart
@@ -1,3 +1,4 @@
+
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
@@ -25,6 +26,84 @@ class CoverImagePicker extends StatefulWidget {
}
class _CoverImagePickerState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (context) => CoverImagePickerBloc()
+ ..add(const CoverImagePickerEvent.initialEvent()),
+ child: BlocListener(
+ listener: (context, state) {
+ if (state is NetworkImagePicked) {
+ state.successOrFail.isRight()
+ ? showSnapBar(context,
+ LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
+ : null;
+ }
+ if (state is Done) {
+ state.successOrFail.fold(
+ (l) => widget.onFileSubmit(l),
+ (r) => showSnapBar(
+ context,
+ LocaleKeys.document_plugins_cover_failedToAddImageToGallery
+ .tr()));
+ }
+ },
+ child: BlocBuilder(
+ builder: (context, state) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ state is Loading
+ ? const SizedBox(
+ height: 180,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ )
+ : CoverImagePreviewWidget(state: state),
+ const SizedBox(
+ height: 10,
+ ),
+ NetworkImageUrlInput(
+ onAdd: (url) {
+ context.read().add(UrlSubmit(url));
+ },
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ ImagePickerActionButtons(
+ onBackPressed: () {
+ widget.onBackPressed();
+ },
+ onSave: () {
+ context.read().add(
+ SaveToGallery(state),
+ );
+ },
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class NetworkImageUrlInput extends StatefulWidget {
+ final void Function(String color) onAdd;
+
+ const NetworkImageUrlInput({
+ super.key,
+ required this.onAdd,
+ });
+
+ @override
+ State createState() => _NetworkImageUrlInputState();
+}
+
+class _NetworkImageUrlInputState extends State {
TextEditingController urlController = TextEditingController();
bool get buttonDisabled => urlController.text.isEmpty;
@@ -36,6 +115,85 @@ class _CoverImagePickerState extends State {
});
}
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Expanded(
+ flex: 4,
+ child: FlowyTextField(
+ controller: urlController,
+ hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(),
+ ),
+ ),
+ const SizedBox(
+ width: 5,
+ ),
+ Expanded(
+ flex: 1,
+ child: RoundedTextButton(
+ onPressed: () {
+ urlController.text.isNotEmpty
+ ? widget.onAdd(urlController.text)
+ : null;
+ },
+ hoverColor: Colors.transparent,
+ fillColor: buttonDisabled
+ ? Colors.grey
+ : Theme.of(context).colorScheme.primary,
+ height: 36,
+ title: LocaleKeys.document_plugins_cover_add.tr(),
+ borderRadius: Corners.s8Border,
+ ),
+ )
+ ],
+ );
+ }
+}
+
+class ImagePickerActionButtons extends StatelessWidget {
+ final VoidCallback onBackPressed;
+ final VoidCallback onSave;
+
+ const ImagePickerActionButtons(
+ {super.key, required this.onBackPressed, required this.onSave});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ FlowyTextButton(
+ LocaleKeys.document_plugins_cover_back.tr(),
+ hoverColor: Colors.transparent,
+ fillColor: Colors.transparent,
+ mainAxisAlignment: MainAxisAlignment.end,
+ onPressed: () => onBackPressed(),
+ ),
+ FlowyTextButton(
+ LocaleKeys.document_plugins_cover_saveToGallery.tr(),
+ onPressed: () => onSave(),
+ hoverColor: Colors.transparent,
+ fillColor: Colors.transparent,
+ mainAxisAlignment: MainAxisAlignment.end,
+ fontColor: Theme.of(context).colorScheme.primary,
+ ),
+ ],
+ );
+ }
+}
+
+class CoverImagePreviewWidget extends StatefulWidget {
+ final dynamic state;
+
+ const CoverImagePreviewWidget({super.key, required this.state});
+
+ @override
+ State createState() =>
+ _CoverImagePreviewWidgetState();
+}
+
+class _CoverImagePreviewWidgetState extends State {
_buildFilePickerWidget(BuildContext ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -105,150 +263,43 @@ class _CoverImagePickerState extends State {
@override
Widget build(BuildContext context) {
- return BlocProvider(
- create: (context) => CoverImagePickerBloc()
- ..add(const CoverImagePickerEvent.initialEvent()),
- child: BlocListener(
- listener: (context, state) {
- if (state is NetworkImagePicked) {
- state.successOrFail.isRight()
- ? showSnapBar(context,
- LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
- : null;
- }
- if (state is Done) {
- state.successOrFail.fold(
- (l) => widget.onFileSubmit(l),
- (r) => showSnapBar(
- context,
- LocaleKeys.document_plugins_cover_failedToAddImageToGallery
- .tr()));
- }
- },
- child: BlocBuilder(
- builder: (context, state) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- state is Loading
- ? const SizedBox(
- height: 180,
- child: Center(
- child: CircularProgressIndicator(),
+ return Stack(
+ children: [
+ Container(
+ height: 180,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.secondary,
+ borderRadius: Corners.s6Border,
+ image: widget.state is Initial
+ ? null
+ : widget.state is NetworkImagePicked
+ ? widget.state.successOrFail.fold(
+ (path) => DecorationImage(
+ image: NetworkImage(path), fit: BoxFit.cover),
+ (r) => null)
+ : widget.state is FileImagePicked
+ ? DecorationImage(
+ image: FileImage(File(widget.state.path)),
+ fit: BoxFit.cover)
+ : null),
+ child: (widget.state is Initial)
+ ? _buildFilePickerWidget(context)
+ : (widget.state is NetworkImagePicked)
+ ? widget.state.successOrFail.fold(
+ (l) => null,
+ (r) => _buildFilePickerWidget(
+ context,
),
)
- : Stack(
- children: [
- Container(
- height: 180,
- alignment: Alignment.center,
- decoration: BoxDecoration(
- color:
- Theme.of(context).colorScheme.secondary,
- borderRadius: Corners.s6Border,
- image: state is Initial
- ? null
- : state is NetworkImagePicked
- ? state.successOrFail.fold(
- (path) => DecorationImage(
- image: NetworkImage(path),
- fit: BoxFit.cover),
- (r) => null)
- : state is FileImagePicked
- ? DecorationImage(
- image: FileImage(
- File(state.path)),
- fit: BoxFit.cover)
- : null),
- child: (state is Initial)
- ? _buildFilePickerWidget(context)
- : (state is NetworkImagePicked)
- ? state.successOrFail.fold(
- (l) => null,
- (r) => _buildFilePickerWidget(
- context,
- ),
- )
- : null),
- (state is FileImagePicked)
- ? _buildImageDeleteButton(context)
- : (state is NetworkImagePicked)
- ? state.successOrFail.fold(
- (l) => _buildImageDeleteButton(context),
- (r) => Container())
- : Container()
- ],
- ),
- const SizedBox(
- height: 10,
- ),
- Row(
- children: [
- Expanded(
- flex: 4,
- child: FlowyTextField(
- controller: urlController,
- hintText: LocaleKeys
- .document_plugins_cover_enterImageUrl
- .tr(),
- ),
- ),
- const SizedBox(
- width: 5,
- ),
- Expanded(
- flex: 1,
- child: RoundedTextButton(
- onPressed: () {
- urlController.text.isNotEmpty
- ? context
- .read()
- .add(UrlSubmit(urlController.text))
- : null;
- },
- hoverColor: Colors.transparent,
- fillColor: buttonDisabled
- ? Colors.grey
- : Theme.of(context).colorScheme.primary,
- height: 36,
- title: LocaleKeys.document_plugins_cover_add.tr(),
- borderRadius: Corners.s8Border,
- ),
- )
- ],
- ),
- const SizedBox(
- height: 10,
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- FlowyTextButton(
- LocaleKeys.document_plugins_cover_back.tr(),
- hoverColor: Colors.transparent,
- fillColor: Colors.transparent,
- mainAxisAlignment: MainAxisAlignment.end,
- onPressed: () => widget.onBackPressed(),
- ),
- FlowyTextButton(
- LocaleKeys.document_plugins_cover_saveToGallery.tr(),
- onPressed: () async {
- context
- .read()
- .add(SaveToGallery(state));
- },
- hoverColor: Colors.transparent,
- fillColor: Colors.transparent,
- mainAxisAlignment: MainAxisAlignment.end,
- fontColor: Theme.of(context).colorScheme.primary,
- ),
- ],
- )
- ],
- );
- },
- ),
- ),
+ : null),
+ (widget.state is FileImagePicked)
+ ? _buildImageDeleteButton(context)
+ : (widget.state is NetworkImagePicked)
+ ? widget.state.successOrFail.fold(
+ (l) => _buildImageDeleteButton(context), (r) => Container())
+ : Container()
+ ],
);
}
}
From 98f1ac52b4cf05036c10b288e8db682095ad3374 Mon Sep 17 00:00:00 2001
From: "Lucas.Xu"
Date: Wed, 22 Mar 2023 14:49:15 +0800
Subject: [PATCH 15/31] chore: sync release 0.1.1 (#2075)
---
CHANGELOG.md | 12 +
frontend/Makefile.toml | 2 +-
frontend/appflowy_flutter/.metadata | 26 +-
.../appflowy_flutter/android/app/build.gradle | 2 +-
.../android/app/src/debug/AndroidManifest.xml | 2 +-
.../android/app/src/main/AndroidManifest.xml | 2 +-
.../com/example/app_flowy/MainActivity.kt | 2 +-
.../app/src/profile/AndroidManifest.xml | 2 +-
.../assets/translations/en.json | 15 +-
.../assets/translations/pt-BR.json | 1 -
.../ios/Runner.xcodeproj/project.pbxproj | 6 +-
.../plugins/openai/service/openai_client.dart | 25 +-
.../openai/util/learn_more_action.dart | 9 +
.../widgets/auto_completion_node_widget.dart | 98 +++--
.../widgets/auto_completion_plugins.dart | 7 +-
.../openai/widgets/discard_dialog.dart | 28 ++
.../openai/widgets/smart_edit_action.dart | 37 +-
.../widgets/smart_edit_node_widget.dart | 334 ++++++++++++------
.../widgets/smart_edit_toolbar_item.dart | 7 +-
.../appflowy_flutter/lib/util/debounce.dart | 24 ++
.../settings/widgets/settings_user_view.dart | 28 +-
.../appflowy_flutter/linux/CMakeLists.txt | 4 +-
.../linux/appflowy.desktop.temp | 2 +-
.../appflowy_flutter/linux/my_application.cc | 64 ++--
.../macos/Runner/Configs/AppInfo.xcconfig | 6 +-
.../lib/src/commands/command_extension.dart | 4 +-
.../lib/src/core/transform/transaction.dart | 32 +-
.../select_all_handler.dart | 4 +-
.../lib/src/service/selection_service.dart | 23 ++
.../test/command/command_extension_test.dart | 4 +-
.../test/core/transform/transaction_test.dart | 37 ++
.../slash_handler_test.dart | 36 +-
.../test/service/toolbar_service_test.dart | 3 +
.../appflowy_popover/lib/src/popover.dart | 9 +-
.../src/flowy_overlay/appflowy_popover.dart | 18 +-
.../appflowy_flutter/windows/CMakeLists.txt | 6 +-
.../windows/runner/CMakeLists.txt | 27 +-
.../appflowy_flutter/windows/runner/Runner.rc | 20 +-
.../appflowy_flutter/windows/runner/main.cpp | 12 +-
.../io.appflowy.AppFlowy.desktop | 2 +-
.../io.appflowy.AppFlowy.yml | 6 +-
frontend/scripts/linux_installer/postinst | 10 +-
.../windows_installer/inno_setup_config.iss | 6 +-
43 files changed, 720 insertions(+), 284 deletions(-)
create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart
create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart
create mode 100644 frontend/appflowy_flutter/lib/util/debounce.dart
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 392f307d7e..25eef12f1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Release Notes
+## Version 0.1.1 - 03/21/2023
+
+### New features
+
+- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy.
+- Support adding a cover image to your page, making your pages beautiful.
+- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide.
+
+### Bug Fixes
+
+- Fix some bugs
+
## Version 0.1.0 - 02/09/2023
### New features
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index 8ca63bfa72..1fc240a42a 100644
--- a/frontend/Makefile.toml
+++ b/frontend/Makefile.toml
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
-CURRENT_APP_VERSION = "0.1.0"
+CURRENT_APP_VERSION = "0.1.1"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
diff --git a/frontend/appflowy_flutter/.metadata b/frontend/appflowy_flutter/.metadata
index a8ebbd603e..9068867840 100644
--- a/frontend/appflowy_flutter/.metadata
+++ b/frontend/appflowy_flutter/.metadata
@@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
-# This file should be version controlled and should not be manually edited.
+# This file should be version controlled.
version:
- revision: fa5883b78e566877613ad1ccb48dd92075cb5c23
- channel: dev
+ revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ channel: stable
project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: windows
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle
index 216ec0c1fb..948e9b7f42 100644
--- a/frontend/appflowy_flutter/android/app/build.gradle
+++ b/frontend/appflowy_flutter/android/app/build.gradle
@@ -45,7 +45,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "com.example.appflowy_flutter"
+ applicationId "io.appflowy.appflowy"
minSdkVersion 19
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
diff --git a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml
index 49c97148ad..7d5632662e 100644
--- a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml
+++ b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml
@@ -1,5 +1,5 @@
+ package="io.appflowy.appflowy">
diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
index 96e75259ad..264e1d3232 100644
--- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
+++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
+ package="io.appflowy.appflowy">
+ package="io.appflowy.appflowy">
diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json
index 927e968b4e..9496a242b7 100644
--- a/frontend/appflowy_flutter/assets/translations/en.json
+++ b/frontend/appflowy_flutter/assets/translations/en.json
@@ -138,7 +138,8 @@
"keep": "Keep",
"tryAgain": "Try again",
"discard": "Discard",
- "replace": "Replace"
+ "replace": "Replace",
+ "insertBelow": "Insert Below"
},
"label": {
"welcome": "Welcome!",
@@ -345,20 +346,21 @@
"plugins": {
"referencedBoard": "Referenced Board",
"referencedGrid": "Referenced Grid",
- "autoCompletionMenuItemName": "Auto Completion",
- "autoGeneratorMenuItemName": "Auto Generator",
+ "autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate",
- "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
+ "autoGeneratorHintText": "Ask OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
- "smartEdit": "Smart Edit",
- "smartEditTitleName": "OpenAI: Smart Edit",
+ "smartEdit": "AI Assistants",
+ "openAI": "OpenAI",
"smartEditFixSpelling": "Fix spelling",
+ "warning": "β οΈ AI responses can be inaccurate or misleading.",
"smartEditSummarize": "Summarize",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
+ "discardResponse": "Do you want to discard the AI responses?",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
@@ -380,6 +382,7 @@
"imageSavingFailed": "Image Saving Failed",
"addIcon": "Add Icon"
}
+
}
},
"board": {
diff --git a/frontend/appflowy_flutter/assets/translations/pt-BR.json b/frontend/appflowy_flutter/assets/translations/pt-BR.json
index aebe816ff0..512de346d4 100644
--- a/frontend/appflowy_flutter/assets/translations/pt-BR.json
+++ b/frontend/appflowy_flutter/assets/translations/pt-BR.json
@@ -349,7 +349,6 @@
"autoGeneratorGenerate": "Gerar",
"autoGeneratorHintText": "Diga-nos o que vocΓͺ deseja gerar por IA ...",
"autoGeneratorCantGetOpenAIKey": "NΓ£o foi possΓvel obter a chave da OpenAI",
- "smartEditTitleName": "IA: edição inteligente",
"smartEditFixSpelling": "Corrigir ortografia",
"smartEditSummarize": "Resumir",
"smartEditCouldNotFetchResult": "NΓ£o foi possΓvel obter o resultado do OpenAI",
diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
index c5e6758eb6..385c983d7f 100644
--- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -359,7 +359,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+ PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -483,7 +483,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+ PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -502,7 +502,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+ PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
index 24c664abe4..ccb4b08866 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
import 'text_completion.dart';
import 'package:dartz/dartz.dart';
@@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
+ bool useAction = false,
}) async {
final parameters = {
'model': 'text-davinci-003',
@@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
syntax += 1;
- if (syntax == 3) {
- await onStart();
- continue;
- } else if (syntax < 3) {
- continue;
+ if (!useAction) {
+ if (syntax == 3) {
+ await onStart();
+ continue;
+ } else if (syntax < 3) {
+ continue;
+ }
+ } else {
+ if (syntax == 2) {
+ await onStart();
+ continue;
+ } else if (syntax < 2) {
+ continue;
+ }
}
final data = chunk.trim().split('data: ');
- Log.editor.info(data.toString());
if (data.length > 1) {
if (data[1] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
@@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
previousSyntax = response.choices.first.text;
}
} else {
- onEnd();
+ await onEnd();
}
}
}
@@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
OpenAIError.fromJson(json.decode(body)['error']),
);
}
+ return;
}
@override
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart
new file mode 100644
index 0000000000..abdeeb162e
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart
@@ -0,0 +1,9 @@
+import 'package:url_launcher/url_launcher.dart';
+
+Future openLearnMorePage() async {
+ final uri = Uri.parse(
+ 'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai');
+ if (await canLaunchUrl(uri)) {
+ await launchUrl(uri);
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart
index c5d7a39946..046f99f65a 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -9,7 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import 'package:flutter/rendering.dart';
import 'package:http/http.dart' as http;
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
final controller = TextEditingController();
final focusNode = FocusNode();
final textFieldFocusNode = FocusNode();
+ final interceptor = SelectionInterceptor();
@override
void initState() {
@@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.addListener(_onFocusChanged);
textFieldFocusNode.requestFocus();
+ widget.editorState.service.selectionService.register(interceptor
+ ..canTap = (details) {
+ final renderBox = context.findRenderObject() as RenderBox?;
+ if (renderBox != null) {
+ if (!isTapDownDetailsInRenderBox(details, renderBox)) {
+ if (text.isNotEmpty || controller.text.isNotEmpty) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return DiscardDialog(
+ onConfirm: () => _onDiscard(),
+ onCancel: () {},
+ );
+ },
+ );
+ } else if (controller.text.isEmpty) {
+ _onExit();
+ }
+ }
+ }
+ return false;
+ });
+ }
+
+ bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
+ var result = BoxHitTestResult();
+ box.hitTest(result, position: box.globalToLocal(details.globalPosition));
+ return result.path.any((entry) => entry.target == box);
}
@override
@@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.removeListener(_onFocusChanged);
widget.editorState.service.selectionService.currentSelection
.removeListener(_onCancelWhenSelectionChanged);
+ widget.editorState.service.selectionService.unRegister(interceptor);
super.dispose();
}
@@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
fontSize: 14,
),
const Spacer(),
- FlowyText.regular(
- LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
- ),
+ FlowyButton(
+ useIntrinsicWidth: true,
+ text: FlowyText.regular(
+ LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
+ ),
+ onTap: () async {
+ await openLearnMorePage();
+ },
+ )
],
);
}
Widget _buildInputWidget(BuildContext context) {
- return RawKeyboardListener(
- focusNode: focusNode,
- onKey: (RawKeyEvent event) async {
- if (event is! RawKeyDownEvent) return;
- if (event.logicalKey == LogicalKeyboardKey.enter) {
- if (controller.text.isNotEmpty) {
- textFieldFocusNode.unfocus();
- await _onGenerate();
- }
- } else if (event.logicalKey == LogicalKeyboardKey.escape) {
- await _onExit();
- }
- },
- child: FlowyTextField(
- hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
- controller: controller,
- maxLines: 3,
- focusNode: textFieldFocusNode,
- autoFocus: false,
- ),
+ return FlowyTextField(
+ hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
+ controller: controller,
+ maxLines: 3,
+ focusNode: textFieldFocusNode,
+ autoFocus: false,
);
}
@@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
- text: '${LocaleKeys.button_generate.tr()} ',
+ text: LocaleKeys.button_generate.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
- TextSpan(
- text: 'β΅',
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Colors.grey,
- ),
- ),
],
),
onPressed: () async => await _onGenerate(),
@@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
- text: '${LocaleKeys.button_Cancel.tr()} ',
+ text: LocaleKeys.button_Cancel.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
- TextSpan(
- text: LocaleKeys.button_esc.tr(),
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Colors.grey,
- ),
- ),
],
),
onPressed: () async => await _onExit(),
),
+ Expanded(
+ child: Container(
+ alignment: Alignment.centerRight,
+ child: FlowyText.regular(
+ LocaleKeys.document_plugins_warning.tr(),
+ color: Theme.of(context).hintColor,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
],
);
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
index 88ba17b47d..ce9eb5dbef 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
- name: 'Auto Generator',
+ name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
iconData: Icons.generating_tokens,
- keywords: ['autogenerator', 'auto generator'],
+ keywords: ['ai', 'openai' 'writer', 'autogenerator'],
nodeBuilder: (editorState) {
final node = Node(
type: kAutoCompletionInputType,
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart
new file mode 100644
index 0000000000..b2f314c425
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart
@@ -0,0 +1,28 @@
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+
+import 'package:flutter/material.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+
+import 'package:easy_localization/easy_localization.dart';
+
+class DiscardDialog extends StatelessWidget {
+ const DiscardDialog({
+ super.key,
+ required this.onConfirm,
+ required this.onCancel,
+ });
+
+ final VoidCallback onConfirm;
+ final VoidCallback onCancel;
+
+ @override
+ Widget build(BuildContext context) {
+ return NavigatorOkCancelDialog(
+ message: LocaleKeys.document_plugins_discardResponse.tr(),
+ okTitle: LocaleKeys.button_discard.tr(),
+ cancelTitle: LocaleKeys.button_Cancel.tr(),
+ onOkPressed: onConfirm,
+ onCancelPressed: onCancel,
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
index 21678c16e5..053aaaa739 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
@@ -10,11 +10,39 @@ enum SmartEditAction {
String get toInstruction {
switch (this) {
case SmartEditAction.summarize:
- return 'Make this shorter and more concise:';
+ return 'Tl;dr';
case SmartEditAction.fixSpelling:
return 'Correct this to standard English:';
}
}
+
+ String prompt(String input) {
+ switch (this) {
+ case SmartEditAction.summarize:
+ return '$input\n\nTl;dr';
+ case SmartEditAction.fixSpelling:
+ return 'Correct this to standard English:\n\n$input';
+ }
+ }
+
+ static SmartEditAction from(int index) {
+ switch (index) {
+ case 0:
+ return SmartEditAction.summarize;
+ case 1:
+ return SmartEditAction.fixSpelling;
+ }
+ return SmartEditAction.fixSpelling;
+ }
+
+ String get name {
+ switch (this) {
+ case SmartEditAction.summarize:
+ return LocaleKeys.document_plugins_smartEditSummarize.tr();
+ case SmartEditAction.fixSpelling:
+ return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
+ }
+ }
}
class SmartEditActionWrapper extends ActionCell {
@@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
@override
String get name {
- switch (inner) {
- case SmartEditAction.summarize:
- return LocaleKeys.document_plugins_smartEditSummarize.tr();
- case SmartEditAction.fixSpelling:
- return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
- }
+ return inner.name;
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
index 0d9c9c29fa..151ccc60d2 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
@@ -1,19 +1,18 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
+import 'dart:async';
+
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flowy_infra_ui/style_widget/button.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart' as http;
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:appflowy/util/either_extension.dart';
const String kSmartEditType = 'smart_edit_input';
const String kSmartEditInstructionType = 'smart_edit_instruction';
@@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input';
class SmartEditInputBuilder extends NodeWidgetBuilder {
@override
NodeValidator get nodeValidator => (node) {
- return SmartEditAction.values.map((e) => e.toInstruction).contains(
- node.attributes[kSmartEditInstructionType],
- ) &&
+ return SmartEditAction.values
+ .map((e) => e.index)
+ .contains(node.attributes[kSmartEditInstructionType]) &&
node.attributes[kSmartEditInputType] is String;
};
@override
Widget build(NodeWidgetContext context) {
- return _SmartEditInput(
+ return _HoverSmartInput(
key: context.node.key,
node: context.node,
editorState: context.editorState,
@@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder {
}
}
-class _SmartEditInput extends StatefulWidget {
- final Node node;
-
- final EditorState editorState;
- const _SmartEditInput({
- Key? key,
+class _HoverSmartInput extends StatefulWidget {
+ const _HoverSmartInput({
+ required super.key,
required this.node,
required this.editorState,
});
+ final Node node;
+ final EditorState editorState;
+
+ @override
+ State<_HoverSmartInput> createState() => _HoverSmartInputState();
+}
+
+class _HoverSmartInputState extends State<_HoverSmartInput> {
+ final popoverController = PopoverController();
+ final key = GlobalKey(debugLabel: 'smart_edit_input');
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ popoverController.show();
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final width = _maxWidth();
+
+ return AppFlowyPopover(
+ controller: popoverController,
+ direction: PopoverDirection.bottomWithLeftAligned,
+ triggerActions: PopoverTriggerFlags.none,
+ margin: EdgeInsets.zero,
+ constraints: BoxConstraints(maxWidth: width),
+ decoration: FlowyDecoration.decoration(
+ Colors.transparent,
+ Colors.transparent,
+ ),
+ child: const SizedBox(
+ width: double.infinity,
+ ),
+ canClose: () async {
+ final completer = Completer();
+ final state = key.currentState as _SmartEditInputState;
+ if (state.result.isEmpty) {
+ completer.complete(true);
+ } else {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return DiscardDialog(
+ onConfirm: () => completer.complete(true),
+ onCancel: () => completer.complete(false),
+ );
+ },
+ );
+ }
+ return completer.future;
+ },
+ popupBuilder: (BuildContext popoverContext) {
+ return _SmartEditInput(
+ key: key,
+ node: widget.node,
+ editorState: widget.editorState,
+ );
+ },
+ );
+ }
+
+ double _maxWidth() {
+ var width = double.infinity;
+ final editorSize = widget.editorState.renderBox?.size;
+ final padding = widget.editorState.editorStyle.padding;
+ if (editorSize != null && padding != null) {
+ width = editorSize.width - padding.left - padding.right;
+ }
+ return width;
+ }
+}
+
+class _SmartEditInput extends StatefulWidget {
+ const _SmartEditInput({
+ required super.key,
+ required this.node,
+ required this.editorState,
+ });
+
+ final Node node;
+ final EditorState editorState;
+
@override
State<_SmartEditInput> createState() => _SmartEditInputState();
}
class _SmartEditInputState extends State<_SmartEditInput> {
- String get instruction => widget.node.attributes[kSmartEditInstructionType];
+ SmartEditAction get action =>
+ SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
String get input => widget.node.attributes[kSmartEditInputType];
final focusNode = FocusNode();
final client = http.Client();
- dartz.Either? result;
bool loading = true;
+ String result = '';
@override
void initState() {
@@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
widget.editorState.service.keyboardService?.enable();
}
});
- _requestEdits().then(
- (value) => setState(() {
- result = value;
- loading = false;
- }),
- );
+ _requestCompletions();
}
@override
@@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}
Widget _buildSmartEditPanel(BuildContext context) {
- return RawKeyboardListener(
- focusNode: focusNode,
- onKey: (RawKeyEvent event) async {
- if (event is! RawKeyDownEvent) return;
- if (event.logicalKey == LogicalKeyboardKey.enter) {
- await _onReplace();
- await _onExit();
- } else if (event.logicalKey == LogicalKeyboardKey.escape) {
- await _onExit();
- }
- },
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildHeaderWidget(context),
- const Space(0, 10),
- _buildResultWidget(context),
- const Space(0, 10),
- _buildInputFooterWidget(context),
- ],
- ),
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildHeaderWidget(context),
+ const Space(0, 10),
+ _buildResultWidget(context),
+ const Space(0, 10),
+ _buildInputFooterWidget(context),
+ ],
);
}
@@ -128,13 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return Row(
children: [
FlowyText.medium(
- LocaleKeys.document_plugins_smartEditTitleName.tr(),
+ '${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
fontSize: 14,
),
const Spacer(),
- FlowyText.regular(
- LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
- ),
+ FlowyButton(
+ useIntrinsicWidth: true,
+ text: FlowyText.regular(
+ LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
+ ),
+ onTap: () async {
+ await openLearnMorePage();
+ },
+ )
],
);
}
@@ -147,25 +218,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
child: const CircularProgressIndicator(),
),
);
- if (result == null) {
+ if (result.isEmpty) {
return loading;
}
- return result!.fold((error) {
- return Flexible(
- child: Text(
- error.message,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Colors.red,
- ),
- ),
- );
- }, (response) {
- return Flexible(
- child: Text(
- response.choices.map((e) => e.text).join('\n'),
- ),
- );
- });
+ return Flexible(
+ child: Text(
+ result,
+ ),
+ );
}
Widget _buildInputFooterWidget(BuildContext context) {
@@ -175,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
- text: '${LocaleKeys.button_replace.tr()} ',
+ text: LocaleKeys.button_replace.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
- TextSpan(
- text: 'β΅',
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Colors.grey,
- ),
- ),
],
),
- onPressed: () {
- _onReplace();
+ onPressed: () async {
+ await _onReplace();
_onExit();
},
),
@@ -196,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
- text: '${LocaleKeys.button_Cancel.tr()} ',
+ text: LocaleKeys.button_insertBelow.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
+ ],
+ ),
+ onPressed: () async {
+ await _onInsertBelow();
+ _onExit();
+ },
+ ),
+ const Space(10, 0),
+ FlowyRichTextButton(
+ TextSpan(
+ children: [
TextSpan(
- text: LocaleKeys.button_esc.tr(),
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Colors.grey,
- ),
+ text: LocaleKeys.button_Cancel.tr(),
+ style: Theme.of(context).textTheme.bodyMedium,
),
],
),
onPressed: () async => await _onExit(),
),
+ const Spacer(),
+ FlowyText.regular(
+ LocaleKeys.document_plugins_warning.tr(),
+ color: Theme.of(context).hintColor,
+ ),
],
);
}
@@ -219,12 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final selectedNodes = widget
.editorState.service.selectionService.currentSelectedNodes.normalized
.whereType();
- if (selection == null || result == null || result!.isLeft()) {
+ if (selection == null || result.isEmpty) {
return;
}
- final texts = result!.asRight().choices.first.text.split('\n')
- ..removeWhere((element) => element.isEmpty);
+ final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
final transaction = widget.editorState.transaction;
transaction.replaceTexts(
selectedNodes.toList(growable: false),
@@ -234,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return widget.editorState.apply(transaction);
}
+ Future _onInsertBelow() async {
+ final selection = widget.editorState.service.selectionService
+ .currentSelection.value?.normalized;
+ if (selection == null || result.isEmpty) {
+ return;
+ }
+ final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
+ final transaction = widget.editorState.transaction;
+ transaction.insertNodes(
+ selection.normalized.end.path.next,
+ texts.map(
+ (e) => TextNode(
+ delta: Delta()..insert(e),
+ ),
+ ),
+ );
+ return widget.editorState.apply(transaction);
+ }
+
Future _onExit() async {
final transaction = widget.editorState.transaction;
transaction.deleteNode(widget.node);
@@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> {
);
}
- Future> _requestEdits() async {
+ Future _requestCompletions() async {
final result = await UserBackendService.getCurrentUserProfile();
- return result.fold((userProfile) async {
+ return result.fold((l) async {
final openAIRepository = HttpOpenAIRepository(
client: client,
- apiKey: userProfile.openaiKey,
+ apiKey: l.openaiKey,
);
- final edits = await openAIRepository.getEdits(
- input: input,
- instruction: instruction,
- n: 1,
- );
- return edits.fold((error) async {
- return dartz.Left(
- OpenAIError(
- message:
- LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
- ),
+
+ var lines = input.split('\n\n');
+ if (action == SmartEditAction.summarize) {
+ lines = [lines.join('\n')];
+ }
+ for (var i = 0; i < lines.length; i++) {
+ final element = lines[i];
+ await openAIRepository.getStreamedCompletions(
+ useAction: true,
+ prompt: action.prompt(element),
+ onStart: () async {
+ setState(() {
+ loading = false;
+ });
+ },
+ onProcess: (response) async {
+ setState(() {
+ this.result += response.choices.first.text;
+ });
+ },
+ onEnd: () async {
+ setState(() {
+ if (i != lines.length - 1) {
+ this.result += '\n';
+ }
+ });
+ },
+ onError: (error) async {
+ await _showError(error.message);
+ await _onExit();
+ },
);
- }, (textEdit) async {
- return dartz.Right(textEdit);
- });
- }, (error) async {
- // error
- return dartz.Left(
- OpenAIError(
- message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
- ),
- );
+ }
+ }, (r) async {
+ await _showError(r.msg);
+ await _onExit();
});
}
+
+ Future _showError(String message) async {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ action: SnackBarAction(
+ label: LocaleKeys.button_Cancel.tr(),
+ onPressed: () {
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ },
+ ),
+ content: FlowyText(message),
+ ),
+ );
+ }
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
index 844ea8df91..8db094e1dc 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
@@ -101,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
textNodes.normalized,
selection.normalized,
);
+ while (input.last.isEmpty) {
+ input.removeLast();
+ }
final transaction = widget.editorState.transaction;
transaction.insertNode(
selection.normalized.end.path.next,
Node(
type: kSmartEditType,
attributes: {
- kSmartEditInstructionType: actionWrapper.inner.toInstruction,
- kSmartEditInputType: input,
+ kSmartEditInstructionType: actionWrapper.inner.index,
+ kSmartEditInputType: input.join('\n\n'),
},
),
);
diff --git a/frontend/appflowy_flutter/lib/util/debounce.dart b/frontend/appflowy_flutter/lib/util/debounce.dart
new file mode 100644
index 0000000000..324818a650
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/util/debounce.dart
@@ -0,0 +1,24 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class Debounce {
+ final Duration duration;
+ Timer? _timer;
+
+ Debounce({
+ this.duration = const Duration(milliseconds: 1000),
+ });
+
+ void call(VoidCallback action) {
+ dispose();
+ _timer = Timer(duration, () {
+ action();
+ });
+ }
+
+ void dispose() {
+ _timer?.cancel();
+ _timer = null;
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
index 35e786ff38..1b6f7a9a6f 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
@@ -1,4 +1,5 @@
import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/util/debounce.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget {
class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
bool visible = false;
+ final textEditingController = TextEditingController();
+ final debounce = Debounce();
+
+ @override
+ void initState() {
+ super.initState();
+
+ textEditingController.text = widget.openAIKey;
+ }
@override
Widget build(BuildContext context) {
return TextField(
- controller: TextEditingController()..text = widget.openAIKey,
+ controller: textEditingController,
obscureText: !visible,
decoration: InputDecoration(
labelText: 'OpenAI Key',
@@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
},
),
),
- onSubmitted: (val) {
- context
- .read()
- .add(SettingsUserEvent.updateUserOpenAIKey(val));
+ onChanged: (value) {
+ debounce.call(() {
+ context
+ .read()
+ .add(SettingsUserEvent.updateUserOpenAIKey(value));
+ });
},
);
}
+
+ @override
+ void dispose() {
+ debounce.dispose();
+ super.dispose();
+ }
}
class _CurrentIcon extends StatelessWidget {
diff --git a/frontend/appflowy_flutter/linux/CMakeLists.txt b/frontend/appflowy_flutter/linux/CMakeLists.txt
index 26146072f2..3c6927375c 100644
--- a/frontend/appflowy_flutter/linux/CMakeLists.txt
+++ b/frontend/appflowy_flutter/linux/CMakeLists.txt
@@ -1,8 +1,8 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
-set(BINARY_NAME "appflowy_flutter")
-set(APPLICATION_ID "com.example.appflowy_flutter")
+set(BINARY_NAME "AppFlowy")
+set(APPLICATION_ID "io.appflowy.appflowy")
cmake_policy(SET CMP0063 NEW)
diff --git a/frontend/appflowy_flutter/linux/appflowy.desktop.temp b/frontend/appflowy_flutter/linux/appflowy.desktop.temp
index d23fdec42b..2b189ef243 100644
--- a/frontend/appflowy_flutter/linux/appflowy.desktop.temp
+++ b/frontend/appflowy_flutter/linux/appflowy.desktop.temp
@@ -2,7 +2,7 @@
Name=AppFlowy
Comment=An Open Source Alternative to Notion
Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg
-Exec=[CHANGE_THIS]/AppFlowy/appflowy_flutter
+Exec=[CHANGE_THIS]/AppFlowy/AppFlowy
Categories=Office
Type=Application
Terminal=false
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/linux/my_application.cc b/frontend/appflowy_flutter/linux/my_application.cc
index d262d685a7..490ead4cd0 100644
--- a/frontend/appflowy_flutter/linux/my_application.cc
+++ b/frontend/appflowy_flutter/linux/my_application.cc
@@ -7,17 +7,19 @@
#include "flutter/generated_plugin_registrant.h"
-struct _MyApplication {
+struct _MyApplication
+{
GtkApplication parent_instance;
- char** dart_entrypoint_arguments;
+ char **dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
-static void my_application_activate(GApplication* application) {
- MyApplication* self = MY_APPLICATION(application);
- GtkWindow* window =
+static void my_application_activate(GApplication *application)
+{
+ MyApplication *self = MY_APPLICATION(application);
+ GtkWindow *window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
@@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) {
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
- GdkScreen* screen = gtk_window_get_screen(window);
- if (GDK_IS_X11_SCREEN(screen)) {
- const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
- if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ GdkScreen *screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen))
+ {
+ const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0)
+ {
use_header_bar = FALSE;
}
}
#endif
- if (use_header_bar) {
- GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ if (use_header_bar)
+ {
+ GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
- gtk_header_bar_set_title(header_bar, "appflowy_flutter");
+ gtk_header_bar_set_title(header_bar, "AppFlowy");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
- } else {
- gtk_window_set_title(window, "appflowy_flutter");
+ }
+ else
+ {
+ gtk_window_set_title(window, "AppFlowy");
}
gtk_window_set_default_size(window, 1280, 720);
@@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) {
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
- FlView* view = fl_view_new(project);
+ FlView *view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
@@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) {
}
// Implements GApplication::local_command_line.
-static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
- MyApplication* self = MY_APPLICATION(application);
+static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
+{
+ MyApplication *self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
- if (!g_application_register(application, nullptr, &error)) {
- g_warning("Failed to register: %s", error->message);
- *exit_status = 1;
- return TRUE;
+ if (!g_application_register(application, nullptr, &error))
+ {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
}
g_application_activate(application);
@@ -82,21 +91,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch
}
// Implements GObject::dispose.
-static void my_application_dispose(GObject* object) {
- MyApplication* self = MY_APPLICATION(object);
+static void my_application_dispose(GObject *object)
+{
+ MyApplication *self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
-static void my_application_class_init(MyApplicationClass* klass) {
+static void my_application_class_init(MyApplicationClass *klass)
+{
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
-static void my_application_init(MyApplication* self) {}
+static void my_application_init(MyApplication *self) {}
-MyApplication* my_application_new() {
+MyApplication *my_application_new()
+{
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig
index d0b1c2acf9..656857119f 100644
--- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig
+++ b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig
@@ -5,10 +5,10 @@
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = appflowy_flutter
+PRODUCT_NAME = AppFlowy
// The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy
+PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy
// The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright Β© 2021 com.example. All rights reserved.
+PRODUCT_COPYRIGHT = Copyright Β© 2023 AppFlowy.IO. All rights reserved.
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
index 8a9648f6a5..71c3ef4a9d 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
@@ -52,7 +52,7 @@ extension CommandExtension on EditorState {
throw Exception('path and textNode cannot be null at the same time');
}
- String getTextInSelection(
+ List getTextInSelection(
List textNodes,
Selection selection,
) {
@@ -77,6 +77,6 @@ extension CommandExtension on EditorState {
}
}
}
- return res.join('\n');
+ return res;
}
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
index c1a311d648..15bce2a6b7 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
@@ -264,11 +264,11 @@ extension TextTransaction on Transaction {
if (index != 0 && attributes == null) {
newAttributes =
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
- if (newAttributes != null) {
- newAttributes = {...newAttributes}; // make a copy
- } else {
- newAttributes =
- textNode.delta.slice(index, index + length).first.attributes;
+ if (newAttributes == null) {
+ final slicedDelta = textNode.delta.slice(index, index + length);
+ if (slicedDelta.isNotEmpty) {
+ newAttributes = slicedDelta.first.attributes;
+ }
}
}
updateText(
@@ -276,7 +276,7 @@ extension TextTransaction on Transaction {
Delta()
..retain(index)
..delete(length)
- ..insert(text, attributes: newAttributes),
+ ..insert(text, attributes: {...newAttributes ?? {}}),
);
afterSelection = Selection.collapsed(
Position(
@@ -347,24 +347,22 @@ extension TextTransaction on Transaction {
textNode.toPlainText().length,
texts.first,
);
- } else if (i == length - 1) {
+ } else if (i == length - 1 && texts.length >= 2) {
replaceText(
textNode,
0,
selection.endIndex,
texts.last,
);
+ } else if (i < texts.length - 1) {
+ replaceText(
+ textNode,
+ 0,
+ textNode.toPlainText().length,
+ texts[i],
+ );
} else {
- if (i < texts.length - 1) {
- replaceText(
- textNode,
- 0,
- textNode.toPlainText().length,
- texts[i],
- );
- } else {
- deleteNode(textNode);
- }
+ deleteNode(textNode);
}
}
afterSelection = null;
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart
index 800877b435..bc130fd6bf 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart
@@ -8,7 +8,9 @@ ShortcutEventHandler selectAllHandler = (editorState, event) {
if (editorState.document.root.children.isEmpty) {
return KeyEventResult.handled;
}
- final firstNode = editorState.document.root.children.first;
+ final firstNode = editorState.document.root.children.firstWhere(
+ (element) => element is TextNode,
+ );
final lastNode = editorState.document.root.children.last;
var offset = 0;
if (lastNode is TextNode) {
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart
index a2e716c9e2..7578407a09 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart
@@ -82,6 +82,13 @@ abstract class AppFlowySelectionService {
/// The current selection areas's rect in editor.
List get selectionRects;
+
+ void register(SelectionInterceptor interceptor);
+ void unRegister(SelectionInterceptor interceptor);
+}
+
+class SelectionInterceptor {
+ bool Function(TapDownDetails details)? canTap;
}
class AppFlowySelection extends StatefulWidget {
@@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State
selectionRects.clear();
clearSelection();
+ _clearToolbar();
if (selection != null) {
if (selection.isCollapsed) {
@@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State
}
void _onTapDown(TapDownDetails details) {
+ final canTap =
+ _interceptors.every((element) => element.canTap?.call(details) ?? true);
+ if (!canTap) return;
+
// clear old state.
_panStartOffset = null;
@@ -701,4 +713,15 @@ class _AppFlowySelectionState extends State
// }
// }
}
+
+ final List _interceptors = [];
+ @override
+ void register(SelectionInterceptor interceptor) {
+ _interceptors.add(interceptor);
+ }
+
+ @override
+ void unRegister(SelectionInterceptor interceptor) {
+ _interceptors.removeWhere((element) => element == interceptor);
+ }
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
index 3fde48495f..45e5ed24f0 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
@@ -26,11 +26,11 @@ void main() {
.editorState.service.selectionService.currentSelectedNodes
.whereType()
.toList(growable: false);
- final text = editor.editorState.getTextInSelection(
+ final texts = editor.editorState.getTextInSelection(
textNodes.normalized,
selection.normalized,
);
- expect(text, 'me\nto\nAppfl');
+ expect(texts, ['me', 'to', 'Appfl']);
});
});
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
index 27a3701d84..8aa53dcabc 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
@@ -91,6 +91,43 @@ void main() async {
expect(textNodes[3].toPlainText(), 'ABC456789');
});
+ testWidgets('test replaceTexts, textNodes.length >> texts.length',
+ (tester) async {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ final editor = tester.editor
+ ..insertTextNode('0123456789')
+ ..insertTextNode('0123456789')
+ ..insertTextNode('0123456789')
+ ..insertTextNode('0123456789')
+ ..insertTextNode('0123456789');
+ await editor.startTesting();
+ await tester.pumpAndSettle();
+
+ expect(editor.documentLength, 5);
+
+ final selection = Selection(
+ start: Position(path: [0], offset: 4),
+ end: Position(path: [4], offset: 4),
+ );
+ final transaction = editor.editorState.transaction;
+ var textNodes = [0, 1, 2, 3, 4]
+ .map((e) => editor.nodeAtPath([e])!)
+ .whereType()
+ .toList(growable: false);
+ final texts = ['ABC'];
+ transaction.replaceTexts(textNodes, selection, texts);
+ editor.editorState.apply(transaction);
+ await tester.pumpAndSettle();
+
+ expect(editor.documentLength, 1);
+ textNodes = [0]
+ .map((e) => editor.nodeAtPath([e])!)
+ .whereType()
+ .toList(growable: false);
+ expect(textNodes[0].toPlainText(), '0123ABC');
+ });
+
testWidgets('test replaceTexts, textNodes.length < texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
index a6e08d5fa8..fd45401acf 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
@@ -10,7 +10,8 @@ void main() async {
});
group('slash_handler.dart', () {
- testWidgets('Presses / to trigger selection menu', (tester) async {
+ testWidgets('Presses / to trigger selection menu in 0 index',
+ (tester) async {
const text = 'Welcome to Appflowy π';
const lines = 3;
final editor = tester.editor;
@@ -41,5 +42,38 @@ void main() async {
findsNothing,
);
});
+
+ testWidgets('Presses / to trigger selection menu in not 0 index',
+ (tester) async {
+ const text = 'Welcome to Appflowy π';
+ const lines = 3;
+ final editor = tester.editor;
+ for (var i = 0; i < lines; i++) {
+ editor.insertTextNode(text);
+ }
+ await editor.startTesting();
+ await editor.updateSelection(Selection.single(path: [1], startOffset: 5));
+ await editor.pressLogicKey(LogicalKeyboardKey.slash);
+
+ await tester.pumpAndSettle(const Duration(milliseconds: 1000));
+
+ expect(
+ find.byType(SelectionMenuWidget, skipOffstage: false),
+ findsOneWidget,
+ );
+
+ for (final item in defaultSelectionMenuItems) {
+ expect(find.text(item.name), findsOneWidget);
+ }
+
+ await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
+
+ await tester.pumpAndSettle(const Duration(milliseconds: 200));
+
+ expect(
+ find.byType(SelectionMenuItemWidget, skipOffstage: false),
+ findsNothing,
+ );
+ });
});
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart
index 86cd29705d..ce6430903d 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart
@@ -94,6 +94,7 @@ void main() async {
await editor.updateSelection(
Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2),
);
+ await tester.pumpAndSettle(const Duration(milliseconds: 500));
testHighlight(false);
await editor.updateSelection(
@@ -103,6 +104,7 @@ void main() async {
endOffset: text.length * 2,
),
);
+ await tester.pumpAndSettle(const Duration(milliseconds: 500));
testHighlight(true);
await editor.updateSelection(
@@ -112,6 +114,7 @@ void main() async {
endOffset: text.length * 2 - 2,
),
);
+ await tester.pumpAndSettle(const Duration(milliseconds: 500));
testHighlight(true);
});
diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart
index 3cb0972112..fdb3628011 100644
--- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart
@@ -72,6 +72,7 @@ class Popover extends StatefulWidget {
final PopoverDirection direction;
final void Function()? onClose;
+ final Future Function()? canClose;
final bool asBarrier;
@@ -92,6 +93,7 @@ class Popover extends StatefulWidget {
this.mutex,
this.windowPadding,
this.onClose,
+ this.canClose,
this.asBarrier = false,
}) : super(key: key);
@@ -122,7 +124,12 @@ class PopoverState extends State {
children.add(
PopoverMask(
decoration: widget.maskDecoration,
- onTap: () => _removeRootOverlay(),
+ onTap: () async {
+ if (!(await widget.canClose?.call() ?? true)) {
+ return;
+ }
+ _removeRootOverlay();
+ },
onExit: () => _removeRootOverlay(),
),
);
diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
index bceadd6dd0..3673720220 100644
--- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
+++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
@@ -10,11 +10,13 @@ class AppFlowyPopover extends StatelessWidget {
final int triggerActions;
final BoxConstraints constraints;
final void Function()? onClose;
+ final Future Function()? canClose;
final PopoverMutex? mutex;
final Offset? offset;
final bool asBarrier;
final EdgeInsets margin;
final EdgeInsets windowPadding;
+ final Decoration? decoration;
const AppFlowyPopover({
Key? key,
@@ -22,6 +24,7 @@ class AppFlowyPopover extends StatelessWidget {
required this.popupBuilder,
this.direction = PopoverDirection.rightWithTopAligned,
this.onClose,
+ this.canClose,
this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),
this.mutex,
this.triggerActions = PopoverTriggerFlags.click,
@@ -30,6 +33,7 @@ class AppFlowyPopover extends StatelessWidget {
this.asBarrier = false,
this.margin = const EdgeInsets.all(6),
this.windowPadding = const EdgeInsets.all(8.0),
+ this.decoration,
}) : super(key: key);
@override
@@ -37,6 +41,7 @@ class AppFlowyPopover extends StatelessWidget {
return Popover(
controller: controller,
onClose: onClose,
+ canClose: canClose,
direction: direction,
mutex: mutex,
asBarrier: asBarrier,
@@ -49,6 +54,7 @@ class AppFlowyPopover extends StatelessWidget {
return _PopoverContainer(
constraints: constraints,
margin: margin,
+ decoration: decoration,
child: child,
);
},
@@ -61,19 +67,23 @@ class _PopoverContainer extends StatelessWidget {
final Widget child;
final BoxConstraints constraints;
final EdgeInsets margin;
+ final Decoration? decoration;
+
const _PopoverContainer({
required this.child,
required this.margin,
required this.constraints,
+ required this.decoration,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
- final decoration = FlowyDecoration.decoration(
- Theme.of(context).colorScheme.surface,
- Theme.of(context).colorScheme.shadow.withOpacity(0.15),
- );
+ final decoration = this.decoration ??
+ FlowyDecoration.decoration(
+ Theme.of(context).colorScheme.surface,
+ Theme.of(context).colorScheme.shadow.withOpacity(0.15),
+ );
return Material(
type: MaterialType.transparency,
diff --git a/frontend/appflowy_flutter/windows/CMakeLists.txt b/frontend/appflowy_flutter/windows/CMakeLists.txt
index 066bfe709e..5be6e64915 100644
--- a/frontend/appflowy_flutter/windows/CMakeLists.txt
+++ b/frontend/appflowy_flutter/windows/CMakeLists.txt
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.14)
project(appflowy_flutter LANGUAGES CXX)
-set(BINARY_NAME "appflowy_flutter")
+set(BINARY_NAME "AppFlowy")
cmake_policy(SET CMP0063 NEW)
@@ -9,6 +9,7 @@ set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Configure build options.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
@@ -50,14 +51,15 @@ add_subdirectory("runner")
# them to the application.
include(flutter/generated_plugins.cmake)
-
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$")
+
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
diff --git a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt b/frontend/appflowy_flutter/windows/runner/CMakeLists.txt
index df6e8b99b5..17411a8ab8 100644
--- a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt
+++ b/frontend/appflowy_flutter/windows/runner/CMakeLists.txt
@@ -1,6 +1,11 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
@@ -10,13 +15,25 @@ add_executable(${BINARY_NAME} WIN32
"Runner.rc"
"runner.exe.manifest"
)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
-
-
-# === Flutter Library ===
-#set(DART_FFI "${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/dart_ffi.dll")
-#set(DART_FFI ${DART_FFI} PARENT_SCOPE)
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/windows/runner/Runner.rc b/frontend/appflowy_flutter/windows/runner/Runner.rc
index 1f8c964ed2..0639f41ec5 100644
--- a/frontend/appflowy_flutter/windows/runner/Runner.rc
+++ b/frontend/appflowy_flutter/windows/runner/Runner.rc
@@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
// Version
//
-#ifdef FLUTTER_BUILD_NUMBER
-#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
-#define VERSION_AS_NUMBER 1,0,0
+#define VERSION_AS_NUMBER 1,0,0,0
#endif
-#ifdef FLUTTER_BUILD_NAME
-#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
@@ -89,13 +89,13 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
- VALUE "CompanyName", "com.example" "\0"
+ VALUE "CompanyName", "io.appflowy" "\0"
VALUE "FileDescription", "AppFlowy" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
- VALUE "InternalName", "appflowy_flutter" "\0"
- VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0"
- VALUE "OriginalFilename", "appflowy_flutter.exe" "\0"
- VALUE "ProductName", "appflowy_flutter" "\0"
+ VALUE "InternalName", "AppFlowy" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2023 io.appflowy. All rights reserved." "\0"
+ VALUE "OriginalFilename", "AppFlowy.exe" "\0"
+ VALUE "ProductName", "AppFlowy" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
diff --git a/frontend/appflowy_flutter/windows/runner/main.cpp b/frontend/appflowy_flutter/windows/runner/main.cpp
index 8f38159b86..2f7c10b343 100644
--- a/frontend/appflowy_flutter/windows/runner/main.cpp
+++ b/frontend/appflowy_flutter/windows/runner/main.cpp
@@ -6,10 +6,12 @@
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
- _In_ wchar_t *command_line, _In_ int show_command) {
+ _In_ wchar_t *command_line, _In_ int show_command)
+{
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
- if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
+ {
CreateAndAttachConsole();
}
@@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
- if (!window.CreateAndShow(L"appflowy_flutter", origin, size)) {
+ if (!window.CreateAndShow(L"AppFlowy", origin, size))
+ {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
- while (::GetMessage(&msg, nullptr, 0, 0)) {
+ while (::GetMessage(&msg, nullptr, 0, 0))
+ {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop
index 1e01eb82bb..8b51c0c2ea 100644
--- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop
+++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop
@@ -2,6 +2,6 @@
Type=Application
Name=AppFlowy
Icon=io.appflowy.AppFlowy
-Exec=env GDK_GL=gles appflowy_flutter %U
+Exec=env GDK_GL=gles AppFlowy %U
Categories=Network;Productivity;
Keywords=Notes
diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml
index 96bcd02e2a..4a0af64e47 100644
--- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml
+++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml
@@ -2,7 +2,7 @@ app-id: io.appflowy.AppFlowy
runtime: org.freedesktop.Platform
runtime-version: '21.08'
sdk: org.freedesktop.Sdk
-command: appflowy_flutter
+command: AppFlowy
separate-locales: false
finish-args:
- --share=ipc
@@ -18,10 +18,10 @@ modules:
build-commands:
# - ls .
- cp -r appflowy /app/appflowy
- - chmod +x /app/appflowy/appflowy_flutter
+ - chmod +x /app/appflowy/AppFlowy
- install -Dm644 logo.svg /app/share/icons/hicolor/scalable/apps/io.appflowy.AppFlowy.svg
- mkdir /app/bin
- - ln -s /app/appflowy/appflowy_flutter /app/bin/appflowy_flutter
+ - ln -s /app/appflowy/AppFlowy /app/bin/AppFlowy
- install -Dm644 io.appflowy.AppFlowy.desktop /app/share/applications/io.appflowy.AppFlowy.desktop
sources:
- type: archive
diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst
index 9255abb598..4f495f86a2 100644
--- a/frontend/scripts/linux_installer/postinst
+++ b/frontend/scripts/linux_installer/postinst
@@ -1,7 +1,7 @@
#!/bin/bash
-if [ -e /usr/local/bin/appflowy ]; then
-echo "Symlink already exists, skipping."
+if [ -e /usr/local/bin/AppFlowy ]; then
+ echo "Symlink already exists, skipping."
else
-echo "Creating Symlink in /usr/local/bin/appflowy"
-ln -s /opt/AppFlowy/appflowy_flutter /usr/local/bin/appflowy
-fi
\ No newline at end of file
+ echo "Creating Symlink in /usr/local/bin/appflowy"
+ ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy
+fi
diff --git a/frontend/scripts/windows_installer/inno_setup_config.iss b/frontend/scripts/windows_installer/inno_setup_config.iss
index adb1e30fc1..dc477d384a 100644
--- a/frontend/scripts/windows_installer/inno_setup_config.iss
+++ b/frontend/scripts/windows_installer/inno_setup_config.iss
@@ -7,15 +7,15 @@ SolidCompression=yes
DefaultDirName={autopf}\AppFlowy\
DefaultGroupName=AppFlowy
SetupIconFile=flowy_logo.ico
-UninstallDisplayIcon={app}\appflowy_flutter.exe
+UninstallDisplayIcon={app}\AppFlowy.exe
UninstallDisplayName=AppFlowy
AppPublisher=AppFlowy-IO
VersionInfoVersion={#AppVersion}
[Files]
-Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "appflowy_flutter.exe"
+Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe"
Source: "AppFlowy\*";DestDir: "{app}"
Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs
[Icons]
-Name: "{group}\AppFlowy";Filename: "{app}\appflowy_flutter.exe"
\ No newline at end of file
+Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe"
\ No newline at end of file
From 3039f0427fe7b12a257b5065cfbedf6198da5a50 Mon Sep 17 00:00:00 2001
From: Gunjan Lunkad <67200542+glunkad@users.noreply.github.com>
Date: Thu, 23 Mar 2023 14:00:52 +0530
Subject: [PATCH 16/31] fix : Code block parser implementation (#2046)
* fix: Code block implementation
* fix: Added test to cover CodeBlock
* fix: typos
* fix: Code block implementation
* fix: typos
---
.../document/application/share_bloc.dart | 2 ++
.../parsers/code_block_node_parser.dart | 13 +++++++++
.../unit_test/editor/share_markdown_test.dart | 27 ++++++++++++++++++-
3 files changed, 41 insertions(+), 1 deletion(-)
create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart
diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart
index 92c2f347a2..ddb4fe915d 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/plugins/document/application/share_service.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -53,6 +54,7 @@ class DocShareBloc extends Bloc {
return documentToMarkdown(document, customParsers: [
const DividerNodeParser(),
const MathEquationNodeParser(),
+ const CodeBlockNodeParser(),
]);
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart
new file mode 100644
index 0000000000..88ec444dec
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart
@@ -0,0 +1,13 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+class CodeBlockNodeParser extends NodeParser {
+ const CodeBlockNodeParser();
+
+ @override
+ String get id => 'code_block';
+
+ @override
+ String transform(Node node) {
+ return '```\n${node.attributes['code_block']}\n```';
+ }
+}
diff --git a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart
index 77311f395d..48afb2b614 100644
--- a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart
+++ b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart
@@ -1,5 +1,6 @@
import 'dart:convert';
+import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -31,7 +32,31 @@ void main() {
]);
expect(result, r'$$E = MC^2$$');
});
-
+ // Changes
+ test('code block', () {
+ const text = '''
+{
+ "document":{
+ "type":"editor",
+ "children":[
+ {
+ "type":"code_block",
+ "attributes":{
+ "code_block":"Some Code"
+ }
+ }
+ ]
+ }
+}
+''';
+ final document = Document.fromJson(
+ Map.from(json.decode(text)),
+ );
+ final result = documentToMarkdown(document, customParsers: [
+ const CodeBlockNodeParser(),
+ ]);
+ expect(result, '```\nSome Code\n```');
+ });
test('divider', () {
const text = '''
{
From 1536cdd15a5cfc94475bb77a4c27c35c2e228991 Mon Sep 17 00:00:00 2001
From: "Lucas.Xu"
Date: Fri, 24 Mar 2023 16:57:35 +0800
Subject: [PATCH 17/31] feat: trigger shortcut event by character (#2101)
---
.../lib/src/service/keyboard_service.dart | 9 ++-
.../built_in_shortcut_events.dart | 4 +-
.../shortcut_event/shortcut_event.dart | 27 ++++++--
frontend/appflowy_flutter/pubspec.lock | 66 +++++++++----------
4 files changed, 66 insertions(+), 40 deletions(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart
index b903fed4f8..2068a2a0f0 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart
@@ -129,7 +129,7 @@ class _AppFlowyKeyboardState extends State
// TODO: use cache to optimize the searching time.
for (final shortcutEvent in widget.shortcutEvents) {
- if (shortcutEvent.keybindings.containsKeyEvent(event)) {
+ if (shortcutEvent.canRespondToRawKeyEvent(event)) {
final result = shortcutEvent.handler(widget.editorState, event);
if (result == KeyEventResult.handled) {
return KeyEventResult.handled;
@@ -157,3 +157,10 @@ class _AppFlowyKeyboardState extends State
return onKey(event);
}
}
+
+extension on ShortcutEvent {
+ bool canRespondToRawKeyEvent(RawKeyEvent event) {
+ return ((character?.isNotEmpty ?? false) && character == event.character) ||
+ keybindings.containsKeyEvent(event);
+ }
+}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
index b4566acf92..825dc0b297 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
@@ -247,7 +247,7 @@ List builtInShortcutEvents = [
),
ShortcutEvent(
key: 'selection menu',
- command: 'slash,shift+slash',
+ character: '/',
handler: slashShortcutHandler,
),
ShortcutEvent(
@@ -304,7 +304,7 @@ List builtInShortcutEvents = [
),
ShortcutEvent(
key: 'Underscore to italic',
- command: 'shift+underscore',
+ character: '_',
handler: underscoreToItalicHandler,
),
ShortcutEvent(
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart
index fb1a245b00..bba5aff1c3 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart
@@ -8,12 +8,29 @@ import 'package:flutter/foundation.dart';
class ShortcutEvent {
ShortcutEvent({
required this.key,
- required this.command,
+ this.character,
+ this.command,
required this.handler,
String? windowsCommand,
String? macOSCommand,
String? linuxCommand,
}) {
+ // character and command cannot be null at the same time
+ assert(
+ !(character == null &&
+ command == null &&
+ windowsCommand == null &&
+ macOSCommand == null &&
+ linuxCommand == null),
+ 'character and command cannot be null at the same time');
+ assert(
+ !(character != null &&
+ (command != null &&
+ windowsCommand != null &&
+ macOSCommand != null &&
+ linuxCommand != null)),
+ 'character and command cannot be set at the same time');
+
updateCommand(
command: command,
windowsCommand: windowsCommand,
@@ -43,7 +60,9 @@ class ShortcutEvent {
///
/// Like, 'ctrl+c,cmd+c'
///
- String command;
+ String? command;
+
+ String? character;
final ShortcutEventHandler handler;
@@ -80,9 +99,9 @@ class ShortcutEvent {
matched = true;
}
- if (matched) {
+ if (matched && this.command != null) {
_keybindings = this
- .command
+ .command!
.split(',')
.map((e) => Keybinding.parse(e))
.toList(growable: false);
diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock
index f880223728..f6e21acf11 100644
--- a/frontend/appflowy_flutter/pubspec.lock
+++ b/frontend/appflowy_flutter/pubspec.lock
@@ -121,7 +121,7 @@ packages:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
- version: "3.1.0"
+ version: "3.1.1"
build_resolvers:
dependency: transitive
description:
@@ -156,7 +156,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
- version: "8.4.3"
+ version: "8.4.4"
calendar_view:
dependency: "direct main"
description:
@@ -500,7 +500,7 @@ packages:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.7"
+ version: "2.0.9"
flutter_svg:
dependency: transitive
description:
@@ -524,7 +524,7 @@ packages:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
- version: "8.1.2"
+ version: "8.2.1"
freezed:
dependency: "direct dev"
description:
@@ -599,7 +599,7 @@ packages:
name: html
url: "https://pub.dartlang.org"
source: hosted
- version: "0.15.1"
+ version: "0.15.2"
http:
dependency: "direct main"
description:
@@ -709,7 +709,7 @@ packages:
name: logger
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.3.0"
logging:
dependency: transitive
description:
@@ -779,7 +779,7 @@ packages:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.1"
+ version: "2.0.2"
package_config:
dependency: transitive
description:
@@ -856,35 +856,35 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.12"
+ version: "2.0.14"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.22"
+ version: "2.0.24"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.1"
+ version: "2.2.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.8"
+ version: "2.1.10"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.5"
+ version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
@@ -898,7 +898,7 @@ packages:
name: percent_indicator
url: "https://pub.dartlang.org"
source: hosted
- version: "4.2.2"
+ version: "4.2.3"
petitparser:
dependency: transitive
description:
@@ -919,7 +919,7 @@ packages:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.3"
+ version: "2.1.4"
pool:
dependency: transitive
description:
@@ -1024,7 +1024,7 @@ packages:
name: rich_clipboard_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.1"
+ version: "1.0.2"
screen_retriever:
dependency: transitive
description:
@@ -1038,49 +1038,49 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.17"
+ version: "2.0.20"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.15"
+ version: "2.0.17"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.3"
+ version: "2.1.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.3"
+ version: "2.1.5"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.0"
+ version: "2.1.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.4"
+ version: "2.0.6"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.3"
+ version: "2.1.5"
shelf:
dependency: transitive
description:
@@ -1122,7 +1122,7 @@ packages:
name: sized_context
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.0+1"
+ version: "1.0.0+4"
sky_engine:
dependency: transitive
description: flutter
@@ -1155,7 +1155,7 @@ packages:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
- version: "0.10.11"
+ version: "0.10.12"
source_span:
dependency: transitive
description:
@@ -1295,56 +1295,56 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
- version: "6.1.9"
+ version: "6.1.10"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
- version: "6.0.23"
+ version: "6.0.26"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
- version: "6.1.0"
+ version: "6.1.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.2"
+ version: "3.0.4"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.2"
+ version: "3.0.4"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.1"
+ version: "2.1.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.14"
+ version: "2.0.16"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.3"
+ version: "3.0.5"
uuid:
dependency: transitive
description:
@@ -1407,7 +1407,7 @@ packages:
name: window_manager
url: "https://pub.dartlang.org"
source: hosted
- version: "0.3.0"
+ version: "0.3.1"
xdg_directories:
dependency: transitive
description:
From f9a1cb26233eeb7d80a6354913dc68ed279d2975 Mon Sep 17 00:00:00 2001
From: Mihir <84044317+squidrye@users.noreply.github.com>
Date: Mon, 27 Mar 2023 07:50:01 +0530
Subject: [PATCH 18/31] fix: open-ai replace does not work in certain use-cases
(#2100)
* test: added test to verify correct ordering after replacement of multiline text-nodes
* fix: open-ai replace does not work on certain use-cases
* refactor: using predefined operation insert node to create new nodes.
* Revert "refactor: using predefined operation insert node to create new nodes."
This reverts commit bcc014e84d09633ee14d5090f06e609fa95af481.
* refactor: using predefined operation insert node to create new nodes.
* fix: open-ai replace does not work in certain use-cases
* fix: fixed logic and tests for replacement of larger textNodes with smaller text.
---------
Co-authored-by: Lucas.Xu
---
.../lib/src/core/transform/transaction.dart | 45 ++++++++++++++++---
.../test/core/transform/transaction_test.dart | 39 +++++++++++++++-
2 files changed, 76 insertions(+), 8 deletions(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
index 15bce2a6b7..6c369f566e 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
@@ -363,6 +363,19 @@ extension TextTransaction on Transaction {
);
} else {
deleteNode(textNode);
+ if (i == textNodes.length - 1) {
+ final delta = Delta()
+ ..insert(texts[0])
+ ..addAll(
+ textNodes.last.delta.slice(selection.end.offset),
+ );
+ replaceText(
+ textNode,
+ selection.start.offset,
+ texts[0].length,
+ delta.toPlainText(),
+ );
+ }
}
}
afterSelection = null;
@@ -371,6 +384,8 @@ extension TextTransaction on Transaction {
if (textNodes.length < texts.length) {
final length = texts.length;
+ var path = textNodes.first.path;
+
for (var i = 0; i < texts.length; i++) {
final text = texts[i];
if (i == 0) {
@@ -380,13 +395,15 @@ extension TextTransaction on Transaction {
textNodes.first.toPlainText().length,
text,
);
- } else if (i == length - 1) {
+ path = path.next;
+ } else if (i == length - 1 && textNodes.length >= 2) {
replaceText(
textNodes.last,
0,
selection.endIndex,
text,
);
+ path = path.next;
} else {
if (i < textNodes.length - 1) {
replaceText(
@@ -395,14 +412,28 @@ extension TextTransaction on Transaction {
textNodes[i].toPlainText().length,
text,
);
+ path = path.next;
} else {
- var path = textNodes.first.path;
- var j = i - textNodes.length + length - 1;
- while (j > 0) {
- path = path.next;
- j--;
+ if (i == texts.length - 1) {
+ final delta = Delta()
+ ..insert(text)
+ ..addAll(
+ textNodes.last.delta.slice(selection.end.offset),
+ );
+ insertNode(
+ path,
+ TextNode(
+ delta: delta,
+ ),
+ );
+ } else {
+ insertNode(
+ path,
+ TextNode(
+ delta: Delta()..insert(text),
+ ),
+ );
}
- insertNode(path, TextNode(delta: Delta()..insert(text)));
}
}
}
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
index 8aa53dcabc..bfb9fa2b3a 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
@@ -125,7 +125,7 @@ void main() async {
.map((e) => editor.nodeAtPath([e])!)
.whereType()
.toList(growable: false);
- expect(textNodes[0].toPlainText(), '0123ABC');
+ expect(textNodes[0].toPlainText(), '0123ABC456789');
});
testWidgets('test replaceTexts, textNodes.length < texts.length',
@@ -165,5 +165,42 @@ void main() async {
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
+
+ testWidgets('test replaceTexts, textNodes.length << texts.length',
+ (tester) async {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ final editor = tester.editor..insertTextNode('Welcome to AppFlowy!');
+ await editor.startTesting();
+ await tester.pumpAndSettle();
+
+ expect(editor.documentLength, 1);
+
+ // select 'to'
+ final selection = Selection(
+ start: Position(path: [0], offset: 8),
+ end: Position(path: [0], offset: 10),
+ );
+ final transaction = editor.editorState.transaction;
+ var textNodes = [0]
+ .map((e) => editor.nodeAtPath([e])!)
+ .whereType()
+ .toList(growable: false);
+ final texts = ['ABC1', 'ABC2', 'ABC3', 'ABC4', 'ABC5'];
+ transaction.replaceTexts(textNodes, selection, texts);
+ editor.editorState.apply(transaction);
+ await tester.pumpAndSettle();
+
+ expect(editor.documentLength, 5);
+ textNodes = [0, 1, 2, 3, 4]
+ .map((e) => editor.nodeAtPath([e])!)
+ .whereType()
+ .toList(growable: false);
+ expect(textNodes[0].toPlainText(), 'Welcome ABC1');
+ expect(textNodes[1].toPlainText(), 'ABC2');
+ expect(textNodes[2].toPlainText(), 'ABC3');
+ expect(textNodes[3].toPlainText(), 'ABC4');
+ expect(textNodes[4].toPlainText(), 'ABC5 AppFlowy!');
+ });
});
}
From f40d1a9a963a784a934667019a19835f0281a322 Mon Sep 17 00:00:00 2001
From: Om Gujarathi <98649066+Om-Gujarathi@users.noreply.github.com>
Date: Mon, 27 Mar 2023 07:50:21 +0530
Subject: [PATCH 19/31] fix: double asterisk to bold fixes #2080 (#2093)
* fix: double asterisk to bold fixed
* double asterisk to bold fixed
---
.../src/service/shortcut_event/built_in_shortcut_events.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
index 825dc0b297..d9a0470f3d 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
@@ -309,7 +309,7 @@ List builtInShortcutEvents = [
),
ShortcutEvent(
key: 'Double asterisk to bold',
- command: 'shift+digit 8',
+ character: '*',
handler: doubleAsteriskToBoldHandler,
),
ShortcutEvent(
From 5afdb5de354c3171eadef64366f9c2267743cb78 Mon Sep 17 00:00:00 2001
From: Akheel Muhammed <73631606+not-shoyo@users.noreply.github.com>
Date: Mon, 27 Mar 2023 10:17:22 +0530
Subject: [PATCH 20/31] Fix: #2028 auto enter edit mode for text field. (#2096)
* chore(): initial commit to make AppFlowy Work
Signed-off-by: not-shoyo
* fix: #2028, fix auto-entering edit mode
Make text field request for focus when popover changes.
Signed-off-by: not-shoyo
* Update section.dart
* Update input_service.dart
* Update input_service.dart
* Update input_service.dart
---------
Signed-off-by: not-shoyo
---
.../grid/presentation/widgets/header/field_editor.dart | 2 ++
.../workspace/presentation/home/menu/app/section/section.dart | 1 -
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
index 29e38e7a72..ff0abf92af 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
@@ -147,6 +147,8 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
widget.popoverMutex.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
+ } else {
+ focusNode.requestFocus();
}
});
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
index b16d9b0e57..c134ca8985 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
@@ -58,7 +58,6 @@ class ViewSection extends StatelessWidget {
.read()
.add(ViewSectionEvent.moveView(oldIndex, index));
},
- ignorePrimaryScrollController: true,
children: children,
);
}
From c73b7d1184be2ed440d5a9a6f9c98d6f013179bd Mon Sep 17 00:00:00 2001
From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Date: Mon, 27 Mar 2023 06:47:41 +0200
Subject: [PATCH 21/31] fix: allow # in links when pasting (#2110)
Closes: #1871
---
.../service/internal_key_event_handlers/copy_paste_handler.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
index 3110a0e559..2a72400b1d 100644
--- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
+++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
@@ -232,7 +232,7 @@ void _pasteSingleLine(
/// parse url from the line text
/// reference: https://stackoverflow.com/questions/59444837/flutter-dart-regex-to-extract-urls-from-a-string
Delta _lineContentToDelta(String lineContent) {
- final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
+ final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\#\w/\-?=%.]+');
final Iterable matches = exp.allMatches(lineContent);
final delta = Delta();
From d62493ce012feca7a26ec8903387703b0a0f88d3 Mon Sep 17 00:00:00 2001
From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Date: Mon, 27 Mar 2023 06:48:07 +0200
Subject: [PATCH 22/31] fix: dropdown state unchanging after file reset (#2109)
---
.../lib/workspace/application/appearance.dart | 4 ++--
.../settings/widgets/settings_language_view.dart | 14 ++++++--------
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart
index b5c3ecb9bf..9cadf7f5ce 100644
--- a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart
+++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart
@@ -55,9 +55,9 @@ class AppearanceSettingsCubit extends Cubit {
newLocale = const Locale('en');
}
- context.setLocale(newLocale);
-
if (state.locale != newLocale) {
+ context.setLocale(newLocale);
+
_setting.locale.languageCode = newLocale.languageCode;
_setting.locale.countryCode = newLocale.countryCode ?? "";
_saveAppearanceSettings();
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
index 20cb1eaafa..7b76aa6157 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
@@ -54,8 +54,8 @@ class _LanguageSelectorDropdownState extends State {
@override
Widget build(BuildContext context) {
return MouseRegion(
- onEnter: (event) => {hoverEnterLanguage()},
- onExit: (event) => {hoverExitLanguage()},
+ onEnter: (_) => hoverEnterLanguage(),
+ onExit: (_) => hoverExitLanguage(),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
@@ -67,12 +67,10 @@ class _LanguageSelectorDropdownState extends State {
padding: const EdgeInsets.symmetric(horizontal: 6),
child: DropdownButton(
value: context.locale,
- onChanged: (val) {
- setState(() {
- context
- .read()
- .setLocale(context, val!);
- });
+ onChanged: (locale) {
+ context
+ .read()
+ .setLocale(context, locale!);
},
autofocus: true,
borderRadius: BorderRadius.circular(8),
From 2a55febe623ae84ad9c58fe20bcfa2447ea88286 Mon Sep 17 00:00:00 2001
From: Aman Negi <37607224+AmanNegi@users.noreply.github.com>
Date: Mon, 27 Mar 2023 10:18:26 +0530
Subject: [PATCH 23/31] fix: Update app version in `pubspec.yaml` (#2104)
* fix: Update app version in `pubspec.yaml`
* fix: Remove Build Number
- Removed unnecessary build number mentioned in the `pubspec.yaml`
---
.../presentation/widgets/float_bubble/question_bubble.dart | 3 +--
frontend/appflowy_flutter/pubspec.yaml | 4 ++--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
index bce35021ef..c6bb7f3ca0 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
@@ -135,7 +135,6 @@ class FlowyVersionDescription extends CustomActionCell {
PackageInfo packageInfo = snapshot.data;
String appName = packageInfo.appName;
String version = packageInfo.version;
- String buildNumber = packageInfo.buildNumber;
return SizedBox(
height: 30,
@@ -149,7 +148,7 @@ class FlowyVersionDescription extends CustomActionCell {
thickness: 1.0),
const VSpace(6),
FlowyText(
- "$appName $version.$buildNumber",
+ "$appName $version",
color: Theme.of(context).hintColor,
),
],
diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml
index b79bcc5fbe..b9d5b4a875 100644
--- a/frontend/appflowy_flutter/pubspec.yaml
+++ b/frontend/appflowy_flutter/pubspec.yaml
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+version: 0.1.1
environment:
sdk: ">=2.18.0 <3.0.0"
@@ -89,7 +89,7 @@ dependencies:
google_fonts: ^3.0.1
file_picker: <=5.0.0
percent_indicator: ^4.0.1
-
+
appflowy_editor_plugins:
path: packages/appflowy_editor_plugins
calendar_view: ^1.0.1
From 03cd9a69936ad647ade84da2e5ef0d470baccfd9 Mon Sep 17 00:00:00 2001
From: qinluhe <108015703+qinluhe@users.noreply.github.com>
Date: Mon, 27 Mar 2023 17:55:24 +0800
Subject: [PATCH 24/31] Refactor tauri document (#2117)
* fix: Optimize the re-render node when the selection changes
* feat: the feature of delete block
* feat: add left tool when hover on block
* refactor: document data and update
* refactor: document component
* refactor: document controller
---
frontend/appflowy_tauri/package.json | 8 +-
frontend/appflowy_tauri/pnpm-lock.yaml | 85 +-
.../block_editor/blocks/text_block/index.ts | 71 --
.../blocks/text_block/text_selection.ts | 35 -
.../appflowy_app/block_editor/core/block.ts | 107 ---
.../block_editor/core/block_chain.ts | 225 -----
.../block_editor/core/op_adapter.ts | 16 -
.../block_editor/core/operation.ts | 153 ----
.../appflowy_app/block_editor/core/sync.ts | 48 --
.../src/appflowy_app/block_editor/index.ts | 60 --
.../block_editor/view/block_position.ts | 73 --
.../appflowy_app/block_editor/view/tree.ts | 165 ----
.../block_editor/view/tree_node.ts | 59 --
.../components/HoveringToolbar/Portal.tsx | 9 -
.../BlockComponent/BlockComponet.hooks.ts | 36 -
.../components/block/BlockComponent/index.tsx | 91 --
.../block/BlockList/BlockList.hooks.tsx | 92 --
.../block/BlockList/BlockListTitle.tsx | 18 -
.../block/BlockList/ListFallbackComponent.tsx | 31 -
.../components/block/BlockList/index.tsx | 58 --
.../components/block/BlockSelection/index.tsx | 18 -
.../components/block/CodeBlock/index.tsx | 6 -
.../components/block/HeadingBlock/index.tsx | 17 -
.../block/ListBlock/ColumnListBlock.tsx | 18 -
.../block/ListBlock/NumberedListBlock.tsx | 31 -
.../components/block/PageBlock/index.tsx | 6 -
.../components/block/TextBlock/index.hooks.ts | 98 ---
.../components/block/TextBlock/index.tsx | 43 -
.../components/document/BlockPortal/index.tsx | 9 +
.../BlockSelection/BlockSelection.hooks.tsx | 53 +-
.../document/BlockSelection/index.tsx | 23 +
.../BlockSideTools/BlockSideTools.hooks.tsx | 126 +++
.../document/BlockSideTools/index.tsx | 36 +
.../components/document/CodeBlock/index.tsx | 3 +
.../{block => document}/ColumnBlock/index.tsx | 21 +-
.../DocumentTitle/DocumentTitle.hooks.ts | 8 +
.../document/DocumentTitle/index.tsx | 13 +
.../document/HeadingBlock/index.tsx | 17 +
.../HoveringToolbar/FormatButton.tsx | 0
.../HoveringToolbar/FormatIcon.tsx | 0
.../HoveringToolbar/index.hooks.ts | 8 +-
.../{ => document}/HoveringToolbar/index.tsx | 9 +-
.../ListBlock/BulletedListBlock.tsx | 21 +-
.../document/ListBlock/ColumnListBlock.tsx | 23 +
.../document/ListBlock/NumberedListBlock.tsx | 30 +
.../{block => document}/ListBlock/index.tsx | 18 +-
.../components/document/Node/Node.hooks.ts | 36 +
.../components/document/Node/index.tsx | 42 +
.../components/document/Overlay/index.tsx | 13 +
.../components/document/Root/Root.hooks.tsx | 16 +
.../components/document/Root/Tree.hooks.tsx | 23 +
.../components/document/Root/index.tsx | 32 +
.../document/TextBlock/BindYjs.hooks.ts | 61 ++
.../{block => document}/TextBlock/Leaf.tsx | 0
.../document/TextBlock/TextBlock.hooks.ts | 110 +++
.../components/document/TextBlock/index.tsx | 46 +
.../VirtualizerList/VirtualizerList.hooks.tsx | 21 +
.../document/VirtualizerList/index.tsx | 59 ++
.../ErrorBoundaryFallbackComponent.tsx | 12 +
.../document/_shared/SubscribeNode.hooks.ts | 32 +
.../src/appflowy_app/constants/toolbar.ts | 14 -
.../src/appflowy_app/interfaces/document.ts | 31 +
.../src/appflowy_app/interfaces/index.ts | 113 +--
.../effects/document/document_controller.ts | 50 ++
.../reducers/document}/region_grid.ts | 17 +-
.../stores/reducers/document/slice.ts | 132 +++
.../src/appflowy_app/stores/store.ts | 2 +
.../src/appflowy_app/utils/block.ts | 25 -
.../src/appflowy_app/utils/block_selection.ts | 36 -
.../src/appflowy_app/utils/slate/context.ts | 6 -
.../src/appflowy_app/utils/tool.ts | 52 ++
.../appflowy_app/views/DocumentPage.hooks.ts | 792 +-----------------
.../src/appflowy_app/views/DocumentPage.tsx | 22 +-
73 files changed, 1249 insertions(+), 2641 deletions(-)
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/BlockSelection/BlockSelection.hooks.tsx (69%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ColumnBlock/index.tsx (59%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/FormatButton.tsx (100%)
rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/FormatIcon.tsx (100%)
rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/index.hooks.ts (76%)
rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/index.tsx (74%)
rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/BulletedListBlock.tsx (54%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx
rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/index.tsx (51%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/TextBlock/Leaf.tsx (100%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
rename frontend/appflowy_tauri/src/appflowy_app/{block_editor/view => stores/reducers/document}/region_grid.ts (79%)
create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json
index 8215381041..9cac4d87c2 100644
--- a/frontend/appflowy_tauri/package.json
+++ b/frontend/appflowy_tauri/package.json
@@ -20,6 +20,7 @@
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@reduxjs/toolkit": "^1.9.2",
+ "@slate-yjs/core": "^0.3.1",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0",
"events": "^3.3.0",
@@ -42,8 +43,9 @@
"slate": "^0.91.4",
"slate-react": "^0.91.9",
"ts-results": "^3.3.0",
- "ulid": "^2.3.0",
- "utf8": "^3.0.0"
+ "utf8": "^3.0.0",
+ "yjs": "^13.5.51",
+ "y-indexeddb": "^9.0.9"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
@@ -53,6 +55,7 @@
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/utf8": "^3.0.1",
+ "@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-react": "^3.0.0",
@@ -64,6 +67,7 @@
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.7",
"typescript": "^4.6.4",
+ "uuid": "^9.0.0",
"vite": "^4.0.0"
}
}
diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml
index 4402ceca71..426fb22859 100644
--- a/frontend/appflowy_tauri/pnpm-lock.yaml
+++ b/frontend/appflowy_tauri/pnpm-lock.yaml
@@ -6,6 +6,7 @@ specifiers:
'@mui/icons-material': ^5.11.11
'@mui/material': ^5.11.12
'@reduxjs/toolkit': ^1.9.2
+ '@slate-yjs/core': ^0.3.1
'@tanstack/react-virtual': 3.0.0-beta.54
'@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.2
@@ -15,6 +16,7 @@ specifiers:
'@types/react': ^18.0.15
'@types/react-dom': ^18.0.6
'@types/utf8': ^3.0.1
+ '@types/uuid': ^9.0.1
'@typescript-eslint/eslint-plugin': ^5.51.0
'@typescript-eslint/parser': ^5.51.0
'@vitejs/plugin-react': ^3.0.0
@@ -31,6 +33,7 @@ specifiers:
postcss: ^8.4.21
prettier: 2.8.4
prettier-plugin-tailwindcss: ^0.2.2
+ protoc-gen-ts: ^0.8.5
react: ^18.2.0
react-dom: ^18.2.0
react-error-boundary: ^3.1.4
@@ -45,9 +48,11 @@ specifiers:
tailwindcss: ^3.2.7
ts-results: ^3.3.0
typescript: ^4.6.4
- ulid: ^2.3.0
utf8: ^3.0.0
+ uuid: ^9.0.0
vite: ^4.0.0
+ y-indexeddb: ^9.0.9
+ yjs: ^13.5.51
dependencies:
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
@@ -55,6 +60,7 @@ dependencies:
'@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
'@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli
'@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq
+ '@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51
'@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
'@tauri-apps/api': 1.2.0
events: 3.3.0
@@ -64,6 +70,7 @@ dependencies:
is-hotkey: 0.2.0
jest: 29.5.0_@types+node@18.14.6
nanoid: 4.0.1
+ protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-error-boundary: 3.1.4_react@18.2.0
@@ -76,8 +83,9 @@ dependencies:
slate: 0.91.4
slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u
ts-results: 3.3.0
- ulid: 2.3.0
utf8: 3.0.0
+ y-indexeddb: 9.0.9_yjs@13.5.51
+ yjs: 13.5.51
devDependencies:
'@tauri-apps/cli': 1.2.3
@@ -87,6 +95,7 @@ devDependencies:
'@types/react': 18.0.28
'@types/react-dom': 18.0.11
'@types/utf8': 3.0.1
+ '@types/uuid': 9.0.1
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
'@vitejs/plugin-react': 3.1.0_vite@4.1.4
@@ -98,6 +107,7 @@ devDependencies:
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
tailwindcss: 3.2.7_postcss@8.4.21
typescript: 4.9.5
+ uuid: 9.0.0
vite: 4.1.4_@types+node@18.14.6
packages:
@@ -1308,6 +1318,17 @@ packages:
'@sinonjs/commons': 2.0.0
dev: false
+ /@slate-yjs/core/0.3.1_slate@0.91.4+yjs@13.5.51:
+ resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==}
+ peerDependencies:
+ slate: '>=0.70.0'
+ yjs: ^13.5.29
+ dependencies:
+ slate: 0.91.4
+ y-protocols: 1.0.5
+ yjs: 13.5.51
+ dev: false
+
/@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0:
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
peerDependencies:
@@ -1553,6 +1574,10 @@ packages:
resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==}
dev: true
+ /@types/uuid/9.0.1:
+ resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
+ dev: true
+
/@types/yargs-parser/21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: false
@@ -3050,6 +3075,10 @@ packages:
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /isomorphic.js/0.2.5:
+ resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+ dev: false
+
/istanbul-lib-coverage/3.2.0:
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
engines: {node: '>=8'}
@@ -3575,6 +3604,14 @@ packages:
type-check: 0.4.0
dev: true
+ /lib0/0.2.73:
+ resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ isomorphic.js: 0.2.5
+ dev: false
+
/lilconfig/2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -4055,6 +4092,17 @@ packages:
object-assign: 4.1.1
react-is: 16.13.1
+ /protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa:
+ resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==}
+ hasBin: true
+ peerDependencies:
+ google-protobuf: ^3.13.0
+ typescript: 4.x.x
+ dependencies:
+ google-protobuf: 3.21.2
+ typescript: 4.9.5
+ dev: false
+
/punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@@ -4678,12 +4726,6 @@ packages:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
engines: {node: '>=4.2.0'}
hasBin: true
- dev: true
-
- /ulid/2.3.0:
- resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
- hasBin: true
- dev: false
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -4726,6 +4768,11 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /uuid/9.0.0:
+ resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+ hasBin: true
+ dev: true
+
/v8-to-istanbul/9.1.0:
resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
engines: {node: '>=10.12.0'}
@@ -4839,6 +4886,21 @@ packages:
engines: {node: '>=0.4'}
dev: true
+ /y-indexeddb/9.0.9_yjs@13.5.51:
+ resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==}
+ peerDependencies:
+ yjs: ^13.0.0
+ dependencies:
+ lib0: 0.2.73
+ yjs: 13.5.51
+ dev: false
+
+ /y-protocols/1.0.5:
+ resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
+ dependencies:
+ lib0: 0.2.73
+ dev: false
+
/y18n/5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -4872,6 +4934,13 @@ packages:
yargs-parser: 21.1.1
dev: false
+ /yjs/13.5.51:
+ resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+ dependencies:
+ lib0: 0.2.73
+ dev: false
+
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
deleted file mode 100644
index de42c3c373..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { BaseEditor, BaseSelection, Descendant } from "slate";
-import { TreeNode } from '$app/block_editor/view/tree_node';
-import { Operation } from "$app/block_editor/core/operation";
-import { TextBlockSelectionManager } from './text_selection';
-
-export class TextBlockManager {
- public selectionManager: TextBlockSelectionManager;
- constructor(private operation: Operation) {
- this.selectionManager = new TextBlockSelectionManager();
- }
-
- setSelection(node: TreeNode, selection: BaseSelection) {
- // console.log(node.id, selection);
- this.selectionManager.setSelection(node.id, selection)
- }
-
- update(node: TreeNode, path: string[], data: Descendant[]) {
- this.operation.updateNode(node.id, path, data);
- }
-
- splitNode(node: TreeNode, editor: BaseEditor) {
- const focus = editor.selection?.focus;
- const path = focus?.path || [0, editor.children.length - 1];
- const offset = focus?.offset || 0;
- const parentIndex = path[0];
- const index = path[1];
- const editorNode = editor.children[parentIndex];
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- const children: { [key: string]: boolean | string; text: string }[] = editorNode.children;
- const retainItems = children.filter((_: any, i: number) => i < index);
- const splitItem: { [key: string]: boolean | string } = children[index];
- const text = splitItem.text.toString();
- const prevText = text.substring(0, offset);
- const afterText = text.substring(offset);
- retainItems.push({
- ...splitItem,
- text: prevText
- });
-
- const removeItems = children.filter((_: any, i: number) => i > index);
-
- const data = {
- type: node.type,
- data: {
- ...node.data,
- content: [
- {
- ...splitItem,
- text: afterText
- },
- ...removeItems
- ]
- }
- };
-
- const newBlock = this.operation.splitNode(node.id, {
- path: ['data', 'content'],
- value: retainItems,
- }, data);
- newBlock && this.selectionManager.focusStart(newBlock.id);
- }
-
- destroy() {
- this.selectionManager.destroy();
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- this.operation = null;
- }
-
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
deleted file mode 100644
index b25d7f6268..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export class TextBlockSelectionManager {
- private focusId = '';
- private selection?: any;
-
- getFocusSelection() {
- return {
- focusId: this.focusId,
- selection: this.selection
- }
- }
-
- focusStart(blockId: string) {
- this.focusId = blockId;
- this.setSelection(blockId, {
- focus: {
- path: [0, 0],
- offset: 0,
- },
- anchor: {
- path: [0, 0],
- offset: 0,
- },
- })
- }
-
- setSelection(blockId: string, selection: any) {
- this.focusId = blockId;
- this.selection = selection;
- }
-
- destroy() {
- this.focusId = '';
- this.selection = undefined;
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
deleted file mode 100644
index c550213daa..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { BlockType, BlockData } from '$app/interfaces/index';
-import { generateBlockId } from '$app/utils/block';
-
-/**
- * Represents a single block of content in a document.
- */
-export class Block {
- id: string;
- type: T;
- data: BlockData;
- parent: Block | null = null; // Pointer to the parent block
- prev: Block | null = null; // Pointer to the previous sibling block
- next: Block | null = null; // Pointer to the next sibling block
- firstChild: Block | null = null; // Pointer to the first child block
-
- constructor(id: string, type: T, data: BlockData) {
- this.id = id;
- this.type = type;
- this.data = data;
- }
-
- /**
- * Adds a new child block to the beginning of the current block's children list.
- *
- * @param {Object} content - The content of the new block, including its type and data.
- * @param {string} content.type - The type of the new block.
- * @param {Object} content.data - The data associated with the new block.
- * @returns {Block} The newly created child block.
- */
- prependChild(content: { type: T, data: BlockData }): Block | null {
- const id = generateBlockId();
- const newBlock = new Block(id, content.type, content.data);
- newBlock.reposition(this, null);
- return newBlock;
- }
-
- /**
- * Add a new sibling block after this block.
- *
- * @param content The type and data for the new sibling block.
- * @returns The newly created sibling block.
- */
- addSibling(content: { type: T, data: BlockData }): Block | null {
- const id = generateBlockId();
- const newBlock = new Block(id, content.type, content.data);
- newBlock.reposition(this.parent, this);
- return newBlock;
- }
-
- /**
- * Remove this block and its descendants from the tree.
- *
- */
- remove() {
- this.detach();
- let child = this.firstChild;
- while (child) {
- const next = child.next;
- child.remove();
- child = next;
- }
- }
-
- reposition(newParent: Block | null, newPrev: Block | null) {
- // Update the block's parent and siblings
- this.parent = newParent;
- this.prev = newPrev;
- this.next = null;
-
- if (newParent) {
- const prev = newPrev;
- if (!prev) {
- const next = newParent.firstChild;
- newParent.firstChild = this;
- if (next) {
- this.next = next;
- next.prev = this;
- }
-
- } else {
- // Update the next and prev pointers of the newPrev and next blocks
- if (prev.next !== this) {
- const next = prev.next;
- if (next) {
- next.prev = this
- this.next = next;
- }
- prev.next = this;
- }
- }
-
- }
- }
-
- // detach the block from its current position in the tree
- detach() {
- if (this.prev) {
- this.prev.next = this.next;
- } else if (this.parent) {
- this.parent.firstChild = this.next;
- }
- if (this.next) {
- this.next.prev = this.prev;
- }
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
deleted file mode 100644
index 877f3592df..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { BlockData, BlockInterface, BlockType } from '$app/interfaces/index';
-import { set } from '../../utils/tool';
-import { Block } from './block';
-export interface BlockChangeProps {
- block?: Block,
- startBlock?: Block,
- endBlock?: Block,
- oldParentId?: string,
- oldPrevId?: string
-}
-export class BlockChain {
- private map: Map> = new Map();
- public head: Block | null = null;
-
- constructor(private onBlockChange: (command: string, data: BlockChangeProps) => void) {
-
- }
- /**
- * generate blocks from doc data
- * @param id doc id
- * @param map doc data
- */
- rebuild = (id: string, map: Record>) => {
- this.map.clear();
- this.head = this.createBlock(id, map[id].type, map[id].data);
-
- const callback = (block: Block) => {
- const firstChildId = map[block.id].firstChild;
- const nextId = map[block.id].next;
- if (!block.firstChild && firstChildId) {
- block.firstChild = this.createBlock(firstChildId, map[firstChildId].type, map[firstChildId].data);
- block.firstChild.parent = block;
- block.firstChild.prev = null;
- }
- if (!block.next && nextId) {
- block.next = this.createBlock(nextId, map[nextId].type, map[nextId].data);
- block.next.parent = block.parent;
- block.next.prev = block;
- }
- }
- this.traverse(callback);
- }
-
- /**
- * Traversing the block list from front to back
- * @param callback It will be call when the block visited
- * @param block block item, it will be equal head node when the block item is undefined
- */
- traverse(callback: (_block: Block) => void, block?: Block) {
- let currentBlock: Block | null = block || this.head;
- while (currentBlock) {
- callback(currentBlock);
- if (currentBlock.firstChild) {
- this.traverse(callback, currentBlock.firstChild);
- }
- currentBlock = currentBlock.next;
- }
- }
-
- /**
- * get block data
- * @param blockId string
- * @returns Block
- */
- getBlock = (blockId: string) => {
- return this.map.get(blockId) || null;
- }
-
- destroy() {
- this.map.clear();
- this.head = null;
- this.onBlockChange = () => null;
- }
-
- /**
- * Adds a new child block to the beginning of the current block's children list.
- *
- * @param {string} parentId
- * @param {Object} content - The content of the new block, including its type and data.
- * @param {string} content.type - The type of the new block.
- * @param {Object} content.data - The data associated with the new block.
- * @returns {Block} The newly created child block.
- */
- prependChild(blockId: string, content: { type: BlockType, data: BlockData }): Block | null {
- const parent = this.getBlock(blockId);
- if (!parent) return null;
- const newBlock = parent.prependChild(content);
-
- if (newBlock) {
- this.map.set(newBlock?.id, newBlock);
- this.onBlockChange('insert', { block: newBlock });
- }
-
- return newBlock;
- }
-
- /**
- * Add a new sibling block after this block.
- * @param {string} blockId
- * @param content The type and data for the new sibling block.
- * @returns The newly created sibling block.
- */
- addSibling(blockId: string, content: { type: BlockType, data: BlockData }): Block | null {
- const block = this.getBlock(blockId);
- if (!block) return null;
- const newBlock = block.addSibling(content);
- if (newBlock) {
- this.map.set(newBlock?.id, newBlock);
- this.onBlockChange('insert', { block: newBlock });
- }
- return newBlock;
- }
-
- /**
- * Remove this block and its descendants from the tree.
- * @param {string} blockId
- */
- remove(blockId: string) {
- const block = this.getBlock(blockId);
- if (!block) return;
- block.remove();
- this.map.delete(block.id);
- this.onBlockChange('delete', { block });
- return block;
- }
-
- /**
- * Move this block to a new position in the tree.
- * @param {string} blockId
- * @param newParentId The new parent block of this block. If null, the block becomes a top-level block.
- * @param newPrevId The new previous sibling block of this block. If null, the block becomes the first child of the new parent.
- * @returns This block after it has been moved.
- */
- move(blockId: string, newParentId: string, newPrevId: string): Block | null {
- const block = this.getBlock(blockId);
- if (!block) return null;
- const oldParentId = block.parent?.id;
- const oldPrevId = block.prev?.id;
- block.detach();
- const newParent = this.getBlock(newParentId);
- const newPrev = this.getBlock(newPrevId);
- block.reposition(newParent, newPrev);
- this.onBlockChange('move', {
- block,
- oldParentId,
- oldPrevId
- });
- return block;
- }
-
- updateBlock(id: string, data: { path: string[], value: any }) {
- const block = this.getBlock(id);
- if (!block) return null;
-
- set(block, data.path, data.value);
- this.onBlockChange('update', {
- block
- });
- return block;
- }
-
-
- moveBulk(startBlockId: string, endBlockId: string, newParentId: string, newPrevId: string): [Block, Block] | null {
- const startBlock = this.getBlock(startBlockId);
- const endBlock = this.getBlock(endBlockId);
- if (!startBlock || !endBlock) return null;
-
- if (startBlockId === endBlockId) {
- const block = this.move(startBlockId, newParentId, '');
- if (!block) return null;
- return [block, block];
- }
-
- const oldParent = startBlock.parent;
- const prev = startBlock.prev;
- const newParent = this.getBlock(newParentId);
- if (!oldParent || !newParent) return null;
-
- if (oldParent.firstChild === startBlock) {
- oldParent.firstChild = endBlock.next;
- } else if (prev) {
- prev.next = endBlock.next;
- }
- startBlock.prev = null;
- endBlock.next = null;
-
- startBlock.parent = newParent;
- endBlock.parent = newParent;
- const newPrev = this.getBlock(newPrevId);
- if (!newPrev) {
- const firstChild = newParent.firstChild;
- newParent.firstChild = startBlock;
- if (firstChild) {
- endBlock.next = firstChild;
- firstChild.prev = endBlock;
- }
- } else {
- const next = newPrev.next;
- newPrev.next = startBlock;
- endBlock.next = next;
- if (next) {
- next.prev = endBlock;
- }
- }
-
- this.onBlockChange('move', {
- startBlock,
- endBlock,
- oldParentId: oldParent.id,
- oldPrevId: prev?.id
- });
-
- return [
- startBlock,
- endBlock
- ];
- }
-
-
- private createBlock(id: string, type: BlockType, data: BlockData) {
- const block = new Block(id, type, data);
- this.map.set(id, block);
- return block;
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
deleted file mode 100644
index 0c5c0b3190..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { BackendOp, LocalOp } from "$app/interfaces";
-
-export class OpAdapter {
-
- toBackendOp(localOp: LocalOp): BackendOp {
- const backendOp: BackendOp = { ...localOp };
- // switch localOp type and generate backendOp
- return backendOp;
- }
-
- toLocalOp(backendOp: BackendOp): LocalOp {
- const localOp: LocalOp = { ...backendOp };
- // switch backendOp type and generate localOp
- return localOp;
- }
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
deleted file mode 100644
index 38f3a3fb76..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { BlockChain } from './block_chain';
-import { BlockInterface, BlockType, InsertOpData, LocalOp, UpdateOpData, moveOpData, moveRangeOpData, removeOpData, BlockData } from '$app/interfaces';
-import { BlockEditorSync } from './sync';
-import { Block } from './block';
-
-export class Operation {
- private sync: BlockEditorSync;
- constructor(private blockChain: BlockChain) {
- this.sync = new BlockEditorSync();
- }
-
-
- splitNode(
- retainId: string,
- retainData: { path: string[], value: any },
- newBlockData: {
- type: BlockType;
- data: BlockData
- }) {
- const ops: {
- type: LocalOp['type'];
- data: LocalOp['data'];
- }[] = [];
- const newBlock = this.blockChain.addSibling(retainId, newBlockData);
- const parentId = newBlock?.parent?.id;
- const retainBlock = this.blockChain.getBlock(retainId);
- if (!newBlock || !parentId || !retainBlock) return null;
-
- const insertOp = this.getInsertNodeOp({
- id: newBlock.id,
- next: newBlock.next?.id || null,
- firstChild: newBlock.firstChild?.id || null,
- data: newBlock.data,
- type: newBlock.type,
- }, parentId, retainId);
-
- const updateOp = this.getUpdateNodeOp(retainId, retainData.path, retainData.value);
- this.blockChain.updateBlock(retainId, retainData);
-
- ops.push(insertOp, updateOp);
- const startBlock = retainBlock.firstChild;
- if (startBlock) {
- const startBlockId = startBlock.id;
- let next: Block | null = startBlock.next;
- let endBlockId = startBlockId;
- while (next) {
- endBlockId = next.id;
- next = next.next;
- }
-
- const moveOp = this.getMoveRangeOp([startBlockId, endBlockId], newBlock.id);
- this.blockChain.moveBulk(startBlockId, endBlockId, newBlock.id, '');
- ops.push(moveOp);
- }
-
- this.sync.sendOps(ops);
-
- return newBlock;
- }
-
- updateNode(blockId: string, path: string[], value: T) {
- const op = this.getUpdateNodeOp(blockId, path, value);
- this.blockChain.updateBlock(blockId, {
- path,
- value
- });
- this.sync.sendOps([op]);
- }
- private getUpdateNodeOp(blockId: string, path: string[], value: T): {
- type: 'update',
- data: UpdateOpData
- } {
- return {
- type: 'update',
- data: {
- blockId,
- path: path,
- value
- }
- };
- }
-
- private getInsertNodeOp(block: T, parentId: string, prevId?: string): {
- type: 'insert';
- data: InsertOpData
- } {
- return {
- type: 'insert',
- data: {
- block,
- parentId,
- prevId
- }
- }
- }
-
- private getMoveRangeOp(range: [string, string], newParentId: string, newPrevId?: string): {
- type: 'move_range',
- data: moveRangeOpData
- } {
- return {
- type: 'move_range',
- data: {
- range,
- newParentId,
- newPrevId,
- }
- }
- }
-
- private getMoveOp(blockId: string, newParentId: string, newPrevId?: string): {
- type: 'move',
- data: moveOpData
- } {
- return {
- type: 'move',
- data: {
- blockId,
- newParentId,
- newPrevId
- }
- }
- }
-
- private getRemoveOp(blockId: string): {
- type: 'remove'
- data: removeOpData
- } {
- return {
- type: 'remove',
- data: {
- blockId
- }
- }
- }
-
- applyOperation(op: LocalOp) {
- switch (op.type) {
- case 'insert':
-
- break;
-
- default:
- break;
- }
- }
-
- destroy() {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- this.blockChain = null;
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
deleted file mode 100644
index 24070c0cd5..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { BackendOp, LocalOp } from '$app/interfaces';
-import { OpAdapter } from './op_adapter';
-
-/**
- * BlockEditorSync is a class that synchronizes changes made to a block chain with a server.
- * It allows for adding, removing, and moving blocks in the chain, and sends pending operations to the server.
- */
-export class BlockEditorSync {
- private version = 0;
- private opAdapter: OpAdapter;
- private pendingOps: BackendOp[] = [];
- private appliedOps: LocalOp[] = [];
-
- constructor() {
- this.opAdapter = new OpAdapter();
- }
-
- private applyOp(op: BackendOp): void {
- const localOp = this.opAdapter.toLocalOp(op);
- this.appliedOps.push(localOp);
- }
-
- private receiveOps(ops: BackendOp[]): void {
- // Apply the incoming operations to the local document
- ops.sort((a, b) => a.version - b.version);
- for (const op of ops) {
- this.applyOp(op);
- }
- }
-
- private resolveConflict(): void {
- // Implement conflict resolution logic here
- }
-
- public sendOps(ops: {
- type: LocalOp["type"];
- data: LocalOp["data"]
- }[]) {
- const backendOps = ops.map(op => this.opAdapter.toBackendOp({
- ...op,
- version: this.version
- }));
- this.pendingOps.push(...backendOps);
- // Send the pending operations to the server
- console.log('==== sync pending ops ====', [...this.pendingOps]);
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
deleted file mode 100644
index 658b284906..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// Import dependencies
-import { BlockInterface } from '../interfaces';
-import { BlockChain, BlockChangeProps } from './core/block_chain';
-import { RenderTree } from './view/tree';
-import { Operation } from './core/operation';
-
-/**
- * The BlockEditor class manages a block chain and a render tree for a document editor.
- * The block chain stores the content blocks of the document in sequence, while the
- * render tree displays the document as a hierarchical tree structure.
- */
-export class BlockEditor {
- // Public properties
- public blockChain: BlockChain; // (local data) the block chain used to store the document
- public renderTree: RenderTree; // the render tree used to display the document
- public operation: Operation;
- /**
- * Constructs a new BlockEditor object.
- * @param id - the ID of the document
- * @param data - the initial data for the document
- */
- constructor(private id: string, data: Record) {
- // Create the block chain and render tree
- this.blockChain = new BlockChain(this.blockChange);
- this.operation = new Operation(this.blockChain);
- this.changeDoc(id, data);
-
- this.renderTree = new RenderTree(this.blockChain);
- }
-
- /**
- * Updates the document ID and block chain when the document changes.
- * @param id - the new ID of the document
- * @param data - the updated data for the document
- */
- changeDoc = (id: string, data: Record) => {
- console.log('==== change document ====', id, data);
-
- // Update the document ID and rebuild the block chain
- this.id = id;
- this.blockChain.rebuild(id, data);
- }
-
-
- /**
- * Destroys the block chain and render tree.
- */
- destroy = () => {
- // Destroy the block chain and render tree
- this.blockChain.destroy();
- this.renderTree.destroy();
- this.operation.destroy();
- }
-
- private blockChange = (command: string, data: BlockChangeProps) => {
- this.renderTree.onBlockChange(command, data);
- }
-
-}
-
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
deleted file mode 100644
index a2841d8a3b..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { RegionGrid, BlockPosition } from './region_grid';
-export class BlockPositionManager {
- private regionGrid: RegionGrid;
- private viewportBlocks: Set = new Set();
- private blockPositions: Map = new Map();
- private observer: IntersectionObserver;
- private container: HTMLDivElement | null = null;
-
- constructor(container: HTMLDivElement) {
- this.container = container;
- this.regionGrid = new RegionGrid(container.offsetHeight);
- this.observer = new IntersectionObserver((entries) => {
- for (const entry of entries) {
- const blockId = entry.target.getAttribute('data-block-id');
- if (!blockId) return;
- if (entry.isIntersecting) {
- this.updateBlockPosition(blockId);
- this.viewportBlocks.add(blockId);
- } else {
- this.viewportBlocks.delete(blockId);
- }
- }
- }, { root: container });
- }
-
- observeBlock(node: HTMLDivElement) {
- this.observer.observe(node);
- return {
- unobserve: () => this.observer.unobserve(node),
- }
- }
-
- getBlockPosition(blockId: string) {
- if (!this.blockPositions.has(blockId)) {
- this.updateBlockPosition(blockId);
- }
- return this.blockPositions.get(blockId);
- }
-
- updateBlockPosition(blockId: string) {
- if (!this.container) return;
- const node = document.querySelector(`[data-block-id=${blockId}]`) as HTMLDivElement;
- if (!node) return;
- const rect = node.getBoundingClientRect();
- const position = {
- id: blockId,
- x: rect.x,
- y: rect.y + this.container.scrollTop,
- height: rect.height,
- width: rect.width
- };
- const prevPosition = this.blockPositions.get(blockId);
- if (prevPosition && prevPosition.x === position.x &&
- prevPosition.y === position.y &&
- prevPosition.height === position.height &&
- prevPosition.width === position.width) {
- return;
- }
- this.blockPositions.set(blockId, position);
- this.regionGrid.removeBlock(blockId);
- this.regionGrid.addBlock(position);
- }
-
- getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] {
- return this.regionGrid.getIntersectBlocks(startX, startY, endX, endY);
- }
-
- destroy() {
- this.container = null;
- this.observer.disconnect();
- }
-
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
deleted file mode 100644
index 4eb136ff09..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-import { BlockChain, BlockChangeProps } from '../core/block_chain';
-import { Block } from '../core/block';
-import { TreeNode } from "./tree_node";
-import { BlockPositionManager } from './block_position';
-import { filterSelections } from '@/appflowy_app/utils/block_selection';
-
-export class RenderTree {
- public blockPositionManager?: BlockPositionManager;
-
- private map: Map = new Map();
- private root: TreeNode | null = null;
- private selections: Set = new Set();
- constructor(private blockChain: BlockChain) {
- }
-
-
- createPositionManager(container: HTMLDivElement) {
- this.blockPositionManager = new BlockPositionManager(container);
- }
-
- observeBlock(node: HTMLDivElement) {
- return this.blockPositionManager?.observeBlock(node);
- }
-
- getBlockPosition(nodeId: string) {
- return this.blockPositionManager?.getBlockPosition(nodeId) || null;
- }
- /**
- * Get the TreeNode data by nodeId
- * @param nodeId string
- * @returns TreeNode|null
- */
- getTreeNode = (nodeId: string): TreeNode | null => {
- // Return the TreeNode instance from the map or null if it does not exist
- return this.map.get(nodeId) || null;
- }
-
- private createNode(block: Block): TreeNode {
- if (this.map.has(block.id)) {
- return this.map.get(block.id)!;
- }
- const node = new TreeNode(block);
- this.map.set(block.id, node);
- return node;
- }
-
-
- buildDeep(rootId: string): TreeNode | null {
- this.map.clear();
- // Define a callback function for the blockChain.traverse() method
- const callback = (block: Block) => {
- // Check if the TreeNode instance already exists in the map
- const node = this.createNode(block);
-
- // Add the TreeNode instance to the map
- this.map.set(block.id, node);
-
- // Add the first child of the block as a child of the current TreeNode instance
- const firstChild = block.firstChild;
- if (firstChild) {
- const child = this.createNode(firstChild);
- node.addChild(child);
- this.map.set(child.id, child);
- }
-
- // Add the next block as a sibling of the current TreeNode instance
- const next = block.next;
- if (next) {
- const nextNode = this.createNode(next);
- node.parent?.addChild(nextNode);
- this.map.set(next.id, nextNode);
- }
- }
-
- // Traverse the blockChain using the callback function
- this.blockChain.traverse(callback);
-
- // Get the root node from the map and return it
- const root = this.map.get(rootId)!;
- this.root = root;
- return root || null;
- }
-
-
- forceUpdate(nodeId: string, shouldUpdateChildren = false) {
- const block = this.blockChain.getBlock(nodeId);
- if (!block) return null;
- const node = this.createNode(block);
- if (!node) return null;
-
- if (shouldUpdateChildren) {
- const children: TreeNode[] = [];
- let childBlock = block.firstChild;
-
- while(childBlock) {
- const child = this.createNode(childBlock);
- child.update(childBlock, child.children);
- children.push(child);
- childBlock = childBlock.next;
- }
-
- node.update(block, children);
- node?.reRender();
- node?.children.forEach(child => {
- child.reRender();
- })
- } else {
- node.update(block, node.children);
- node?.reRender();
- }
- }
-
- onBlockChange(command: string, data: BlockChangeProps) {
- const { block, startBlock, endBlock, oldParentId = '', oldPrevId = '' } = data;
- switch (command) {
- case 'insert':
- if (block?.parent) this.forceUpdate(block.parent.id, true);
- break;
- case 'update':
- this.forceUpdate(block!.id);
- break;
- case 'move':
- if (oldParentId) this.forceUpdate(oldParentId, true);
- if (block?.parent) this.forceUpdate(block.parent.id, true);
- if (startBlock?.parent) this.forceUpdate(startBlock.parent.id, true);
- break;
- default:
- break;
- }
-
- }
-
- updateSelections(selections: string[]) {
- const newSelections = filterSelections(selections, this.map);
-
- let isDiff = false;
- if (newSelections.length !== this.selections.size) {
- isDiff = true;
- }
-
- const selectedBlocksSet = new Set(newSelections);
- if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) {
- isDiff = true;
- }
-
- if (isDiff) {
- const shouldUpdateIds = new Set([...this.selections, ...newSelections]);
- this.selections = selectedBlocksSet;
- shouldUpdateIds.forEach((id) => this.forceUpdate(id));
- }
- }
-
- isSelected(nodeId: string) {
- return this.selections.has(nodeId);
- }
-
- /**
- * Destroy the RenderTreeRectManager instance
- */
- destroy() {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- this.blockChain = null;
- }
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
deleted file mode 100644
index 9ed78bd4b4..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { BlockData, BlockType } from '$app/interfaces/index';
-import { Block } from '../core/block';
-
-/**
- * Represents a node in a tree structure of blocks.
- */
-export class TreeNode {
- id: string;
- type: BlockType;
- parent: TreeNode | null = null;
- children: TreeNode[] = [];
- data: BlockData;
-
- private forceUpdate?: () => void;
-
- /**
- * Create a new TreeNode instance.
- * @param block - The block data used to create the node.
- */
- constructor(private _block: Block) {
- this.id = _block.id;
- this.data = _block.data;
- this.type = _block.type;
- }
-
- registerUpdate(forceUpdate: () => void) {
- this.forceUpdate = forceUpdate;
- }
-
- unregisterUpdate() {
- this.forceUpdate = undefined;
- }
-
- reRender() {
- this.forceUpdate?.();
- }
-
- update(block: Block, children: TreeNode[]) {
- this.data = block.data;
- this.children = [];
- children.forEach(child => {
- this.addChild(child);
- })
- }
-
- /**
- * Add a child node to the current node.
- * @param node - The child node to add.
- */
- addChild(node: TreeNode) {
- node.parent = this;
- this.children.push(node);
- }
-
- get block() {
- return this._block;
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx
deleted file mode 100644
index 0176c8f429..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import ReactDOM from 'react-dom';
-
-const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
- const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0];
-
- return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
-};
-
-export default Portal;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
deleted file mode 100644
index 20e31a1793..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useEffect, useState, useRef, useContext } from 'react';
-
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { BlockContext } from '$app/utils/block';
-
-export function useBlockComponent({
- node
-}: {
- node: TreeNode
-}) {
- const { blockEditor } = useContext(BlockContext);
-
- const [version, forceUpdate] = useState(0);
- const myRef = useRef(null);
-
- const isSelected = blockEditor?.renderTree.isSelected(node.id);
-
- useEffect(() => {
- if (!myRef.current) {
- return;
- }
- const observe = blockEditor?.renderTree.observeBlock(myRef.current);
- node.registerUpdate(() => forceUpdate((prev) => prev + 1));
-
- return () => {
- node.unregisterUpdate();
- observe?.unobserve();
- };
- }, []);
- return {
- version,
- myRef,
- isSelected,
- className: `relative my-[1px] px-1`
- }
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
deleted file mode 100644
index 9c8ee223dd..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { forwardRef } from 'react';
-import { BlockCommonProps, BlockType } from '$app/interfaces';
-import PageBlock from '../PageBlock';
-import TextBlock from '../TextBlock';
-import HeadingBlock from '../HeadingBlock';
-import ListBlock from '../ListBlock';
-import CodeBlock from '../CodeBlock';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { withErrorBoundary } from 'react-error-boundary';
-import { ErrorBoundaryFallbackComponent } from '../BlockList/BlockList.hooks';
-import { useBlockComponent } from './BlockComponet.hooks';
-
-const BlockComponent = forwardRef(
- (
- {
- node,
- renderChild,
- ...props
- }: { node: TreeNode; renderChild?: (_node: TreeNode) => React.ReactNode } & React.DetailedHTMLProps<
- React.HTMLAttributes,
- HTMLDivElement
- >,
- ref: React.ForwardedRef
- ) => {
- const { myRef, className, version, isSelected } = useBlockComponent({
- node,
- });
-
- const renderComponent = () => {
- let BlockComponentClass: (_: BlockCommonProps) => JSX.Element | null;
- switch (node.type) {
- case BlockType.PageBlock:
- BlockComponentClass = PageBlock;
- break;
- case BlockType.TextBlock:
- BlockComponentClass = TextBlock;
- break;
- case BlockType.HeadingBlock:
- BlockComponentClass = HeadingBlock;
- break;
- case BlockType.ListBlock:
- BlockComponentClass = ListBlock;
- break;
- case BlockType.CodeBlock:
- BlockComponentClass = CodeBlock;
- break;
- default:
- break;
- }
-
- const blockProps: BlockCommonProps = {
- version,
- node,
- };
-
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- if (BlockComponentClass) {
- return ;
- }
- return null;
- };
-
- return (
- {
- myRef.current = el;
- if (typeof ref === 'function') {
- ref(el);
- } else if (ref) {
- ref.current = el;
- }
- }}
- {...props}
- data-block-id={node.id}
- data-block-selected={isSelected}
- className={props.className ? `${props.className} ${className}` : className}
- >
- {renderComponent()}
- {renderChild ? node.children.map(renderChild) : null}
-
- {isSelected ?
: null}
-
- );
- }
-);
-
-const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, {
- FallbackComponent: ErrorBoundaryFallbackComponent,
-});
-export default React.memo(ComponentWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
deleted file mode 100644
index 0d673a47e8..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { BlockEditor } from '@/appflowy_app/block_editor';
-import { TreeNode } from '$app/block_editor/view/tree_node';
-import { Alert } from '@mui/material';
-import { FallbackProps } from 'react-error-boundary';
-import { TextBlockManager } from '@/appflowy_app/block_editor/blocks/text_block';
-import { TextBlockContext } from '@/appflowy_app/utils/slate/context';
-import { useVirtualizer } from '@tanstack/react-virtual';
-export interface BlockListProps {
- blockId: string;
- blockEditor: BlockEditor;
-}
-
-const defaultSize = 45;
-
-export function useBlockList({ blockId, blockEditor }: BlockListProps) {
- const [root, setRoot] = useState(null);
-
- const parentRef = useRef(null);
-
- const rowVirtualizer = useVirtualizer({
- count: root?.children.length || 0,
- getScrollElement: () => parentRef.current,
- overscan: 5,
- estimateSize: () => {
- return defaultSize;
- },
- });
-
- const [version, forceUpdate] = useState(0);
-
- const buildDeepTree = useCallback(() => {
- const treeNode = blockEditor.renderTree.buildDeep(blockId);
- setRoot(treeNode);
- }, [blockEditor]);
-
- useEffect(() => {
- if (!parentRef.current) return;
- blockEditor.renderTree.createPositionManager(parentRef.current);
- buildDeepTree();
-
- return () => {
- blockEditor.destroy();
- };
- }, [blockId, blockEditor]);
-
- useEffect(() => {
- root?.registerUpdate(() => forceUpdate((prev) => prev + 1));
- return () => {
- root?.unregisterUpdate();
- };
- }, [root]);
-
- return {
- root,
- rowVirtualizer,
- parentRef,
- blockEditor,
- };
-}
-
-export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
- return (
-
- Something went wrong:
- {error.message}
- Try again
-
- );
-}
-
-export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) {
- return (props: BlockListProps) => {
- const textBlockManager = new TextBlockManager(props.blockEditor.operation);
-
- useEffect(() => {
- return () => {
- textBlockManager.destroy();
- };
- }, []);
-
- return (
-
-
-
- );
- };
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
deleted file mode 100644
index f74ae72283..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import TextBlock from '../TextBlock';
-import { TreeNode } from '$app/block_editor/view/tree_node';
-
-export default function BlockListTitle({ node }: { node: TreeNode | null }) {
- if (!node) return null;
- return (
-
-
-
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
deleted file mode 100644
index 6078180374..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as React from 'react';
-import Typography, { TypographyProps } from '@mui/material/Typography';
-import Skeleton from '@mui/material/Skeleton';
-import Grid from '@mui/material/Grid';
-
-const variants = ['h1', 'h3', 'body1', 'caption'] as readonly TypographyProps['variant'][];
-
-export default function ListFallbackComponent() {
- return (
-
-
-
-
-
-
-
-
-
-
- {variants.map((variant) => (
-
-
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
deleted file mode 100644
index 9a8709ea64..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import { BlockListProps, useBlockList, withTextBlockManager } from './BlockList.hooks';
-import { withErrorBoundary } from 'react-error-boundary';
-import ListFallbackComponent from './ListFallbackComponent';
-import BlockListTitle from './BlockListTitle';
-import BlockComponent from '../BlockComponent';
-import BlockSelection from '../BlockSelection';
-
-function BlockList(props: BlockListProps) {
- const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props);
-
- const virtualItems = rowVirtualizer.getVirtualItems();
- return (
-
-
-
- {root && virtualItems.length ? (
-
- {virtualItems.map((virtualRow) => {
- const id = root.children[virtualRow.index].id;
- return (
-
- {virtualRow.index === 0 ? : null}
-
-
- );
- })}
-
- ) : null}
-
-
- {parentRef.current ?
: null}
-
- );
-}
-
-const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), {
- FallbackComponent: ListFallbackComponent,
-});
-
-export default React.memo(ListWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
deleted file mode 100644
index 4ef554d489..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useBlockSelection } from './BlockSelection.hooks';
-import { BlockEditor } from '$app/block_editor';
-import React from 'react';
-
-function BlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) {
- const { isDragging, style } = useBlockSelection({
- container,
- blockEditor,
- });
-
- return (
-
- {isDragging ?
: null}
-
- );
-}
-
-export default React.memo(BlockSelection);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
deleted file mode 100644
index eb34844d2c..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { BlockCommonProps } from '@/appflowy_app/interfaces';
-
-export default function CodeBlock({ node }: BlockCommonProps) {
- return {node.data.text}
;
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
deleted file mode 100644
index f0a1bd3323..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import TextBlock from '../TextBlock';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { BlockCommonProps } from '@/appflowy_app/interfaces';
-
-const fontSize: Record = {
- 1: 'mt-8 text-3xl',
- 2: 'mt-6 text-2xl',
- 3: 'mt-4 text-xl',
-};
-
-export default function HeadingBlock({ node, version }: BlockCommonProps) {
- return (
-
-
-
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
deleted file mode 100644
index ce0a1254d3..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import React, { useMemo } from 'react';
-import ColumnBlock from '../ColumnBlock';
-
-export default function ColumnListBlock({ node }: { node: TreeNode }) {
- const resizerWidth = useMemo(() => {
- return 46 * (node.children?.length || 0);
- }, [node.children?.length]);
- return (
- <>
-
- {node.children?.map((item, index) => (
-
- ))}
-
- >
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
deleted file mode 100644
index 6bc63d41ef..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import BlockComponent from '../BlockComponent';
-import { BlockType } from '@/appflowy_app/interfaces';
-import { Block } from '@/appflowy_app/block_editor/core/block';
-
-export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) {
- let prev = node.block.prev;
- let index = 1;
- while (prev && prev.type === BlockType.ListBlock && (prev as Block).data.type === 'numbered') {
- index++;
- prev = prev.prev;
- }
- return (
-
-
-
{`${index} .`}
- {title}
-
-
-
- {node.children?.map((item) => (
-
-
-
- ))}
-
-
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
deleted file mode 100644
index a79e036dbe..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { BlockCommonProps } from '@/appflowy_app/interfaces';
-
-export default function PageBlock({ node }: BlockCommonProps) {
- return {node.data.title}
;
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
deleted file mode 100644
index a776ae8be4..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node";
-import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
-import { useCallback, useContext, useLayoutEffect, useState } from "react";
-import { Transforms, createEditor, Descendant } from 'slate';
-import { ReactEditor, withReact } from 'slate-react';
-import { TextBlockContext } from '$app/utils/slate/context';
-
-export function useTextBlock({
- node,
-}: {
- node: TreeNode;
-}) {
- const [editor] = useState(() => withReact(createEditor()));
-
- const { textBlockManager } = useContext(TextBlockContext);
-
- const value = [
- {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- type: 'paragraph',
- children: node.data.content,
- },
- ];
-
-
- const onChange = useCallback(
- (e: Descendant[]) => {
- if (!editor.operations || editor.operations.length === 0) return;
- if (editor.operations[0].type !== 'set_selection') {
- console.log('====text block ==== ', editor.operations)
- const children = 'children' in e[0] ? e[0].children : [];
- textBlockManager?.update(node, ['data', 'content'], children);
- } else {
- const newProperties = editor.operations[0].newProperties;
- textBlockManager?.setSelection(node, editor.selection);
- }
- },
- [node.id, editor],
- );
-
-
- const onKeyDownCapture = (event: React.KeyboardEvent) => {
- switch (event.key) {
- case 'Enter': {
- event.stopPropagation();
- event.preventDefault();
- textBlockManager?.splitNode(node, editor);
-
- return;
- }
- }
-
- triggerHotkey(event, editor);
- }
-
-
-
- const { focusId, selection } = textBlockManager!.selectionManager.getFocusSelection();
-
- editor.children = value;
- Transforms.collapse(editor);
-
- useLayoutEffect(() => {
- let timer: NodeJS.Timeout;
- if (focusId === node.id && selection) {
- ReactEditor.focus(editor);
- Transforms.select(editor, selection);
- // Use setTimeout to delay setting the selection
- // until Slate has fully loaded and rendered all components and contents,
- // to ensure that the operation succeeds.
- timer = setTimeout(() => {
- Transforms.select(editor, selection);
- }, 100);
- }
-
- return () => timer && clearTimeout(timer)
- }, [editor]);
-
- const onDOMBeforeInput = useCallback((e: InputEvent) => {
- // COMPAT: in Apple, `compositionend` is dispatched after the
- // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
- // Here, prevent the beforeInput event and wait for the compositionend event to take effect
- if (e.inputType === 'insertFromComposition') {
- e.preventDefault();
- }
-
- }, []);
-
-
- return {
- editor,
- value,
- onChange,
- onKeyDownCapture,
- onDOMBeforeInput,
- }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
deleted file mode 100644
index 906e9a4060..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import BlockComponent from '../BlockComponent';
-import { Slate, Editable } from 'slate-react';
-import Leaf from './Leaf';
-import HoveringToolbar from '$app/components/HoveringToolbar';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { useTextBlock } from './index.hooks';
-import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces';
-import { toolbarDefaultProps } from '@/appflowy_app/constants/toolbar';
-
-export default function TextBlock({
- node,
- needRenderChildren = true,
- toolbarProps,
- ...props
-}: {
- needRenderChildren?: boolean;
- toolbarProps?: TextBlockToolbarProps;
-} & BlockCommonProps &
- React.HTMLAttributes) {
- const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node });
- const { showGroups } = toolbarProps || toolbarDefaultProps;
-
- return (
-
-
- {showGroups.length > 0 && }
- }
- placeholder='Enter some text...'
- />
-
- {needRenderChildren && node.children.length > 0 ? (
-
- {node.children.map((item) => (
-
- ))}
-
- ) : null}
-
- );
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
new file mode 100644
index 0000000000..49ede75648
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
@@ -0,0 +1,9 @@
+import ReactDOM from 'react-dom';
+
+const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
+ const root = document.querySelectorAll(`[data-block-id="${blockId}"] > .block-overlay`)[0];
+
+ return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
+};
+
+export default BlockPortal;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
similarity index 69%
rename from frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
index 00bc05f2d1..0404fe42b8 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
@@ -1,13 +1,25 @@
-import { BlockEditor } from '@/appflowy_app/block_editor';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+import { useAppDispatch } from '$app/stores/store';
+import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
-export function useBlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) {
- const blockPositionManager = blockEditor.renderTree.blockPositionManager;
+export function useBlockSelection({
+ container,
+ onDragging,
+}: {
+ container: HTMLDivElement;
+ onDragging?: (_isDragging: boolean) => void;
+}) {
+ const ref = useRef(null);
+ const disaptch = useAppDispatch();
const [isDragging, setDragging] = useState(false);
const pointRef = useRef([]);
const startScrollTopRef = useRef(0);
+ useEffect(() => {
+ onDragging?.(isDragging);
+ }, [isDragging]);
+
const [rect, setRect] = useState<{
startX: number;
startY: number;
@@ -62,7 +74,7 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD
const calcIntersectBlocks = useCallback(
(clientX: number, clientY: number) => {
- if (!isDragging || !blockPositionManager) return;
+ if (!isDragging) return;
const [startX, startY] = pointRef.current;
const endX = clientX + container.scrollLeft;
const endY = clientY + container.scrollTop;
@@ -73,22 +85,23 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD
endX,
endY,
});
- const selectedBlocks = blockPositionManager.getIntersectBlocks(
- Math.min(startX, endX),
- Math.min(startY, endY),
- Math.max(startX, endX),
- Math.max(startY, endY)
+ disaptch(
+ documentActions.changeSelectionByIntersectRect({
+ startX: Math.min(startX, endX),
+ startY: Math.min(startY, endY),
+ endX: Math.max(startX, endX),
+ endY: Math.max(startY, endY),
+ })
);
- const ids = selectedBlocks.map((item) => item.id);
- blockEditor.renderTree.updateSelections(ids);
},
[isDragging]
);
const handleDraging = useCallback(
(e: MouseEvent) => {
- if (!isDragging || !blockPositionManager) return;
+ if (!isDragging) return;
e.preventDefault();
+ e.stopPropagation();
calcIntersectBlocks(e.clientX, e.clientY);
const { top, bottom } = container.getBoundingClientRect();
@@ -106,7 +119,7 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD
const handleDragEnd = useCallback(
(e: MouseEvent) => {
if (isPointInBlock(e.target as HTMLElement) && !isDragging) {
- blockEditor.renderTree.updateSelections([]);
+ disaptch(documentActions.updateSelections([]));
return;
}
if (!isDragging) return;
@@ -119,19 +132,21 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD
);
useEffect(() => {
- window.addEventListener('mousedown', handleDragStart);
- window.addEventListener('mousemove', handleDraging);
- window.addEventListener('mouseup', handleDragEnd);
+ if (!ref.current) return;
+ document.addEventListener('mousedown', handleDragStart);
+ document.addEventListener('mousemove', handleDraging);
+ document.addEventListener('mouseup', handleDragEnd);
return () => {
- window.removeEventListener('mousedown', handleDragStart);
- window.removeEventListener('mousemove', handleDraging);
- window.removeEventListener('mouseup', handleDragEnd);
+ document.removeEventListener('mousedown', handleDragStart);
+ document.removeEventListener('mousemove', handleDraging);
+ document.removeEventListener('mouseup', handleDragEnd);
};
}, [handleDragStart, handleDragEnd, handleDraging]);
return {
isDragging,
style,
+ ref,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx
new file mode 100644
index 0000000000..0a3ac62a84
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx
@@ -0,0 +1,23 @@
+import { useBlockSelection } from './BlockSelection.hooks';
+import React from 'react';
+
+function BlockSelection({
+ container,
+ onDragging,
+}: {
+ container: HTMLDivElement;
+ onDragging?: (_isDragging: boolean) => void;
+}) {
+ const { isDragging, style, ref } = useBlockSelection({
+ container,
+ onDragging,
+ });
+
+ return (
+
+ {isDragging ?
: null}
+
+ );
+}
+
+export default React.memo(BlockSelection);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
new file mode 100644
index 0000000000..c707e4c4e1
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
@@ -0,0 +1,126 @@
+import { BlockType } from '@/appflowy_app/interfaces/document';
+import { useAppSelector } from '@/appflowy_app/stores/store';
+import { debounce } from '@/appflowy_app/utils/tool';
+import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { v4 } from 'uuid';
+
+export function useBlockSideTools({ container }: { container: HTMLDivElement }) {
+ const [nodeId, setHoverNodeId] = useState('');
+ const ref = useRef(null);
+ const nodes = useAppSelector((state) => state.document.nodes);
+ const { insertAfter } = useController();
+
+ const handleMouseMove = useCallback((e: MouseEvent) => {
+ const { clientX, clientY } = e;
+ const x = clientX;
+ const y = clientY;
+ const id = getNodeIdByPoint(x, y);
+ if (!id) {
+ setHoverNodeId('');
+ } else {
+ if ([BlockType.ColumnBlock].includes(nodes[id].type)) {
+ setHoverNodeId('');
+ return;
+ }
+ setHoverNodeId(id);
+ }
+ }, []);
+
+ const debounceMove = useMemo(() => debounce(handleMouseMove, 30), [handleMouseMove]);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el || !nodeId) return;
+
+ const node = nodes[nodeId];
+ if (!node) {
+ el.style.opacity = '0';
+ el.style.zIndex = '-1';
+ } else {
+ el.style.opacity = '1';
+ el.style.zIndex = '1';
+ el.style.top = '1px';
+ if (node?.type === BlockType.HeadingBlock) {
+ if (node.data.style?.level === 1) {
+ el.style.top = '8px';
+ } else if (node.data.style?.level === 2) {
+ el.style.top = '6px';
+ } else {
+ el.style.top = '5px';
+ }
+ }
+ }
+ }, [nodeId, nodes]);
+
+ const handleAddClick = useCallback(() => {
+ if (!nodeId) return;
+ insertAfter(nodes[nodeId]);
+ }, [nodeId, nodes]);
+
+ useEffect(() => {
+ container.addEventListener('mousemove', debounceMove);
+ return () => {
+ container.removeEventListener('mousemove', debounceMove);
+ };
+ }, [debounceMove]);
+
+ return {
+ nodeId,
+ ref,
+ handleAddClick,
+ };
+}
+
+function useController() {
+ const controller = useContext(DocumentControllerContext);
+
+ const insertAfter = useCallback((node: Node) => {
+ const parentId = node.parent;
+ if (!parentId || !controller) return;
+
+ controller.transact([
+ () => {
+ const newNode = {
+ id: v4(),
+ delta: [],
+ type: BlockType.TextBlock,
+ };
+ controller.insert(newNode, parentId, node.id);
+ },
+ ]);
+ }, []);
+
+ return {
+ insertAfter,
+ };
+}
+
+function getNodeIdByPoint(x: number, y: number) {
+ const viewportNodes = document.querySelectorAll('[data-block-id]');
+ let node: {
+ el: Element;
+ rect: DOMRect;
+ } | null = null;
+ viewportNodes.forEach((el) => {
+ const rect = el.getBoundingClientRect();
+
+ if (rect.x + rect.width - 1 >= x && rect.y + rect.height - 1 >= y && rect.y <= y) {
+ if (!node || rect.y > node.rect.y) {
+ node = {
+ el,
+ rect,
+ };
+ }
+ }
+ });
+ return node
+ ? (
+ node as {
+ el: Element;
+ rect: DOMRect;
+ }
+ ).el.getAttribute('data-block-id')
+ : null;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx
new file mode 100644
index 0000000000..cf2631f474
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { useBlockSideTools } from './BlockSideTools.hooks';
+import AddIcon from '@mui/icons-material/Add';
+import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
+import Portal from '../BlockPortal';
+import { IconButton } from '@mui/material';
+
+const sx = { height: 24, width: 24 };
+
+export default function BlockSideTools(props: { container: HTMLDivElement }) {
+ const { nodeId, ref, handleAddClick } = useBlockSideTools(props);
+
+ if (!nodeId) return null;
+ return (
+
+ {
+ // prevent toolbar from taking focus away from editor
+ e.preventDefault();
+ }}
+ >
+
handleAddClick()} sx={sx}>
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
new file mode 100644
index 0000000000..b4a152a824
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
@@ -0,0 +1,3 @@
+export default function CodeBlock({ id }: { id: string }) {
+ return {id}
;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx
similarity index 59%
rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx
index 8a6298bb2b..cd12b16f06 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx
@@ -1,17 +1,7 @@
import React from 'react';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
+import NodeComponent from '../Node';
-import BlockComponent from '../BlockComponent';
-
-export default function ColumnBlock({
- node,
- resizerWidth,
- index,
-}: {
- node: TreeNode;
- resizerWidth: number;
- index: number;
-}) {
+export default function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) {
const renderResizer = () => {
return (
@@ -35,15 +25,14 @@ export default function ColumnBlock({
renderResizer()
)}
- }
+ id={id}
/>
>
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
new file mode 100644
index 0000000000..dc67320b26
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
@@ -0,0 +1,8 @@
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+export function useDocumentTitle(id: string) {
+ const { node, delta } = useSubscribeNode(id);
+ return {
+ node,
+ delta
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
new file mode 100644
index 0000000000..2a7815b536
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { useDocumentTitle } from './DocumentTitle.hooks';
+import TextBlock from '../TextBlock';
+
+export default function DocumentTitle({ id }: { id: string }) {
+ const { node, delta } = useDocumentTitle(id);
+ if (!node) return null;
+ return (
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
new file mode 100644
index 0000000000..186d98e51c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
@@ -0,0 +1,17 @@
+import TextBlock from '../TextBlock';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
+
+const fontSize: Record = {
+ 1: 'mt-8 text-3xl',
+ 2: 'mt-6 text-2xl',
+ 3: 'mt-4 text-xl',
+};
+
+export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
similarity index 76%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
index 8319291046..ac512b536f 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
@@ -1,11 +1,9 @@
import { useEffect, useRef } from 'react';
import { useFocused, useSlate } from 'slate-react';
import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
-import { TreeNode } from '$app/block_editor/view/tree_node';
-export function useHoveringToolbar({node}: {
- node: TreeNode
-}) {
+
+export function useHoveringToolbar(id: string) {
const editor = useSlate();
const inFocus = useFocused();
const ref = useRef(null);
@@ -13,7 +11,7 @@ export function useHoveringToolbar({node}: {
useEffect(() => {
const el = ref.current;
if (!el) return;
- const nodeRect = document.querySelector(`[data-block-id=${node.id}]`)?.getBoundingClientRect();
+ const nodeRect = document.querySelector(`[data-block-id="${id}"]`)?.getBoundingClientRect();
if (!nodeRect) return;
const position = calcToolbarPosition(editor, el, nodeRect);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
similarity index 74%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
index dcd502905f..a35588033c 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
@@ -1,14 +1,13 @@
import FormatButton from './FormatButton';
-import Portal from './Portal';
-import { TreeNode } from '$app/block_editor/view/tree_node';
+import Portal from '../BlockPortal';
import { useHoveringToolbar } from './index.hooks';
-const HoveringToolbar = ({ blockId, node }: { blockId: string; node: TreeNode }) => {
- const { inFocus, ref, editor } = useHoveringToolbar({ node });
+const HoveringToolbar = ({ id }: { id: string }) => {
+ const { inFocus, ref, editor } = useHoveringToolbar(id);
if (!inFocus) return null;
return (
-
+
@@ -14,10 +21,8 @@ export default function BulletedListBlock({ title, node }: { title: JSX.Element;
- {node.children?.map((item) => (
-
-
-
+ {childIds?.map((item) => (
+
))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx
new file mode 100644
index 0000000000..82fd423e9d
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx
@@ -0,0 +1,23 @@
+import React, { useMemo } from 'react';
+import ColumnBlock from '../ColumnBlock';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+
+export default function ColumnListBlock({ node, childIds }: { node: Node; childIds?: string[] }) {
+ const resizerWidth = useMemo(() => {
+ return 46 * (node.children?.length || 0);
+ }, [node.children?.length]);
+ return (
+ <>
+
+ {childIds?.map((item, index) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx
new file mode 100644
index 0000000000..5c66f61133
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx
@@ -0,0 +1,30 @@
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import NodeComponent from '../Node';
+
+export default function NumberedListBlock({
+ title,
+ node,
+ childIds,
+}: {
+ title: JSX.Element;
+ node: Node;
+ childIds?: string[];
+}) {
+ const index = 1;
+ return (
+
+
+
{`${index} .`}
+ {title}
+
+
+
+ {childIds?.map((item) => (
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
similarity index 51%
rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
index 87c31795ce..a33b36cbde 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
@@ -3,28 +3,28 @@ import TextBlock from '../TextBlock';
import NumberedListBlock from './NumberedListBlock';
import BulletedListBlock from './BulletedListBlock';
import ColumnListBlock from './ColumnListBlock';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-import { BlockCommonProps } from '@/appflowy_app/interfaces';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
-export default function ListBlock({ node, version }: BlockCommonProps) {
+export default function ListBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
const title = useMemo(() => {
- if (node.data.type === 'column') return <>>;
+ if (node.data.style?.type === 'column') return <>>;
return (
-
+
);
- }, [node, version]);
+ }, [node, delta]);
- if (node.data.type === 'numbered') {
+ if (node.data.style?.type === 'numbered') {
return ;
}
- if (node.data.type === 'bulleted') {
+ if (node.data.style?.type === 'bulleted') {
return ;
}
- if (node.data.type === 'column') {
+ if (node.data.style?.type === 'column') {
return ;
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
new file mode 100644
index 0000000000..1bb2e2b25d
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
@@ -0,0 +1,36 @@
+
+import { useEffect, useRef } from 'react';
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+import { useAppDispatch } from '$app/stores/store';
+import { documentActions } from '$app/stores/reducers/document/slice';
+
+export function useNode(id: string) {
+ const { node, childIds, delta, isSelected } = useSubscribeNode(id);
+ const ref = useRef(null);
+
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ if (!ref.current) return;
+ const rect = ref.current.getBoundingClientRect();
+
+ const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement;
+ dispatch(documentActions.updateNodePosition({
+ id,
+ rect: {
+ x: rect.x,
+ y: rect.y + scrollContainer.scrollTop,
+ height: rect.height,
+ width: rect.width
+ }
+ }))
+ }, [])
+
+ return {
+ ref,
+ node,
+ childIds,
+ delta,
+ isSelected
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
new file mode 100644
index 0000000000..bfe2e9649b
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
@@ -0,0 +1,42 @@
+import React, { useCallback } from 'react';
+import { useNode } from './Node.hooks';
+import { withErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import TextBlock from '../TextBlock';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
+
+function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) {
+ const { node, childIds, delta, isSelected, ref } = useNode(id);
+
+ console.log('=====', id);
+ const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => {
+ switch (_props.node.type) {
+ case 'text':
+ if (!_props.delta) return null;
+ return ;
+ default:
+ break;
+ }
+ }, []);
+
+ if (!node) return null;
+
+ return (
+
+ {renderBlock({
+ node,
+ childIds,
+ delta,
+ })}
+
+ {isSelected ?
: null}
+
+ );
+}
+
+const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
+ FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(NodeWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
new file mode 100644
index 0000000000..62d15de804
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
@@ -0,0 +1,13 @@
+import React, { useState } from 'react';
+import BlockSideTools from '../BlockSideTools';
+import BlockSelection from '../BlockSelection';
+
+export default function Overlay({ container }: { container: HTMLDivElement }) {
+ const [isDragging, setDragging] = useState(false);
+ return (
+ <>
+ {isDragging ? null : }
+
+ >
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
new file mode 100644
index 0000000000..faf8df0897
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
@@ -0,0 +1,16 @@
+import { DocumentData } from '$app/interfaces/document';
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+import { useParseTree } from './Tree.hooks';
+
+export function useRoot({ documentData }: { documentData: DocumentData }) {
+ const { rootId } = documentData;
+
+ useParseTree(documentData);
+
+ const { node: rootNode, childIds: rootChildIds } = useSubscribeNode(rootId);
+
+ return {
+ node: rootNode,
+ childIds: rootChildIds,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
new file mode 100644
index 0000000000..1191705f0b
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+import { DocumentData } from '$app/interfaces/document';
+import { useAppDispatch } from '@/appflowy_app/stores/store';
+import { documentActions } from '$app/stores/reducers/document/slice';
+
+export function useParseTree(documentData: DocumentData) {
+ const dispatch = useAppDispatch();
+ const { blocks, ytexts, yarrays } = documentData;
+
+ useEffect(() => {
+ dispatch(
+ documentActions.createTree({
+ nodes: blocks,
+ delta: ytexts,
+ children: yarrays,
+ })
+ );
+
+ return () => {
+ dispatch(documentActions.clear());
+ };
+ }, [documentData]);
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
new file mode 100644
index 0000000000..3e89c1b31e
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
@@ -0,0 +1,32 @@
+import { DocumentData } from '@/appflowy_app/interfaces/document';
+import React, { useCallback } from 'react';
+import { useRoot } from './Root.hooks';
+import Node from '../Node';
+import { withErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
+import VirtualizerList from '../VirtualizerList';
+import { Skeleton } from '@mui/material';
+
+function Root({ documentData }: { documentData: DocumentData }) {
+ const { node, childIds } = useRoot({ documentData });
+
+ const renderNode = useCallback((nodeId: string) => {
+ return ;
+ }, []);
+
+ if (!node || !childIds) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+const RootWithErrorBoundary = withErrorBoundary(Root, {
+ FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(RootWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
new file mode 100644
index 0000000000..01aa2d204f
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
@@ -0,0 +1,61 @@
+
+
+import { useEffect, useMemo, useRef } from "react";
+import { createEditor } from "slate";
+import { withReact } from "slate-react";
+
+import * as Y from 'yjs';
+import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
+import { Delta } from '@slate-yjs/core/dist/model/types';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
+
+const initialValue = [{
+ type: 'paragraph',
+ children: [{ text: '' }],
+}];
+
+export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
+ const yTextRef = useRef();
+ // Create a yjs document and get the shared type
+ const sharedType = useMemo(() => {
+ const ydoc = new Y.Doc()
+ const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText;
+
+ const insertDelta = slateNodesToInsertDelta(initialValue);
+ // Load the initial value into the yjs document
+ _sharedType.applyDelta(insertDelta);
+
+ const yText = insertDelta[0].insert as Y.XmlText;
+ yTextRef.current = yText;
+
+ return _sharedType;
+ }, []);
+
+ const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
+
+ useEffect(() => {
+ YjsEditor.connect(editor);
+ return () => {
+ yTextRef.current = undefined;
+ YjsEditor.disconnect(editor);
+ }
+ }, [editor]);
+
+ useEffect(() => {
+ const yText = yTextRef.current;
+ if (!yText) return;
+
+ const textEventHandler = (event: Y.YTextEvent) => {
+ update(event.changes.delta as TextDelta[]);
+ }
+ yText.applyDelta(delta);
+ yText.observe(textEventHandler);
+
+ return () => {
+ yText.unobserve(textEventHandler);
+ }
+ }, [delta])
+
+
+ return { editor }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
new file mode 100644
index 0000000000..85ad6aced0
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
@@ -0,0 +1,110 @@
+import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
+import { useCallback, useContext, useMemo, useRef, useState } from "react";
+import { Descendant, Range } from "slate";
+import { useBindYjs } from "./BindYjs.hooks";
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import { TextDelta } from '$app/interfaces/document';
+import { debounce } from "@/appflowy_app/utils/tool";
+
+function useController(textId: string) {
+ const docController = useContext(DocumentControllerContext);
+
+ const update = useCallback(
+ (delta: TextDelta[]) => {
+ docController?.yTextApply(textId, delta)
+ },
+ [textId],
+ );
+ const transact = useCallback(
+ (actions: (() => void)[]) => {
+ docController?.transact(actions)
+ },
+ [textId],
+ )
+
+ return {
+ update,
+ transact
+ }
+}
+
+function useTransact(textId: string) {
+ const pendingActions = useRef<(() => void)[]>([]);
+ const { update, transact } = useController(textId);
+
+ const sendTransact = useCallback(
+ () => {
+ const actions = pendingActions.current;
+ transact(actions);
+ },
+ [transact],
+ )
+
+ const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
+
+ const sendDelta = useCallback(
+ (delta: TextDelta[]) => {
+ const action = () => update(delta);
+ pendingActions.current.push(action);
+ debounceSendTransact()
+ },
+ [update, debounceSendTransact],
+ );
+ return {
+ sendDelta
+ }
+}
+
+export function useTextBlock(text: string, delta: TextDelta[]) {
+ const { sendDelta } = useTransact(text);
+
+ const { editor } = useBindYjs(delta, sendDelta);
+ const [value, setValue] = useState([]);
+
+ const onChange = useCallback(
+ (e: Descendant[]) => {
+ setValue(e);
+ },
+ [editor],
+ );
+
+ const onKeyDownCapture = (event: React.KeyboardEvent) => {
+ switch (event.key) {
+ case 'Enter': {
+ event.stopPropagation();
+ event.preventDefault();
+
+ return;
+ }
+ case 'Backspace': {
+ if (!editor.selection) return;
+ const { anchor } = editor.selection;
+ const isCollapase = Range.isCollapsed(editor.selection);
+ if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+ }
+ }
+ triggerHotkey(event, editor);
+ }
+
+ const onDOMBeforeInput = useCallback((e: InputEvent) => {
+ // COMPAT: in Apple, `compositionend` is dispatched after the
+ // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
+ // Here, prevent the beforeInput event and wait for the compositionend event to take effect
+ if (e.inputType === 'insertFromComposition') {
+ e.preventDefault();
+ }
+
+ }, []);
+
+ return {
+ onChange,
+ onKeyDownCapture,
+ onDOMBeforeInput,
+ editor,
+ value
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
new file mode 100644
index 0000000000..a64bd56990
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
@@ -0,0 +1,46 @@
+import { Slate, Editable } from 'slate-react';
+import Leaf from './Leaf';
+import { useTextBlock } from './TextBlock.hooks';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import NodeComponent from '../Node';
+import HoveringToolbar from '../HoveringToolbar';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
+import React from 'react';
+
+function TextBlock({
+ node,
+ childIds,
+ placeholder,
+ delta,
+ ...props
+}: {
+ node: Node;
+ delta: TextDelta[];
+ childIds?: string[];
+ placeholder?: string;
+} & React.HTMLAttributes) {
+ const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta);
+
+ return (
+
+
+
+ }
+ placeholder={placeholder || 'Please enter some text...'}
+ />
+
+ {childIds && childIds.length > 0 ? (
+
+ {childIds.map((item) => (
+
+ ))}
+
+ ) : null}
+
+ );
+}
+
+export default React.memo(TextBlock);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
new file mode 100644
index 0000000000..c0e543bf5f
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
@@ -0,0 +1,21 @@
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { useRef } from 'react';
+
+const defaultSize = 60;
+
+export function useVirtualizerList(count: number) {
+ const parentRef = useRef(null);
+
+ const rowVirtualizer = useVirtualizer({
+ count,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => {
+ return defaultSize;
+ },
+ });
+
+ return {
+ rowVirtualizer,
+ parentRef,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
new file mode 100644
index 0000000000..5b3253b299
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useVirtualizerList } from './VirtualizerList.hooks';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import DocumentTitle from '../DocumentTitle';
+import Overlay from '../Overlay';
+
+export default function VirtualizerList({
+ childIds,
+ node,
+ renderNode,
+}: {
+ childIds: string[];
+ node: Node;
+ renderNode: (nodeId: string) => JSX.Element;
+}) {
+ const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
+
+ const virtualItems = rowVirtualizer.getVirtualItems();
+
+ return (
+ <>
+
+
+ {node && childIds && virtualItems.length ? (
+
+ {virtualItems.map((virtualRow) => {
+ const id = childIds[virtualRow.index];
+ return (
+
+ {virtualRow.index === 0 ? : null}
+ {renderNode(id)}
+
+ );
+ })}
+
+ ) : null}
+
+
+ {parentRef.current ? : null}
+ >
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
new file mode 100644
index 0000000000..fc6851734c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
@@ -0,0 +1,12 @@
+import { Alert } from '@mui/material';
+import { FallbackProps } from 'react-error-boundary';
+
+export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
+ return (
+
+ Something went wrong:
+ {error.message}
+ Try again
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
new file mode 100644
index 0000000000..1b3b4b71c8
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
@@ -0,0 +1,32 @@
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { useAppSelector } from '@/appflowy_app/stores/store';
+import { useMemo } from 'react';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
+
+export function useSubscribeNode(id: string) {
+ const node = useAppSelector(state => state.document.nodes[id]);
+ const childIds = useAppSelector(state => {
+ const childrenId = state.document.nodes[id]?.children;
+ if (!childrenId) return;
+ return state.document.children[childrenId];
+ });
+ const delta = useAppSelector(state => {
+ const deltaId = state.document.nodes[id]?.data?.text;
+ if (!deltaId) return;
+ return state.document.delta[deltaId];
+ });
+ const isSelected = useAppSelector(state => {
+ return state.document.selections?.includes(id) || false;
+ });
+
+ const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
+ const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
+ const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
+
+ return {
+ node: memoizedNode,
+ childIds: memoizedChildIds,
+ delta: memoizedDelta,
+ isSelected
+ };
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
index a0efb98d60..61c9a88e06 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
@@ -1,4 +1,3 @@
-import { TextBlockToolbarGroup } from "../interfaces";
export const iconSize = { width: 18, height: 18 };
@@ -24,16 +23,3 @@ export const command: Record = {
key: 'β + Shift + S or β + Shift + X',
},
};
-
-export const toolbarDefaultProps = {
- showGroups: [
- TextBlockToolbarGroup.ASK_AI,
- TextBlockToolbarGroup.BLOCK_SELECT,
- TextBlockToolbarGroup.ADD_LINK,
- TextBlockToolbarGroup.COMMENT,
- TextBlockToolbarGroup.TEXT_FORMAT,
- TextBlockToolbarGroup.TEXT_COLOR,
- TextBlockToolbarGroup.MENTION,
- TextBlockToolbarGroup.MORE,
- ],
-};
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
new file mode 100644
index 0000000000..0cfefe0d75
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
@@ -0,0 +1,31 @@
+// eslint-disable-next-line no-shadow
+export enum BlockType {
+ PageBlock = 'page',
+ HeadingBlock = 'heading',
+ ListBlock = 'list',
+ TextBlock = 'text',
+ CodeBlock = 'code',
+ EmbedBlock = 'embed',
+ QuoteBlock = 'quote',
+ DividerBlock = 'divider',
+ MediaBlock = 'media',
+ TableBlock = 'table',
+ ColumnBlock = 'column'
+}
+export interface NestedBlock {
+ id: string;
+ type: BlockType;
+ data: Record;
+ parent: string | null;
+ children: string;
+}
+export interface TextDelta {
+ insert: string;
+ attributes?: Record;
+}
+export interface DocumentData {
+ rootId: string;
+ blocks: Record;
+ ytexts: Record;
+ yarrays: Record;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
index e6d0760f64..db6c7f48b3 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
@@ -1,112 +1 @@
-import { Descendant } from "slate";
-
-// eslint-disable-next-line no-shadow
-export enum BlockType {
- PageBlock = 'page',
- HeadingBlock = 'heading',
- ListBlock = 'list',
- TextBlock = 'text',
- CodeBlock = 'code',
- EmbedBlock = 'embed',
- QuoteBlock = 'quote',
- DividerBlock = 'divider',
- MediaBlock = 'media',
- TableBlock = 'table',
- ColumnBlock = 'column'
-
-}
-
-export type BlockData = T extends BlockType.TextBlock ? TextBlockData :
-T extends BlockType.PageBlock ? PageBlockData :
-T extends BlockType.HeadingBlock ? HeadingBlockData :
-T extends BlockType.ListBlock ? ListBlockData :
-T extends BlockType.ColumnBlock ? ColumnBlockData : any;
-
-
-export interface BlockInterface {
- id: string;
- type: BlockType;
- data: BlockData;
- next: string | null;
- firstChild: string | null;
-}
-
-
-export interface TextBlockData {
- content: Descendant[];
-}
-
-interface PageBlockData {
- title: string;
-}
-
-interface ListBlockData extends TextBlockData {
- type: 'numbered' | 'bulleted' | 'column';
-}
-
-interface HeadingBlockData extends TextBlockData {
- level: number;
-}
-
-interface ColumnBlockData {
- ratio: string;
-}
-
-// eslint-disable-next-line no-shadow
-export enum TextBlockToolbarGroup {
- ASK_AI,
- BLOCK_SELECT,
- ADD_LINK,
- COMMENT,
- TEXT_FORMAT,
- TEXT_COLOR,
- MENTION,
- MORE
-}
-export interface TextBlockToolbarProps {
- showGroups: TextBlockToolbarGroup[]
-}
-
-
-export interface BlockCommonProps {
- version: number;
- node: T;
-}
-
-export interface BackendOp {
- type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
- version: number;
- data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
-}
-export interface LocalOp {
- type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
- version: number;
- data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
-}
-
-export interface UpdateOpData {
- blockId: string;
- value: BlockData;
- path: string[];
-}
-export interface InsertOpData {
- block: BlockInterface;
- parentId: string;
- prevId?: string
-}
-
-export interface moveRangeOpData {
- range: [string, string];
- newParentId: string;
- newPrevId?: string
-}
-
-export interface moveOpData {
- blockId: string;
- newParentId: string;
- newPrevId?: string
-}
-
-export interface removeOpData {
- blockId: string
-}
\ No newline at end of file
+export interface Document {}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
new file mode 100644
index 0000000000..e6ee3ec250
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
@@ -0,0 +1,50 @@
+import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
+import { createContext } from 'react';
+import { DocumentBackendService } from './document_bd_svc';
+import { Err } from 'ts-results';
+import { FlowyError } from '@/services/backend';
+
+export const DocumentControllerContext = createContext