refactor: checkbox field type (#4682)

This commit is contained in:
Richard Shiue 2024-02-20 14:02:32 +08:00 committed by GitHub
parent a6c6aa819c
commit 334aedd6c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 121 additions and 140 deletions

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -86,7 +87,6 @@ class CheckboxCellState with _$CheckboxCellState {
}
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
bool _isSelected(CheckboxCellDataPB? cellData) {
return cellData != null && cellData.isChecked;
}

View File

@ -6,7 +6,7 @@ import 'cell_data_loader.dart';
import 'cell_data_persistence.dart';
typedef TextCellController = CellController<String, String>;
typedef CheckboxCellController = CellController<String, String>;
typedef CheckboxCellController = CellController<CheckboxCellDataPB, String>;
typedef NumberCellController = CellController<String, String>;
typedef SelectOptionCellController
= CellController<SelectOptionCellDataPB, String>;
@ -24,13 +24,13 @@ CellController makeCellController(
final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType;
switch (fieldType) {
case FieldType.Checkbox:
return TextCellController(
return CheckboxCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
parser: CheckboxCellDataParser(),
),
cellDataPersistence: TextCellDataPersistence(),
);

View File

@ -66,6 +66,16 @@ class StringCellDataParser implements CellDataParser<String> {
}
}
class CheckboxCellDataParser implements CellDataParser<CheckboxCellDataPB> {
@override
CheckboxCellDataPB? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return CheckboxCellDataPB.fromBuffer(data);
}
}
class NumberCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {

View File

@ -1,5 +1,6 @@
import {
CellPB,
CheckboxCellDataPB,
ChecklistCellDataPB,
DateCellDataPB,
FieldType,
@ -28,7 +29,7 @@ export interface NumberCell extends Cell {
export interface CheckboxCell extends Cell {
fieldType: FieldType.Checkbox;
data: 'Yes' | 'No';
data: boolean;
}
export interface UrlCell extends Cell {
@ -99,6 +100,10 @@ export type UndeterminedCell =
| UrlCell
| ChecklistCell;
const pbToCheckboxCellData = (pb: CheckboxCellDataPB): boolean => (
pb.is_checked
);
const pbToDateTimeCellData = (pb: DateCellDataPB): DateTimeCellData => ({
date: pb.date,
time: pb.time,
@ -136,8 +141,9 @@ function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) {
switch (fieldType) {
case FieldType.RichText:
case FieldType.Number:
case FieldType.Checkbox:
return new TextDecoder().decode(bytes);
case FieldType.Checkbox:
return pbToCheckboxCellData(CheckboxCellDataPB.deserialize(bytes));
case FieldType.DateTime:
return pbToDateTimeCellData(DateCellDataPB.deserialize(bytes));
case FieldType.LastEditedTime:

View File

@ -98,7 +98,7 @@ function pbToSelectTypeOption(pb: SingleSelectTypeOptionPB | MultiSelectTypeOpti
function pbToCheckboxTypeOption(pb: CheckboxTypeOptionPB): CheckboxTypeOption {
return {
isSelected: pb.is_selected,
isSelected: pb.dummy_field,
};
}

View File

@ -9,7 +9,7 @@ export const CheckboxCell: FC<{
cell: CheckboxCellType;
}> = ({ field, cell }) => {
const viewId = useViewId();
const checked = cell.data === 'Yes';
const checked = cell.data;
const handleClick = useCallback(() => {
void cellService.updateCell(viewId, cell.rowId, field.id, !checked ? 'Yes' : 'No');

View File

@ -5,7 +5,7 @@ use bytes::Bytes;
use event_integration::event_builder::EventBuilder;
use event_integration::EventIntegrationTest;
use flowy_database2::entities::{
CellChangesetPB, CellIdPB, ChecklistCellDataChangesetPB, DatabaseLayoutPB,
CellChangesetPB, CellIdPB, CheckboxCellDataPB, ChecklistCellDataChangesetPB, DatabaseLayoutPB,
DatabaseSettingChangesetPB, DatabaseViewIdPB, DateChangesetPB, FieldType, OrderObjectPositionPB,
SelectOptionCellDataPB, UpdateRowMetaChangesetPB,
};
@ -476,8 +476,8 @@ async fn update_checkbox_cell_event_test() {
assert!(error.is_none());
let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
let output = String::from_utf8(cell.data).unwrap();
assert_eq!(output, "Yes");
let output = CheckboxCellDataPB::try_from(Bytes::from(cell.data)).unwrap();
assert!(output.is_checked);
}
}

View File

@ -1,24 +1,33 @@
use crate::services::field::CheckboxTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Default, Debug, Clone, ProtoBuf)]
pub struct CheckboxCellDataPB {
#[pb(index = 1)]
pub is_checked: bool,
}
impl CheckboxCellDataPB {
pub fn new(is_checked: bool) -> Self {
Self { is_checked }
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CheckboxTypeOptionPB {
/// unused
#[pb(index = 1)]
pub is_selected: bool,
pub dummy_field: bool,
}
impl From<CheckboxTypeOption> for CheckboxTypeOptionPB {
fn from(data: CheckboxTypeOption) -> Self {
Self {
is_selected: data.is_selected,
}
fn from(_type_option: CheckboxTypeOption) -> Self {
Self { dummy_field: false }
}
}
impl From<CheckboxTypeOptionPB> for CheckboxTypeOption {
fn from(data: CheckboxTypeOptionPB) -> Self {
Self {
is_selected: data.is_selected,
}
fn from(_type_option: CheckboxTypeOptionPB) -> Self {
Self()
}
}

View File

@ -6,7 +6,7 @@ use collab_database::rows::{get_field_type_from_cell, Cell, Cells};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::entities::FieldType;
use crate::entities::{CheckboxCellDataPB, FieldType};
use crate::services::cell::{CellCache, CellProtobufBlob};
use crate::services::field::checklist_type_option::ChecklistCellChangeset;
use crate::services::field::*;
@ -203,8 +203,8 @@ pub fn insert_url_cell(url: String, field: &Field) -> Cell {
apply_cell_changeset(url, None, field, None).unwrap()
}
pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
let s = if is_check {
pub fn insert_checkbox_cell(is_checked: bool, field: &Field) -> Cell {
let s = if is_checked {
CHECK.to_string()
} else {
UNCHECK.to_string()
@ -343,8 +343,8 @@ impl<'a> CellBuilder<'a> {
}
},
FieldType::Checkbox => {
if let Ok(value) = CheckboxCellData::from_cell_str(&cell_str) {
cells.insert(field_id, insert_checkbox_cell(value.into_inner(), field));
if let Ok(value) = CheckboxCellDataPB::from_cell_str(&cell_str) {
cells.insert(field_id, insert_checkbox_cell(value.is_checked, field));
}
},
FieldType::URL => {
@ -399,13 +399,13 @@ impl<'a> CellBuilder<'a> {
}
}
pub fn insert_checkbox_cell(&mut self, field_id: &str, is_check: bool) {
pub fn insert_checkbox_cell(&mut self, field_id: &str, is_checked: bool) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the checkbox field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_checkbox_cell(is_check, field));
.insert(field_id.to_owned(), insert_checkbox_cell(is_checked, field));
},
}
}

View File

@ -1,20 +1,17 @@
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};
use crate::services::field::CheckboxCellData;
use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB};
impl CheckboxFilterPB {
pub fn is_visible(&self, cell_data: &CheckboxCellData) -> bool {
let is_check = cell_data.is_check();
pub fn is_visible(&self, cell_data: &CheckboxCellDataPB) -> bool {
match self.condition {
CheckboxFilterConditionPB::IsChecked => is_check,
CheckboxFilterConditionPB::IsUnChecked => !is_check,
CheckboxFilterConditionPB::IsChecked => cell_data.is_checked,
CheckboxFilterConditionPB::IsUnChecked => !cell_data.is_checked,
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};
use crate::services::field::CheckboxCellData;
use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB};
use std::str::FromStr;
#[test]
@ -27,8 +24,9 @@ mod tests {
("yes", true),
("false", false),
("no", false),
("", false),
] {
let data = CheckboxCellData::from_str(value).unwrap();
let data = CheckboxCellDataPB::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}
@ -43,8 +41,9 @@ mod tests {
("no", true),
("true", false),
("yes", false),
("", true),
] {
let data = CheckboxCellData::from_str(value).unwrap();
let data = CheckboxCellDataPB::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}

View File

@ -2,6 +2,7 @@
mod tests {
use collab_database::fields::Field;
use crate::entities::CheckboxCellDataPB;
use crate::entities::FieldType;
use crate::services::cell::CellDataDecoder;
use crate::services::cell::FromCellString;
@ -27,9 +28,9 @@ mod tests {
assert_checkbox(&type_option, "NO", UNCHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "0", UNCHECK, &field_type, &field_rev);
// the checkout value will be empty if the value is letters or empty string
assert_checkbox(&type_option, "abc", "", &field_type, &field_rev);
assert_checkbox(&type_option, "", "", &field_type, &field_rev);
// the checkout value will be uncheck as well if the value is letters or empty string
assert_checkbox(&type_option, "abc", UNCHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "", UNCHECK, &field_type, &field_rev);
}
fn assert_checkbox(
@ -42,7 +43,7 @@ mod tests {
assert_eq!(
type_option
.decode_cell(
&CheckboxCellData::from_cell_str(input_str).unwrap().into(),
&CheckboxCellDataPB::from_cell_str(input_str).unwrap().into(),
field_type,
field
)

View File

@ -1,30 +1,27 @@
use std::cmp::Ordering;
use std::str::FromStr;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use serde::{Deserialize, Serialize};
use flowy_error::FlowyResult;
use crate::entities::{CheckboxFilterPB, FieldType};
use crate::entities::{CheckboxCellDataPB, CheckboxFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
CheckboxCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform,
};
use crate::services::sort::SortCondition;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CheckboxTypeOption {
pub is_selected: bool,
}
pub struct CheckboxTypeOption();
impl TypeOption for CheckboxTypeOption {
type CellData = CheckboxCellData;
type CellData = CheckboxCellDataPB;
type CellChangeset = CheckboxCellChangeset;
type CellProtobufType = CheckboxCellData;
type CellProtobufType = CheckboxCellDataPB;
type CellFilter = CheckboxFilterPB;
}
@ -47,7 +44,7 @@ impl TypeOptionTransform for CheckboxTypeOption {
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
if transformed_field_type.is_text() {
Some(CheckboxCellData::from(cell))
Some(CheckboxCellDataPB::from(cell))
} else {
None
}
@ -55,17 +52,14 @@ impl TypeOptionTransform for CheckboxTypeOption {
}
impl From<TypeOptionData> for CheckboxTypeOption {
fn from(data: TypeOptionData) -> Self {
let is_selected = data.get_bool_value("is_selected").unwrap_or(false);
CheckboxTypeOption { is_selected }
fn from(_data: TypeOptionData) -> Self {
Self()
}
}
impl From<CheckboxTypeOption> for TypeOptionData {
fn from(data: CheckboxTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_bool_value("is_selected", data.is_selected)
.build()
fn from(_data: CheckboxTypeOption) -> Self {
TypeOptionDataBuilder::new().build()
}
}
@ -78,7 +72,7 @@ impl TypeOptionCellDataSerde for CheckboxTypeOption {
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(CheckboxCellData::from(cell))
Ok(CheckboxCellDataPB::from(cell))
}
}
@ -105,7 +99,7 @@ impl CellDataDecoder for CheckboxTypeOption {
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let cell_data = self.parse_cell(cell).ok()?;
if cell_data.is_check() {
if cell_data.is_checked {
Some(1.0)
} else {
Some(0.0)
@ -121,7 +115,7 @@ impl CellDataChangeset for CheckboxTypeOption {
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let checkbox_cell_data = CheckboxCellData::from_str(&changeset)?;
let checkbox_cell_data = CheckboxCellDataPB::from_str(&changeset)?;
Ok((checkbox_cell_data.clone().into(), checkbox_cell_data))
}
}
@ -147,7 +141,7 @@ impl TypeOptionCellDataCompare for CheckboxTypeOption {
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
let order = cell_data.is_check().cmp(&other_cell_data.is_check());
let order = cell_data.is_checked.cmp(&other_cell_data.is_checked);
sort_condition.evaluate_order(order)
}
@ -164,10 +158,10 @@ impl TypeOptionCellDataCompare for CheckboxTypeOption {
sort_condition: SortCondition,
) -> Ordering {
match (cell_data, other_cell_data) {
(None, Some(right_cell_data)) if right_cell_data.is_check() => {
(None, Some(right_cell_data)) if right_cell_data.is_checked => {
sort_condition.evaluate_order(Ordering::Less)
},
(Some(left_cell_data), None) if left_cell_data.is_check() => {
(Some(left_cell_data), None) if left_cell_data.is_checked => {
sort_condition.evaluate_order(Ordering::Greater)
},
_ => Ordering::Equal,

View File

@ -3,93 +3,53 @@ use std::str::FromStr;
use bytes::Bytes;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use protobuf::ProtobufError;
use flowy_error::{FlowyError, FlowyResult};
use crate::entities::FieldType;
use crate::entities::{CheckboxCellDataPB, FieldType};
use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString};
use crate::services::field::{TypeOptionCellData, CELL_DATA};
pub const CHECK: &str = "Yes";
pub const UNCHECK: &str = "No";
#[derive(Default, Debug, Clone)]
pub struct CheckboxCellData(pub String);
impl CheckboxCellData {
pub fn into_inner(self) -> bool {
self.is_check()
}
pub fn is_check(&self) -> bool {
self.0 == CHECK
}
pub fn is_uncheck(&self) -> bool {
self.0 == UNCHECK
}
}
impl TypeOptionCellData for CheckboxCellData {
impl TypeOptionCellData for CheckboxCellDataPB {
fn is_cell_empty(&self) -> bool {
false
}
}
impl AsRef<[u8]> for CheckboxCellData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl From<&Cell> for CheckboxCellData {
impl From<&Cell> for CheckboxCellDataPB {
fn from(cell: &Cell) -> Self {
let value = cell.get_str_value(CELL_DATA).unwrap_or_default();
CheckboxCellData::from_cell_str(&value).unwrap_or_default()
CheckboxCellDataPB::from_cell_str(&value).unwrap_or_default()
}
}
impl From<CheckboxCellData> for Cell {
fn from(data: CheckboxCellData) -> Self {
impl From<CheckboxCellDataPB> for Cell {
fn from(data: CheckboxCellDataPB) -> Self {
new_cell_builder(FieldType::Checkbox)
.insert_str_value(CELL_DATA, data.to_string())
.build()
}
}
impl FromStr for CheckboxCellData {
impl FromStr for CheckboxCellDataPB {
type Err = FlowyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower_case_str: &str = &s.to_lowercase();
let val = match lower_case_str {
"1" => Some(true),
"true" => Some(true),
"yes" => Some(true),
"0" => Some(false),
"false" => Some(false),
"no" => Some(false),
_ => None,
let is_checked = match lower_case_str {
"1" | "true" | "yes" => true,
"0" | "false" | "no" => false,
_ => false,
};
match val {
Some(true) => Ok(Self(CHECK.to_string())),
Some(false) => Ok(Self(UNCHECK.to_string())),
None => Ok(Self("".to_string())),
}
Ok(Self::new(is_checked))
}
}
impl std::convert::TryFrom<CheckboxCellData> for Bytes {
type Error = ProtobufError;
fn try_from(value: CheckboxCellData) -> Result<Self, Self::Error> {
Ok(Bytes::from(value.0))
}
}
impl FromCellString for CheckboxCellData {
impl FromCellString for CheckboxCellDataPB {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
@ -98,27 +58,29 @@ impl FromCellString for CheckboxCellData {
}
}
impl ToString for CheckboxCellData {
impl ToString for CheckboxCellDataPB {
fn to_string(&self) -> String {
self.0.clone()
if self.is_checked {
CHECK.to_string()
} else {
UNCHECK.to_string()
}
}
}
impl DecodedCellData for CheckboxCellData {
type Object = CheckboxCellData;
impl DecodedCellData for CheckboxCellDataPB {
type Object = CheckboxCellDataPB;
fn is_empty(&self) -> bool {
self.0.is_empty()
false
}
}
pub struct CheckboxCellDataParser();
impl CellProtobufBlobParser for CheckboxCellDataParser {
type Object = CheckboxCellData;
type Object = CheckboxCellDataPB;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => CheckboxCellData::from_cell_str(&s),
Err(_) => Ok(CheckboxCellData("".to_string())),
}
CheckboxCellDataPB::try_from(bytes.as_ref()).or_else(|_| Ok(CheckboxCellDataPB::default()))
}
}

View File

@ -190,7 +190,7 @@ mod tests {
#[test]
fn multi_select_transform_with_checkbox_type_option_test() {
let checkbox_type_option = CheckboxTypeOption { is_selected: false };
let checkbox_type_option = CheckboxTypeOption();
let mut multi_select = MultiSelectTypeOption::default();
multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.clone().into());
debug_assert_eq!(multi_select.options.len(), 2);

View File

@ -5,15 +5,15 @@ use serde::{Deserialize, Serialize};
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use crate::entities::{FieldType, SelectOptionCellDataPB};
use crate::entities::{CheckboxCellDataPB, FieldType, SelectOptionCellDataPB};
use crate::services::cell::{
CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellChangeset, ToCellChangeset,
};
use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper;
use crate::services::field::{
make_selected_options, CheckboxCellData, MultiSelectTypeOption, SelectOption,
SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, TypeOption,
TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
make_selected_options, MultiSelectTypeOption, SelectOption, SelectOptionCellData,
SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, TypeOption, TypeOptionCellDataSerde,
TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
/// Defines the shared actions used by SingleSelect or Multi-Select.
@ -104,7 +104,7 @@ where
None
},
FieldType::Checkbox => {
let cell_content = CheckboxCellData::from(cell).to_string();
let cell_content = CheckboxCellDataPB::from(cell).to_string();
let mut transformed_ids = Vec::new();
let options = self.options();
if let Some(option) = options.iter().find(|option| option.name == cell_content) {

View File

@ -47,7 +47,7 @@ impl GroupCustomize for CheckboxGroupController {
content: &str,
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
) -> bool {
if cell_data.is_check() {
if cell_data.is_checked {
content == CHECK
} else {
content == UNCHECK
@ -64,7 +64,7 @@ impl GroupCustomize for CheckboxGroupController {
let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
let is_not_contained = !group.contains_row(&row_detail.row.id);
if group.id == CHECK {
if cell_data.is_uncheck() {
if !cell_data.is_checked {
// Remove the row if the group.id is CHECK but the cell_data is UNCHECK
changeset
.deleted_rows
@ -82,7 +82,7 @@ impl GroupCustomize for CheckboxGroupController {
}
if group.id == UNCHECK {
if cell_data.is_check() {
if cell_data.is_checked {
// Remove the row if the group.id is UNCHECK but the cell_data is CHECK
changeset
.deleted_rows
@ -154,8 +154,8 @@ impl GroupController for CheckboxGroupController {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
let is_check = group.id == CHECK;
let cell = insert_checkbox_cell(is_check, field);
let is_checked = group.id == CHECK;
let cell = insert_checkbox_cell(is_checked, field);
cells.insert(field.id.clone(), cell);
},
}