test: add databaase event test (#2728)

* test: add tests and fix modify primary field bug

* test: add more test

* fix: tauri buiuld

* chore: disable share link button
This commit is contained in:
Nathan.fooo 2023-06-07 14:52:35 +08:00 committed by GitHub
parent 1b56538a2f
commit ce8cee5637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 623 additions and 343 deletions

View File

@ -16,8 +16,10 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
@override
Future<Option<FlowyError>> save(String data) async {
final fut =
_cellBackendSvc.updateCell(cellContext: cellContext, data: data);
final fut = _cellBackendSvc.updateCell(
cellContext: cellContext,
data: data,
);
return fut.then((result) {
return result.fold(
(l) => none(),

View File

@ -29,7 +29,6 @@ class FieldBackendService {
Future<Either<Unit, FlowyError>> updateField({
String? name,
FieldType? fieldType,
bool? frozen,
bool? visibility,
double? width,
@ -42,10 +41,6 @@ class FieldBackendService {
payload.name = name;
}
if (fieldType != null) {
payload.fieldType = fieldType;
}
if (frozen != null) {
payload.frozen = frozen;
}

View File

@ -124,13 +124,29 @@ class _FieldOperationList extends StatelessWidget {
}
Widget _actionCell(FieldAction action) {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldInfo.field.isPrimary) {
switch (action) {
case FieldAction.hide:
break;
case FieldAction.duplicate:
enable = false;
break;
case FieldAction.delete:
enable = false;
break;
}
}
return Flexible(
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FieldActionCell(
fieldInfo: fieldInfo,
action: action,
enable: action != FieldAction.delete || !fieldInfo.field.isPrimary,
enable: enable,
),
),
);

View File

@ -72,5 +72,6 @@ class CheckboxCardCellState with _$CheckboxCardCellState {
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
}

View File

@ -4,7 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/plugins/document/application/share_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -104,11 +103,11 @@ class ShareActionList extends StatelessWidget {
showMessageToast('Exported to: $exportPath');
}
break;
case ShareAction.copyLink:
NavigatorAlertDialog(
title: LocaleKeys.shareAction_workInProgress.tr(),
).show(context);
break;
// case ShareAction.copyLink:
// NavigatorAlertDialog(
// title: LocaleKeys.shareAction_workInProgress.tr(),
// ).show(context);
// break;
}
controller.close();
},
@ -118,7 +117,7 @@ class ShareActionList extends StatelessWidget {
enum ShareAction {
markdown,
copyLink,
// copyLink,
}
class ShareActionWrapper extends ActionCell {
@ -133,8 +132,8 @@ class ShareActionWrapper extends ActionCell {
switch (inner) {
case ShareAction.markdown:
return LocaleKeys.shareAction_markdown.tr();
case ShareAction.copyLink:
return LocaleKeys.shareAction_copyLink.tr();
// case ShareAction.copyLink:
// return LocaleKeys.shareAction_copyLink.tr();
}
}
}

View File

@ -1,57 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test("create a text filter and then alter the filter's field)", () async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.textFieldContext();
// Create the filter menu bloc
final menuBloc = GridFilterMenuBloc(
fieldController: context.fieldController,
viewId: context.gridView.id,
)..add(const GridFilterMenuEvent.initial());
// Insert filter for the text field
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
assert(menuBloc.state.filters.length == 1);
// Edit the text field
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: textField.field,
);
final editorBloc = FieldEditorBloc(
isGroupField: false,
loader: loader,
field: textField.field,
)..add(const FieldEditorEvent.initial());
await gridResponseFuture();
// Alter the field type to Number
editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number));
await gridResponseFuture();
// Check the number of filters
assert(menuBloc.state.filters.isEmpty);
});
}

View File

@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"collab",
@ -1024,7 +1024,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"bytes",
@ -1042,7 +1042,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"bytes",
"collab-sync",
@ -1060,7 +1060,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"async-trait",
@ -1086,7 +1086,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"proc-macro2",
"quote",
@ -1098,7 +1098,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"collab",
@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"collab",
@ -1134,7 +1134,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"bincode",
"chrono",
@ -1154,7 +1154,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"anyhow",
"async-trait",
@ -1173,6 +1173,7 @@ dependencies = [
"rusoto_credential",
"serde",
"serde_json",
"similar 2.2.1",
"thiserror",
"tokio",
"tokio-retry",
@ -1184,7 +1185,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
dependencies = [
"bytes",
"collab",
@ -1789,7 +1790,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"similar",
"similar 1.3.0",
"syn 1.0.109",
"tera",
"toml 0.5.11",
@ -1818,6 +1819,7 @@ version = "0.1.0"
dependencies = [
"appflowy-integrate",
"bytes",
"diesel",
"flowy-config",
"flowy-database2",
"flowy-document2",
@ -1839,6 +1841,7 @@ dependencies = [
"serde_repr",
"tokio",
"tracing",
"uuid",
]
[[package]]
@ -5055,6 +5058,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]]
name = "similar"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
[[package]]
name = "siphasher"
version = "0.3.10"

View File

@ -21,23 +21,13 @@ export abstract class TypeOptionParser<T> {
export class FieldBackendService {
constructor(public readonly viewId: string, public readonly fieldId: string) {}
updateField = (data: {
name?: string;
fieldType?: FieldType;
frozen?: boolean;
visibility?: boolean;
width?: number;
}) => {
updateField = (data: { name?: string; frozen?: boolean; visibility?: boolean; width?: number }) => {
const payload = FieldChangesetPB.fromObject({ view_id: this.viewId, field_id: this.fieldId });
if (data.name !== undefined) {
payload.name = data.name;
}
if (data.fieldType !== undefined) {
payload.field_type = data.fieldType;
}
if (data.frozen !== undefined) {
payload.frozen = data.frozen;
}

View File

@ -1864,6 +1864,7 @@ dependencies = [
"bytes",
"dotenv",
"flowy-core",
"flowy-database2",
"flowy-folder2",
"flowy-net",
"flowy-notification",

View File

@ -1,182 +0,0 @@
use crate::entities::FieldType;
use crate::services::cell::TypeCellData;
use crate::services::database::DatabaseEditor;
use crate::services::field::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
URLCellData,
};
use database_model::{FieldRevision, TypeOptionDataDeserializer};
use flowy_error::{FlowyError, FlowyResult};
use indexmap::IndexMap;
use serde::Serialize;
use serde_json::{json, Map, Value};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize)]
pub struct ExportField {
pub id: String,
pub name: String,
pub field_type: i64,
pub visibility: bool,
pub width: i64,
pub type_options: HashMap<String, Value>,
pub is_primary: bool,
}
#[derive(Debug, Clone, Serialize)]
struct ExportCell {
data: String,
field_type: FieldType,
}
impl From<&Arc<FieldRevision>> for ExportField {
fn from(field_rev: &Arc<FieldRevision>) -> Self {
let field_type = FieldType::from(field_rev.ty);
let mut type_options: HashMap<String, Value> = HashMap::new();
field_rev
.type_options
.iter()
.filter(|(k, _)| k == &&field_rev.ty.to_string())
.for_each(|(k, s)| {
let value = match field_type {
FieldType::RichText => {
let pb = RichTextTypeOptionPB::from_json_str(s);
serde_json::to_value(pb).unwrap()
},
FieldType::Number => {
let pb = NumberTypeOptionPB::from_json_str(s);
let mut map = Map::new();
map.insert("format".to_string(), json!(pb.format as u8));
map.insert("scale".to_string(), json!(pb.scale));
map.insert("symbol".to_string(), json!(pb.symbol));
map.insert("name".to_string(), json!(pb.name));
Value::Object(map)
},
FieldType::DateTime => {
let pb = DateTypeOptionPB::from_json_str(s);
let mut map = Map::new();
map.insert("date_format".to_string(), json!(pb.date_format as u8));
map.insert("time_format".to_string(), json!(pb.time_format as u8));
map.insert("field_type".to_string(), json!(FieldType::DateTime as u8));
Value::Object(map)
},
FieldType::SingleSelect => {
let pb = SingleSelectTypeOptionPB::from_json_str(s);
let value = serde_json::to_string(&pb).unwrap();
let mut map = Map::new();
map.insert("content".to_string(), Value::String(value));
Value::Object(map)
},
FieldType::MultiSelect => {
let pb = MultiSelectTypeOptionPB::from_json_str(s);
let value = serde_json::to_string(&pb).unwrap();
let mut map = Map::new();
map.insert("content".to_string(), Value::String(value));
Value::Object(map)
},
FieldType::Checkbox => {
let pb = CheckboxTypeOptionPB::from_json_str(s);
serde_json::to_value(pb).unwrap()
},
FieldType::URL => {
let pb = RichTextTypeOptionPB::from_json_str(s);
serde_json::to_value(pb).unwrap()
},
FieldType::Checklist => {
let pb = ChecklistTypeOptionPB::from_json_str(s);
let value = serde_json::to_string(&pb).unwrap();
let mut map = Map::new();
map.insert("content".to_string(), Value::String(value));
Value::Object(map)
},
};
type_options.insert(k.clone(), value);
});
Self {
id: field_rev.id.clone(),
name: field_rev.name.clone(),
field_type: field_rev.ty as i64,
visibility: true,
width: 100,
type_options,
is_primary: field_rev.is_primary,
}
}
}
pub struct CSVExport;
impl CSVExport {
pub async fn export_database(
&self,
view_id: &str,
database_editor: &Arc<DatabaseEditor>,
) -> FlowyResult<String> {
let mut wtr = csv::Writer::from_writer(vec![]);
let row_revs = database_editor.get_all_row_revs(view_id).await?;
let field_revs = database_editor.get_field_revs(None).await?;
// Write fields
let field_records = field_revs
.iter()
.map(|field| ExportField::from(field))
.map(|field| serde_json::to_string(&field).unwrap())
.collect::<Vec<String>>();
wtr
.write_record(&field_records)
.map_err(|e| FlowyError::internal().context(e))?;
// Write rows
let mut field_by_field_id = IndexMap::new();
field_revs.into_iter().for_each(|field| {
field_by_field_id.insert(field.id.clone(), field);
});
for row_rev in row_revs {
let cells = field_by_field_id
.iter()
.map(|(field_id, field)| {
let field_type = FieldType::from(field.ty);
let data = row_rev
.cells
.get(field_id)
.map(|cell| TypeCellData::try_from(cell))
.map(|data| {
data
.map(|data| match field_type {
FieldType::DateTime => {
match serde_json::from_str::<DateCellData>(&data.cell_str) {
Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
Err(_) => "".to_string(),
}
},
FieldType::URL => match serde_json::from_str::<URLCellData>(&data.cell_str) {
Ok(cell_data) => cell_data.content,
Err(_) => "".to_string(),
},
_ => data.cell_str,
})
.unwrap_or_default()
})
.unwrap_or_else(|| "".to_string());
let cell = ExportCell { data, field_type };
serde_json::to_string(&cell).unwrap()
})
.collect::<Vec<_>>();
if let Err(e) = wtr.write_record(&cells) {
tracing::warn!("CSV failed to write record: {}", e);
}
}
let data = wtr
.into_inner()
.map_err(|e| FlowyError::internal().context(e))?;
let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?;
Ok(csv)
}
}

View File

@ -155,6 +155,8 @@ pub struct CreateFieldPayloadPB {
#[pb(index = 2)]
pub field_type: FieldType,
/// If the type_option_data is not empty, it will be used to create the field.
/// Otherwise, the default value will be used.
#[pb(index = 3, one_of)]
pub type_option_data: Option<Vec<u8>>,
}
@ -163,6 +165,8 @@ pub struct CreateFieldPayloadPB {
pub struct CreateFieldParams {
pub view_id: String,
pub field_type: FieldType,
/// If the type_option_data is not empty, it will be used to create the field.
/// Otherwise, the default value will be used.
pub type_option_data: Option<Vec<u8>>,
}
@ -189,9 +193,6 @@ pub struct UpdateFieldTypePayloadPB {
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub create_if_not_exist: bool,
}
pub struct EditFieldParams {
@ -401,18 +402,13 @@ pub struct FieldChangesetPB {
pub desc: Option<String>,
#[pb(index = 5, one_of)]
pub field_type: Option<FieldType>,
#[pb(index = 6, one_of)]
pub frozen: Option<bool>,
#[pb(index = 7, one_of)]
#[pb(index = 6, one_of)]
pub visibility: Option<bool>,
#[pb(index = 8, one_of)]
#[pb(index = 7, one_of)]
pub width: Option<i32>,
// #[pb(index = 9, one_of)]
// pub type_option_data: Option<Vec<u8>>,
}
impl TryInto<FieldChangesetParams> for FieldChangesetPB {
@ -421,7 +417,6 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
fn try_into(self) -> Result<FieldChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
let field_type = self.field_type.map(FieldType::from);
// if let Some(type_option_data) = self.type_option_data.as_ref() {
// if type_option_data.is_empty() {
// return Err(ErrorCode::TypeOptionDataIsEmpty);
@ -433,7 +428,6 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
view_id: view_id.0,
name: self.name,
desc: self.desc,
field_type,
frozen: self.frozen,
visibility: self.visibility,
width: self.width,
@ -452,8 +446,6 @@ pub struct FieldChangesetParams {
pub desc: Option<String>,
pub field_type: Option<FieldType>,
pub frozen: Option<bool>,
pub visibility: Option<bool>,

View File

@ -249,7 +249,7 @@ pub(crate) async fn get_field_type_option_data_handler(
}
}
/// Create FieldMeta and save it. Return the FieldTypeOptionData.
/// Create TypeOptionPB and save it. Return the FieldTypeOptionData.
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn create_field_type_option_data_handler(
data: AFPluginData<CreateFieldPayloadPB>,

View File

@ -10,7 +10,7 @@ use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
use parking_lot::Mutex;
use tokio::sync::{broadcast, RwLock};
use flowy_error::{internal_error, FlowyError, FlowyResult};
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_task::TaskDispatcher;
use lib_infra::future::{to_fut, Fut};
@ -199,32 +199,32 @@ impl DatabaseEditor {
}
}
/// Returns a list of fields of the view.
/// If `field_ids` is not provided, all the fields will be returned in the order of the field that
/// defined in the view. Otherwise, the fields will be returned in the order of the `field_ids`.
pub fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> {
self.database.lock().get_fields(view_id, field_ids)
let database = self.database.lock();
let field_ids = field_ids.unwrap_or_else(|| {
database
.fields
.get_all_field_orders()
.into_iter()
.map(|field| field.id)
.collect()
});
database.get_fields(view_id, Some(field_ids))
}
pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> {
let is_primary = self
.database
.lock()
.fields
.get_field(&params.field_id)
.map(|field| field.is_primary)
.unwrap_or(false);
self
.database
.lock()
.fields
.update_field(&params.field_id, |mut update| {
update = update
.update_field(&params.field_id, |update| {
update
.set_name_if_not_none(params.name)
.set_width_at_if_not_none(params.width.map(|value| value as i64))
.set_visibility_if_not_none(params.visibility);
if is_primary {
tracing::warn!("Cannot update primary field type");
} else {
update.set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into()));
}
});
self
.notify_did_update_database_field(&params.field_id)
@ -233,6 +233,21 @@ impl DatabaseEditor {
}
pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> {
let is_primary = self
.database
.lock()
.fields
.get_field(field_id)
.map(|field| field.is_primary)
.unwrap_or(false);
if is_primary {
return Err(FlowyError::new(
ErrorCode::Internal,
"Can not delete primary field",
));
}
let database_id = {
let database = self.database.lock();
database.delete_field(field_id);
@ -283,6 +298,13 @@ impl DatabaseEditor {
match field {
None => {},
Some(field) => {
if field.is_primary {
return Err(FlowyError::new(
ErrorCode::Internal,
"Can not update primary field's field type",
));
}
let old_field_type = FieldType::from(field.field_type);
let old_type_option = field.get_any_type_option(old_field_type.clone());
let new_type_option = field
@ -312,6 +334,21 @@ impl DatabaseEditor {
}
pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> {
let is_primary = self
.database
.lock()
.fields
.get_field(field_id)
.map(|field| field.is_primary)
.unwrap_or(false);
if is_primary {
return Err(FlowyError::new(
ErrorCode::Internal,
"Can not duplicate primary field",
));
}
let value = self
.database
.lock()

View File

@ -90,7 +90,9 @@ impl CellDataDecoder for CheckboxTypeOption {
return Ok(Default::default());
}
self.parse_cell(cell)
let cell = self.parse_cell(cell);
println!("cell: {:?}", cell);
return cell;
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {

View File

@ -233,30 +233,6 @@ async fn grid_switch_from_checkbox_to_text_test() {
test.run_scripts(scripts).await;
}
// Test when switching the current field from Checkbox to Text test
// input:
// "Yes" -> check
// "" -> unchecked
#[tokio::test]
async fn grid_switch_from_text_to_checkbox_test() {
let mut test = DatabaseFieldTest::new().await;
let field = test.get_first_field(FieldType::RichText).clone();
let scripts = vec![
SwitchToField {
field_id: field.id.clone(),
new_field_type: FieldType::Checkbox,
},
AssertCellContent {
field_id: field.id.clone(),
row_index: 0,
from_field_type: FieldType::RichText,
expected_content: "".to_string(),
},
];
test.run_scripts(scripts).await;
}
// Test when switching the current field from Date to Text test
// input:
// 1647251762 -> Mar 14,2022 (This string will be different base on current data setting)

View File

@ -41,10 +41,14 @@ pub(crate) async fn open_workspace_handler(
match params.value {
None => Err(FlowyError::workspace_id().context("workspace id should not be empty")),
Some(workspace_id) => {
let workspace = folder.open_workspace(&workspace_id).await?;
let views = folder.get_workspace_views(&workspace_id).await?;
let workspace_pb: WorkspacePB = (workspace, views).into();
data_result_ok(workspace_pb)
if workspace_id.is_empty() {
return Err(FlowyError::workspace_id().context("workspace id should not be empty"));
} else {
let workspace = folder.open_workspace(&workspace_id).await?;
let views = folder.get_workspace_views(&workspace_id).await?;
let workspace_pb: WorkspacePB = (workspace, views).into();
data_result_ok(workspace_pb)
}
},
}
}
@ -68,7 +72,7 @@ pub(crate) async fn read_workspaces_handler(
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub async fn read_cur_workspace_setting_handler(
pub async fn read_current_workspace_setting_handler(
folder: AFPluginState<Arc<Folder2Manager>>,
) -> DataResult<WorkspaceSettingPB, FlowyError> {
let workspace = folder.get_current_workspace().await?;

View File

@ -13,7 +13,7 @@ pub fn init(folder: Arc<Folder2Manager>) -> AFPlugin {
.event(FolderEvent::CreateWorkspace, create_workspace_handler)
.event(
FolderEvent::GetCurrentWorkspace,
read_cur_workspace_setting_handler,
read_current_workspace_setting_handler,
)
.event(FolderEvent::ReadAllWorkspaces, read_workspaces_handler)
.event(FolderEvent::OpenWorkspace, open_workspace_handler)

View File

@ -10,6 +10,7 @@ flowy-core = { path = "../flowy-core" }
flowy-user = { path = "../flowy-user"}
flowy-net = { path = "../flowy-net"}
flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
flowy-database2 = { path = "../flowy-database2" }
lib-dispatch = { path = "../lib-dispatch" }
lib-ot = { path = "../../../shared-lib/lib-ot" }
lib-infra = { path = "../../../shared-lib/lib-infra" }

View File

@ -5,10 +5,10 @@ use nanoid::nanoid;
use parking_lot::RwLock;
use flowy_core::{AppFlowyCore, AppFlowyCoreConfig};
use flowy_folder2::entities::{
CreateViewPayloadPB, RepeatedViewIdPB, ViewIdPB, ViewPB, WorkspaceSettingPB,
};
use flowy_database2::entities::*;
use flowy_folder2::entities::*;
use flowy_user::entities::{AuthTypePB, UserProfilePB};
use flowy_user::errors::FlowyError;
use crate::event_builder::EventBuilder;
use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
@ -113,6 +113,210 @@ impl FlowyCoreTest {
.parse::<flowy_folder2::entities::ViewPB>()
}
pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {
let payload = CreateViewPayloadPB {
parent_view_id: parent_id.to_string(),
name,
desc: "".to_string(),
thumbnail: None,
layout: ViewLayoutPB::Grid,
initial_data,
meta: Default::default(),
set_as_current: true,
};
EventBuilder::new(self.clone())
.event(flowy_folder2::event_map::FolderEvent::CreateView)
.payload(payload)
.async_send()
.await
.parse::<flowy_folder2::entities::ViewPB>()
}
pub async fn get_database(&self, view_id: &str) -> DatabasePB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::GetDatabase)
.payload(DatabaseViewIdPB {
value: view_id.to_string(),
})
.async_send()
.await
.parse::<flowy_database2::entities::DatabasePB>()
}
pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::GetFields)
.payload(GetFieldPayloadPB {
view_id: view_id.to_string(),
field_ids: None,
})
.async_send()
.await
.parse::<RepeatedFieldPB>()
}
pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::CreateTypeOption)
.payload(CreateFieldPayloadPB {
view_id: view_id.to_string(),
field_type,
type_option_data: None,
})
.async_send()
.await
.parse::<TypeOptionPB>()
.field
}
pub async fn update_field(&self, changeset: FieldChangesetPB) {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::UpdateField)
.payload(changeset)
.async_send()
.await;
}
pub async fn delete_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::DeleteField)
.payload(DeleteFieldPayloadPB {
view_id: view_id.to_string(),
field_id: field_id.to_string(),
})
.async_send()
.await
.error()
}
pub async fn update_field_type(
&self,
view_id: &str,
field_id: &str,
field_type: FieldType,
) -> Option<FlowyError> {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::UpdateFieldType)
.payload(UpdateFieldTypePayloadPB {
view_id: view_id.to_string(),
field_id: field_id.to_string(),
field_type,
})
.async_send()
.await
.error()
}
pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::DuplicateField)
.payload(DuplicateFieldPayloadPB {
view_id: view_id.to_string(),
field_id: field_id.to_string(),
})
.async_send()
.await
.error()
}
pub async fn create_row(
&self,
view_id: &str,
start_row_id: Option<String>,
data: Option<RowDataPB>,
) -> RowPB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::CreateRow)
.payload(CreateRowPayloadPB {
view_id: view_id.to_string(),
start_row_id,
group_id: None,
data,
})
.async_send()
.await
.parse::<RowPB>()
}
pub async fn get_row(&self, view_id: &str, row_id: &str) -> RowPB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::GetRow)
.payload(RowIdPB {
view_id: view_id.to_string(),
row_id: row_id.to_string(),
group_id: None,
})
.async_send()
.await
.parse::<RowPB>()
}
pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::DuplicateRow)
.payload(RowIdPB {
view_id: view_id.to_string(),
row_id: row_id.to_string(),
group_id: None,
})
.async_send()
.await
.error()
}
pub async fn update_cell(&self, changeset: CellChangesetPB) -> Option<FlowyError> {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::UpdateCell)
.payload(changeset)
.async_send()
.await
.error()
}
pub async fn get_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> CellPB {
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::GetCell)
.payload(CellIdPB {
view_id: view_id.to_string(),
row_id: row_id.to_string(),
field_id: field_id.to_string(),
})
.async_send()
.await
.parse::<CellPB>()
}
pub async fn insert_option(
&self,
view_id: &str,
field_id: &str,
row_id: &str,
name: &str,
) -> Option<FlowyError> {
let option = EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::CreateSelectOption)
.payload(CreateSelectOptionPayloadPB {
field_id: field_id.to_string(),
view_id: view_id.to_string(),
option_name: name.to_string(),
})
.async_send()
.await
.parse::<SelectOptionPB>();
EventBuilder::new(self.clone())
.event(flowy_database2::event_map::DatabaseEvent::InsertOrUpdateSelectOption)
.payload(RepeatedSelectOptionPayload {
view_id: view_id.to_string(),
field_id: field_id.to_string(),
row_id: row_id.to_string(),
items: vec![option],
})
.async_send()
.await
.error()
}
pub async fn get_view(&self, view_id: &str) -> ViewPB {
EventBuilder::new(self.clone())
.event(flowy_folder2::event_map::FolderEvent::ReadView)

View File

@ -1 +1,291 @@
use bytes::Bytes;
use flowy_database2::entities::{
CellChangesetPB, DatabaseLayoutPB, DatabaseViewIdPB, FieldType, SelectOptionCellDataPB,
};
use flowy_test::event_builder::EventBuilder;
use flowy_test::FlowyCoreTest;
use std::convert::TryFrom;
#[tokio::test]
async fn get_database_id_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
// The view id can be used to get the database id.
let database_id = EventBuilder::new(test.clone())
.event(flowy_database2::event_map::DatabaseEvent::GetDatabaseId)
.payload(DatabaseViewIdPB {
value: grid_view.id.clone(),
})
.async_send()
.await
.parse::<flowy_database2::entities::DatabaseIdPB>()
.value;
assert_ne!(database_id, grid_view.id);
}
#[tokio::test]
async fn get_database_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let database = test.get_database(&grid_view.id).await;
assert_eq!(database.fields.len(), 3);
assert_eq!(database.rows.len(), 3);
assert_eq!(database.layout_type, DatabaseLayoutPB::Grid);
}
#[tokio::test]
async fn get_field_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields[0].field_type, FieldType::RichText);
assert_eq!(fields[1].field_type, FieldType::SingleSelect);
assert_eq!(fields[2].field_type, FieldType::Checkbox);
assert_eq!(fields.len(), 3);
}
#[tokio::test]
async fn create_field_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
test.create_field(&grid_view.id, FieldType::Checkbox).await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields.len(), 4);
assert_eq!(fields[3].field_type, FieldType::Checkbox);
}
#[tokio::test]
async fn delete_field_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields[0].field_type, FieldType::RichText);
assert_eq!(fields[1].field_type, FieldType::SingleSelect);
assert_eq!(fields[2].field_type, FieldType::Checkbox);
let error = test.delete_field(&grid_view.id, &fields[1].id).await;
assert!(error.is_none());
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields.len(), 2);
}
#[tokio::test]
async fn delete_primary_field_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
// the primary field is not allowed to be deleted.
assert!(fields[0].is_primary);
let error = test.delete_field(&grid_view.id, &fields[0].id).await;
assert!(error.is_some());
}
#[tokio::test]
async fn update_field_type_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
let error = test
.update_field_type(&grid_view.id, &fields[1].id, FieldType::Checklist)
.await;
assert!(error.is_none());
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields[1].field_type, FieldType::Checklist);
}
#[tokio::test]
async fn update_primary_field_type_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
// the primary field is not allowed to be deleted.
assert!(fields[0].is_primary);
// the primary field is not allowed to be updated.
let error = test
.update_field_type(&grid_view.id, &fields[0].id, FieldType::Checklist)
.await;
assert!(error.is_some());
}
#[tokio::test]
async fn duplicate_field_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
// the primary field is not allowed to be updated.
let error = test.duplicate_field(&grid_view.id, &fields[1].id).await;
assert!(error.is_none());
let fields = test.get_all_database_fields(&grid_view.id).await.items;
assert_eq!(fields.len(), 4);
}
#[tokio::test]
async fn duplicate_primary_field_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
// the primary field is not allowed to be duplicated.
let error = test.duplicate_field(&grid_view.id, &fields[0].id).await;
assert!(error.is_some());
}
#[tokio::test]
async fn create_row_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let _ = test.create_row(&grid_view.id, None, None).await;
let database = test.get_database(&grid_view.id).await;
assert_eq!(database.rows.len(), 4);
}
#[tokio::test]
async fn duplicate_row_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let database = test.get_database(&grid_view.id).await;
let error = test
.duplicate_row(&grid_view.id, &database.rows[0].id)
.await;
assert!(error.is_none());
let database = test.get_database(&grid_view.id).await;
assert_eq!(database.rows.len(), 4);
}
#[tokio::test]
async fn update_text_cell_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let database = test.get_database(&grid_view.id).await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
let row_id = database.rows[0].id.clone();
let field_id = fields[0].id.clone();
assert_eq!(fields[0].field_type, FieldType::RichText);
// Update the first cell of the first row.
let error = test
.update_cell(CellChangesetPB {
view_id: grid_view.id.clone(),
row_id: row_id.clone(),
field_id: field_id.clone(),
cell_changeset: "hello world".to_string(),
})
.await;
assert!(error.is_none());
let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
let s = String::from_utf8(cell.data).unwrap();
assert_eq!(s, "hello world");
}
#[tokio::test]
async fn update_checkbox_cell_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let database = test.get_database(&grid_view.id).await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
let row_id = database.rows[0].id.clone();
let field_id = fields[2].id.clone();
assert_eq!(fields[2].field_type, FieldType::Checkbox);
for input in vec!["yes", "true", "1"] {
let error = test
.update_cell(CellChangesetPB {
view_id: grid_view.id.clone(),
row_id: row_id.clone(),
field_id: field_id.clone(),
cell_changeset: input.to_string(),
})
.await;
assert!(error.is_none());
let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
let output = String::from_utf8(cell.data).unwrap();
assert_eq!(output, "Yes");
}
}
#[tokio::test]
async fn update_single_select_cell_event_test() {
let test = FlowyCoreTest::new_with_user().await;
let current_workspace = test.get_current_workspace().await.workspace;
let grid_view = test
.create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
.await;
let database = test.get_database(&grid_view.id).await;
let fields = test.get_all_database_fields(&grid_view.id).await.items;
let row_id = database.rows[0].id.clone();
let field_id = fields[1].id.clone();
assert_eq!(fields[1].field_type, FieldType::SingleSelect);
let error = test
.insert_option(&grid_view.id, &field_id, &row_id, "task 1")
.await;
assert!(error.is_none());
let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
let select_option_cell = SelectOptionCellDataPB::try_from(Bytes::from(cell.data)).unwrap();
assert_eq!(select_option_cell.options.len(), 1);
assert_eq!(select_option_cell.select_options.len(), 1);
}