mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add basic relation field (#4397)
* feat: add basic relation field
* fix: clippy
* fix: tauri build 🤞
* chore: merge changes
* fix: merge main
* chore: initial code review pass
* fix: rust-lib test
* chore: code cleanup
* fix: unwrap or default
This commit is contained in:
@ -69,6 +69,12 @@ impl AsRef<str> for DatabaseIdPB {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct RepeatedDatabaseIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub value: Vec<DatabaseIdPB>,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug, Validate)]
|
||||
pub struct DatabaseViewIdPB {
|
||||
#[pb(index = 1)]
|
||||
|
@ -473,6 +473,7 @@ pub enum FieldType {
|
||||
Checklist = 7,
|
||||
LastEditedTime = 8,
|
||||
CreatedTime = 9,
|
||||
Relation = 10,
|
||||
}
|
||||
|
||||
impl Display for FieldType {
|
||||
@ -509,8 +510,9 @@ impl FieldType {
|
||||
FieldType::Checkbox => "Checkbox",
|
||||
FieldType::URL => "URL",
|
||||
FieldType::Checklist => "Checklist",
|
||||
FieldType::LastEditedTime => "Last edited time",
|
||||
FieldType::LastEditedTime => "Last modified",
|
||||
FieldType::CreatedTime => "Created time",
|
||||
FieldType::Relation => "Relation",
|
||||
};
|
||||
s.to_string()
|
||||
}
|
||||
@ -559,6 +561,10 @@ impl FieldType {
|
||||
matches!(self, FieldType::Checklist)
|
||||
}
|
||||
|
||||
pub fn is_relation(&self) -> bool {
|
||||
matches!(self, FieldType::Relation)
|
||||
}
|
||||
|
||||
pub fn can_be_group(&self) -> bool {
|
||||
self.is_select_option() || self.is_checkbox() || self.is_url()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ mod checklist_filter;
|
||||
mod date_filter;
|
||||
mod filter_changeset;
|
||||
mod number_filter;
|
||||
mod relation_filter;
|
||||
mod select_option_filter;
|
||||
mod text_filter;
|
||||
mod util;
|
||||
@ -12,6 +13,7 @@ pub use checklist_filter::*;
|
||||
pub use date_filter::*;
|
||||
pub use filter_changeset::*;
|
||||
pub use number_filter::*;
|
||||
pub use relation_filter::*;
|
||||
pub use select_option_filter::*;
|
||||
pub use text_filter::*;
|
||||
pub use util::*;
|
||||
|
@ -0,0 +1,24 @@
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
use crate::services::filter::{Filter, FromFilterString};
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct RelationFilterPB {
|
||||
#[pb(index = 1)]
|
||||
pub condition: i64,
|
||||
}
|
||||
|
||||
impl FromFilterString for RelationFilterPB {
|
||||
fn from_filter(_filter: &Filter) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
RelationFilterPB { condition: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Filter> for RelationFilterPB {
|
||||
fn from(_filter: &Filter) -> Self {
|
||||
RelationFilterPB { condition: 0 }
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ use validator::Validate;
|
||||
use crate::entities::parser::NotEmptyStr;
|
||||
use crate::entities::{
|
||||
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType,
|
||||
NumberFilterPB, SelectOptionFilterPB, TextFilterPB,
|
||||
NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
|
||||
};
|
||||
use crate::services::field::SelectOptionIds;
|
||||
use crate::services::filter::Filter;
|
||||
@ -44,6 +44,7 @@ impl std::convert::From<&Filter> for FilterPB {
|
||||
FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::Relation => RelationFilterPB::from(filter).try_into().unwrap(),
|
||||
};
|
||||
Self {
|
||||
id: filter.id.clone(),
|
||||
@ -186,6 +187,10 @@ impl TryInto<UpdateFilterParams> for UpdateFilterPayloadPB {
|
||||
condition = filter.condition as u8;
|
||||
content = SelectOptionIds::from(filter.option_ids).to_string();
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let filter = RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
|
||||
condition = filter.condition as u8;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(UpdateFilterParams {
|
||||
|
@ -14,8 +14,9 @@ macro_rules! impl_into_field_type {
|
||||
7 => FieldType::Checklist,
|
||||
8 => FieldType::LastEditedTime,
|
||||
9 => FieldType::CreatedTime,
|
||||
10 => FieldType::Relation,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parser FieldType from value: {}", ty);
|
||||
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
||||
FieldType::RichText
|
||||
},
|
||||
}
|
||||
@ -34,7 +35,7 @@ macro_rules! impl_into_field_visibility {
|
||||
1 => FieldVisibility::HideWhenEmpty,
|
||||
2 => FieldVisibility::AlwaysHidden,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parser FieldVisibility from value: {}", ty);
|
||||
tracing::error!("🔴Can't parse FieldVisibility from value: {}", ty);
|
||||
FieldVisibility::AlwaysShown
|
||||
},
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ mod checkbox_entities;
|
||||
mod checklist_entities;
|
||||
mod date_entities;
|
||||
mod number_entities;
|
||||
mod relation_entities;
|
||||
mod select_option_entities;
|
||||
mod text_entities;
|
||||
mod timestamp_entities;
|
||||
@ -11,6 +12,7 @@ pub use checkbox_entities::*;
|
||||
pub use checklist_entities::*;
|
||||
pub use date_entities::*;
|
||||
pub use number_entities::*;
|
||||
pub use relation_entities::*;
|
||||
pub use select_option_entities::*;
|
||||
pub use text_entities::*;
|
||||
pub use timestamp_entities::*;
|
||||
|
@ -0,0 +1,87 @@
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
use crate::entities::CellIdPB;
|
||||
use crate::services::field::{RelationCellData, RelationTypeOption};
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelationCellDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub row_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<RelationCellData> for RelationCellDataPB {
|
||||
fn from(data: RelationCellData) -> Self {
|
||||
Self {
|
||||
row_ids: data.row_ids.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationCellDataPB> for RelationCellData {
|
||||
fn from(data: RelationCellDataPB) -> Self {
|
||||
Self {
|
||||
row_ids: data.row_ids.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelationCellChangesetPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub cell_id: CellIdPB,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub inserted_row_ids: Vec<String>,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub removed_row_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct RelationTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub database_id: String,
|
||||
}
|
||||
|
||||
impl From<RelationTypeOption> for RelationTypeOptionPB {
|
||||
fn from(value: RelationTypeOption) -> Self {
|
||||
RelationTypeOptionPB {
|
||||
database_id: value.database_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationTypeOptionPB> for RelationTypeOption {
|
||||
fn from(value: RelationTypeOptionPB) -> Self {
|
||||
RelationTypeOption {
|
||||
database_id: value.database_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelatedRowDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub row_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RepeatedRelatedRowDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub rows: Vec<RelatedRowDataPB>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||
pub struct RepeatedRowIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub database_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub row_ids: Vec<String>,
|
||||
}
|
@ -13,7 +13,8 @@ use crate::entities::*;
|
||||
use crate::manager::DatabaseManager;
|
||||
use crate::services::cell::CellBuilder;
|
||||
use crate::services::field::{
|
||||
type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, SelectOptionCellChangeset,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, RelationCellChangeset,
|
||||
SelectOptionCellChangeset,
|
||||
};
|
||||
use crate::services::field_settings::FieldSettingsChangesetParams;
|
||||
use crate::services::group::GroupChangeset;
|
||||
@ -978,3 +979,81 @@ pub(crate) async fn remove_calculation_handler(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_database_ids_handler(
|
||||
_data: AFPluginData<DatabaseViewIdPB>,
|
||||
_manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub(crate) async fn update_relation_cell_handler(
|
||||
data: AFPluginData<RelationCellChangesetPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RelationCellChangesetPB = data.into_inner();
|
||||
let view_id = parser::NotEmptyStr::parse(params.view_id)
|
||||
.map_err(|_| flowy_error::ErrorCode::DatabaseViewIdIsEmpty)?
|
||||
.0;
|
||||
let cell_id: CellIdParams = params.cell_id.try_into()?;
|
||||
let params = RelationCellChangeset {
|
||||
inserted_row_ids: params
|
||||
.inserted_row_ids
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
removed_row_ids: params.removed_row_ids.into_iter().map(Into::into).collect(),
|
||||
};
|
||||
|
||||
let database_editor = manager.get_database_with_view_id(&view_id).await?;
|
||||
|
||||
// // get the related database
|
||||
// let related_database_id = database_editor
|
||||
// .get_related_database_id(&cell_id.field_id)
|
||||
// .await?;
|
||||
// let related_database_editor = manager.get_database(&related_database_id).await?;
|
||||
|
||||
// // validate the changeset contents
|
||||
// related_database_editor
|
||||
// .validate_row_ids_exist(¶ms)
|
||||
// .await?;
|
||||
|
||||
// update the cell in the database
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
&view_id,
|
||||
cell_id.row_id,
|
||||
&cell_id.field_id,
|
||||
BoxAny::new(params),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_row_datas_handler(
|
||||
data: AFPluginData<RepeatedRowIdPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RepeatedRowIdPB = data.into_inner();
|
||||
let database_editor = manager.get_database(¶ms.database_id).await?;
|
||||
let row_datas = database_editor
|
||||
.get_related_rows(Some(¶ms.row_ids))
|
||||
.await?;
|
||||
|
||||
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_database_rows_handler(
|
||||
data: AFPluginData<DatabaseIdPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let database_id = data.into_inner().value;
|
||||
let database_editor = manager.get_database(&database_id).await?;
|
||||
let row_datas = database_editor.get_related_rows(None).await?;
|
||||
|
||||
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
|
||||
}
|
||||
|
@ -83,6 +83,11 @@ 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)
|
||||
}
|
||||
|
||||
/// [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)
|
||||
@ -342,4 +347,22 @@ pub enum DatabaseEvent {
|
||||
|
||||
#[event(input = "RemoveCalculationChangesetPB")]
|
||||
RemoveCalculation = 165,
|
||||
|
||||
/// Currently unused. Get a list of database ids that this database relates
|
||||
/// to.
|
||||
#[event(input = "DatabaseViewIdPB", output = "RepeatedDatabaseIdPB")]
|
||||
GetRelatedDatabaseIds = 170,
|
||||
|
||||
/// Updates a relation cell, adding or removing links to rows in another
|
||||
/// database
|
||||
#[event(input = "RelationCellChangesetPB")]
|
||||
UpdateRelationCell = 171,
|
||||
|
||||
/// Get the names of the linked rows in a relation cell.
|
||||
#[event(input = "RepeatedRowIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedRowDatas = 172,
|
||||
|
||||
/// Get the names of all the rows in a related database.
|
||||
#[event(input = "DatabaseIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedDatabaseRows = 173,
|
||||
}
|
||||
|
@ -307,6 +307,9 @@ impl<'a> CellBuilder<'a> {
|
||||
cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field));
|
||||
}
|
||||
},
|
||||
FieldType::Relation => {
|
||||
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ 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, SelectOptionCellChangeset, SelectOptionIds,
|
||||
TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
|
||||
SelectOptionIds, StrCellData, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
|
||||
};
|
||||
use crate::services::field_settings::{
|
||||
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
|
||||
@ -1238,6 +1238,61 @@ impl DatabaseEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_related_database_id(&self, field_id: &str) -> FlowyResult<String> {
|
||||
let mut field = self
|
||||
.database
|
||||
.lock()
|
||||
.get_fields(Some(vec![field_id.to_string()]));
|
||||
let field = field.pop().ok_or(FlowyError::internal())?;
|
||||
|
||||
let type_option = field
|
||||
.get_type_option::<RelationTypeOption>(FieldType::Relation)
|
||||
.ok_or(FlowyError::record_not_found())?;
|
||||
|
||||
Ok(type_option.database_id)
|
||||
}
|
||||
|
||||
pub async fn get_related_rows(
|
||||
&self,
|
||||
row_ids: Option<&Vec<String>>,
|
||||
) -> FlowyResult<Vec<RelatedRowDataPB>> {
|
||||
let primary_field = self.database.lock().fields.get_primary_field().unwrap();
|
||||
let handler =
|
||||
TypeOptionCellExt::new_with_cell_data_cache(&primary_field, Some(self.cell_cache.clone()))
|
||||
.get_type_option_cell_data_handler(&FieldType::RichText)
|
||||
.ok_or(FlowyError::internal())?;
|
||||
|
||||
let row_data = {
|
||||
let database = self.database.lock();
|
||||
let mut rows = database.get_database_rows();
|
||||
if let Some(row_ids) = row_ids {
|
||||
rows.retain(|row| row_ids.contains(&row.id));
|
||||
}
|
||||
rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let title = database
|
||||
.get_cell(&primary_field.id, &row.id)
|
||||
.cell
|
||||
.and_then(|cell| {
|
||||
handler
|
||||
.get_cell_data(&cell, &FieldType::RichText, &primary_field)
|
||||
.ok()
|
||||
})
|
||||
.and_then(|cell_data| cell_data.unbox_or_none())
|
||||
.unwrap_or_else(|| StrCellData("".to_string()));
|
||||
|
||||
RelatedRowDataPB {
|
||||
row_id: row.id.to_string(),
|
||||
name: title.0,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok(row_data)
|
||||
}
|
||||
|
||||
fn get_auto_updated_fields(&self, view_id: &str) -> Vec<Field> {
|
||||
self
|
||||
.database
|
||||
|
@ -1,6 +1,6 @@
|
||||
mod field_builder;
|
||||
mod field_operation;
|
||||
mod type_options;
|
||||
pub mod type_options;
|
||||
|
||||
pub use field_builder::*;
|
||||
pub use field_operation::*;
|
||||
|
@ -2,6 +2,7 @@ pub mod checkbox_type_option;
|
||||
pub mod checklist_type_option;
|
||||
pub mod date_type_option;
|
||||
pub mod number_type_option;
|
||||
pub mod relation_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod timestamp_type_option;
|
||||
@ -14,6 +15,7 @@ pub use checkbox_type_option::*;
|
||||
pub use checklist_type_option::*;
|
||||
pub use date_type_option::*;
|
||||
pub use number_type_option::*;
|
||||
pub use relation_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use timestamp_type_option::*;
|
||||
|
@ -0,0 +1,5 @@
|
||||
mod relation;
|
||||
mod relation_entities;
|
||||
|
||||
pub use relation::*;
|
||||
pub use relation_entities::*;
|
@ -0,0 +1,159 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::FlowyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{FieldType, RelationCellDataPB, RelationFilterPB};
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
use crate::services::field::{
|
||||
default_order, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||
TypeOptionCellDataSerde, TypeOptionTransform,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
|
||||
use super::{RelationCellChangeset, RelationCellData};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RelationTypeOption {
|
||||
pub database_id: String,
|
||||
}
|
||||
|
||||
impl From<TypeOptionData> for RelationTypeOption {
|
||||
fn from(value: TypeOptionData) -> Self {
|
||||
let database_id = value.get_str_value("database_id").unwrap_or_default();
|
||||
Self { database_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationTypeOption> for TypeOptionData {
|
||||
fn from(value: RelationTypeOption) -> Self {
|
||||
TypeOptionDataBuilder::new()
|
||||
.insert_str_value("database_id", value.database_id)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOption for RelationTypeOption {
|
||||
type CellData = RelationCellData;
|
||||
type CellChangeset = RelationCellChangeset;
|
||||
type CellProtobufType = RelationCellDataPB;
|
||||
type CellFilter = RelationFilterPB;
|
||||
}
|
||||
|
||||
impl CellDataChangeset for RelationTypeOption {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: RelationCellChangeset,
|
||||
cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, RelationCellData)> {
|
||||
if cell.is_none() {
|
||||
let cell_data = RelationCellData {
|
||||
row_ids: changeset.inserted_row_ids,
|
||||
};
|
||||
|
||||
return Ok(((&cell_data).into(), cell_data));
|
||||
}
|
||||
|
||||
let cell_data: RelationCellData = cell.unwrap().as_ref().into();
|
||||
let mut row_ids = cell_data.row_ids.clone();
|
||||
for inserted in changeset.inserted_row_ids.iter() {
|
||||
if row_ids.iter().any(|row_id| row_id == inserted) {
|
||||
row_ids.push(inserted.clone())
|
||||
}
|
||||
}
|
||||
for removed_id in changeset.removed_row_ids.iter() {
|
||||
if let Some(index) = row_ids.iter().position(|row_id| row_id == removed_id) {
|
||||
row_ids.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
let cell_data = RelationCellData { row_ids };
|
||||
|
||||
Ok(((&cell_data).into(), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for RelationTypeOption {
|
||||
fn decode_cell(
|
||||
&self,
|
||||
cell: &Cell,
|
||||
decoded_field_type: &FieldType,
|
||||
_field: &Field,
|
||||
) -> FlowyResult<RelationCellData> {
|
||||
if !decoded_field_type.is_relation() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
Ok(cell.into())
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: RelationCellData) -> String {
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||
let cell_data = RelationCellData::from(cell);
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for RelationTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
_cell_data: &RelationCellData,
|
||||
_other_cell_data: &RelationCellData,
|
||||
_sort_condition: SortCondition,
|
||||
) -> Ordering {
|
||||
default_order()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for RelationTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
_filter: &RelationFilterPB,
|
||||
_field_type: &FieldType,
|
||||
_cell_data: &RelationCellData,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionTransform for RelationTypeOption {
|
||||
fn transformable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn transform_type_option(
|
||||
&mut self,
|
||||
_old_type_option_field_type: FieldType,
|
||||
_old_type_option_data: TypeOptionData,
|
||||
) {
|
||||
}
|
||||
|
||||
fn transform_type_option_cell(
|
||||
&self,
|
||||
_cell: &Cell,
|
||||
_transformed_field_type: &FieldType,
|
||||
_field: &Field,
|
||||
) -> Option<RelationCellData> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataSerde for RelationTypeOption {
|
||||
fn protobuf_encode(&self, cell_data: RelationCellData) -> RelationCellDataPB {
|
||||
cell_data.into()
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<RelationCellData> {
|
||||
Ok(cell.into())
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collab::preclude::Any;
|
||||
use collab_database::rows::{new_cell_builder, Cell, RowId};
|
||||
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RelationCellData {
|
||||
pub row_ids: Vec<RowId>,
|
||||
}
|
||||
|
||||
impl From<&Cell> for RelationCellData {
|
||||
fn from(value: &Cell) -> Self {
|
||||
let row_ids = match value.get(CELL_DATA) {
|
||||
Some(Any::Array(array)) => array
|
||||
.iter()
|
||||
.flat_map(|item| {
|
||||
if let Any::String(string) = item {
|
||||
Some(RowId::from(string.clone().to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
Self { row_ids }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RelationCellData> for Cell {
|
||||
fn from(value: &RelationCellData) -> Self {
|
||||
let data = Any::Array(Arc::from(
|
||||
value
|
||||
.row_ids
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|id| Any::String(Arc::from(id.to_string())))
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
new_cell_builder(FieldType::Relation)
|
||||
.insert_any(CELL_DATA, data)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RelationCellData {
|
||||
fn from(s: String) -> Self {
|
||||
if s.is_empty() {
|
||||
return RelationCellData { row_ids: vec![] };
|
||||
}
|
||||
|
||||
let ids = s
|
||||
.split(", ")
|
||||
.map(|id| id.to_string().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
RelationCellData { row_ids: ids }
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for RelationCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.row_ids.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for RelationCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self
|
||||
.row_ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RelationCellChangeset {
|
||||
pub inserted_row_ids: Vec<RowId>,
|
||||
pub removed_row_ids: Vec<RowId>,
|
||||
}
|
@ -10,14 +10,14 @@ use flowy_error::FlowyResult;
|
||||
|
||||
use crate::entities::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||
TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
};
|
||||
use crate::services::cell::CellDataDecoder;
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
||||
SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
};
|
||||
use crate::services::filter::FromFilterString;
|
||||
use crate::services::sort::SortCondition;
|
||||
@ -202,6 +202,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
||||
FieldType::Checklist => {
|
||||
ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into())
|
||||
},
|
||||
FieldType::Relation => {
|
||||
RelationTypeOptionPB::try_from(bytes).map(|pb| RelationTypeOption::from(pb).into())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,6 +260,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let relation_type_option: RelationTypeOption = type_option.into();
|
||||
RelationTypeOptionPB::from(relation_type_option)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,5 +285,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
||||
FieldType::Checkbox => CheckboxTypeOption::default().into(),
|
||||
FieldType::URL => URLTypeOption::default().into(),
|
||||
FieldType::Checklist => ChecklistTypeOption.into(),
|
||||
FieldType::Relation => RelationTypeOption::default().into(),
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ use crate::services::cell::{
|
||||
};
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
||||
SingleSelectTypeOption, TimestampTypeOption, TypeOption, TypeOptionCellDataCompare,
|
||||
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
|
||||
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
|
||||
TypeOptionTransform, URLTypeOption,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
|
||||
@ -490,6 +491,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Relation => self
|
||||
.field
|
||||
.get_type_option::<RelationTypeOption>(field_type)
|
||||
.map(|type_option| {
|
||||
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||
type_option,
|
||||
self.cell_filter_cache.clone(),
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -568,6 +579,9 @@ fn get_type_option_transform_handler(
|
||||
FieldType::Checklist => {
|
||||
Box::new(ChecklistTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
FieldType::Relation => {
|
||||
Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,6 +358,12 @@ impl FilterController {
|
||||
.write()
|
||||
.insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref()));
|
||||
},
|
||||
FieldType::Relation => {
|
||||
self
|
||||
.cell_filter_cache
|
||||
.write()
|
||||
.insert(field_id, RelationFilterPB::from_filter(filter.as_ref()));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ use std::time::Duration;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
|
||||
SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData, URLCellData,
|
||||
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData,
|
||||
URLCellData,
|
||||
};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
|
||||
@ -52,6 +53,10 @@ async fn grid_cell_update() {
|
||||
}),
|
||||
FieldType::Checkbox => BoxAny::new("1".to_string()),
|
||||
FieldType::URL => BoxAny::new("1".to_string()),
|
||||
FieldType::Relation => BoxAny::new(RelationCellChangeset {
|
||||
inserted_row_ids: vec!["abcdefabcdef".to_string().into()],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => BoxAny::new("".to_string()),
|
||||
};
|
||||
|
||||
|
@ -5,8 +5,8 @@ use strum::IntoEnumIterator;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, RelationTypeOption,
|
||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
use flowy_database2::services::setting::BoardLayoutSetting;
|
||||
@ -126,6 +126,16 @@ pub fn make_test_board() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(checklist_field);
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let type_option = RelationTypeOption {
|
||||
database_id: "".to_string(),
|
||||
};
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("Related")
|
||||
.visibility(true)
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +237,6 @@ pub fn make_test_board() -> DatabaseData {
|
||||
FieldType::SingleSelect => {
|
||||
row_builder.insert_single_select_cell(|mut options| options.remove(2))
|
||||
},
|
||||
|
||||
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
|
||||
_ => "".to_owned(),
|
||||
};
|
||||
|
@ -3,10 +3,10 @@ use collab_database::views::{DatabaseLayout, DatabaseView};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
|
||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
|
||||
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
|
||||
@ -128,6 +128,16 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(checklist_field);
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let type_option = RelationTypeOption {
|
||||
database_id: "".to_string(),
|
||||
};
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("Related")
|
||||
.visibility(true)
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,14 +27,14 @@ 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 expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At
|
||||
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,,,,,,,,,
|
||||
let expected = 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,,,,,,,,,,
|
||||
"#;
|
||||
println!("{}", s);
|
||||
assert_eq!(s, expected);
|
||||
@ -99,6 +99,7 @@ async fn export_and_then_import_meta_csv_test() {
|
||||
FieldType::Checklist => {},
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
@ -180,6 +181,7 @@ async fn history_database_import_test() {
|
||||
FieldType::Checklist => {},
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
|
Reference in New Issue
Block a user