mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Implement summary field for database row (#5246)
* chore: impl summary field * chore: draft ui * chore: add summary event * chore: impl desktop ui * chore: impl mobile ui * chore: update test * chore: disable ai test
This commit is contained in:
@ -448,6 +448,7 @@ pub enum FieldType {
|
||||
LastEditedTime = 8,
|
||||
CreatedTime = 9,
|
||||
Relation = 10,
|
||||
Summary = 11,
|
||||
}
|
||||
|
||||
impl Display for FieldType {
|
||||
@ -487,6 +488,7 @@ impl FieldType {
|
||||
FieldType::LastEditedTime => "Last modified",
|
||||
FieldType::CreatedTime => "Created time",
|
||||
FieldType::Relation => "Relation",
|
||||
FieldType::Summary => "Summarize",
|
||||
};
|
||||
s.to_string()
|
||||
}
|
||||
|
@ -101,11 +101,14 @@ impl From<&Filter> for FilterPB {
|
||||
.cloned::<CheckboxFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
|
||||
FieldType::Relation => condition_and_content
|
||||
.cloned::<RelationFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
FieldType::Summary => condition_and_content
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -150,6 +153,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
|
||||
FieldType::Relation => {
|
||||
BoxAny::new(RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
FieldType::Summary => {
|
||||
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Self::Data {
|
||||
|
@ -15,6 +15,7 @@ macro_rules! impl_into_field_type {
|
||||
8 => FieldType::LastEditedTime,
|
||||
9 => FieldType::CreatedTime,
|
||||
10 => FieldType::Relation,
|
||||
11 => FieldType::Summary,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
||||
FieldType::RichText
|
||||
|
@ -359,3 +359,15 @@ pub struct CreateRowParams {
|
||||
pub collab_params: collab_database::rows::CreateRowParams,
|
||||
pub open_after_create: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||
pub struct SummaryRowPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub row_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub field_id: String,
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ mod date_entities;
|
||||
mod number_entities;
|
||||
mod relation_entities;
|
||||
mod select_option_entities;
|
||||
mod summary_entities;
|
||||
mod text_entities;
|
||||
mod timestamp_entities;
|
||||
mod url_entities;
|
||||
@ -14,6 +15,7 @@ pub use date_entities::*;
|
||||
pub use number_entities::*;
|
||||
pub use relation_entities::*;
|
||||
pub use select_option_entities::*;
|
||||
pub use summary_entities::*;
|
||||
pub use text_entities::*;
|
||||
pub use timestamp_entities::*;
|
||||
pub use url_entities::*;
|
||||
|
@ -0,0 +1,24 @@
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct SummarizationTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub auto_fill: bool,
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOption> for SummarizationTypeOptionPB {
|
||||
fn from(value: SummarizationTypeOption) -> Self {
|
||||
SummarizationTypeOptionPB {
|
||||
auto_fill: value.auto_fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOptionPB> for SummarizationTypeOption {
|
||||
fn from(value: SummarizationTypeOptionPB) -> Self {
|
||||
SummarizationTypeOption {
|
||||
auto_fill: value.auto_fill,
|
||||
}
|
||||
}
|
||||
}
|
@ -465,7 +465,7 @@ pub(crate) async fn update_cell_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
¶ms.view_id,
|
||||
RowId::from(params.row_id),
|
||||
&RowId::from(params.row_id),
|
||||
¶ms.field_id,
|
||||
BoxAny::new(params.cell_changeset),
|
||||
)
|
||||
@ -548,7 +548,7 @@ pub(crate) async fn update_select_option_cell_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
¶ms.cell_identifier.view_id,
|
||||
params.cell_identifier.row_id,
|
||||
¶ms.cell_identifier.row_id,
|
||||
¶ms.cell_identifier.field_id,
|
||||
BoxAny::new(changeset),
|
||||
)
|
||||
@ -577,7 +577,7 @@ pub(crate) async fn update_checklist_cell_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
¶ms.view_id,
|
||||
params.row_id,
|
||||
¶ms.row_id,
|
||||
¶ms.field_id,
|
||||
BoxAny::new(changeset),
|
||||
)
|
||||
@ -608,7 +608,7 @@ pub(crate) async fn update_date_cell_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
&cell_id.view_id,
|
||||
cell_id.row_id,
|
||||
&cell_id.row_id,
|
||||
&cell_id.field_id,
|
||||
BoxAny::new(cell_changeset),
|
||||
)
|
||||
@ -868,7 +868,7 @@ pub(crate) async fn move_calendar_event_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
&cell_id.view_id,
|
||||
cell_id.row_id,
|
||||
&cell_id.row_id,
|
||||
&cell_id.field_id,
|
||||
BoxAny::new(cell_changeset),
|
||||
)
|
||||
@ -1053,7 +1053,7 @@ pub(crate) async fn update_relation_cell_handler(
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
&view_id,
|
||||
cell_id.row_id,
|
||||
&cell_id.row_id,
|
||||
&cell_id.field_id,
|
||||
BoxAny::new(params),
|
||||
)
|
||||
@ -1086,3 +1086,16 @@ pub(crate) async fn get_related_database_rows_handler(
|
||||
|
||||
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_row_handler(
|
||||
data: AFPluginData<SummaryRowPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let data = data.into_inner();
|
||||
let row_id = RowId::from(data.row_id);
|
||||
manager
|
||||
.summarize_row(data.view_id, row_id, data.field_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -84,11 +84,13 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler)
|
||||
.event(DatabaseEvent::UpdateCalculation, update_calculation_handler)
|
||||
.event(DatabaseEvent::RemoveCalculation, remove_calculation_handler)
|
||||
// Relation
|
||||
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
|
||||
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
|
||||
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
|
||||
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
|
||||
// Relation
|
||||
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
|
||||
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
|
||||
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
|
||||
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
|
||||
// AI
|
||||
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
|
||||
}
|
||||
|
||||
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
||||
@ -368,4 +370,7 @@ pub enum DatabaseEvent {
|
||||
/// Get the names of all the rows in a related database.
|
||||
#[event(input = "DatabaseIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedDatabaseRows = 173,
|
||||
|
||||
#[event(input = "SummaryRowPB")]
|
||||
SummarizeRow = 174,
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use std::sync::{Arc, Weak};
|
||||
use collab::core::collab::{DataSource, MutexCollab};
|
||||
use collab_database::database::DatabaseData;
|
||||
use collab_database::error::DatabaseError;
|
||||
use collab_database::rows::RowId;
|
||||
use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
|
||||
use collab_database::workspace_database::{
|
||||
CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase,
|
||||
@ -16,11 +17,13 @@ use tracing::{event, instrument, trace};
|
||||
|
||||
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
|
||||
use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig};
|
||||
use flowy_database_pub::cloud::DatabaseCloudService;
|
||||
use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent};
|
||||
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
use lib_infra::priority_task::TaskDispatcher;
|
||||
|
||||
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB};
|
||||
use crate::services::cell::stringify_cell;
|
||||
use crate::services::database::DatabaseEditor;
|
||||
use crate::services::database_view::DatabaseLayoutDepsResolver;
|
||||
use crate::services::field_settings::default_field_settings_by_layout_map;
|
||||
@ -156,7 +159,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult<String> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let database_collab = wdb.get_database(database_id).await.ok_or_else(|| {
|
||||
FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id))
|
||||
})?;
|
||||
@ -167,17 +170,17 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn get_all_databases_meta(&self) -> Vec<DatabaseMeta> {
|
||||
let mut items = vec![];
|
||||
if let Ok(wdb) = self.get_workspace_database().await {
|
||||
if let Ok(wdb) = self.get_database_indexer().await {
|
||||
items = wdb.get_all_database_meta()
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
pub async fn track_database(
|
||||
pub async fn update_database_indexing(
|
||||
&self,
|
||||
view_ids_by_database_id: HashMap<String, Vec<String>>,
|
||||
) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
view_ids_by_database_id
|
||||
.into_iter()
|
||||
.for_each(|(database_id, view_ids)| {
|
||||
@ -192,7 +195,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult<String> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
wdb.get_database_id_with_view_id(view_id).ok_or_else(|| {
|
||||
FlowyError::record_not_found()
|
||||
.with_context(format!("The database for view id: {} not found", view_id))
|
||||
@ -210,7 +213,7 @@ impl DatabaseManager {
|
||||
pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
|
||||
trace!("open database editor:{}", database_id);
|
||||
let database = self
|
||||
.get_workspace_database()
|
||||
.get_database_indexer()
|
||||
.await?
|
||||
.get_database(database_id)
|
||||
.await
|
||||
@ -227,7 +230,7 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn open_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) {
|
||||
if let Some(database) = wdb.open_database(&database_id) {
|
||||
if let Some(lock_database) = database.try_lock() {
|
||||
@ -243,7 +246,7 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let database_id = wdb.get_database_id_with_view_id(view_id);
|
||||
if let Some(database_id) = database_id {
|
||||
let mut editors = self.editors.lock().await;
|
||||
@ -270,7 +273,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let data = wdb.get_database_data(view_id).await?;
|
||||
let json_bytes = data.to_json_bytes()?;
|
||||
Ok(json_bytes)
|
||||
@ -297,13 +300,13 @@ impl DatabaseManager {
|
||||
create_view_params.view_id = view_id.to_string();
|
||||
}
|
||||
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let _ = wdb.create_database(create_database_params)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let _ = wdb.create_database(params)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -317,7 +320,7 @@ impl DatabaseManager {
|
||||
database_id: String,
|
||||
database_view_id: String,
|
||||
) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout);
|
||||
if let Some(database) = wdb.get_database(&database_id).await {
|
||||
let (field, layout_setting) = DatabaseLayoutDepsResolver::new(database, layout)
|
||||
@ -397,7 +400,9 @@ impl DatabaseManager {
|
||||
Ok(snapshots)
|
||||
}
|
||||
|
||||
async fn get_workspace_database(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
|
||||
/// Return the database indexer.
|
||||
/// Each workspace has itw own Database indexer that manages all the databases and database views
|
||||
async fn get_database_indexer(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
|
||||
let database = self.workspace_database.read().await;
|
||||
match &*database {
|
||||
None => Err(FlowyError::internal().with_context("Workspace database not initialized")),
|
||||
@ -405,6 +410,45 @@ impl DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn summarize_row(
|
||||
&self,
|
||||
view_id: String,
|
||||
row_id: RowId,
|
||||
field_id: String,
|
||||
) -> FlowyResult<()> {
|
||||
let database = self.get_database_with_view_id(&view_id).await?;
|
||||
|
||||
//
|
||||
let mut summary_row_content = SummaryRowContent::new();
|
||||
if let Some(row) = database.get_row(&view_id, &row_id) {
|
||||
let fields = database.get_fields(&view_id, None);
|
||||
for field in fields {
|
||||
if let Some(cell) = row.cells.get(&field.id) {
|
||||
summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the cloud service to summarize the row.
|
||||
trace!(
|
||||
"[AI]: summarize row:{}, content:{:?}",
|
||||
row_id,
|
||||
summary_row_content
|
||||
);
|
||||
let response = self
|
||||
.cloud_service
|
||||
.summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content)
|
||||
.await?;
|
||||
trace!("[AI]:summarize row response: {}", response);
|
||||
|
||||
// Update the cell with the response from the cloud service.
|
||||
database
|
||||
.update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Only expose this method for testing
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn get_cloud_service(&self) -> &Arc<dyn DatabaseCloudService> {
|
||||
|
@ -259,6 +259,9 @@ impl<'a> CellBuilder<'a> {
|
||||
FieldType::Relation => {
|
||||
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
|
||||
},
|
||||
FieldType::Summary => {
|
||||
cells.insert(field_id, insert_text_cell(cell_str, field));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::services::database_view::{
|
||||
use crate::services::field::{
|
||||
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
|
||||
StrCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
|
||||
StringCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
|
||||
TypeOptionCellExt,
|
||||
};
|
||||
use crate::services::field_settings::{default_field_settings_by_layout_map, FieldSettings};
|
||||
@ -34,7 +34,7 @@ use lib_infra::util::timestamp;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
use tracing::{event, warn};
|
||||
use tracing::{event, instrument, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DatabaseEditor {
|
||||
@ -440,7 +440,7 @@ impl DatabaseEditor {
|
||||
for cell in cells {
|
||||
if let Some(new_cell) = cell.cell.clone() {
|
||||
self
|
||||
.update_cell(view_id, cell.row_id, &new_field_id, new_cell)
|
||||
.update_cell(view_id, &cell.row_id, &new_field_id, new_cell)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@ -755,10 +755,11 @@ impl DatabaseEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn update_cell_with_changeset(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
cell_changeset: BoxAny,
|
||||
) -> FlowyResult<()> {
|
||||
@ -771,7 +772,7 @@ impl DatabaseEditor {
|
||||
Err(FlowyError::internal().with_context(msg))
|
||||
},
|
||||
}?;
|
||||
(field, database.get_cell(field_id, &row_id).cell)
|
||||
(field, database.get_cell(field_id, row_id).cell)
|
||||
};
|
||||
|
||||
let new_cell =
|
||||
@ -800,14 +801,13 @@ impl DatabaseEditor {
|
||||
pub async fn update_cell(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
new_cell: Cell,
|
||||
) -> FlowyResult<()> {
|
||||
// Get the old row before updating the cell. It would be better to get the old cell
|
||||
let old_row = { self.get_row_detail(view_id, &row_id) };
|
||||
|
||||
self.database.lock().update_row(&row_id, |row_update| {
|
||||
let old_row = { self.get_row_detail(view_id, row_id) };
|
||||
self.database.lock().update_row(row_id, |row_update| {
|
||||
row_update.update_cells(|cell_update| {
|
||||
cell_update.insert(field_id, new_cell);
|
||||
});
|
||||
@ -831,7 +831,7 @@ impl DatabaseEditor {
|
||||
});
|
||||
|
||||
self
|
||||
.did_update_row(view_id, row_id, field_id, old_row)
|
||||
.did_update_row(view_id, &row_id, field_id, old_row)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
@ -840,11 +840,11 @@ impl DatabaseEditor {
|
||||
async fn did_update_row(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
old_row: Option<RowDetail>,
|
||||
) {
|
||||
let option_row = self.get_row_detail(view_id, &row_id);
|
||||
let option_row = self.get_row_detail(view_id, row_id);
|
||||
if let Some(new_row_detail) = option_row {
|
||||
for view in self.database_views.editors().await {
|
||||
view
|
||||
@ -931,7 +931,7 @@ impl DatabaseEditor {
|
||||
|
||||
// Insert the options into the cell
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -970,7 +970,7 @@ impl DatabaseEditor {
|
||||
.await?;
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -994,7 +994,7 @@ impl DatabaseEditor {
|
||||
debug_assert!(FieldType::from(field.field_type).is_checklist());
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -1294,7 +1294,7 @@ impl DatabaseEditor {
|
||||
.cell
|
||||
.and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field))
|
||||
.and_then(|cell_data| cell_data.unbox_or_none())
|
||||
.unwrap_or_else(|| StrCellData("".to_string()));
|
||||
.unwrap_or_else(|| StringCellData("".to_string()));
|
||||
|
||||
RelatedRowDataPB {
|
||||
row_id: row.id.to_string(),
|
||||
|
@ -4,6 +4,7 @@ pub mod date_type_option;
|
||||
pub mod number_type_option;
|
||||
pub mod relation_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod summary_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod timestamp_type_option;
|
||||
mod type_option;
|
||||
|
@ -0,0 +1,2 @@
|
||||
pub mod summary;
|
||||
pub mod summary_entities;
|
@ -0,0 +1,109 @@
|
||||
use crate::entities::TextFilterPB;
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
use crate::services::field::summary_type_option::summary_entities::SummaryCellData;
|
||||
use crate::services::field::type_options::util::ProtobufStr;
|
||||
use crate::services::field::{
|
||||
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||
TypeOptionCellDataSerde, TypeOptionTransform,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::FlowyResult;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SummarizationTypeOption {
|
||||
pub auto_fill: bool,
|
||||
}
|
||||
|
||||
impl From<TypeOptionData> for SummarizationTypeOption {
|
||||
fn from(value: TypeOptionData) -> Self {
|
||||
let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default();
|
||||
Self { auto_fill }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOption> for TypeOptionData {
|
||||
fn from(value: SummarizationTypeOption) -> Self {
|
||||
TypeOptionDataBuilder::new()
|
||||
.insert_bool_value("auto_fill", value.auto_fill)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOption for SummarizationTypeOption {
|
||||
type CellData = SummaryCellData;
|
||||
type CellChangeset = String;
|
||||
type CellProtobufType = ProtobufStr;
|
||||
type CellFilter = TextFilterPB;
|
||||
}
|
||||
|
||||
impl CellDataChangeset for SummarizationTypeOption {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: String,
|
||||
_cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, SummaryCellData)> {
|
||||
let cell_data = SummaryCellData(changeset);
|
||||
Ok((cell_data.clone().into(), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for SummarizationTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
filter: &<Self as TypeOption>::CellFilter,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> bool {
|
||||
filter.is_visible(cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for SummarizationTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::CellData,
|
||||
sort_condition: SortCondition,
|
||||
) -> Ordering {
|
||||
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
|
||||
(true, true) => Ordering::Equal,
|
||||
(true, false) => Ordering::Greater,
|
||||
(false, true) => Ordering::Less,
|
||||
(false, false) => {
|
||||
let order = cell_data.0.cmp(&other_cell_data.0);
|
||||
sort_condition.evaluate_order(order)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for SummarizationTypeOption {
|
||||
fn decode_cell(&self, cell: &Cell) -> FlowyResult<SummaryCellData> {
|
||||
Ok(SummaryCellData::from(cell))
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: SummaryCellData) -> String {
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl TypeOptionTransform for SummarizationTypeOption {}
|
||||
|
||||
impl TypeOptionCellDataSerde for SummarizationTypeOption {
|
||||
fn protobuf_encode(
|
||||
&self,
|
||||
cell_data: <Self as TypeOption>::CellData,
|
||||
) -> <Self as TypeOption>::CellProtobufType {
|
||||
ProtobufStr::from(cell_data.0)
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(SummaryCellData::from(cell))
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::rows::{new_cell_builder, Cell};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SummaryCellData(pub String);
|
||||
impl std::ops::Deref for SummaryCellData {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for SummaryCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Cell> for SummaryCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummaryCellData> for Cell {
|
||||
fn from(data: SummaryCellData) -> Self {
|
||||
new_cell_builder(FieldType::Summary)
|
||||
.insert_str_value(CELL_DATA, data.0)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for SummaryCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SummaryCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ pub struct RichTextTypeOption {
|
||||
}
|
||||
|
||||
impl TypeOption for RichTextTypeOption {
|
||||
type CellData = StrCellData;
|
||||
type CellData = StringCellData;
|
||||
type CellChangeset = String;
|
||||
type CellProtobufType = ProtobufStr;
|
||||
type CellFilter = TextFilterPB;
|
||||
@ -57,13 +57,13 @@ impl TypeOptionCellDataSerde for RichTextTypeOption {
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(StrCellData::from(cell))
|
||||
Ok(StringCellData::from(cell))
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for RichTextTypeOption {
|
||||
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(StrCellData::from(cell))
|
||||
Ok(StringCellData::from(cell))
|
||||
}
|
||||
|
||||
fn decode_cell_with_transform(
|
||||
@ -79,11 +79,12 @@ impl CellDataDecoder for RichTextTypeOption {
|
||||
| FieldType::SingleSelect
|
||||
| FieldType::MultiSelect
|
||||
| FieldType::Checkbox
|
||||
| FieldType::URL => Some(StrCellData::from(stringify_cell(cell, field))),
|
||||
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
FieldType::Checklist
|
||||
| FieldType::LastEditedTime
|
||||
| FieldType::CreatedTime
|
||||
| FieldType::Relation => None,
|
||||
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +93,7 @@ impl CellDataDecoder for RichTextTypeOption {
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||
StrCellData::from(cell).0.parse::<f64>().ok()
|
||||
StringCellData::from(cell).0.parse::<f64>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +109,7 @@ impl CellDataChangeset for RichTextTypeOption {
|
||||
.with_context("The len of the text should not be more than 10000"),
|
||||
)
|
||||
} else {
|
||||
let text_cell_data = StrCellData(changeset);
|
||||
let text_cell_data = StringCellData(changeset);
|
||||
Ok((text_cell_data.clone().into(), text_cell_data))
|
||||
}
|
||||
}
|
||||
@ -144,8 +145,8 @@ impl TypeOptionCellDataCompare for RichTextTypeOption {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct StrCellData(pub String);
|
||||
impl std::ops::Deref for StrCellData {
|
||||
pub struct StringCellData(pub String);
|
||||
impl std::ops::Deref for StringCellData {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -153,57 +154,57 @@ impl std::ops::Deref for StrCellData {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for StrCellData {
|
||||
impl TypeOptionCellData for StringCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Cell> for StrCellData {
|
||||
impl From<&Cell> for StringCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StrCellData> for Cell {
|
||||
fn from(data: StrCellData) -> Self {
|
||||
impl From<StringCellData> for Cell {
|
||||
fn from(data: StringCellData) -> Self {
|
||||
new_cell_builder(FieldType::RichText)
|
||||
.insert_str_value(CELL_DATA, data.0)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for StrCellData {
|
||||
impl std::ops::DerefMut for StringCellData {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<String> for StrCellData {
|
||||
impl std::convert::From<String> for StringCellData {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for StrCellData {
|
||||
impl ToString for StringCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<StrCellData> for String {
|
||||
fn from(value: StrCellData) -> Self {
|
||||
impl std::convert::From<StringCellData> for String {
|
||||
fn from(value: StringCellData) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&str> for StrCellData {
|
||||
impl std::convert::From<&str> for StringCellData {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for StrCellData {
|
||||
impl AsRef<str> for StringCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ use flowy_error::FlowyResult;
|
||||
use crate::entities::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
};
|
||||
use crate::services::cell::CellDataDecoder;
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
@ -181,6 +182,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
||||
FieldType::Relation => {
|
||||
RelationTypeOptionPB::try_from(bytes).map(|pb| RelationTypeOption::from(pb).into())
|
||||
},
|
||||
FieldType::Summary => {
|
||||
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +246,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let summarization_type_option: SummarizationTypeOption = type_option.into();
|
||||
SummarizationTypeOptionPB::from(summarization_type_option)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,5 +271,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
||||
FieldType::URL => URLTypeOption::default().into(),
|
||||
FieldType::Checklist => ChecklistTypeOption.into(),
|
||||
FieldType::Relation => RelationTypeOption::default().into(),
|
||||
FieldType::Summary => SummarizationTypeOption::default().into(),
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use lib_infra::box_any::BoxAny;
|
||||
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
|
||||
@ -166,23 +167,24 @@ where
|
||||
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
|
||||
let field_type = FieldType::from(field.field_type);
|
||||
let key = CellDataCacheKey::new(field, field_type, cell);
|
||||
// tracing::trace!(
|
||||
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
|
||||
// field_type,
|
||||
// cell,
|
||||
// cell_data
|
||||
// );
|
||||
tracing::trace!(
|
||||
"Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
|
||||
field_type,
|
||||
cell,
|
||||
cell_data
|
||||
);
|
||||
cell_data_cache.write().insert(key.as_ref(), cell_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cell_data(&self, cell: &Cell, field: &Field) -> Option<T::CellData> {
|
||||
let field_type_of_cell = get_field_type_from_cell(cell)?;
|
||||
|
||||
if let Some(cell_data) = self.get_cell_data_from_cache(cell, field) {
|
||||
return Some(cell_data);
|
||||
}
|
||||
|
||||
// If the field type of the cell is the same as the field type of the handler, we can directly decode the cell.
|
||||
// Otherwise, we need to transform the cell to the field type of the handler.
|
||||
let cell_data = if field_type_of_cell == self.field_type {
|
||||
Some(self.decode_cell(cell).unwrap_or_default())
|
||||
} else if is_type_option_cell_transformable(field_type_of_cell, self.field_type) {
|
||||
@ -437,6 +439,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Summary => self
|
||||
.field
|
||||
.get_type_option::<SummarizationTypeOption>(field_type)
|
||||
.map(|type_option| {
|
||||
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||
type_option,
|
||||
field_type,
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -538,6 +550,8 @@ fn get_type_option_transform_handler(
|
||||
FieldType::Relation => {
|
||||
Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
|
||||
as Box<dyn TypeOptionTransformHandler>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ impl FieldSettings {
|
||||
.unwrap_or(DEFAULT_WIDTH);
|
||||
let wrap_cell_content = field_settings
|
||||
.get_bool_value(WRAP_CELL_CONTENT)
|
||||
.unwrap_or(false);
|
||||
.unwrap_or(true);
|
||||
|
||||
Self {
|
||||
field_id: field_id.to_string(),
|
||||
|
@ -20,7 +20,7 @@ impl FieldSettingsBuilder {
|
||||
field_id: field_id.to_string(),
|
||||
visibility: FieldVisibility::AlwaysShown,
|
||||
width: DEFAULT_WIDTH,
|
||||
wrap_cell_content: false,
|
||||
wrap_cell_content: true,
|
||||
};
|
||||
|
||||
Self {
|
||||
|
@ -280,6 +280,7 @@ impl FilterInner {
|
||||
FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||
};
|
||||
|
||||
FilterInner::Data {
|
||||
@ -362,6 +363,10 @@ impl<'a> From<&'a Filter> for FilterMap {
|
||||
let filter = condition_and_content.cloned::<RelationFilterPB>()?;
|
||||
(filter.condition as u8, "".to_string())
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
};
|
||||
Some((condition, content))
|
||||
};
|
||||
|
@ -46,7 +46,7 @@ impl DatabaseCellTest {
|
||||
} => {
|
||||
self
|
||||
.editor
|
||||
.update_cell_with_changeset(&view_id, row_id, &field_id, changeset)
|
||||
.update_cell_with_changeset(&view_id, &row_id, &field_id, changeset)
|
||||
.await
|
||||
.unwrap();
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
|
||||
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData,
|
||||
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData,
|
||||
URLCellData,
|
||||
};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
@ -84,7 +84,7 @@ async fn text_cell_data_test() {
|
||||
.await;
|
||||
|
||||
for (i, row_cell) in cells.into_iter().enumerate() {
|
||||
let text = StrCellData::from(row_cell.cell.as_ref().unwrap());
|
||||
let text = StringCellData::from(row_cell.cell.as_ref().unwrap());
|
||||
match i {
|
||||
0 => assert_eq!(text.as_str(), "A"),
|
||||
1 => assert_eq!(text.as_str(), ""),
|
||||
|
@ -1,16 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use collab_database::database::{gen_database_view_id, timestamp};
|
||||
use collab_database::database::gen_database_view_id;
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::{Row, RowDetail, RowId};
|
||||
use collab_database::rows::{RowDetail, RowId};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
use strum::EnumCount;
|
||||
|
||||
use event_integration_test::folder_event::ViewTest;
|
||||
use event_integration_test::EventIntegrationTest;
|
||||
use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB};
|
||||
use flowy_database2::services::cell::CellBuilder;
|
||||
|
||||
use flowy_database2::services::database::DatabaseEditor;
|
||||
use flowy_database2::services::field::checklist_type_option::{
|
||||
ChecklistCellChangeset, ChecklistTypeOption,
|
||||
@ -196,7 +196,7 @@ impl DatabaseEditorTest {
|
||||
|
||||
self
|
||||
.editor
|
||||
.update_cell_with_changeset(&self.view_id, row_id, &field.id, cell_changeset)
|
||||
.update_cell_with_changeset(&self.view_id, &row_id, &field.id, cell_changeset)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -282,139 +282,3 @@ impl DatabaseEditorTest {
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestRowBuilder<'a> {
|
||||
database_id: &'a str,
|
||||
row_id: RowId,
|
||||
fields: &'a [Field],
|
||||
cell_build: CellBuilder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TestRowBuilder<'a> {
|
||||
pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {
|
||||
let cell_build = CellBuilder::with_cells(Default::default(), fields);
|
||||
Self {
|
||||
database_id,
|
||||
row_id,
|
||||
fields,
|
||||
cell_build,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_text_cell(&mut self, data: &str) -> String {
|
||||
let text_field = self.field_with_type(&FieldType::RichText);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&text_field.id, data.to_string());
|
||||
|
||||
text_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_number_cell(&mut self, data: &str) -> String {
|
||||
let number_field = self.field_with_type(&FieldType::Number);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&number_field.id, data.to_string());
|
||||
number_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_date_cell(
|
||||
&mut self,
|
||||
date: i64,
|
||||
time: Option<String>,
|
||||
include_time: Option<bool>,
|
||||
field_type: &FieldType,
|
||||
) -> String {
|
||||
let date_field = self.field_with_type(field_type);
|
||||
self
|
||||
.cell_build
|
||||
.insert_date_cell(&date_field.id, date, time, include_time);
|
||||
date_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
|
||||
let checkbox_field = self.field_with_type(&FieldType::Checkbox);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&checkbox_field.id, data.to_string());
|
||||
|
||||
checkbox_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_url_cell(&mut self, content: &str) -> String {
|
||||
let url_field = self.field_with_type(&FieldType::URL);
|
||||
self
|
||||
.cell_build
|
||||
.insert_url_cell(&url_field.id, content.to_string());
|
||||
url_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> SelectOption,
|
||||
{
|
||||
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
|
||||
let type_option = single_select_field
|
||||
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
|
||||
.unwrap();
|
||||
let option = f(type_option.options);
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
|
||||
|
||||
single_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
|
||||
{
|
||||
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
|
||||
let type_option = multi_select_field
|
||||
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
|
||||
.unwrap();
|
||||
let options = f(type_option.options);
|
||||
let ops_ids = options
|
||||
.iter()
|
||||
.map(|option| option.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&multi_select_field.id, ops_ids);
|
||||
|
||||
multi_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String {
|
||||
let checklist_field = self.field_with_type(&FieldType::Checklist);
|
||||
self
|
||||
.cell_build
|
||||
.insert_checklist_cell(&checklist_field.id, options);
|
||||
checklist_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
||||
self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| {
|
||||
let t_field_type = FieldType::from(field.field_type);
|
||||
&t_field_type == field_type
|
||||
})
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn build(self) -> Row {
|
||||
let timestamp = timestamp();
|
||||
Row {
|
||||
id: self.row_id,
|
||||
database_id: self.database_id.to_string(),
|
||||
cells: self.cell_build.build(),
|
||||
height: 60,
|
||||
visibility: true,
|
||||
modified_at: timestamp,
|
||||
created_at: timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ impl DatabaseGroupTest {
|
||||
let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id);
|
||||
self
|
||||
.editor
|
||||
.update_cell(&self.view_id, row_id, &field_id, cell)
|
||||
.update_cell(&self.view_id, &row_id, &field_id, cell)
|
||||
.await
|
||||
.unwrap();
|
||||
},
|
||||
@ -218,7 +218,7 @@ impl DatabaseGroupTest {
|
||||
let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id);
|
||||
self
|
||||
.editor
|
||||
.update_cell(&self.view_id, row_id, &field_id, cell)
|
||||
.update_cell(&self.view_id, &row_id, &field_id, cell)
|
||||
.await
|
||||
.unwrap();
|
||||
},
|
||||
|
@ -2,8 +2,11 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_i
|
||||
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
|
||||
use event_integration_test::database_event::TestRowBuilder;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, RelationTypeOption,
|
||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
@ -11,9 +14,6 @@ use flowy_database2::services::field::{
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
use flowy_database2::services::setting::BoardLayoutSetting;
|
||||
|
||||
use crate::database::database_editor::TestRowBuilder;
|
||||
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
|
||||
|
||||
// Kanban board unit test mock data
|
||||
pub fn make_test_board() -> DatabaseData {
|
||||
let database_id = gen_database_id();
|
||||
@ -127,6 +127,13 @@ pub fn make_test_board() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let type_option = SummarizationTypeOption { auto_fill: false };
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("AI summary")
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,11 @@ use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, Layout
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use event_integration_test::database_event::TestRowBuilder;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::{FieldBuilder, MultiSelectTypeOption};
|
||||
use flowy_database2::services::setting::CalendarLayoutSetting;
|
||||
|
||||
use crate::database::database_editor::TestRowBuilder;
|
||||
|
||||
// Calendar unit test mock data
|
||||
pub fn make_test_calendar() -> DatabaseData {
|
||||
let database_id = gen_database_id();
|
||||
|
@ -2,7 +2,10 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_i
|
||||
use collab_database::views::{DatabaseLayout, DatabaseView};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
|
||||
use event_integration_test::database_event::TestRowBuilder;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
|
||||
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
|
||||
@ -10,9 +13,6 @@ use flowy_database2::services::field::{
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
|
||||
use crate::database::database_editor::TestRowBuilder;
|
||||
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
|
||||
|
||||
pub fn make_test_grid() -> DatabaseData {
|
||||
let database_id = gen_database_id();
|
||||
let mut fields = vec![];
|
||||
@ -125,6 +125,13 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let type_option = SummarizationTypeOption { auto_fill: false };
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("AI summary")
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
use chrono::{DateTime, Local, Offset};
|
||||
use collab_database::database::timestamp;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::cell::stringify_cell;
|
||||
use flowy_database2::services::field::CHECK;
|
||||
@ -24,45 +22,6 @@ async fn export_meta_csv_test() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_csv_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
let database = test.editor.clone();
|
||||
let s = database.export_csv(CSVFormat::Original).await.unwrap();
|
||||
let format = "%Y/%m/%d %R";
|
||||
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp(), 0).unwrap();
|
||||
let offset = Local::now().offset().fix();
|
||||
let date_time = DateTime::<Local>::from_naive_utc_and_offset(naive, offset);
|
||||
let date_string = format!("{}", date_time.format(format));
|
||||
let expected = format!(
|
||||
r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At,Related
|
||||
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,First thing,{},{},
|
||||
,$2,2022/03/14,,"Google,Twitter",Yes,,"Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed",{},{},
|
||||
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,{},{},
|
||||
DA,$14,2022/11/17,Completed,,No,,Task 1,{},{},
|
||||
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,{},{},
|
||||
AE,$5,2022/12/25,Planned,Facebook,Yes,,"Sprint,Sprint some more,Rest",{},{},
|
||||
CB,,,,,,,,{},{},
|
||||
"#,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
);
|
||||
println!("{}", s);
|
||||
assert_eq!(s, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_and_then_import_meta_csv_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
@ -123,6 +82,7 @@ async fn export_and_then_import_meta_csv_test() {
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
@ -205,6 +165,7 @@ async fn history_database_import_test() {
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
|
Reference in New Issue
Block a user