chore: add more test and fix warnings

This commit is contained in:
appflowy 2022-03-16 10:02:37 +08:00
parent 47081f3095
commit b3ed254caf
24 changed files with 318 additions and 132 deletions

View File

@ -64,6 +64,8 @@ impl FlowyError {
static_flowy_error!(name_empty, ErrorCode::UserNameIsEmpty); static_flowy_error!(name_empty, ErrorCode::UserNameIsEmpty);
static_flowy_error!(user_id, ErrorCode::UserIdInvalid); static_flowy_error!(user_id, ErrorCode::UserIdInvalid);
static_flowy_error!(user_not_exist, ErrorCode::UserNotExist); static_flowy_error!(user_not_exist, ErrorCode::UserNotExist);
static_flowy_error!(text_too_long, ErrorCode::TextTooLong);
static_flowy_error!(invalid_data, ErrorCode::InvalidData);
} }
impl std::convert::From<ErrorCode> for FlowyError { impl std::convert::From<ErrorCode> for FlowyError {

View File

@ -351,7 +351,7 @@ pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &st
thumbnail: None, thumbnail: None,
data_type, data_type,
plugin_type: 0, plugin_type: 0,
data: "".to_string(), data: vec![],
}; };
let view = FolderEventBuilder::new(sdk.clone()) let view = FolderEventBuilder::new(sdk.clone())
.event(CreateView) .event(CreateView)

View File

@ -36,11 +36,7 @@ pub(crate) async fn get_fields_handler(
let payload: QueryFieldPayload = data.into_inner(); let payload: QueryFieldPayload = data.into_inner();
let editor = manager.get_grid_editor(&payload.grid_id)?; let editor = manager.get_grid_editor(&payload.grid_id)?;
let field_metas = editor.get_field_metas(Some(payload.field_orders)).await?; let field_metas = editor.get_field_metas(Some(payload.field_orders)).await?;
let repeated_field: RepeatedField = field_metas let repeated_field: RepeatedField = field_metas.into_iter().map(Field::from).collect::<Vec<_>>().into();
.into_iter()
.map(|field_meta| Field::from(field_meta))
.collect::<Vec<_>>()
.into();
data_result(repeated_field) data_result(repeated_field)
} }

View File

