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:
Nathan.fooo
2024-05-05 22:04:34 +08:00
committed by GitHub
parent 999ffeba21
commit a69e83c2cb
83 changed files with 1802 additions and 628 deletions

View File

@ -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()
}

View File

@ -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 {

View File

@ -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

View File

@ -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,
}

View File

@ -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::*;

View File

@ -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,
}
}
}

View File

@ -465,7 +465,7 @@ pub(crate) async fn update_cell_handler(
database_editor
.update_cell_with_changeset(
&params.view_id,
RowId::from(params.row_id),
&RowId::from(params.row_id),
&params.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(
&params.cell_identifier.view_id,
params.cell_identifier.row_id,
&params.cell_identifier.row_id,
&params.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(
&params.view_id,
params.row_id,
&params.row_id,
&params.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(())
}

View File

@ -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,
}

View File

@ -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> {

View File

@ -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));
},
}
}
}

View File

@ -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(),

View File

@ -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;

View File

@ -0,0 +1,2 @@
pub mod summary;
pub mod summary_entities;

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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(),
}
}

View File

@ -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>,
}
}

View File

@ -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(),

View File

@ -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 {

View File

@ -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))
};

View File

@ -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();
},

View File

@ -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(), ""),

View File

@ -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,
}
}
}

View File

@ -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();
},

View File

@ -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);
},
}
}

View File

@ -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();

View File

@ -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);
},
}
}

View File

@ -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!(