refactor: cell data transform logic (#5039)

* refactor: cell data transform logic

* chore: remove redundant select option event

* chore: adapt tests to changes
This commit is contained in:
Richard Shiue 2024-04-11 14:49:36 +08:00 committed by GitHub
parent 828f312294
commit b7b4ea2da1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 574 additions and 912 deletions

View File

@ -164,6 +164,39 @@ class SelectOptionCellEditorBloc
return super.close(); return super.close();
} }
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (_) {
_loadOptions();
},
onCellFieldChanged: (field) {
_loadOptions();
},
);
}
void _loadOptions() {
if (isClosed) {
Log.warn("Unexpecteded closing the bloc");
return;
}
final cellData = cellController.getCellData();
if (cellData != null) {
add(
SelectOptionCellEditorEvent.didReceiveOptions(
cellData.options,
cellData.selectOptions,
),
);
} else {
add(
const SelectOptionCellEditorEvent.didReceiveOptions([], []),
);
}
}
Future<void> _createOption({ Future<void> _createOption({
required String name, required String name,
required SelectOptionColorPB color, required SelectOptionColorPB color,
@ -251,27 +284,6 @@ class SelectOptionCellEditorBloc
); );
} }
Future<void> _loadOptions() async {
final result = await _selectOptionService.getCellData();
if (isClosed) {
Log.warn("Unexpecteded closing the bloc");
return;
}
return result.fold(
(data) => add(
SelectOptionCellEditorEvent.didReceiveOptions(
data.options,
data.selectOptions,
),
),
(err) {
Log.error(err);
return null;
},
);
}
_MakeOptionResult _getVisibleOptions( _MakeOptionResult _getVisibleOptions(
List<SelectOptionPB> allOptions, List<SelectOptionPB> allOptions,
) { ) {
@ -331,17 +343,6 @@ class SelectOptionCellEditorBloc
emit(state.copyWith(focusedOptionId: optionIds[newIndex])); emit(state.copyWith(focusedOptionId: optionIds[newIndex]));
} }
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (selectOptionContext) {
_loadOptions();
},
onCellFieldChanged: (field) {
_loadOptions();
},
);
}
} }
@freezed @freezed

View File

@ -60,15 +60,6 @@ class SelectOptionCellBackendService {
return DatabaseEventDeleteSelectOption(payload).send(); return DatabaseEventDeleteSelectOption(payload).send();
} }
Future<FlowyResult<SelectOptionCellDataPB, FlowyError>> getCellData() {
final payload = CellIdPB()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
return DatabaseEventGetSelectOptionCellData(payload).send();
}
Future<FlowyResult<void, FlowyError>> select({ Future<FlowyResult<void, FlowyError>> select({
required Iterable<String> optionIds, required Iterable<String> optionIds,
}) { }) {

View File

@ -91,12 +91,19 @@ void main() {
"Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}", "Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
); );
bloc.add(SelectOptionCellEditorEvent.deleteOption(bloc.state.options[0]));
await gridResponseFuture();
assert(
bloc.state.options.length == 2,
"Expect 2 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
);
bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions()); bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions());
await gridResponseFuture(); await gridResponseFuture();
assert( assert(
bloc.state.options.isEmpty, bloc.state.options.isEmpty,
"Expect empty but receive ${bloc.state.options.length}", "Expect empty but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
); );
}); });

View File