@ -111,8 +111,7 @@ impl GridManager {
) -> Result<Arc<ClientGridEditor>, FlowyError> { ) -> Result<Arc<ClientGridEditor>, FlowyError> {
let user = self.grid_user.clone(); let user = self.grid_user.clone();
let rev_manager = self.make_grid_rev_manager(grid_id, pool.clone())?; let rev_manager = self.make_grid_rev_manager(grid_id, pool.clone())?;
let kv_persistence = self.get_kv_persistence()?; let grid_editor = ClientGridEditor::new(grid_id, user, rev_manager).await?;
let grid_editor = ClientGridEditor::new(grid_id, user, rev_manager, kv_persistence).await?;
Ok(grid_editor) Ok(grid_editor)
} }
@ -137,6 +136,7 @@ impl GridManager {
Ok(rev_manager) Ok(rev_manager)
} }
#[allow(dead_code)]
fn get_kv_persistence(&self) -> FlowyResult<Arc<GridKVPersistence>> { fn get_kv_persistence(&self) -> FlowyResult<Arc<GridKVPersistence>> {
let read_guard = self.kv_persistence.read(); let read_guard = self.kv_persistence.read();
if read_guard.is_some() { if read_guard.is_some() {
@ -198,7 +198,7 @@ pub async fn make_grid_view_data(
let grid_block_meta_delta = make_block_meta_delta(&build_context.grid_block_meta); let grid_block_meta_delta = make_block_meta_delta(&build_context.grid_block_meta);
let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes(); let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes();
let repeated_revision: RepeatedRevision = let repeated_revision: RepeatedRevision =
Revision::initial_revision(&user_id, &block_id, block_meta_delta_data).into(); Revision::initial_revision(user_id, &block_id, block_meta_delta_data).into();
let _ = grid_manager let _ = grid_manager
.create_grid_block_meta(&block_id, repeated_revision) .create_grid_block_meta(&block_id, repeated_revision)
.await?; .await?;

View File

@ -139,7 +139,7 @@ impl GridBlockMetaEditorManager {
for grid_block in grid_blocks { for grid_block in grid_blocks {
let editor = self.get_editor(&grid_block.id).await?; let editor = self.get_editor(&grid_block.id).await?;
let row_metas = editor.get_row_metas(None).await?; let row_metas = editor.get_row_metas(None).await?;
let block_row_orders = row_metas.iter().map(|row_meta| RowOrder::from(row_meta)); let block_row_orders = row_metas.iter().map(RowOrder::from);
row_orders.extend(block_row_orders); row_orders.extend(block_row_orders);
} }
Ok(row_orders) Ok(row_orders)
@ -257,7 +257,7 @@ impl ClientGridBlockMetaEditor {
.await .await
.get_rows(None)? .get_rows(None)?
.iter() .iter()
.map(|row_meta| RowOrder::from(row_meta)) .map(RowOrder::from)
.collect::<Vec<RowOrder>>(); .collect::<Vec<RowOrder>>();
Ok(row_orders) Ok(row_orders)
} }

View File

@ -53,14 +53,11 @@ impl CellDataSerde for DateDescription {
} }
fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> { fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> {
let timestamp = match data.parse::<i64>() { if let Err(e) = data.parse::<i64>() {
Ok(timestamp) => timestamp,
Err(e) => {
tracing::error!("Parse {} to i64 failed: {}", data, e); tracing::error!("Parse {} to i64 failed: {}", data, e);
chrono::Utc::now().timestamp() return Err(FlowyError::internal().context(e));
}
}; };
Ok(format!("{}", timestamp)) Ok(data.to_owned())
} }
} }

View File

@ -93,7 +93,7 @@ impl NumberDescription {
} }
fn decimal_from_str(&self, s: &str) -> Decimal { fn decimal_from_str(&self, s: &str) -> Decimal {
let mut decimal = Decimal::from_str(s).unwrap_or(Decimal::zero()); let mut decimal = Decimal::from_str(s).unwrap_or_else(|_| Decimal::zero());
match decimal.set_scale(self.scale) { match decimal.set_scale(self.scale) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@ -130,7 +130,12 @@ impl CellDataSerde for NumberDescription {
} }
fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> { fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> {
Ok(self.strip_symbol(data)) let data = self.strip_symbol(data);
if !data.chars().all(char::is_numeric) {
return Err(FlowyError::invalid_data().context("Should only contain numbers"));
}
Ok(data)
} }
} }
@ -188,8 +193,10 @@ mod tests {
#[test] #[test]
fn number_description_scale_test() { fn number_description_scale_test() {
let mut description = NumberDescription::default(); let mut description = NumberDescription {
description.scale = 1; scale: 1,
..Default::default()
};
for format in NumberFormat::iter() { for format in NumberFormat::iter() {
description.format = format; description.format = format;
@ -224,8 +231,10 @@ mod tests {
#[test] #[test]
fn number_description_sign_test() { fn number_description_sign_test() {
let mut description = NumberDescription::default(); let mut description = NumberDescription {
description.sign_positive = false; sign_positive: false,
..Default::default()
};
for format in NumberFormat::iter() { for format in NumberFormat::iter() {
description.format = format; description.format = format;

View File

@ -2,9 +2,13 @@ use crate::impl_from_and_to_type_option;
use crate::services::row::CellDataSerde; use crate::services::row::CellDataSerde;
use crate::services::util::*; use crate::services::util::*;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::FlowyError; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{FieldMeta, FieldType}; use flowy_grid_data_model::entities::{FieldMeta, FieldType};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub const SELECTION_IDS_SEPARATOR: &str = ",";
// Single select // Single select
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
@ -23,7 +27,7 @@ impl CellDataSerde for SingleSelectDescription {
} }
fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> { fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> {
Ok(select_option_id_from_data(data.to_owned(), true)) single_select_option_id_from_data(data.to_owned())
} }
} }
@ -43,20 +47,45 @@ impl CellDataSerde for MultiSelectDescription {
} }
fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> { fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> {
Ok(select_option_id_from_data(data.to_owned(), false)) multi_select_option_id_from_data(data.to_owned())
} }
} }
fn select_option_id_from_data(data: String, is_single_select: bool) -> String { fn single_select_option_id_from_data(data: String) -> FlowyResult<String> {
if !is_single_select { let select_option_ids = select_option_ids(data)?;
return data;
}
let select_option_ids = data.split(',').collect::<Vec<&str>>();
if select_option_ids.is_empty() { if select_option_ids.is_empty() {
return "".to_owned(); return Ok("".to_owned());
} }
select_option_ids.split_first().unwrap().0.to_string() Ok(select_option_ids.split_first().unwrap().0.to_string())
}
fn multi_select_option_id_from_data(data: String) -> FlowyResult<String> {
let select_option_ids = select_option_ids(data)?;
Ok(select_option_ids.join(SELECTION_IDS_SEPARATOR))
}
fn select_option_ids(mut data: String) -> FlowyResult<Vec<String>> {
data.retain(|c| !c.is_whitespace());
let select_option_ids = data.split(SELECTION_IDS_SEPARATOR).collect::<Vec<&str>>();
if select_option_ids
.par_iter()
.find_first(|option_id| match Uuid::parse_str(option_id) {
Ok(_) => false,
Err(e) => {
tracing::error!("{}", e);
true
}
})
.is_some()
{
let msg = format!(
"Invalid selection id string: {}. It should consist of the uuid string and separated by comma",
data
);
return Err(FlowyError::internal().context(msg));
}
Ok(select_option_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]

View File

@ -19,6 +19,11 @@ impl CellDataSerde for RichTextDescription {
} }
fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> { fn serialize_cell_data(&self, data: &str) -> Result<String, FlowyError> {
Ok(data.to_owned()) let data = data.to_owned();
if data.len() > 10000 {
Err(FlowyError::text_too_long().context("The len of the text should not be more than 10000"))
} else {
Ok(data)
}
} }
} }

View File

@ -1,19 +1,19 @@
use crate::manager::GridUser; use crate::manager::GridUser;
use crate::services::block_meta_editor::GridBlockMetaEditorManager; use crate::services::block_meta_editor::GridBlockMetaEditorManager;
use crate::services::kv_persistence::{GridKVPersistence, KVTransaction};
use bytes::Bytes; use bytes::Bytes;
use flowy_collaboration::client_grid::{GridChange, GridMetaPad}; use flowy_collaboration::client_grid::{GridChange, GridMetaPad};
use flowy_collaboration::entities::revision::Revision; use flowy_collaboration::entities::revision::Revision;
use flowy_collaboration::util::make_delta_from_revisions; use flowy_collaboration::util::make_delta_from_revisions;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{ use flowy_grid_data_model::entities::{
CellMetaChangeset, Field, FieldChangeset, FieldMeta, Grid, GridBlock, GridBlockChangeset, RepeatedFieldOrder, CellMetaChangeset, FieldChangeset, FieldMeta, Grid, GridBlock, GridBlockChangeset, RepeatedFieldOrder,
RepeatedRowOrder, Row, RowMeta, RowMetaChangeset, RepeatedRowOrder, Row, RowMeta, RowMetaChangeset,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use crate::services::row::{ use crate::services::row::{
make_row_by_row_id, make_rows, row_meta_from_context, CreateRowContext, CreateRowContextBuilder, make_row_by_row_id, make_rows, row_meta_from_context, serialize_cell_data, CreateRowContext,
CreateRowContextBuilder,
}; };
use flowy_sync::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder}; use flowy_sync::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
@ -27,7 +27,6 @@ pub struct ClientGridEditor {
grid_meta_pad: Arc<RwLock<GridMetaPad>>, grid_meta_pad: Arc<RwLock<GridMetaPad>>,
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
block_meta_manager: Arc<GridBlockMetaEditorManager>, block_meta_manager: Arc<GridBlockMetaEditorManager>,
kv_persistence: Arc<GridKVPersistence>,
} }
impl ClientGridEditor { impl ClientGridEditor {
@ -35,7 +34,6 @@ impl ClientGridEditor {
grid_id: &str, grid_id: &str,
user: Arc<dyn GridUser>, user: Arc<dyn GridUser>,
mut rev_manager: RevisionManager, mut rev_manager: RevisionManager,
kv_persistence: Arc<GridKVPersistence>,
) -> FlowyResult<Arc<Self>> { ) -> FlowyResult<Arc<Self>> {
let token = user.token()?; let token = user.token()?;
let cloud = Arc::new(GridRevisionCloudService { token }); let cloud = Arc::new(GridRevisionCloudService { token });
@ -52,7 +50,6 @@ impl ClientGridEditor {
grid_meta_pad, grid_meta_pad,
rev_manager, rev_manager,
block_meta_manager, block_meta_manager,
kv_persistence,
})) }))
} }
@ -88,7 +85,8 @@ impl ClientGridEditor {
pub async fn create_row(&self) -> FlowyResult<()> { pub async fn create_row(&self) -> FlowyResult<()> {
let field_metas = self.grid_meta_pad.read().await.get_field_metas(None)?; let field_metas = self.grid_meta_pad.read().await.get_field_metas(None)?;
let block_id = self.last_block_id().await?; let block_id = self.last_block_id().await?;
let row = row_meta_from_context(&block_id, CreateRowContextBuilder::new(&field_metas).build()); let create_row_ctx = CreateRowContextBuilder::new(&field_metas).build();
let row = row_meta_from_context(&block_id, create_row_ctx);
let row_count = self.block_meta_manager.create_row(row).await?; let row_count = self.block_meta_manager.create_row(row).await?;
let changeset = GridBlockChangeset::from_row_count(&block_id, row_count); let changeset = GridBlockChangeset::from_row_count(&block_id, row_count);
let _ = self.update_block(changeset).await?; let _ = self.update_block(changeset).await?;
@ -98,12 +96,11 @@ impl ClientGridEditor {
pub async fn insert_rows(&self, contexts: Vec<CreateRowContext>) -> FlowyResult<()> { pub async fn insert_rows(&self, contexts: Vec<CreateRowContext>) -> FlowyResult<()> {
let block_id = self.last_block_id().await?; let block_id = self.last_block_id().await?;
let mut rows_by_block_id: HashMap<String, Vec<RowMeta>> = HashMap::new(); let mut rows_by_block_id: HashMap<String, Vec<RowMeta>> = HashMap::new();
for ctx in contexts { for ctx in contexts {
let row_meta = row_meta_from_context(&block_id, ctx); let row_meta = row_meta_from_context(&block_id, ctx);
rows_by_block_id rows_by_block_id
.entry(block_id.clone()) .entry(block_id.clone())
.or_insert(Vec::new()) .or_insert_with(Vec::new)
.push(row_meta); .push(row_meta);
} }
let changesets = self.block_meta_manager.insert_row(rows_by_block_id).await?; let changesets = self.block_meta_manager.insert_row(rows_by_block_id).await?;
@ -118,6 +115,18 @@ impl ClientGridEditor {
} }
pub async fn update_cell(&self, changeset: CellMetaChangeset) -> FlowyResult<()> { pub async fn update_cell(&self, changeset: CellMetaChangeset) -> FlowyResult<()> {
if let Some(cell_data) = changeset.data.as_ref() {
match self.grid_meta_pad.read().await.get_field(&changeset.field_id) {
None => {
return Err(FlowyError::internal()
.context(format!("Can not find the field with id: {}", &changeset.field_id)));
}
Some(field_meta) => {
let _ = serialize_cell_data(cell_data, field_meta)?;
}
}
}
let row_changeset: RowMetaChangeset = changeset.into(); let row_changeset: RowMetaChangeset = changeset.into();
self.update_row(row_changeset).await self.update_row(row_changeset).await
} }

View File

@ -1,28 +1,43 @@
use crate::services::row::serialize_cell_data;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{CellMeta, FieldMeta, RowMeta, DEFAULT_ROW_HEIGHT}; use flowy_grid_data_model::entities::{CellMeta, FieldMeta, RowMeta, DEFAULT_ROW_HEIGHT};
use std::collections::HashMap; use std::collections::HashMap;
pub struct CreateRowContextBuilder<'a> { pub struct CreateRowContextBuilder<'a> {
#[allow(dead_code)] field_meta_map: HashMap<&'a String, &'a FieldMeta>,
fields: &'a [FieldMeta],
ctx: CreateRowContext, ctx: CreateRowContext,
} }
impl<'a> CreateRowContextBuilder<'a> { impl<'a> CreateRowContextBuilder<'a> {
pub fn new(fields: &'a [FieldMeta]) -> Self { pub fn new(fields: &'a [FieldMeta]) -> Self {
let field_meta_map = fields
.iter()
.map(|field| (&field.id, field))
.collect::<HashMap<&String, &FieldMeta>>();
let ctx = CreateRowContext { let ctx = CreateRowContext {
row_id: uuid::Uuid::new_v4().to_string(), row_id: uuid::Uuid::new_v4().to_string(),
cell_by_field_id: Default::default(), cell_by_field_id: Default::default(),
height: DEFAULT_ROW_HEIGHT, height: DEFAULT_ROW_HEIGHT,
visibility: true, visibility: true,
}; };
Self { fields, ctx }
Self { field_meta_map, ctx }
} }
#[allow(dead_code)] pub fn add_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> {
pub fn add_cell(mut self, field_id: &str, data: String) -> Self { match self.field_meta_map.get(&field_id.to_owned()) {
None => {
let msg = format!("Invalid field_id: {}", field_id);
Err(FlowyError::internal().context(msg))
}
Some(field_meta) => {
let data = serialize_cell_data(&data, field_meta)?;
let cell = CellMeta::new(field_id, data); let cell = CellMeta::new(field_id, data);
self.ctx.cell_by_field_id.insert(field_id.to_owned(), cell); self.ctx.cell_by_field_id.insert(field_id.to_owned(), cell);
self Ok(())
}
}
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@ -1,8 +1,11 @@
use crate::grid::script::EditorScript::*; use crate::grid::script::EditorScript::*;
use crate::grid::script::*; use crate::grid::script::*;
use chrono::NaiveDateTime;
use flowy_grid::services::cell::*; use flowy_grid::services::cell::*;
use flowy_grid::services::row::{deserialize_cell_data, serialize_cell_data, CellDataSerde, CreateRowContextBuilder}; use flowy_grid::services::row::{deserialize_cell_data, serialize_cell_data, CellDataSerde, CreateRowContextBuilder};
use flowy_grid_data_model::entities::{FieldChangeset, FieldType, GridBlock, GridBlockChangeset, RowMetaChangeset}; use flowy_grid_data_model::entities::{
CellMetaChangeset, FieldChangeset, FieldType, GridBlock, GridBlockChangeset, RowMetaChangeset,
};
#[tokio::test] #[tokio::test]
async fn grid_create_field() { async fn grid_create_field() {
@ -237,29 +240,28 @@ async fn grid_delete_row() {
} }
#[tokio::test] #[tokio::test]
async fn grid_update_cell() { async fn grid_row_add_cells_test() {
let mut test = GridEditorTest::new().await; let mut test = GridEditorTest::new().await;
let mut builder = CreateRowContextBuilder::new(&test.field_metas); let mut builder = CreateRowContextBuilder::new(&test.field_metas);
for field in &test.field_metas { for field in &test.field_metas {
match field.field_type { match field.field_type {
FieldType::RichText => { FieldType::RichText => {
let data = serialize_cell_data("hello world", field).unwrap(); let data = serialize_cell_data("hello world", field).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
FieldType::Number => { FieldType::Number => {
let data = serialize_cell_data("¥18,443", field).unwrap(); let data = serialize_cell_data("¥18,443", field).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
FieldType::DateTime => { FieldType::DateTime => {
let data = serialize_cell_data("1647251762", field).unwrap(); let data = serialize_cell_data("1647251762", field).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
FieldType::SingleSelect => { FieldType::SingleSelect => {
let description = SingleSelectDescription::from(field); let description = SingleSelectDescription::from(field);
let options = description.options.first().unwrap(); let options = description.options.first().unwrap();
let data = description.serialize_cell_data(&options.id).unwrap(); let data = description.serialize_cell_data(&options.id).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
FieldType::MultiSelect => { FieldType::MultiSelect => {
let description = MultiSelectDescription::from(field); let description = MultiSelectDescription::from(field);
@ -268,17 +270,161 @@ async fn grid_update_cell() {
.iter() .iter()
.map(|option| option.id.clone()) .map(|option| option.id.clone())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(SELECTION_IDS_SEPARATOR);
let data = description.serialize_cell_data(&options).unwrap(); let data = description.serialize_cell_data(&options).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
FieldType::Checkbox => { FieldType::Checkbox => {
let data = serialize_cell_data("false", field).unwrap(); let data = serialize_cell_data("false", field).unwrap();
builder = builder.add_cell(&field.id, data); builder.add_cell(&field.id, data).unwrap();
} }
} }
} }
let context = builder.build(); let context = builder.build();
let scripts = vec![AssertRowCount(3), CreateRow { context }, AssertGridMetaPad]; let scripts = vec![CreateRow { context }, AssertGridMetaPad];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_row_add_selection_cell_test() {
let mut test = GridEditorTest::new().await;
let mut builder = CreateRowContextBuilder::new(&test.field_metas);
let uuid = uuid::Uuid::new_v4().to_string();
let mut single_select_field_id = "".to_string();
let mut multi_select_field_id = "".to_string();
for field in &test.field_metas {
match field.field_type {
FieldType::SingleSelect => {
single_select_field_id = field.id.clone();
// The element must be parsed as uuid
assert!(builder.add_cell(&field.id, "data".to_owned()).is_err());
// // The data should not be empty
assert!(builder.add_cell(&field.id, "".to_owned()).is_err());
// The element must be parsed as uuid
assert!(builder.add_cell(&field.id, "1,2,3".to_owned()).is_err(),);
// The separator must be comma
assert!(builder.add_cell(&field.id, format!("{}. {}", &uuid, &uuid),).is_err());
//
assert!(builder.add_cell(&field.id, uuid.clone()).is_ok());
assert!(builder.add_cell(&field.id, format!("{}, {}", &uuid, &uuid)).is_ok());
}
FieldType::MultiSelect => {
multi_select_field_id = field.id.clone();
assert!(builder.add_cell(&field.id, format!("{}, {}", &uuid, &uuid)).is_ok());
}
_ => {}
}
}
let context = builder.build();
assert_eq!(
&context
.cell_by_field_id
.get(&single_select_field_id)
.as_ref()
.unwrap()
.data,
&uuid
);
assert_eq!(
context
.cell_by_field_id
.get(&multi_select_field_id)
.as_ref()
.unwrap()
.data,
format!("{},{}", &uuid, &uuid)
);
let scripts = vec![CreateRow { context }];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_row_add_date_cell_test() {
let mut test = GridEditorTest::new().await;
let mut builder = CreateRowContextBuilder::new(&test.field_metas);
let mut date_field = None;
let timestamp = 1647390674;
for field in &test.field_metas {
if field.field_type == FieldType::DateTime {
date_field = Some(field.clone());
NaiveDateTime::from_timestamp(123, 0);
// The data should not be empty
assert!(builder.add_cell(&field.id, "".to_owned()).is_err());
assert!(builder.add_cell(&field.id, "123".to_owned()).is_ok());
assert!(builder.add_cell(&field.id, format!("{}", timestamp)).is_ok());
}
}
let context = builder.build();
let date_field = date_field.unwrap();
let cell_data = context.cell_by_field_id.get(&date_field.id).unwrap().clone();
assert_eq!(
deserialize_cell_data(cell_data.data.clone(), &date_field).unwrap(),
"2022/03/16 08:31",
);
let scripts = vec![CreateRow { context }];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_cell_update() {
let mut test = GridEditorTest::new().await;
let field_metas = &test.field_metas;
let row_metas = &test.row_metas;
assert_eq!(row_metas.len(), 3);
let mut scripts = vec![];
for (index, row_meta) in row_metas.iter().enumerate() {
for field_meta in field_metas {
if index == 0 {
let data = match field_meta.field_type {
FieldType::RichText => "".to_string(),
FieldType::Number => "123".to_string(),
FieldType::DateTime => "123".to_string(),
FieldType::SingleSelect => {
let description = SingleSelectDescription::from(field_meta);
description.options.first().unwrap().id.clone()
}
FieldType::MultiSelect => {
let description = MultiSelectDescription::from(field_meta);
description.options.first().unwrap().id.clone()
}
FieldType::Checkbox => "1".to_string(),
};
scripts.push(UpdateCell {
changeset: CellMetaChangeset {
row_id: row_meta.id.clone(),
field_id: field_meta.id.clone(),
data: Some(data),
},
is_err: false,
});
}
if index == 1 {
let (data, is_err) = match field_meta.field_type {
FieldType::RichText => ("1".to_string().repeat(10001), true),
FieldType::Number => ("abc".to_string(), true),
FieldType::DateTime => ("abc".to_string(), true),
FieldType::SingleSelect => ("abc".to_string(), true),
FieldType::MultiSelect => ("abc".to_string(), true),
FieldType::Checkbox => ("2".to_string(), false),
};
scripts.push(UpdateCell {
changeset: CellMetaChangeset {
row_id: row_meta.id.clone(),
field_id: field_meta.id.clone(),
data: Some(data),
},
is_err,
});
}
}
}
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }

View File

@ -1,8 +1,6 @@
use bytes::Bytes; use bytes::Bytes;
use flowy_collaboration::client_grid::GridBuilder; use flowy_collaboration::client_grid::GridBuilder;
use flowy_collaboration::entities::revision::{RepeatedRevision, Revision};
use flowy_error::FlowyResult;
use flowy_grid::manager::{make_grid_view_data, GridManager};
use flowy_grid::services::cell::*; use flowy_grid::services::cell::*;
use flowy_grid::services::field::*; use flowy_grid::services::field::*;
use flowy_grid::services::grid_editor::{ClientGridEditor, GridPadBuilder}; use flowy_grid::services::grid_editor::{ClientGridEditor, GridPadBuilder};
@ -16,7 +14,7 @@ use flowy_test::helper::ViewTest;
use flowy_test::FlowySDKTest; use flowy_test::FlowySDKTest;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use strum::{EnumCount, IntoEnumIterator}; use strum::EnumCount;
use tokio::time::sleep; use tokio::time::sleep;
pub enum EditorScript { pub enum EditorScript {
@ -65,6 +63,7 @@ pub enum EditorScript {
}, },
UpdateCell { UpdateCell {
changeset: CellMetaChangeset, changeset: CellMetaChangeset,
is_err: bool,
}, },
AssertRowCount(usize), AssertRowCount(usize),
// AssertRowEqual{ row_index: usize, row: RowMeta}, // AssertRowEqual{ row_index: usize, row: RowMeta},
@ -199,10 +198,15 @@ impl GridEditorTest {
assert_eq!(row.height, height); assert_eq!(row.height, height);
} }
} }
EditorScript::UpdateCell { changeset } => { EditorScript::UpdateCell { changeset, is_err } => {
self.editor.update_cell(changeset).await.unwrap(); let result = self.editor.update_cell(changeset).await;
if is_err {
assert!(result.is_err())
} else {
let _ = result.unwrap();
self.row_metas = self.editor.get_row_metas(None).await.unwrap(); self.row_metas = self.editor.get_row_metas(None).await.unwrap();
} }
}
EditorScript::AssertRowCount(count) => { EditorScript::AssertRowCount(count) => {
assert_eq!(self.editor.get_rows(None).await.unwrap().len(), count); assert_eq!(self.editor.get_rows(None).await.unwrap().len(), count);
} }

View File

@ -4,7 +4,7 @@ use flowy_collaboration::client_document::default::initial_quill_delta_string;
use flowy_collaboration::entities::revision::{RepeatedRevision, Revision}; use flowy_collaboration::entities::revision::{RepeatedRevision, Revision};
use flowy_collaboration::entities::ws_data::ClientRevisionWSData; use flowy_collaboration::entities::ws_data::ClientRevisionWSData;
use flowy_database::ConnectionPool; use flowy_database::ConnectionPool;
use flowy_folder::errors::FlowyResult;
use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap}; use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap};
use flowy_folder::prelude::ViewDataType; use flowy_folder::prelude::ViewDataType;
use flowy_folder::{ use flowy_folder::{

View File

@ -52,7 +52,7 @@ impl GridBlockMetaPad {
pub fn get_rows(&self, row_ids: Option<Vec<String>>) -> CollaborateResult<Vec<Arc<RowMeta>>> { pub fn get_rows(&self, row_ids: Option<Vec<String>>) -> CollaborateResult<Vec<Arc<RowMeta>>> {
match row_ids { match row_ids {
None => Ok(self.rows.iter().map(|row| row.clone()).collect::<Vec<_>>()), None => Ok(self.rows.to_vec()),
Some(row_ids) => { Some(row_ids) => {
let row_map = self let row_map = self
.rows .rows

View File

@ -1,19 +1,11 @@
use crate::client_grid::{make_block_meta_delta, make_grid_delta, GridBlockMetaDelta, GridMetaDelta};
use crate::errors::{CollaborateError, CollaborateResult}; use crate::errors::{CollaborateError, CollaborateResult};
use flowy_grid_data_model::entities::{BuildGridContext, FieldMeta, GridBlock, GridBlockMeta, GridMeta, RowMeta}; use flowy_grid_data_model::entities::{BuildGridContext, FieldMeta, RowMeta};
#[derive(Default)]
pub struct GridBuilder { pub struct GridBuilder {
build_context: BuildGridContext, build_context: BuildGridContext,
} }
impl std::default::Default for GridBuilder {
fn default() -> Self {
Self {
build_context: Default::default(),
}
}
}
impl GridBuilder { impl GridBuilder {
pub fn add_field(mut self, field: FieldMeta) -> Self { pub fn add_field(mut self, field: FieldMeta) -> Self {
self.build_context.field_metas.push(field); self.build_context.field_metas.push(field);
@ -63,7 +55,7 @@ mod tests {
.build(); .build();
let grid_meta = GridMeta { let grid_meta = GridMeta {
grid_id: grid_id.clone(), grid_id,
fields: build_context.field_metas, fields: build_context.field_metas,
blocks: vec![build_context.grid_block], blocks: vec![build_context.grid_block],
}; };

View File

@ -58,19 +58,15 @@ impl GridMetaPad {
} }
pub fn contain_field(&self, field_id: &str) -> bool { pub fn contain_field(&self, field_id: &str) -> bool {
self.grid_meta self.grid_meta.fields.iter().any(|field| field.id == field_id)
.fields }
.iter()
.find(|field| &field.id == field_id) pub fn get_field(&self, field_id: &str) -> Option<&FieldMeta> {
.is_some() self.grid_meta.fields.iter().find(|field| field.id == field_id)
} }
pub fn get_field_orders(&self) -> Vec<FieldOrder> { pub fn get_field_orders(&self) -> Vec<FieldOrder> {
self.grid_meta self.grid_meta.fields.iter().map(FieldOrder::from).collect()
.fields
.iter()
.map(|field_meta| FieldOrder::from(field_meta))
.collect()
} }
pub fn get_field_metas(&self, field_orders: Option<RepeatedFieldOrder>) -> CollaborateResult<Vec<FieldMeta>> { pub fn get_field_metas(&self, field_orders: Option<RepeatedFieldOrder>) -> CollaborateResult<Vec<FieldMeta>> {
@ -154,7 +150,7 @@ impl GridMetaPad {
if last_block.start_row_index > block.start_row_index if last_block.start_row_index > block.start_row_index
&& last_block.len() > block.start_row_index && last_block.len() > block.start_row_index
{ {
let msg = format!("GridBlock's start_row_index should be greater than the last_block's start_row_index and its len"); let msg = "GridBlock's start_row_index should be greater than the last_block's start_row_index and its len".to_string();
return Err(CollaborateError::internal().context(msg)) return Err(CollaborateError::internal().context(msg))
} }
grid.blocks.push(block); grid.blocks.push(block);

View File

@ -1,7 +1,7 @@
mod block_pad; mod block_pad;
mod grid_builder; mod grid_builder;
mod grid_pad; mod grid_meta_pad;
pub use block_pad::*; pub use block_pad::*;
pub use grid_builder::*; pub use grid_builder::*;
pub use grid_pad::*; pub use grid_meta_pad::*;

View File

@ -83,6 +83,10 @@ pub enum ErrorCode {
UserIdInvalid = 311, UserIdInvalid = 311,
#[display(fmt = "User not exist")] #[display(fmt = "User not exist")]
UserNotExist = 312, UserNotExist = 312,
#[display(fmt = "Text is too long")]
TextTooLong = 400,
#[display(fmt = "Invalid data")]
InvalidData = 401,
} }
impl ErrorCode { impl ErrorCode {

View File

@ -55,6 +55,8 @@ pub enum ErrorCode {
UserNameIsEmpty = 310, UserNameIsEmpty = 310,
UserIdInvalid = 311, UserIdInvalid = 311,
UserNotExist = 312, UserNotExist = 312,
TextTooLong = 400,
InvalidData = 401,
} }
impl ::protobuf::ProtobufEnum for ErrorCode { impl ::protobuf::ProtobufEnum for ErrorCode {
@ -94,6 +96,8 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
310 => ::std::option::Option::Some(ErrorCode::UserNameIsEmpty), 310 => ::std::option::Option::Some(ErrorCode::UserNameIsEmpty),
311 => ::std::option::Option::Some(ErrorCode::UserIdInvalid), 311 => ::std::option::Option::Some(ErrorCode::UserIdInvalid),
312 => ::std::option::Option::Some(ErrorCode::UserNotExist), 312 => ::std::option::Option::Some(ErrorCode::UserNotExist),
400 => ::std::option::Option::Some(ErrorCode::TextTooLong),
401 => ::std::option::Option::Some(ErrorCode::InvalidData),
_ => ::std::option::Option::None _ => ::std::option::Option::None
} }
} }
@ -130,6 +134,8 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
ErrorCode::UserNameIsEmpty, ErrorCode::UserNameIsEmpty,
ErrorCode::UserIdInvalid, ErrorCode::UserIdInvalid,
ErrorCode::UserNotExist, ErrorCode::UserNotExist,
ErrorCode::TextTooLong,
ErrorCode::InvalidData,
]; ];
values values
} }
@ -158,7 +164,7 @@ impl ::protobuf::reflect::ProtobufValue for ErrorCode {
} }
static file_descriptor_proto_data: &'static [u8] = b"\ static file_descriptor_proto_data: &'static [u8] = b"\
\n\ncode.proto*\xc4\x05\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\ \n\ncode.proto*\xe8\x05\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\
\n\x10UserUnauthorized\x10\x02\x12\x12\n\x0eRecordNotFound\x10\x03\x12\ \n\x10UserUnauthorized\x10\x02\x12\x12\n\x0eRecordNotFound\x10\x03\x12\
\x18\n\x14WorkspaceNameInvalid\x10d\x12\x16\n\x12WorkspaceIdInvalid\x10e\ \x18\n\x14WorkspaceNameInvalid\x10d\x12\x16\n\x12WorkspaceIdInvalid\x10e\
\x12\x18\n\x14AppColorStyleInvalid\x10f\x12\x18\n\x14WorkspaceDescTooLon\ \x12\x18\n\x14AppColorStyleInvalid\x10f\x12\x18\n\x14WorkspaceDescTooLon\
@ -174,8 +180,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
rmatInvalid\x10\xb2\x02\x12\x15\n\x10PasswordNotMatch\x10\xb3\x02\x12\ rmatInvalid\x10\xb2\x02\x12\x15\n\x10PasswordNotMatch\x10\xb3\x02\x12\
\x14\n\x0fUserNameTooLong\x10\xb4\x02\x12'\n\"UserNameContainForbiddenCh\ \x14\n\x0fUserNameTooLong\x10\xb4\x02\x12'\n\"UserNameContainForbiddenCh\
aracters\x10\xb5\x02\x12\x14\n\x0fUserNameIsEmpty\x10\xb6\x02\x12\x12\n\ aracters\x10\xb5\x02\x12\x14\n\x0fUserNameIsEmpty\x10\xb6\x02\x12\x12\n\
\rUserIdInvalid\x10\xb7\x02\x12\x11\n\x0cUserNotExist\x10\xb8\x02b\x06pr\ \rUserIdInvalid\x10\xb7\x02\x12\x11\n\x0cUserNotExist\x10\xb8\x02\x12\
oto3\ \x10\n\x0bTextTooLong\x10\x90\x03\x12\x10\n\x0bInvalidData\x10\x91\x03b\
\x06proto3\
"; ";
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

View File

@ -31,4 +31,6 @@ enum ErrorCode {
UserNameIsEmpty = 310; UserNameIsEmpty = 310;
UserIdInvalid = 311; UserIdInvalid = 311;
UserNotExist = 312; UserNotExist = 312;
TextTooLong = 400;
InvalidData = 401;
} }

View File

@ -4,7 +4,7 @@ use crate::{
impl_def_and_def_mut, impl_def_and_def_mut,
parser::{ parser::{
app::AppIdentify, app::AppIdentify,
view::{ViewDesc, ViewExtensionData, ViewIdentify, ViewName, ViewThumbnail}, view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail},
}, },
}; };
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};

View File

@ -1,7 +1,7 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use strum::{EnumCount, IntoEnumIterator};
use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, EnumString}; use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, EnumString};
pub const DEFAULT_ROW_HEIGHT: i32 = 36; pub const DEFAULT_ROW_HEIGHT: i32 = 36;
@ -35,6 +35,10 @@ impl GridBlock {
pub fn len(&self) -> i32 { pub fn len(&self) -> i32 {
self.start_row_index + self.row_count self.start_row_index + self.row_count
} }
pub fn is_empty(&self) -> bool {
self.row_count == 0
}
} }
impl GridBlock { impl GridBlock {

View File

@ -65,34 +65,3 @@ where
} }
pub type BoxResultFuture<'a, T, E> = BoxFuture<'a, Result<T, E>>; pub type BoxResultFuture<'a, T, E> = BoxFuture<'a, Result<T, E>>;
#[pin_project]
pub struct FutureResultSend<T, E> {
#[pin]
pub fut: Pin<Box<dyn Future<Output = Result<T, E>> + Send>>,
}
impl<T, E> FutureResultSend<T, E> {
pub fn new<F>(f: F) -> Self
where
F: Future<Output = Result<T, E>> + Send + 'static,
{
Self {
fut: Box::pin(async { f.await }),
}
}
}
impl<T, E> Future for FutureResultSend<T, E>
where
T: Send,
E: Debug,
{
type Output = Result<T, E>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
let result = ready!(this.fut.poll(cx));
Poll::Ready(result)
}
}