@ -532,20 +532,6 @@ pub(crate) async fn delete_select_option_handler(
Ok(()) Ok(())
} }
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_select_option_handler(
data: AFPluginData<CellIdPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<SelectOptionCellDataPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: CellIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let options = database_editor
.get_select_options(params.row_id, &params.field_id)
.await;
data_result_ok(options)
}
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_select_option_cell_handler( pub(crate) async fn update_select_option_cell_handler(
data: AFPluginData<SelectOptionCellChangesetPB>, data: AFPluginData<SelectOptionCellChangesetPB>,

View File

@ -47,7 +47,6 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::CreateSelectOption, new_select_option_handler) .event(DatabaseEvent::CreateSelectOption, new_select_option_handler)
.event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler)
.event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler)
.event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler)
.event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler)
// Checklist // Checklist
.event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler)
@ -201,12 +200,6 @@ pub enum DatabaseEvent {
#[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")] #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")]
CreateSelectOption = 30, CreateSelectOption = 30,
/// [GetSelectOptionCellData] event is used to get the select option data for cell editing.
/// [CellIdPB] locate which cell data that will be read from. The return value, [SelectOptionCellDataPB]
/// contains the available options and the currently selected options.
#[event(input = "CellIdPB", output = "SelectOptionCellDataPB")]
GetSelectOptionCellData = 31,
/// [InsertOrUpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is /// [InsertOrUpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is
/// FieldType::SingleSelect or FieldType::MultiSelect. /// FieldType::SingleSelect or FieldType::MultiSelect.
/// ///
@ -214,10 +207,10 @@ pub enum DatabaseEvent {
/// For example, DatabaseNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB] /// For example, DatabaseNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
/// carries a change that updates the name of the option. /// carries a change that updates the name of the option.
#[event(input = "RepeatedSelectOptionPayload")] #[event(input = "RepeatedSelectOptionPayload")]
InsertOrUpdateSelectOption = 32, InsertOrUpdateSelectOption = 31,
#[event(input = "RepeatedSelectOptionPayload")] #[event(input = "RepeatedSelectOptionPayload")]
DeleteSelectOption = 33, DeleteSelectOption = 32,
#[event(input = "CreateRowPayloadPB", output = "RowMetaPB")] #[event(input = "CreateRowPayloadPB", output = "RowMetaPB")]
CreateRow = 50, CreateRow = 50,

View File

@ -1,9 +1,10 @@
use crate::entities::{CalculationType, FieldType}; use std::sync::Arc;
use crate::services::field::TypeOptionCellExt;
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::rows::RowCell; use collab_database::rows::RowCell;
use std::sync::Arc;
use crate::entities::CalculationType;
use crate::services::field::TypeOptionCellExt;
pub struct CalculationsService {} pub struct CalculationsService {}
@ -35,10 +36,7 @@ impl CalculationsService {
fn calculate_average(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_average(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
let mut sum = 0.0; let mut sum = 0.0;
let mut len = 0.0; let mut len = 0.0;
let field_type = FieldType::from(field.field_type); if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() {
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
for row_cell in row_cells { for row_cell in row_cells {
if let Some(cell) = &row_cell.cell { if let Some(cell) = &row_cell.cell {
if let Some(value) = handler.handle_numeric_cell(cell) { if let Some(value) = handler.handle_numeric_cell(cell) {
@ -130,50 +128,39 @@ impl CalculationsService {
} }
fn calculate_count_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_count_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
let field_type = FieldType::from(field.field_type); match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() {
if let Some(handler) = Some(handler) if !row_cells.is_empty() => row_cells
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) .iter()
{ .filter(|row_cell| {
if !row_cells.is_empty() { if let Some(cell) = &row_cell.cell {
return format!( handler.handle_is_cell_empty(cell, field)
"{}", } else {
row_cells true
.iter() }
.filter(|c| c.is_none() })
|| handler .collect::<Vec<_>>()
.handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field) .len()
.is_empty()) .to_string(),
.collect::<Vec<_>>() _ => "".to_string(),
.len()
);
}
} }
String::new()
} }
fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
let field_type = FieldType::from(field.field_type); match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() {
if let Some(handler) = Some(handler) if !row_cells.is_empty() => row_cells
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) .iter()
{ .filter(|row_cell| {
if !row_cells.is_empty() { if let Some(cell) = &row_cell.cell {
return format!( !handler.handle_is_cell_empty(cell, field)
"{}", } else {
row_cells false
.iter() }
// Check the Cell has data and that the stringified version is not empty })
.filter(|c| c.is_some() .collect::<Vec<_>>()
&& !handler .len()
.handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field) .to_string(),
.is_empty()) _ => "".to_string(),
.collect::<Vec<_>>()
.len()
);
}
} }
String::new()
} }
fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T
@ -182,10 +169,7 @@ impl CalculationsService {
{ {
let mut values = vec![]; let mut values = vec![];
let field_type = FieldType::from(field.field_type); if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() {
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
for row_cell in row_cells { for row_cell in row_cells {
if let Some(cell) = &row_cell.cell { if let Some(cell) = &row_cell.cell {
if let Some(value) = handler.handle_numeric_cell(cell) { if let Some(value) = handler.handle_numeric_cell(cell) {

View File

@ -14,7 +14,6 @@ use crate::services::group::make_no_status_group;
/// Decode the opaque cell data into readable format content /// Decode the opaque cell data into readable format content
pub trait CellDataDecoder: TypeOption { pub trait CellDataDecoder: TypeOption {
///
/// Tries to decode the [Cell] to `decoded_field_type`'s cell data. Sometimes, the `field_type` /// Tries to decode the [Cell] to `decoded_field_type`'s cell data. Sometimes, the `field_type`
/// of the `Field` is not equal to the `decoded_field_type`(This happened When switching /// of the `Field` is not equal to the `decoded_field_type`(This happened When switching
/// the field type of the `Field` to another field type). So the cell data is need to do /// the field type of the `Field` to another field type). So the cell data is need to do
@ -25,21 +24,30 @@ pub trait CellDataDecoder: TypeOption {
/// But the data of the cell doesn't change. We can't iterate all the rows to transform the cell /// But the data of the cell doesn't change. We can't iterate all the rows to transform the cell
/// data that can be parsed by the current field type. One approach is to transform the cell data /// data that can be parsed by the current field type. One approach is to transform the cell data
/// when reading. /// when reading.
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>;
/// Transform the cell data from one field type to another
///
/// # Arguments
///
/// * `cell`: the cell in the current field type
/// * `transformed_field_type`: the cell will be transformed to the is field type's cell data.
/// current `TypeOption` field type.
///
fn decode_cell_with_transform(
&self, &self,
cell: &Cell, _cell: &Cell,
decoded_field_type: &FieldType, _from_field_type: FieldType,
field: &Field, _field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData>; ) -> Option<<Self as TypeOption>::CellData> {
None
}
/// Decode the cell data to readable `String` /// Decode the cell data to readable `String`
/// For example, The string of the Multi-Select cell will be a list of the option's name /// For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma. /// separated by a comma.
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String; fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String;
/// Same as [CellDataDecoder::stringify_cell_data] but the input parameter is the [Cell]
fn stringify_cell(&self, cell: &Cell) -> String;
/// Decode the cell into f64 /// Decode the cell into f64
/// Different field type has different way to decode the cell data into f64 /// Different field type has different way to decode the cell data into f64
/// If the field type doesn't support to decode the cell data into f64, it will return None /// If the field type doesn't support to decode the cell data into f64, it will return None
@ -74,10 +82,7 @@ pub fn apply_cell_changeset(
field: &Field, field: &Field,
cell_data_cache: Option<CellCache>, cell_data_cache: Option<CellCache>,
) -> Result<Cell, FlowyError> { ) -> Result<Cell, FlowyError> {
let field_type = FieldType::from(field.field_type); match TypeOptionCellExt::new(field, cell_data_cache).get_type_option_cell_data_handler() {
match TypeOptionCellExt::new(field, cell_data_cache)
.get_type_option_cell_data_handler(&field_type)
{
None => Ok(Cell::default()), None => Ok(Cell::default()),
Some(handler) => Ok(handler.handle_cell_changeset(changeset, cell, field)?), Some(handler) => Ok(handler.handle_cell_changeset(changeset, cell, field)?),
} }
@ -88,15 +93,7 @@ pub fn get_cell_protobuf(
field: &Field, field: &Field,
cell_cache: Option<CellCache>, cell_cache: Option<CellCache>,
) -> CellProtobufBlob { ) -> CellProtobufBlob {
let from_field_type = get_field_type_from_cell(cell); match try_decode_cell_to_cell_protobuf(cell, field, cell_cache) {
if from_field_type.is_none() {
return CellProtobufBlob::default();
}
let from_field_type = from_field_type.unwrap();
let to_field_type = FieldType::from(field.field_type);
match try_decode_cell_to_cell_protobuf(cell, &from_field_type, &to_field_type, field, cell_cache)
{
Ok(cell_bytes) => cell_bytes, Ok(cell_bytes) => cell_bytes,
Err(e) => { Err(e) => {
tracing::error!("Decode cell data failed, {:?}", e); tracing::error!("Decode cell data failed, {:?}", e);
@ -123,34 +120,15 @@ pub fn get_cell_protobuf(
/// ///
pub fn try_decode_cell_to_cell_protobuf( pub fn try_decode_cell_to_cell_protobuf(
cell: &Cell, cell: &Cell,
from_field_type: &FieldType,
to_field_type: &FieldType,
field: &Field, field: &Field,
cell_data_cache: Option<CellCache>, cell_data_cache: Option<CellCache>,
) -> FlowyResult<CellProtobufBlob> { ) -> FlowyResult<CellProtobufBlob> {
match TypeOptionCellExt::new(field, cell_data_cache) match TypeOptionCellExt::new(field, cell_data_cache).get_type_option_cell_data_handler() {
.get_type_option_cell_data_handler(to_field_type)
{
None => Ok(CellProtobufBlob::default()), None => Ok(CellProtobufBlob::default()),
Some(handler) => handler.handle_cell_protobuf(cell, from_field_type, field), Some(handler) => handler.handle_get_protobuf_cell_data(cell, field),
} }
} }
pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
cell: &Cell,
from_field_type: &FieldType,
to_field_type: &FieldType,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Option<T> {
let handler = TypeOptionCellExt::new(field, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)?;
handler
.get_cell_data(cell, from_field_type, field)
.ok()?
.unbox_or_none::<T>()
}
/// Returns a string that represents the current field_type's cell data. /// Returns a string that represents the current field_type's cell data.
/// For example, a Multi-Select cell will be represented by a list of the options' names /// For example, a Multi-Select cell will be represented by a list of the options' names
/// separated by commas. /// separated by commas.
@ -162,15 +140,14 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
/// * `from_field_type`: the original field type of the passed-in cell data. /// * `from_field_type`: the original field type of the passed-in cell data.
/// * `field`: used to get the corresponding TypeOption for the specified field type. /// * `field`: used to get the corresponding TypeOption for the specified field type.
/// ///
pub fn stringify_cell_data( pub fn stringify_cell(cell: &Cell, field: &Field) -> String {
cell: &Cell, if let Some(field_type_of_cell) = get_field_type_from_cell::<FieldType>(cell) {
to_field_type: &FieldType, TypeOptionCellExt::new(field, None)
from_field_type: &FieldType, .get_type_option_cell_data_handler_with_field_type(field_type_of_cell)
field: &Field, .map(|handler| handler.handle_stringify_cell(cell, field))
) -> String { .unwrap_or_default()
match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(from_field_type) { } else {
None => "".to_string(), "".to_string()
Some(handler) => handler.handle_stringify_cell(cell, to_field_type, field),
} }
} }

View File

@ -10,7 +10,7 @@ use crate::services::database_view::{
use crate::services::field::{ use crate::services::field::{
default_type_option_data_from_type, select_type_option_from_field, transform_type_option, default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset, type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
SelectOptionIds, StrCellData, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt, StrCellData, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
}; };
use crate::services::field_settings::{ use crate::services::field_settings::{
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams, default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
@ -406,16 +406,16 @@ impl DatabaseEditor {
} }
let old_field_type = FieldType::from(field.field_type); let old_field_type = FieldType::from(field.field_type);
let old_type_option = field.get_any_type_option(old_field_type); let old_type_option_data = field.get_any_type_option(old_field_type);
let new_type_option = field let new_type_option_data = field
.get_any_type_option(new_field_type) .get_any_type_option(new_field_type)
.unwrap_or_else(|| default_type_option_data_from_type(new_field_type)); .unwrap_or_else(|| default_type_option_data_from_type(new_field_type));
let transformed_type_option = transform_type_option( let transformed_type_option = transform_type_option(
&new_type_option,
new_field_type,
old_type_option,
old_field_type, old_field_type,
new_field_type,
old_type_option_data,
new_type_option_data,
); );
self self
.database .database
@ -1015,24 +1015,6 @@ impl DatabaseEditor {
Ok(()) Ok(())
} }
pub async fn get_select_options(&self, row_id: RowId, field_id: &str) -> SelectOptionCellDataPB {
let field = self.database.lock().fields.get_field(field_id);
match field {
None => SelectOptionCellDataPB::default(),
Some(field) => {
let cell = self.database.lock().get_cell(field_id, &row_id).cell;
let ids = match cell {
None => SelectOptionIds::new(),
Some(cell) => SelectOptionIds::from(&cell),
};
match select_type_option_from_field(&field) {
Ok(type_option) => type_option.get_selected_options(ids).into(),
Err(_) => SelectOptionCellDataPB::default(),
}
},
}
}
pub async fn set_checklist_options( pub async fn set_checklist_options(
&self, &self,
view_id: &str, view_id: &str,
@ -1349,7 +1331,7 @@ impl DatabaseEditor {
) -> FlowyResult<Vec<RelatedRowDataPB>> { ) -> FlowyResult<Vec<RelatedRowDataPB>> {
let primary_field = self.database.lock().fields.get_primary_field().unwrap(); let primary_field = self.database.lock().fields.get_primary_field().unwrap();
let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone())) let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone()))
.get_type_option_cell_data_handler(&FieldType::RichText) .get_type_option_cell_data_handler_with_field_type(FieldType::RichText)
.ok_or(FlowyError::internal())?; .ok_or(FlowyError::internal())?;
let row_data = { let row_data = {
@ -1364,11 +1346,7 @@ impl DatabaseEditor {
let title = database let title = database
.get_cell(&primary_field.id, &row.id) .get_cell(&primary_field.id, &row.id)
.cell .cell
.and_then(|cell| { .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field))
handler
.get_cell_data(&cell, &FieldType::RichText, &primary_field)
.ok()
})
.and_then(|cell_data| cell_data.unbox_or_none()) .and_then(|cell_data| cell_data.unbox_or_none())
.unwrap_or_else(|| StrCellData("".to_string())); .unwrap_or_else(|| StrCellData("".to_string()));
@ -1715,10 +1693,8 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
fn get_type_option_cell_handler( fn get_type_option_cell_handler(
&self, &self,
field: &Field, field: &Field,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> { ) -> Option<Box<dyn TypeOptionCellDataHandler>> {
TypeOptionCellExt::new(field, Some(self.cell_cache.clone())) TypeOptionCellExt::new(field, Some(self.cell_cache.clone())).get_type_option_cell_data_handler()
.get_type_option_cell_data_handler(field_type)
} }
fn get_field_settings( fn get_field_settings(

View File

@ -111,11 +111,11 @@ pub(crate) async fn get_cell_for_row(
let field = delegate.get_field(field_id)?; let field = delegate.get_field(field_id)?;
let row_cell = delegate.get_cell_in_row(field_id, row_id).await; let row_cell = delegate.get_cell_in_row(field_id, row_id).await;
let field_type = FieldType::from(field.field_type); let field_type = FieldType::from(field.field_type);
let handler = delegate.get_type_option_cell_handler(&field, &field_type)?; let handler = delegate.get_type_option_cell_handler(&field)?;
let cell_data = match &row_cell.cell { let cell_data = match &row_cell.cell {
None => None, None => None,
Some(cell) => handler.get_cell_data(cell, &field_type, &field).ok(), Some(cell) => handler.handle_get_boxed_cell_data(cell, &field),
}; };
Some(RowSingleCellData { Some(RowSingleCellData {
row_id: row_cell.row_id.clone(), row_id: row_cell.row_id.clone(),
@ -133,14 +133,14 @@ pub(crate) async fn get_cells_for_field(
) -> Vec<RowSingleCellData> { ) -> Vec<RowSingleCellData> {
if let Some(field) = delegate.get_field(field_id) { if let Some(field) = delegate.get_field(field_id) {
let field_type = FieldType::from(field.field_type); let field_type = FieldType::from(field.field_type);
if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) { if let Some(handler) = delegate.get_type_option_cell_handler(&field) {
let cells = delegate.get_cells_for_field(view_id, field_id).await; let cells = delegate.get_cells_for_field(view_id, field_id).await;
return cells return cells
.iter() .iter()
.map(|row_cell| { .map(|row_cell| {
let cell_data = match &row_cell.cell { let cell_data = match &row_cell.cell {
None => None, None => None,
Some(cell) => handler.get_cell_data(cell, &field_type, &field).ok(), Some(cell) => handler.handle_get_boxed_cell_data(cell, &field),
}; };
RowSingleCellData { RowSingleCellData {
row_id: row_cell.row_id.clone(), row_id: row_cell.row_id.clone(),

View File

@ -118,7 +118,6 @@ pub trait DatabaseViewOperation: Send + Sync + 'static {
fn get_type_option_cell_handler( fn get_type_option_cell_handler(
&self, &self,
field: &Field, field: &Field,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>>; ) -> Option<Box<dyn TypeOptionCellDataHandler>>;
fn get_field_settings( fn get_field_settings(

View File

@ -1,14 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use collab_database::fields::TypeOptionData;
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::services::database::DatabaseEditor; use crate::services::database::DatabaseEditor;
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption, TypeOption};
pub async fn edit_field_type_option<T: From<TypeOptionData> + Into<TypeOptionData>>( pub async fn edit_field_type_option<T: TypeOption>(
field_id: &str, field_id: &str,
editor: Arc<DatabaseEditor>, editor: Arc<DatabaseEditor>,
action: impl FnOnce(&mut T), action: impl FnOnce(&mut T),
@ -22,7 +20,7 @@ pub async fn edit_field_type_option<T: From<TypeOptionData> + Into<TypeOptionDat
if let Some(mut type_option) = get_type_option.await { if let Some(mut type_option) = get_type_option.await {
if let Some(old_field) = editor.get_field(field_id) { if let Some(old_field) = editor.get_field(field_id) {
action(&mut type_option); action(&mut type_option);
let type_option_data: TypeOptionData = type_option.into(); let type_option_data = type_option.into();
editor editor
.update_field_type_option(field_id, type_option_data, old_field) .update_field_type_option(field_id, type_option_data, old_field)
.await?; .await?;

View File

@ -38,16 +38,12 @@ mod tests {
type_option: &CheckboxTypeOption, type_option: &CheckboxTypeOption,
input_str: &str, input_str: &str,
expected_str: &str, expected_str: &str,
field_type: &FieldType, _field_type: &FieldType,
field: &Field, _field: &Field,
) { ) {
assert_eq!( assert_eq!(
type_option type_option
.decode_cell( .decode_cell(&CheckboxCellDataPB::from_str(input_str).unwrap().into())
&CheckboxCellDataPB::from_str(input_str).unwrap().into(),
field_type,
field
)
.unwrap() .unwrap()
.to_string(), .to_string(),
expected_str.to_owned() expected_str.to_owned()

View File

@ -25,31 +25,7 @@ impl TypeOption for CheckboxTypeOption {
type CellFilter = CheckboxFilterPB; type CellFilter = CheckboxFilterPB;
} }
impl TypeOptionTransform for CheckboxTypeOption { impl TypeOptionTransform for CheckboxTypeOption {}
fn transformable(&self) -> bool {
true
}
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<<Self as TypeOption>::CellData> {
if transformed_field_type.is_text() {
Some(CheckboxCellDataPB::from(cell))
} else {
None
}
}
}
impl From<TypeOptionData> for CheckboxTypeOption { impl From<TypeOptionData> for CheckboxTypeOption {
fn from(_data: TypeOptionData) -> Self { fn from(_data: TypeOptionData) -> Self {
@ -77,26 +53,27 @@ impl TypeOptionCellDataSerde for CheckboxTypeOption {
} }
impl CellDataDecoder for CheckboxTypeOption { impl CellDataDecoder for CheckboxTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
self.parse_cell(cell)
}
fn decode_cell_with_transform(
&self, &self,
cell: &Cell, cell: &Cell,
decoded_field_type: &FieldType, from_field_type: FieldType,
_field: &Field, _field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> { ) -> Option<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_checkbox() { if from_field_type.is_text() {
return Ok(Default::default()); Some(CheckboxCellDataPB::from(cell))
} else {
None
} }
self.parse_cell(cell)
} }
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string() cell_data.to_string()
} }
fn stringify_cell(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> { fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let cell_data = self.parse_cell(cell).ok()?; let cell_data = self.parse_cell(cell).ok()?;
if cell_data.is_checked { if cell_data.is_checked {

View File

@ -1,4 +1,10 @@
use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, FieldType, SelectOptionPB}; use std::cmp::Ordering;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, SelectOptionPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
use crate::services::field::{ use crate::services::field::{
@ -6,10 +12,6 @@ use crate::services::field::{
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR, TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
}; };
use crate::services::sort::SortCondition; use crate::services::sort::SortCondition;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use std::cmp::Ordering;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ChecklistTypeOption; pub struct ChecklistTypeOption;
@ -141,16 +143,7 @@ fn update_cell_data_with_changeset(
} }
impl CellDataDecoder for ChecklistTypeOption { impl CellDataDecoder for ChecklistTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_checklist() {
return Ok(Default::default());
}
self.parse_cell(cell) self.parse_cell(cell)
} }
@ -163,11 +156,6 @@ impl CellDataDecoder for ChecklistTypeOption {
.join(SELECTION_IDS_SEPARATOR) .join(SELECTION_IDS_SEPARATOR)
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = self.parse_cell(cell).unwrap_or_default();
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
// return the percentage complete if needed // return the percentage complete if needed
None None

View File

@ -546,10 +546,8 @@ mod tests {
assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,); assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,);
} }
fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, field: &Field) -> String { fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, _field: &Field) -> String {
let decoded_data = type_option let decoded_data = type_option.decode_cell(cell).unwrap();
.decode_cell(cell, &FieldType::DateTime, field)
.unwrap();
type_option.stringify_cell_data(decoded_data) type_option.stringify_cell_data(decoded_data)
} }

View File

@ -4,13 +4,13 @@ use std::str::FromStr;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone};
use chrono_tz::Tz; use chrono_tz::Tz;
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell; use collab_database::rows::Cell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::entities::{DateCellDataPB, DateFilterPB, FieldType}; use crate::entities::{DateCellDataPB, DateFilterPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{ use crate::services::field::{
default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption, default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption,
@ -199,20 +199,7 @@ impl DateTypeOption {
impl TypeOptionTransform for DateTypeOption {} impl TypeOptionTransform for DateTypeOption {}
impl CellDataDecoder for DateTypeOption { impl CellDataDecoder for DateTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(Default::default());
}
self.parse_cell(cell) self.parse_cell(cell)
} }
@ -244,11 +231,6 @@ impl CellDataDecoder for DateTypeOption {
} }
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None None
} }

View File

@ -1,111 +1,77 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use collab_database::fields::Field;
use crate::entities::FieldType;
use crate::services::cell::CellDataDecoder; use crate::services::cell::CellDataDecoder;
use crate::services::field::{FieldBuilder, NumberCellData}; use crate::services::field::NumberCellData;
use crate::services::field::{NumberFormat, NumberTypeOption}; use crate::services::field::{NumberFormat, NumberTypeOption};
/// Testing when the input is not a number. /// Testing when the input is not a number.
#[test] #[test]
fn number_type_option_input_test() { fn number_type_option_input_test() {
let type_option = NumberTypeOption::default(); let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field = FieldBuilder::from_field_type(field_type).build();
// Input is empty String // Input is empty String
assert_number(&type_option, "", "", &field_type, &field); assert_number(&type_option, "", "");
assert_number(&type_option, "abc", "", &field_type, &field); assert_number(&type_option, "abc", "");
assert_number(&type_option, "-123", "-123", &field_type, &field); assert_number(&type_option, "-123", "-123");
assert_number(&type_option, "abc-123", "-123", &field_type, &field); assert_number(&type_option, "abc-123", "-123");
assert_number(&type_option, "+123", "123", &field_type, &field); assert_number(&type_option, "+123", "123");
assert_number(&type_option, "0.2", "0.2", &field_type, &field); assert_number(&type_option, "0.2", "0.2");
assert_number(&type_option, "-0.2", "-0.2", &field_type, &field); assert_number(&type_option, "-0.2", "-0.2");
assert_number(&type_option, "-$0.2", "0.2", &field_type, &field); assert_number(&type_option, "-$0.2", "0.2");
assert_number(&type_option, ".2", "0.2", &field_type, &field); assert_number(&type_option, ".2", "0.2");
} }
#[test] #[test]
fn dollar_type_option_test() { fn dollar_type_option_test() {
let field_type = FieldType::Number;
let mut type_option = NumberTypeOption::new(); let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD; type_option.format = NumberFormat::USD;
let field = FieldBuilder::new(field_type, type_option.clone()).build();
assert_number(&type_option, "", "", &field_type, &field); assert_number(&type_option, "", "");
assert_number(&type_option, "abc", "", &field_type, &field); assert_number(&type_option, "abc", "");
assert_number(&type_option, "-123", "-$123", &field_type, &field); assert_number(&type_option, "-123", "-$123");
assert_number(&type_option, "+123", "$123", &field_type, &field); assert_number(&type_option, "+123", "$123");
assert_number(&type_option, "0.2", "$0.2", &field_type, &field); assert_number(&type_option, "0.2", "$0.2");
assert_number(&type_option, "-0.2", "-$0.2", &field_type, &field); assert_number(&type_option, "-0.2", "-$0.2");
assert_number(&type_option, "-$0.2", "-$0.2", &field_type, &field); assert_number(&type_option, "-$0.2", "-$0.2");
assert_number(&type_option, "-€0.2", "-$0.2", &field_type, &field); assert_number(&type_option, "-€0.2", "-$0.2");
assert_number(&type_option, ".2", "$0.2", &field_type, &field); assert_number(&type_option, ".2", "$0.2");
} }
#[test] #[test]
fn dollar_type_option_test2() { fn dollar_type_option_test2() {
let field_type = FieldType::Number;
let mut type_option = NumberTypeOption::new(); let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD; type_option.format = NumberFormat::USD;
let field = FieldBuilder::new(field_type, type_option.clone()).build();
assert_number( assert_number(&type_option, "99999999999", "$99,999,999,999");
&type_option, assert_number(&type_option, "$99,999,999,999", "$99,999,999,999");
"99999999999",
"$99,999,999,999",
&field_type,
&field,
);
assert_number(
&type_option,
"$99,999,999,999",
"$99,999,999,999",
&field_type,
&field,
);
} }
#[test] #[test]
fn other_symbol_to_dollar_type_option_test() { fn other_symbol_to_dollar_type_option_test() {
let field_type = FieldType::Number;
let mut type_option = NumberTypeOption::new(); let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD; type_option.format = NumberFormat::USD;
let field = FieldBuilder::new(field_type, type_option.clone()).build();
assert_number(&type_option, "€0.2", "$0.2", &field_type, &field); assert_number(&type_option, "€0.2", "$0.2");
assert_number(&type_option, "-€0.2", "-$0.2", &field_type, &field); assert_number(&type_option, "-€0.2", "-$0.2");
assert_number(&type_option, "-CN¥0.2", "-$0.2", &field_type, &field); assert_number(&type_option, "-CN¥0.2", "-$0.2");
assert_number(&type_option, "CN¥0.2", "$0.2", &field_type, &field); assert_number(&type_option, "CN¥0.2", "$0.2");
assert_number(&type_option, "0.2", "$0.2", &field_type, &field); assert_number(&type_option, "0.2", "$0.2");
} }
#[test] #[test]
fn euro_type_option_test() { fn euro_type_option_test() {
let field_type = FieldType::Number;
let mut type_option = NumberTypeOption::new(); let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::EUR; type_option.format = NumberFormat::EUR;
let field = FieldBuilder::new(field_type, type_option.clone()).build();
assert_number(&type_option, "0.2", "€0,2", &field_type, &field); assert_number(&type_option, "0.2", "€0,2");
assert_number(&type_option, "1000", "€1.000", &field_type, &field); assert_number(&type_option, "1000", "€1.000");
assert_number(&type_option, "1234.56", "€1.234,56", &field_type, &field); assert_number(&type_option, "1234.56", "€1.234,56");
} }
fn assert_number( fn assert_number(type_option: &NumberTypeOption, input_str: &str, expected_str: &str) {
type_option: &NumberTypeOption,
input_str: &str,
expected_str: &str,
field_type: &FieldType,
field: &Field,
) {
assert_eq!( assert_eq!(
type_option type_option
.decode_cell( .decode_cell(&NumberCellData(input_str.to_owned()).into(),)
&NumberCellData(input_str.to_owned()).into(),
field_type,
field
)
.unwrap() .unwrap()
.to_string(), .to_string(),
expected_str.to_owned() expected_str.to_owned()

View File

@ -3,7 +3,7 @@ use std::default::Default;
use std::str::FromStr; use std::str::FromStr;
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::{new_cell_builder, Cell}; use collab_database::rows::{new_cell_builder, Cell};
use fancy_regex::Regex; use fancy_regex::Regex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -175,19 +175,7 @@ impl NumberTypeOption {
impl TypeOptionTransform for NumberTypeOption {} impl TypeOptionTransform for NumberTypeOption {}
impl CellDataDecoder for NumberTypeOption { impl CellDataDecoder for NumberTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if decoded_field_type.is_date()
|| decoded_field_type.is_created_time()
|| decoded_field_type.is_last_edited_time()
{
return Ok(Default::default());
}
let num_cell_data = self.parse_cell(cell)?; let num_cell_data = self.parse_cell(cell)?;
Ok(NumberCellData::from( Ok(NumberCellData::from(
self.format_cell_data(&num_cell_data)?.to_string(), self.format_cell_data(&num_cell_data)?.to_string(),
@ -201,11 +189,6 @@ impl CellDataDecoder for NumberTypeOption {
} }
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> { fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let num_cell_data = self.parse_cell(cell).ok()?; let num_cell_data = self.parse_cell(cell).ok()?;
num_cell_data.0.parse::<f64>().ok() num_cell_data.0.parse::<f64>().ok()

View File

@ -1,12 +1,12 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell; use collab_database::rows::Cell;
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::entities::{FieldType, RelationCellDataPB, RelationFilterPB}; use crate::entities::{RelationCellDataPB, RelationFilterPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{ use crate::services::field::{
default_order, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, default_order, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
@ -77,16 +77,7 @@ impl CellDataChangeset for RelationTypeOption {
} }
impl CellDataDecoder for RelationTypeOption { impl CellDataDecoder for RelationTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<RelationCellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<RelationCellData> {
if !decoded_field_type.is_relation() {
return Ok(Default::default());
}
Ok(cell.into()) Ok(cell.into())
} }
@ -94,11 +85,6 @@ impl CellDataDecoder for RelationTypeOption {
cell_data.to_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> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None None
} }
@ -121,27 +107,7 @@ impl TypeOptionCellDataFilter for RelationTypeOption {
} }
} }
impl TypeOptionTransform for RelationTypeOption { 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 { impl TypeOptionCellDataSerde for RelationTypeOption {
fn protobuf_encode(&self, cell_data: RelationCellData) -> RelationCellDataPB { fn protobuf_encode(&self, cell_data: RelationCellData) -> RelationCellDataPB {

View File

@ -75,10 +75,6 @@ impl<T> TypeOptionTransform for T
where where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + CellDataDecoder, T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + CellDataDecoder,
{ {
fn transformable(&self) -> bool {
true
}
fn transform_type_option( fn transform_type_option(
&mut self, &mut self,
_old_type_option_field_type: FieldType, _old_type_option_field_type: FieldType,
@ -90,18 +86,24 @@ where
_old_type_option_data, _old_type_option_data,
); );
} }
}
fn transform_type_option_cell( impl<T> CellDataDecoder for T
where
T:
SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellDataSerde,
{
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
self.parse_cell(cell)
}
fn decode_cell_with_transform(
&self, &self,
cell: &Cell, cell: &Cell,
transformed_field_type: &FieldType, from_field_type: FieldType,
_field: &Field, _field: &Field,
) -> Option<<Self as TypeOption>::CellData> { ) -> Option<<Self as TypeOption>::CellData> {
match transformed_field_type { match from_field_type {
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
// If the transformed field type is SingleSelect, MultiSelect or Checklist, Do nothing.
None
},
FieldType::Checkbox => { FieldType::Checkbox => {
let cell_content = CheckboxCellDataPB::from(cell).to_string(); let cell_content = CheckboxCellDataPB::from(cell).to_string();
let mut transformed_ids = Vec::new(); let mut transformed_ids = Vec::new();
@ -112,28 +114,14 @@ where
Some(SelectOptionIds::from(transformed_ids)) Some(SelectOptionIds::from(transformed_ids))
}, },
FieldType::RichText => Some(SelectOptionIds::from(cell)), FieldType::RichText => Some(SelectOptionIds::from(cell)),
_ => Some(SelectOptionIds::from(vec![])), FieldType::SingleSelect | FieldType::MultiSelect => Some(SelectOptionIds::from(cell)),
_ => None,
} }
} }
}
impl<T, C> CellDataDecoder for T fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
where
C: Into<SelectOptionIds> + for<'a> From<&'a Cell>,
T: SelectTypeOptionSharedAction + TypeOption<CellData = C> + TypeOptionCellDataSerde,
{
fn decode_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
self.parse_cell(cell)
}
fn stringify_cell_data(&self, cell_data: C) -> String {
self self
.get_selected_options(cell_data.into()) .get_selected_options(cell_data)
.select_options .select_options
.into_iter() .into_iter()
.map(|option| option.name) .map(|option| option.name)
@ -141,11 +129,6 @@ where
.join(SELECTION_IDS_SEPARATOR) .join(SELECTION_IDS_SEPARATOR)
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = C::from(cell);
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None None
} }

View File

@ -23,7 +23,7 @@ impl SelectOptionTypeOptionTransformHelper {
{ {
match old_field_type { match old_field_type {
FieldType::Checkbox => { FieldType::Checkbox => {
//add Yes and No options if it does not exist. // add Yes and No options if it does not exist.
if !shared.options().iter().any(|option| option.name == CHECK) { if !shared.options().iter().any(|option| option.name == CHECK) {
let check_option = SelectOption::with_color(CHECK, SelectOptionColor::Green); let check_option = SelectOption::with_color(CHECK, SelectOptionColor::Green);
shared.mut_options().push(check_option); shared.mut_options().push(check_option);

View File

@ -1,9 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use collab_database::rows::Cell;
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::services::cell::stringify_cell_data; use crate::services::cell::{insert_select_option_cell, stringify_cell};
use crate::services::field::FieldBuilder; use crate::services::field::FieldBuilder;
use crate::services::field::*; use crate::services::field::*;
@ -14,15 +12,12 @@ mod tests {
let field_type = FieldType::DateTime; let field_type = FieldType::DateTime;
let field = FieldBuilder::new(field_type, DateTypeOption::test()).build(); let field = FieldBuilder::new(field_type, DateTypeOption::test()).build();
assert_eq!( let data = DateCellData {
stringify_cell_data( timestamp: Some(1647251762),
&to_text_cell(1647251762.to_string()), ..Default::default()
&FieldType::RichText, };
&field_type,
&field assert_eq!(stringify_cell(&(&data).into(), &field), "Mar 14, 2022");
),
"Mar 14, 2022"
);
let data = DateCellData { let data = DateCellData {
timestamp: Some(1647251762), timestamp: Some(1647251762),
@ -33,7 +28,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), stringify_cell(&(&data).into(), &field),
"Mar 14, 2022 09:56" "Mar 14, 2022 09:56"
); );
@ -46,7 +41,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), stringify_cell(&(&data).into(), &field),
"Mar 14, 2022 09:56" "Mar 14, 2022 09:56"
); );
@ -59,15 +54,11 @@ mod tests {
}; };
assert_eq!( assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), stringify_cell(&(&data).into(), &field),
"Mar 14, 2022 09:56 → Mar 29, 2022 06:03" "Mar 14, 2022 09:56 → Mar 29, 2022 06:03"
); );
} }
fn to_text_cell(s: String) -> Cell {
StrCellData(s).into()
}
// Test parser the cell data which field's type is FieldType::SingleSelect to cell data // Test parser the cell data which field's type is FieldType::SingleSelect to cell data
// which field's type is FieldType::Text // which field's type is FieldType::Text
#[test] #[test]
@ -82,20 +73,11 @@ mod tests {
}; };
let field = FieldBuilder::new(field_type, single_select).build(); let field = FieldBuilder::new(field_type, single_select).build();
assert_eq!( let cell = insert_select_option_cell(vec![option_id], &field);
stringify_cell_data(
&to_text_cell(option_id), assert_eq!(stringify_cell(&cell, &field), done_option.name,);
&FieldType::RichText,
&field_type,
&field
),
done_option.name,
);
} }
/*
- [Unit Test] Testing the switching from Multi-selection type to Text type
- Tracking : https://github.com/AppFlowy-IO/AppFlowy/issues/1183
*/
#[test] #[test]
fn multiselect_to_text_type() { fn multiselect_to_text_type() {
let field_type = FieldType::MultiSelect; let field_type = FieldType::MultiSelect;
@ -110,15 +92,12 @@ mod tests {
let france_option_id = france.id; let france_option_id = france.id;
let argentina_option_id = argentina.id; let argentina_option_id = argentina.id;
let field_rev = FieldBuilder::new(field_type, multi_select).build(); let field = FieldBuilder::new(field_type, multi_select).build();
let cell = insert_select_option_cell(vec![france_option_id, argentina_option_id], &field);
assert_eq!( assert_eq!(
stringify_cell_data( stringify_cell(&cell, &field),
&to_text_cell(format!("{},{}", france_option_id, argentina_option_id)),
&FieldType::RichText,
&field_type,
&field_rev
),
format!("{},{}", france.name, argentina.name) format!("{},{}", france.name, argentina.name)
); );
} }

View File

@ -10,7 +10,7 @@ use flowy_error::{FlowyError, FlowyResult};
use crate::entities::{FieldType, TextFilterPB}; use crate::entities::{FieldType, TextFilterPB};
use crate::services::cell::{ use crate::services::cell::{
stringify_cell_data, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser, stringify_cell, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser,
}; };
use crate::services::field::type_options::util::ProtobufStr; use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{ use crate::services::field::{
@ -49,42 +49,7 @@ impl From<RichTextTypeOption> for TypeOptionData {
} }
} }
impl TypeOptionTransform for RichTextTypeOption { impl TypeOptionTransform for RichTextTypeOption {}
fn transformable(&self) -> bool {
true
}
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<<Self as TypeOption>::CellData> {
if transformed_field_type.is_date()
|| transformed_field_type.is_single_select()
|| transformed_field_type.is_multi_select()
|| transformed_field_type.is_number()
|| transformed_field_type.is_url()
|| transformed_field_type.is_checklist()
{
Some(StrCellData::from(stringify_cell_data(
cell,
transformed_field_type,
transformed_field_type,
_field,
)))
} else {
Some(StrCellData::from(cell))
}
}
}
impl TypeOptionCellDataSerde for RichTextTypeOption { impl TypeOptionCellDataSerde for RichTextTypeOption {
fn protobuf_encode( fn protobuf_encode(
@ -100,23 +65,35 @@ impl TypeOptionCellDataSerde for RichTextTypeOption {
} }
impl CellDataDecoder for RichTextTypeOption { impl CellDataDecoder for RichTextTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
}
fn decode_cell_with_transform(
&self, &self,
cell: &Cell, cell: &Cell,
_decoded_field_type: &FieldType, from_field_type: FieldType,
_field: &Field, field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> { ) -> Option<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell)) match from_field_type {
FieldType::RichText
| FieldType::Number
| FieldType::DateTime
| FieldType::SingleSelect
| FieldType::MultiSelect
| FieldType::Checkbox
| FieldType::URL => Some(StrCellData::from(stringify_cell(cell, field))),
FieldType::Checklist
| FieldType::LastEditedTime
| FieldType::CreatedTime
| FieldType::Relation => None,
}
} }
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string() cell_data.to_string()
} }
fn stringify_cell(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> { fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
StrCellData::from(cell).0.parse::<f64>().ok() StrCellData::from(cell).0.parse::<f64>().ok()
} }

View File

@ -2,7 +2,7 @@ use std::cmp::Ordering;
use chrono::{DateTime, Local, Offset}; use chrono::{DateTime, Local, Offset};
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell; use collab_database::rows::Cell;
use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -124,17 +124,7 @@ impl TimestampTypeOption {
impl TypeOptionTransform for TimestampTypeOption {} impl TypeOptionTransform for TimestampTypeOption {}
impl CellDataDecoder for TimestampTypeOption { impl CellDataDecoder for TimestampTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
// Return default data if the type_option_cell_data is not FieldType::CreatedTime nor FieldType::LastEditedTime
if !decoded_field_type.is_last_edited_time() && !decoded_field_type.is_created_time() {
return Ok(Default::default());
}
self.parse_cell(cell) self.parse_cell(cell)
} }
@ -148,11 +138,6 @@ impl CellDataDecoder for TimestampTypeOption {
} }
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None None
} }

View File

@ -2,7 +2,7 @@ use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::Debug;
use bytes::Bytes; use bytes::Bytes;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::TypeOptionData;
use collab_database::rows::Cell; use collab_database::rows::Cell;
use protobuf::ProtobufError; use protobuf::ProtobufError;
@ -90,11 +90,6 @@ pub trait TypeOptionCellData {
} }
pub trait TypeOptionTransform: TypeOption { pub trait TypeOptionTransform: TypeOption {
/// Returns true if the current `TypeOption` provides custom type option transformation
fn transformable(&self) -> bool {
false
}
/// Transform the TypeOption from one field type to another /// Transform the TypeOption from one field type to another
/// For example, when switching from `Checkbox` type option to `Single-Select` /// For example, when switching from `Checkbox` type option to `Single-Select`
/// type option, adding the `Yes` option if the `Single-select` type-option doesn't contain it. /// type option, adding the `Yes` option if the `Single-select` type-option doesn't contain it.
@ -112,23 +107,6 @@ pub trait TypeOptionTransform: TypeOption {
_old_type_option_data: TypeOptionData, _old_type_option_data: TypeOptionData,
) { ) {
} }
/// Transform the cell data from one field type to another
///
/// # Arguments
///
/// * `cell`: the cell in the current field type
/// * `transformed_field_type`: the cell will be transformed to the is field type's cell data.
/// current `TypeOption` field type.
///
fn transform_type_option_cell(
&self,
_cell: &Cell,
_transformed_field_type: &FieldType,
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
None
}
} }
pub trait TypeOptionCellDataFilter: TypeOption + CellDataDecoder { pub trait TypeOptionCellDataFilter: TypeOption + CellDataDecoder {

View File

@ -3,18 +3,17 @@ use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, RowId}; use collab_database::rows::{get_field_type_from_cell, Cell, RowId};
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use lib_infra::box_any::BoxAny; use lib_infra::box_any::BoxAny;
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob}; use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{ use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform, URLTypeOption, TypeOptionTransform, URLTypeOption,
}; };
use crate::services::sort::SortCondition; use crate::services::sort::SortCondition;
@ -31,10 +30,13 @@ pub const CELL_DATA: &str = "data";
/// 2. there are no generic types parameters. /// 2. there are no generic types parameters.
/// ///
pub trait TypeOptionCellDataHandler: Send + Sync + 'static { pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
fn handle_cell_protobuf( /// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field].
/// The caller can get the cell data by calling [BoxCellData::unbox_or_none].
fn handle_get_boxed_cell_data(&self, cell: &Cell, field: &Field) -> Option<BoxCellData>;
fn handle_get_protobuf_cell_data(
&self, &self,
cell: &Cell, cell: &Cell,
decoded_field_type: &FieldType,
field_rev: &Field, field_rev: &Field,
) -> FlowyResult<CellProtobufBlob>; ) -> FlowyResult<CellProtobufBlob>;
@ -45,190 +47,6 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
field: &Field, field: &Field,
) -> FlowyResult<Cell>; ) -> FlowyResult<Cell>;
fn handle_cell_compare(
&self,
left_cell: Option<&Cell>,
right_cell: Option<&Cell>,
field: &Field,
sort_condition: SortCondition,
) -> Ordering;
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool;
/// Format the cell to string using the passed-in [FieldType] and [Field].
/// The [Cell] is generic, so we need to know the [FieldType] and [Field] to format the cell.
///
/// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and
/// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022".
///
fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String;
fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64>;
/// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field].
/// The caller can get the cell data by calling [BoxCellData::unbox_or_none].
fn get_cell_data(
&self,
cell: &Cell,
field_type: &FieldType,
field: &Field,
) -> FlowyResult<BoxCellData>;
}
struct CellDataCacheKey(u64);
impl CellDataCacheKey {
pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self {
let mut hasher = DefaultHasher::new();
if let Some(type_option_data) = field_rev.get_any_type_option(decoded_field_type) {
type_option_data.hash(&mut hasher);
}
hasher.write(field_rev.id.as_bytes());
hasher.write_u8(decoded_field_type as u8);
cell.hash(&mut hasher);
Self(hasher.finish())
}
}
impl AsRef<u64> for CellDataCacheKey {
fn as_ref(&self) -> &u64 {
&self.0
}
}
struct TypeOptionCellDataHandlerImpl<T> {
inner: T,
cell_data_cache: Option<CellCache>,
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ Send
+ Sync
+ 'static,
{
pub fn into_boxed(self) -> Box<dyn TypeOptionCellDataHandler> {
Box::new(self) as Box<dyn TypeOptionCellDataHandler>
}
pub fn new_with_boxed(
inner: T,
cell_data_cache: Option<CellCache>,
) -> Box<dyn TypeOptionCellDataHandler> {
Self {
inner,
cell_data_cache,
}
.into_boxed()
}
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption + CellDataDecoder + Send + Sync,
{
fn get_decoded_cell_data(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<T::CellData> {
let key = CellDataCacheKey::new(field, *decoded_field_type, cell);
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let read_guard = cell_data_cache.read();
if let Some(cell_data) = read_guard.get(key.as_ref()).cloned() {
// tracing::trace!(
// "Cell cache hit: field_type:{}, cell: {:?}, cell_data: {:?}",
// decoded_field_type,
// cell,
// cell_data
// );
return Ok(cell_data);
}
}
let cell_data = self.decode_cell(cell, decoded_field_type, field)?;
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
// tracing::trace!(
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
// decoded_field_type,
// cell,
// cell_data
// );
cell_data_cache
.write()
.insert(key.as_ref(), cell_data.clone());
}
Ok(cell_data)
}
fn set_decoded_cell_data(&self, cell: &Cell, cell_data: T::CellData, field: &Field) {
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
// );
cell_data_cache.write().insert(key.as_ref(), cell_data);
}
}
}
impl<T> std::ops::Deref for TypeOptionCellDataHandlerImpl<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ Send
+ Sync
+ 'static,
{
fn handle_cell_protobuf(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field_rev: &Field,
) -> FlowyResult<CellProtobufBlob> {
let cell_data = self
.get_cell_data(cell, decoded_field_type, field_rev)?
.unbox_or_default::<T::CellData>();
CellProtobufBlob::from(self.protobuf_encode(cell_data))
}
fn handle_cell_changeset(
&self,
cell_changeset: BoxAny,
old_cell: Option<Cell>,
field: &Field,
) -> FlowyResult<Cell> {
let changeset = cell_changeset.unbox_or_error::<T::CellChangeset>()?;
let (cell, cell_data) = self.apply_changeset(changeset, old_cell)?;
self.set_decoded_cell_data(&cell, cell_data, field);
Ok(cell)
}
/// Compares two cell data values given their optional references, field information, and sorting condition. /// Compares two cell data values given their optional references, field information, and sorting condition.
/// ///
/// This function is designed to handle the comparison of cells that might not be initialized. The cells are /// This function is designed to handle the comparison of cells that might not be initialized. The cells are
@ -256,32 +74,207 @@ where
right_cell: Option<&Cell>, right_cell: Option<&Cell>,
field: &Field, field: &Field,
sort_condition: SortCondition, sort_condition: SortCondition,
) -> Ordering { ) -> Ordering;
let field_type = FieldType::from(field.field_type);
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool;
/// Format the cell to string using the passed-in [FieldType] and [Field].
/// The [Cell] is generic, so we need to know the [FieldType] and [Field] to format the cell.
///
/// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and
/// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022".
///
fn handle_stringify_cell(&self, cell: &Cell, field: &Field) -> String;
fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64>;
fn handle_is_cell_empty(&self, cell: &Cell, field: &Field) -> bool;
}
struct CellDataCacheKey(u64);
impl CellDataCacheKey {
pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self {
let mut hasher = DefaultHasher::new();
if let Some(type_option_data) = field_rev.get_any_type_option(decoded_field_type) {
type_option_data.hash(&mut hasher);
}
hasher.write(field_rev.id.as_bytes());
hasher.write_u8(decoded_field_type as u8);
cell.hash(&mut hasher);
Self(hasher.finish())
}
}
impl AsRef<u64> for CellDataCacheKey {
fn as_ref(&self) -> &u64 {
&self.0
}
}
struct TypeOptionCellDataHandlerImpl<T> {
inner: T,
field_type: FieldType,
cell_data_cache: Option<CellCache>,
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ Send
+ Sync
+ 'static,
{
pub fn into_boxed(self) -> Box<dyn TypeOptionCellDataHandler> {
Box::new(self) as Box<dyn TypeOptionCellDataHandler>
}
pub fn new_with_boxed(
inner: T,
field_type: FieldType,
cell_data_cache: Option<CellCache>,
) -> Box<dyn TypeOptionCellDataHandler> {
Self {
inner,
field_type,
cell_data_cache,
}
.into_boxed()
}
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption + CellDataDecoder + Send + Sync,
{
fn get_cell_data_cache_key(&self, cell: &Cell, field: &Field) -> CellDataCacheKey {
CellDataCacheKey::new(field, self.field_type, cell)
}
fn get_cell_data_from_cache(&self, cell: &Cell, field: &Field) -> Option<T::CellData> {
let key = self.get_cell_data_cache_key(cell, field);
let cell_data_cache = self.cell_data_cache.as_ref()?.read();
cell_data_cache.get(key.as_ref()).cloned()
}
fn set_cell_data_in_cache(&self, cell: &Cell, cell_data: T::CellData, field: &Field) {
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
// );
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);
}
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) {
Some(
self
.decode_cell_with_transform(cell, field_type_of_cell, field)
.unwrap_or_default(),
)
} else {
None
};
if let Some(data) = &cell_data {
self.set_cell_data_in_cache(cell, data.clone(), field);
}
cell_data
}
}
impl<T> std::ops::Deref for TypeOptionCellDataHandlerImpl<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ Send
+ Sync
+ 'static,
{
fn handle_get_boxed_cell_data(&self, cell: &Cell, field: &Field) -> Option<BoxCellData> {
let cell_data = self.get_cell_data(cell, field)?;
Some(BoxCellData::new(cell_data))
}
fn handle_get_protobuf_cell_data(
&self,
cell: &Cell,
field_rev: &Field,
) -> FlowyResult<CellProtobufBlob> {
let cell_data = self.get_cell_data(cell, field_rev).unwrap_or_default();
CellProtobufBlob::from(self.protobuf_encode(cell_data))
}
fn handle_cell_changeset(
&self,
cell_changeset: BoxAny,
old_cell: Option<Cell>,
field: &Field,
) -> FlowyResult<Cell> {
let changeset = cell_changeset.unbox_or_error::<T::CellChangeset>()?;
let (cell, cell_data) = self.apply_changeset(changeset, old_cell)?;
self.set_cell_data_in_cache(&cell, cell_data, field);
Ok(cell)
}
fn handle_cell_compare(
&self,
left_cell: Option<&Cell>,
right_cell: Option<&Cell>,
field: &Field,
sort_condition: SortCondition,
) -> Ordering {
match (left_cell, right_cell) { match (left_cell, right_cell) {
(None, None) => Ordering::Equal, (None, None) => Ordering::Equal,
(None, Some(right_cell)) => { (None, Some(right_cell)) => {
let right_cell_data = self let right_cell_data = self.get_cell_data(right_cell, field).unwrap_or_default();
.get_decoded_cell_data(right_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp_with_uninitialized(None, Some(right_cell_data).as_ref(), sort_condition) self.apply_cmp_with_uninitialized(None, Some(right_cell_data).as_ref(), sort_condition)
}, },
(Some(left_cell), None) => { (Some(left_cell), None) => {
let left_cell_data = self let left_cell_data = self.get_cell_data(left_cell, field).unwrap_or_default();
.get_decoded_cell_data(left_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp_with_uninitialized(Some(left_cell_data).as_ref(), None, sort_condition) self.apply_cmp_with_uninitialized(Some(left_cell_data).as_ref(), None, sort_condition)
}, },
(Some(left_cell), Some(right_cell)) => { (Some(left_cell), Some(right_cell)) => {
let left_cell_data: <T as TypeOption>::CellData = self let left_cell_data = self.get_cell_data(left_cell, field).unwrap_or_default();
.get_decoded_cell_data(left_cell, &field_type, field) let right_cell_data = self.get_cell_data(right_cell, field).unwrap_or_default();
.unwrap_or_default();
let right_cell_data = self
.get_decoded_cell_data(right_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp(&left_cell_data, &right_cell_data, sort_condition) self.apply_cmp(&left_cell_data, &right_cell_data, sort_condition)
}, },
@ -290,9 +283,8 @@ where
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool { fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool {
let perform_filter = || { let perform_filter = || {
let field_type = FieldType::from(field.field_type);
let cell_filter = filter.downcast_ref::<T::CellFilter>()?; let cell_filter = filter.downcast_ref::<T::CellFilter>()?;
let cell_data = self.get_decoded_cell_data(cell, &field_type, field).ok()?; let cell_data = self.get_cell_data(cell, field).unwrap_or_default();
Some(self.apply_filter(cell_filter, &cell_data)) Some(self.apply_filter(cell_filter, &cell_data))
}; };
@ -306,36 +298,24 @@ where
/// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the /// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the
/// option's name. /// option's name.
/// ///
fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String { fn handle_stringify_cell(&self, cell: &Cell, field: &Field) -> String {
if self.transformable() { if is_type_option_cell_transformable(self.field_type, FieldType::RichText) {
let cell_data = self.transform_type_option_cell(cell, field_type, field); let cell_data = self.get_cell_data(cell, field);
if let Some(cell_data) = cell_data { if let Some(cell_data) = cell_data {
return self.stringify_cell_data(cell_data); return self.stringify_cell_data(cell_data);
} }
} }
self.stringify_cell(cell) "".to_string()
} }
fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64> { fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64> {
self.numeric_cell(cell) self.numeric_cell(cell)
} }
fn get_cell_data( fn handle_is_cell_empty(&self, cell: &Cell, field: &Field) -> bool {
&self, let cell_data = self.get_cell_data(cell, field).unwrap_or_default();
cell: &Cell,
field_type: &FieldType, cell_data.is_cell_empty()
field: &Field,
) -> FlowyResult<BoxCellData> {
// tracing::debug!("get_cell_data: {:?}", std::any::type_name::<Self>());
let cell_data = if self.transformable() {
match self.transform_type_option_cell(cell, field_type, field) {
None => self.get_decoded_cell_data(cell, field_type, field)?,
Some(cell_data) => cell_data,
}
} else {
self.get_decoded_cell_data(cell, field_type, field)?
};
Ok(BoxCellData::new(cell_data))
} }
} }
@ -352,98 +332,150 @@ impl<'a> TypeOptionCellExt<'a> {
} }
} }
pub fn get_cells<T>(&self) -> Vec<T> { pub fn get_type_option_cell_data_handler_with_field_type(
let field_type = FieldType::from(self.field.field_type);
match self.get_type_option_cell_data_handler(&field_type) {
None => vec![],
Some(_handler) => {
todo!()
},
}
}
pub fn get_type_option_cell_data_handler(
&self, &self,
field_type: &FieldType, field_type: FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> { ) -> Option<Box<dyn TypeOptionCellDataHandler>> {
match field_type { match field_type {
FieldType::RichText => self FieldType::RichText => self
.field .field
.get_type_option::<RichTextTypeOption>(field_type) .get_type_option::<RichTextTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::Number => self FieldType::Number => self
.field .field
.get_type_option::<NumberTypeOption>(field_type) .get_type_option::<NumberTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::DateTime => self FieldType::DateTime => self
.field .field
.get_type_option::<DateTypeOption>(field_type) .get_type_option::<DateTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::LastEditedTime | FieldType::CreatedTime => self FieldType::LastEditedTime | FieldType::CreatedTime => self
.field .field
.get_type_option::<TimestampTypeOption>(field_type) .get_type_option::<TimestampTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::SingleSelect => self FieldType::SingleSelect => self
.field .field
.get_type_option::<SingleSelectTypeOption>(field_type) .get_type_option::<SingleSelectTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::MultiSelect => self FieldType::MultiSelect => self
.field .field
.get_type_option::<MultiSelectTypeOption>(field_type) .get_type_option::<MultiSelectTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::Checkbox => self FieldType::Checkbox => self
.field .field
.get_type_option::<CheckboxTypeOption>(field_type) .get_type_option::<CheckboxTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::URL => { FieldType::URL => {
self self
.field .field
.get_type_option::<URLTypeOption>(field_type) .get_type_option::<URLTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}) })
}, },
FieldType::Checklist => self FieldType::Checklist => self
.field .field
.get_type_option::<ChecklistTypeOption>(field_type) .get_type_option::<ChecklistTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
FieldType::Relation => self FieldType::Relation => self
.field .field
.get_type_option::<RelationTypeOption>(field_type) .get_type_option::<RelationTypeOption>(field_type)
.map(|type_option| { .map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}), }),
} }
} }
pub fn get_type_option_cell_data_handler(&self) -> Option<Box<dyn TypeOptionCellDataHandler>> {
let field_type = FieldType::from(self.field.field_type);
self.get_type_option_cell_data_handler_with_field_type(field_type)
}
}
pub fn is_type_option_cell_transformable(
from_field_type: FieldType,
to_field_type: FieldType,
) -> bool {
matches!(
(from_field_type, to_field_type),
(FieldType::Checkbox, FieldType::SingleSelect)
| (FieldType::Checkbox, FieldType::MultiSelect)
| (FieldType::SingleSelect, FieldType::MultiSelect)
| (FieldType::MultiSelect, FieldType::SingleSelect)
| (_, FieldType::RichText)
)
} }
pub fn transform_type_option( pub fn transform_type_option(
type_option_data: &TypeOptionData, old_field_type: FieldType,
new_field_type: FieldType, new_field_type: FieldType,
old_type_option_data: Option<TypeOptionData>, old_type_option_data: Option<TypeOptionData>,
old_field_type: FieldType, new_type_option_data: TypeOptionData,
) -> TypeOptionData { ) -> TypeOptionData {
let mut transform_handler = get_type_option_transform_handler(type_option_data, new_field_type);
if let Some(old_type_option_data) = old_type_option_data { if let Some(old_type_option_data) = old_type_option_data {
let mut transform_handler =
get_type_option_transform_handler(new_type_option_data, new_field_type);
transform_handler.transform(old_field_type, old_type_option_data); transform_handler.transform(old_field_type, old_type_option_data);
transform_handler.to_type_option_data()
} else {
new_type_option_data
} }
transform_handler.to_type_option_data()
} }
/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait. /// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait.
@ -459,27 +491,25 @@ pub trait TypeOptionTransformHandler {
impl<T> TypeOptionTransformHandler for T impl<T> TypeOptionTransformHandler for T
where where
T: TypeOptionTransform + Into<TypeOptionData> + Clone, T: TypeOptionTransform + Clone,
{ {
fn transform( fn transform(
&mut self, &mut self,
old_type_option_field_type: FieldType, old_type_option_field_type: FieldType,
old_type_option_data: TypeOptionData, old_type_option_data: TypeOptionData,
) { ) {
if self.transformable() { self.transform_type_option(old_type_option_field_type, old_type_option_data)
self.transform_type_option(old_type_option_field_type, old_type_option_data)
}
} }
fn to_type_option_data(&self) -> TypeOptionData { fn to_type_option_data(&self) -> TypeOptionData {
self.clone().into() self.clone().into()
} }
} }
fn get_type_option_transform_handler( fn get_type_option_transform_handler(
type_option_data: &TypeOptionData, type_option_data: TypeOptionData,
field_type: FieldType, field_type: FieldType,
) -> Box<dyn TypeOptionTransformHandler> { ) -> Box<dyn TypeOptionTransformHandler> {
let type_option_data = type_option_data.clone();
match field_type { match field_type {
FieldType::RichText => { FieldType::RichText => {
Box::new(RichTextTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> Box::new(RichTextTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>

View File

@ -1,4 +1,4 @@
use crate::entities::{FieldType, TextFilterPB, URLCellDataPB}; use crate::entities::{TextFilterPB, URLCellDataPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{ use crate::services::field::{
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
@ -7,7 +7,7 @@ use crate::services::field::{
use crate::services::sort::SortCondition; use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell; use collab_database::rows::Cell;
use fancy_regex::Regex; use fancy_regex::Regex;
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
@ -61,16 +61,7 @@ impl TypeOptionCellDataSerde for URLTypeOption {
} }
impl CellDataDecoder for URLTypeOption { impl CellDataDecoder for URLTypeOption {
fn decode_cell( fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_url() {
return Ok(Default::default());
}
self.parse_cell(cell) self.parse_cell(cell)
} }
@ -78,11 +69,6 @@ impl CellDataDecoder for URLTypeOption {
cell_data.data cell_data.data
} }
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.stringify_cell_data(cell_data)
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None None
} }

View File

@ -528,9 +528,8 @@ fn apply_filter(
return Some(false); return Some(false);
} }
let cell = row.cells.get(field_id).cloned(); let cell = row.cells.get(field_id).cloned();
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new(field, Some(cell_data_cache.clone())) if let Some(handler) = TypeOptionCellExt::new(field, Some(cell_data_cache.clone()))
.get_type_option_cell_data_handler(&field_type) .get_type_option_cell_data_handler()
{ {
Some(handler.handle_cell_filter(field, &cell.unwrap_or_default(), condition_and_content)) Some(handler.handle_cell_filter(field, &cell.unwrap_or_default(), condition_and_content))
} else { } else {

View File

@ -3,8 +3,7 @@ use indexmap::IndexMap;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use crate::entities::FieldType; use crate::services::cell::stringify_cell;
use crate::services::cell::stringify_cell_data;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum CSVFormat { pub enum CSVFormat {
@ -46,12 +45,9 @@ impl CSVExport {
.iter() .iter()
.map(|(field_id, field)| match row.cells.get(field_id) { .map(|(field_id, field)| match row.cells.get(field_id) {
None => "".to_string(), None => "".to_string(),
Some(cell) => { Some(cell) => match style {
let field_type = FieldType::from(field.field_type); CSVFormat::Original => stringify_cell(cell, field),
match style { CSVFormat::META => serde_json::to_string(cell).unwrap_or_else(|_| "".to_string()),
CSVFormat::Original => stringify_cell_data(cell, &field_type, &field_type, field),
CSVFormat::META => serde_json::to_string(cell).unwrap_or_else(|_| "".to_string()),
}
}, },
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@ -336,7 +336,6 @@ fn cmp_row(
.as_ref() .as_ref()
.map_or_else(|| right.cells.get(&sort.field_id), |cell| cell.1.as_ref()), .map_or_else(|| right.cells.get(&sort.field_id), |cell| cell.1.as_ref()),
field_rev, field_rev,
field_type,
cell_data_cache, cell_data_cache,
sort.condition, sort.condition,
) )
@ -348,12 +347,11 @@ fn cmp_cell(
left_cell: Option<&Cell>, left_cell: Option<&Cell>,
right_cell: Option<&Cell>, right_cell: Option<&Cell>,
field: &Field, field: &Field,
field_type: FieldType,
cell_data_cache: &CellCache, cell_data_cache: &CellCache,
sort_condition: SortCondition, sort_condition: SortCondition,
) -> Ordering { ) -> Ordering {
match TypeOptionCellExt::new(field, Some(cell_data_cache.clone())) match TypeOptionCellExt::new(field, Some(cell_data_cache.clone()))
.get_type_option_cell_data_handler(&field_type) .get_type_option_cell_data_handler()
{ {
None => default_order(), None => default_order(),
Some(handler) => handler.handle_cell_compare(left_cell, right_cell, field, sort_condition), Some(handler) => handler.handle_cell_compare(left_cell, right_cell, field, sort_condition),

View File

@ -1,7 +1,7 @@
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use flowy_database2::entities::{CreateFieldParams, FieldChangesetParams, FieldType}; use flowy_database2::entities::{CreateFieldParams, FieldChangesetParams, FieldType};
use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::cell::stringify_cell;
use crate::database::database_editor::DatabaseEditorTest; use crate::database::database_editor::DatabaseEditorTest;
@ -31,7 +31,6 @@ pub enum FieldScript {
AssertCellContent { AssertCellContent {
field_id: String, field_id: String,
row_index: usize, row_index: usize,
from_field_type: FieldType,
expected_content: String, expected_content: String,
}, },
} }
@ -118,17 +117,15 @@ impl DatabaseFieldTest {
FieldScript::AssertCellContent { FieldScript::AssertCellContent {
field_id, field_id,
row_index, row_index,
from_field_type,
expected_content, expected_content,
} => { } => {
let field = self.editor.get_field(&field_id).unwrap(); let field = self.editor.get_field(&field_id).unwrap();
let field_type = FieldType::from(field.field_type);
let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); let rows = self.editor.get_rows(&self.view_id()).await.unwrap();
let row_detail = rows.get(row_index).unwrap(); let row_detail = rows.get(row_index).unwrap();
let cell = row_detail.row.cells.get(&field_id).unwrap().clone(); let cell = row_detail.row.cells.get(&field_id).unwrap().clone();
let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); let content = stringify_cell(&cell, &field);
assert_eq!(content, expected_content); assert_eq!(content, expected_content);
}, },
} }

View File

@ -155,8 +155,6 @@ async fn grid_switch_from_checkbox_to_select_option_test() {
field_id: checkbox_field.id.clone(), field_id: checkbox_field.id.clone(),
// the mock data of the checkbox with row_index one is "true" // the mock data of the checkbox with row_index one is "true"
row_index: 1, row_index: 1,
// the from_field_type represents as the current field type
from_field_type: FieldType::Checkbox,
// The content of the checkbox should transform to the corresponding option name. // The content of the checkbox should transform to the corresponding option name.
expected_content: CHECK.to_string(), expected_content: CHECK.to_string(),
}, },
@ -190,7 +188,6 @@ async fn grid_switch_from_multi_select_to_text_test() {
let script_assert_field = vec![AssertCellContent { let script_assert_field = vec![AssertCellContent {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
row_index: 0, row_index: 0,
from_field_type: FieldType::MultiSelect,
expected_content: format!( expected_content: format!(
"{},{}", "{},{}",
multi_select_type_option.first().unwrap().name, multi_select_type_option.first().unwrap().name,
@ -218,13 +215,11 @@ async fn grid_switch_from_checkbox_to_text_test() {
AssertCellContent { AssertCellContent {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
row_index: 1, row_index: 1,
from_field_type: FieldType::Checkbox,
expected_content: "Yes".to_string(), expected_content: "Yes".to_string(),
}, },
AssertCellContent { AssertCellContent {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
row_index: 2, row_index: 2,
from_field_type: FieldType::Checkbox,
expected_content: "No".to_string(), expected_content: "No".to_string(),
}, },
]; ];
@ -246,13 +241,11 @@ async fn grid_switch_from_date_to_text_test() {
AssertCellContent { AssertCellContent {
field_id: field.id.clone(), field_id: field.id.clone(),
row_index: 2, row_index: 2,
from_field_type: FieldType::DateTime,
expected_content: "2022/03/14".to_string(), expected_content: "2022/03/14".to_string(),
}, },
AssertCellContent { AssertCellContent {
field_id: field.id.clone(), field_id: field.id.clone(),
row_index: 3, row_index: 3,
from_field_type: FieldType::DateTime,
expected_content: "2022/11/17".to_string(), expected_content: "2022/11/17".to_string(),
}, },
]; ];
@ -275,13 +268,11 @@ async fn grid_switch_from_number_to_text_test() {
AssertCellContent { AssertCellContent {
field_id: field.id.clone(), field_id: field.id.clone(),
row_index: 0, row_index: 0,
from_field_type: FieldType::Number,
expected_content: "$1".to_string(), expected_content: "$1".to_string(),
}, },
AssertCellContent { AssertCellContent {
field_id: field.id.clone(), field_id: field.id.clone(),
row_index: 4, row_index: 4,
from_field_type: FieldType::Number,
expected_content: "".to_string(), expected_content: "".to_string(),
}, },
]; ];
@ -303,7 +294,6 @@ async fn grid_switch_from_checklist_to_text_test() {
AssertCellContent { AssertCellContent {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
row_index: 0, row_index: 0,
from_field_type: FieldType::Checklist,
expected_content: "First thing".to_string(), expected_content: "First thing".to_string(),
}, },
]; ];

View File

@ -48,7 +48,7 @@ async fn according_to_text_contains_filter_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id, field_id: text_field.id,
row_index: test.row_details.len() - 1, row_index: test.row_details.len() - 1,
from_field_type: FieldType::RichText,
expected_content: "sample".to_string(), expected_content: "sample".to_string(),
}, },
]; ];
@ -195,7 +195,7 @@ async fn according_to_checkbox_is_checked_filter_test() {
AssertCellContent { AssertCellContent {
field_id: checkbox_field.id, field_id: checkbox_field.id,
row_index: 3, row_index: 3,
from_field_type: FieldType::Checkbox,
expected_content: "Yes".to_string(), expected_content: "Yes".to_string(),
}, },
]; ];
@ -242,7 +242,7 @@ async fn according_to_date_time_is_filter_test() {
AssertCellContent { AssertCellContent {
field_id: datetime_field.id, field_id: datetime_field.id,
row_index: 0, row_index: 0,
from_field_type: FieldType::DateTime,
expected_content: "2024/03/15".to_string(), expected_content: "2024/03/15".to_string(),
}, },
]; ];
@ -331,7 +331,7 @@ async fn according_to_select_option_is_filter_test() {
AssertCellContent { AssertCellContent {
field_id: multi_select_field.id, field_id: multi_select_field.id,
row_index: 1, row_index: 1,
from_field_type: FieldType::MultiSelect,
expected_content: stringified_expected, expected_content: stringified_expected,
}, },
]; ];
@ -380,7 +380,7 @@ async fn according_to_select_option_contains_filter_test() {
AssertCellContent { AssertCellContent {
field_id: multi_select_field.id, field_id: multi_select_field.id,
row_index: 5, row_index: 5,
from_field_type: FieldType::MultiSelect,
expected_content: stringified_expected, expected_content: stringified_expected,
}, },
]; ];
@ -424,7 +424,7 @@ async fn according_to_select_option_is_not_empty_filter_test() {
AssertCellContent { AssertCellContent {
field_id: multi_select_field.id, field_id: multi_select_field.id,
row_index: 5, row_index: 5,
from_field_type: FieldType::MultiSelect,
expected_content: stringified_expected, expected_content: stringified_expected,
}, },
]; ];

View File

@ -35,7 +35,7 @@ async fn row_data_payload_with_empty_hashmap_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id, field_id: text_field.id,
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: "".to_string(), expected_content: "".to_string(),
}, },
]; ];
@ -70,7 +70,7 @@ async fn row_data_payload_with_unknown_field_id_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id.clone(), field_id: text_field.id.clone(),
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: "".to_string(), expected_content: "".to_string(),
}, },
AssertCellExistence { AssertCellExistence {
@ -107,7 +107,7 @@ async fn row_data_payload_with_empty_string_text_data_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id, field_id: text_field.id,
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: cell_data.to_string(), expected_content: cell_data.to_string(),
}, },
]; ];
@ -139,7 +139,7 @@ async fn row_data_payload_with_text_data_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id.clone(), field_id: text_field.id.clone(),
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: cell_data.to_string(), expected_content: cell_data.to_string(),
}, },
]; ];
@ -180,7 +180,7 @@ async fn row_data_payload_with_multi_text_data_test() {
AssertCellContent { AssertCellContent {
field_id: text_field.id, field_id: text_field.id,
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: text_cell_data.to_string(), expected_content: text_cell_data.to_string(),
}, },
AssertCellExistence { AssertCellExistence {
@ -191,7 +191,7 @@ async fn row_data_payload_with_multi_text_data_test() {
AssertCellContent { AssertCellContent {
field_id: number_field.id, field_id: number_field.id,
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: "$1,234".to_string(), expected_content: "$1,234".to_string(),
}, },
AssertCellExistence { AssertCellExistence {
@ -202,7 +202,7 @@ async fn row_data_payload_with_multi_text_data_test() {
AssertCellContent { AssertCellContent {
field_id: url_field.id, field_id: url_field.id,
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: url_cell_data.to_string(), expected_content: url_cell_data.to_string(),
}, },
]; ];
@ -234,7 +234,7 @@ async fn row_data_payload_with_date_time_test() {
AssertCellContent { AssertCellContent {
field_id: date_field.id.clone(), field_id: date_field.id.clone(),
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::RichText,
expected_content: "2024/03/15".to_string(), expected_content: "2024/03/15".to_string(),
}, },
]; ];
@ -296,7 +296,7 @@ async fn row_data_payload_with_checkbox_test() {
AssertCellContent { AssertCellContent {
field_id: checkbox_field.id.clone(), field_id: checkbox_field.id.clone(),
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::Checkbox,
expected_content: cell_data.to_string(), expected_content: cell_data.to_string(),
}, },
]; ];
@ -340,7 +340,7 @@ async fn row_data_payload_with_select_option_test() {
AssertCellContent { AssertCellContent {
field_id: multi_select_field.id.clone(), field_id: multi_select_field.id.clone(),
row_index: test.row_details.len(), row_index: test.row_details.len(),
from_field_type: FieldType::MultiSelect,
expected_content: stringified_cell_data, expected_content: stringified_cell_data,
}, },
]; ];

View File

@ -1,8 +1,8 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::time::Duration; use std::time::Duration;
use flowy_database2::entities::{CreateRowPayloadPB, FieldType, FilterDataPB, InsertFilterPB}; use flowy_database2::entities::{CreateRowPayloadPB, FilterDataPB, InsertFilterPB};
use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::cell::stringify_cell;
use flowy_database2::services::field::{SelectOptionIds, SELECTION_IDS_SEPARATOR}; use flowy_database2::services::field::{SelectOptionIds, SELECTION_IDS_SEPARATOR};
use crate::database::database_editor::DatabaseEditorTest; use crate::database::database_editor::DatabaseEditorTest;
@ -24,7 +24,6 @@ pub enum PreFillRowCellTestScript {
AssertCellContent { AssertCellContent {
field_id: String, field_id: String,
row_index: usize, row_index: usize,
from_field_type: FieldType,
expected_content: String, expected_content: String,
}, },
AssertSelectOptionCellStrict { AssertSelectOptionCellStrict {
@ -105,11 +104,9 @@ impl DatabasePreFillRowCellTest {
PreFillRowCellTestScript::AssertCellContent { PreFillRowCellTestScript::AssertCellContent {
field_id, field_id,
row_index, row_index,
from_field_type,
expected_content, expected_content,
} => { } => {
let field = self.editor.get_field(&field_id).unwrap(); let field = self.editor.get_field(&field_id).unwrap();
let field_type = FieldType::from(field.field_type);
let rows = self.editor.get_rows(&self.view_id).await.unwrap(); let rows = self.editor.get_rows(&self.view_id).await.unwrap();
let row_detail = rows.get(row_index).unwrap(); let row_detail = rows.get(row_index).unwrap();
@ -120,7 +117,7 @@ impl DatabasePreFillRowCellTest {
.get(&field_id) .get(&field_id)
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); let content = stringify_cell(&cell, &field);
assert_eq!(content, expected_content); assert_eq!(content, expected_content);
}, },
PreFillRowCellTestScript::AssertSelectOptionCellStrict { PreFillRowCellTestScript::AssertSelectOptionCellStrict {

View File

@ -1,5 +1,5 @@
use flowy_database2::entities::FieldType; use flowy_database2::entities::FieldType;
use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::cell::stringify_cell;
use flowy_database2::services::field::CHECK; use flowy_database2::services::field::CHECK;
use flowy_database2::services::share::csv::CSVFormat; use flowy_database2::services::share::csv::CSVFormat;
@ -67,7 +67,7 @@ async fn export_and_then_import_meta_csv_test() {
for (index, row_detail) in rows.iter().enumerate() { for (index, row_detail) in rows.iter().enumerate() {
if let Some(cell) = row_detail.row.cells.get(&field.id) { if let Some(cell) = row_detail.row.cells.get(&field.id) {
let field_type = FieldType::from(field.field_type); let field_type = FieldType::from(field.field_type);
let s = stringify_cell_data(cell, &field_type, &field_type, &field); let s = stringify_cell(cell, &field);
match &field_type { match &field_type {
FieldType::RichText => { FieldType::RichText => {
if index == 0 { if index == 0 {
@ -141,7 +141,7 @@ async fn history_database_import_test() {
for (index, row_detail) in rows.iter().enumerate() { for (index, row_detail) in rows.iter().enumerate() {
if let Some(cell) = row_detail.row.cells.get(&field.id) { if let Some(cell) = row_detail.row.cells.get(&field.id) {
let field_type = FieldType::from(field.field_type); let field_type = FieldType::from(field.field_type);
let s = stringify_cell_data(cell, &field_type, &field_type, &field); let s = stringify_cell(cell, &field);
match &field_type { match &field_type {
FieldType::RichText => { FieldType::RichText => {
if index == 0 { if index == 0 {

View File

@ -8,9 +8,9 @@ use futures::stream::StreamExt;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use flowy_database2::entities::{ use flowy_database2::entities::{
CreateRowPayloadPB, DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB, CreateRowPayloadPB, DeleteSortPayloadPB, ReorderSortPayloadPB, UpdateSortPayloadPB,
}; };
use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::cell::stringify_cell;
use flowy_database2::services::database_view::DatabaseViewChanged; use flowy_database2::services::database_view::DatabaseViewChanged;
use flowy_database2::services::sort::SortCondition; use flowy_database2::services::sort::SortCondition;
@ -119,10 +119,9 @@ impl DatabaseSortTest {
let mut cells = vec![]; let mut cells = vec![];
let rows = self.editor.get_rows(&self.view_id).await.unwrap(); let rows = self.editor.get_rows(&self.view_id).await.unwrap();
let field = self.editor.get_field(&field_id).unwrap(); let field = self.editor.get_field(&field_id).unwrap();
let field_type = FieldType::from(field.field_type);
for row_detail in rows { for row_detail in rows {
if let Some(cell) = row_detail.row.cells.get(&field_id) { if let Some(cell) = row_detail.row.cells.get(&field_id) {
let content = stringify_cell_data(cell, &field_type, &field_type, &field); let content = stringify_cell(cell, &field);
cells.push(content); cells.push(content);
} else { } else {
cells.push("".to_string()); cells.push("".to_string());